特点

  • 采用组件化模式,提高代码复用率,且让代码更好维护
  • 声明式编码而非命令式编码,让编码人员无需直接操作 DOM,提高开发效率
  • 使用虚拟 DOM 加优秀的 Diff 算法,尽量复用 DOM 节点

模板语法

  1. 数据绑定,主要通过 css 选择器来进行标签属性对象绑定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Vue.config.productionTip=false
const v=new Vue({
el:"#root", // 1. css选择器
data:{ // 对象式
name:"ivan",
url:"http://wanstarge.github.io"
}
data:function(){ // 函数式,:function可以省略,在组件中,data必须使用函数式
return{
name:"ivan"
}
}
})
v.$mount("#root"); // 2. 原型方法

attention: 由 Vue 所管理的函数比如 data,一定不要写箭头函数,一旦写了箭头函数,this 就不再是 Vue 实例了

  1. 插值语法:用于解析标签体内容,{{xxx}},其中 xxx 为 js 表达式,可以直接读到 data 中的所有属性
  2. 指令语法:用于解析标签(包括标签属性,标签体内容,绑定事件…),
    1. 举例: v-bind:href="xxx" 或简写为 :href="xxx",xxx 同样要写 js 表达式,且可以直接读 data 中的所有属性

attention: Vue 中有很多的指令,形式都是 v-xxx,此处只是用 v-bind 举例

  1. 单向数据绑定 v-bind,页面随着数据变化,数据不随着页面变化
  2. 双向数据绑定 v-model,页面和数据互相影响,仅可以用于表单类元素,也就是可以输入的元素,带有 value 属性,v-model:value 可以简写为 v-model,如果绑定的是 checkbox 属性,且正好是布尔值,那么就根据布尔值来决定勾选还是不勾选

MVVM 模型

其中 M: 模型 model,对应 data 中的数据;V:视图 View,模板;VM:视图模型(ViewModel):View 实例对象, 常用 VM 来代指 Vue 实例对象

MVVM.png

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

数据代理

通过一个对象代理对另一个对象中属性的操作即为数据代理,vm._data 中存储了 data 中的数据,指向了同一块地址,仅仅是浅拷贝

知识背景:为对象添加属性 Object.defineProperty

1
2
3
4
5
6
7
8
9
10
11
12
13
<script type="text/javascript">
let person={
name:"张三",
sex:"男"
}
Object.defineProperty(person,"age",{
value:18,
enumerable:true, // 是否可以枚举
configurable:true, // 是否可以删除
writable:true // 控制是否可以被修改
})
console.log(person)
</script>
  • 将数据从正常状态变为 getter 和 setter 的状态可以称之为数据劫持,即对数据进行操作处理

事件处理

事件的基本使用:

  1. 使用 v-on:xxx@xxx 绑定事件,其中 xxx 是事件名,
  2. 事件的回调需要配置在 methods 对象中,最终会在 vm 上:
  3. methods 中配置的函数,不要用箭头函数!否则 this 就不是 vm 了;
  4. methods 中配置的函数,都是被 Vue 所管理的函数,this 的指向是 vm 或组件实例对象;
  5. @click=“demo” 和@click=“demo($event)” 效果一致,但后者可以传参;
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
<body>
<div id="root">
<h3>hello, {{name}}</h3>
<a :href="url">来看我的博客</a>
<button v-on:click="showMsg">点我提示信息1</button>
<button @click="showMsg2(37,$event)">点我提示信息2</button>
</div>
<script type="text/javascript">
Vue.config.productionTip=false
const vm=new Vue({
el:"#root",
data:{
name:"jack",
url:"http://wanstarge.github.io"
},
methods:{
showMsg(event){
console.log(event.target.innerText)
alert("同学你好")
},
showMsg2(number,event){
console.log(event.target.innerText,number)
alert("同学你好!!")
}
}
})
</script>
</body>

事件修饰符

Vue 中的事件修饰符,修饰符可以连续输入,链式调用:

修饰符 作用
prevent 阻止默认事件(常用)
stop 阻止事件冒泡(常用)
once 事件只触发一次(常用)
capture 使用事件的捕获模式
self 只有 event,target 是当前操作的元素时才触发事件
passive 事件的默认行为立即执行,无需等待事件回调执行完毕
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="js/vue.js"></script>
<style>
*{
margin: 20px;
}
.demo1{
height: 60px;
background-color: blueviolet;
}
.box1{
padding: 5px;
background-color: rgb(138, 90, 138);
}
.box2{
padding: 5px;
background-color: rgb(73, 210, 217);
}
.list{
width: 200px;
height: 200px;
background-color: peru;
overflow: auto;
}
li{
height: 70px;
}
</style>
<title>万星阁</title>
</head>
<body>
<div id="root">
<h3>hello, {{name}}</h3>
<hr/>
<!-- 阻止默认事件的a标签 -->
<a :href="url" @click.prevent="showInfo">来看我的博客</a>
<!-- 阻止事件冒泡 -->
<div class="demo1" @click="showInfo">
<button @click.stop="showInfo">点我原本会弹两次</button>
</div>
<!-- 事件只触发一次 -->
<button @click.once="showInfo">点我原本点一次弹一次</button>
<!-- 使用事件的捕获模式 -->
<div class="box1" @click.capture="showMsg(1)">
div1
<div class="box2" @click="showMsg(2)">div2</div>
</div>
<!-- 只有event,target是当前操作的元素时才触发事件,一定程度上类似冒泡 -->
<div class="demo1" @click.self="showInfo">
<button @click.stop="showInfo">点我原本会弹两次</button>
</div>
<!-- 事件的默认行为立即执行,无需等待事件回调执行完毕 -->
<ul @scroll.passive="demo" class="list">
<!-- 滚动的触发有scroll和wheel两种,wheel仅检测鼠标滚动 -->
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.productionTip=false
const vm=new Vue({
el:"#root",
data:{
name:"jack",
url:"http://wanstarge.github.io"
},
methods:{
showInfo(event){
// event.preventDefault() // 阻止默认事件
// event.stopPropagation() // 阻止事件冒泡
alert("同学你好")
console.log(event.target)
},
showMsg(msg){
console.log(msg)
},
demo(){
for(let i=0;i<100000;i++){
console.log("#")
}
console.log("滚完了")
}
}
})
</script>
</body>
</html>

键盘事件

  1. Vue 为常用的按键值提供了一些别名,如下表所示:
按键 别名
回车 enter
删除 delete,捕获删除和退格键
退出 esc
换行 tab
up
down
left
right
  1. 而对于 Vue 未提供别名的按键,可以使用按键原始的 key 值去绑定,但注意要转为 kebab-case(短横线命名);
  2. 系统修饰键(用法特殊):ctrl,alt.shift,meta(允许链式调用)
    1. 配合 kyup 使用:按下修饰键的同时,再按下其他健,随后释放其他键,事件才被触发。
    2. 配合 keydown 使用:正常触发事件。
  3. 也可以使用 keyCode 去指定具体的按键(不推荐)
  4. Vue.config.keyCodes,自定义键名=键码,可以去定制按键别名
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
<body>
<div id="root">
<h3>hello, {{name}}</h3>
<hr/>
<input type="text" placeholder="按下回车提示输出" @keyup.enter="showInfo">
<input type="text" placeholder="按下切换大小写提示输出" @keyup.caps-lock="showInfo">
<input type="text" placeholder="按下tab提示输出" @keydown.tab="showInfo">
<input type="text" placeholder="使用键值绑定" @keydown.13="showInfo">
<input type="text" placeholder="使用别名绑定" @keydown.huiche="showInfo">
</div>
<script type="text/javascript">
Vue.config.productionTip=false
Vue.config.keyCodes.huiche=13 // 定义了一个别名按键
const vm=new Vue({
el:"#root",
data:{
name:"jack",
url:"http://wanstarge.github.io"
},
methods:{
showInfo(event){
//console.log(event.key,event.keyCode)
console.log(event.target.value)
}
}
})
</script>
</body>

计算属性

  1. 定义:要用的属性不存在,要通过已布属阳计算得来
  2. 原理:底层借助了 Objcet.defineproperty 方法提供的 getter 和 setter。
  3. get 函数什么时候执行?
    (1) 初次读取时会执行一次。
    (2) 当依赖的数据发生改变时会被再次调用。
  4. 优势:与 methods 实现相比,内部有缓存机制(复用),效率更高,调试方便。

note: 备注:计算属性最终会出现在 Vm 上,直接读取使用即可;如果计算属性要被修改,那必须写 set 函数去响应修改,且 set 中要引起计算属性依赖的数据发生改变

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
<body>
<div id="root">
<h3>hello, {{name}}</h3>
<hr/>
姓:<input type="text" v-model="surname"> <br><br>
名:<input type="text" v-model="name"> <br><br>
全名:<span> {{fullName}} </span>
</div>
<script type="text/javascript">
Vue.config.devtools=true
Vue.config.productionTip=false
Vue.config.keyCodes.huiche=13 // 定义了一个别名按键
const vm=new Vue({
el:"#root",
data:{
name:"jack",
surname:"star",
url:"http://wanstarge.github.io"
},
computed:{
fullName:{
// 当有人读取fullName时,get就会被调用,且返回值就作为fullName的值,初次读取fullName,所依赖的数据发生变化时get会被调用
get(){
return this.surname+"-"+this.name
},
set(value){
const arr=value.split("-")
this.surname=arr[0]
this.name=arr[1]
}
}
// 当不使用set时计算属性可以被简写
fullName(){
return this.surname + "-" + this.name
}
}
})
</script>
</body>

监视属性

监视属性 watch:

  1. 当被监视的属性变化时,回调函数自动调用,进行相关操作
  2. 监视的属性必须存在,才能进行监视!!
  3. 监视的两种写法:
    1. new Vue 时传入 watch 配置
    2. 通过 vm.$watch 监视
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
<div id="root">
<h3>今天天气很{{info}}</h3>
<hr/>
<!-- 绑定事件的时候@xxx="yyy" yyy可以写一些简单的语句 -->
<!-- <button @click="ishot=!ishot">切换天气</button> -->
<button @click="changeWeather">切换天气</button>
</div>
<script type="text/javascript">
Vue.config.devtools=true
Vue.config.productionTip=false
Vue.config.keyCodes.huiche=13 // 定义了一个别名按键
const vm=new Vue({
el:"#root",
data:{
ishot:"cold"
},
computed:{
info(){
return this.ishot ? '炎热' : '凉爽'
}
},
methods:{
changeWeather(){
this.ishot = !this.ishot
}
},
watch:{
ishot:{
immediate: true, // 初始化时让handler调用
// handler在ishot发生改变时被调用
handler(newValue,oldValue){
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
}
}
}
})
// 另一种方式
vm.$watch('ishot',{
immediate: true, // 初始化时让handler调用
// handler在ishot发生改变时被调用
handler(newValue,oldValue){
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
}
})
</script>
</body>

深度监视

  1. Vue 中的 watch 默认不监测对象内部值的改变(一层。
  2. 配置 deep:true 可以监测对象内部值改变(多层)。

note: >- Vue 自身可以监测对象内部值的改变,但 Vue 提供的 watch 默认不可以!

  • 使用 watch 时根据数据的具体结构,决定是否采用深度监视。
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
<body>
<div id="root">
<h3>a:{{numbers.a}},b:{{numbers.b}}</h3>
<hr/>
<button @click="numbers.a++">点我让a加1</button>
<button @click="numbers.b++">点我让b加1</button>
</div>
<script type="text/javascript">
Vue.config.devtools=true
Vue.config.productionTip=false
Vue.config.keyCodes.huiche=13 // 定义了一个别名按键
const vm=new Vue({
el:"#root",
data:{
numbers:{
a:1,
b:1
}
},
watch:{
// 监视多级结构中某个属性的变化
'numbers.a':{
handler(newValue,oldValue){
console.log(`a变化了,现在是${newValue},之前是${oldValue}`)
},
},
'numbers': {
deep: true, // 深度监视,不只监视整体,还关注子属性的值
handler(newValue, oldValue) {
console.log(`numbers变化了,现在是${newValue},之前是${oldValue}`)
},
}
}
})
</script>
</body>

简写方法

只有 handler 函数,而没有其他属性时才能使用简写,类似于计算属性的简写

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
/* 结构体初始化简写 */
watch: {
// 完整写法
ishot: {
// handler在ishot发生改变时被调用
handler(newValue, oldValue) {
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
}
},
// 简写方法
ishot(newValue, oldValue) {
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
}
}
/* 外部监视简写 */
//完整写法
vm.$watch('ishot', {
// handler在ishot发生改变时被调用
handler(newValue, oldValue) {
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
}
})
// 简写方法
vm.$watch('ishot', function (newValue, oldValue) {
console.log(`ishot变化了,现在是${newValue},之前是${oldValue}`)
})

note: - 计算属性和监视属性的区别

  1. computedi 能完成的功能,watch 都可以完成。
  2. watch 能完成的功能,computed.不一定能完成,例如:watch 可以进行异步操作。

两个重要的小原则:
3. 所被 Vue 管理的函数,最好写成普通函数,这样 this 的指向才是 vm 或组件实例对象。
4. 所有不被 Vue 所管理的函数(定时器的回调函数、ajax 的回调函数、Promise 的回调函数等),最好写成箭头函数,这样 this 的指向才是 vm 或组件实例对象。

绑定样式

  1. class 样式,写法:class="XXX" XXX 可以是字符串、对象、数组。
    • 字符串写法 适用于:类名不确定,要动态获取。
    • 对象写法 适用于:要绑定多个样式,个数不确定,名字也不确定。
    • 数组写法 适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。
  2. style 样式
    • :style="{fontsize:xxx}" 其中 xxx 是动态值。
    • :style="[a,b]" 其中 a、b 是样式对象。
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
<body>
<div id="root">
<!--绑定c1ass样式-字符串写法,适用于:样式的类名不确定,需要动态指定-->
<div class="basic" :class="mood" @click="changeMode">{{name}}</div>
<!-- 绑定c1ass样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定-->
<div class="basic" :class="classarr" @click="changeMode">{{name}}</div>
<!-- 绑定c1ss样式--对象写法,适用于:要绑定的样式个数确定、名字也确定但要动态决定用不用 -->
<div class="basic" :class="classobj" @click="changeMode">{{name}}</div>
<!-- 绑定c1ss样式--对象写法-->
<div class="basic" :style="styobj">{{name}}</div>
<!-- 绑定c1ss样式--数组写法-->
<div class="basic" :style="[styobj, styobj2]">{{name}}</div>
<div class="basic" :style="styarr">{{name}}</div>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
Vue.config.keyCodes.huiche = 13 // 定义了一个别名按键
const vm = new Vue({
el: "#root",
data: {
name:"万星阁",
mood:"normal",
classarr:['sty1','sty2','sty3'],
classobj:{
atguigu1:false,
atguigu2:true
},
styobj:{
fontSize: '40px',
color:'red'
},
styobj2: {
backgroundColor:"orange"
},
styarr:[
{
fontSize: '40px',
color: 'red'
},
{
backgroundColor: "orange"
},
]
},
methods: {
changeMode() {
const arr=['happy','sad','normal']
this.mood=arr[Math.floor(Math.random()*arr.length)]
}
}
})
</script>
</body>

条件渲染

语句 写法 适用范围 特点 注意项
v-if v-if="表达式"
v-else-if="表达式"
v-e1se="表达式"
切换频率较低的场景。 不展示的 DOM 元素直接被移除。
v-if 可以和:v-else-if、v-else 一起使用,但要求结构不能被“打断”。
v-show v-show="表达式" 切换频率较高的场景。 不展示的 D0M 元素未被移除,仅仅是使用样式隐藏掉 使用 v-if 的时,元素可能无法获取到,而使用 v-show 一定可以获取到。
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
<body>
<div id="root">
<!-- 使用 v-show 做条件渲染,适用于切换频率高,只是隐藏 -->
<h2 v-show="dispTitle">{{name}},更高处见</h2>
<h2 v-show="1===3">{{name}},更高处见</h2>
<!-- 使用 v-if 做条件渲染 -->
<h2 v-if="dispTitle">{{name}},更高处见</h2>
<h2 v-if="1===3">{{name}},更高处见</h2>
<h2>当前的n值为: {{n}}</h2>
<button @click="n++">点我n+1</button>
<!-- 判断必须连续,不可以被其他节点打断 -->
<div v-if="n===1">Angular</div>
<div v-else-if="n===2">React</div>
<div v-else-if="n===3">Vue</div>
<div v-else>MX</div>
<!-- 使用template可以整体判断渲染,且不会产生额外元素,只能v-if使用 -->
<template v-if="n===1">
<h2>牧星</h2>
<h2>凌霄</h2>
<h2>万星阁</h2>
</template>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
Vue.config.keyCodes.huiche = 13 // 定义了一个别名按键
const vm = new Vue({
el: "#root",
data: {
name:"万星阁",
dispTitle: true,
n:0
},
methods: {

}
})
</script>
</body>

列表渲染

v-for 指令,用于展示列表数据:v-for="(item,index) in xxx" :key="yyy"; 可遍历:数组、对象、字符串(用的很少)、指定次数(用的很少)

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
<body>
<div id="root">
<h2>人员列表(遍历列表)</h2>
<ul>
<li v-for="(person,index) in persons" :key="person.id">
{{index}}-{{person.name}}-{{person.age}}
</li>
</ul>
<h2>汽车信息(遍历对象)</h2>
<ul>
<li v-for="(info,key) in car" :key="key">
{{key}}-{{info}}
</li>
</ul>
<h2>姓名(遍历字符串)</h2>
<ul>
<li v-for="(info,index) in name" :key="index" >
{{index}}-{{info}}
</li>
</ul>
<h2>测试(遍历指定次数)</h2>
<ul>
<li v-for="(number,index) in 5" :key="index">
{{index}}-{{number}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
Vue.config.keyCodes.huiche = 13 // 定义了一个别名按键
const vm = new Vue({
el: "#root",
data: {
name: "万星阁",
persons: [
{ id: '001', name: "张三", age: 18 },
{ id: '002', name: "李四", age: 19 },
{ id: '003', name: "王五", age: 20 }
],
car:{
name:"奥迪A8",
price:'70万',
color:'black'
}
},
})
</script>
</body>

key

react、vue 中的 key 有什么作用?(key 的内部原理)

  1. 虚拟 D0N 中 key 的作用:key 是虚拟 Do 州对象的标识,当状态中的数据发生变化时,Vue 会根据【新数据】生成【新的虚拟 DOM】,随后 Vue 进行【新虚拟 DoM】与【旧虚拟 Do 州】的差异比较,比较规则如下:
    1. 旧虚拟 D0M 中找到了与新虚拟 Do 州相同的 key:
      • 若虚拟 DOM 中内容没变,直接使用之前的真实 DOM
      • 若虚拟 DOM 中内容变了,则生成新的真实 DOM,随后替换掉页面中之前的真实 D0 州。
    2. 旧虚拟 DOM 中未找到与新虚拟 DOM 相同的 key,创建新的真实 DO 州,随后渲染到到页面。
  2. 用 index 作为 key 可能会引发的问题:
    • 若对数据进行:逆序添加、逆序刚除等破坏顺序操作:会产生没有必要的真实 DOM 更新==>界面效果没问题,但效率低。
    • 如果结构中还包含输入类的 DOM:会产生错误 DOM 更新=>界面有问题
  3. 开发中如何选择 key?:
    • 最好使用每条数据的唯一标识作为 ky,比如 id、手机号、身份证号、学号等唯一值。
    • 如果不存在对数据的逆序添加、逆序刚除等破坏顺序操作,仅用于渲染列表用于展示,使用 index 作为 key 是没有问题的。

attention: 倘若不指定 key,Vue 会默认将 index 作为 key 值

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
<body>
<div id="root">
<h2>人员列表(遍历列表)</h2>
<button @click.once="add">添加一个老刘</button>
<ul>
<li v-for="(person,index) in persons" :key="index">
{{index}}-{{person.name}}-{{person.age}}
<input type="text">
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
Vue.config.keyCodes.huiche = 13 // 定义了一个别名按键
const vm = new Vue({
el: "#root",
data: {
name: "万星阁",
persons: [
{ id: '001', name: "张三", age: 18 },
{ id: '002', name: "李四", age: 19 },
{ id: '003', name: "王五", age: 20 }
],
},
methods:{
add(){
const p = {id:"004",name:"老刘",age:40}
this.persons.unshift(p)
}
}
})
</script>
</body>

列表过滤

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
<body>
<div id="root">
<h2>人员列表</h2>
<input type="text" placeholder="请输入名字" v-model="keyWord">
<button @click="sortType=0">原顺序</button>
<button @click="sortType=1">年龄降序</button>
<button @click="sortType=2">年龄升序</button>
<ul>
<li v-for="(person,index) in filPersons" :key="person.id">
{{person.name}}-{{person.age}}-{{person.sex}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
Vue.config.keyCodes.huiche = 13 // 定义了一个别名按键
const vm = new Vue({
el: "#root",
data: {
keyWord: "",
sortType:0, // 0:原顺序 1:年龄降序 2:年龄升序
persons: [
{ id: '004', name: "温兆伦", age: 21, sex: '男' },
{ id: '001', name: "马冬梅", age: 18, sex: '女' },
{ id: '002', name: "周冬雨", age: 19, sex: '女' },
{ id: '003', name: "周杰伦", age: 20, sex: '男' },
],
// filPersons:[]
},
// #region watch 实现
watch: {
keyWord: {
immediate: true,
handler(newVal) {
this.filPersons = this.persons.filter(person => person.name.includes(newVal))
}
}
},
// #endregion
// compute 实现
computed: {
filPersons: {
get() {
const arr=this.persons.filter(person => person.name.includes(this.keyWord))
if(this.sortType){
arr.sort((a,b)=>this.sortType===1?b.age-a.age:a.age-b.age);
}
return arr;
}
}
}
})
</script>
</body>

数据更新监测原理探究

数据监测实质

收到 data 后先对数据进行加工处理,而后将 _data=data,其加工过程大概可以按照如下所示,只是简单的示例,Vue 还可以实现,直接对属性名进行赋值,以及递归嵌套识别属性放入 _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
let data = {
name: "ivan",
address: "众星之上"
}
// 创建一个监视的实例对象,用于监视data中属性的变化
const obs = new Observer(data);
let vm = {}
vm._data = data = obs
function Observer(obj) {
// 汇总对象中所有的属性形成一个数组
const keys = Object.keys(obj);
// 遍历
keys.forEach(key => {
Object.defineProperty(this, key, {
get() {
return obj[k]
},
set(val) {
console.log(`属性${key}的值发生了变化,准备解析模板,生成虚拟DOM`);
obj[key] = val
}
})
})
}

数据监测原理

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
59
60
61
62
<body>
<div id="root">
<h1>学生信息</h1>
<button @click="student.age++">年龄+1岁</button><br />
<button @click="addSex">添加性别属性,默认值:男</button><br />
<button @click="student.sex='未知'">修改性别</button><br />
<button @click="addFriend">在列表首位添加一个朋友</button><br />
<button @click="updateFirstFriendName">修改第一个朋友的名字为:张三</button><br />
<button @click="student.hobby.push('游泳')">添加一个爱好</button><br />
<button @click="updateHobby">修改第一个爱好为:开车</button>
<br />
<h2>姓名:{{student.name}}</h2>
<h2>年龄:{{student.age}}</h2>
<h2 v-if="student.sex">性别:{{student.sex}}</h2>
<h2>爱好</h2>
<ul>
<li v-for="(hob,index) in student.hobby" :key="index">
{{hob}}
</li>
</ul>
<h2>朋友们</h2>
<ul>
<li v-for="(friend,index) in student.friends" :key="index">
{{friend.name}}-{{friend.age}}
</li>
</ul>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
const vm = new Vue({
el: "#root",
data: {
student:{
name: 'mx',
age: 18,
hobby: ['篮球', '足球', '编程'],
friends: [
{ name: 'star', age: 15 },
{ name: 'ivan', age: 20 },
]
}
},
methods:{
addSex(){
this.$set(this.student,'sex','男') // 也可以Vue.set(....)
},
addFriend(){
this.student.friends.unshift({name:'博文',age:25})
},
updateFirstFriendName(){
this.student.friends[0].name='张三'
this.student.friends[0].age = 21
},
updateHobby(){
//this.student.hobby.splice(0,1,'开车')
this.$set(this.student.hobby, '0', '开车')
}
}
})
</script>
</body>

summary: Vue 监视数据的原理

  1. vue 会监视 data 中所有层次的数据。
  2. 通过 setter 实现监视监测对象中的数据,且要在 new Vue 时就传入要监测的数据。
  3. 对象中后追加的属性,Vue 默认不做响应式处理
  4. 如需给后添加的属性做响应式,请使用如下 API:
    • Vue.set(target,propertyName/index,value)
    • vm.$set(target,propertyName/index,value)
  5. 如何监测数组中的数据: 通过包裹数组更新元素的方法实现,本质就是做了两件事:
  6. 调用原生对应的方法对数组进行更新。
  7. 重新解析模板,进而更新页面。
  8. 在 Vue 修改数组中的某个元素一定要用如下方法:
  9. 使用这些 API:push()pop()shift()unshift()splice()sort()reverse()
  10. Vue.set(),vm.$set()

attention: 特别注意:Vue.set()vm.$set() 不能给 vm 或 vm 的根数据对象添加属性!!!

收集表单数据

  • 若:<input type="text"/>,则 v-modele 收集的是 value 值,用户输入的就是 value 值。
  • 若:<input type:="radio"/>,则 v-model 收集的是 value 值,且要给标签配置 value 值。
  • 若:<input type="checkbox"/>
    1. 没有配置 input 的 value 属性,那么收集的就是 checked(勾选 or 未勾选,是布尔值)
    2. 配置 input 的 value 属性:
      1. v-model 的初始值是非数组,那么收集的就是 checked(勾选 or 未勾选,是布尔值)
      2. v-model 的初始值是数组,那么收集的的就是 value 组成的数组

note: - v-model 的三个修饰符:

修饰符 作用
lazy 失去焦点再收集数据
number 输入字符串转为有效的数字
trim 输入首尾空格过滤
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
<body>
<div id="root">
<form @submit.prevent="submitForm">
<label for="demo">账号:</label> <!-- 用于绑定文字和输入框 -->
<input type="text" id="demo" v-model.trim="account"> <br><br>
密码:<input type="password" v-model="password"><br><br>
性别:<input type="radio" name="gender" v-model="gender" value="male">
<input type="radio" name="gender" v-model="gender" value="female"><br><br>
年龄:<input type="number" v-model.number="age"><br><br>
爱好:
<input type="checkbox" value="reading" v-model="hobby"> 读书
<input type="checkbox" value="swimming" v-model="hobby"> 游泳
<input type="checkbox" value="running" v-model="hobby"> 跑步<br><br>
生日:<input type="date" v-model="birthday"><br><br>
所属校区:<select name="school" id="sarea" v-model="area">
<option value="">请选择校区</option>
<option value="龙子湖">龙子湖校区</option>
<option value="新校区">新校区</option>
<option value="老校区">老校区</option>
</select><br><br>
备注:<textarea v-model.lazy="note"></textarea><br><br>
<input type="checkbox" v-model="agree">阅读并接受 <a href="http://www.wanxingge.site">用户协议</a><br><br>
<button>提交</button>
</form>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
const vm = new Vue({
el: "#root",
data: {
account:"",
password:"",
gender:"female",
age:18,
hobby:[],
area:"龙子湖",
note:"",
agree:"",
birthday:"",
},
methods:{
submitForm(){
console.log(JSON.stringify(this._data))
}
}
})
</script>
</body>

* 过滤器

对要显示的数据进行恃定格式化后再显示 (适用于一些简单逻辑的处理),语法:

  1. 注册过滤器:Vue.filter(name,cal1back)new Vue{filters:()}
  2. 使用过滤器:{{xxx|过滤器名}}v-bind:属性="xxx|过滤器名"

note: >3. 过滤器也可以接收额外参数、多个过滤器也可以串联
4. 并没有改变原本的数据,是产生新的对应的数据

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
<body>
<div id="root">
<h2>显示格式化后的时间</h2>
<h3>计算属性:现在是:{{fmtTime}}</h3>
<h3>方法:现在是:{{getFmtTime()}}</h3>
<h3>过滤器:现在是:{{time | timeFmt}}</h3>
<h3>过滤器参数:现在是:{{time | timeFmt("YYYY_MM_DD")|mxSlice}}</h3>
<h3 :data="msg | mxSlice">属性过滤</h3>
</div>
<div id="root2">
<h2>{{msg|mxSlice}}</h2>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
/* 全局过滤器 */
Vue.filter('mxSlice', function (value) {
return value.slice(0, 4)
})
const vm = new Vue({
el: "#root",
data: {
time: 1621561377603,
msg:"是非在己,毁誉由人,得失不论"
},
computed: {
fmtTime() {
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss')
}
},
methods: {
getFmtTime() {
return dayjs(this.time).format('YYYY年MM月DD日 HH:mm:ss')
}
},
filters: {
/* 局部过滤器 */
timeFmt(value, ctr = "YYYY-MM-DD HH:mm:ss") {
return dayjs(value).format(ctr)
},
}
})
new Vue({
el: "#root2",
data: {
msg: "你好,万星阁"
},
})
</script>
</body>

内置指令

指令 作用 用法
v-bind 单向绑定解析表达式,可简写为:xxx v-bind:value="name" 或:value="name"
v-model 双向数据绑定 由于其仅绑定 value 属性,故可以直接写为 v-model="name"
v-for 遍历数组,对象,字符串 v-for="(p,index) in persons"
v-on 绑定事件监听,可以简写为@ v-on:click="updateAge"@click="updateAge"
v-if 条件渲染(动态控制节点是否存存在),不展示的 DOM 元素直接被移除。 v-if 可以和:v-else-if、v-else 一起使用,但要求结构不能被“打断”。v-if="person.agree"
v-show 条件渲染,不展示的 D0M 元素未被移除,仅仅是使用样式隐藏掉 v-show="person.agree"
v-text 向其所在的节点中渲染文本内容。与插值语法的区别:v-text 会替换掉节点中的内容,{{xx}} 则不会。 v-text="name"
v-html 与 v-text 相比,此指令支持 html 结构的解析 注意: 会有安全性问题,可以在其中执行 js 代码,导致 XSS 攻击,如果必须使用,一定要在可信的内容上,永远不要在用户提交的内容上
v-cloak 实现延迟解析,用于等待各种情况导致的 Vue 加载阻塞 配合 css[v-cloak]{display:none;} 来进行实现
v-once v-once 所在节点在初次动态渲染后,就视为静态内容了。
以后数据的改变不会引起 v-once 所在结构的更新,可以用于优化性能。
<h2 v-once></h2>
v-pre 跳过其所在节点的编译过程。
可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译。
<h2 v-pre></h2>

自定义指令

  1. 定义语法:
    • 局部指令:new Vue({ directives:(指令名:配置对象)}) 或者 new Vue({directives(){}})
    • 全局指令:Vue.directive(指令名,配置对象)Vue.directive(指令名,回调函数)
  2. 配置对象中常用的 3 个回调:
    • bind: 指令与元素成功绑定时调用。
    • inserted: 指令所在元素被插入页面时调用。
    • update: 指令所在模板结构被重新解析时调用。
  3. 备注
    • 指令定义时不加 v-,但使用时要加 v-;
    • 指令名如果是多个单词,要使用 kebab-case 命名方式,不要用 camelCase 命名。
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
<body>
<div id="root">
<h2>当前的n值为<span v-text="n"></span></h2>
<h2>放大十倍后的n值为<span v-big="n"></span></h2>
<h2>放大十倍后的n值为<span v-big-number="n"></span></h2>
<button @click="n++">点我n++</button>
<hr>
<input type="text" v-fbind:value="n">
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
// 全局自定义指令,如果是对象,那就把第二个参数换成对象
Vue.directive('big-number', function (element, binding) {
element.innerText = binding.value * 10;
})
const vm = new Vue({
el: "#root",
data: {
n: 1
},
directives: {
/* 简单功能,指令与元素成功绑定时 和 指令所在的模板被重新解析时会被调用 */
big(element, binding) {
element.innerText = binding.value * 10;
},
/* 详细步骤 */
fbind: {
// 指令与元素成功绑定时(一上来)被调用
bind(element, binding) {
element.value = binding.value
console.log('big', this) // 注意此处的指令是window
},
// 指令所在元素插入页面时被调用
inserted(element, binding) {
element.focus()
},
// 指令所在模板被重新解析时被调用
update(element, binding) {
element.value = binding.value
}
}
}
})
</script>
</body>

生命周期

  1. 又名生命周期回调函数、生命周期函数、生命周期钩子。
  2. 是什么:Vue 在关键时刻帮我们调用的一些特殊名称的函数。
  3. 生命周期函数的名字不可更改,但函数的具体内容是程序员根据需求编写的。
  4. 生命周期函数中的 this 指向是 vm 或组件实例对象。
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
<body>
<div id="root">
<h2 :style="{opacity}">欢迎学习</h2>
<button @click="stop">点我停止变换</button>
<button @click="stop">点我暴力停止变换</button>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
// 全局自定义指令,如果是对象,那就把第二个参数换成对象
Vue.directive('big-number', function (element, binding) {
element.innerText = binding.value * 10;
})
const vm = new Vue({
el: "#root",
data: {
opacity: 1
},
methods: {
stop(){
this.$destroy()
}
},
mounted() {
console.log('mounted');
this.timer=setInterval(() => {
this.opacity -= 0.01;
if (this.opacity <= 0) {
this.opacity = 1;
}
}, 16)
},
beforeDestroy(){
console.log('vm 将会被删除');
clearInterval(this.timer);
}
})
</script>
</body>

分析生命周期

Vue 实例有⼀个完整的⽣命周期,也就是从 new Vue()、初始化事件 (.once 事件) 和生命周期、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是 Vue 的⽣命周期。

livePeriod.png

  1. beforeCreate(创建前):数据监测 (getter 和 setter) 和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到 data、computed、watch、methods 上的方法和数据。
  2. created(创建后):实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。
  3. beforeMount(挂载前):在挂载开始之前被调用,相关的 render 函数首次被调用。此阶段 Vue 开始解析模板,生成虚拟 DOM 存在内存中,还没有把虚拟 DOM 转换成真实 DOM,插入页面中。所以网页不能显示解析好的内容。
  4. !mounted(挂载后):在 el 被新创建的 vm.$el(就是真实 DOM 的拷贝)替换,并挂载到实例上去之后调用(将内存中的虚拟 DOM 转为真实 DOM,真实 DOM 插入页面)。此时页面中呈现的是经过 Vue 编译的 DOM,这时在这个钩子函数中对 DOM 的操作可以有效,但要尽量避免。一般在这个阶段进行:开启定时器,发送网络请求,订阅消息,绑定自定义事件等等
  5. beforeUpdate(更新前):响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染(数据是新的,但页面是旧的,页面和数据没保持同步呢)。
  6. updated(更新后) :在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
  7. !beforeDestroy(销毁前):使用 $destroy 执行实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例,但是不会再渲染页面,一般不进行数据操作。在这个阶段一般进行关闭定时器,取消订阅消息,解绑自定义事件,DOM 事件不会被解绑。
  8. destroyed(销毁后):实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。

template

  1. 有 template 选项(属性)
    如果 el 绑定的容器没有任何内容,就一个空壳子,但在 Vue 实例中写了 template,就会编译解析这个 template 里的内容,生成虚拟 DOM,最后将 虚拟 DOM 转为 真实 DOM 插入页面(其实就可以理解为 template 替换了 el 绑定的容器的内容)
  2. 没有 template 选项(属性)没有 template,就编译解析 el 绑定的容器,生成虚拟 DOM,后面就顺着生命周期执行下去,也就是我们之前常用的方式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<div id="root">

</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
// 全局自定义指令,如果是对象,那就把第二个参数换成对象
Vue.directive('big-number', function (element, binding) {
element.innerText = binding.value * 10;
})
const vm = new Vue({
el: "#root",
template:`
<div>
<h2>当前n的值为:{{n}}</h2>
<button @click="n++">点我n+1</button>
</div>`,
data: {
n: 1,
},
})
</script>
</body>

attention: 在 Vue2 中 template 属性中不允许放 template 标签

组件

模块:向外提供特定功能的 js 程序,一般就是一个 js 文件;组件:用来实现局部(特定)功能效果的代码集合 (html/css/js/,image…)

* 非单文件组件

一个文件中包含有 n 个组件,使用组件有三大步骤:

  1. 定义组件: 使用 Vue.extend(options) 创建,其中 options 和 new Vue(options) 时传入的那个 options 几乎一样,但也有点区别:
    1. el 不要写,因为最终所有的组件都要经过一个 vm 的管理,由 vm 中的 el 决定服务哪个容器。
    2. data 必须写成函数,避免组件被复用时,数据存在引用关系。(备注:使用 template 可以配置组件结构。)
    3. 简写的方式:const school = Vue.extend(options) 可以简写为 const school = options
    4. 可以在 options 中使用 name 属性指定在开发者工具中呈现的组件名
  2. 注册组件: 分为全局注册和局部注册:
    1. 局部注册:靠 new Vue 的时候传入 components 选项
    2. * 全局注册:靠 Vue.component('组件名',组件)
  3. 使用组件: 写组件标签,<组件名></组件名>,也可以使用 <school/> 但是要求使用脚手架,不是用脚手架时会导致后续组件不能渲染。

attention: 组件名如果是多个单词,可以使用全小写的方式 mxschool 或者中间用 - 连接,mx-school,或者每一个单词的首字母均大写(!要求使用脚手架)MxSchool;组件名尽可能回避 HTML 中已有的元素名称,例如:h2、H2 都不行。可以使用 name 配置项指定组件在开发者工具中呈现的名字。

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
59
60
61
62
63
64
65
<body>
<div id="root">
<!-- 第三步 编写组件标签 -->
<xuexiao></xuexiao>
<hr>
<student></student>
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
/* 第一步:创建组件 */
const hello = Vue.extend({
template: `
<div>
<h2>你好啊,初学者</h2>
</div>
`,
})
const school = Vue.extend({
template: `
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
// el: '#root' 组件定义时不要写el配置项,因为最终所有的组件都要被一个vm管理,由vm决定服务于哪个容器
return {
name: '万星阁',
address: '众星之上',
}
},
methods:{
showName(){
alert(this.name)
}
}
})
const student = {
name:"student"
template: `
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
`,
data() {
return {
name: '张三',
age: 20
}
}
}
Vue.component('hello',hello) /* 第二步:注册组件(全局注册),所有vm均可以使用 */
const vm = new Vue({
el: "#root",
/* 第二步:注册组件(局部注册) */
components: {
xuexiao:school, // 或者直接名称相同时school,以前者为准
student,
}
})
</script>
</body>

组件的嵌套

被谁所包含,就在谁的 template 属性中写标签,同时在谁的 component 属性中注册,一般 vm 下方会直接引领一个 App,而后在 App 下方管理各个组件的注册

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
59
60
61
62
<body>
<div id="root">
</div>
<script type="text/javascript">
Vue.config.devtools = true
Vue.config.productionTip = false
const student = Vue.extend({
template: `
<div>
<h2>学生姓名:{{name}}</h2>
<h2>学生年龄:{{age}}</h2>
</div>
`,
data() {
return {
name: '张三',
age: 20
}
}
})
const school = Vue.extend({
template: `
<div>
<h2>学校名称:{{name}}</h2>
<h2>学校地址:{{address}}</h2>
<student></student>
<button @click="showName">点我提示学校名</button>
</div>
`,
data() {
return {
name: '万星阁',
address: '众星之上',
}
},
methods: {
showName() {
alert(this.name)
}
},
components: {
student
}
})
// 定义APP组件
const app = Vue.extend({
template: `
<school></school>
`,
components: {
school
}
})
const vm = new Vue({
el: "#root",
template:`<app></app>`,
components: {
app
}
})
</script>
</body>

VueComponent

  1. school 组件本质是一个名为 VueComponent 的构造函数,且不是程序员定义的,是 Vue.extend 生成的。
  2. 我们只需要写 <school/><school></school>,Vue 解析时会帮我们创建 school 组件的实例对象即 Vue 帮我们执行:new VueComponent(options)
  3. 特别注意:每次调用 Vue.extend,返回的都是一个全新的 VueComponent!!!
  4. 关于 this 指向:
    1. 组件配置中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数它们的 this 均是【VueComponent 实例对象】
    2. new Vue() 配置中:data 函数、methods 中的函数、watch 中的函数、computed 中的函数它们的 this 均是【Vue 实例对象】。
  5. VueComponent的实例对象,以后简称vc(也可称之为:组件实例对象)。Vue的实例对象,以后简称vm

内置关系

  1. 一个重要的内置关系:VueComponent.prototype._proto==Vue.prototype
  2. 为什么要有这个关系:让组件实例对象 (vc) 可以访问到 Vue 原型上的属性、方法。
    prototype.png

单文件组件

一个文件中只包含有 1 个组件,file.vue,文件名的命名规则尽量保持和 非单文件组件 中提到的命名规则保持一致。

使用测试

  • School.Vue
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
<template>
<!-- 组件的结构 -->
<div>
<h2 class="demo">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<button @click="showName">点我提示学校名</button>
</div>
</template>
<script>
// 组件交互相关的代码(数据,方法等等)
export default {
name: "School",
data() {
return {
name: "万星阁",
address: "众星之上",
};
},
methods: {
showName() {
alert(this.name);
},
},
};
</script>
<style>
/* 组件的样式 */
.demo {
background-color: chartreuse;
}
</style>
  • App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template lang="">
<div>
<School></School>
</div>
</template>
<script>
import School from './School.vue';
export default {
name: 'App',
components: {
School
}
}
</script>
  • main.js
1
2
3
4
5
6
import { App } from "./App.vue";
new Vue({
el:'#root',
template:'<App></App>',
components:{App},
})
  • index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>测试单文件组件语法</title>
</head>
<body>
<div id="root"></div>
<script src="./js/vue.js"></script>
<script src="./main.js"></script>
</body>
</html>

脚手架

Vue 脚手架是 Vue 官方提供的标准化开发工具(开发平台),Vue-CLI (command line interface)

安装

使用 nodejs 来安装 vue 脚手架,nodejs 的安装参考 NodeJS

  1. 全局安装 @vue/cli:npm install -g @vue/cli
  2. 切换到需要创建工程的目录,并使用 vue create 项目名 来创建项目
  3. 启动项目:npm run serve

note: Vue 脚手架隐藏了所有 webpack 相关的配置,若想查看具体的 webpakc 配置,请执行:vue inspect>output.js

工程分析

文件名 作用
babel.config.js 用于配置 babel 来实现 ES6 到 ES5 的转换
package.json nodejs 自动对工程进行的配置文件,包含了当前工程所需的依赖以及其对应的版本等等,以及定义的一些短命令均在此处设置(build 进行构建,lint 进行语法检查)
package-lock.json nodejs 的包版本管理文件,用于锁定当前项目依赖的包版本
vue.config.js 原则上 public,src 文件夹,图标名称,index.html 以及 main.js 不可以修改,但是可以通过这个文件进行修改,参考 Vue2,Vue3
main.js 该文件是整个项目的入口文件,引入 Vue 以及 APP 管理组件,render 代码实现了将 APP 组件放入容器中
App.vue 管理其他组件的文件,对组件进行注册结构进行管理等等
assets 该文件夹常用于存放静态资源
components 所有除了 App 以外的组件都放到该文件夹下
public 包含了页面以及公共使用的文件
index.html 浏览器的初始页面,<meta http-equiv="X-UA-Compatible" content="IE=edge"> 是为了让 IE 以最高级别渲染界面,<meta name="viewport" content="width=device-width,initial-scale=1.0"> 开启移动端的理想视口,<%= BASE_URL =%> 指的是 public 文件夹所在的路径,<% htmlWebpackPlugin.options.title %> 为使用 webpack 插件来获取 package.json 文件中指定的工程名

如果不使用上述 main.js 文件中提到的 render 函数而按照 [[编程语言/JavaScript/功能框架/Vue#单文件组件|单文件组件]] 中提到的直接写 template,其会报错要求使用 render 函数或者使用完整版的 vue,通过修改 main.js 文件中引入的 vue 路径为 vue/dist/vue 即为完整版的 vue(核心 + 模板解析器),此时问题解决,但是在打包时文件会比较大,同时保留解析器不是开发的做法

render 为渲染的意思,可以在没有编译器的 vue 的情况下创建并修改页面元素,使用下面的代码可以直接在页面创建元素 render(createElement){return createElement('hi','你好啊')

summary: >vue.js 与 vue.runtime.xxx.js 的区别:vue.js 是完整版的 Vue,包含:核心功能 + 模板解析器;vue.runtime.xxx.js 是运行版的 Vue,只包含:核心功能:没有模板解析器。

因为 vue.runtime.xxx.js 没有模板解析器,所以不能使用 template 配置项,需要使用 render 函数接收到的 createElement 函数去指定具体内容。

ref 属性

  1. 被用来给元素或子组件注册引用信息 (id 的替代者)
  2. 应用在 html 标签上获取的是真实 D0M 元素,应用在组件标签上是组件实例对象 (vc)
  3. 使用方式:打标识:<h1 ref="xxx">......</h1><School ref="xxx"></Schoo1>; 获取元素的方法:this.$refs.XXX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template lang="">
<div>
<h1 ref="title">Welcome to MX Vue Study</h1>
<School ref="sch"/>
<button @click="showDOM" ref="btn">点我输出DOM元素</button>
</div>
</template>
<script>
import School from './components/School.vue';
export default {
name: 'App',
components: {
School
},
methods: {
showDOM() {
console.log(this.$refs.title); // 真实DOM元素
console.log(this.$refs.sch); // School组件的实例对象(vc)
console.log(this.$refs.btn); // 真实DOM元素
}
}
}
</script>

props 属性

功能:让组件接收外部传过来的数据

  1. 传递数据:<Demo name="xxx"/>
  2. 接收数据 (有三种方式):
    1. 只接收:props:['name']
    2. 限制类型:props:{name:String,age:Number}
    3. 限制类型、限制必要性、指定默认值:
1
2
3
4
5
6
7
8
9
10
props:{
name:{
type:String,
required:true, // 需要输入,那默认值也就没有作用了,故不需要
},
age:{
type:Number,
default:18
}
}

note: props 是只读的,Vue 底层会监测你对 props 的修改,如果进行了修改,就会发出警告,若业务需求确实需要修改,那么请复制 props 的内容到 data中一份,然后去修改 data 中的数据。

  • App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<template lang="">
<div>
<Student name="允异" :age="18"></Student>
</div>
</template>
<script>
import Student from './components/Student.vue';
export default {
name: 'App',
components: {
Student
}
}
</script>
  • Student.vue
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
<template>
<!-- 组件的结构 -->
<div>
<h2>学生姓名:{{ name }}</h2>
<h2>学生年龄:{{ mxAge +1 }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="updateAge">修改年龄</button>
</div>
</template>
<script>
// 组件交互相关的代码(数据,方法等等)
export default {
name: "School-item",
data() {
return {
msg:"are you ok",
mxAge:this.age
};
},
methods: {
updateAge() {
this.mxAge ++;
}
},
/* 简单接受外部参数 */
// props:['name','age','sex']
/* 接收同时限制类型 */
// props:{
// name:String,
// age:Number,
// sex:String
// },
/* 接收同时限制类型和必要性并指定默认值 */
props:{
name:{
type:String,
required:true,
},
age:{
type:Number,
default:18
},
sex:{
type:String,
default:"男"
}
}
};
</script>

mixin

可以把多个组件共用的配置提取成一个混入对象,以进行简写:

使用方式:

  1. 定义混合,例如:
1
2
3
4
{
data(){...},
methods:{...}
}
  1. 使用混入,例如:
    1. 全局混入:Vue.mixin(xxx)
    2. 局部混入:Vue.mixins:['xxx']
  • mixin.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export const hunhe = {
methods: {
showName() {
alert(this.name);
}
},
};
export const hunhe2 = {
data() {
return {
x:100,
y:200
}
},
}
  • School.vue
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
<template>
<!-- 组件的结构 -->
<div>
<h2 @click="showName">学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
import { hunhe,hunhe2 } from "../mixin";
export default {
name: "School-item",
data() {
return {
name: "万星阁",
address: "众星之上",
};
},
methods: {
showName() {
alert(this.name);
},
},
mixins: [hunhe,hunhe2],
};
</script>

插件

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

  1. 定义插件,示例如下:
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
export default {
install(Vue){
console.log("@@@install",Vue);
// 全局过滤器
Vue.filter('mxSlice', function (value) {
return value.slice(0, 4)
})
// 全局自定义指令
Vue.directive('big-number', function (element, binding) {
element.innerText = binding.value * 10;
})
// 定义混入
Vue.mixin({
methods: {
showName() {
alert(this.name);
}
},
})
// 原型添加方法
Vue.prototype.hello = ()=> {
alert("hello world");
}
}
}
  1. 使用插件:Vue.use()
1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

import plugins from "./plugins";
Vue.use(plugins);
new Vue({
render: h => h(App),
}).$mount('#app')
  1. 而后就可以在各个单独的组件里面使用前面全局自定义的指令和混合了

Scoped 样式

当在不同的 Vue 组件文件中定义了同名称的样式时,浏览器最终会将所有的样式汇总到一个文件中,这时所显示的样式就咦在 main.js 中先导入的那个为准,这时我们就可以通过 scoped 属性来进行区分,指定当前样式的作用域,让样式在局部生效,防止冲突。写法:<style scoped>

  • School.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<!-- 组件的结构 -->
<div class="test">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School-item",
data() {
return {
name: "万星阁",
address: "众星之上",
age: 10
};
},
};
</script>
<style scoped>
.test{
background-color: blueviolet;
}
</style>
  • Student.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<!-- 组件的结构 -->
<div class="test">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
</div>
</template>
<script>
export default {
name: "Student-item",
data() {
return {
msg:"are you ok",
name: "牧星",
sex: "男",
};
},
};
</script>
<style scoped>
.test {
background-color: red;
}
</style>

项目拆分

  1. 组件化编码流程:
    1. 拆分静态组件:组件要按功能点拆分,命名不要与 html 元素冲突。
    2. 实现动态组件:考虑好数据的存放位置,数据是一个组件在用,还是一些组件在用:
      1. 一个组件在用:放在组件自身即可。
      2. 一些组件在用:放在他们共同的父组件上(状态提升)。
    3. 实现交互:从绑定事件开始。
  2. props 适用于:
    1. 父组件=>子组件通信
    2. 子组件==>父组件通信(要求父先给子一个函数),具体实现参考 自定义事件
  3. 使用 v-model 时要切记:v-model 绑定的值不能是 props 传过来的值,因为 props 是不可以修改的!
  4. props 传过来的若是对象类型的值,修改对象中的属性时 Vue 不会报错,但不推荐这样做。

组件间通信

基础通信

C|397

第一种方式:最基础的同级组件间的数据传递:可以通过红线到橙线的方式进行传递,由 APP 进行中转,对于橙线来说,APP 将数据通过标签属性的方式传给 List 作为接收参数;而对于红线来说,App 声明一个接收参数的函数,并将该函数作为参数传递给 Header,Header 调用该参数即可实现将数据传递给 APP

自定义事件

  1. 一种组件间通信的方式,适用于 子组件=>父组件
  2. 使用场景:A 是父组件,B 是子组件,B 想给 A 传数据,那么就要在 A 中给 B 绑定自定义事件(事件的回调在 A 中)。
  3. 绑定自定义事件:
    1. 第一种方式,在父组件中:<Demo @wxg="test"/><Demo v-on:wxg="test"/>
    2. 第二种方式,在父组件中如下示例
    3. 若想让自定义事件只能触发一次,可以使用 once 修饰符,或 $once 方法。
1
2
3
4
5
6
7
8
9
10
11
<template>
<Student ref="student"/>
</template>
<script>
mounted() {
// 第三种方法,使用ref属性拿到实例对象,绑定自定义事件
this.$refs.student.$on('wxg', this.getStudentName);
// 绑定自定义事件(一次性),也可以使用once修饰符
// this.$refs.student.$once('wxg', this.getStudentName);
},
</script>
  1. 触发自定义事件: this.$emit('wxg',this.test)
  2. 解绑自定义事件:
    1. 解绑一个自定义事件: this.$off("wxg");
    2. 解绑多个自定义事件: this.$off(["wxg", "demo"]);
    3. 解绑所有自定义事件: this.$off();
  3. 组件上也可以绑定原生 DOM 事件,需要使用 native 修饰符: {html}<Student ref="student" @click.native="show"/>

attention: >7. 通过 this.$refs.xxx.$on('wxg',回调) 绑定自定义事件时,回调要么配置在 methods 中,要么用箭头函数,否则 this 指向会出问题!
8. 给谁绑定的事件,最后就由谁来触发

示例
  • App.vue
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
<template lang="">
<div class="app">
<h1>{{msg}},学生姓名:{{studentName}}</h1>
<!-- 第一种方法:接收用于通信的函数 -->
<School :getSchoolName="getSchoolName"/>
<hr>
<!-- 第二种方法,使用v-on或者@绑定事件,子组件使用$emit方法进行触发 -->
<!-- <Student name="mh" @wxg="getStudentName" @demo="m1"/> -->
<Student name="mh" ref="student" @click.native="show"/>
</div>
</template>
<script>
import School from './components/School.vue';
import Student from './components/Student.vue';

export default {
name: 'App',
components: {
School,
Student
},
data() {
return {
msg: 'Hello Vue',
studentName: ''
}
},
methods: {
getSchoolName(name){
console.log('App收到了学校名:',name);
},
getStudentName(name,...params){
console.log('App收到了学生名:',name,params);
this.studentName = name;
},
m1(){
console.log("demo事件被触发了");
},
show() {
alert("click the Student");
}
},
mounted() {
// 第三种方法,使用ref属性拿到实例对象,绑定自定义事件
this.$refs.student.$on('wxg', this.getStudentName);
// 绑定自定义事件(一次性),也可以使用once修饰符
// this.$refs.student.$once('wxg', this.getStudentName);
},
}
</script>

<style lang="css">
.app{
background-color: gray;
}
</style>
  • School.vue
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
<template>
<!-- 组件的结构 -->
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
<button @click="sendSchoolName">把学校名给App</button>
</div>
</template>
<script>
export default {
name: "School-item",
data() {
return {
name: "万星阁",
address: "众星之上",
age: 10
};
},
/* 第一种方法:接收用于通信的函数 */
props: ["getSchoolName"],
methods: {
sendSchoolName() {
this.getSchoolName(this.name);
}
},
};
</script>
<style scoped>
.school{
background-color: blueviolet;
padding: 5px;
margin: 5px;
margin-top: 30px;
}
</style>
  • Student.vue
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
<template>
<!-- 组件的结构 -->
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendStudentName">把学生名给App</button>
<button @click="unbind">解绑wxg事件</button>
</div>
</template>
<script>
export default {
name: "Student-item",
data() {
return {
msg:"are you ok",
name: "牧星",
sex: "男",
};
},
methods: {
/* 第二种方法,使用v- on绑定事件,子组件使用$emit方法进行触发 */
sendStudentName() {
this.$emit("wxg", this.name);
this.$emit("demo");
},
unbind(){
this.$off("wxg"); // 解绑一个自定义事件
this.$off(["wxg", "demo"]); // 解绑多个自定义事件
this.$off(); // 解绑所有自定义事件
},
},
};
</script>
<style scoped>
.student {
background-color: red;
}
</style>

全局事件总线

其目的是为了实现任意组件间的通信,先给出一个如下图所示的例子:

==⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠== You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

App ^rsOKNBNN

A ^raXgcmMX

B ^NWERw7U2

C ^YjZnoxKC

D ^2HhRrE4b

X ^g7BjquH9

demo ^gWNyyOSH

demo,666 ^33jSC4NV

test ^9pEnPyXq

test,8 ^tpg8HxMh

666 ^dIOHCXve

8 ^PtwYQC5Y

%%

Drawing

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
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

h7gQAA==

%%

  1. A 给 X 绑定了一些自定义事件,回调函数 demo 在 A 中
  2. D 触发 A 所绑定的自定义事件,并携带参数,即可以实现与 A 的通信
  3. B 也给 X 绑定了一些自定义事件,回调函数 demo 在 B 中
  4. D 触发 B 所绑定的自定义事件,并携带参数,即可以实现与 B 的通信

attention: 此时要求 X 可以被所有组件看到,并且其可以调用 $emit,$off,$on 等方法

具体实现
  1. 安装全局事件总线
1
2
3
4
5
new Vue({
beforeCreate(){
Vue.prototype.$bus=this //安装全局事件总线,$bus就是当前应用的vm
},
})
  1. 使用事件总线:
    1. 接收数据:A 组件想接收数据,则在 A 组件中给 $bus 绑定自定义事件,事件的回调留在 A 组件自身。
    2. 提供数据:this.$bus.$emit('事件名',数据)
1
2
3
4
5
6
7
methods(){
demo(data){......}
}
......
mounted(){
this.$bus.$on('事件名',回调函数)
}
示例
  • main.js
1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
beforeCreate() {
// 安装全局事件总线
Vue.prototype.$bus = this
},
}).$mount('#app')
  • School.vue
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
<template>
<!-- 组件的结构 -->
<div class="school">
<h2>学校名称:{{ name }}</h2>
<h2>学校地址:{{ address }}</h2>
</div>
</template>
<script>
export default {
name: "School-item",
data() {
return {
name: "万星阁",
address: "众星之上",
age: 10
};
},
mounted() {
this.$bus.$on("student-item-event", (msg) => {
console.log("School 收到了:", msg);
});
},
beforeDestroy() {
// 销毁之前解绑事件
this.$bus.$off("student-item-event");
},
};
</script>
<style scoped>
.school{
background-color: blueviolet;
padding: 5px;
}
</style>
  • Student.vue
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
<template>
<!-- 组件的结构 -->
<div class="student">
<h2>学生姓名:{{ name }}</h2>
<h2>学生性别:{{ sex }}</h2>
<button @click="sendMsgToSchool">发送给School信息</button>
</div>
</template>
<script>
export default {
name: "Student-item",
data() {
return {
msg:"are you ok",
name: "牧星",
sex: "男",
};
},
methods: {
sendMsgToSchool() {
this.$bus.$emit("student-item-event", this.name);
}
},
};
</script>
<style scoped>
.student {
background-color: red;
padding: 5px;
margin-top: 30px;
}
</style>

消息订阅与发布

原生 JS 没有这些功能,可以借助 pubsub-js 之类的库实现,首先执行 npm i pubsub-js 命令进行安装,而后依次进行消息的订阅,消息的发布以及消息的解绑即可,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* 1. 导入库 */
import { PubSub } from "pubsub-js";
/* 2. 订阅以及解绑消息 */
mounted() {
// 订阅消息,并指定回调函数
this.wxgId=PubSub.subscribe("wxg", (msg,data) => {
console.log("wxg msg is published",msg,data);
});
},
beforeDestroy() {
// 取消订阅
PubSub.unsubscribe(this.wxgId);
},
/* 3. 发布消息 */
methods: {
sendMsgToSchool() {
PubSub.publish("wxg", this.msg);
}
},

nextTick

this.$nextTick(回调函数),主要作用是在下一次 DOM 更新结束后执行其指定的回调,用于:当改变数据后,要基于更新后的新 DOM 进行某些操作时,要在 nextTick/所指定的回调函数中执行。

过渡动画

在插入、更新或移除 DOM 元素时,在合适的时候给元素添加样式类名,其大致属性如下

animate.png

  1. 准备好样式:
    • 元素进入的样式:
      1. v-enter: 进入的起点
      2. v-enter-active: 进入过程中
      3. v-enter-to: 进入的终点
    • 元素离开的样式:
      1. v-leave: 离开的起点
      2. v-leave-active: 离开过程中
      3. v-leave-to: 离开的终点
  2. 使用 <transition> 包裹要配置过渡动画的元素,并配置 name 属性:
1
2
3
<transition name="hello">
<h1 v-show="isShow">你好啊!</h1>
</transition>

note: >1. 对于 <transition> 如果制定了 name 属性,则对应的 v-enter-active 应改为 fade-enter-active
2. 若有多个元素需要过度,则需要使用:<transition-group>,且每个元素都要指定 key 值。

示例

  • 基础版
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
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition name="fade" :appear="true">
<h1 v-show="isShow" class="v-enter-active">你好呀</h1>
</transition>
</div>
</template>
<script>
export default {
name: 'Animo-item',
data() {
return {
isShow: true
}
}
}
</script>
<style scoped>
h1 {
background-color: orange;
}
.fade-enter-active {
animation: wxg 1s;
}
.fade-leave-active {
animation: wxg 1s reverse;
}
@keyframes wxg {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0px);
}
}
</style>
  • 适用版
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
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group name="fade" :appear="true">
<h1 v-show="isShow" :key="1">你好呀</h1>
<h1 v-show="isShow" :key="2">牧星</h1>
</transition-group>
</div>
</template>
<script>
export default {
name: 'Animo2-item',
data() {
return {
isShow: true
}
}
}
</script>
<style scoped>
h1 {
background-color: orange;
}
/* 进入起点,离开终点 */
.fade-enter,.fade-leave-to {
transform: translateX(-100%);
}
.fade-enter-active,
.fade-leave-active {
transition: 1s;
}
/* 进入终点,离开起点 */
.fade-enter-to,.fade-leave {
transform: translateX(0px);
}
</style>
  • 外部库版
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
<template>
<div>
<button @click="isShow = !isShow">显示/隐藏</button>
<transition-group appear name="animate__animated animate__bounce" enter-active-class="animate__swing"
leave-active-class="animate__backOutUp">
<h1 v-show="isShow" :key="1">你好呀</h1>
<h1 v-show="isShow" :key="2">牧星</h1>
</transition-group>
</div>
</template>
<script>
import 'animate.css'
export default {
name: 'Animo3-item',
data() {
return {
isShow: true
}
}
}
</script>
<style scoped>
h1 {
background-color: orange;
}
</style>

Ajax

脚手架配置代理

配置代理的主要目的是避免 跨域 问题,参照下述代码在 Vue.config.js 中添加配置项以设定基础代理和更专业的代理:

  1. 基础代理的特点:
    1. 优点:配置简单请求资源时直接发给前端 (8080) 即可。
    2. 缺点:不能配置多个代理,不能灵活的控制请求是否走代理
    3. 工作方式:若按照上述配置代理,当请求了前端不存在的资源时,那么该请求会转发给服务器(优先匹配前端资源)
  2. 专项代理的特点:
    1. 优点:可以配置多个代理,且可以灵活的控制请求是否走代理。
    2. 缺点:配置略微繁琐,请求资源时必须加前缀。
  • 基础代理
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
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
transpileDependencies: true,
lintOnSave: 'warning', // 关闭语法检查
// 开启基础代理服务器
devServer:{
proxy:"http://localhost:5000"
},
// 开启特定代理服务器
devServer:{
proxy:{
'/car':{
target: "http://localhost:5000",
// 允许跨域,用于控制请求头中的host值
changeOrigin: true,
// 用于支持websocket
ws: true,
// 重写路径,防止/car/car请求
pathRewrite: {
'^/car': ''
}
},
'/user': {
target: "http://localhost:5000",
changeOrigin: true,
ws: true,
pathRewrite: {
'^/user': ''
}
}
}
}
})
  • 请求数据
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
<template lang="">
<div class="app">
<h1>{{msg}}</h1>
<button @click="fetchData()">获取学生信息</button>
</div>
</template>
<script>
import axios from "axios";
export default {
name: 'App',
components: { },
data() {
return {
msg: 'Hello Vue',
}
},
methods: {
fetchData() {
axios.get("http://127.0.0.1:8080/car/car").then(
response => {
console.log("请求成功",response.data);
},
error => {
console.log("请求失败",error.message);
}
)
}
},
}
</script>

Vue-resource

原本是 Vue 官方维护的一个 Vue 插件,用于处理网络请求,在 1.0 版本中使用较多,现在交给了其他团队维护,使用较少,引入插件之后只需要将 axios 换为 this.$http 即可

1
2
3
4
5
6
7
8
9
10
11
12
// 安装插件
import VueResource from 'vue-resource'
Vue.use(VueResource)
// 网络请求
this.$http.get("http://127.0.0.1:8080/car/car").then(
response => {
console.log("请求成功",response.data);
},
error => {
console.log("请求失败",error.message);
}
)

插槽

父组件以向子组件指定位置插入 html 结构,也是一种组件间通信的方式,适用于 父组件=>子组件

默认插槽

示例
  • Category.vue
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
<template>
<div class="category">
<h3>{{title}}</h3>
<!-- 定义一个插槽,等待使用者填充 -->
<slot>默认值,未传值时会显示</slot>
</div>
</template>
<script>
export default {
name: 'Category-item',
props:['listData','title'],
data() {
return{
}
},
}
</script>
<style>
.category {
background-color: skyblue;
width: 200px;
height: 300px;
}
h3{
text-align: center;
background-color: orange;
}
</style>
  • App.vue
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
<template>
<div class="container">
<Category title="美食">
<img src="https://wanstarge.github.io/images/mx.jpg" alt="">
</Category>
<Category title="游戏" :listData="games">
<ul>
<li v-for="(item, index) in games" :key="index">{{ item }}</li>
</ul>
</Category>
<Category title="电影">
<video controls src="./assets/videos/yar.mp4"></video>
</Category>
</div>
</template>
<script>
import Category from "./components/Category";
export default {
name: 'App',
components: { Category },
data() {
return {
foods: ['火锅', '烧烤', '小龙虾', '牛排'],
games: ['红色警戒', '穿越火线', '劲舞团', '超级玛丽'],
films: ['《教父》', '《拆弹专家》', '《你好,李焕英》', '《万星阁》']
}
},
}
</script>
<style >
.container{
display: flex;
justify-content: space-around;
}
img{
width: 100%;
}
video{
width: 100%;
}
</style>

具名插槽

和上述默认插槽类似,指定名称即可,示例如下:

  • Category.vue
1
2
3
4
5
6
7
8
<template>
<div class="category">
<h3>{{title}}</h3>
<!-- 定义一个插槽,等待使用者填充 -->
<slot name="center">默认值,未传值时会显示</slot>
<slot name="footer">默认值,未传值时会显示</slot>
</div>
</template>
  • App.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<Category title="游戏" :listData="games">
<ul slot="center">
<li v-for="(item, index) in games" :key="index">{{ item }}</li>
</ul>
<a slot="footer" href="http://www.baidu.com">百度搜一下</a>
</Category>
<Category title="电影">
<video slot="center" controls src="./assets/videos/yar.mp4"></video>
<template v-slot:footer> <!-- 新版写法 -->
<div class="foot">
<a href="https://btnull.net">来片库看看</a>
<a href="https://iqiyi.com">推荐</a>
</div>
</template>
</Category>
</template>

作用域插槽

数据在组件的自身,但根据数据生成的结构需要组件的使用者来决定,(games 数据在 Category 组件中,但使用数据所遍历出来的结构由 App 组件决定

  • Category.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div class="category">
<h3>{{title}}</h3>
<slot :games="games">我是默认的一些内容</slot>
</div>
</template>
<script>
export default {
name: 'Category-item',
props:['listData','title'],
data() {
return{
games: ['红色警戒', '穿越火线', '劲舞团', '超级玛丽'],
}
},
}
</script>
  • App.vue
1
2
3
4
5
6
7
8
9
10
11
<template>
<div class="container">
<Category :games="games" title="游戏">
<template slot-scope="wxg">
<ul>
<li v-for="(game, index) in wxg.games" :key="index">{{ game }}</li>
</ul>
</template>
</Category>
</div>
</template>

vuex

在 Vue 中实现集中式状态(数据)管理的一个 vue 插件,对 vue 应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。可以用于实现多组件共享数据,前面说到的 全局事件总线 同样可以实现,如下图所示:

globalBus.png

使用 vuex 实现如下图所示:

vuex.png

原理分析

vuexProcess.png

首先 Actions,Mutations,State 都是一个结构体,流程图以顺时针方向直行:

  1. 首先由组件分发一个消息,到 Actions 中去查询对应的函数(注:如果函数的参数要从其他渠道获得,可以在此处执行代码,如果参数已知可以跳过改步骤)
  2. Actions 将查询到的函数递交给 Mutations 中去查询,Mutations 中如果查村到了对应的函数,就执行该函数并更新 State
  3. State 中的数据会渲染到组件中

note: Actions 和 Mutations,State 都被 Store 所管理

搭建环境

  1. 创建文件: src/store/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 该文件中,用于创建Vuex中最为核心的Store对象,并将其挂载到Vue实例的$store上。
// 引入Vuex
import Vuex from 'vuex'
import Vue from "vue";
//准备actions-一用于响应组件中的动作
const actions = {}
//准备mutations-一用于操作数据(state)
const mutations = {}
//准备state一用于存储数据
const state = {}
Vue.use(Vuex)
// 创建并暴露store对象
export default new Vuex.Store({
actions,
mutations,
state
})
  1. 在 main.js 中创建 vm 是传入 store 配置项
1
2
3
4
5
6
7
8
9
import Vue from 'vue'
import App from './App.vue'
import store from "./store/index";
Vue.config.productionTip = false

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

基础使用

  1. 在 index.js 中准备好将被调用的任何函数,在组件中调用对应的函数
  2. 组件中读取 vuex 中的数据:$store.state.num
  3. 组件中修改 vuex 中的数据:$store.dispatch('actiont中的方法名',数据)
    $store.commit('mutations中的方法名',数据)

note: 若没有网络请求或其他业务逻辑,组件中也可以越过 actions,即不写 dispatch,直接编写 commit

  • index.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
25
26
27
28
29
30
31
32
33
34
35
36
37
// 该文件中,用于创建Vuex中最为核心的Store对象,并将其挂载到Vue实例的$store上。
// 引入Vuex
import Vuex from 'vuex'
import Vue from "vue";
//准备actions-一用于响应组件中的动作
const actions = {
jiaOdd(context, value){
if (context.state.sum % 2) {
context.commit('JIA', value)
}
},
jiaWait(context,value){
setTimeout(() => {
context.commit('JIA', value)
}, 500);
}
}
//准备mutations-一用于操作数据(state)
const mutations = {
JIA(state,value) {
state.sum += value
},
JIAN(state, value) {
state.sum -= value
}
}
//准备state一用于存储数据
const state = {
sum: 0,
}
Vue.use(Vuex)
// 创建并暴露store对象
export default new Vuex.Store({
actions,
mutations,
state
})
  • count.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<script>
export default {
name: 'Count-item',
data() {
return {
n: 1,
}
},
methods: {
increment() {
this.$store.commit('JIA',this.n)
},
decrement() {
this.$store.commit('JIAN', this.n)
},
incrementOdd() {
this.$store.dispatch('jiaOdd', this.n)
},
incrementWait() {
this.$store.dispatch('jiaWait', this.n)
}
},
}
</script>

getters

  1. 当 state 中的数据需要经过加工后再使用时,可以使用 getters 加工
  2. store.js 中追加 getters 配置
1
2
3
4
5
6
7
8
9
const getters={
bigSum(state){
return state.sum * 10
}
}
export default new Vuex.Store({
......
getters
})
  1. 组件中读取对应的数据:$store.getters.bigSum

note: 类似于 vm 中的 datacomputed

map 方法

为了避免复杂的 $store.state 或者 this.$store 写法

  1. mapState 方法: 用于帮助映射 state 中的数据为计算属性
1
2
3
4
5
6
computed: {
// 借助mapState生成计算属性,sum,school,subject(对象写法)
...mapState({'sum':'sum', 'school':'school', 'subject':'subject'}),
// 借助mapState生成计算属性,sum,school,subject(数组写法)
...mapState(['sum', 'school', 'subject' ]),
}
  1. mapGetters 方法: 用于帮助我们映射 getters 中的数据为计算属性
1
2
3
4
5
6
computed: {
// 借助mapGetters生成计算属性,bigSum(对象写法)
...mapGetters({'sum':'sum', 'school':'school', 'subject':'subject'}),
// 借助mapGetters生成计算属性,bigSum(数组写法)
...mapGetters(['sum', 'school', 'subject' ]),
}
  1. mapActions 方法: 用于帮助我们生成与 actions 对话的方法,即:包含 $store.dispatch(xxx) 的函数

note: 注意此时默认传递的参数为 value 也就是 event,在组件调用时应当指明参数,比如 @click="inecermentOdd(n)"

1
2
3
4
5
6
methods: {
// 借助mapActions生成计算属性,incrementOdd,incrementWait(对象写法)
...mapActions({'incrementOdd':'jiaOdd', 'incrementWait':'jiaWait'}),
// 借助mapActions生成计算属性,incrementOdd,incrementWait(数组写法)
...mapActions(['jiaOdd', 'jiaWait']),
}
  1. mapMutations 方法: 用于帮助我们生成与 mutations 对话的方法,即:包含 $store.commit(xxx) 的函数
1
2
3
4
5
6
methods: {
// 借助mapGetters生成计算属性,bigSum(对象写法)
...mapActions({'increment':'JIA', 'decrement':'JIAN'}),
// 借助mapGetters生成计算属性,bigSum(数组写法)
...mapActions(['JIA', 'JIAN']),
}

模块化

  1. 在 store 对应的 index.js 中以模块化的形式创建对应的配置项,并在其中写入对应的函数及变量,而后使用 mmodules 属性进行挂载
  2. 在 Vue 文件中使用上述提到的 map 方法时,可以直接使用 ...mapState(['cntOptions','perOptions']) 来读取,而后在组件中使用 cntOptions.变量名 来插值绑定数据,也可以在创建配置项时指定成员属性 namespaced:true 来启用命名空间,这样就可以使用 ...mapState('perOptions', ['personList']), 来读取,从而直接使用变量名来绑定插值数据
  3. 直接通过 this.$store 访问
    • 使用 this.$store.commit("方法名",...) 来触发事件的可以改为 this.$store.commit("模块名/方法名",...) 来进行触发
    • 对于使用 this.$store.state.变量名 来读取变量的方式应当修改为 this.$store.模块名/变量名 来读取数据
    • 对于通过 this.$store.getters.属性名 访问数据的可以修改为 this.$store.getters["模块名/属性名"] 来进行访问

note: 模块化编程的好处,可以将各个数据和对应的操作分开保存在不同的文件中

  • index.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
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
const cntOptions={
namespaced:true,
actions:{
jiaOdd(context, value) {
if (context.state.sum % 2) {
context.commit('JIA', value)
}
},
jiaWait(context, value) {
setTimeout(() => {
context.commit('JIA', value)
}, 500);
}
},
mutations:{
JIA(state, value) {
state.sum += value
},
JIAN(state, value) {
state.sum -= value
}
},
state:{
sum: 0,
school: '万星阁',
subject: '全栈',
},
getters:{
bigSum(state) {
return state.sum * 10
}
}
}
const perOptions={
namespaced: true,
actions:{},
mutations:{},
state:{
personList: [
{ id: '001', name: '张三' }
]
},
getters:{}
}
// 创建并暴露store对象
export default new Vuex.Store({
modules: {
cntOptions,
perOptions
}
})
  • Count.vue
1
2
3
4
5
6
7
8
9
methods: {
...mapMutations('cntOptions',{ 'increment': 'JIA', 'decrement': 'JIAN' }),
...mapActions('cntOptions',{ 'incrementOdd': 'jiaOdd', 'incrementWait': 'jiaWait' }),
},
computed: {
...mapState('cntOptions',['sum', 'school', 'subject','personList' ]),
...mapState('perOptions', ['personList']),
...mapGetters('cntOptions',['bigSum'])
}

路由

路由就是一组 key-value 的(key 为路径,value 可能是 function 或 component)对应关系,多个路由,需要经过路由器的管理,编程中的路由往往是为了为实现单页 Web 应用 (SPA, single page web application) :

  • 整个应用只有一个完整的页面。
  • 点击页面中的导航链接不会刷新页面,只会做页面的局部更新。
  • 数据需要通过 ajax 请求获取。

vue 中路由的实现依靠 vue-router 这个插件(Vue2 使用 3 版本)来实现,路由又分为前端路由和后端路由:

  • 后端路由
    • 理解:value 是 function,用于处理客户端提交的请求
    • 工作过程:服务器接收到一个请求时,根据请求路径找到匹配的函数来处理请求,返回响应数据。
  • 前端路由
    • 理解:value 是 component,用于展示页面内容
    • 工作过程:当浏览器的路径改变时,对应的组件就会显示

基础使用

  1. 安装 vue-router: npm install vue-router@3
  2. src/router/index.js 文件中编写 router 配置项
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//该文件专门用于创建整个应用的路由器
import Vue from 'vue'
import VueRouter from "vue-router";
import Home from "../components/Home";
import About from '../components/About';

// 安装路由器
Vue.use(VueRouter)
// 创建并暴露一个路由器
export default new VueRouter({
routes: [
{
path: '/about',
component: About,
},
{
path: '/home',
component: Home,
}
]
})
  1. 实现切换 (active-class 可配置高亮样式){html}<router-link class="list-group-item" active-class="active" to="/home">Home</router-link>
  2. 指定显示位置:{html}<router-view></router-view>

attention: >5. 路由组件通常存放在 pages 文件夹,一般组件通常存放在 components 文件夹。
6. 通过切换,“隐藏了的路由组件,默认是被销毁掉的,需要的时候再去挂载。
7. 每个组件都有自己的 $route 属性,里面存储着自己的路由信息。
8. 整个应用只有一个 router,可以通过组件的 $router 属性获取到。

嵌套(多级)路由

  1. 配置多级子路由规则
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export default new VueRouter({
routes: [
{
path: '/about',
component: About,
},
{
path: '/home',
component: Home,
children: [
{
path: 'news',
component: News
},
{
path: 'message',
component: Message
}
]
}
]
})
  1. 在对应父组件 (此处为 Home.vue) 中进行调用:{html}<router-link class="list-group-item" active-class="active" to="/home/news">新闻</router-link>
  2. 同样使用 {html}<router-view></router-view> 标签进行展示

attention: >4. 子路由配置时不要写 /
5. 标签跳转使用时,要带上父路径

路由传参

query 参数

  1. 传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- 跳转路由并携带query参数,to的字符串写法 -->
<router-link
:to="`/home/message/detail?id=${message.id}&title=${message.title}`">{{message.title}}</router-link>
<!-- 跳转路由并携带query参数,to的对象写法 -->
<router-link
:to="{
path:'/home/message/detail',
query:{
id:message.id,
title:message.title
}
}"
>{{ message.title }}
</router-link>
  1. 接收参数
1
2
$route.query.id
$route.query.title

命名路由

可以简化路由的跳转

  1. 给路由命名:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
path: '/home',
component: Home,
children: [
{
path: 'news',
component: News
},
{
path: 'message',
component: Message,
children: [
{
name:'detail',
path: 'detail',
component: () => import('../pages/Detail')
},
]
}
]
}
  1. 简化跳转
1
2
3
4
5
6
7
8
9
10
11
12
13
<router-link
:to="{
// 简化后通过名字直接跳转
name:'detail',
// 简化前通过路径跳转
path:'/home/message/detail',
query:{
id:message.id,
title:message.title
}
}"
>{{ message.title }}
</router-link>

params 参数

  1. 配置路由,声明接收 params 参数
1
2
3
4
5
6
7
8
9
10
11
{
path: 'message',
component: Message,
children: [
{
name:'detail',
path: 'detail/:id/:title',// 使用占位符声明接收params参数
component: () => import('../pages/Detail')
},
]
}
  1. 传递参数
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 跳转路由并携带params参数,to的字符串写法 -->
<router-link
:to="`/home/message/detail/${message.id}/${message.title}`">{{message.title}}
</router-link>
<!-- 跳转路由并携带params参数,to的对象写法 -->
<router-link :to="{
name: 'detail',
params: {
id: message.id,
title: message.title
}
}">{{ message.title }}
</router-link>

attention: 此时若使用对象写法,则必须要求使用 name 属性,也就是上方提到的命名路由方法,而不能使用 path 属性

props

可以让路由组件更方便地接收参数,有对象写法,布尔写法,函数写法,最为常用的是函数写法,示例配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
name:'detail',
path: 'detail',
component: () => import('../pages/Detail'),
// 对象写法,该对象中的所有key-value对都会作为props传递给组件,固定数据
props: {
test: 'test'
},
// 布尔写法,若布尔值为真,就会把该路由组件收到的所有params参数,以props的形式传给Detail组件。
props: true,
// 函数写法(推荐),该函数会返回一个对象,该对象中的所有key-value对都会作为props传递给组件。
props($route){
return{
id: $route.query.id,
title: $route.query.title
}
}
},

在组件中使用 props:['id','title'] 等来接收数据

replace 模式

和浏览器的网址切换模式有关,基础的模式是栈模式 (push),即每路由到一个新的地址,向栈中压入一条现有的网址信息;可以通过在 {html}<route-link> 标签中指定 replace 属性为真来切换为替换模式,即直接将栈顶的网址替换

1
2
3
<router-link :replace=true class="list-group-item" active-class="active" to="/about">About</router-link>
<!--或者简单点-->
<router-link replace class="list-group-item" active-class="active" to="/about">About</router-link>

编程式路由

也就是不使用指定的 {html}<route-link> 标签,可以通过自指定函数实现跳转的路由方法

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
<template>
<button @click="pushShow(message)">push查看</button>
<button @click="replaceShow(message)">replace查看</button>
<button @click="back">后退</button>
<button @click="forward">前进</button>
</template>
<script>
methods: {
pushShow(message){
this.$router.push({
name: 'detail',
query: {
id: message.id,
title: message.title
}
}, () => { })
},
replaceShow(message){
this.$router.replace({
name: 'detail',
query: {
id: message.id,
title: message.title
}
}, () => { })
},
back(){
this.$router.back()
},
forward(){
this.$router.forward()
}
},
</script>

还有一个 this.$router.go() 方法在其中传入正数表示前进多少步,负数则是后退多少步

缓存路由

防止切换时组件被销毁,输入的信息需要重新输入,这时可以使用缓存路由,在需要缓存的组件展示区外包裹 <keepalive></keepalive> 标签,include 属性中指定缓存哪些组件(注:此时要求为组件名,即在编写组件时 name 属性指定的名称)

1
2
3
4
5
6
7
<keep-alive include="News-item">
<router-view></router-view>
</keep-alive>
<!--缓存多个-->
<keep-alive include="News-item,Message-item">
<router-view></router-view>
</keep-alive>

生命周期

作用:路由组件所独有的两个钩子,用于捕获路由组件的激活状态。

  • activated: 路由组件被激活时发。
  • deactivated: 路由组件失活时发。

路由守卫

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
// 全局前置路由守卫--初始化的时候被调用,每次路由切换之前被调用
router.beforeEach((to, from, next) => {
if (to.meta.isAuth) {
if (localStorage.getItem('school')==='wxg') {
next()
}else{
alert('无权限')
}
}else{
next()
}
})
// 全局后置守卫--每次路由切换之后被调用
router.afterEach((to, from) => {
if (from.path === '/home/news') {
console.log('全局后置守卫')
}
})
// 独享守卫
{
name: 'news',
path: 'news',
component: News,
beforeEnter: (to, from, next) => {
if (localStorage.getItem('school') === 'wxg') {
next()
} else {
alert('无权限')
}
}
},
  1. 组件内路由守卫
1
2
3
4
5
6
7
8
// 进入守卫,通过路由规则,进入该组件时被调用
beforeRouteEnter (to, from, next) {
next()
},
// 离开守卫,通过路由规则,离开该组件时被调用
beforeRouteLeave (to, from, next) {
next()
}

History 模式

对于一个 url 来说,#及其后面的内容就是 hash 值,hash 值不会包含在 HTTP 请求中,即:hash 值不会带给服务器

在路由的配置文件中与 routes 并列设置一个 mode 属性,在其中可以指定下列两种模式:

  • hash 模式:
    • 网址路径中会包含 /#/,不美观
    • 若以后将地址通过第三方手机 app 分享,若 app 校验严格,则地址会被标 i 记为不合法
    • 兼容性较好
  • history 模式:
    • 网址路径中不包含 /#/,干净美观
    • 兼容性和 hash 模式相比略差
    • 应用部署上线时需要后端人员支持,解决刷新页面服务端 404 的问题,在 nodejs 中参考 history模式解决
1
2
3
4
const router = new VueRouter({
mode: history,
routes: [...],
})

UI

ElementUI

elementui 默认是全部引入,为了按需引入需要使用 babel-plugin-component 插件来实现

  • 按需引入
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset',
["@babel/preset-env", { "modules": false }]
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
]
]
}
  • 组件注册
1
2
3
import { Button, Row } from 'element-ui';
Vue.component(Button.name, Button);
Vue.component(Row.name, Row);