一、web应用开发
Vue是一款优秀的web应用开发框架,使用它可以让我们开发web应用时候更加高效。在学习Vue之前需要先了解一下web应用开发。
什么是web应用?当我们开发web应用时候,我们在写什么?
从用户角度,web应用是一个可以提供用户交互并呈现信息的软件。
用户在使用一个web应用的时候,可以通过点击、输入、滑动、语音等等与应用进行交互,控制应用。应用通过界面、音频方式给用户呈现信息,比如电商类应用,用户可以通过搜索和选择查看需要的商品信息;视频类应用,用户可以通过搜索和点击查看想要观看的影片;游戏类应用,用户可以通过滑动、点击等操作控制界面变化和游戏运作。
所以从用户角度,web应用要提供交互能力和信息展示。
从开发者角度, web应用程序是一种利用网络浏览器和网络技术在互联网上执行任务的计算机程序。
广义的web应用包括客户端和后端程序,这里我们特指网页前端程序。
传统的前端应用程序运行在浏览器中(这里暂不讨论跨端、小程序等领域)。
前端开发者实现一个web应用需要用到创建和改变界面(dom)的能力来给用户呈现预期的界面、网络通信能力(ajax、websocket)来控制和获取用户关心的数据。
因此,前端开发者开发web应用时候,就是在获取和控制数据,然后根据用户交互将信息展示在界面上。
二、前端框架
前端面试刷题网站:灵题库,收集大厂面试真题,相关知识点详细解析。
如果使用原生技术(html、js、css)开发web应用,会非常复杂,复杂度来源于:
- 需要进行大量dom操作。
- 代码复用,如何能够让通用的代码可以被复用,而不是在每次开发类似的功能时候都需要重新实现一遍。
这样就大大提升了开发的门槛,因为能很好地处理好上面的问题是不容易的。
如何解决上面两个问题呢?
首先看第一个问题,对于任意一个web应用,它的视图都是和数据对应的,当我们查看商品详情时候,我们需要先请求到对应商品的数据,然后将它的一系列属性(缩略图、名称、描述、价格、销量等等)展示到界面上,不同的商品数据不同,界面也就不同,但是界面的结构是一样的
我们不希望每次更换商品的时候都要进行一系列的dom操作,而希望声明数据和界面的关系,例如
{{item.desc}}
{{item.price}}
{{item.sale}}
我们希望当数据变更时候,界面能够自动更新渲染。
这样,我们只需要实现界面的结构和界面与数据的关系,然后再实现数据变化的逻辑,就可以完成一个功能了。
对于第二个问题,在大部分的web应用中,都会有一些通用的界面和逻辑,例如删除的二次确认弹窗
如果在一个应用内有很多删除的场景,比如在论坛类型的应用中,我们可能需要在首页删除自己的帖子,也需要在个人中心页面删除自己的帖子,我们希望删除的提示逻辑只实现一次,可以在首页和个人中心页面都能复用。这就是组件化解决的问题。
这个弹窗的取消和确定的按钮的样式相同,删除的逻辑也相同,我们把一段可复用的界面和逻辑的集合称为一个组件。一个提示弹窗是一个组件,一个按钮是一个组件,一个列表是一个组件,想上面提到的一个商品展示框也是一个组件。
我们通过给组件传入属性控制它的界面和行为,通过回调、事件等和它进行通信。
组件化还有另一个好处,当我们把一个应用拆分成一个个的组件时候,在开发时候只需要关心一个组件的内部逻辑和与外部的关系,能够让整个应用更易于维护和阅读。
综上,我们可以通过声明式渲染视图 + 组件化解决这两个问题。
目前主流的前端框架主要做了这两方面的工作,大大解放了生产力,让开发者可以更专注与业务逻辑而不用关心复杂的dom操作的实现和可能导致的bug,让开发者可以通过组件化实现通用代码复用和合理管理代码,减少代码量提升效率和质量。
下面通过几个Q&A加深对前端框架的理解:
什么是前端框架?:前端框架是规范了开发模式的js库,规范主要包括组件化,如何声明数据和视图的关系。
当开发者使用框架开发,是在写什么?主要是在写组件:通过写组件的视图实现界面,通过写组件的声明周期钩子函数实现业务逻辑。
为什么要用框架?框架简化了工作,能够降低开发大型应用的门槛。
框架做了哪些工作?根据数据和声明视图进行dom操作,进行实际的渲染,当数据更新后更新dom;根据开发者实现的组件构建整个界面。
有框架和无框架的区别(框架的好处)?声明式渲染避免了dom操作,只需要关心视图和数据的关系;框架组件化规范了项目组织方式,让代码更容易管理,而不会让代码纷杂混乱。
三、Vue基础
这里说明Vue基本用法和概念,更详细的内容查看官方文档。
1. 简单示例
使用vue开发应用,应用是由一个个的Vue组件组成的,我们用vue写应用就是在写一个个的Vue组件,一个vue组件定义了一个视图单元和这个视图对应的业务逻辑。
使用Vue开发应用,先要定义好组件,然后调用Vue的createApp将组件渲染。
Vue会根据传入的组件进行处理,构建组件树,渲染界面,执行逻辑。
先通过一个官网的例子看下Vue是如何让一个应用运行起来的:
vue-demo
效果如下:
可以看到,Vue通过createApp方法创建一个应用,并通过mount方法挂载到指定的dom节点上。
Vue的组件组件就是一个对象,可以看到这个组件包含template(组件的视图模板)和data(组件的数据),当然组件对象还包含其它的属性,后面会陆续接触到。
在渲染组件的时候,Vue会根据data数据替换模板中的变量,用变量实际的值渲染视图,示例中模板里面的"counter"变量就被替换成了组件data中的counter变量。
一般一个应用界面会有很多元素,我们在布局时候会把应用分成一个个的盒子,每个盒子里面还可能嵌套盒子,相应地,Vue组件可能有子组件,子组件可能还有子组件,形成一个树状的结构。
对于视图,最外层盒子包含着所有其他盒子,相应地,Vue组件树也有一个根组件。
这里的Counter是这个应用的根组件(目前只有一个组件)。
当执行Vue.createApp(Counter).mount('#counter');这句代码之后,Vue会创建应用,将组件解析并把对应的视图渲染到#counter节点上。后续数据变化后还会自动更新视图。
看一个数据改变,进而更新视图的示例
vue-demo
效果如下
上面代码中实现组件Counter的生命周期钩子:mounted
一个组件在整个应用运行过程中有不同的阶段,称为生命周期,Vue在组件的生命周期过程中的不同阶段会执行我们声明的声明周期钩子。其中mounted钩子方法是在组件挂载之后(即dom渲染完成)执行的。
现在我们已经了解了根据数据渲染视图和更新数据从而更新视图的简单案例,这只是一个组件的情况,在实际项目中都会有很多组件,下面看一个有多个组件的示例
vue-demo
效果如下:
上面代码展示了如何使用一个子组件:
- 声明组件,即一个包含Vue要求的属性的对象。
- 注册组件,即在父组件的components中声明子组件的标签名,这里是'my-test'。
- 在父组件模板中使用子组件,子组件的标签名就是上一步注册的名字。
上面简单示例展示了如何渲染模板、如何更新数据从而更新视图、如何使用子组件。下面更详细地列举Vue的基本用法。
2. 视图
1. 文本
const MyComponent = {
data() {
return {
text: 'hello, vue'
};
},
template: '{{ text }}'
};
2. 属性、class、style
用v-bind指令绑定属性、class和style。
v-bind是Vue模板语法中的指令,指令用来控制模板渲染,后面会看到更多的指令。
const MyComponent = {
data() {
return {
text: 'hello, vue',
id: 'myComponent',
link: 'https://v3.cn.vuejs.org/',
className: 'my-comonent'
};
},
template: `
{{ text }}
Vue
`
};
"v-bind:"也可以简写为":"
3. 事件
v-on
事件用v-on指令绑定,事件的回调写在组件的"methods"属性中。
const MyComponent = {
data() {
return {
counter: 1
};
},
template: `
{{ counter }}
`,
methods: {
increase() {
this.counter++;
}
}
};
点击按钮时候,会调increase方法,让counter数据自增,界面上可以看到计数增加的效果。
v-on:也可以简写为"@",例如v-on:click可以简写为"@click"。
v-model
输入事件是个比较特殊的事件,很多场景我们希望输入的文本能够用来渲染界面,而我们也希望能够在代码中控制输入框内容,这就用到了双向绑定的概念,即输入框的内容绑定到我们组建的data,我们改变data也能更新input的内容。
看下面的例子:
vue-demo
这段代码实现了这个效果
在input输入框中输入的内容实时展示在上面,点击清空按钮之后,输入框和上面展示都变为空。
实现这个效果的关键就是在input输入框上面加上v-model指令,这样Vue会把input的输入内容绑定到组件的text数据上,并且组件的text数据变化也会直接触发更新input。
4. 条件渲染
v-if
v-if指令用来控制是否渲染某个节点,下面示例展示了其基本用法。当counter为偶数时候渲染内容为"偶数"的div,否则渲染内容为"奇数"的div节点。
const MyComponent = {
data() {
return {
counter: 1
};
},
template: `
{{ counter }}
偶数
奇数
`,
methods: {
increase() {
this.counter++;
}
}
};
v-show
v-show控制元素是否展示:
const MyComponent = {
data() {
return {
counter: 1
};
},
template: `
{{ counter }}
偶数
奇数
`,
methods: {
increase() {
this.counter++;
}
}
};
v-show 的元素始终会被渲染并保留在 DOM 中。v-show 只是简单地切换元素的 CSS property display。
v-if vs v-show
v-if vs v-show
行为不同:v-if指令在满足条件时候才会渲染DOM,v-show一开始就渲染DOM,满足条件时候才设置CSS的display属性让元素显示出来。
应用场景不同:一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
5. 列表渲染
通过v-for指令实现列表渲染:
const MyComponent = {
data() {
return {
digitList: [1, 2, 3, 4, 5],
userList: [
{name: 'Tom', age: 12},
{name: 'Joy', age: 11},
{name: "Ann", age: 10}
]
};
},
template: `
{{digit}}
{{user.name}}
{{user.age}}
`
};
注意:v-if和v-for不要同时使用,因为会在每次渲染时候都要遍历列表并判断是否需要渲染,这个遍历操作其实是有一部分冗余或者完全不必要的。
应该用以下方式替换v-if和v-for同时使用的方案:
- 如果是为了过滤一个列表中的项目(v-for循环,v-if过滤条件),可以将列表作为计算属性,在computed中过滤出需要渲染的列表,再进行渲染。这样避免了每次渲染都计算(只在computed依赖的属性变化时候才计算),同时渲染列表是过滤了的,那么循环的次数也可能减少。
- 如果是为了控制整个列表的展示和隐藏(v-for循环,v-if判断整个列表是否需要展示),可以将判断条件放到父元素(ul、ol)上。这样展示和隐藏的判断只需要执行一次(在列表最开始)。
详细说明参考官网:避免 v-if 和 v-for 用在一起
6. 计算属性computed
上面通过v-if渲染奇数和偶数的案例中,我们用"counter % 2 === 0"表达式判断展示奇数节点/偶数节点。实际项目中一些v-if的判断比较复杂,很难直接放在v-if指令中,这时候我们可以使用computed属性。
computed属性根据数据计算出另一个数据。每当其依赖的数据改变,computed属性也会改变,从而触发视图更新。
示例中,当counter改变,even也改变,从而视图更新,展示奇数/偶数。
const MyComponent = {
data() {
return {
counter: 1
};
},
computed: {
even() {
return this.counter % 2 === 0;
}
},
template: `
{{ counter }}
偶数
奇数
`,
methods: {
increase() {
this.counter++;
}
}
}
7. watch
如果数据变化时候我们希望做一些操作,比如弹出提示、请求等等,这时候可以使用watch属性,watch指定了数据变化时候的回调。
下面实例中,counter变化时候,如果counter为偶数,则弹出提示。
const MyComponent = {
data() {
return {
counter: 1
};
},
watch: {
counter(value) {
if (value % 2 === 0) {
alert('偶数');
}
}
},
template: `
{{ counter }}
`,
methods: {
increase() {
this.counter++;
}
}
};
watch有两个选项:deep和immediate,deep决定是否深度监听,即是否监听多层对象数据,immediate决定是否立即执行,如果为true,会在绑定时候(初始时候)立即执行,如果为false,只在监听的值变更时候执行。默认为false。
const MyComponent = {
name: 'myComponent',
data() {
return {
message: {
info: 'hello'
}
};
},
watch: {
message: {
handler: function(value) {
console.log('message change', value.info);
},
deep: true,
immediate: true
}
}
}
8. computed和watch的区别
应用场景不同
computed用在根据data属性或者其他computed计算得到一个新值的情况,computed的值一般被用在渲染中。
watch用在监听数据变化,然后做一些有副作用的操作的场景。
执行过程不同
在依赖的data属性变化后,computed并不会重新计算新的值,而是等到访问的时候再判断,如果依赖的data有改动则重新计算并返回结果,如果依赖的data没有改动,就不计算,直接返回当前结果。
依赖的数据变化后就会执行watch的回调。
3. 组件
上面介绍过,我们可以通过将应用拆分为一个个的组件来提高项目可维护性。下面介绍组件的常见用法。
1. 组件注册
组件注册有全局注册和局部注册
在之前的组件示例中,展示了引入子组件,其中注册组件是局部注册,即只有在父组件的"components"属性中声明了子组件的引用,才能使用子组件,除了局部注册,还可以对组件进行全局注册,全局注册的组件不需要父组件声明子组件即可使用。
全局注册组件:
const MyText = {
data() {
return {
text: 'hello, vue'
};
},
template: '{{text}}'
};
const Counter = {
template: ' Counter: {{ counter }} ',
data() {
return {
counter: 1
};
},
mounted() {
setInterval(() => {
this.counter++;
}, 1000);
}
}
const app = Vue.createApp(Counter);
app.component('my-text', MyText);
app.mount('#counter')
上面代码展示了全局注册组件。
全局注册组件的步骤:
- 使用Vue.createApp(<根组件>) 创建应用。
- 调用app.component('
', ChildComponet);注册组件。 - 在父组件模板中就可以正常使用子组件了。
2. 组件生命周期钩子
说明
在组件被加载后,在应用运行过程中,组件可能会经历挂载、数据更新、销毁等各个阶段,称为组件的生命周期。每个阶段会执行相应的生命周期钩子,用户可以在生命周期钩子中处理业务逻辑。
示例
我们以挂载和销毁来说明其用法。
看下面的示例
vue-demo
示例中父组件通过点击按钮控制变量"isShowChildComponent",通过变量控制是否展示子组件。
可以在控制台上看到当子组件挂载和销毁时候都会执行相应的生命周期钩子。
Vue的生命周期钩子
- 创建:beforeCreate、created
- 挂载:beforeMount、mounted
- 更新:beforeUpdate、updated
- 销毁:beforeDestroy、destroyed(Vue3中被更改为 beforeUnmount、mounted)
父子组件的生命周期钩子执行顺序
加载渲染过程
父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted。
更新过程
父beforeUpdate->子beforeUpdate->子updated->父updated。
销毁过程
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed。
生命周期钩子的使用
通常我们会在初始化(created/mounted)中绑定事件、启动定时器,相应地,在beforeDestroyed中解绑事件、停止定时器。
在updated中执行一些依赖新状态,或者依赖新的DOM的操作,例如一个聊天面板组件中,收到消息后更新数据,在update中需要判断,如果当前面板的列表是向上滚动的状态,即用户正在会看之前的消息,就给一个提示“有新消息”,如果列表处在底部,就自动向上滚动,展示出最新消息。
更多生命周期钩子参考官方文档。
3. 组件通信
通常父组件引用了子组件后,都会需要和子组件进行通信,比如父组件需要控制子组件的展示内容、父组件需要监听子组件的变化等等。下面说明父子组件通信的用法。
父子组件通信主要有3种方式:
- 父组件通过props给子组件传递属性
- 子组件通过$emit方法给父组件抛出事件
- 父组件通过ref或得子组件引用,从而调用子组件的方法
父子组件通信
- props:
先看下父组件给子组件传递属性的示例
vue-demo
上面示例中,父组件给子组件传递了两个属性,'className'和'digit',注意className是个字符串字常量,因此给子组件传递时候直接 className="my-child"即可,而digit是变量,因此要用v-bind指令来进行传递。
子组件拿到className属性后绑定到自己元素的class上面,拿到digit属性后经过computed计算奇偶,展示出来。
当父组件的counter变化时候,子组件的props属性也跟着变化,可以通过界面观察到其变化。
父组件给子组件传递属性的步骤
- 子组件声明props属性,声明需要的属性
- 父组件在模板中将属性传递给子组件
- 子组件使用属性
子组件声明属性时候,除了使用数组形式,还可以通过对象形式,指定属性的类型
const MyText = {
props: {
digit: Number,
className: String
},
template: '{{text}}',
computed: {
text() {
return this.digit % 2 === 0 ? '偶数' : '奇数';
}
}
};
上面示例子组件声明了属性digit为数值类型,className为字符串类型。
声明了props类型后,如果父组件传入的属性的类型不匹配,则会在控制台warning提示,这样可以避免一些预期之外的错误,
- 事件
除了父组件可以给子组件传递属性,子组件还可以给父组件抛出事件,这样父组件就会知道子组件的变化,进而做出一些反应。
子组件触发事件,父组件监听事件 的示例:
vue-demo
上面示例的效果是,点击按钮增加计数,当计数达到10的倍数时候,会在控制台打印提示。
父组件监听子组件的步骤是
1. 子组件通过this.$emit()触发事件,第一个参数是事件名,第二个是事件的参数
2. 父组件通过v-on:(或简写为@)绑定事件,指定回调函数,注意绑定的事件名会把驼峰转换成横杠格式
3. 在methods指定的回调中处理事件
- ref
有些场景父组件需要直接调用子组件的方法,这时候需要通过ref获取子组件引用
见下面示例
vue-demo
上面示例展示了父组件通过ref获取子组件引用并调用子组件show方法的效果
父组件有一个按钮,点击可以展示子组件,而子组件有个按钮,点击可以隐藏子组件。
实现这个效果需要3步
- 父组件在模板中子组件标签上加上ref属性,指定为一个自定义字符串:"myTextNode"
- 父组件通过this.$refs.myTextNode就获取到了子组件的示例的引用
- 子组件在methods里面实现相应的方法,这里是"show",父组件就可以调用了:this.$refs.myTextNode.show()
兄弟组件通信
- eventBus
eventBus是一个典型的发布-订阅模式,当状态改变时候,改变方通过eventBus发布状态改变事件,关心这个状态的组件可以通过订阅该事件来获知最新的状态,这样就实现组件通信,即组件间的状态共享。
Vue项目中可以通过简单地实例化一个Vue对象来实现一个eventBus:
const eventBus = new Vue();
然后使用实例提供的$on()
方法订阅,使用$emit()
方法实现发布。
另外可以通过$off()
解绑事件。
- vuex
参考vuex知识点总结vuex。
4. 父子组件挂载顺序
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created ->子 beforeMount -> 子 mounted -> 父 mounted\
更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
四、使用Vue开发
1. 单文件组件
使用单文件组件开发
从上面的说明可以知道,从开发者角度实现的Vue的组件是一个包含template、data、props、methods、watch等属性的对象。通过声明一个包含这些属性的对象的方式来声明一个Vue组件有一些缺点:
- 模板字符串可读性很差,而且没有HTML语法的高亮和提示,开发体验很差
- 组件包含了模板和js逻辑,但是并没有css样式。而我们划分组件的时候,样式也是组件的一部分,组件的样式代码和模板、js逻辑代码放到一起才更合理,更容易维护。
这两个主要的问题在项目规模较大时候会降低项目的可维护性。
所以在实际开发中,我们可以使用单文件组件来避免上面两个问题。提升项目可维护性。
单文件组件一般文件后缀名为.vue,示例如下:
demo.vue
输入的文本:{{text}}
上面是一个单文件组件的示例,文件名为demo.vue。可以看到每个单文件组件分为3个区域,template、script和style,分别位于标签、