Vue 核心基础

官网传送门

Vue 是动态构建用户界面的渐进式 JavaScript 框架

Vue 借鉴 Angular 的模板和数据绑定技术,React 的组件化和虚拟 DOM 技术

数据驱动视图

MVVM模型

  • M:模型Modeldata中的数据
  • V:视图View,模板代码
  • VM:视图模型ViewModelVue实例

观察发现

  • data中所有的属性,最后都出现在了vm身上
  • vm身上所有的属性及Vue原型身上所有的属性,在Vue模板中都可以直接使用

img

MVVM:数据驱动视图,即通过修改视图就能改变数据,反过来也可以。

本质:事件 + 方法 + 改变数据 = ViewModel

1
<p @click="changeName">{{name}}</p>  // 事件
1
2
3
4
5
6
7
8
9
10
11
data(){   
return {
name:'vue',
list: ['a','b','c']
}
} // Model
methods: {
changeName(){
this.name = "双越"
}
} // 方法

数据代理

Object.defineProperty() 的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
let person = {
name: 'Vue',
sex: 'none',
}
let number = 19

// 配置一
// 参数:对象、属性名、配置
Object.defineProperty(person, 'age', {
// 属性值
value: 21,
// 属性是否可修改
writable: true,
// 属性是否可枚举(遍历)
enumerable: true,
// 属性是否可删除
configurable: true,
})

Object.keys(person)

// 配置二
// getter、setter 不能和 value、writable 同时指定
Object.defineProperty(person, 'age', {
enumberable: true,
configurable: true,

get() {
console.log('age 属性被读取')
return number
}

set(value) {
console.log('age 属性被修改', value)
number = value
}
})

何为数据代理

数据代理:通过一个对象对另一个对象的属性进行操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let obj = { a: 21 }
let obj2 = { b: 10 }

Object.defineProperty(obj2, 'a', {
get() {
return obj.a
}
set(value) {
obj.a = value
}
})

obj2.a // 21
obj2.a = 1000
obj.a // 1000

Vue 中的数据代理

img

Vue 中通过 vm 实例对象代理对 data 对象属性的操作,让我们更方便操作 data 中的数据。

data 中的数据实际上被存在 vm._data 属性上,如果不进行代理,使用起来很不方便。

通过 Object.defineProperty() 给 vm 添加属性,并且指定 getter 和 setter,通过 getter 和 setter 访问和修改 data 对应是属性。

Vue 监测数据的原理

监测数据,即 Vue 是如何监听数据发生变化,从而重新解析模板渲染页面的。Vue 会监测 data 中所有层级的数据。

Vue 监测对象数据

  • 原理:通过 Object.defineProperty() 为属性添加 gettersetter ,对属性的读取、修改进行拦截,即数据劫持

  • 存在问题:

    • 对象新增加的属性,默认不做响应式处理
    • 对象删除属性,也不是响应式的
  • 解决办法,使用如下 API :

    • Vue.set(target, propertyName/index, value)
    • vm.$set(target, propertyName/index, value)
    • Vue.delete(target, propertyName/index)
    • vm.$delete(target, propertyName/index)
  • Vue.set()vm.$set() 不能给 vm 或 vm 的根数据对象添加属性(即 data)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 简单模拟实现对象的数据监测,Vue 更完善
// Vue 通过 vm.name 即可修改
// Vue 实现深层监听
let person = {
name: 'Vue',
age: 99
}

function Observer(obj) {
const keys = Object.keys(obj)

keys.forEach(key => {
Object.defineProperty(this, key, {
get() {
return obj[key]
}
set(value) {
console.log('数据被修改,重新解析模板...')
obj[key] = value
}
})
})
}

let vm = {}
let observer = new Observer(person)
vm._data = observer

Vue 监测数组

  • 原理:通过重写数组的 API 实现拦截:push()、pop()、shift()、unshift()、splice()、sort()、reverse()
  • 7 个 API 之所以是响应式的,是因为 Vue 对这些方法进行了包裹 ,即二次封装。做了两件事:调用对应的原生方法更新数组 & 重新解析模板更新页面
  • 存在问题:
  • 解决办法:
    • 使用 7 个 API 修改数组
    • Vue.set()vm.$set()
    • Vue.delete()vm.$delete()
  • 思否文章

插值语法

1
2
3
4
5
<p>用户名:{{ username }}</p>
<p>密码{{ password }}</p>

<!-- {{}} 支持简 JS 表达式 -->
<p>{{ flag ? 111 : 000}}</p>
1
2
3
4
5
6
7
data() {
return {
username: 'docsify',
password: 55520,
flag: true
}
}

属性绑定指令 v-bind

1
2
3
4
5
6
7
<input type="text" v-bind:placeholder="desc" />

<!-- v-bind 简写形式 -->
<img :src="url" alt="这是一张图片" />

<!-- v-bind 支持 JS 表达式 -->
<div :id="'hello' + 1"></div>
1
2
3
4
5
6
7
data() {
return {
desc: '请输入用户名',
url: 'www.baidu.com',
name: 'hello'
}
}

双向绑定指令 v-model

v-model 用于表单元素如 inputtextareaselect

v-model 基础用法

1
2
3
4
5
6
7
8
9
10
11
<p>{{ username }}</p>
<input type="text" v-model:value="username" />
<input type="text" v-model="username" />

<p>{{ province }}</p>
<select v-model="province">
<option value="">请选择</option>
<option value="1">北京</option>
<option value="2">上海</option>
<option value="3">广州</option>
</select>

v-model 指令修饰符

修饰符 作用 示例
.number 将用户输入转为数值类型 <input v-model.number="age" />
.trim 删除输入的首尾空白字符 <input v-model.trim="msg">
.lazy 当失去焦点时,才更新数据,类似防抖 <input v-model.lazy="msg">

v-model 收集表单数据

  • <input type="text"/> ,收集的是 value 值,用户输入的就是 value 值。

  • <input type="radio"/> ,收集的是 value 值,且要给标签配置 value 值。

  • <input type="checkbox"/>

    • 没有配置 value 属性,收集的就是 checked
    • 配置了 value属性:
      • v-model 的初始值是非数组,那么收集的就是 checked
      • v-model 的初始值是数组,那么收集的的就是 value 组成的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div id="root">
<form @submit.prevent="demo">
账号:<input type="text" v-model.trim="userInfo.account" />

密码:<input type="password" v-model="userInfo.password" />

年龄:<input type="text" v-model.number="userInfo.age" />

性别: 男<input type="radio" name="sex" v-model="userInfo.sex" value="male" />

<input type="radio" name="sex" v-model="userInfo.sex" value="female" />

爱好: 学习<input type="checkbox" v-model="userInfo.hobby" value="study" />

打游戏<input type="checkbox" v-model="userInfo.hobby" value="game" />

吃饭<input type="checkbox" v-model="userInfo.hobby" value="eat" />

所属校区
<select v-model="userInfo.city">
<option value="">请选择校区</option>
<option value="beijing">北京</option>
<option value="shanghai">上海</option>
<option value="shenzhen">深圳</option>
<option value="wuhan">武汉</option>
</select>

其他信息:
<textarea v-model.lazy="userInfo.other"></textarea>
<input type="checkbox" v-model="userInfo.agree" />阅读接受协议
<button>提交</button>
</form>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data() {
return {
userInfo:{
account:'',
password:'',
age:18,
sex:'female',
hobby:[],
city:'beijing',
other:'',
agree:''
}
}
},
methods: {
demo() {
console.log(JSON.stringify(this.userInfo))
}
}

事件绑定指令 v-on

v-on 基础用法

1
2
3
4
5
<p>count的值:{{ count }}</p>
<button v-on:click="add">+1</button>

<!-- v-on 缩写形式 -->
<button @click="add">+1</button>
1
2
3
4
5
6
7
8
9
10
data() {
return {
count: 1
}
},
methods: {
add() {
this.count++
}
}

事件参数对象

如果事件处理函数没有传参,则默认会传一个时间参数对象 $event ,通过它可以获取触发事件的元素,并进行相关操作。

1
2
3
4
5
6
methods: {
add(e) {
e.target.style.backgroundColor = 'red'
this.count++
}
}

如果事件处理函数传递参数了,则默认的 $event 会被覆盖,需要手动进行传递。

1
<button @click="add(2, $event)">+1</button>
1
2
3
4
5
6
methods: {
add(step, e) {
e.target.style.backgroundColor = 'red'
this.count += step
}
}

事件修饰符

事件修饰符 说明
.prevent 阻止默认行为,如 a 链接跳转、表单提交
.stop 阻止事件冒泡
.once 绑定的事件只触发 1 次
.capture 以捕获模式触发事件处理函数
.self 只有在 event.target 是当前元素自身时触发事件处理函数
.passive 事件的默认行为立即执行,无需等待事件回调执行完毕
1
2
3
4
5
6
7
<a href="www.baidu.com" @click.prevent="fn">阻止链接跳转</a>

<div @click.stop="handleClick">阻止事件冒泡</div>

<!-- .passive :如 `onwheel` 鼠标滚轮事件,是先执行事件的回调再进行滚动。 -->

<!-- 如果回调比较耗时,那么会等一段时间才发生滚动。 添加 .passive 后,则先进行滚动再执行回调。-->

按键修饰符

  1. Vue 中常用的按键别名:
  • 回车 => enter
  • 删除 => delete (捕获“删除”和“退格”键)
  • 退出 => esc
  • 空格 => space
  • 换行 => tab (特殊,必须配合 keydown 去使用)
  • 上 => up
  • 下 => down
  • 左 => left
  • 右 => right
  1. Vue 未提供别名的按键,可以使用按键原始的 key 值去绑定,但注意要转为 kebab-case(短横线命名)

  2. 系统修饰键(用法特殊):ctrl、alt、shift、meta(即 win 键)

  • 配合 keyup 使用:按下修饰键的同时,再按下其他键,随后释放其他键,事件才被触发。
  • 配合 keydown 使用:正常触发事件。
  1. 可使用 keyCode 去指定具体的按键,此法不推荐,因为 keyCode 以后可能废除

  2. Vue.config.keyCodes.自定义键名 = 键码 ,可以去定制按键别名

1
2
3
4
5
6
7
8
9
10
<input type="text" @keyup.enter="submit" />
<input type="text" @keyup.esc="back" />
<input type="text" @keydown.tab="showInfo" />
<input type="text" @keyup.caps-lock="showInfo" />

<input type="text" @keyup.huiche="showInfo" />
<input type="text" @keyup.13="showInfo" />
<script>
Vue.config.keyCodes.huiche = 13
</script>

条件渲染指令

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
<p v-if="status === 200">success</p>
<p v-else-if="status === 201">xxx</p>
<p v-else>yyy</p>

<p v-show="status === 404">error</p>

<!-- template 只能和 v-if 搭配使用 -->
<template v-if="status === 200">
<p>111</p>
<p>222</p>
<p>333</p>
</template>

v-if 和 v-show 的区别

实现原理不同:

  • v-if 通过创建或删除 DOM 元素来控制元素的显示与隐藏
  • v-show 通过添加或删除元素的 style="display: none" 样式来控制元素的显示与隐藏

性能消耗不同:

  • v-if 切换开销更高,如果运行时条件很少改变,使用 v-if 更好
  • v-show 初始渲染开销更高,如果切换频繁,使用 v-show 更好

列表渲染指令 v-for

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- 遍历数组 -->
<ul>
<li v-for="(item, index) in list" :key="item.id">{{ item.name }}</li>
</ul>

<!-- 遍历对象 -->
<ul>
<li v-for="(value, key) in obj" :key="key">{{ key }} - {{ value }}</li>
</ul>

<!-- 遍历字符串 -->
<ul>
<li v-for="(char, index) in str" :key="index">{{ index }} - {{ char }}</li>
</ul>

<!-- 遍历次数 -->
<ul>
<li v-for="(number, index) in 5" :key="index">{{ index }} - {{ number }}</li>
</ul>
1
2
3
4
5
6
7
8
9
10
11
data() {
return {
list: [...],
obj: {
name: 'Bruce',
age: 88,
sex: 'unknown'
},
str: 'hello vue'
}
}

key 的作用

key 的作用:

  • 当列表的数据变化时,默认情况下,vue 会尽可能的复用已存在的 DOM 元素,从而提升渲染的性能。但这种默认的性能优化策略,会导致有状态的列表无法被正确更新。
  • 为了给 vue 一个提示,以便它能跟踪每个节点的身份,从而在保证有状态的列表被正确更新的前提下,提升渲染的性能。此时,需要为每项提供一个唯一的 key 属性。
  • key 是虚拟 DOM 对象的标识,可提高页面更新渲染的效率。当数据变化时,Vue 会根据新数据生成新的虚拟 DOM,随后进行新旧虚拟 DOM 的差异比较

比较规则

  • 旧虚拟 DOM 找到和新虚拟 DOM 相同的 key:
    • 若内容没变,直接复用真实 DOM
    • 若内容改变,生成新的真实 DOM,替换旧的真实 DOM
  • 旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:创建新的真实 DOM,渲染到页面

key 的注意事项:

  • key 的值只能是字符串数字类型
  • key 的值必须具有唯一性(即:key 的值不能重复)
  • 建议把数据项 id 属性的值作为 key 的值(因为 id 属性的值具有唯一性)
  • 使用 index 的值当作 key 的值没有意义(因为 index 的值不具有唯一性)
  • 建议使用 v-for 指令时一定要指定 key 的值(既提升性能、又防止列表状态紊乱)

key的作用

其它内置指令

v-text

v-text 指令会覆盖元素默认值。

1
<p v-text="username">这段内容会被覆盖</p>
1
data() { return { username: "Bruce" } }

v-html

v-html 存在安全问题,容易导致 XSS 攻击

1
<p v-html="desc">原本内容被覆盖</p>
1
2
3
4
5
6
data() {
return {
desc: '<h1 style="color: red">红色标题</h1>'
str: '<a href="http://www.baidu.com?"+document.cookie>兄弟我找到你想要的资源了,快来!</a>'
}
}

v-cloak

  • 本质是一个特殊属性,Vue 实例创建完毕并接管容器后,会删除 v-cloak 属性
  • 使用 CSS 配合 v-cloak 可解决网速慢时页面展示 的问题
1
2
3
[v-cloak] {
display: none;
}
1
<h2 v-cloak>{{ username }}</h2>

v-once

  • v-once 所在节点初次渲染后就成为静态内容
  • 即数据变化不会引起 v-once 所在节点内容的更新,可用于优化性能
1
2
<h2 v-once>初次的内容:{{ content }}</h2>
<h2>最新的内容:{{ content }}</h2>

v-pre

  • 跳过所在节点的编译过程
  • 没有使用插值语法等特殊语法的节点,可用其跳过编译过程,加快编译
1
2
<h2 v-pre>Vue 内置指令</h2>
<p>用户名:{{ username }}</p>

过滤器

  • 过滤器常用于文本的格式化,可用在插值表达式和 v-bind 属性绑定。
  • 过滤器只在 vue 2.xvue 1.x 中支持,vue 3.x 废弃了过滤器,官方建议使用计算属性或方法代替过滤器。

基本使用

1
2
3
4
<!-- 在 JS 表达式尾部通过管道符进行调用-->
<p>{{ message | capitalize }}</p>

<div :id="rawId | formatId"></div>
1
2
3
4
5
6
// 定义私有过滤器
filters: {
capitalize(str) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
}
1
2
3
4
// 在 main.js 中定义全局过滤器
Vue.filter('capitalize', (str) => {
return str.charAt(0).toUpperCase() + str.slice(1)
})

如果私有过滤器和全局过滤器冲突,按照就近原则调用私有过滤器。

连续调用多个过滤器

过滤器从左往右调用,前一个过滤器的结果交给下一个过滤器继续处理。

1
<p>{{ text | capitalize | maxLength }}</p>

过滤器传参

1
<p>{{ message | myFilter(arg1, arg2) }}</p>
1
2
3
4
// 第一个参数永远都是管道符前的值
Vue.filter('myFilter', (value, arg1, arg2) => {
...
})

computed 计算属性

  • 定义:使用的属性不存在,要通过已有属性计算得到
  • 原理:底层使用了 Object.defineProperty() 提供的 getter 和 setter
  • getter 何时执行:
    • 初次读取时执行一次
    • 依赖的数据发生改变时执行
  • 优点:与 methods 相比,有缓存机制,效率更高
  • 若计算属性要修改,必须声明 setter 响应修改,且 setter 中要引起依赖的数据发生改变
1
<span>{{ fullName }}</span>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 完整写法
computed: {
fullName: {
get() {
// 计算属性依赖于已有属性得到
return this.firstName + '-' + this.lastName
}
set(value) {
// setter 中要引起依赖数据的变化
const arr = value.split('-')
this.firstName = arr[0]
this.lastName = arr[1]
}
}
}

// 简写形式
// 只有明确计算属性不需要被修改时,才能用简写形式,即没有 setter
computed: {
fullName() {
return this.firstName + this.lastName
}
}

watch 侦听器

watch 侦听器允许开发者监视数据的变化,针对数据的变化做特定的操作。

侦听器可以监听普通属性和计算属性

computed 能完成的功能,watch 也能

watch 能完成的功能,computed 不一定,如异步操作

  • Vue 管理的函数写成普通函数,使其 this 指向 vue 实例对象
  • 不被 Vue 管理的函数写成箭头函数(定时器回调、ajax 回调、Promise 回调),这样其 this 才是 vue 实例对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// watch 简写形式
export default {
data() {
return {
username: '',
}
},
watch: {
username(newVal, oldVal) {
console.log('新值: ', newVal)
console.log('旧值: ', oldVal)
},
},
}

默认情况下,组件在初次加载完毕后不会调用 watch 侦听器。如果想让 watch 侦听器立即被调用,需要使用 immediate 选项:

1
2
3
4
5
6
7
8
9
10
watch: {
// 对象形式的侦听器
username: {
// handler 属性是固定写法
handler(newVal, oldVal) {
...
},
immediate: true
}
}

watch 侦听的是一个对象,如果对象中的属性值发生了变化,则无法被监听到。此时需要使用 deep 选项进行深度监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data() {
return {
info: { username: 'admin' },
}
},
watch: {
info: {
handler(newVal) {
console.log(newVal)
},
deep: true,
},
},
}

若只想监听对象里单个属性的变化,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export default {
data() {
return {
info: { username: 'admin' },
}
},
watch: {
// 记得加引号
'info.username': {
handler(newVal) {
console.log(newVal)
},
},
},
}

通过 Vue 实例的 $watch 监听:

1
2
3
4
5
6
7
8
9
10
11
12
13
const vm = new Vue({...})

vm.$watch('isHot',{
immediate: true,
deep: true,
handler(newValue,oldValue) {
console.log(newValue, oldValue)
}
})

vm.$watch('isHot',function(newValue,oldValue) {
console.log(newValue, oldValue)
})

动态绑定 class 和 style

通过动态绑定 class 属性和行内 style 样式,可动态操作元素样式。

动态绑定 class 类名

  • 字符串写法:类名不确定,要动态获取
  • 对象写法:要绑定多个样式,个数不确定,名字也不确定
  • 数组写法:要绑定多个样式,个数确定,名字也确定,但不确定用不用
1
2
3
4
5
6
7
8
<style>
.happy {
...;
}
.sad {
...;
}
</style>
1
2
3
<div class="basic" :class="mood" @click="changeMood">字符串写法</div>
<div class="basic" :class="arr">数组写法</div>
<div class="basic" :class="classObj">对象写法</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data() {
return {
mood: 'happy',
arr: ['happy', 'sad'],
classObj: {
happy: true,
sad: false,
},
}
},
methods: {
changeMood() {
this.mood = 'sad'
},
},
}

动态绑定 style 样式

css 属性名既可以用驼峰形式,也能用短横线形式(需要使用引号括起来)。

1
2
3
4
<div :style="{color: active, fontSize: fsize + 'px', 'background-color': bgcolor}">对象写法</div>
<div :style="styleObj">对象写法</div>

<div :style="[styleObj, styleObj2]">数组写法(用得少)</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
data() {
return {
active: 'red',
fsize: 30,
bgcolor: 'yellow',
styleObj: {
color: 'active',
'font-size': '20px'
},
styleObj2: {
backgroundColor: 'yellow'
}
}
}

vue 生命周期

vue 生命周期是指一个组件从创建、运行、销毁的整个过程。每个阶段对应着不同的生命周期钩子。

生命周期钩子也可理解为:Vue 在特定的时刻调用特定的函数。

除了图中 8 个钩子,还有 nextTickactivateddeactivated

关于销毁过程:

  • 销毁后借助 Vue 开发者工具看不到任何信息。
  • 销毁后自定义事件会失效,但原生 DOM 事件依然有效。
  • 一般不在 beforeDestroy 操作数据,因为即便操作数据,也不会再触发更新流程

生命周期图2 生命周期图

Vue 组件化编程

组件:实现应用局部功能代码和资源的集合

非单文件组件

非单文件组件即所有组件写在同一个文件里。

基本使用

定义组件:

  • 使用Vue.extend(options)创建,和new Vue(options)的区别
    • el 不写,最终所有的组件都要经过 vm 的管理,由 vm 的 el 决定服务哪个容器
    • data 必须写成函数,避免组件被复用时,数据存在引用关系
    • 使用 template 节点可配置组件结构

注册组件;

  • 局部注册:components 选项
  • 全局注册:Vue.component('组件名',组件)

使用组件:<school></school>

1
2
3
4
<div id="root">
<hello></hello>
<school></school>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 创建 student 组件
const student = Vue.extend({
template: `
<div>
<h2>学生姓名:{{studentName}}</h2>
</div>
`,
data() {
return {
studentName: '张三',
}
},
})

// 创建 hello 组件
const hello = Vue.extend({
template: `
<div>
<h2>{{name}}</h2>
</div>
`,
data() {
return {
name: 'Tom',
}
},
})

// 创建 school 组件
const school = Vue.extend({
name: 'school',
template: `
<div>
<h2>学校名称:{{name}}</h2>
<student></student>
</div>
`,
data() {
return {
name: '北京大学',
}
},
// 组件嵌套
components: {
student,
},
})

// 全局注册
Vue.component('hello', hello)

new Vue({
el: '#root',
// 局部注册
components: {
school,
},
})

注意事项:

  1. 组件名
  • 一个单词:school, School
  • 多个单词:my-school, MySchool(需要 vue-cli 支持)
  1. 使用组件
  • <school></school>
  • <school /> (需要 vue-cli 支持)
  1. const school = Vue.extend(options) 可简写为 const school = options。这是脚手架里 <script> 代码的简写来源。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import HelloWorld from './components/HelloWorld.vue'

export default {
name: 'App',
components: {
HelloWorld
}
}

// 完整写法
const vc = Vue.extend({
name: 'App',
components: {
HelloWorld
}
})
export default vc

关于 VueComponent 构造函数

  1. 组件本质是一个名为 VueComponent 的构造函数,不是程序员定义的,是 Vue.extend 生成的
1
2
3
const school = Vue.extend({...})

console.dir(school) //ƒ VueComponent (options)
  1. 使用组件时,Vue 自动创建组件实例对象,即 new VueComponent(options) 是 Vue 做的

  2. 每次调用 Vue.extend,返回的都是一个全新的 VueComponent 构造函数

1
2
3
4
const school = Vue.extend({...})
const student = Vue.extend({...})

console.log(school === student) // false
  1. 组件的 this 指向 VueComponent 实例对象,而非 Vue 实例对象

  2. 重要的内置关系:VueComponent.prototype.__proto__ === Vue.prototype ,这个改动使得组件实例对象得以访问 Vue 原型上的属性方法

VueComponent

单文件组件

单文件组件即 .vue 文件

scoped 解决样式冲突

  • 原理:为当前组件所有 DOM 元素分配唯一的自定义属性,写样式时使用属性选择器防止样式冲突问题
  • scoped 只给子组件最外层的 div 添加了自定义属性 [data-v-xxx] ,子组件内部的标签并没有添加。因此父组件只能修改子组件最外层的 div 样式,修改子组件内层元素的样式是不可行的
  • 若想让某些样式对子组件生效,需使用 /deep/ 深度选择器
1
2
3
4
5
6
7
8
9
10
11
12
/* 细细品味 */
<style lang="less" scoped>
.title {
/* 不加 /deep/,选择器格式为 .title[data-v-052242de] */
color: blue;
}

/deep/ .title {
/* 加 /deep/,选择器格式为 [data-v-052242de] .title */
color: blue;
}
</style>

组件通信

自定义属性 props

父传子、子传父

props 验证:

  • 基础类型检查:String, Number, Boolean, Array, Object, Date, Function, Symbol
  • 多个可能的类型
  • 必填项检查
  • 默认值
  • 自定义验证函数 validator
  • props 是只读的,若是对象,对象内部的修改不报错,但不推荐。若需修改,则把 props 内容拷贝一份到 data 进行修改

父传子:

1
2
3
<!-- 父组件 -->
<!-- 若 props 属性使用驼峰命名法,可使用驼峰形式或短横线分隔形式 -->
<Son :num="count" :msg="message" :pub-time="time"></Son>
1
2
3
<!-- 子组件 -->
<p>父组件传过来的值:{{ num }}</p>
<p>父组件传过来的值:{{ msg }}</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 数组形式
props: ['num', 'msg', 'pubTime']

// 仅限制类型
props: {
num: Number,
msg: String
}

// 对象形式
props: {
num: {
type: Number,
default: 0
},
msg: {
type: [String, Number],
required: true,
validator(value) {
return value === 'hello' || value === 1
},
default: 1
}
}

子传父:

  • 父组件通过 props 给子组件传递函数,子组件调用该函数即可修改父组件的数据
  • 组件 methods 里函数的 this 始终指向该组件实例,可理解为 Vue 底层对这些函数做了bind处理
  • 通过bind修改 this 指向后的新函数,其 this 指向不能再次修改。官网说明
  • 思否文章
  • 不推荐该方式进行子传父,推荐使用自定义事件
1
<Son :addCount="addCount"></Son>
1
2
3
4
5
6
7
8
9
10
11
12
export default {
data() {
return {
count: 1,
}
},
methods: {
addCount() {
this.count++
},
},
}
1
2
3
4
5
6
7
8
export default {
props: ['addCount'],
methods: {
add() {
this.addCount()
},
},
}

自定义事件

自定义事件可用于实现子传父

子组件触发自定义事件,并传递数据:

1
2
3
4
5
6
7
8
9
10
11
12
// 子组件
data() {
return {
count: 1
}
},
methods: {
add() {
this.count += 1
this.$emit('count-change', this.count)
}
}

父组件监听子组件的自定义事件,并调用回调函数处理数据:

  • 父组件通过 this.$refs.xxx.$on('事件名称',回调) 监听子组件自定义事件时,回调函数要么配置在 methods 中,要么用箭头函数,否则 this 指向会出问题
  • 组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符
  • 若想让自定义事件只触发一次,可以使用 once 修饰符,或 $once 方法
1
2
3
4
5
6
7
8
9
<!-- 方式一 -->
<Son @count-change="getNewCount"></Son>
<Son @count-change.once="getNewCount"></Son>

<!-- 方式二 -->
<Son ref="sonRef"></Son>

<!-- 监听子组件原生 DOM 事件 -->
<Son @click.native="handleClick"></Son>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export default {
data() {
return {
father: 1,
}
},
methods: {
getNewCount(val) {
this.father = val
},
},
mounted() {
// 方式二
this.$refs.sonRef.$on('count-change', this.getNewCount)
this.$refs.sonRef.$once('count-change', this.getNewCount)

// 或
this.$refs.sonRef.$on('count-change', (val) => (this.father = val))
},
}

解绑自定义事件this.$off()

1
2
3
4
5
6
// 解绑单个自定义事件
this.$off('count-change')
// 解绑多个自定义事件
this.$off(['count-change', 'add'])
// 解绑所有自定义事件
this.$off()

EventBus 全局事件总线

思想:弄一个所有组件实例都能访问到的 Vue 实例对象,Vue 原型上包含事件处理的相关方法,包括 $on, $emit, $off, $once

方式一

安装全局事件总线:

1
2
3
4
5
6
7
8
// main.js
new Vue({
...
beforeCreate() {
Vue.prototype.$bus = this
}
...
})

数据接收方为自定义事件绑定回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
export default {
methods: {
handleData() {...}
},
created() {
this.$bus.$on('share', this.handleData)
},
beforeDestroy() {
// 组件销毁,解绑事件
this.$bus.$off('share')
}
}

数据发送方触发自定义事件:

1
2
3
4
5
6
7
export default {
methods: {
sendData() {
this.$bus.$emit('share', 666)
},
},
}

方式二

创建 eventBus.js 模块,并向外共享一个 Vue 的实例对象。

1
2
3
4
// eventBus.js
import Vue from 'vue'

export default new Vue()

在数据发送方,调用 bus.$emit('事件名称', 要发送的数据) 方法触发自定义事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 数据发送方
import bus from './eventBus.js'

export default {
data() {
return {
message: 'hello',
}
},
methods: {
sendData() {
bus.$emit('share', this.message)
},
},
}

在数据接收方,通过 bus.$on('事件名称', 事件处理函数) 为自定义事件注册事件处理函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 数据接收方
import bus from './eventBus.js'

export default {
data() {
return {
msg: '',
}
},
// 细节1:在 created 钩子中注册函数
created() {
// 细节2:使用箭头函数,则 this 指向该组件而非 bus
bus.$on('share', (val) => {
this.msg = val
})
},
}

消息订阅与发布

与全局事件总线很相似,因此一般用事件总线,不用这个

安装第三方库 PubSubJSnpm install -S pubsub.js

订阅消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import pubsub from 'pubsub-js'

export default {
methods: {
handleData(messageName, data) {...}
},
created() {
this.pubId = pubsub.subscribe('share', this.handleData)
// or
this.pubId = pubsub.subscribe('share', (messageName, data) => {
console.log(data)
})
},
beforeDestroy() {
// 组件销毁,取消订阅
pubsub.unsubscribe(this.pubId)
}
}

发布消息:

1
2
3
4
5
6
7
8
9
import pubsub from 'pubsub-js'

export default {
methods: {
sendData() {
pubsub.publish('share', 666)
},
},
}

ref / $refs

ref 用于给 DOM 元素或子组件注册引用信息。每个 vue 实例都有 $refs 对象,里面存储着 DOM 元素或子组件的引用。通过该方式可以获取到 DOM 元素或子组件实例。

可以父传子,也能子传父。子传父要和自定义事件搭配使用。

1
2
3
4
5
6
7
<!-- 引用 DOM 元素 -->
<p ref="pp">这是段落</p>
<button @click="getRef">获取 DOM 元素</button>

<!-- 引用子组件 -->
<son ref="sonRef"></son>
<button @click="getComponent">获取子组件实例引用</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
methods: {
getRef() {
// 获取元素的引用
console.log(this.$refs.pp)
this.$refs.pp.style.color = 'red'
},
getComponent() {
console.log(this.$refs.sonRef)
// 可以访问子组件的数据和方法
this.$refs.sonRef.count = 1
this.$refs.sonRef.add()
}
}

组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素。

1
2
3
4
5
6
7
8
9
methods: {
showInput() {
this.inputVisible = true
// 对输入框的操作推迟到 DOM 更新完成之后
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}

Vue 组件进阶

动态组件

vue 提供了内置的 <component> 组件用于动态切换组件。

1
2
3
4
5
<!-- 通过 is 属性指定要渲染的组件,传递的是字符串 -->
<component :is="comName"></component>

<button @click="comName = 'Left'">展示Left组件</button>
<button @click="comName = 'Right'">展示Right组件</button>

keep-alive

默认情况下,切换动态组件时无法保持组件的状态。此时可以使用 vue 内置的 <keep-alive> 组件保持动态组件的状态,对被包裹的组件进行状态缓存。

<keep-alive> 包裹的组件会多出两个生命周期函数:当组件被激活时,触发 activated 钩子;当组件被缓存时,触发 deactivated 钩子。

1
2
3
<keep-alive>
<component :is="comName"></component>
</keep-alive>

<keep-alive>includeexclude 属性,分别用于指明哪些组件要缓存、哪些组件不要缓存。

1
2
3
4
5
6
7
<keep-alive include="Left, Right">
<component :is="comName"></component>
</keep-alive>

<keep-alive :include="['News', 'Message']">
<router-view></router-view>
</keep-alive>

插槽

何为插槽

插槽可以理解为组件封装期间,为用户预留的内容占位符。它是 vue 为组件封装者提供的能力,允许开发者在封装组件时,把不确定的、希望由用户指定的部分定义为插槽。

插槽基本用法

基础使用:

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 子组件中预留插槽 -->
<template>
<div class="contianer">
<h1>这是子组件</h1>
<slot></slot>
</div>
</template>

<!-- 父组件使用子组件时,向插槽填充内容 -->
<child-comp>
<p>填充到插槽的内容</p>
</child-comp>

如果子组件没有预留插槽,那么父组件填充给子组件的自定义内容会被丢弃:

1
2
3
4
5
6
7
8
9
10
11
<!-- 子组件没有预留插槽 -->
<template>
<div class="contianer">
<h1>这是子组件</h1>
</div>
</template>

<!-- 父组件的自定义内容会被丢弃 -->
<child-comp>
<p>这段自定义内容会被丢弃</p>
</child-comp>

子组件可以为插槽提供后备内容,当父组件没有提供自定义内容时,后备内容就会生效。

1
2
3
4
5
6
7
8
9
10
<!-- 子组件提供后备内容 -->
<template>
<div class="contianer">
<h1>这是子组件</h1>
<slot>这是后备内容,父组件没有提供自定义内容就会生效</slot>
</div>
</template>

<!-- 父组件没有提供自定义内容 -->
<child-comp> </child-comp>

具名插槽

组件在预留插槽时可以设置 name 属性,为插槽指定名称,这种有具体名称的插槽就叫具名插槽。 没有设置 name 名称的插槽默认名称为 default

1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 子组件预留多个具名插槽 -->
<template>
<div class="contianer">
<h1>这是子组件</h1>

<slot name="title">title 具名插槽</slot>
<hr />
<slot name="content">content 具名插槽</slot>>
<hr />
<slot>没有设置 name 名称则默认为 default</slot>
<slot name="default"></slot>
</div>
</template>

父组件向具名插槽提供自定义内容

  • 新的写法:包裹一个 <template> 标签,同时在 <template> 中通过 v-slot:名称 指明插槽的名称。简写形式为 #名称 ,且 v-slot 只能使用在 <template> 和组件标签上,普通 HTML 标签不行
  • 旧的写法:slot="名称" 指明插槽名称
  • 如果不指定插槽名称,那么自定义内容会被填充到所有的 default 插槽当中
  • 同一插槽填充多个内容,是追加不是覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 父组件向具名插槽提供自定义内容 -->
<child-comp>
<h1 slot="title">《赠汪伦》</h1>

<template v-slot:title>
<h1>《静夜思》</h1>
</template>

<!-- 简写形式 -->
<template #content>
<p>床前明月光,疑是地上霜。</p>
<p>举头望明月,低头思故乡。</p>
</template>

<template>
<p>这段内容没有指定名称,会被填充到所有 default 插槽中。</p>
</template>
</child-comp>

作用域插槽

  • 组件可以为插槽绑定自定义属性 props ,这种插槽叫作用域插槽
  • 理解:数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定
1
2
3
4
<!-- 子组件为插槽绑定 props 数据 -->
<template>
<slot v-for="item in list" :user="item"></slot>
</template>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
export default {
data() {
return {
list: [
{
id: 1,
name: 'Lily',
state: true,
},
{
id: 2,
name: 'Ben',
state: false,
},
{
id: 3,
name: 'Water',
state: true,
},
],
}
},
}

父组件向插槽提供自定义内容时,可以接收作用域插槽提供的数据:

  • 旧写法:scope="scope"slot-scope="scope"
  • 新写法:v-slot:default="scope"
1
2
3
4
5
6
7
8
9
<child-comp>
<template #default="scope">
<p>作用域插槽提供的数据:{{ scope }}</p>
</template>

<template slot-scope="scope" slot="default">
<p>{{ scope }}</p>
</template>
</child-comp>

其中接收到的数据 scope 是一个对象。

1
2
3
4
5
6
7
8
// scope 的内容
{
'user': {
'id': 1,
'name': 'Lily',
'state': true
}
}

在接收作用域插槽提供的数据时可以使用解构赋值。

1
2
3
4
5
6
7
<child-comp>
<template #default="{user}">
<p>id:{{ user.id }}</p>
<p>name:{{ user.name }}</p>
<p>state:{{ user.state }}</p>
</template>
</child-comp>

自定义指令

分类

  • 私有自定义指令:在组件的 directives 节点声明
  • 全局自定义指令:在 main.js 文件中声明

完整写法

1
<input type="text" v-focus="content" />
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
data() {
return {
content: 666
}
},
directives: {
focus: {
// 指令与元素成功绑定时执行,执行一次
bind(el, binding) {
el.value = binding.value
}

// 指令所在元素插入页面时执行,执行一次
inserted(el, binding) {
// 一进入页面输入框获得焦点
el.focus()
}

// 指令所在元素重新解析(个人觉得不应是渲染,而是解析,重新解析不一定重新渲染)时执行,执行 0+N 次
update(el, binding) {
el.value = binding.value
}
}
}

// 全局写法
Vue.directive('focus', {
bind(el, binding) {
el.value = binding.value
}
inserted(el, binding) {
el.focus()
}
update(el, binding) {
el.value = binding.value
}
})

简写形式

  • bind 函数和 update 函数里的逻辑完全相同时,可以简写
  • 不需要定义 inserted 函数才使用简写形式
  • 因此简写形式的调用时机:初次绑定和 DOM 更新(指令所在模板被重新解析)
1
<h2 v-color="'red'">简写形式</h2>
1
2
3
4
5
6
7
8
9
10
directives: {
color(el, binding) {
el.style.color = binding.value
}
}

// 全局写法
Vue.directive('color', (el, binding) => {
el.style.color = binding.value
}))

注意事项

  • 自定义指令使用时需要添加 v- 前缀
  • 指令名如果是多个单词,要使用 kebab-case 短横线命名方式,不要用 camelCase 驼峰命名
  • 自定义指令三个函数里的 this 指向 window
1
<span v-big-number="n"></span>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
data() {
return {
n: 1
}
},
directives: {
// 添加引号才是对象键名完整写法
// 平时不加引号都是简写形式
// 遇到短横线的键名就必须添加引号
'big-number': {
bind(el, binding) {
console.log(this) // Window
el.innerText = binding.value * 10
}
}
}

Mixin 混入

  • Mixin 可以把多个组件共用的配置提取成一个混入对象
  • 混入和组件自身的配置会合并
  • datamethods 若冲突以自身为准
  • 对于生命周期钩子,执行动作会合并,且先执行 Mixin 里的动作

定义混入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// mixin.js
export const mixin = {
methods: {
showName() {
alert(this.name)
},
},
mounted() {
console.log('hello mixin')
},
}
export const mixin2 = {
data() {
return {
x: 100,
y: 200,
}
},
}

使用局部混入:

1
2
3
4
5
6
7
8
9
10
11
import { mixin, mixin2 } from '../mixin.js'

export default {
name: 'School',
data() {
return {
schoolName: '北大',
}
},
mixins: [mixin, mixin2],
}

使用全局混入:

1
2
3
4
5
// main.js
import { mixin, mixin2 } from './mixin.js'

Vue.mixin(mixin)
Vue.mixin(mixin2)

插件

  • 用于增强 Vue
  • 本质是包含 install 方法的一对象,install 第一个参数是 Vue 构造函数,第二个以后的参数是插件使用者传递的数据

定义插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
// plugins.js
export default {
install(Vue, ...rest) {
console.log(rest)

Vue.filter(...)
Vue.directive(...)
Vue.mixin(...)

Vue.prototype.myProperty = 'plugins'
Vue.prototype.myMethod = function() {}
}
}

使用插件:

1
2
3
4
// main.js
import plugins from './plugins.js'

Vue.use(plugins, 1, 2)

$nextTick

  • 语法:this.$nextTick(回调函数)
  • 作用:在下一次 DOM 更新结束后执行其指定的回调
  • 什么时候用:当改变数据后,要基于更新后的 DOM 进行操作时,要在 nextTick 指定的回调函数中执行
  • 组件的 $nextTick(cb) 方法,会把 cb 回调推迟到下一个 DOM 更新周期之后执行,即在 DOM 更新完成后再执行回调,从而保证 cb 回调可以获取最新的 DOM 元素
1
2
3
4
5
6
7
8
9
methods: {
showInput() {
this.inputVisible = true
// 对输入框的操作推迟到 DOM 更新完成之后
this.$nextTick(() => {
this.$refs.input.focus()
})
}
}

Vue 脚手架

官网传送门

创建 Vue 项目

  • 全局安装 vue 脚手架:npm i -g @vue/cli
  • 创建项目:vue create project-name
  • 运行项目:npm run serve
  • 暴露 webpack 配置:vue inspect > output.js

Vue 脚手架项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
├── node_modules
├── public
│ ├── favicon.ico: 页签图标
│ └── index.html: 主页面
├── src
│ ├── assets: 存放静态资源
│ │ └── logo.png
│ │── component: 存放组件
│ │ └── HelloWorld.vue
│ │── App.vue: 汇总所有组件
│ │── main.js: 入口文件
├── .gitignore: git 版本管制忽略的配置
├── babel.config.js: babel 的配置文件
├── package.json: 应用包配置文件
├── README.md: 应用描述文件
├── package-lock.json:包版本控制文件

Vue脚手架项目结构

index.html 代码分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8" />
<!-- 针对IE浏览器的一个特殊配置,含义是让IE浏览器以最高的渲染级别渲染页面 -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- 开启移动端的理想视口 -->
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<!-- <%= BASE_URL %> 表示 public 文件夹路径 -->
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<!-- 拿 package-lock.json 的 name 作为标题 -->
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<!-- 当浏览器不支持js时noscript中的元素就会被渲染 -->
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<!-- 容器 -->
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>

不同版本的 Vue 与 render 函数

  1. vue.jsvue.runtime.xxx.js 的区别:
  • import Vue from 'vue' 默认导入 vue.runtime.esm.js
  • vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器
  • vue.runtime.xxx.js 是运行版的 Vue,只包含:核心功能;没有模板解析器
  1. vue.runtime.xxx.js 没有模板解析器,故不能使用 template 配置项,需使用 render 函数接收到的 createElement 函数去指定具体内容
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import Vue from 'vue'
import App from './App.js'
Vue.config.productionTip = false

new Vue({
render: h => h(App),
}).$mount('#app')

// render()
render: function(createElement) {
// 创建元素
return createElement('h1', 'Hello Vue')
}
render: createElement => createElement(App)

vue.config.js 配置文件

  1. 使用 vue inspect > output.js 可以查看到 Vue 脚手架的默认配置。
  2. 使用 vue.config.js 可以对脚手架进行个性化定制,详情

TodoList 案例总结

  1. 组件化编码流程:
  • 拆分静态组件:组件要按照功能点拆分,命名不要与 html 元素冲突。
  • 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
    • 一个组件在用:放在组件自身即可
    • 一些组件在用:放在他们共同的父组件上(状态提升)
  • 实现交互:从绑定事件开始。
  1. props 适用于:
  • 父 ==> 子
  • 子 ==> 父(要求父先给子一个函数)
  1. v-model 绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!

  2. props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐

一些第三方包

1
2
3
4
5
npm install nanoid

import { nanoid } from nanoid

model.id = nanoid()

Vue 网络请求

Vue 脚手架配置代理

配置单个代理

vue.config.js 中添加如下配置:

1
2
3
devServer: {
proxy: 'http://localhost:5000'
}
  • 优点:配置简单,请求资源时直接发给前端(8080)即可。
  • 缺点:不能配置多个代理,不能灵活的控制请求是否走代理。
  • 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器 (优先匹配前端资源)

配置多个代理

编写 vue.config.js 配置具体代理规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
devServer: {
proxy: {
'/api1': {
// 匹配所有以 '/api1'开头的请求路径
target: 'http://localhost:5000', // 代理目标的基础路径
changeOrigin: true,
// 把路径的 /api1 替换为空串
pathRewrite: { '^/api1': '' },
},
'/api2': {
// 匹配所有以 '/api2'开头的请求路径
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: { '^/api2': '' },
},
},
},
}
/*
changeOrigin 设置为 true 时,服务器收到的请求头中的 host:localhost:5000
changeOrigin 设置为 false 时,服务器收到的请求头中的 host:localhost:8080
changeOrigin 默认为 true
*/
  • 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
  • 缺点:配置略微繁琐,请求资源时必须加前缀

vue 2.x 全局配置 axios

实际项目开发中,几乎每个组件中都会使用 axios 发起数据请求。此时会遇到如下两个问题:

  • 每个组件中都需要导入 axios(代码臃肿)
  • 每次发请求都需要填写完整的请求路径(不利于后期的维护)

main.js 文件中进行配置:

1
2
3
4
5
// 配置请求根路径
axios.defaults.baseURL = 'http://api.com'

// 把 axios 挂载到 Vue 原型上
Vue.prototype.$http = aixos

优点:每个组件可以通过 this.$http.get 直接发起请求,无需再导入 axios ;若根路径发生改变,只需修改 axios.defaults.baseURL ,有利于代码维护。

缺点:无法实现 API 的复用。即多个组件需要对同一个接口发起请求,那么每个组件都需要重复书写 this.$http.get('/users') 类似的代码,造成冗余。(视频上的说法,个人认为真正的缺点是如果存在多个根路径,这种方式无法解决,所以才会有下面的改进方式。)

改进:对于每一个根路径,独立封装一个 request.js 模块,组件导入所需根路径对应的 axios 进行使用。

1
2
3
4
5
6
7
8
import axios from 'axios'

// 创建 axios 实例
const request = axios.create({
baseURL: 'http://api.taobao.com',
})

export default request

Vuex

官网传送门

概述

何为 Vuex ?

Vuex 专门在 Vue 中实现集中式状态(数据)管理的一个 Vue 插件,对 Vue 应用中多个组件的共享状态进行集中式的管理,也是一种组件间通信的方式,适用于任意组件间通信

何时用 Vuex ?

  • 多个组件依赖于同一状态
  • 来自不同组件的行为需要变更同一状态

Vuex 工作原理图:

Vuex工作原理

官方 Vuex 项目结构示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
├── index.html
├── main.js
├── api
│ └── ... # 抽取出API请求
├── components
│ ├── App.vue
│ └── ...
└── store
├── index.js # 组装模块并导出 store 的地方
├── actions.js # 根级别的 action
├── mutations.js # 根级别的 mutation
└── modules
├── cart.js # 购物车模块
└── products.js # 产品模块

Vuex 核心概念

state

  • Vuex 管理的状态对象
  • 唯一的

actions

  • 值为一个对象,包含多个响应用户动作的回调函数
  • 通过 commit()触发 mutation 中函数的调用,间接更新 state
  • 可包含异步代码

mutations

  • 值为一个对象,包含多个直接更新 state 的方法
  • 不能写异步代码,只能单纯地操作 state

getters

  • 值为一个对象,包含多个用于返回数据的函数
  • 类似于计算属性,getters 返回的数据依赖于 state 的数据

modules

  • 一个 module 是一个 store 的配置对象,与一个组件对应

搭建 Vuex 环境

安装 Vuex:npm install vuex@3 --save

注意:Vue2 安装 Vuex3,Vue3 安装 Vuex4,版本需对应。

创建文件 src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const actions = {}
const mutations = {}
const state = {}

export default new Vuex.Store({
actions,
mutations,
state,
})

main.js 配置 store

1
2
3
4
5
6
7
import store from './store'

new Vue({
el: '#app',
store,
render: (h) => h(App),
})

基本使用

组件实例与 ActionsMutations 对话:

  • 若没有网络请求或其他业务逻辑,组件中也可以越过 actions ,即不写dispatch,直接编写commit
1
2
3
4
5
6
7
8
9
10
11
methods: {
increment() {
this.$store.commit('ADD', this.number)
},
incrementOdd() {
this.$store.dispatch('addOdd', this.number)
},
incrementAsync() {
this.$store.dispatch('addAsync', this.number)
}
}

定义 ActionsMutations

  • context 是一个迷你版的 store,可访问 dispatch, commit 方法和 state
  • mutations 的动作类型一般用大写,与 actions 区分
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

const actions = {
// context 是一个迷你版的 store
// 可访问 dispatch, commit 方法和 state
addOdd(context, value) {
if (context.state.sum % 2 !== 0) {
context.commit('ADD', value)
}
},
addAsync(context, value) {
setTimeout(() => {
context.commit('ADD', value)
})
},
}
const mutations = {
// mutations 的动作类型一般用大写,与 actions 区分
ADD(state, value) {
state.sum += value
},
}

const state = {
sum: 0,
}

export default new Vuex.Store({
actions,
mutations,
state,
})

组件访问 Vuex 的数据:

1
<p>{{ $store.state.sum }}</p>

getters 的使用

  • state 中的数据需要经过加工后再使用时,可以使用 getters 加工
  • 它不是必须的,当加工逻辑复杂且需要复用时,可以考虑使用
  • stategetters 的关系有点像 datacomputed 的关系
  • 组件读取:$store.getters.bigSum
1
2
3
4
5
6
7
8
9
10
11
12
13
...
const getters = {
bigSum(state) {
return state.sum * 10
}
}

export default new Vuex.Store({
actions,
mutations,
state,
getters
})

四个 mapXxx 方法

mapState()

  • state 状态映射为计算属性
  • 对象写法:键为自取的计算属性名,值为对应的状态(必须为字符串)
  • 数组写法:当键值同名,可直接写状态名(字符串)
  • 函数返回一个对象:{sum: f, price: f}
  • 注意对象的 ...{} 展开写法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { mapState } from 'vuex'

computed: {
// 手动写法
sum() {
return this.$store.state.sum
},
price() {
return this.$store.state.price
},

// 对象写法
...mapState({sum: 'sum', price: 'price'}),

// 数组写法
...mapState(['sum', 'price'])
}

mapGetters

  • getters 的数据映射为计算属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapGetters } from 'vuex'

computed: {
bigSum() {
return this.$store.getters.bigSum
},
double() {
return this.$store.getters.double
},

// 对象写法
...mapGetters({bigSum: 'bigSum', double: 'double'}),

// 数组写法
...mapGetters(['bigSum', 'double']),
}

mapActions

  • 生成与 actions 对话的函数,即包含 $store.dispatch()
  • mapActions 生成的函数不会传入参数,需要在调用时手动传入数据,不传参默认传入 $event
  • 数组写法要注意函数名和 actions 动作类型同名,调用时勿写错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { mapActions } from 'vuex'

methods: {
// 手动写法
incrementOdd() {
this.$store.dispatch('addOdd', this.number)
},
incrementAsync() {
this.$store.dispatch('addAsync', this.number)
},

// 对象写法
...mapActions({incrementOdd: 'addOdd', incrementAsync: 'addAsync'}),

// 数组写法
...mapActions(['addOdd', 'addAsync']),
}
1
<button @click="incrementOdd(number)">奇数+1</button>

mapMutations

  • 生成与 mutations 对话的函数,即包含 $store.commit()
  • 同样注意传递参数,以及数组形式函数名的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { mapMutations } from 'vuex'

methods: {
increment() {
this.$store.commit('ADD', this.number)
},
decrement() {
this.$store.commit('SUB', this.number)
},

// 对象写法
...mapMutations({increment: 'ADD', decrement: 'SUB'}),

// 数组写法
...mapMutations(['ADD', 'SUB']),
}

Vuex 模块化&命名空间

让代码更好维护,让多种数据分类更加明确,每一类数据及其相关操作对应一个 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/store/index.js
const countAbout = {
// 开启命名空间
namespaced: true,
state: {
sum: 0
},
actions: {...},
mutations: {...},
getters: {
bigSum(state) {
return state.sum * 10
}
}
}
const personAbout = {
// 开启命名空间
namespaced: true,
state: {
personList: []
},
actions: {...},
mutations: {...},
getters: {...}
}

export default new Vuex.Store({
modules: {
countAbout,
personAbout
}
})

开启命名空间后,组件中读取 state 数据:

1
2
3
4
5
// 直接读取
this.$store.state.personAbout.personList

// mapState 读取
...mapState('countAbout',['sum','school']),

开启命名空间后,组件中读取 getters 数据:

1
2
3
4
5
// 直接读取
this.$store.getters['countAbout/bigSum']

// mapGetters 读取:
...mapGetters('countAbout',['bigSum'])

开启命名空间后,组件中调用 dispatch

1
2
3
4
// 直接 dispatch
this.$store.dispatch('countAbout/addODdd', this.number)
// 借助 mapActions
...mapActions('countAbout', {incrementOdd:'addOdd', incrementWait:'addAsync'})

开启命名空间后,组件中调用 commit

1
2
3
4
// 直接 commit
this.$store.commit('personAbout/ADD_PERSON', person)
// 借助 mapMutations
...mapMutations('countAbout',{increment:'ADD',decrement:'SUB'}),

Vue Router

官网传送门

路由

何为路由

  • 一组路由即一组映射关系(key-value)
  • key 为路径,value 可能是 function 或 component

前端路由

前端路由即地址和组件之间的对应关系(以下已哈希模式为例)。

前端路由简易工作方式:

  1. 用户点击了页面上的路由链接
  2. URL 地址的 Hash 值发生变化
  3. 前端路由监听到 Hash 值的变化
  4. 前端路由渲染 Hash 地址对应的组件

实现简易的前端路由:

1
2
3
4
5
6
7
<!-- a 链接添加对应 Hash 值 -->
<a href="#/home">Home</a>
<a href="#/movie">Movie</a>
<a href="#/about">About</a>

<!-- 动态渲染组件 -->
<component :is="compName"></component>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default {
name: 'App',
data() {
return {
compName: 'Home'
}
},
created() {
// 监听 Hash 地址改变,切换组件
window.onhashchange = () => {
switch(location.hash) {
case: '#/home':
this.compName = 'Home'
break
case: '#/movie':
this.compName = 'Movie'
break
case: '#/about':
this.compName = 'About'
break
}
}
}
}

后端路由

  • 后端路由是指请求方式、请求地址与 function 处理函数之间的对应关系
  • 服务器收到一个请求,根据请求方式、路径匹配对应的函数处理请求,返回响应数据
1
2
3
4
5
6
const express = require('express')
const router = express.Router()

router.get('/userlist', function(req, res) {...})

module.exports = router

单页面应用程序 SPA

单页面应用程序将所有的功能局限于一个 web 页面中,仅在该 web 页面初始化时加载相应的资源( HTML、JavaScript 和 CSS)。 一旦页面加载完成了,SPA 不会因为用户的操作而进行页面的重新加载或跳转。而是利用 JavaScript 动态地变换 HTML 的内容,从而实现页面与用户的交互。

SPA 的优点:

  • 良好的交互体验
    • 内容的改变不需要重新加载整个页面
    • 数据通过 Ajax 异步获取
    • 没有页面跳转,不会出现白屏现象
  • 良好的前后盾工作分离模式
    • 后端专注于提供 API 接口,更易实现接口复用
    • 前端专注页面渲染,更利于前端工程化发展
  • 减轻服务器压力
    • 服务器只提供数据,不负责页面的合成与逻辑处理,吞吐能力会提高

SPA 的缺点:

  • 首屏加载慢:可使用路由懒加载、代码压缩、CDN 加速、网络传输压缩
  • 不利于 SEO :SSR 服务器端渲染

vue-router 初体验

安装 vue-router

1
npm install vue-router@3.5.2 -S

创建路由模块,在 src 源代码目录下,新建 router/index.js 路由模块,初始化代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '@/component/Home.vue'
import About from '@/component/About.vue'
import Movie from '@/component/Movie.vue'

// 把 VueRouter 安装为 Vue 的插件
Vue.use(VueRouter)

// 路由匹配规则
const routes = [
{ path: '/home', component: Home },
{ path: '/about', component: About },
{ path: '/movie', component: Movie },
]

// 创建路由实例对象
const router = new VueRouter({
routes,
})

export default router

main.js 文件中导入并挂载路由模块:

1
2
3
4
5
6
7
8
import Vue from 'vue'
import App from './App.vue'
import router from './router/index.js'

new Vue({
router,
render: (h) => h(App),
}).$mount('#app')

在组件中声明路由链接和占位符:

1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="app-container">
<!-- 路由链接 -->
<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link to="/movie">电影</router-link>

<!-- 路由出口 -->
<router-view></router-view>
</div>
</template>

注意事项:

  • 组件分为路由组件和一般组件,前者放在 pages(或views) 文件夹,后者放在 components 文件夹
  • 每个组件都有 $route 属性,存储着组件的路由规则信息
  • $router 是路由器对象,整个 SPA 只有一个

声明式导航

<router-link> 4 个常用属性:

  1. to 属性
  • 用于指定跳转路由
1
<router-link to="/about"></router-link>
  1. tag 属性
  • 指明 <router-link> 最终被渲染为何种标签,默认是 a 标签
  • 渲染为其他标签也会监听点击,触发导航
1
2
3
<router-link to="/about" tag="li">tag</router-link>

<li>tag</li>
  1. replace 属性
  • 路由跳转不会增加新的历史记录,而是替换当前历史记录
1
<router-link to="/about" replace>About</router-link>
  1. active-class 属性
  • 指明路由被激活时添加的类名,默认为 router-link-active
  • 详见路由高亮
1
<router-link to="/about" active-class="active">About</router-link>

路由高亮

被激活的路由链接,默认会添加 router-link-active 的类名。可据此为激活的路由链接设置高亮的样式:

1
2
3
4
.router-link-active {
color: white;
background-color: pink;
}

定义路由模块时可以自定义路由链接被激活时添加的类名:

1
2
3
4
5
const router = new VueRouter({
// 默认的 router-link-active 会被覆盖
linkActiveClass: 'active-hello',
routes,
})

声明路由链接时也可用 active-class 属性自定义激活类名:

1
2
<!-- router-link-active 会被覆盖为 active -->
<router-link active-class="active" to="/about">About</router-link>

路由重定向

1
2
3
4
5
6
7
8
9
10
11
12
13
const routes = [
// 访问 / 跳转到 /home
{ path: '/', redirect: '/home' },
{ path: '/home', component: 'Home' },
{ path: '/about', component: 'About' },
{ path: '/movie', component: 'Movie' },
]

const router = new VueRouter({
routes,
})

export default router

嵌套路由

About 组件中声明子路由链接和子路由占位符:

1
2
3
4
5
6
7
8
9
<template>
<div class="about-container">
<!-- 要把父路由寫上 -->
<router-link to="/about/tab1">tab1</router-link>
<router-link to="/about/tab2">tab2</router-link>

<router-view></router-view>
</div>
</template>

通过 children 属性声明子路由规则:

1
2
3
4
5
6
7
8
9
10
11
const routes = [
{
path: '/about',
component: 'About',
children: [
// 注意不要写成 /tab1
{ path: 'tab1', component: Tab1 },
{ path: 'tab2', component: Tab2 },
],
},
]

编程式导航

声明式导航:

  • 通过点击链接实现导航
  • 如普通网页点击 a 链接,vue 点击 <router-link>

编程式导航:

  • 通过调用 API 实现导航
  • 普通网页通过 location.href 的方式跳转页面也是编程式导航

vue-router 中实现编程式导航的 API :

  • this.$router.push('hash地址') :跳转到指定页面,并增加一条历史记录
  • this.$router.replace('hash地址') :跳转页面,但不会新增历史记录,而是替换当前的历史记录
  • this.$router.go(数值) :历史记录前进或后退,相当于点击浏览器前进后退箭头
  • this.$router.forward() :前进一步
  • this.$router.back() :后退一步

命名路由

给路由命名,某些情况可简化路由跳转写法

1
2
3
4
5
6
7
const routes = [
{
name: 'about',
path: '/about',
component: About,
},
]
1
2
3
<router-link :to="{ name: 'about'} "></router-link>

<router-link :to="{ name: 'about', query: { id: 1, title: 'hello' }}"></router-link>

路由传参

query 参数

传递参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 字符串写法 -->
<router-link :to="`/home/detail?id=${id}&title=${title}`">字符串写法</router-link>
<!-- 对象写法 -->
<router-link
:to="{
path: '/home/detail',
query: {
id: 1,
title: 'hello',
}
}"
>
对象写法
</router-link>
1
2
this.$router.push(`/home/detail?id=${id}&title=${title}`)
this.$router.push({ path: '/home/detail', query: { id: 1, title: 'query' } })

接收参数:

1
2
this.$route.query.id
this.$route.query.title

params 参数(动态路由)

动态路由是把 Hash 地址中可变的部分定义为参数项,从而提高路由规则的复用性。

声明 params 参数:

1
2
3
4
5
6
7
// 定义动态路由参数
{ path: '/movie/:id/:age', component: Movie }

// 以下类似的路由规则都能合并为上述规则,复用性得到提高
{ path: '/movie/1/21', component: Movie }
{ path: '/movie/2/22', component: Movie }
{ path: '/movie/3/23', component: Movie }

传递 params 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<router-link :to="/movie/1/21">字符串写法</router-link>
<!-- 对象写法只能和 name 搭配使用,不能和 path 搭配 -->
<router-link
:to="{
name: 'movie',
params: {
id: 1,
age: 21
}
}"
>
对象写法
</router-link>

<!-- query 和 params 可以一起用 -->
<router-link :to="`/movie/1/21?id=${id}`">字符串写法</router-link>
<router-link
:to="{
name:'movie',
params: {id:1, age:21},
query: {school: 'love'}
}"
>
对象写法
</router-link>
1
2
this.$router.push(`/movie/1/21?id=${id}`)
this.$router.push({ name: 'movie', params: { id: 1, age: 21 }, query: { school: 'love' } })

接收 params 参数:

1
2
3
4
5
<template>
<div class="movie-container">
<h1>Movie组件,参数值:{{ this.$route.params.id }}</h1>
</div>
</template>

路由的 props 配置

简化路由组件接收参数。

在路由规则中开启 props 传参,组件可以使用 props 接收路由参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
path: '/movie/:id/:title',
component: Movie,

// 方式一:该对象中的所有 key-value 都会以 props 的形式传给组件
// 该方式是写死的,少用
props: {id: 666, title: 'hello'}

// 方式二:把 params 参数以 props 的形式传给组件
// 只能接收 params 参数
props: true

// 方式三:函数写法,啥都能传
props($route) {
return {
id: $route.query.id,
title: $route.params.title,
age: 21
}
}
}

组件接收参数:

1
2
3
4
export default {
// 使用 props 接收路由规则的参数项
props: ['id', 'title'],
}
1
2
3
<template>
<h1>Movie组件,参数值:{{ id }},题目:{{ title }}</h1>
</template>

路由传参注意事项

  1. path 不能和 params 一起使用。path+query、name+query/params 都行
  2. 如何指定 params 参数可传可不传?
  • 若声明了 params 参数 path: '/movie/:title',默认是必须要传递 params 参数的,否则 URL 会出现问题
  • 指定 params 参数可不传:path: '/movie/:title?'
  1. 已指明 params 参数可传可不传,但如果传递空串,如何解决?
  • 传递空串,URL 也会出现问题
  • 方法:使用 undefined
1
2
this.$router.push({ name: 'search', params: { keyword: '' }, query: { key: this.key } })
this.$router.push({ name: 'search', params: { keyword: '' || undefined }, query: { key: this.key } })

路由元信息 meta

meta 中可以为路由添加自定义信息:

1
2
3
4
5
6
7
8
const routes = [
{
name: 'about',
path: '/about',
component: About,
meta: { title: 'hello', isAuth: true },
},
]

路由守卫

作用:对路由进行权限控制。

分类:全局守卫、独享守卫、组件内守卫

全局守卫

  • 全局前置守卫:beforeEach()
  • 全局后置守卫:afterEach()

守卫回调函数 3 个形参:

  • to :将要访问的路由的信息对象,即 $route
  • from :将要离开的路由的信息对象
  • next :放行函数(后置守卫没有)

next 函数 3 种调用方式:

  • 直接放行:next()
  • 强制跳转到其他路由:next(/login)
  • 阻止本次跳转:next(false)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const router = new VueRouter({...})

router.beforeEach((to, from, next) => {
if(to.path === '/main') {
const token = localStorage.getItem('token')

if(token) {
next()
} else {
next('/login')
}
} else {
next()
}
})

router.afterEach((to,from) => {
if(to.meta.title) {
// 修改网页标题
document.title = to.meta.title
} else {
document.title = 'vue_test'
}
})

独享路由守卫

  • 某一条路由规则独享的守卫
  • 独享守卫只一个
1
2
3
4
5
6
7
{
path: 'about',
component: About,
beforeEnter(to, from ,next) {
...
}
}

组件内路由守卫

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
name: 'About',

// 进入守卫:通过路由规则,进入该组件时被调用
beforeRouteEnter(to, from, next) {
...
}

// 离开守卫:通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
...
}
}

各个守卫执行顺序

About 组件通过路由规则进入 Home 组件:

1
2
3
4
5
6
About-beforeRouteLeave
beforeEach
Home-beforeEnter
Home-beforeRouteEnter
afterEach
Home组件生命周期开始

vue-router 4.x

目前 vue-router3.x4.x 两个版本,前者只能在 vue2.x 中使用,后者只能在 vue3.x 中使用。

下面是 vue-router 4.x 初体验:

安装 vue-router 4.x

1
npm install vue-router@next -S

创建路由模块:

1
2
3
4
5
6
7
8
9
10
11
// 从 vue-router 按需导入两个方法
// createRouter:创建路由实例对象
// createWebHashHistory:指定路由工作模式为 hash 模式
import { createRouter, createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes: [{ path: '/home', component: Home }],
})

export default router

main.js 导入并挂载路由模块:

1
2
3
4
5
6
7
8
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

const app = createApp(App)

app.use(router)
app.mount('#app')

Vue3

官网传送门Vue3 的变化

性能的提升

  • 打包大小减少 41%
  • 初次渲染快 55%, 更新渲染快 133%
  • 内存减少 54%

源码的升级

  • 使用 Proxy 代替 defineProperty 实现响应式
  • 重写虚拟 DOM 的实现和 Tree-Shaking

拥抱 TypeScript

  • Vue3 可以更好的支持 TypeScript

新的特性

  1. Composition API(组合 API)
  • setup 配置
  • ref 与 reactive
  • watch 与 watchEffect
  • provide 与 inject
  • ……
  1. 新的内置组件
  • Fragment
  • Teleport
  • Suspense
  1. 其他改变
  • 新的生命周期钩子
  • data 选项应始终被声明为一个函数
  • 移除 keyCode 支持作为 v-on 的修饰符
  • ……

创建 Vue3 工程

vite 和 vue-cli 对比

vite vue-cli
支持的 vue 版本 仅支持 vue3.x 支持 2.x 和 3.x
是否基于 webpack
运行速度 较慢
功能完整度 小而巧 大而全
是否建议企业级开发使用 暂不建议 建议

使用 vue-cli 创建

1
2
3
4
5
6
7
8
9
### 查看 @vue/cli 版本,确保 @vue/cli 版本在4.5.0以上
vue --version
### 安装或者升级 @vue/cli
npm install -g @vue/cli
### 创建
vue create vue_test
### 启动
cd vue_test
npm run serve

使用 vite 创建

  • vite :新一代前端构建工具
  • 优势:
    • 开发环境中,无需打包操作,可快速冷启动(webpack 每次运行项目都要打包)
    • 轻量快速的热重载 HMR(更改代码局部刷新,webpack 也行,但 vite 更轻量)
    • 真正的按需编译,无需等待整个应用编译完成
  • 传统构建 与 vite 构建对比(vite 现用现分析,按需导入,因此项目启动更快)

Bundle

ESM

1
2
3
4
5
npm init vite-app 项目名称

cd 项目名称
npm install
npm run dev

Vue3 项目结构

Vue3 中 main.js 代码有所改变:

1
2
3
4
5
6
7
8
// 不再引入 Vue 构造函数,而是引入 createApp 工厂函数
// createApp函数:创建 vue 的 SPA 实例
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象
const app = createApp(App)
app.mount('#app')

Vue3 支持定义多个根节点,组件的 <template> 支持定义多个根节点:

1
2
3
4
<template>
<h1>根节点</h1>
<h1>根节点</h1>
</template>

常用 Composition API

setup

  • setup 是 Vue3 中一个新的配置项,值为函数
  • 组件中使用的数据、方法等都要配置在 setup 中
  • setup 函数两种返回值:
    • 返回一个对象,对象中的属性、方法可在模板中直接使用
    • 返回一个渲染函数,可自定义渲染内容
  • setup 函数的参数:
    • props:值为对象,包含了组件外部传进来,且组件内部声明接收的属性
    • context:上下文对象
      • attrs:值为对象,包含了组件外部传进来,且组件内部没有声明接收的属性,相当于 this.$attrs
      • slots:收到的插槽内容,相当于 this.$slots
      • emit:触发自定义事件的函数,相当于 this.$emit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 没错,渲染函数就叫 h
import { h } from 'vue'

export default {
name: 'App',
props: ['title'],
// Vue3 需要声明自定义事件,虽然不声明也能运行
emits: ['changeCount'],

// 返回函数
/*
setup() {
return () => h('h1', 'Hello')
},
*/

// 返回对象
setup(props, context) {
let name = 'Vue3'
function sayHello() {}
function test() {
context.emit('changeCount', 888)
}

return {
name,
sayHello,
test,
}
},
}

注意:

  • setup 在 beforeCreate 钩子之前执行,thisundefined
  • setup 不要和 Vue2 配置混用。Vue2 的配置可以访问到 setup 的属性方法,反过来不行;如有重名,setup 优先
  • setup 不能是 async 函数,因为 async 函数返回的是 promise 不是对象,会导致模板无法访问属性方法
  • 若要返回 promise 实例,需要 Suspense 和异步组件的配合

ref 函数

作用:定义响应式数据

语法:const name = ref(initValue)

  • ref 函数返回一个 RefImpl(reference implement) 实例对象,全称引用实现的实例对象
  • 它包含响应式数据,简称引用对象、reference 对象、ref 对象
  • JS 访问数据:name.value
  • 模板访问数据:<div></div>

注意事项:

  • ref 函数可以接收基本数据类型和引用数据类型
  • 基本类型数据的响应式还是靠 Object.defineProperty() 完成
  • 对象类型数据使用 ES6 的 Proxy 实现响应式,Vue3 把相关操作封装在 reactive 函数中
  • 按照之前的办法,对于对象数据,应该遍历每一层的属性添加 gettersetter,但 Vue3 使用 Proxy 把内部数据一口气监测了
1
2
<h2>{{ name }}</h2>
<p>{{ jobInfo.type }}</p>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { ref } from 'vue'

export default {
setup() {
let name = ref('Vue3')
let jobInfo = ref({
type: 'frontend',
salary: '40w',
})

function changeInfo() {
name.value = '鱿鱼丝'
// jobInfo 是 RefImpl 实例
// jobInfo.value 是 Proxy 实例对象
jobInfo.value.salary = '50w'
}

return {
name,
jobInfo,
changeInfo,
}
},
}

reactive 函数

  • 定义引用类型的响应式数据,不可用于 jibenleixingshuju
  • const 代理对象 = reactive(源对象) 接收对象或数组,返回代理对象(Proxy 的实例对象)
  • reactive 的响应式是深度的
  • 基于 ES6 的 Proxy 实现,通过代理对象操作源对象内部数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { reactive } from 'vue'

export default {
setup() {
let person = reactive({
name: 'Vue3',
sex: 'unknown',
info: {
school: 'Oxford',
major: 'computer',
},
})

let color = reactive(['red', 'green', 'blue'])

function changeInfo() {
person.info.major = 'art'
color[0] = 'yellow'
}

return {
person,
color,
changeInfo,
}
},
}

ref VS reactive

定义数据:

  • ref 用于定义基本类型数据
  • reactive 用于定义对象或数组类型数据
  • ref 也可定义对象或数组类型数据,内部通过 reactive 转为代理对象
  • 一般使用 reactive 函数,可以把所有数据封装为一个对象

原理:

  • ref 通过 Object.defineProperty() 实现响应式
  • reactive 通过 Proxy 实现响应式,Reflect 操作源对象数据

使用角度:

  • ref 定义数据,访问数据需要 .value,模板中不需要
  • reactive 定义的数据,都不需要

Vue3 响应式原理

  • 通过 Proxy 代理,拦截对对象属性的操作,包括增删改查
  • 通过 Reflect 反射,对源对象的属性进行操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let originPerson = {
name: 'Lily',
age: 22,
}

let person = new Proxy(originPerson, {
// 拦截增加和查询操作
get(target, prop) {
// 读取源对象的属性
return Reflect.get(originPerson, prop)
},
// 拦截修改操作
set(target, prop, value) {
// 修改源对象的属性
return Reflect.set(target, prop, value)
},
// 拦截删除操作
deleteProperty(target, prop) {
// 删除源对象的属性
return Reflect.deleteProperty(target, prop)
},
})

console.log(person.name)
person.age = 33
person.sex = 'unknown'
delete person.age

computed 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { reactive, computed } from 'vue'

export default {
setup() {
let person = reactive({
firstName: 'Cai',
lastName: 'QP',
})

// 计算属性简写形式
person.fullName = computed(() => {
return person.firstName + '-' + person.lastName
})

// 计算属性完整形式
person.fullName = computed({
get() {
return person.firstName + '-' + person.lastName
},
set(value) {
const arr = value.split('-')
person.firstName = arr[0]
person.lastName = arr[1]
},
})

return {
person,
}
},
}

watch 函数

Vue3 watch 能侦听的东西:

A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, reactive, watch } from 'vue'
...

// 数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

侦听 ref 定义的响应式数据

  • 注意不要写成 sum.value
1
2
3
4
5
6
7
8
// 参数:侦听的数据,回调,其他配置
watch(
sum,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ immediate: true }
)

侦听多个 ref 定义的响应式数据

1
2
3
4
// newVal,oldVal 也是数组
watch([sum, msg], (newVal, oldVal) => {
console.log(newVal, oldVal)
})

侦听 ref 定义的对象类型数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用 ref 定义对象类型数据
let person = ref({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

// 开启深度监听才有效,此时监听的是 RefImpl 实例
// Ref 实例的 value 是 Proxy 对象,存的是地址
// 因此无法监听 person 内部属性的变化
watch(person, (newVal, oldVal) => { ... }, { deep:true })

// 这个和 “侦听 reactive 函数直接返回的那一整坨响应式数据” 效果一致
watch(person.value, (newVal, oldVal) => {...})

侦听 reactive 函数直接返回的那一整坨响应式数据

  • oldVal 是错误的!和 newVal 的值一样
  • 强制开启了深度侦听,deep 配置不生效!
1
2
3
4
5
6
7
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ immediate: true, deep: false }
)

侦听 reactive 定义的响应式数据某个属性

  • 如果是 () => person.info oldVal 也是错误的!
  • () => person.name oldVal 是正确的,何时对何时错自己琢磨吧!
  • 此处没有强制开启深度监听
1
2
3
4
5
6
7
8
// 如果监视的属性还是对象,则需要开启深度监听
watch(
() => person.info,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ deep: true }
)

侦听 reactive 定义的响应式数据多个属性

1
2
3
4
5
6
7
watch(
[() => person.name, () => person.info],
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ deep: true }
)

watchEffect 函数

  • watchEffect 不需要指明监听哪个属性,回调里用到哪个属性,就自动监听哪个属性
  • computed 注重计算的值,即回调函数的返回值,因此必须有返回值
  • watchEffect 更注重过程,即回调函数的函数体,因此可没有返回值
  • watchEffect 没有开启深度监听,也不能开启深度监听!
  • watchEffect 内部自行修改数据,不会重新调用回调,因此不会出现递归调用
1
2
3
4
5
6
// 回调中用到的数据只要发生变化,则直接重新执行回调
watchEffect(() => {
let total = sum.value
let p = person
console.log('watchEffect...')
})

生命周期

注意和 vue2.x 的生命周期图作对比,beforeDestroydestroyed 变为 beforeUnmountunmounted

vu3-lifecycle

Vue3 也提供了 Composition API 形式的生命周期钩子,与 Vue2 中钩子对应关系如下:

  • beforeCreate===>setup()
  • created=======>setup()
  • beforeMount ===>onBeforeMount
  • mounted=======>onMounted
  • beforeUpdate===>onBeforeUpdate
  • updated =======>onUpdated
  • beforeUnmount ==>onBeforeUnmount
  • unmounted =====>onUnmounted

若和配置项生命钩子一起使用,则组合式会比配置项的先执行,如 onBeforeMount 先于 beforeMount

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

setup(){
console.log('---setup---')
let sum = ref(0)

//通过组合式API的形式去使用生命周期钩子
onBeforeMount(()=>{
console.log('---onBeforeMount---')
})
onMounted(()=>{
console.log('---onMounted---')
})
onBeforeUpdate(()=>{
console.log('---onBeforeUpdate---')
})
onUpdated(()=>{
console.log('---onUpdated---')
})
onBeforeUnmount(()=>{
console.log('---onBeforeUnmount---')
})
onUnmounted(()=>{
console.log('---onUnmounted---')
})

return {sum}
},

hook 函数

  • hook 是一个函数,把 setup 函数的 Composition API 进行了封装
  • 类似 Vue2 的 Mixin,能复用代码,让 setup 里的逻辑更清晰
  • hook 放在 hooks 文件夹中,一个文件对应一个功能模块,以 useXxx 命名
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// hooks/usePoint.js

import { reactive, onMounted, onBeforeUnmount } from 'vue'

export default function () {
//实现鼠标“打点”相关的数据
let point = reactive({
x: 0,
y: 0,
})

//实现鼠标“打点”相关的方法
function savePoint(event) {
point.x = event.pageX
point.y = event.pageY
}

//实现鼠标“打点”相关的生命周期钩子
onMounted(() => {
window.addEventListener('click', savePoint)
})

onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})

return point
}
1
2
3
4
5
6
7
8
9
10
// 使用 hook
import usePoint from '../hooks/usePoint.js'

export default {
setup() {
let point = usePoint()

return { point }
},
}

toRef 函数

  • 创建一个 RefImpl 实例对象,其 value 值指向另一个对象的某个属性,修改 value 值会修改源对象对应的属性
  • 应用:需要把响应式对象的某个属性单独提供给外部使用
  • 批量创建:toRefs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {reactive, toRef, toRefs} from 'vue'
...

setup() {
let person = reactive({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

return {
// 注意不能写成 ref(person.name),这和源对象是割裂开的
name: toRef(person, 'name'),
salary: toRef(person.info.job, 'salary')
// or
...toRefs(person)
}
}

其它 Composition API

shallowReactive & shallowRef

  • shallowReactive:只处理对象最外层属性的响应式,即浅响应式
  • shallowRef:基本数据类型和 ref 相同,对象数据不再会调用 reactive,因此只有对象引用改变了才是响应式的
  • 若一个对象数据,结构很深,但只有最外层属性变化,可用 shallowReactive
  • 若一个对象数据,属性不会改变,而是使用新对象替换,可用 shallowRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { shallowReactive, shallowRef } from 'vue'

setup() {
let person = shallowReactive({
name: 'Vue3',
age: 21,
info: {
job: {
salary: 22
}
}
})
let x = shallowRef({
y: 0
})
return {
person,
x
}
}

readonly & shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)
  • 应用场景: 不希望数据被修改时,如你用了别人的响应式数据,但是别人不希望你修改时
1
2
3
4
5
6
7
8
9
10
11
12
setup() {
let sum = ref(0)
let person = reactive({...})

sum = readonly(sum)
person = shallowReadonly(person)

return {
sum,
person
}
}

toRaw & markRaw

toRaw

  • 将一个由 reactive 生成的响应式对象转为普通对象
  • 用于读取响应式对象对应的普通对象,对该普通对象的操作不会引起页面更新

markRaw

  • 标记一个对象,让其不成为响应式对象
  • 有些值不应设置为响应式,比如复杂的第三方库
  • 当渲染复杂且不变的数据时,跳过响应式转换可提高性能

注意:仅仅让数据变为非响应式的,数据变的依旧变,只是没引起页面更新

1
2
3
4
5
6
7
8
9
10
11
12
13
setup() {
function showRawPerson() {
const p = toRaw(person);
p.age++;
console.log(p);
console.log(person);
}

function addCar() {
let car = { name: "奔驰", price: 40 };
person.car = markRaw(car);
}
}

customRef

创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制

1
2
<input type="text" v-model="keyword" />
<h3>{{ keyword }}</h3>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import { ref, customRef } from 'vue'

export default {
name: 'Demo',
setup() {
// 自定义 myRef
function myRef(value, delay) {
let timer
// 通过customRef去实现自定义
return customRef((track, trigger) => {
//
return {
get() {
//告诉Vue这个value值是需要被“追踪”的
track()
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
// 告诉Vue去更新界面
trigger()
}, delay)
},
}
})
}
let keyword = myRef('hello', 500)
return {
keyword,
}
},
}

provide / inject

实现祖先组件与后代组件之间通信。

provide-inject

1
2
3
4
5
6
7
8
9
10
// 祖先组件传递数据
import { provide, reactive, ref } from 'vue'

setup() {
let car = reactive({...})
let sum = ref(0)

provide('sum', sum)
provide('car', car)
}
1
2
3
4
5
6
7
8
// 后代组件接收数据
import { inject } from 'vue'

setup() {
const car = inject('car')
const sum = inject('sum')
return { car, sum }
}

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

Compositon API 的优势

Options API 存在的问题

使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data,methods,computed 等地方修改。

Composition API 的优势

可以更加优雅地组织代码、函数,让相关功能的代码更加有序的组织在一起。说白了就是让同一个功能的代码整合到一起,日后修改代码直接找对应的功能模块。

新的组件

Fragment

  • 在 Vue2 中: 组件必须有一个根标签
  • 在 Vue3 中: 组件可以没有根标签, 内部会将多个标签包含在一个 Fragment 虚拟元素中
  • 好处: 减少标签层级, 减小内存占用

Teleport

  • 将包裹的 HTML 结构移动到指定元素的末尾
  • to 属性为 CSS 选择器

简易的模态框效果:

1
2
3
4
5
6
7
8
<teleport to="#root">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.mask {
/* 遮罩层铺满窗口 */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 300px;
height: 300px;
background-color: green;
}

Suspense

等待异步组件时渲染额外内容,让用户体验更好

异步引入组件:

1
2
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./components/Child.vue'))

使用 Suspense 包裹组件,实际上是往插槽填充内容,default 插槽填充组件内容,fallback 插槽填充组件加载时显示的内容:

1
2
3
4
5
6
7
8
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<h3>加载中,请稍等...</h3>
</template>
</Suspense>

另外,若 Child 组件的 setup 函数返回一个 Promise 对象,也能渲染 fallback 里的内容:

1
2
3
4
5
6
7
8
async setup() {
let sum = ref(0)
return await new Promise((resolve, reject) => {
setTimeout(() => {
resolve({sum})
}, 3000)
})
}

其他改变

  • 全局 API 的转移

Vue3 将全局的 API,即:Vue.xxx 调整到应用实例 app 上:

Vue2 全局 API Vue3 实例 API
Vue.config.xxx app.config.xxx
Vue.config.productionTip 移除
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties
  • data 选项应始终被声明为一个函数
  • 过渡类名的更改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Vue2 */
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}

/* Vue3 */
.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-leave-from,
.v-enter-to {
opacity: 1;
}
  • 移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes
  • 移除 v-on.native修饰符,子组件没有在 emits: ['close'] 声明的自定义事件作为原生事件处理
  • 移除过滤器 filter

组件上的 v-model

当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。

父组件传值:

1
2
<!-- 父组件传值 -->
<my-counter v-model:number="count"></my-counter>

子组件在 emits 节点声明自定义事件,格式为 update:xxx ,调用 $emit 触发自定义事件:

1
2
3
4
5
6
7
8
9
export default {
props: ['number'],
emits: ['update:number'],
methods: {
add() {
this.$emit('update:number', this.number++)
},
},
}

注意,在 vue3props 属性同样是只读的,上面 this.number++ 并没有修改 number 的值。

其实通过 v-bind 传值和监听自定义事件的方式能实现和 v-model 相同的效果。

EventBus

借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。

安装 mitt 依赖包:

1
npm install mitt@2.1.0

创建公共的 eventBus 模块:

1
2
3
4
5
6
import mitt from 'mitt'

// 创建 EventBus 实例对象
const bus = mitt()

export default bus

数据接收方调用 bus.on() 监听自定义事件:

1
2
3
4
5
6
7
8
9
10
11
12
import bus from './eventBus.js'

export default {
data() {
return { count: 0 }
},
created() {
bus.on('countChange', (count) => {
this.count = count
})
},
}

数据接发送方调用 bus.emit() 触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import bus from './eventBus.js'

export default {
data() {
return { cout: 0 }
},
methods: {
addCount() {
this.count++
bus.emit('countChange', this.count)
},
},
}

vue 3.x 全局配置 axios

实际项目开发中,几乎每个组件中都会使用 axios 发起数据请求。此时会遇到如下两个问题:

  • 每个组件中都需要导入 axios(代码臃肿)
  • 每次发请求都需要填写完整的请求路径(不利于后期的维护)

main.js 文件中进行配置:

1
2
3
4
5
6
// 配置请求根路径
axios.defaults.baseURL = 'http://api.com'

// 将 axios 挂载为 app 全局自定义属性
// 每个组件可通过 this.$http 访问到 axios
app.config.globalProperties.$http = axios

组件调用:

1
this.$http.get('/users')

视频笔记内容来自B站尚硅谷的张天禹老师