Vue基础笔记

源码

【复习本篇笔记时一边看源码】

https://github.com/rockdunteng/rockdunteng.github.io/tree/master/note/vue/%E6%B8%A1%E4%B8%80%E6%96%B0Vue%E6%BA%90%E7%A0%81


一、Vue前导

Vue的优点:

  1. 减少dom操作,性能更好
  2. 视图、数据分离
  3. 维护成本低

点击该pdf查看详情




二、数据绑定与视图渲染

使用“Mustache”语法 (双花括号)插值表达式渲染视图。

1564215958637



数据要先存在,才能实现数据绑定

在下图所示中,可以通过vm.name的方式进行数据的获取和修改,数据修改的同时,视图也随之改变,如本例中的name的值由jimmy变成了tony,视图立即随之改变。但是当vm.obj.xx=12时,虽然vm.obj.xx成功创建了,但是视图没有随之渲染。

因为在 Vue 中,数据要先存在,才能实现数据绑定,从而渲染视图

1564212932741



通过索引的方式修改数组,或通过修改数组长度的方式修改数组,都无法重新渲染视图

1564213382860



数组变异方法

发现使用数组方法时,就会触发视图重新渲染。

Vue的数据绑定是通过数据劫持来实现的,在数据劫持的时候通过索引修改数组或者通过修改数组长度来修改数组是无法被感知的,Vue在设计的时候通过修改数组的一些方法,在这些方法里面添加了一个更新视图的方法的执行,所以当我们调用这些数组的方法的时候可以触发更新视图方法,从而更新视图。这些数组的方法在vue中被称为数组变异方法。这些方法已经不是原生的 js 方法了,是被 Vue 改写了,让它能有一个重新渲染视图的功能。

1564214165792



Vue 有以下7中变异方法

  • push() 往数组最后面添加一个元素,成功返回当前数组的长度
  • pop() 删除数组的最后一个元素,成功返回删除元素的值
  • shift() 删除数组的第一个元素,成功返回删除元素的值
  • unshift() 往数组最前面添加一个元素,成功返回当前数组的长度
  • splice() 有三个参数,第一个是想要删除的元素的下标(必选),第二个是想要删除的个数(必选), 第三个是删除后想要在原位置替换的值(可选)
  • sort() 使数组按照字符编码默认从小到大排序,成功返回排序后的数组
  • reverse() 将数组倒序,成功返回倒序后的数组



$set方法

$set方法可以解决对象中未存在数据的数据绑定问题

可以实现视图的实时更新。

1564216195094



$el

Vue实例vm中有一个 $el 属性,它能指向挂载 Vue 的Dom结构。

1564217181273

1564217272687


上图一共修改了 name 三次,修改了 age 两次,最后一行代码是打印 $el.innerText,但是这五次的数据修改是异步的,结果就是是先打印了一开始的 $el.innerText:

1564217455589


这样导致我们无法打印获取到数据更改后的$el.innerText,如何解决,看下面的 $nextTick



$nextTick

1564217936576

它会在数据都修改完成并且重新渲染到页面上之后才执行里面的函数。这样就能获取到数据更改后的dom。



$mount

创建 Vue 实例的时候可以不写 el: '#app', 使用 $mount也能实现同样的效果。

1564218209720



$refs

在元素标签上使用ref来标记,然后通过$refs来获取到标记的引用:

1567579888756


注意的是,不能重复标记相同名称,相同名称的 ref 只会覆盖为最后一个:

1567580190773


但是,如果是用 v-for 的方式来循环标记,则不会发生覆盖,会得到一个数组:

1567580308732

ref 除了可以标记 dom 元素,还可以标记组件。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例。下文讲组件传值时会讲到。




三、Vue 指令

v-pre、v-cloak和v-once指令

v-pre指令

在模板中跳过vue的编译,直接输出原始值。就是在标签中加入v-pre就不会输出vue中的data值了。

1
<div v-pre>{{message}}</div>

这时并不会输出我们的message值,而是直接在网页中显示

v-cloak指令

在vue渲染完指定的整个DOM后才进行显示。它必须和CSS样式一起使用,

1
2
3
4
5
6
[v-cloak] {
display: none;
}
<div v-cloak>
{{ message }}
</div>


v-once指令

在第一次DOM时进行渲染,渲染完成后视为静态内容,跳出以后的渲染过程。


其他常用指令

还有 v-html、v-text、v-if、v-else-if、v-else、v-show等,具体见旧版笔记。



xss攻击

讲到 v-html 的时候,提到了XSS攻击,了解一下。

https://github.com/lynnic26/LynnNote/issues/1



v-bind和v-on

<img v-bind:src="imgUrl" alt="">中的v-bind可以简写为<img :src="imgUrl" alt="">



绑定class

  1. 使用 :class 来绑定类名:

    1
    2
    3
    4
    5
    <style>
    .red {
    color: red
    }
    </style>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <div id="app">
    <p :class='redClass'>peace!</p>
    </div>

    <script>
    var vm = new Vue({
    data: {
    redClass: 'red',
    }
    })

    vm.$mount('#app')

    </script>

    这样 p 标签就添加上了一个名为 red 的类了。

  1. 结合数组方式绑定类名:

    如果想要给一个标签绑定多个类名,

    1564384342505

    正确方式是使用【数组】:

    1
    2
    3
    <div id="app">
    <p :class="[redClass, bigClass]">peace!</p>
    </div>

​ 还可以在数组中进行判断运算:

1564385335421


  1. 结合对象绑定class:

    1564386032576

    也可以这样:

    1564386210997



绑定style

将样式写进一个对象花括号里面,注意键值对中的值要用引号包起来。

1
<p :style="{width:'100px',height:'200px',backgroundColor:'red'}"></p>

也可以在data中写样式对象,然后用:style绑定,这样比较清晰:

1564386818920

如果想要给一个标签绑定上多个样式对象,需要使用数组:

1564387087174

注意一点是在一个标签内部,style的权重低于:style的权重,先执行style的样式,然后再执行:style的样式,遇到相同的backgroundColor:style的样式会覆盖style的。

1564387434614

p 的背景颜色为red



v-for



v-model

实现双向数据绑定

可以实现 v-model 的表单元素:

  • input text
  • input checkbox
  • input radio
  • textarea
  • select

【这部分看一下源码】




四、自定义键盘修饰符

2.x中自定义键盘修饰符

  1. 通过Vue.config.keyCodes.名称 = 按键值来自定义案件修饰符的别名:
1
Vue.config.keyCodes.f2 = 113;
  1. 使用自定义的按键修饰符:
1
<input type="text" v-model="name" @keyup.f2="add">




五、directive自定义指令

  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
26
27
28
29
30
31
32
33
34
35
// 自定义全局指令 v-focus,为绑定的元素自动获取焦点:

Vue.directive('focus', {

inserted: function (el) { // inserted 表示被绑定元素插入父节点时调用

el.focus();

}

});



// 自定义局部指令 v-color 和 v-font-weight,为绑定的元素设置指定的字体颜色 和 字体粗细:

directives: {

color: { // 为元素设置指定的字体颜色

bind(el, binding) {

el.style.color = binding.value;

}

},

'font-weight': function (el, binding2) { // 自定义指令的简写形式,等同于定义了 bind 和 update 两个钩子函数

el.style.fontWeight = binding2.value;

}

}
  1. 自定义指令的使用方式:
1
<input type="text" v-model="searchName" v-focus v-color="'red'" v-font-weight="900">

【渡一案例见源码和视频】

F:\渡一前端课\新录制的vue课程资料\9. directive\vue.html




六、filter过滤器

概念:Vue.js 允许你自定义过滤器,可被用作一些常见的文本格式化。过滤器可以用在两个地方:mustache 插值和 v-bind 表达式。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符指示;

私有过滤器

  1. HTML元素:
1
<td>{{item.ctime | dataFormat('yyyy-mm-dd')}}</td>
  1. 私有 filters 定义方式:
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
filters: { // 私有局部过滤器,只能在 当前 VM 对象所控制的 View 区域进行使用

dataFormat(input, pattern = "") { // 在参数列表中 通过 pattern="" 来指定形参默认值,防止报错

var dt = new Date(input);

// 获取年月日

var y = dt.getFullYear();

var m = (dt.getMonth() + 1).toString().padStart(2, '0');

var d = dt.getDate().toString().padStart(2, '0');



// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日

// 否则,就返回 年-月-日 时:分:秒

if (pattern.toLowerCase() === 'yyyy-mm-dd') {

return `${y}-${m}-${d}`;

} else {

// 获取时分秒

var hh = dt.getHours().toString().padStart(2, '0');

var mm = dt.getMinutes().toString().padStart(2, '0');

var ss = dt.getSeconds().toString().padStart(2, '0');



return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;

}

}

}

使用ES6中的字符串新方法 String.prototype.padStart(maxLength, fillString=’’) 或 String.prototype.padEnd(maxLength, fillString=’’)来填充字符串;



全局过滤器

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
// 定义一个全局过滤器

Vue.filter('dataFormat', function (input, pattern = '') {

var dt = new Date(input);

// 获取年月日

var y = dt.getFullYear();

var m = (dt.getMonth() + 1).toString().padStart(2, '0');

var d = dt.getDate().toString().padStart(2, '0');



// 如果 传递进来的字符串类型,转为小写之后,等于 yyyy-mm-dd,那么就返回 年-月-日

// 否则,就返回 年-月-日 时:分:秒

if (pattern.toLowerCase() === 'yyyy-mm-dd') {

return `${y}-${m}-${d}`;

} else {

// 获取时分秒

var hh = dt.getHours().toString().padStart(2, '0');

var mm = dt.getMinutes().toString().padStart(2, '0');

var ss = dt.getSeconds().toString().padStart(2, '0');



return `${y}-${m}-${d} ${hh}:${mm}:${ss}`;

}

});

注意:当有局部和全局两个名称相同的过滤器时候,会以就近原则进行调用,即:局部过滤器优先于全局过滤器被调用!

【渡一案例见源码和视频】

F:\渡一前端课\新录制的vue课程资料\10. filter\vue.html




七、el、 template、render

挂载过程:

【渡一案例见源码和视频】

F:\渡一前端课\新录制的vue课程资料\11. el、template、render\vue.html




八、vue实例的生命周期

[查看大图]

  • 什么是生命周期:从Vue实例创建、运行、到销毁期间,总是伴随着各种各样的事件,这些事件,统称为生命周期!
  • 生命周期钩子:就是生命周期事件的别名而已;
  • 生命周期钩子 = 生命周期函数 = 生命周期事件
  • 主要的生命周期函数分类:

  • 创建期间的生命周期函数:

  • beforeCreate:实例刚在内存中被创建出来,此时,还没有初始化好 data 和 methods 属性

  • created:实例已经在内存中创建OK,此时 data 和 methods 已经创建OK,此时还没有开始 编译模板
  • beforeMount:此时已经完成了模板的编译,但是还没有挂载到页面中
  • mounted:此时,已经将编译好的模板,挂载到了页面指定的容器中显示

  • 运行期间的生命周期函数:

  • beforeUpdate:状态更新之前执行此函数, 此时 data 中的状态值是最新的,但是界面上显示的 数据还是旧的,因为此时还没有开始重新渲染DOM节点

  • updated:实例更新完毕之后调用此函数,此时 data 中的状态值 和 界面上显示的数据,都已经完成了更新,界面已经被重新渲染好了!

  • 销毁期间的生命周期函数:

  • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。

  • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。




九、计算属性computed和监听器watch

模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。

当我想要拼接name,looks,和age时,使用模板表达式很麻烦。直接写入describe属性中有显得代码亢余。

1
2
3
4
5
<div id="dv">
<div id="dv1">{{name}} {{age}}</div>
<div id="dv2">{{name+' '+age}}</div>
<div id="dv3">{{describe}}</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
describe: "dunteng 18"
},
methods: {

}
})
</script>

效果:

以上三种方法都可以,但是并不灵活。

应该用methods中的方法来实现:

1
2
3
4
5
<div id="dv">
<div id="dv1">{{name}} {{age}}</div>
<div id="dv2">{{name+' '+age}}</div>
<div id="dv3">{{describe()}}</div> //括号不可丢1
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
height: 180,
// describe: "dunteng 18" data中的变量命名和methods中的方法不可同名
},
methods: {
describe(){
console.log("方法describe被执行了");
return this.name+" "+this.age;
}
}
})
</script>

函数返回了 name和age这两个属性,并且渲染到页面

如此相比直接用模板语法来拼接确实简洁很多。



【这里需要知道】

在控制台上修改了这两个属性的其中一个之后,函数会被再执行一次,比如我在控制台输入vm.name = "hh"后, 页面上的dunteng 18会立即变成hh 18并再打印一次console语句。简而言之就是,当视图重新渲染了就会再执行一次该函数。另外当我们修改与describe方法无关的变量且该变量没有被渲染到试图上如height时,不会触发describe方法,但是一旦这个与describe方法无关的变量是有渲染到视图里的,则照样触发describe方法。

因此我们修改属性影响视图渲染的同时,会涉及到重新加载无关的函数的问题,如此对性能十分不友好。


解决方法一:watch

1
2
3
<div id="dv">
<div id="dv3">{{describe}}</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
height: 180,
describe: "dunteng 18"
},
watch: {
// 监听name的变化
name(){
this.describe = this.name + " " +this.age;
},
// 监听age的变化
age(){
this.describe = this.name + " " +this.age;
}
}
})
</script>

这个方式虽然解决了问题,但是并不方便,代码冗余。


解决方法二: computed

这是最优的解决方法。

1
2
3
<div id="dv">
<div id="dv3">{{describe}}</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
height: 180
},
computed: {
describe(){
return this.name + " " + this.age;
}
}
})
</script>

computed里面除了可以像上面那样写成方法(实际上并不是方法)的样子,还可以写成对象:

1
2
3
<div id="dv">
<div id="dv3">{{describe}}</div>
</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
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
height: 180
},
computed: {
// describe(){
// return this.name + " " + this.age;
// }
describe: {
//当我们获得这个describe的时候(渲染的时候)会执行get方法
get(){
return this.name + " " + this.age;
},
set(){
console.log("describe被修改了");
}
}
}
})
</script>





【注意】:上述例子中,当vm.namevm.age修改了的时候,因为有computed监听,所以在修改的同时页面的内容也相应地修改了。但是当vm.describe整个被修改的时候,页面时没有同步渲染更新的,所以还要如下操作:

1
2
3
4
<div id="dv">
<div id="dv3">{{describe}}</div>
<div id="dv4">{{height}}cm</div>
</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
<script>
const vm = new Vue({
el: "#dv",
data: {
name: "dunteng",
age: "18",
height: 180
},
computed: {
// describe(){
// return this.name + " " + this.age;
// }
describe: {
get(){
return this.name + " " + this.age;
},
set(value){
console.log("describe被修改了");
const arr = value.split(" ");
console.log(arr[0]);
console.log(arr[1]);
this.name = arr[0];
this.age = arr[1];
if(arr[0]=="大帅哥"){
this.height = 200;
}
}
}

}
})
</script>

新版渡一代码:F:\渡一前端课\新录制的vue课程资料\13. 计算属性和侦听器\vue.html

思否文章https://segmentfault.com/a/1190000012948175




十、组件

全局组件

1567510618984

这样注册了一个全局组件,名为lin-dunteng,使用时直接标签化。

也可以写成小驼峰式:linDunteng,标签化为<lin-Dunteng><lin-Dunteng>或者<lin-dunteng></lin-dunteng>,但是不能写成<linDunteng></linDunteng>

也可以写成大驼峰式:LinDunteng,标签化为<lin-Dunteng><lin-Dunteng>或者<lin-dunteng></lin-dunteng>或者<Lin-dunteng></Lin-dunteng>或者<Lin-Dunteng></Lin-Dunteng>, 但是不能写成<LinDunteng></LinDunteng>

【提醒】不过以上情况是针对.html文件而言的,之后在.vue文件中就没这么多禁忌,相对比较自由宽泛。还有一点就是在.html文件中最好使用双标签,使用单标签可能会有些bug,但是在.vue文件中就没事

【问题】:为什么组件中的data要通过函数返回对象?

答:当一个组件被定义, data 必须声明为返回一个初始数据对象的函数,因为组件可能被用来创建多个实例。如果 data 仍然是一个纯粹的对象,则所有的实例将共享引用同一个数据对象!通过提供 data 函数,每次创建一个新实例后,我们能够调用 data 函数,从而返回初始数据的一个全新副本数据对象。

其实也很好理解,大概意思就是:

类比与引用数据类型。如果不用function return 每个组件的data都是内存的同一个地址,那一个数据改变其他也改变了,这当然就不是我们想要的。用function return 其实就相当于申明了新的变量,相互独立,自然就不会有这样的问题;js在赋值object对象时,是直接一个相同的内存地址。所以为了每个组件的data独立,采用了这种方式。
如果不是组件的话,正常data的写法可以直接写一个对象,比如data:{ msg : ‘ 下载 ‘ },但由于组件是会在多个地方引用
的,JS中直接共享对象会造成引用传递,也就是说修改了msg后所有按钮的msg都会跟着修改,所以这里用function来每次返回一个对象实例。



局部组件

1567511594805

该局部组件只能在相应的el中使用



动态组件和插槽 v-slot

动态组件

关键为<component :is="comName"></component><keep-alive></keep-alive>

1567668494678



插槽

【匿名插槽】

1567668892295



【具名插槽】

1567669076852



【作用域插槽】

详见F:\渡一前端课\0我的学习\VUE\上课学习代码\Vue全家桶一\09动态组件与插槽\作用域插槽01.html

和F:\渡一前端课\0我的学习\VUE\上课学习代码\Vue全家桶一\09动态组件与插槽\作用域插槽02.html

https://ke.qq.com/webcourse/index.html#cid=288781&term_id=100342071&taid=2943852189345805&type=1024&vid=l14309p5itq



组件传值

利用属性的方式传值

组件传递普通的值

1567512737130



组件传递data中的值,父组件传值给子组件

1567513134224

以上的props都是一个数组的形式,它还可以写成一个对象的形式,而且有多个属性,看下面的例子 :

1567514346664

数组和对象这两个引用类型,default需要写成一个函数的形式。

【一个小练习】:

F:\渡一前端课\新录制的vue课程资料\15. 组件数据传递&属性校验



多层父传子通信

【场景】: 父组件要传值给子组件和孙子组件(子组件的子组件)

####【方案一】两层的父传子通信。

1567519232543



【方案二】利用 $attrs 来传值

vm.$attrs 包含是组件中未被props注册的属性

1567573718978

$attrs–继承所有的父组件属性(除了prop注册传递的属性、class 和 style ),一般用在子组件的子元素上。

在本例中 <my-p :content='$attrs.content'></my-p>得到应用。

【注意】如果$attrs继承了很多的属性,那么像上栗这样子一个一个在子组件标签中列出来很麻烦,可以直接使用

v-bind='$attrs'来一次性全部拿到$attrs中的全部属性。

1567574861249

但是我们发现,在html结构中,被props注册传递的属性不会出现,而未被props注册传递的属性会出现,如下图所示:

1567574935531

如何去除呢?

使用inheritAttrs:false!

inheritAttrs:默认值true,继承所有的父组件属性(除props的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置inheritAttrs: false,但是class属性会继承(简单的说,inheritAttrs:true 继承除props之外的所有属性;inheritAttrs:false 只继承class属性

1567575042562

像这样子写之后,就大功告成了。

1567575099573



【方案三】利用 $parent

1567576385970

对于$children,是一个数组,因为可以有多个子组件。

但是这种方法官方不推荐使用,像孙子组件myP中这样多个$parent写起来很不友好。



【方案四】利用provide和inject

1567576901607

在组件中inject拿到的属性和数据是来自provide的,而非data中的。

虽然很方便却也是不太推荐使用的, 容易使得代码混乱,不知道属性是从何而来的。但是必须知道,面试的时候可以装逼。



子组件向父组件传值

【方案一】利用 $children

1567579334058

但是这种方法官方不推荐使用。



【方案二】 利用 $refs

1567581936328

通过 $refs 获取到了子组件的数据和方法。



####【方案三】$emit

这是比较常用的方法

1567585452418

效果如下:

1567585482438

1567585581527



兄弟组件之间的传值

利用事件总线EventBus

1567588110311

【具体实现方式】:

1
2
3
var Eventbus=new Vue();    
Eventbus.$emit(事件名,数据);
Eventbus.$on(事件名,data => {});



组件双向数据通信

暂未整理




十一、Vue-cli 脚手架

安装初试

npm install -g @vue/cli 安装脚手架,用于生成项目,使用指令vue --version查看是否安装成功
npm install -g @vue/cli-service-global 快速原型开发,可以直接通过vue serve xxx.vue的指令来编译.vue文件

如果之前已经安装过旧版本(非3.x)脚手架,需先卸载旧版本:
npm uninstall vue-cli -g

如果仍然需要使用旧版本的 vue init 功能,可以全局安装一个桥接工具:
npm install -g @vue/cli-init 拉取旧版本

插件名字:Vetur(由于vscode并不认识.vue文件,所以没有代码高亮,需要安装Vetur这个插件来弥补

vue create vue-app创建一个vue项目文件夹,叫做vue-app,【注意这里的命令操作最好用cmd执行,git bash有些操作完成不了】。会提示选择一个预设的方案,有default和manually selectfeature,选择后者。然后会有一些配置需要你选择,按空格键进行选择和取消选择,这里只选择Babel。再然后选择In package.json选项,选1567834770585

  • ★ 同时注意使用redirect来重定向到具体的一个子路由路径上,否则一打开“community”路由的时候没有默认的选中子路由,只会显示空白不会显示出具体的子路由页面。
  • 在一级路由.vue文件中继续使用<router-link><router-view>

    1567834990024

  • 关于首页路径和router-link-active的优化

    在本例子中路由community的样式是加在router-link-extra-active上的,且匹配的是路径/community,因此路由community的子路由被点击时community的样式没有显现,因为此时的路径变成了/community/xxx没有完全匹配/community

    1567836335642因此要把样式加到router-link-active上。但是问题又来了,router-link-active匹配了两个路径//community

    1567836265612

    这样就应该把原本首页匹配的路径由根路径/换成/home。然后又有一个问题,就是我们访问http://localhost:8080的时候无法自动跳转到首页。所以还需要进行一步重定向:

    1567836714591

    或者还可以这样子使用redirect ( )重定向:

    1567836792711

  • 不存在的路径和优化

    比如不存在该路径http://localhost:8080/gjalhj,对此要将这一切不存在路径同意跳转到某一指定页面

    1567837033399

    这里的path: *表示当以上的所有路径都没有匹配的时候,判断路径是否为/,是则重定向到/home,否则重定向到统一的指定页面。



$router的push、replace和go

  • this.$router.push()

    描述:跳转到不同的url,但这个方法回向history栈添加一个记录,点击后退会返回到上一个页面。

    用法:

  • this.$router.replace()

    描述:同样是跳转到指定的url,但是这个方法不会向history里面添加新的记录,点击返回,会跳转到上上一个页面。上一个记录是不存在的。

  • this.$router.go(n)

    相对于当前页面向前或向后跳转多少个页面,类似 window.history.go(n)。n可为正数可为负数。正数返回上一个页面

1567838042925

-



导航守卫和动态路由

【重要的一节课】F:\渡一前端课\新录制的vue课程资料\28. 导航守卫&动态路由

动态路由

【方案一】使用params:

1567850470296

1567850528088

【方案二】使用query:

1567851075014

1567851012789



组件内的守卫

你可以在路由组件内直接定义以下路由导航守卫:

  • beforeRouteEnter
  • beforeRouteUpdate (2.2 新增)
  • beforeRouteLeave
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const Foo = {
template: `...`,
beforeRouteEnter (to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
},
beforeRouteUpdate (to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
},
beforeRouteLeave (to, from, next) {
// 导航离开该组件的对应路由时调用
// 可以访问组件实例 `this`
}
}

beforeRouteEnter 守卫 不能 访问 this,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。

不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。

1
2
3
4
5
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}

注意 beforeRouteEnter 是支持给 next 传递回调的唯一守卫。对于 beforeRouteUpdatebeforeRouteLeave 来说,this 已经可用了,所以不支持传递回调,因为没有必要了。

1
2
3
4
5
beforeRouteUpdate (to, from, next) {
// just use `this`
this.name = to.params.name
next()
}

这个离开守卫通常用来禁止用户在还未保存修改前突然离开。该导航可以通过 next(false) 来取消。

1
2
3
4
5
6
7
8
beforeRouteLeave (to, from , next) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}

【案例】实现一个功能

1567845124241

这就要用到导航守卫之beforeRouterLeave

1567845232834

相应的还有beforeRouteEnter

1567850373975

beforeRouteUpdate

★ 这个看视频【F:\渡一前端课\新录制的vue课程资料\28. 导航守卫&动态路由】



路由独享守卫

1567926980649



全局前置守卫

你可以使用 router.beforeEach 注册一个全局前置守卫:

1
2
3
4
5
const router = new VueRouter({ ... })

router.beforeEach((to, from, next) => {
// ...
})

当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

每个守卫方法接收三个参数:

  • to: Route: 即将要进入的目标 路由对象
  • from: Route: 当前导航正要离开的路由
  • next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
    • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
    • next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
    • next(‘/‘) 或者 next({ path: ‘/‘ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: truename: 'home' 之类的选项以及任何用在 router-linkto proprouter.push 中的选项。
    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。

确保要调用 next 方法,否则钩子就不会被 resolved。



全局解析守卫

2.5.0 新增

在 2.5.0+ 你可以用 router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,区别是在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。



全局后置钩子

你也可以注册全局后置钩子,然而和守卫不同的是,这些钩子不会接受 next 函数也不会改变导航本身:

1
2
3
router.afterEach((to, from) => {
// ...
})

1567928081876



导航守卫小汇总

  1. 全局守卫
  • beforeEach

  • beforeResolve

  • afterEach

  1. 路由独享守卫
  • beforeEnter
  1. 组件内守卫
  • beforeRouteLeave 当离开这个路径时执行

  • beforeRouteEnter

  • beforeRouteUpdate mounted

进入某一个路径时,执行顺序

- beforeEach -> beforeEnter -> beforeRouteEnter -> beforeResolve -> afterEach


完整的导航解析流程

  1. 导航被触发。
  2. 在失活的组件里调用离开守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 用创建好的实例调用 beforeRouteEnter 守卫中传给 next 的回调函数。



路由元信息

定义路由的时候可以配置 meta 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})

那么如何访问这个 meta 字段呢?

首先,我们称呼 routes 配置中的每个路由对象为 路由记录。路由记录可以是嵌套的,因此,当一个路由匹配成功后,他可能匹配多个路由记录

例如,根据上面的路由配置,/foo/bar 这个 URL 将会匹配父路由记录以及子路由记录。

一个路由匹配到的所有路由记录会暴露为 $route 对象 (还有在导航守卫中的路由对象) 的 $route.matched 数组。因此,我们需要遍历 $route.matched 来检查路由记录中的 meta 字段。

下面例子展示在全局导航守卫中检查元字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!auth.loggedIn()) {
next({
path: '/login',
query: { redirect: to.fullPath }
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})

【案例】实现用户登录后才有查看内容权限,仍然基于上面的程序,要求用户登录后才可以查看“学员展示”和”社区“

★★★ F:\渡一前端课\新录制的vue课程资料\30. 路由元信息




十三、Vuex

state、getters、mutations、actions

详见我的完整笔记:

https://github.com/Dunteng/learnVue/tree/master/%E5%AD%A6%E4%B9%A0Vuex#%E6%9C%AC%E6%96%87%E4%BB%A3%E7%A0%81



vuex module

【F:\渡一前端课\新录制的vue课程资料\33. vuex mutation&action\src】

1567958712382

1567958795682

注意到在此,使用的 vuex state都是存放关于 “学员展示” 模块的,一旦其他板块如 “课程学习” “社区” 等也需要用到 vuex , 那么一大堆的数据都堆积到state中,这样就很庞大很杂乱,不好维护和管理,因此需要分模块用到 vuex module

★★★★★ 看代码【F:\渡一前端课\新录制的vue课程资料\34. vuex module\src】

modules

  • 根据功能让vuex分出模块

  • state会放入到每一个模块下,getters、mutations、actions会直接放入到全局

    获取vuex中的数据(无namespaced)

  • 获取state : this.$store.state.moduleName.xxx

  • 获取getters: this.$store.getters.xxx

  • 获取mutations: this.$store.commit(‘xxx’)

  • 获取actions: this.$store.dispatch(‘xxx’)

  • 可以通过mapXXX 方式拿到getters、mutations、action,但是不能拿到state,如果想通过这种方式获取state,需要加命名空间:namespaced:true

    获取vuex中的数据(有namespaced)

  • 获取state : this.$store.state.moduleName.xxx

  • 获取getters: this.$store[‘moduleName/getters’].xxx

  • 获取mutations: this.$store.commit(‘moduleName/xxx’)

  • 获取actions: this.$store.dispatch(‘moduleName/xxx’)

  • 可以通过mapXXX: mapXXX(‘moduleName’, [‘xxx’]) mapXXX(‘moduleName’, {})




十四、跨域

十五、网络请求axios

参考:

axios 是 Vue 作者尤雨溪推荐的官方 ajax 库。有一下主要功能特点:

  • 在浏览器中发送 XMLHttpRequest 请求
  • 在 node.js 中发送 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转化请求和响应数据


先安装:

1
npm install axios --save

再引入:

1
import axios from 'axios'

发送请求:

1
2
3
4
5
axios({
url: "http://123.207.32.32:8000/home/multidata"
}).then(res => {
console.log(res);
})

以上没有指定 GET 请求还是 POST 请求,只传了一个 url ,默认为 GET 请求。


Example

执行 GET 请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 为给定 ID 的 user 创建请求
axios.get('/user?ID=12345')
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

// 可选地,上面的请求可以这样做
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

执行 POST 请求

1
2
3
4
5
6
7
8
9
10
axios.post('/user', {
firstName: 'Fred',
lastName: 'Flintstone'
})
.then(function (response) {
console.log(response);
})
.catch(function (error) {
console.log(error);
});

执行多个并发请求

1
2
3
4
5
6
7
8
9
10
11
12
13
function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()])
.then(axios.spread((res1, res2)=>{
console.log(res1);
console.log(res2);
});

1568962541144


全局配置

看下面的例子

1568962967595

这里面的 baseURL 和 timeout 存在冗余,可以将其再全局配置中写好。事实上,在开发中可能很多参数都是固定的,这时候我们可以进行一些抽取,也可以利用axios的全局配置。

1568963218053

类似的还有

1
axios.defaults.headers.post['Content-Type']='application/x-www-form-urlencoded'

还有其他配置选项,我就不一一列出来了。


axios 的实例使用

像上面的几个例子都是直接使用 axios ,但是有时候这样子直接使用 axios 是不合适的。

比如,我们的项目并非只是在 baseURL=“http://aaaa:8000”,而是还有多个 baseURL,那么这时候如果直接用 axios 来配置 baseURL 和其他的相关配置,就实现不了了,所以可以使用 axios 的实例,使用实例就可以避免这样的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 创建一个axios的实例 instance1
const instance1 = axios.create({
baseURL: 'http://123.123.123.12:8000',
timeout: 5000
})
// 使用instance1实例
instance1({
url: '/home/multidata'
}).then(res => {
console.log(res);
})


// 创建一个axios实例 instance2
const instance2 = axios.create({
baseURL: 'http://456.123.123.12:8000',
timeout: 5000
})
// 使用instance2实例
instance2({
url: '/home/multidata'
}).then(res => {
console.log(res);
})


axios 封装代码

假设项目中有十处组件要用到 axios 来请求数据,那么我们可能需要引入 axios 十次,如果有五十次要用到就要引入五十次。万一某天 axios 挂掉了或者不再维护了,那么我们的项目就要大改了,每次用到 axios 的地方都要重新编写,五十次就够呛了,大型项目的话直接扑街。

所以在实际开发中,一定要有意识的封装 axios 代码,比如我们在 src/network/request.js中对 axios 进行了封装,对外导出实例request,在进行数据请求的时候就可以使用封装好的request而不是直接使用 axios,这样一来,如果以后要更换技术或者修改,就不用每处地方都进行修改,只要该封装代码request就可以了。

下面开始进行封装:

level 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//   src/network/request.js

import axios from 'axios'

export function request(config, success, failure) {
// 创建axios的实例
const instance = axios.create({
baseURL: ' https://easy-mock.com/mock/5c88abff4c25252b7c484888/magazine',
timeout: 5000
})

// 发送网络请求,使用回调函数来处理数据
instance(config).then(res => {
success(res)
}).catch(err => {
failure(err)
})
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
import { request } from './network/request'

request({
url: '/home'
},
function (res) {
console.log(res);
},
err => {
console.log(err);
}
)


level 2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import axios from 'axios'

export function request(config) {
// 创建axios的实例
const instance = axios.create({
baseURL: ' https://easy-mock.com/mock/5c88abff4c25252b7c484888/magazine',
timeout: 5000
})

// 发送真正的网络请求
instance(config.baseConfig).then(res => {
config.success(res)
}).catch(err => {
config.failure(err)
})
}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { request } from './network/request'

request({
baseConfig: {
url: '/home'
},
success: res => {
console.log(res);
},
failure: err => {
console.log(err);
}
},
)


level 3

Promise化

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

export function request(config) {
// 返回一个Promise
return new Promise((resolve, reject) => {

const instance = axios.create({
baseURL: ' http://www.liulongbin.top:3005',
timeout: 5000
})

instance(config).then(res => {
resolve(res)
}).catch(err => {
reject(err)
})
})

调用:

1
2
3
4
5
6
7
8
9
import { request } from './network/request'

request({
url: '/api/getlunbo'
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})


level 4

上面封装成 Promise 的版本还不是最好的,事实上instance(config)所返回的就是一个 Promise,因此可以不必再去封装成 Promise ,直接将 instance(config)返回出来即可:

1
2
3
4
5
6
7
8
9
10
11
12
import axios from 'axios'

export function request(config) {
// 返回一个Promise

const instance = axios.create({
baseURL: ' http://www.liulongbin.top:3005',
timeout: 5000
})

return instance(config)
}

调用:

1
2
3
4
5
6
7
8
9
import { request } from './network/request'

request({
url: '/api/getlunbo'
}).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
})



拦截器

在请求或响应被 thencatch 处理前拦截它们。

一般有以下业务场景:

  • 当config中的一些信息不符合服务器要求,可以在发送请求之前拦截之并进行修改
  • 每次发送网络请求时,在界面中触发加载动画,响应完成时关闭加载动画
  • 某些网络请求(比如登录的token),必须携带一些特殊的信息
  • 等等等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});

如果你想在稍后移除拦截器,可以这样:

1
2
var myInterceptor = axios.interceptors.request.use(function () {/*...*/});
axios.interceptors.request.eject(myInterceptor);

可以为自定义 axios 实例添加拦截器

1
2
var instance = axios.create();
instance.interceptors.request.use(function () {/*...*/});

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 axios from 'axios'

export function request(config) {
// 返回一个Promise

const instance = axios.create({
baseURL: ' http://www.liulongbin.top:3005',
timeout: 5000
})
instance.interceptors.request.use(config => {
console.log(config);//拦截config进行处理,这里将其打印出来,这里的config实际上就是上面的config
return config //这里必须return config以取消拦截,否则之后无法响应成功
}, err => {
console.log(err);
})

instance.interceptors.response.use(res => {
console.log(res);//拦截res进行处理,这里将其打印出来
return res.data//这里必须return res.data以取消拦截,否则之后无法响应成功
}, err => {
console.log(err);
})
return instance(config)
}