Vue
# Vue.js
# Vue介绍
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链 (opens new window)以及各种支持类库 (opens new window)结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
Vue是一个渐进式的框架:
渐进式意味着你可以将Vue作为你应用的一部分嵌入其中,带来丰富的交互体验。
# 生命周期
# vue 安装
方法一:直接CDN引入
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<!-- 生产环境版本,优化了尺寸和速度 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
2
3
4
5
方法二:下载和引入
官网下载地址:https://cn.vuejs.org/v2/guide/installation.html
方法三:npm安装
# 最新稳定版
$ npm install vue
2
# 入门案例:
<div id="app">
<h1>{{message}}</h1>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
message: 'HelloVue'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
# v-for列表循环:
<div id="app">
<ul>
<li v-for = "item in movies">{{item}}</li>
</ul>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
movies: ['aaa', 'bbb', 'ccc']
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# 案例:计数器
<div id="app">
<h1>当前计数:{{counter}}</h1>
<button @click="add">+</button><button v-on:click="sub">-</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
counter: 0
},
methods: { //定义方法
add: function(){
this.counter ++
},
sub: function(){
this.counter--
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# @click="方法名"
或者v-on:click="方法名"
# MVVM
1.1基本概念MVVM是Model-View-ViewModel的缩写,表示模型-视图-视图模型。模型层主要负责处理交互请示并返回响应的数据;视图层主要负责展示视图;视图-模型层起到前两者的桥梁作用,一方面响应用户事件并向模型层发送请求,另一方面将模型层返回的数据通过数据绑定在视图中展示。
1.2与MVC模式的区别1.MVVM模式本质上是MVC模式的改进版,MVVM有着最为独特的一个特性,那就是数据绑定;2.MVC模式是系统架构级别的,MVVM是用于单页面的,因此,MVVM也更灵活。
MVVM分为三个部分:分别是M(Model,模型层 ),V(View,视图层),VM(ViewModel,V与M连接的桥梁,也可以看作为控制器)
1、 M:模型层,主要负责业务数据相关;
2、 V:视图层,顾名思义,负视图相关,细分下来就是html+css层;
3、 VM:V与M沟通的桥梁,负责监听M或者V的修改,是实现MVVM双向绑定的要点;
MVVM支持双向绑定,意思就是当M层数据进行修改时,VM层会监测到变化,并且通知V层进行相应的修改,反之修改V层则会通知M层数据进行修改,以此也实现了视图与模型层的相互解耦;
# Mustache语法(双大括号)
# v-once
该指令表示元素和组件(组件后面才会学习)只渲染一次,不会随着数据的改变而改变。
<div id="app">
<h1>{{message}}</h1>
<h1 v-once>{{message}}</h1> <!--message的值发生变化,这个不会发生变化-->>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
message: 'HelloVue'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
# v-html
如果我们希望解析出HTML展示,使用v-html
标签
<div id="app">
<div >{{link}}</div>
<div v-text="link"></div>
<div v-html="link"></div>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
link: '<a href="http://www.baidu.com">百度</a>'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# v-text
将数据显示在页面中
<div id="app">
<div v-text="content">这里的内容会被覆盖掉</div>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
# v-pre
v-pre
用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。
<div id="app">
<h2 v-pre>v-pre用于跳过这个元素和它子元素的编译过程,用于显示原本的Mustache语法。
{{content}}
</h2> <!--{{content}}不会被编译-->
<h2>
{{content}}
</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# v-cloak
这个指令保持在元素上直到关联实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到实例准备完毕。
<style>
[v-cloak]{
display: none;
}
</style>
2
3
4
5
<div id="app">
<h1>{{content}}</h1>
<h1 v-cloak>{{content}}</h1>
</div>
<script src="js/vue.js"></script>
<script>
//vue解析之前,<div id="app">中有一个v-cloak属性
//vue解析结束之后,v-cloak属性被删除
setTimeout(function (){
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容'
}
})
},1000) //延迟加载以后会看到{{content}},渲染之后才会显示真正的内容
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# v-bind
某些属性我们也希望动态来绑定。
比如动态绑定a元素的href属性
比如动态绑定img元素的src属性这个时候,我们可以使用v-bind
指令:
- 作用: 动态绑定属性
- 缩写: (
v-bind
语法糖(简写)也就是:) - 预期:any (with argument)| Object (without argument)口参-数:attrOrProp (optional)
<div id="app">
<img :src="imgUrl" width="200px" height="200px">
<img v-bind:src="imgUrl" width="200px" height="200px">
<a :href="url">百度</a>
<a v-bind:href="url">百度</a>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
imgUrl: 'https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fimg.jj20.com%2Fup%2Fallimg%2F4k%2Fs%2F02%2F2109242306111155-0-lp.jpg&refer=http%3A%2F%2Fimg.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1658834535&t=722f054e22f82844513def82c53d8491',
url:"http://www.baidu.com"
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
v-bind动态绑定class(1)
<style>
.active {
background-color: burlywood;
}
.line {
border-bottom: 1px solid red;
}
</style>
2
3
4
5
6
7
8
<div id="app">
<!-- <h2 v-bind:class = "{key1:value1, key2:value2}"></h2>
<h2 v-bind:class="{类名1:true,类名2:boolean}"></h2> -->
<h2 :class="active">{{content}}</h2>
<h2 :class="{active:isActive,line:isLine}">{{content}}</h2>
<button v-on:click="btnClick">点击</button>
<h2 :class="getClass()">{{content}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容',
active: 'active',
isActive: true,
isLine:true
},
methods :{
btnClick: function(){
this.isActive = !this.isActive
},
getClass: function(){
return {active: this.isActive, line: this.isLine}
}
}
})
</script>
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
# v-bind
动态绑定class
# 对象语法
# 数组语法
<h2 :class="['active']">{{content}}</h2>
<h2 :class="classes">{{content}}</h2>
computed: {
classes: function(){
return [this.active, this.line]
}
}
2
3
4
5
6
7
# 作业:鼠标点击高亮显示
<style>
.active {
background-color: brown;
}
</style>
2
3
4
5
<div id="app">
<ol>
<li v-for="(item,index) in lists" :key="index" :class="{'active':current==index}" @click="addRed(index)">{{item}}</li>
</ol>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
current: -1,
lists:['aaa', 'bbb', 'ccc', 'ddd']
},
methods:{
addRed: function(index){
this.current = index
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# v-bind
绑定style样式
我们可以利用v-bind:style
来绑定一些CSS内联样式,
·在写CSS属性名的时候,比如font-size
1.可以使用驼峰式 (camelCase) fontSize
2.横线分隔(kebab-case,记得用单引号括起来)'font-size‘
绑定class有两种方式:
(1)对象语法
<div id="app">
<!-- <h2 :style="{key(属性名): value(属性值)}">{{content}}</h2> -->
<!-- 50px要加上单引号,否则会被当成变量去解析 -->
<h2 :style="{fontSize:'50px'}">{{content}}</h2>
<h2 :style="{fontSize: fontSize + 'px',backgroundColor:finalColor}">{{content}}</h2>
<h2 :style="getStyles()">{{content}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容',
fontSize: 50,
finalColor: 'red'
},
methods:{
getStyles:function(){
return {fontSize: this.fontSize + 'px',backgroundColor: this.finalColor}
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(2)数姐语法
<div id="app">
<h2 :style="[bck,font]">{{content}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
content:'内容',
bck:{backgroundColor: 'red'},
font: {fontSize: '50px'}
},
methods:{
getStyles:function(){
return {fontSize: this.fontSize + 'px',backgroundColor: this.finalColor}
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 计算属性和监视
<div id="app">
<h2>{{firstName + ' '+ lastName}}</h2>
<h2>{{firstName}} {{lastName}}</h2>
<h2>{{getFullName()}}</h2>
<h2>{{fullName}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
firstName: 'zhang',
lastName: 'san'
},
methods: {
getFullName : function(){
return this.firstName + ' '+ this.lastName
}
},
computed: {
fullName: function(){
return this.firstName + ' '+ this.lastName
}
}
})
</script>
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
# 利用计算属性计算总价格
<div id="app">
<h2 >{{sumPrice}}</h2>
<h2>{{Allprice}}</h2>
<h2>{{prices}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
books:[
{id:100, name:'aaa', price:100},
{id:101, name:'bbb', price:86},
{id:102, name:'ccc', price:130},
]
},
computed: {
sumPrice: function(){
sum = 0
for(let i = 0; i < this.books.length; i++){
sum += this.books[i].price
}
return sum
},
Allprice: function(){
sum = 0
for(let i in this.books){
sum += this.books[i].price
}
return sum
},
prices: function(){
sum = 0
for(let item of this.books){
sum += item.price
}
return sum
},
totalPrice(){
//使用高级函数
return this.books.reduce((total, n)=>{
return total + n.price
}, 0)
}
}
})
</script>
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
# 计算属性的setter和getter
计算属性一般是没有set方法的,只读属性
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
firstName: 'zhang',
lastName: 'san'
},
computed:{
fullName:{
get(){
console.log('调用了fullName的get')
return this.firstName+ ' ' + this.lastName
},
set(newValue){
console.log('调用了fullName的set')
const names = newValue.split(' ')
this.firstName = names[0]
this.lastName = names[1]
}
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
在控制台输入app.fullName = "li si",即可调用计算属性的set和get方法
# 计算属性的缓存
使用函数会调用多次,而计算属性只会调用一次
<div id="app">
<h1>{{fullName}}</h1>
<h1>{{fullName}}</h1>
<h1>{{fullName}}</h1>
<h1>{{getfullName()}}</h1>
<h1>{{getfullName()}}</h1>
<h1>{{getfullName()}}</h1>
<h1>{{getfullName()}}</h1>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
firstName: 'zhang',
lastName: 'san'
},
methods:{
getfullName(){
console.log('getfullName方法调用')
return this.firstName + ' ' + this.lastName
}
},
computed:{
fullName:{
get(){
console.log('计算属性调用fullName的get')
return this.firstName+ ' ' + this.lastName
}
}
}
})
</script>
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="./vue.js"></script>
</head>
<body>
<div id="app">
<h2>今天的天气很{{info}}</h2>
<button @click="isHot = !isHot">切换天气</button>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
isHot: false,
},
computed: {
info() {
return this.isHot ? "炎热" : "凉爽";
}
}
})
</script>
</body>
</html>
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
# 监视属性watch:
1.当被监视的属性变化时,回调函数自动调用,进行相关操作
2.监视的属性必须存在,才能进行监视!!
3.监视的两种写法:
(1).new Vue
时传入watch配置 (2).通过vm.$watch
监视
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js"></script>
</head>
<body>
<div id="app">
<h2>今天的天气很{{info}}</h2>
<button @click="isHot = !isHot">切换天气</button>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
isHot: false,
},
computed: {
info() {
return this.isHot ? "炎热" : "凉爽";
}
},
// watch: {
// isHot: { //原始写法这里是带引号的,所以最好加上引号
// immediate: true, // 初始化时让handler调用一下
// handler(newValue, oldValue) { // 当isHot发生变化时,handler调用
// console.log('isHot被修改了',newValue, oldValue);
// }
// }
// }
})
vm.$watch('isHot',{
immediate: true, // 初始化时让handler调用一下
handler(newValue, oldValue) { // 当isHot发生变化时,handler调用,newValue代表新的值,oldValue代表原来的值
console.log('isHot被修改了',newValue, oldValue);
}
})
</script>
</body>
</html>
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
# 深度监视:
(1).Vue中的watch默认不监测对象内部值的改变(一层)。
(2).配置deep:true
可以监测对象内部值改变(多层)。
备注: (1).Vue自身可以监测对象内部值的改变,但Vue提供的watch默认不可以!
(2).使用watch时根据数据的具体结构,决定是否采用深度监视。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js"></script>
</head>
<body>
<div id="app">
<h2>a:{{numbers.a}}</h2>
<button @click="numbers.a++">add</button>
<br/>
<h2>b:{{numbers.b}}</h2>
<button @click="numbers.b++">add</button>
<br/>
<button @click="numbers={a:999,b:999}">彻底改变numbers</button>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
numbers: {
a: 1,
b: 1
}
},
watch: {
// 监听多级层次结构中某个属性的变化
'numbers.a': {
handler(newValue, oldValue) {
console.log("a被改变了")
}
},
// 检测多级层次结构中多有属性的改变(a,b属性改变都会触发)
numbers: {
deep: true,
handler(newValue, oldValue) {
console.log("numbers被改变了")
}
}
}
})
</script>
</body>
</html>
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
# 简写
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js"></script>
</head>
<body>
<div id="app">
<h2>今天的天气很{{info}}</h2>
<button @click="isHot = !isHot">切换天气</button>
</div>
<script>
const vm = new Vue({
el: '#app',
data: {
isHot: false,
},
computed: {
info() {
return this.isHot ? "炎热" : "凉爽";
}
},
// 完整写法
// watch: {
// isHot: {
// immediate: true, // 初始化时让handler调用一下
// handler(newValue, oldValue) { // 当isHot发生变化时,handler调用
// console.log('isHot被修改了',newValue, oldValue);
// }
// }
// }
// 简写,这时候不能用其他属性,相当于直接使用handler方法
watch: {
isHot(newValue, oldValue) {
console.log('isHot被修改了',newValue, oldValue);
},
// isHot: (newValue, oldValue) => { 如果使用的是箭头函数,this指向的是windows
// console.log('isHot被修改了',newValue, oldValue,this);
// }
}
})
// 完整写法
// vm.$watch('isHot',{
// immediate: true, // 初始化时让handler调用一下
// handler(newValue, oldValue) { // 当isHot发生变化时,handler调用,newValue代表新的值,oldValue代表原来的值
// console.log('isHot被修改了',newValue, oldValue);
// }
// })
// 简写,
// vm.$watch('isHot',function(newValue, oldValue) {
// console.log('isHot被修改了',newValue, oldValue);
// })
</script>
</body>
</html>
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
姓名案例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="js/vue.js"></script>
</head>
<body>
<div id="app">
<input type="text" v-model="firstName">
<input type="text" v-model="lastName">
{{fullName}}
</div>
<script>
new Vue({
el: '#app',
data: {
firstName: '',
lastName: '',
fullName: ''
},
watch: {
firstName(newValue) {
this.fullName = newValue + this.lastName;
},
lastName: {
handler(newValue){
this.fullName = this.firstName + newValue;
}
}
}
})
</script>
</body>
</html>
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
# computed和watch之间的区别:
1.computed能完成的功能,watch都可以完成。
2.watch能完成的功能,computed不一定能完成,例如: watch可以进行异步操作.
两个重要的小原则:
1.所被Vue管理的函数,最好写成普通函数,这样this的指向才是vm或组件实例对象。
2.所有不被Vue所管理的函数(定时器的回调函数、Ajax的回调函数等、Promise的回调函数),最好写成箭头函数,这样this的指向才是vm或组件实例对象.
# 绑定样式
<body>
<!--
绑定样式:
1.class样式
写法:class=" xxx"xxx可以是字符串、对象、数组。
字符串写法适用于:类名不确定,要动态获取。
对象写法适用于:要绑定多个样式,个数不确定,名字也不确定。
数组写法适用于:要绑定多个样式,个数确定,名字也确定,但不确定用不用。
2. style样式
:style="{fontSize: xxx}"其中xxx是动态值。
:style="[a,b]"其中a、b是样式对象。
-->
<div id="app">
<!--绑定class样式--字符串写法,适用于:样式的类名不确定,需要动态指定-->
<div class="basic" :class="normal">样式</div>
<!--绑定class样式--数组写法,适用于:要绑定的样式个数不确定、名字也不确定-->
<!--<div class="basic" :class="['c1','c2','c3']">样式2</div> 需要加引号,不加引号找的是变量对应的值-->
<div class="basic" :class="classArr">样式2</div>
<!--绑定class样式--对象写法,适用于:要绑定的样式个数确定、名字也确定,但要动态决定用不用-->
<div class="basic" :class="classObj">样式3</div>
<!--:style="{fontSize: xxx}"其中xxx是动态值。-->
<!--<div :style="{fontSize: fontsize + 'px'}">style样式</div>-->
<div :style="styleObj">style样式</div>
<!--:style="[a,b]"其中a、b是样式对象。-->
<div :style="[styleObj,styleObj2]">style数组样式</div>
</div>
<script>
const app = new Vue({
el: '#app',
data: {
normal: 'normal',
classArr: ['c1', 'c2', 'c3'],
classObj: {
c1: true,
c2: false,
c3: true
},
fontsize: 40,
styleObj: {
color: 'red',
backgroundColor: 'orange'
},
styleObj2: {
fontSize: 50 + 'px'
}
}
})
</script>
</body>
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
# 事件监听
# v-on
情况一:如果该方法不需要额外参数,那么方法后的()可以不添加。
但是注意:如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
情况二:如果需要同时传入某个参数,同时需要event时,可以通过$event
传入事件。
<div id="app">
<h2>{{count}}</h2>
<!-- <button v-on:click="count++">+</button>
<button v-on:click="count--">-</button> -->
<!-- 1.事件调用的方法没有参数 -->
<!-- 2.在时间定义时,写方法时省略了小括号,但是方法本身是需要一个参数的,Vue会
默认将浏览器产生的event事件作为参数传入到方法 -->
<button @click="btnClick">点击事件</button>
<button @click="btnClick()">点击事件</button>
<!-- 3.方法定义时,我们需要event对象,同时又需要其他参数
调用方法,手动获取到浏览器的event对象
-->
<button @click="increment(2)">+</button>
<button @click="decrement(10, $event)">-</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
count: 0
},
methods: {
increment(num){
console.log(event)
this.count += num
},
decrement(num, event){
console.log(event)
this.count -= num
},
btnClick(){
console.log(event)
console.log('点击事件')
}
}
})
</script>
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
# v-on
修饰符
# .stop阻止冒泡
<div @click.stop="ziDivClick" ></div>
# .prevent阻止默认行为
<!-- 2. .prevent修饰符的使用 -->
<form action="baidu">
<input @click.prevent="formSubmit()" type="submit" value="提交">
</form>
2
3
4
# .once只调用一次
<!-- 4. .once只触发一次回调 -->
<button @click.once="btnClick">只会调用一次</button>
2
代码:
<div id="app">
<!-- 1.stop修饰符的使用 -->
<div @click="fuDivClick"class="fu">
<!-- 点击子类事件时,会发生冒泡,触发父类事件 -->
<!-- <div @click="ziDivClick" ></div> -->
<!-- 停止冒泡 -->
<div @click.stop="ziDivClick" ></div>
</div>
<!-- 2. .prevent修饰符的使用 -->
<form action="baidu">
<input @click.prevent="formSubmit()" type="submit" value="提交">
</form>
<!-- 3.键盘某个键盘的键帽 -->
<input type="text" @keyup.enter="keyUp">
<!-- 4. .once只触发一次回调 -->
<button @click.once="btnClick">只会调用一次</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
count: 0
},
methods: {
ziDivClick(){
console.log('子类div被点击了')
},
fuDivClick(){
console.log('父类div被点击了')
},
formSubmit(){
console.log('表单提交')
},
keyUp(){
console.log('提交')
},
btnClick(){
console.log('只会调用一次')
}
}
})
</script>
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
# 条件判断
# v-if
<div id="app">
<h2 v-if="isShow">
<div>abc</div>
<div>abc</div>
<div>abc</div>
<div>abc</div>
</h2>
<h2 v-if="false">不会显示</h2>
<h2 v-if="true">会显示</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isShow: true
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# v-else
<div id="app">
<h2 v-if="isShow">
<div>abc</div>
<div>abc</div>
<div>abc</div>
<div>abc</div>
</h2>
<h2 v-else>
isShow为false是显示
</h2>
<button @click="btnClick">点击</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isShow: true
},
methods:{
btnClick() {
this.isShow = !this.isShow
}
}
})
</script>
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
# v-else-if
<div id="app">
<h2 v-if="score>=90">优秀</h2>
<h2 v-else-if="score>=80">良好</h2>
<h2 v-else-if="score>=70">中等</h2>
<h2 v-else-if="score>=60">及格</h2>
<h2 v-else>不及格</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
score: 89
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 案例:用户登录切换
<div id="app">
<span v-if="isUser">
<label>账号</label>
<!-- 加上可以属性时,切换就不会保留原来输入的值了 -->
<input type="text" placeholder="请输入账号" key="username">
</span>
<span v-else>
<label>邮箱</label>
<input type="email" placeholder="请输入邮箱" key="email">
</span>
<button @click="btnClick">切换类型</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isUser: true
},
methods: {
btnClick(){
this.isUser = !this.isUser
}
}
})
</script>
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
小问题: 如果我们在有输入内容的情况下,切换了类型,我们会发现文字依然显示之前的输入的内容。 但是按道理讲,我们应该切换到另外一个input元素中了。 在另一个input元素中,我们并没有输入内容。
为什么会出现这个问题呢?
问题解答: 这是因为Vue在进行DOM渲染时,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。 在上面的案例中,Vue内部会发现原来的input元素不再使用,直接作为else中的input来使用了。
解决方案: 如果我们不希望Vue出现类似重复利用的问题,可以给对应的input添加key 并且我们需要保证key的不同
# v-show
v-show
当条件为false时,只是给元素添加了行内样式display:none
<div id="app">
<h2 v-if="isShow">
isShow为True显示
</h2>
<h2 v-else> isShow为fasle显示</h2>
<h2 v-show="isShow">v-show显示</h2>
<button @click="btnClick">点击</button>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
isShow: true
},
methods:{
btnClick() {
this.isShow = !this.isShow
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# v-show
和v-if
对比
v-show
的用法和v-if
非常相似,也用于决定一个元素是否渲染:
v-if
和v-show
对比 v-if
和v-show
都可以决定一个元素是否渲染,那么开发中我们如何选择呢?
v-if
当条件为false时,压根不会有对应的元素在DOM中。v-show
当条件为false时,仅仅是将元素的display属性设置为none而已。 开发中如何选择呢?- 当需要在显示与隐藏之间切片很频繁时,使用
v-show
当只有一次切换时,通过使用v-if
# 循环遍历
# v-for
遍历数组
<div id="app">
<ul>
<li v-for = "item in movies">{{item}}</li>
</ul>
<ul>
<li v-for = "(item, index) in movies">{{index+1}}-{{item}}</li>
</ul>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
movies: ['aaa', 'bbb', 'ccc']
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# v-for
循环遍历对象
<div id="app">
<ul>
<!-- 1.在遍历对象的过程中,如果只是获取一个值,name获取的是value -->
<li v-for = "value in obj">{{value}}</li>
</ul>
<ul>
<!-- 2.获取key,value 格式:(value,key) -->
<li v-for = "(value, key) in obj">{{key}}-{{value}}</li>
</ul>
<ul>
<!-- 3.获取key,value,index 格式:(value, key, index) -->
<li v-for = "(value, key, index) in obj">{{key}}-{{value}}--{{index}}</li>
</ul>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app', //用于挂载要管理的元素
data: { //定义数据
obj:{
name:'lisi',
age: 18,
height: 188
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 检测数组更新
因为Vue是响应式的,所以当数据发生变化时,Vue会自动检测数据变化,视图会发生对应的更新。 Vue中包含了一组观察数组编译的方法,使用它们改变数组也会触发视图的更新
push()
数组末尾添加元素//1.push方法,数组末尾添加元素 this.list.push('eee') this.list.push('eee', 'fff', 'ggg')
1
2
3pop()
删除数组中的最后一个元素//2.pop()删除数组中的最后一个元素 this.list.pop()
1
2shift()
删除数组中的第一个元素//3.删除数组中的第一个元素 this.list.shift()
1
2unshift()
在数组最前面添加元素//4..unshift()在数组最前面添加元素 this.list.unshift('000') this.list.unshift('000','111','222')
1
2
3splice()
删除元素/插入元素/替换元素 1.删除元素:第一个参数:从这个索引后面开始删除 第二个参数:传入要删除几个元素(如果没有传,就删除后面所有元素) 2.替换元素:第二个参数:标识我们要替换几个元素,后面就是用于替换前面的元素 3.插入元素:第二个参数:传入0,并且后面跟上要插入的元素//5.splice()作用:删除元素/插入元素/替换元素 //删除元素:第一个参数:从这个索引后面开始删除 // 第二个参数:传入要删除几个元素(如果没有传,就删除后面所有元素) //替换元素:第二个参数:标识我们要替换几个元素,后面就是用于替换前面的元素 //插入元素:第二个参数:传入0,并且后面跟上要插入的元素 const start = 1 this.list.splice(start) //删除start后面所有的元素 this.list.splice(start,2) //删除start后的两个元素 this.list.splice(start, 2 ,'ffff') //只传一个就只会替换一个 this.list.splice(start, 2 ,'fff', 'fff', 'fff') //会替换两个,再插入一个 this.list.splice(start,0, 'x','y') //插入
1
2
3
4
5
6
7
8
9
10
11
12
13
14sort()
排序this.list.sort() this.list.sort((a,b)=>{ return a-b // 升序 }) this.list.sort((a,b)=>{ return b-a // 降序 })
1
2
3
4
5
6
7reverse()
反转this.list.reverse()
1filter()
过滤 返回一个新数组this.list.filter((p)=>{ return 条件 })
1
2
3注意:
this.list[0] = '1111111' //通过这个修改方式修改无效, this.list[0].name = "zhangsan" //这种可以 this.list[0] = {name:"张三",age:15} //不可以
1
2
3# Vue数据监测原理
let data = { name : "张三", age : 14 } // 创建一个监视的实例对象,用于监视data中属性的变化 const obs = new Observer(data); console.log(obs); // 准备一个vm实例 let vm = {} vm._data = data = obs function Observer(obj) { // 汇总对象中所有属性形成一个数组 const keys = Object.keys(obj); // 遍历 keys.forEach((k) => { Object.defineProperty(this,k,{ get() { return obj[k] }, set(val) { console.log(`${k}被改变了,我要去解析模板,生成虚拟DOM....`) obj[k] = 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#
Vue.set
// this.list.splice(0, 1 ,'1111') Vue.set(this.list, 0, '1111') //修改索引为0的值
1
2# 列表过滤
<script type="text/javascript"> Vue.config.productionTip = false //用watch实现 //#region /* new Vue({ el:'#root', data:{ keyWord:'', persons:[ {id:'001',name:'马冬梅',age:19,sex:'女'}, {id:'002',name:'周冬雨',age:20,sex:'女'}, {id:'003',name:'周杰伦',age:21,sex:'男'}, {id:'004',name:'温兆伦',age:22,sex:'男'} ], filPerons:[] }, watch:{ keyWord:{ immediate:true, handler(val){ this.filPerons = this.persons.filter((p)=>{ return p.name.indexOf(val) !== -1 }) } } } }) */ //#endregion //用computed实现 new Vue({ el:'#root', data:{ keyWord:'', persons:[ {id:'001',name:'马冬梅',age:19,sex:'女'}, {id:'002',name:'周冬雨',age:20,sex:'女'}, {id:'003',name:'周杰伦',age:21,sex:'男'}, {id:'004',name:'温兆伦',age:22,sex:'男'} ] }, computed:{ filPerons(){ return this.persons.filter((p)=>{ return p.name.indexOf(this.keyWord) !== -1 }) } } }) </script>
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# 图书购物车案例
# filters过滤器使用
<td>{{item.price | showPrice}} </td>
filters:{
showPrice(price){
return '¥'+ price.toFixed(2)
}
}
2
3
4
5
table{
border: 1px solid #e9e9e9;
border-collapse: collapse;
border-spacing: 0;
}
th,td {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
th{
background-color: #f7f7f7;
color: #5c6b77;
font-weight: 600;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="app">
<div v-if="books.length">
<table>
<thead>
<th></th>
<th>书籍名称</th>
<th>出版日期</th>
<th>价格</th>
<th>购买数量</th>
<th>操作</th>
</thead>
<tbody>
<tr v-for="(item, index) in books" :key="index">
<td>{{item.id}}</td>
<td>{{item.name}}</td>
<td>{{item.date}} </td>
<td>{{item.price | showPrice}} </td>
<td><button @click="increment(index)">+</button>{{item.count}}<button :disabled="item.count <= 0" @click="decrement(index)">-</button></td>
<td><button @Click="removeBook(index)">移除</button></td>
</tr>
</tbody>
</table>
<h2>总价格:{{totalPrice | showPrice}}</h2>
</div>
<h2 v-else>图书列表为空</h2>
</div>
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
const app = new Vue({
el: '#app',
data: {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.00,
count: 1
},
]
},
methods:{
increment(index){
this.books[index].count ++
},
decrement(index){
this.books[index].count --
},
removeBook(index){
this.books.splice(index,1)
}
},
filters: {
showPrice(price){
return '¥'+ price.toFixed(2)
}
},
computed: {
// totalPrice(){
// let sum = 0;
// for (let item of this.books){
// sum += item.count * item.price;
// }
// return sum
// }
totalPrice(){
return this.books.reduce(function(preValue, book){
return preValue +book.count* book.price
},0 )
}
}
})
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
# 自定义指令
- 注册全局指令
Vue.directive('my-directive', function(el, binding){
el.innerHTML = binding.value.toupperCase()
})
2
3
- 注册局部指令
directives : {
'my-directive' : {
bind (el, binding) {
el.innerHTML = binding.value.toupperCase()
}
}
}
2
3
4
5
6
7
- 使用指令
v-my-directive='xxx'
<body>
<div id="app">
<h2>{{name}}</h2>
<h2>当前的n值是:<span v-text="n"></span></h2>
<h2>放大10倍n值是:<span v-big="n"></span></h2>
<button @click="n++">点我n+1</button>
<input type="text" v-fbind="n">
</div>
<script>
new Vue({
el:'#app',
data: {
name: "张三",
n: 1
},
directives: {
// big什么时候会被调用:
// 1.指令与元素成功绑定时,
// 2.指令所在的模板被重新解析时
big(element, binding) { // 简写,相当于bind()和update()
console.log('big')
console.log(element, binding)
element.innerText = binding.value * 10
},
fbind: { // 完整写法
// 指令与元素成功绑定时
bind(element, binding){
element.value = binding.value
},
// 指令所在元素被插入页面时
inserted(element, binding){
element.focus()
},
// 指令所在的模板被重新解析时
update(element, binding){
element.value = binding.value
}
}
}
})
</script>
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
# v-model
Vue中使用v-model
指令来实现表单元素和数据的双向绑定。
- 案例的解析:
当我们在输入框输入内容时
因为input中的
v-model
绑定了message,所以会实时将输入的内容传递给message,message发生改变。 当message发生改变时,因为上面我们使用Mustache语法,将message的值插入到DOM中,所以DOM会发生响应的改变。 所以,通过v-model
实现了双向的绑定。 当然,我们也可以将v-model
用于textarea元素
<div id="app">
<input type="text" v-model="message">
{{message}}
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
# v-model
原理
v-model
其实是一个语法糖,它的背后本质上是包含两个操作:
- 1.
v-bind
绑定一个value属性 - 2.
v-on
指令给当前元素绑定input事件
<input type="text" v-model="message">
等同于
<input type="text" v-bind:value="message" v-on:input="message = $event.target.value">
2
3
<div id="app">
<input type="text" :value="message" @input="inputChange">
{{message}}
<input type="text" :value="message2" @input="message2 = $event.target.value">
{{message2}}
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
message2: 'hello'
},
methods: {
inputChange(){
this.message = event.target.value;
}
}
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# v-model:ratio
<div id="app">
<!-- 加上相同的name就可以实现单选 -->
<!-- <input type="radio" name="sex" value="男">男
<input type="radio" name="sex" value="女">女 -->
<label for="male">
<input type="radio" id="male" value="男" v-model="sex">男
</label>
<label for="female">
<input type="radio" id="female" value="女" v-model="sex">女
</label>
<h2>您选择的性别是: {{sex}}</h2>
</div>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
sex:'男'
},
methods: {
}
})
</script>
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
# v-model:checkbox
- 复选框分为两种情况:单个勾选框和多个勾选框
- 单个勾选框:
- v-model即为布尔值。
- 此时input的value并不影响v-model的值。
<label for="agree">
<input type="checkbox" id="agree" v-model="isAgree">同意协议
</label>
<h2>您选择的是: {{isAgree}}</h2>
<button :disabled="!isAgree">下一步</button>
2
3
4
5
- 多个复选框:
- 当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。 当选中某一个时,就会将input的value添加到数组中。
<!--2.checkbox多选框-->
<input type="checkbox" value="篮球" v-model="hobbies">篮球
<input type="checkbox" value="足球" v-model="hobbies">足球
<input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
<input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
<h2>您的爱好是: {{hobbies}}</h2>
data: {
isAgree: true,
hobbies:[]
},
2
3
4
5
6
7
8
9
10
# v-model
结合select类型
- 和checkbox一样,select也分单选和多选两种情况。
- 单选:只能选中一个值。
v-model
绑定的是一个值。- 当我们选中option中的一个时,会将它对应的value赋值到melect中
<!--1.选择一个-->
<select name="abc" v-model="fruit">
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="葡萄">葡萄</option>
</select>
<h2>您选择的水果是: {{fruit}}</h2>
data: {
fruit: '香蕉',
fruits: []
}
2
3
4
5
6
7
8
9
10
11
12
- 多选:可以选中多个值。
v-model
绑定的是一个数组。- 当选中多个值时,就会将选中的option对应的value添加到数组mySelect
<!--2.选择多个-->
<select name="abc" v-model="fruits" multiple>
<option value="苹果">苹果</option>
<option value="香蕉">香蕉</option>
<option value="榴莲">榴莲</option>
<option value="葡萄">葡萄</option>
</select>
<h2>您选择的水果是: {{fruits}}</h2>
data: {
fruit: '香蕉',
fruits: []
}
2
3
4
5
6
7
8
9
10
11
12
# :value
值绑定
# lazy修饰符:
默认情况下,
v-model
默认是在input事件中同步输入框的数据的。也就是说,一旦有数据发生改变对应的data中的数据就会自动发生改变。
lazy修饰符可以让数据在失去焦点或者回车时才会更新
<input type="text" v-model.lazy="message" > {{message}}
1
2
# number修饰符:
- 默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。
- 但是如果我们希望处理的是数字类型,那么最好直接将内容当做数字处理。
- number修饰符可以让在输入框中输入的内容自动转成数字类型:
<input type="number" v-model.number="age" >
<h2>{{age}}-{{typeof age}}</h2>
2
# trim修饰符:
- 如果输入的内容首尾有很多空格,通常我们希望将其去除
- trim修饰符可以过滤内容左右两边的空格
<input type="text" v-model.trim="name">
<h2>您输入的名字:{{name}}</h2>
2
# 组件化开发
# 注册组件基本步骤
组件的使用分成三个步骤:
创建组件构造器
// 1.创建组件构造器对象 const cnp = Vue.extend({ template:` <div> <h2>我是标题</h2> <p>我是内容,哈哈哈哈哈</p> <p>我是内容,呵呵呵呵呵</p> </div> ` })
1
2
3
4
5
6
7
8
9
10注册组件
//2.注册组件 Vue.component('my-cpn', cnp)
1
2使用组件
<my-cpn></my-cpn> <my-cpn></my-cpn>
1
2
1.
Vue.extend()
: 调用Vue.extend()
创建的是一个组件构造器。 通常在创建组件构造器时,传入template代表我们自定义组件的模板。 该模板就是在使用到组件的地方,要显示的HTML代码。 事实上,这种写法在Vue2.x的文档中几乎已经看不到了,它会直接使用下面我们会讲到的语法糖,但是在很多资料还是会提到这种方式,而且这种方式是学习后面方式的基础。2.
Vue.component()
: 调用Vue.component()
是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。 所以需要传递两个参数:1、注册组件的标签名 2、组件构造器3.组件必须挂载在某个Vue实例下,否则它不会生效。(见下页) 我们来看下面我使用了三次
<my-cpn></my-cpn>
而第三次其实并没有生效:
# 全局组件和局部组件
调用Vue.component()
注册组件时,组件的注册是全局的
<div id="app">
<cnp></cnp>
<cnp></cnp>
</div>
<hr>
<div id="app2">
<cnp></cnp>
<cnp></cnp>
</div>
<script src="js/vue.js"></script>
<script>
// 1.创建组件构造器对象
const cnp = Vue.extend({
template:`
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈哈哈</p>
<p>我是内容,呵呵呵呵呵</p>
</div>
`
})
// //2.注册组件
// Vue.component('cnp', cnp)
const app = new Vue({
el: '#app',
data: {
},
components:{ //局部组件
cnp
}
})
</script>
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
# 父组件与子组件
子组件不能访问父组件
在前面我们看到了组件树: 组件和组件之间存在层级关系 而其中一种非常重要的关系就是父子组件的关系 我们来看通过代码如何组成的这种层级关系:
父子组件错误用法:以子标签的形式在Vue实例中使用
因为当子组件注册到父组件的components时,Vue会编译好父组件的模块
该模板的内容已经决定了父组件将要渲染的HTML(相当于父组件中已经有了子组件中的内容了)
<child-cpn></child-cpn>
是只能在父组件中被识别的。
类似这种用法,<child-cpn></child-cpn>
是会被浏览器忽略的。
<div id="app">
<!-- <cnp></cnp>
<cnp></cnp> -->
</div>
<hr>
<div id="app2">
<cnp></cnp>
<cnp></cnp>
</div>
<script src="js/vue.js"></script>
<script>
// 1.创建组件构造器对象
const cnp = Vue.extend({
template:`
<div>
<h2>我是标题</h2>
<p>我是内容,哈哈哈哈哈</p>
<p>我是内容,呵呵呵呵呵</p>
</div>
`
})
// //2.注册组件
// Vue.component('cnp', cnp)
const app = new Vue({
el: '#app',
data: {
},
components:{
cnp
}
})
const app2 = new Vue({
el: '#app2',
data:{
}
})
</script>
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
# 注册组件语法糖
注册全局组件
<div id="app">
<cnp1></cnp1>
</div>
<hr>
<div id="app2">
<cnp1></cnp1>
</div>
<script src="js/vue.js"></script>
<script>
//注册全局组件
Vue.component('cnp1',{
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容, 哈哈哈哈</p>
</div>
`
})
const app = new Vue({
el: '#app',
data:{
}
})
const app2 = new Vue({
el: '#app2',
data:{
}
})
</script>
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
<div id="app">
<cnp1></cnp1>
<cnp2></cnp2>
</div>
<hr>
<div id="app2">
<cnp1></cnp1>
<cnp2></cnp2>
</div>
<script src="js/vue.js"></script>
<script>
//注册全局组件
Vue.component('cnp1',{
template: `
<div>
<h2>我是标题1</h2>
<p>我是内容, 哈哈哈哈</p>
</div>
`
})
const app = new Vue({
el: '#app',
data:{
},
components:{
'cnp2':{
template: `
<div>
<h2>我是标题2</h2>
<p>我是内容, 哈哈哈哈</p>
</div>
`
}
}
})
const app2 = new Vue({
el: '#app2',
data:{
}
})
</script>
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
# 模板的分离写法
刚才,我们通过语法糖简化了Vue组件的注册过程,另外还有一个地方的写法比较麻烦,就是template模块写法。 如果我们能将其中的HTML分离出来写,然后挂载到对应的组件上,必然结构会变得非常清晰。
Vue提供了两种方案来定义HTML模块内容:
使用
<script>
标签<!--1.script标签, 注意:类型必须是text/x-template--> <script type="text/x-template" id="cnp1"> <div> <h2>我是标题1</h2> <p>我是内容, 哈哈哈哈</p> </div> </script>
1
2
3
4
5
6
7//注册全局组件 Vue.component('cnp1',{ template: '#cnp1' }) const app = new Vue({ el: '#app', data:{ }, })
1
2
3
4
5
6
7
8
9使用
<template>
标签
<template id="cnp1"> <div> <h2>我是标题1</h2> <p>我是内容, 哈哈哈哈</p> </div> </template>
1
2
3
4
5
6
7//注册全局组件 Vue.component('cnp1',{ template: '#cnp1' }) const app = new Vue({ el: '#app', data:{ }, })
1
2
3
4
5
6
7
8
9
# 组件数据的存放
组件自己的数据存放在哪里呢?
- 组件对象也有一个data属性(也可以有methods等属性,下面我们有用到)
- 只是这个data属性必须是一个函数
- 而且这个函数返回一个对象,对象内部保存着数据
<template id="cnp1">
<div>
<h2>我是标题1{{message}}</h2>
<p>我是内容, 哈哈哈哈</p>
</div>
</template>
2
3
4
5
6
//注册全局组件
Vue.component('cnp1',{
template: '#cnp1',
data(){
return {
message:'组件自己的内容'
}
}
})
const app = new Vue({
el: '#app',
data:{
message:'你好啊'
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
为什么data在组件中必须是一个函数呢?
- 首先,如果不是一个函数,Vue直接就会报错。
- 其次,原因是在于Vue让每个组件对象都返回一个新的对象,因为如果是同一个对象的,组件在多次使用后会相互影响。
<div id="app">
<cnp></cnp>
<cnp></cnp>
<cnp></cnp>
</div>
<template id="cnp">
<div>
<h2>当前计数: {{counter}}</h2>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
<script src="js/vue.js"></script>
<script>
const obj = {
counter: 0
}
Vue.component('cnp',{
template: '#cnp',
data(){
// return obj //引用对象引用的是地址
return {
counter: 0
}
},
methods:{
increment() {
this.counter++
},
decrement() {
this.counter--
}
}
})
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
}
})
</script>
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
# 父子组件的通信
# props基本用法
在组件中,使用选项props来声明需要从父级接收到的数据。
props的值有两种方式: 方式一:字符串数组,数组中的字符串就是传递时的名称。 方式二:对象,对象可以设置传递时的类型,也可以设置默认值等。
<div id="app">
<cnp2></cnp2>
</div>
<!-- 子组件 -->
<template id="cnp1">
<div>
<h2>我是子组件</h2>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<template id="cnp2">
<div>
<h2>我是父组件</h2>
<p>我是内容, 哈哈哈哈</p>
<cnp1 :cmessage="message" :cmovies="movies"></cnp1>
</div>
</template>
<script src="js/vue.js"></script>
<script>
//子组件
const cnp1 = Vue.extend({
template: '#cnp1',
// props:['cmovies', 'cmessage']
props: {
// 1.类型限制
// cmovies: Array,
// cmessage: String,
// 2.提供一些默认值, 以及必传值
cmessage: {
type: String,
default: 'aaaaaaaa',
required: true
},
// 类型是对象或者数组时, 默认值必须是一个函数
cmovies: {
type: Array,
default() {
return []
}
}
},
})
//父组件
Vue.component('cnp2',{
template: '#cnp2',
components:{
cnp1
},
data(){
return {
message: '你好啊',
movies: ['海王', '海贼王', '海尔兄弟']
}
}
})
const app = new Vue({
el: '#app',
data:{
message:'你好啊'
},
})
</script>
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
<div id="app">
<!--<cpn v-bind:cmovies="movies"></cpn>-->
<!--<cpn cmovies="movies" cmessage="message"></cpn>-->
<cpn :cmessage="message" :cmovies="movies"></cpn>
</div>
<template id="cpn">
<div>
<ul>
<li v-for="item in cmovies">{{item}}</li>
</ul>
<h2>{{cmessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
// 父传子: props
const cpn = {
template: '#cpn',
// props: ['cmovies', 'cmessage'],
props: {
// 1.类型限制
// cmovies: Array,
// cmessage: String,
// 2.提供一些默认值, 以及必传值
cmessage: {
type: String,
default: 'aaaaaaaa',
required: true
},
// 类型是对象或者数组时, 默认值必须是一个函数
cmovies: {
type: Array,
default() {
return []
}
}
},
data() {
return {}
},
methods: {
}
}
const app = new Vue({
el: '#app',
data: {
message: '你好啊',
movies: ['海王', '海贼王', '海尔兄弟']
},
components: {
cpn
}
})
</script>
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
# props数据验证
驼峰命名法:
<div id="app">
<cpn :c-info="info" :child-my-message="message" v-bind:class></cpn>
</div>
<template id="cpn">
<div>
<h2>{{cInfo}}</h2>
<h2>{{childMyMessage}}</h2>
</div>
</template>
<script src="../js/vue.js"></script>
<script>
const cpn = {
template: '#cpn',
props: {
cInfo: {
type: Object,
default() {
return {}
}
},
childMyMessage: {
type: String,
default: ''
}
}
}
const app = new Vue({
el: '#app',
data: {
info: {
name: 'why',
age: 18,
height: 1.88
},
message: 'aaaaaa'
},
components: {
cpn
}
})
</script>
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
# 子级向父级传递
<!-- 父组件模板 -->
<div id="app">
<cpn @item-click="cpnClick"></cpn>
</div>
<!-- 子组件模板 -->
<template id="cpn">
<div>
<button v-for="item in categories"
@click="btnClick(item)"
>
{{item.name}}
</button>
</div>
</template>
<script src="js/vue.js"></script>
<script>
//子组件
const cpn = {
template: '#cpn',
data() {
return {
categories: [
{id: 'aaa', name: '热门推荐'},
{id: 'bbb', name: '手机数码'},
{id: 'ccc', name: '家用家电'},
{id: 'ddd', name: '电脑办公'},
]
}
},
methods: {
btnClick(item){
// console.log(item)
// 发射事件:自定义事件
this.$emit('item-click', item)
}
},
}
// 2.父组件
const app = new Vue({
el: '#app',
data: {
message: '你好啊'
},
components: {
cpn
},
methods: {
cpnClick(item) {
console.log(item);
}
}
})
</script>
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
# 父子间通信案例
<div id="app">
<cpn :num1="number1" :num2="number2"
@num1change="num1change"
@num2change="num2change"
></cpn>
</div>
<template id="cpn">
<div>
<h2>props:{{num1}}</h2>
<h2>data:{{dnumber1}}</h2>
<input type="text" :value="dnumber1" @input="num1Input">
<h2>props:{{num2}}</h2>
<h2>data:{{dnumber2}}</h2>
<input type="text" :value="dnumber2" @input="num2Input">
</div>
</template>
<script src="js/vue.js"></script>
<script>
const app = new Vue({
el: '#app',
data: {
number1: 1,
number2: 2
},
methods:{
num1change(value){
this.number1 = parseFloat(value)
},
num2change(value){
this.number2 = parseFloat(value)
}
},
components:{
'cpn':{
template:'#cpn',
data(){
return {
dnumber1: this.num1,
dnumber2: this.num2
}
},
props:{
num1: Number,
num2: Number
},
methods:{
num1Input(){
this.dnumber1 = event.target.value
this.$emit('num1change',this.dnumber1)
},
num2Input(){
this.dnumber2 = event.target.value
this.$emit('num2change',this.dnumber2)
}
}
}
}
})
</script>
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
# 父子组件的访问方式$chilid
父组件访问子组件:使用$children或$refs
子组件访问父组件:使用$parent
this.$children是一个数组类型,它包含所有子组件对象。 我们这里通过一个遍历,取出所有子组件的message状态。
<div id="app"> <parentcpn></parentcpn> </div> <template id="parent-cpn"> <div> <h2>我是父组件</h2> <button @click="btnClick">我是父组件的按钮</button> <childrencpn></childrencpn> <childrencpn></childrencpn> <childrencpn></childrencpn> </div> </template> <template id="children-cpn"> <div> <h2>我是子组件</h2> </div> </template> <script src="js/vue.js"></script> <script> const app = new Vue({ el: '#app', data :{ }, components: { parentcpn : { template: '#parent-cpn', data() { return { name: '我是父组件....' } }, methods: { btnClick(){ console.log(this.$children) // [VueComponent, VueComponent, VueComponent] console.log(this.$children[0]) for (let c of this.$children){ console.log(c.name) } } }, components:{ childrencpn: { template: '#children-cpn', data() { return { name: '我是子组件....' } }, } } }, } }) </script>
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
# 父子组件的访问方式$refs
$children的缺陷:
- 通过$children访问子组件时,是一个数组类型,访问其中的子组件必须通过索引值。
- 但是当子组件过多,我们需要拿到其中一个时,往往不能确定它的索引值,甚至还可能会发生变化。
- 有时候,我们想明确获取其中一个特定的组件,这个时候就可以使用$refs
$refs的使用:
- $refs和ref指令通常是一起使用的。
- 首先,我们通过ref给某一个子组件绑定一个特定的ID。
- 其次,通过this.$refs.ID就可以访问到该组件了。
<template id="parent-cpn">
<div>
<h2>我是父组件</h2>
<button @click="btnClick">我是父组件的按钮</button>
<childrencpn ref="children1"></childrencpn>
<childrencpn ref="children2"></childrencpn>
<childrencpn></childrencpn>
</div>
</template>
2
3
4
5
6
7
8
9
10
btnClick(){
console.log(this.$refs.children1)
console.log(this.$refs.children2)
}
2
3
4
# 父子组件的访问方式$parent
- 如果我们想在子组件中直接访问父组件,可以通过$parent
- 注意事项:
- 尽管在Vue开发中,我们允许通过$parent来访问父组件,但是在真实开发中尽量不要这样做。 子组件应该尽量避免直接访问父组件的数据,因为这样耦合度太高了。
- 如果我们将子组件放在另外一个组件之内,很可能该父组件没有对应的属性,往往会引起问题。
- 另外,更不好做的是通过$parent直接修改父组件的状态,那么父组件中的状态将变得飘忽不定,很不利于我的调试和维护。
console.log(this.$parent)
console.log(this.$parent.name)
console.log(this.$root)
console.log(this.$root.message)
2
3
4
5
# 非父子组件通信
刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
- 非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。
在Vue1.x的时候,可以通过$dispatch和$broadcast完成
- $dispatch用于向上级派发事件
- $broadcast用于向下级广播事件
- 但是在Vue2.x都被取消了
在Vue2.x中,有一种方案是通过中央事件总线,也就是一个中介来完成。
- 但是这种方案和直接使用Vuex的状态管理方案还是逊色很多。
- 并且Vuex提供了更多好用的功能,所以这里我们暂且不讨论这种方案,后续我们专门学习Vuex的状态管理。
# ref和Props
# ref
- 作用:用于给节点打标识
- 读取方式:
this.$refs.xxxxxx
# props
- 作用:用于父组件给子组件传递数据
- 读取方式一: 只指定名称
props: ['name', 'age', 'setName']
- 读取方式二: 指定名称和类型
props: {
name: String,
age: Number,
setNmae: Function
}
2
3
4
5
- 读取方式三: 指定名称/类型/必要性/默认值
props: {
name: {type: String, required: true, default:xxx},
}
2
3
# 自定义事件
Fu.vue
<template>
<div>
父组件
<hr>
<!--通过父组件给子组件传递函数类型的props实现:子给父传递数据-->
<!--父组件传递给子组件一个方法,子组件接收并调用这个方法,传递参数回来-->
<!--<Zi :getZiName="getZiName"></Zi>-->
<!--通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
<!-- 子组件:this.$emit('abcd',this.name,1,2,3);-->
<!--<Zi @abcd="getZiName"></Zi>-->
<!--<Zi @abcd.once="getZiName"></Zi> 只触发一次-->
<Zi v-on:abcd="getZiName" @demo="test"></Zi>
<!--通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref)-->
<!--<Zi ref="ZiRef"></Zi>-->
</div>
</template>
<script>
import Zi from './Zi'
export default {
name: 'Fu.vue',
components: {Zi},
data() {
return {}
},
methods: {
getZiName(name, ...a) {
console.log("父组件拿到:",name, a)
},
test() {
console.log("demo事件触发了")
}
},
mounted() {
//this.$refs.ZiRef.$on("abcd",this.getZiName) // 绑定自定义事件
// this.$refs.ZiRef.$once("abcd",this.getZiName) // 绑定自定义事件,只绑定一次
}
}
</script>
<style lang="scss" scoped>
</style>
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
Zi.vue
<template>
<div>
父组件
<hr>
<!--通过父组件给子组件传递函数类型的props实现:子给父传递数据-->
<!--父组件传递给子组件一个方法,子组件接收并调用这个方法,传递参数回来-->
<!--<Zi :getZiName="getZiName"></Zi>-->
<!--通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第一种写法,使用@或v-on) -->
<!-- 子组件:this.$emit('abcd',this.name,1,2,3);-->
<!--<Zi @abcd="getZiName"></Zi>-->
<!--<Zi @abcd.once="getZiName"></Zi> 只触发一次-->
<Zi v-on:abcd="getZiName" @demo="test"></Zi>
<!--通过父组件给子组件绑定一个自定义事件实现:子给父传递数据(第二种写法,使用ref)-->
<!--<Zi ref="ZiRef"></Zi>-->
</div>
</template>
<script>
import Zi from './Zi'
export default {
name: 'Fu.vue',
components: {Zi},
data() {
return {}
},
methods: {
getZiName(name, ...a) {
console.log("父组件拿到:",name, a)
},
test() {
console.log("demo事件触发了")
}
},
mounted() {
//this.$refs.ZiRef.$on("abcd",this.getZiName) // 绑定自定义事件
// this.$refs.ZiRef.$once("abcd",this.getZiName) // 绑定自定义事件,只绑定一次
}
}
</script>
<style lang="scss" scoped>
</style>
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
# @click.native
如果想在父组件给子组件绑定一个非自定义方法,就用这种方式
# 全局事件总线
main.js
new Vue({
router,
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线
}
}).$mount('#app')
2
3
4
5
6
7
A.vue
mounted() {
this.$bus.$on('hello',(data) => {
console.log('接收到B组件的data:' + data)
})
},
beforeDestroy() {
this.$bus.off('hello')
}
2
3
4
5
6
7
8
B.vue
methods: {
send() {
this.$bus.$emit('hello',"我是B组件的数据")
}
},
2
3
4
5
# 编译作用域
- 在真正学习插槽之前,我们需要先理解一个概念:编译作用域。
- 官方对于编译的作用域解析比较简单,我们自己来通过一个例子来理解这个概念:
- 我们来考虑下面的代码是否最终是可以渲染出来的:
<my-cpn v-show="isShow"></my-cpn>
中,我们使用了isShow属性。 isShow属性包含在组件中,也包含在Vue实例中。 - 答案:最终可以渲染出来,也就是使用的是Vue实例的属性。 为什么呢?
- 官方给出了一条准则:父组件模板的所有东西都会在父级作用域内编译;子组件模板的所有东西都会在子级作用域内编译。
而我们在使用
<my-cpn v-show="isShow"></my-cpn>
的时候,整个组件的使用过程是相当于在父组件中出现的。 那么他的作用域就是父组件,使用的属性也是属于父组件的属性。 因此,isShow使用的是Vue实例中的属性,而不是子组件的属性。
# 插槽slot
- 组件的插槽:
- 组件的插槽也是为了让我们封装的组件更加具有扩展性。
- 让使用者可以决定组件内部的一些内容到底展示什么。
# slot基本使用
- 在子组件中,使用特殊的元素
<slot>
就可以为子组件开启一个插槽。 - 该插槽插入什么内容取决于父组件如何使用。
<!--
1.插槽的基本使用 <slot></slot>
2.插槽的默认值 <slot>button</slot>
3.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
-->
<div id="app">
<!-- <cpn><h4>我填充在预留的插槽里</h4></cpn> -->
<cpn></cpn>
<cpn><h4>我填充在预留的插槽里,会替换掉默认的button</h4></cpn>
</div>
<template id="cpn">
<div>
<h2>我是组件</h2>
<p>我是组件, 哈哈哈</p>
<!-- <slot></slot> 预留一个插槽 -->
<slot><button>默认值,传值会被替换</button></slot>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 具名插槽slot
当子组件的功能复杂时,子组件的插槽可能并非是一个。
比如我们封装一个导航栏的子组件,可能就需要三个插槽,分别代表左边、中间、右边。
那么,外面在给插槽插入内容时,如何区分插入的是哪一个呢
这个时候,我们就需要给插槽起一个名字
如何使用具名插槽呢?
- 非常简单,只要给slot元素一个name属性即可
<slot name='myslot'></slot>
- 非常简单,只要给slot元素一个name属性即可
<div id="app">
<cpn><span slot="center">中间标题</span></cpn>
<cpn><button slot="left">返回</button></cpn>
</div>
<template id="cpn">
<div>
<slot name="left"><span>左边</span></slot>
<slot name="center"><span>中间</span></slot>
<slot name="right"><span>右边</span></slot>
</div>
</template>
2
3
4
5
6
7
8
9
10
11
12
# 作用域插槽
父组件替换插槽的标签,但是内容由子组件来提供。
<div id="app">
<cpn></cpn>
<cpn>
<!--slot-scope属性里面的内容任意,也可以写scope=“”(这个是旧版的,不推荐使用)-->
<!--支持解构赋值-->
<!--<template slot-scope="{data}">-->
<template slot-scope="slot">
<span>{{slot.data.join('-')}}</span>
</template>
</cpn>
<cpn>
<template slot-scope="abc">
<i v-for="item in abc.data">{{item}} </i>
</template>
</cpn>
</div>
<template id="cpn">
<div>
<slot :data="pLanguages">
<ul>
<li v-for="item in pLanguages">{{item}}</li>
</ul>
</slot>
</div>
</template>
//pLanguages数据在子组件中
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
# 前端模块化
# 匿名函数解决重命问题
# 使用模块作为出口
CommonJS
# export基本使用
# 导出变量
# 导出函数
# export default
# import
使用
# webpack
什么是webpack?
- At its core, webpack is a static module bundler for modern JavaScript applications.
- 从本质上来讲,webpack是一个现代的JavaScript应用的静态模块打包工具。
前端模块化:
- 在前面学习中,我已经用了大量的篇幅解释了为什么前端需要模块化。
- 而且我也提到了目前使用前端模块化的一些方案:AMD、CMD、CommonJS、ES6。
- 在ES6之前,我们要想进行模块化开发,就必须借助于其他的工具,让我们可以进行模块化开发。
- 并且在通过模块化开发完成了项目后,还需要处理模块间的各种依赖,并且将其进行整合打包。
- 而webpack其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。
- 而且不仅仅是JavaScript文件,我们的CSS、图片、json文件等等在webpack中都可以被当做模块来使用(在后续我们会看到)。
- 这就是webpack中模块化的概念。
打包如何理解呢?
- 理解了webpack可以帮助我们进行模块化,并且处理模块间的各种复杂关系后,打包的概念就非常好理解了。
- 就是将webpack中的各种资源模块进行打包合并成一个或多个包(Bundle)。
- 并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将scss转成css,将ES6语法转成ES5语法,将TypeScript转成JavaScript等等操作。
- 但是打包的操作似乎grunt/gulp也可以帮助我们完成,它们有什么不同呢?
# 和grunt/gulp的对比
grunt/gulp的核心是Task
我们可以配置一系列的task,并且定义task要处理的事务(例如ES6、ts转化,图片压缩,scss转成css) 之后让grunt/gulp来依次执行这些task,而且让整个流程自动化。
所以grunt/gulp也被称为前端自动化任务管理工具。
我们来看一个gulp的task
下面的task就是将src下面的所有js文件转成ES5的语法。
并且最终输出到dist文件夹中。
- 什么时候用grunt/gulp呢?
- 如果你的工程模块依赖非常简单,甚至是没有用到模块化的概念。
- 只需要进行简单的合并、压缩,就使用grunt/gulp即可。
- 但是如果整个项目使用了模块化理,而且相互依赖非常强,我们就可以使用更加强大的webpack了。
- 所以,grunt/gulp和webpack有什么不同呢?
- grunt/gulp更加强调的是前端流程的自动化,模块化不是它的核心。
- webpack更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能。
# webpack安装
安装webpack首先需要安装Node.js,Node.js自带了软件包管理工具npm(node packages manger)
- 查看node版本
node -v
- 全局安装webpack(这里我先指定版本号3.6.0,因为vue cli2依赖该版本)
npm install webpack@3.6.0 -g
- 局部安装webpack(后续才需要)
- --save-dev`是开发时依赖,项目打包后不需要继续使用的。
cd 对应目录
npm install webpack@3.6.0 --save-dev
2
为什么全局安装后,还需要局部安装呢?
在终端直接执行webpack命令,使用的全局安装的webpack
当在package.json中定义了scripts时,其中包含了webpack命令,那么使用的是局部webpack
# 准备工作
全局安装webpack
# 创建文件夹
# 初始化包(一路回车)
npm install
# js文件打包
(1)打包命令:(不推荐)
不打包js无法使用,因为包含有import和export
webpack src/main.js dist/bundle.js
打包之后在index.html中引用 <script src="./dist/bundle.js"></script>
(2)另一种打包方式:(配置入口和出口)
webpack配置(文件名:webpack.config.js需要自己建)
const path = require('path')
module.exports = {
//入口,可以是字符串/数组/对象,这里我们入口只有一个,所以写一个字符串即可
entry: './src/main.js',
//出口,通常是一个对象,里面至少包含两个重要性,path和filename
output: {
path: path.resolve(__dirname, 'dist'), //注意:path是一个绝对路径
filename: 'bundle.js'
}
}
2
3
4
5
6
7
8
9
10
11
输入命令webpack即可打包:
webpack
# 局部安装webpack
目前,我们使用的webpack是全局的webpack,如果我们想使用局部来打包呢?
因为一个项目往往依赖特定的webpack版本,全局的版本可能很这个项目的webpack版本不一致,导出打包出现问题。
所以通常一个项目,都有自己局部的webpack。
第一步,项目中需要安装自己局部的webpack 这里我们让局部安装webpack3.6.0
npm install webpack@3.6.0 --save-dev
1Vue CLI3中已经升级到webpack4,但是它将配置文件隐藏了起来,所以查看起来不是很方便。
第二步,通过node_modules/.bin/webpack启动webpack打包
node_modules/.bin/webpack
### package.json中定义启动
* 但是,每次执行都敲这么一长串有没有觉得不方便呢?
* OK,我们可以在package.json的scripts中定义自己的执行脚本。
* package.json中的scripts的脚本在执行时,会按照一定的顺序寻找命令对应的位置。
* 首先,会寻找本地的node_modules/.bin路径中对应的命令。
* 如果没有找到,会去全局的环境变量中寻找。
在psckage.json中配置"build":"webpack"
```json
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack"
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
以后就可以通过npm run build来导包
npm run build
# loader
- loader是webpack中一个非常核心的概念。
- webpack用来做什么呢?
- 在我们之前的实例中,我们主要是用webpack来处理我们写的js代码,并且webpack会自动处理js之间相关的依赖。
- 但是,在开发中我们不仅仅有基本的js代码处理,我们也需要加载css、图片,也包括一些高级的将ES6转成ES5代码,将TypeScript转成ES5代码,将scss、less转成css,将.jsx、.vue文件转成js文件等等。 对于webpack本身的能力来说,对于这些转化是不支持的。
- 那怎么办呢?给webpack扩展对应的loader就可以啦。
- loader使用过程:
- 步骤一:通过npm安装需要使用的loader
- 步骤二:在webpack.config.js中的modules关键字下进行配置 大部分loader我们都可以在webpack的官网中找到,并且学习对应的用法。
- 官网:https://www.webpackjs.com/concepts/loaders/
# css文件处理
- 项目开发过程中,我们必然需要添加很多的样式,而样式我们往往写到一个单独的文件中。
- 在src目录中,创建一个css文件,其中创建一个normal.css文件。
- 我们也可以重新组织文件的目录结构,将零散的js文件放在一个js文件夹中。
- normal.css中的代码非常简单,就是将body设置为red
- 但是,这个时候normal.css中的样式会生效吗? 当然不会,因为我们压根就没有引用它。 webpack也不可能找到它,因为我们只有一个入口,webpack会从入口开始查找其他依赖的文件。
main.js导入css
//引入css
require('./css/normal.css')
2
加载css文件必须有对应的loader
npm install --save-dev css-loader
npm install --save-dev style-loader
2
配置webpack.config.js
module: {
rules: [
{ test: /\.css$/,
use: ['style-loader','css-loader']
},
]
}
2
3
4
5
6
7
出现错误:原因版本不匹配
npm install --save-dev css-loader@3.3.0
npm install --save-dev style-loader@1.0.0
2
# less文件处理
导入less文件
require('./css/index.less')
//或者
import index from './css/index.less'
2
3
less文件
@fontSize: 50px;
@fontColor: red;
body {
font-size: @fontSize;
color: @fontColor;
}
2
3
4
5
6
7
安装lessloader
//这个版本不匹配
npm install --save-dev less-loader less
npm install --save-dev less-loader@4.1.0
2
3
配置webpack.config.js
{
test: /\.less$/,
use: [{
loader: "style-loader", // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader", // compiles Less to CSS
}]
}
2
3
4
5
6
7
8
9
10
# 图片文件处理
url-loader
npm install --save-dev url-loader
npm install --save-dev url-loader@1.1.2
2
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 8192,
name: 'img/[name].[hash:8].[ext]'
},
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
file-loader
因为大于8kb的图片,会通过file-loader进行处理,但是我们的项目中并没有file-loader
npm install --save-dev file-loader
npm install --save-dev file-loader@1.1.5
2
# 修改文件名称
我们发现webpack自动帮助我们生成一个非常长的名字
这是一个32位hash值,目的是防止名字重复
但是,真实开发中,我们可能对打包的图片名字有一定的要求
比如,将所有的图片放在一个文件夹中,跟上图片原来的名称,同时也要防止重复
所以,我们可以在options中添加上如下选项:
- img:文件要打包到的文件夹
- name:获取图片原来的名字,放在该位置
- hash:8:为了防止图片名称冲突,依然使用hash,但是我们只保留8位
- ext:使用图片原来的扩展名
但是,我们发现图片并没有显示出来,这是因为图片使用的路径不正确
默认情况下,webpack会将生成的路径直接返回给使用者
但是,我们整个程序是打包在dist文件夹下的,所以这里我们需要在路径下再添加一个dist/
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 8192,
name: 'img/[name].[hash:8].[ext]'
},
}
]
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# es6语法处理
- 如果希望将ES6的语法转成ES5,那么就需要使用babel。
- 而在webpack中,我们直接使用babel对应的loader就可以了。
npm install --save-dev babel-loader@7 babel-core@6.26.3 babel-preset-es2015@6.24.1
webpack.config.js
,
{
test: /\.js$/,
// exclude: 排除
// include: 包含
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
# 引入Vue.js
- 我们希望在项目中使用Vuejs,那么必然需要对其有依赖,所以需要先进行安装
- 注:因为我们后续是在实际项目中也会使用vue的,所以并不是开发时依赖
npm install vue --save
npm install vue@2.5.21 --save
2
安装vue-loader和vue-template-compiler
如果vue-loader超过15以后需要配置VueLoaderPlugin
npm install vue-loader vue-template-compiler --save-dev
//下面版本可以用
npm install vue-loader@13.0.0 vue-template-compiler@2.5.21 --save-dev
2
3
4
修改webpack.config.js的配置文件:
,
{
test: /\.vue$/,
use: ['vue-loader']
}
2
3
4
5
runtime-only ->代码中,不可以有任何的template
runtime-complier ->代码中,可以有template,因为有complier可以编译template
解决方案:
修改webpack.config.js的配置文件:放在module:的外面和这个是兄弟关系
resolve: {
// alias: 别名
extensions: ['.js', '.css', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
}
2
3
4
5
6
7
# 补充:el和template区别
# Plugin的使用
# 添加版权的Plugin
- 我们先来使用一个最简单的插件,为打包的文件添加版权声明
- 该插件名字叫BannerPlugin,属于webpack自带的插件。
const webpack = require('webpack')
plugins: [
new webpack.BannerPlugin('最终版权归aaa所有'),
],
2
3
4
# 打包html的plugin
npm install html-webpack-plugin --save-dev
npm install html-webpack-plugin@3.2.0 --save-dev
2
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins: [
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
}),
new UglifyjsWebpackPlugin()
],
//注意:
output: {
path: path.resolve(__dirname, 'dist'), //注意:path是一个绝对路径
filename: 'bundle.js',
// publicPath: 'dist/' 安装html-webpack-plugin需要删去
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# js压缩的Plugin
npm install uglifyjs-webpack-plugin@1.1.1 --save-dev
2
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
plugins: [
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
}),
new UglifyjsWebpackPlugin()
],
2
3
4
5
6
7
8
# 搭建本地服务器
webpack提供了一个可选的本地开发服务器,这个本地服务器基于node.js搭建,内部使用express框架,可以实现我们想要的让浏览器自动刷新显示我们修改后的结果。
不过它是一个单独的模块,在webpack中使用之前需要先安装它
npm install --save-dev webpack-dev-server@2.9.1
1devserver也是作为webpack中的一个选项,选项本身可以设置如下属性:
contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
port:端口号
inline:页面实时刷新
historyApiFallback:在SPA页面中,依赖HTML5的history模式
webpack.config.js文件配置修改如下:(和mudule还有plugins是兄弟关系)
devServer: { contentBase: './dist', inline: true }
1
2
3
4
5我们可以再配置另外一个scripts: --open参数表示直接打开浏览器
"scripts": { "test": "echo \"Error: no test specified\" && exit 1", "build": "webpack", "dev": "webpack-dev-server --open" },
1
2
3
4
5
以后运行命令直接改为:自动在浏览器打开
npm run dev
# 配置文件的分离
安装:
npm install webpack-merge --save-dev
npm install webpack-merge@4.1.5 --save-dev
2
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/prod.config.js",
"dev": "webpack-dev-server --open --config ./build/dev.config.js"
},
2
3
4
5
base.config.js
const path = require('path')
const webpack = require('webpack')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
module.exports = {
entry: './src/main.js',
output: {
path: path.resolve(__dirname, '../dist'),
filename: 'bundle.js',
// publicPath: 'dist/'
},
module: {
rules: [
{
test: /\.css$/,
// css-loader只负责将css文件进行加载
// style-loader负责将样式添加到DOM中
// 使用多个loader时, 是从右向左
use: [ 'style-loader', 'css-loader' ]
},
{
test: /\.less$/,
use: [{
loader: "style-loader", // creates style nodes from JS strings
}, {
loader: "css-loader" // translates CSS into CommonJS
}, {
loader: "less-loader", // compiles Less to CSS
}]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'url-loader',
options: {
// 当加载的图片, 小于limit时, 会将图片编译成base64字符串形式.
// 当加载的图片, 大于limit时, 需要使用file-loader模块进行加载.
limit: 13000,
name: 'img/[name].[hash:8].[ext]'
},
}
]
},
{
test: /\.js$/,
// exclude: 排除
// include: 包含
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['es2015']
}
}
},
{
test: /\.vue$/,
use: ['vue-loader']
}
]
},
resolve: {
// alias: 别名
extensions: ['.js', '.css', '.vue'],
alias: {
'vue$': 'vue/dist/vue.esm.js'
}
},
plugins: [
new webpack.BannerPlugin('最终版权归aaa所有'),
new HtmlWebpackPlugin({
template: 'index.html'
})
]
}
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
dev.config.js
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')
module.exports = webpackMerge(baseConfig, {
devServer: {
contentBase: './dist',
inline: true
}
})
2
3
4
5
6
7
8
9
10
prod.config.js
const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
const webpackMerge = require('webpack-merge')
const baseConfig = require('./base.config')
module.exports = webpackMerge(baseConfig, {
plugins: [
new UglifyjsWebpackPlugin()
]
})
2
3
4
5
6
7
8
9
package.json
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "main.js",
"dependencies": {
"vue": "^2.5.21",
"webpack": "^3.6.0"
},
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-es2015": "^6.24.1",
"css-loader": "^3.3.0",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^3.2.0",
"install": "^0.13.0",
"less": "^4.1.3",
"less-loader": "^4.1.0",
"npm": "^8.13.2",
"style-loader": "^1.0.0",
"uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^1.1.2",
"vue-loader": "^13.0.0",
"vue-template-compiler": "^2.5.21",
"webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.5"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack --config ./build/prod.config.js",
"dev": "webpack-dev-server --open --config ./build/dev.config.js"
},
"author": "",
"license": "ISC"
}
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
# Vue Cli
如果你在开发大型项目, 那么你需要, 并且必然需要使用Vue CLI
使用Vue.js开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。
- 如果每个项目都要手动完成这些工作,那无以效率比较低效,所以通常我们会使用一些脚手架工具来帮助完成这些事情。
CLI是什么意思?
- CLI是Command-Line Interface, 翻译为命令行界面, 但是俗称脚手架.
- Vue CLI是一个官方发布 vue.js 项目脚手架
- 使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置.
# Vue Cli的使用前提-Node
安装NodeJS
可以直接在官方网站中下载安装.
网址: http://nodejs.cn/download/
检测安装的版本 默认情况下自动安装Node和NPM
node -v
Node环境要求8.9以上或者更高版本
- 什么是NPM呢?
- NPM的全称是Node Package Manager
- 是一个NodeJS包管理和分发工具,已经成为了非官方的发布Node模块(包)的标准。
- 后续我们会经常使用NPM来安装一些开发过程中依赖包.
# cnpm安装
由于国内直接使用 npm 的官方镜像是非常慢的,这里推荐使用淘宝 NPM 镜像。
你可以使用淘宝定制的 cnpm (gzip 压缩支持) 命令行工具代替默认的 npm:
npm install -g cnpm --registry=https://registry.npm.taobao.org
1这样就可以使用 cnpm 命令来安装模块了:
cnpm install [name]
1
# Vue Cli的使用前提-webpack
Vue.js官方脚手架工具就使用了webpack模板
- 对所有的资源会压缩等优化操作
- 它在开发过程中提供了一套完整的功能,能够使得我们开发过程中变得高效。
Webpack的全局安装
npm install webpack -g
1
# Vue Cli的使用
安装脚手架:
npm install -g @vue/cli
注意:上面安装的是Vue CLI3的版本,如果需要想按照Vue CLI2的方式初始化项目时不可以的。
Vue CLI2初始化项目
npm install -g @vue/cli-init # `vue init` 的运行效果将会跟 `vue-cli@2.x` 相同 vue init webpack my-project
1
2
3Vue CLI3初始化项目
vue create my-project
1
# 创建Vue Cli2项目
目录结构详解
# 不使用Eslint验证
# Runtime-Compiler和Runtime-only的区别
- 如果在之后的开发中,你依然使用template,就需要选择Runtime-Compiler
- 如果你之后的开发中,使用的是.vue文件夹开发,那么可以选择Runtime-only
# Vue运行过程
parse:解析
ast:抽象语法树(abstract syntax code,AST)
# render函数的使用
main.js
new Vue({
el: '#app',
// components: { App },
// template: '<App/>'
render: (createElement) => {
// 使用方式一: return createElement('标签', '相关数据对象(可以不传)', ['内容数组'])
// return createElement('div', {class: 'box'}, ['我是div中的内容'])
// 嵌套render函数
return createElement('div', {class: 'box'}, [createElement('h2', {class: 'h2box'}, ['我是render函数里面嵌套的h2'])])
}
})
2
3
4
5
6
7
8
9
10
11
12
使用方式二:
const cpn = Vue.component('cpn', {
template: '<div>我是cpn组件</div>',
data () {
return {}
}
})
/* eslint-disable no-new */
new Vue({
el: '#app',
// components: { App },
// template: '<App/>'
render: (createElement) => {
// 使用方式一: return createElement('标签', '相关数据对象(可以不传)', ['内容数组'])
// return createElement('div', {class: 'box'}, ['我是div中的内容'])
// 嵌套render函数
// return createElement('div', {class: 'box'}, [createElement('h2', {class: 'h2box'}, ['我是render函数里面嵌套的h2'])])
// 使用方式二: 传入一个组件对象
return createElement(cpn)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
最终方式
import Vue from 'vue'
import App from './App'
Vue.config.productionTip = false
new Vue({
el: '#app',
// components: { App },
// template: '<App/>'
render: (createElement) => {
return createElement(App)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
运行
npm run dev
# Vue Cli3
- vue-cli 3 与 2 版本有很大区别
- vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
- vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,build和config等目录
- vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
- 移除了static文件夹,新增了public文件夹,并且index.html移动到public中
创建vue cli3项目
vue create 项目名
采用手动选择,配置分多个文件
删掉自定义配置:
# 运行
npm run serve
# 编译
npm run build
# 目录结构详解
# 图形化管理界面
启动命令
vue ui
# Vue-Router
路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动. --- 维基百科
# 后端路由
早期的网站开发整个HTML页面是由服务器来渲染的.
- 服务器直接生产渲染好对应的HTML页面, 返回给客户端进行展示.
但是, 一个网站, 这么多页面服务器如何处理呢?
一个页面有自己对应的网址, 也就是URL.
URL会发送到服务器, 服务器会通过正则对该URL进行匹配, 并且最后交给一个Controller进行处理. Controller进行各种处理, 最终生成HTML或者数据, 返回给前端.
这就完成了一个IO操作
上面的这种操作, 就是后端路由.
当我们页面中需要请求不同的路径内容时, 交给服务器来进行处理, 服务器渲染好整个页面, 并且将页面返回给客户端.
这种情况下渲染好的页面, 不需要单独加载任何的js和css, 可以直接交给浏览器展示, 这样也有利于SEO的优化.
后端路由的缺点:
一种情况是整个页面的模块由后端人员来编写和维护的.
另一种情况是前端开发人员如果要开发页面, 需要通过PHP和Java等语言来编写页面代码.
而且通常情况下HTML代码和数据以及对应的逻辑会混在一起, 编写和维护都是非常糟糕的事情.
# 前端路由
- 前后端分离阶段:
- 随着Ajax的出现, 有了前后端分离的开发模式.
- 后端只提供API来返回数据, 前端通过Ajax获取数据, 并且可以通过JavaScript将数据渲染到页面中.
- 这样做最大的优点就是前后端责任的清晰, 后端专注于数据上, 前端专注于交互和可视化上.
- 并且当移动端(iOS/Android)出现后, 后端不需要进行任何处理, 依然使用之前的一套API即可.
- 目前很多的网站依然采用这种模式开发.
- 单页面富应用阶段:
- 其实SPA最主要的特点就是在前后端分离的基础上加了一层前端路由.
- 也就是前端来维护一套路由规则.
- 前端路由的核心是什么呢?
- 改变URL,但是页面不进行整体的刷新。
# URL的hash
URL的hash也就是锚点(#), 本质上是改变window.location的href属性.
我们可以通过直接赋值location.hash来改变href, 但是页面不发生刷新
方法 含义 window.location.href:"url" 在本页跳转到url所指的链接 window.location.replace:"url" 用新的url替换原先的路径 window.location.reload() 强制刷新页面,重新向服务端发送请求 location.href:"url" 在本页跳转到链接地址 parent.location.href:"url" 跳转到上一层页面的指定url链接 top.location.href:"url" 在最外层页面上进行跳转 window.location.href:这种请求方式,需要刷新整个界面,故而用户体验度不好。但是在文件下载的时候,却只能用这种方式发送请求,ajax请求得不到响应。
# HTML5的history模式
- history接口是HTML5新增的, 它有五种模式改变URL而不刷新页面.
history.pushState()
history.replaceState()
history.go()
history.back() == history.go(-1)
history.forword() == history.go(1)
# 认识Vue-Router
- 目前前端流行的三大框架, 都有自己的路由实现:
- Angular的ngRouter
- React的ReactRouter
- Vue的vue-router
- vue-router是Vue.js官方的路由插件,它和vue.js是深度集成的,适合用于构建单页面应用。
- 官方网站: https://router.vuejs.org/zh/
- vue-router是基于路由和组件的
- 路由用于设定访问路径, 将路径和组件映射起来.
- 在vue-router的单页面应用中, 页面的路径的改变就是组件的切换.
# 安装和使用Vue-Router
步骤一: 安装vue-router
npm install vue-router --save
1步骤二: 在模块化工程中使用它(因为是一个插件, 所以可以通过Vue.use()来安装路由功能)
第一步:导入路由对象,并且调用 Vue.use(VueRouter)
import Vue from 'vue' import VueRouter from 'vue-router' // 1.注入插件 Vue.use(VueRouter)
1
2
3
4
5第二步:创建路由实例,并且传入路由映射配置
// 2.定义路由 const routers = [] // 3.创建路由实例 const router = new VueRouter({ routers }) // 4.导出router实例 export default router
1
2
3
4
5
6
7
8
9
10第三步:在Vue实例中挂载创建的路由实例(main.js)
import router from './router' new Vue({ router, // 路由挂载到Vue实例中 render: h => h(App), }).$mount('#app')
1
2
3
4
5
6
使用vue-router的步骤: 第一步: 创建路由组件 第二步: 配置路由映射: 组件和路径映射关系
// 2.定义路由 const routers = [ { path: '/home', component: Home }, { path: '/about', component: About } ]
1
2
3
4
5
6
7
8
9
10
11第三步: 使用路由: 通过
<router-link>
和<router-view>
(App.vue)<div id="app"> <router-link to="/home">首页</router-link> <router-link to="/about">关于</router-link> <router-view></router-view> </div>
1
2
3
4
5
src-->router-->index.js(没有文件可以自己创建)
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../components/Home.vue'
import About from '../components/About.vue'
// 1.注入插件
Vue.use(VueRouter)
// 2.定义路由
const routers = [
{
path: '/home',
component: Home
},
{
path: '/about',
component: About
}
]
// 3.创建路由实例
const router = new VueRouter({
routers
})
// 4.导出router实例
export default router
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
App.vue
<template>
<div id="app">
<router-link to="/home">首页</router-link>
<router-link to="/about">关于</router-link>
<router-view></router-view>
</div>
</template>
<script>
export default {
name: 'App',
components: {
}
}
</script>
<style>
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<router-link>
: 该标签是一个vue-router中已经内置的组件, 它会被渲染成一个<a>
标签.
<router-view>
: 该标签会根据当前的路径, 动态渲染出不同的组件.
网页的其他内容, 比如顶部的标题/导航, 或者底部的一些版权信息等会和<router-view>
处于同一个等级.
在路由切换时, 切换的是<router-view>
挂载的组件, 其他内容不会发生改变.
# 路由的默认路径
- 配置解析:
- 我们在routes中又配置了一个映射.
- path配置的是根路径: /
- redirect是重定向, 也就是我们将根路径重定向到/home的路径下, 这样就可以得到我们想要的结果了.
# 改变路径的hash模式
当配置了代理服务器后,哈希模式不会向服务器端发送请求,而历史模式可能会
哈希模式的兼容性更好
我们前面说过改变路径的方式有两种:
URL的hash
HTML5的history
默认情况下, 路径的改变使用的URL的hash.
如果希望使用HTML5的history模式, 非常简单, 进行如下配置即可:
// 3.创建路由实例
const router = new VueRouter({
routes,
mode: 'history'
})
2
3
4
5
- 对于一个url来说,什么是hash值?—— #及其后面的内容就是hash值。
- hash值不会包含在 HTTP 请求中,即:hash值不会带给服务器。
- hash模式:
- 地址中永远带着#号,不美观 。
- 若以后将地址通过第三方手机app分享,若app校验严格,则地址会被标记为不合法。
- 兼容性较好。
- history模式:
- 地址干净,美观 。
- 兼容性和hash模式相比略差。
- 应用部署上线时需要后端人员支持,解决刷新页面服务端404的问题。
# router-link属性
在前面的
<router-link>
中, 我们只是使用了一个属性: to, 用于指定跳转的路径.<router-link>
还有一些其他属性: tag: tag可以指定<router-link>
之后渲染成什么组件, 比如上面的代码会被渲染成一个<li>
元素, 而不是<a>
<router-link to="/home" tag="button">首页</router-link> <router-link to="/about" tag="button">关于</router-link>
1
2replace: replace不会留下history记录, 所以指定replace的情况下, 后退键返回不能返回到上一个页面中
<router-link to="/home" tag="button" replace>首页</router-link> <router-link to="/about" tag="button" replace>关于</router-link>
1
2active-class: 当
<router-link>
对应的路由匹配成功时, 会自动给当前元素设置一个router-link-active的class, 设置active-class可以修改默认的名称.- 在进行高亮显示的导航菜单或者底部tabbar时, 会使用到该类.
- 但是通常不会修改类的属性, 会直接使用默认的router-link-active即可.
// 3.创建路由实例
const router = new VueRouter({
routes,
mode: 'history',
// 修改linkActiveClass
linkActiveClass: 'active'
})
2
3
4
5
6
7
8
路由切换,未被用到的组件会被销毁
# 路由代码跳转
this.$router.push('/home')
this.$router.replace('/home')
2
# 获取当前路由($route)
this.$route.path
点击相同路由报错解决方案:https://blog.csdn.net/weixin_44196222/article/details/125565441?spm=1001.2014.3001.5502
# 动态路由
- 在某些情况下,一个页面的path路径可能是不确定的,比如我们进入用户界面时,希望是如下的路径: /user/aaaa或/user/bbbb 除了有前面的/user之外,后面还跟上了用户的ID 这种path和Component的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)。
{
path: '/user/:userId',
component: User
}
2
3
4
获取参数:(上面如果写abc,这里也要写abc)
this.$route.params.userId
# 路由懒加载
- 官方给出了解释:
- 当打包构建应用时,Javascript 包会变得非常大,影响页面加载。
- 如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了
- 官方在说什么呢?
- 首先, 我们知道路由中通常会定义很多不同的页面.
- 这个页面最后被打包在哪里呢? 一般情况下, 是放在一个js文件中.
- 但是, 页面这么多放在一个js文件中, 必然会造成这个页面非常的大.
- 如果我们一次性从服务器请求下来这个页面, 可能需要花费一定的时间, 甚至用户的电脑上还出现了短暂空白的情况. 如何避免这种情况呢? 使用路由懒加载就可以了.
- 路由懒加载做了什么?
- 路由懒加载的主要作用就是将路由对应的组件打包成一个个的js代码块.
- 只有在这个路由被访问到的时候, 才加载对应的组件
# 懒加载的方式
- 方式一: 结合Vue的异步组件和Webpack的代码分析.
const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) })};
- 方式二: AMD写法
const About = resolve => require(['../components/About.vue'], resolve);
- 方式三: 在ES6中, 我们可以有更加简单的写法来组织Vue异步组件和Webpack的代码分割.
const Home = () => import('../components/Home.vue')
未使用路由懒加载打包:
使用路由懒加载后打包:(会把每个路由单独打包到一个js文件里)
const Home = () => import('../components/Home.vue')
const About = () => import('../components/About.vue')
const User = () => import('../components/User.vue')
2
3
# 嵌套路由
- 实现嵌套路由有两个步骤:
- 创建对应的子组件, 并且在路由映射中配置对应的子路由.
- 在组件内部使用
<router-view>
标签.
// 2.定义路由
const routes = [
{
path: '/',
// 重定向
redirect: '/home'
},
{
path: '/home',
component: Home,
children:[
{
path: '',
redirect: 'news'
},
{
path: 'news',
component: HomeNews
},
{
path: 'message',
component: HomeMessage
}
]
}
]
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
# 路由传递参数
# 传递参数的方式:
传递参数主要有两种类型: params和query
params的类型:
- 配置路由格式: /router/:id
- 传递的方式: 在path后面跟上对应的值
- 传递后形成的路径: /router/123, /router/ab
<router-link :to="'/user/'+userId" tag="button" >用户</router-link> <button @click="userClick">用户</button>
1
2userClick() { this.$router.push('/user/'+ this.userId) },
1
2
3
query的类型:
- 配置路由格式: /router, 也就是普通配置
- 传递的方式: 对象中使用query的key作为传递方式
- 传递后形成的路径: /router?id=123, /router?id=abc
<router-link :to="{path: '/profile', query: {name: 'zhangsan', age: 19, height: 188}}" tag="button">档案</router-link> <button @click="profileClick">档案</button>
1
2profileClick () { this.$router.push({ path: '/profile' + 123, query: { name: 'zhangsan', age: 18 } }) },
1
2
3
4
5
6
7
8
9
获取
this.$route.query.参数名
# 命名路由
作用:简化路由的跳转
const routes = [
{
name: 'home'
path: '/home',
component: Home,
}
]
2
3
4
5
6
7
使用
<router-link :to="{name:'home'}" >主页</router-link>
# 路由组件的props配置
让路由组件更方便的接收参数
# 组件激活与失活
两个函数: activated,deactivated
# $route和$router的区别
- $route和$router是有区别的
- $router为VueRouter实例,想要导航到不同URL,则使用$router.push方法
- $route为当前router跳转对象里面可以获取name、path、query、params等
# 全局导航守卫
我们来考虑一个需求: 在一个SPA应用中, 如何改变网页的标题呢?
- 网页标题是通过
<title>
来显示的, 但是SPA只有一个固定的HTML, 切换不同的页面时, 标题并不会改变. 但是我们可以通过JavaScript来修改<title>
的内容.window.document.title = '新的标题'.
- 网页标题是通过
普通的修改方式:
我们比较容易想到的修改标题的位置是每一个路由对应的组件.vue文件中.
通过mounted声明周期函数, 执行对应的代码进行修改即可.
但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码).
created () { document.title = "关于" }
1
2
3
有没有更好的办法呢? 使用导航守卫即可.
什么是导航守卫?
- vue-router提供的导航守卫主要用来监听监听路由的进入和离开的.
- vue-router提供了beforeEach和afterEach的钩子函数, 它们会在路由即将改变前和改变后触发.
导航钩子的三个参数解析:
- to: 即将要进入的目标的路由对象.
- from: 当前导航即将要离开的路由对象.
- next: 调用该方法后, 才能进入下一个钩子.
// 前置路由导航守卫
router.beforeEach((to, from, next) => {
window.document.title = to.matched[0].meta.title
console.log(to)
next()
})
// 后置路由导航守卫
router.afterEach((to, from) => {
document.title = to.meta.title
})
2
3
4
5
6
7
8
9
10
官网:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E8%B7%AF%E7%94%B1%E7%8B%AC%E4%BA%AB%E7%9A%84%E5%AE%88%E5%8D%AB
# keep-alive
- keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染。
- 它们有两个非常重要的属性:
- include - 字符串或正则表达,只有匹配的组件会被缓存
- exclude - 字符串或正则表达式,任何匹配的组件都不会被缓存
- router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存:
<keep-alive exclude="User,Profile"> <!-- 逗号之间不要空格 -->
<router-view>
<!-- 所有路径匹配到的视图组件都会被缓存 -->
</router-view>
</keep-alive>
2
3
4
5
exclude,里面的属性是这里的name
实现保持默认状态:(保持上一个选中的)
activated() {
this.$router.push(this.path)
},
beforeRouteLeave (to, from, next) {
this.path = this.$route.path
next()
}
2
3
4
5
6
7
# 独享导航守卫
写在routes里面,只有一个beforeEnter方法
# 组件导航守卫
写在组件里面,两个方法 beforeRouteEnter, beforeRouteLeave
# TabBar
创建项目
vue create 项目名称
- 如果在下方有一个单独的TabBar组件,你如何封装 自定义TabBar组件,在APP中使用 让TabBar出于底部,并且设置相关的样式
- TabBar中显示的内容由外界决定 定义插槽 flex布局平分TabBar
- 自定义TabBarItem,可以传入 图片和文字 定义TabBarItem,并且定义两个插槽:图片、文字。 给两个插槽外层包装div,用于设置样式。 填充插槽,实现底部TabBar的效果
- 4.传入 高亮图片
定义另外一个插槽,插入active-icon的数据
定义一个变量isActive,通过
v-show
来决定是否显示对应的icon - 5.TabBarItem绑定路由数据
安装路由:
npm install vue-router —save
完成router/index.js的内容,以及创建对应的组件 main.js中注册router APP中加入<router-view>
组件 - 6.点击item跳转到对应路由,并且动态决定isActive
监听item的点击,通过
this.$router.replace()
替换路由路径 通过this.$route.path.indexOf(this.link) !== -1
来判断是否是active - 7.动态计算active样式
封装新的计算属性:
this.isActive ? {'color': 'red'} : {}
# MainTab
<template>
<div>
<tab-bar>
<tab-bar-item path="/home" activeColor="#FF5777">
<img slot="item-icon" src="~img/tabbar/home.svg">
<img slot="item-icon-active" src="~img/tabbar/home_active.svg"/>
<div slot="item-text">首页</div>
</tab-bar-item>
<tab-bar-item path="/category" activeColor="#FF5777">
<img slot="item-icon" src="~img/tabbar/category.svg">
<img slot="item-icon-active" src="~img/tabbar/category_active.svg"/>
<div slot="item-text">分类</div>
</tab-bar-item>
<tab-bar-item path="/cart" activeColor="#FF5777">
<img slot="item-icon" src="~img/tabbar/shopcart.svg">
<img slot="item-icon-active" src="~img/tabbar/shopcart_active.svg"/>
<div slot="item-text">购物车</div>
</tab-bar-item>
<tab-bar-item path="/profile" activeColor="#FF5777">
<img slot="item-icon" src="~img/tabbar/profile.svg">
<img slot="item-icon-active" src="~img/tabbar/profile_active.svg"/>
<div slot="item-text">我的</div>
</tab-bar-item>
</tab-bar>
</div>
</template>
<script>
import TabBar from '../tabbar/TabBar.vue'
import TabBarItem from '../tabbar/TabBarItem.vue'
export default {
name: 'MianTab',
components: {
TabBar,
TabBarItem
}
}
</script>
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
# TabBar
<template>
<div class="tab-bar">
<slot/>
</div>
</template>
<script>
export default {
name: 'TabBar'
}
</script>
<style scoped>
.tab-bar {
display: flex;
background-color: #f6f6f6;
position: fixed;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 -1px 1px rgba(100, 100, 100, .2);
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# TabBarItem
<template>
<div class="tab-bar-item" @click="itemClick">
<div v-if="!isActive"><slot name="item-icon"></slot></div>
<div v-else><slot name="item-icon-active"></slot></div>
<div :style="activeStyle"><slot name="item-text"></slot></div>
</div>
</template>
<script>
export default {
name: 'TabBarItem',
props: {
path: String,
activeColor: {
type: String,
default: 'red'
}
},
computed: {
isActive() {
return this.$route.path.indexOf(this.path) !== -1
},
activeStyle() {
return this.isActive ? {color: this.activeColor} : {}
}
},
methods: {
itemClick() {
if (this.path != this.$route.path){
this.$router.push(this.path)
}
}
}
}
</script>
<style scoped>
.tab-bar-item {
flex: 1;
text-align: center;
height: 49px;
font-size: 14px;
}
.tab-bar-item img {
width: 24px;
height: 24px;
margin-top: 3px;
vertical-align: middle;
margin-bottom: 2px;
}
</style>
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
# 修改文件路径别名
vue.config.js(修改完要重启)
const path = require('path')
function resolve(dir) {
return path.join(__dirname, dir);
}
module.exports = {
lintOnSave: false, //去掉EsLint代码规范验证
configureWebpack: {
resolve: {
alias: {
'@': resolve('src'),
'img': resolve('src/assets/img')
}
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Todo-List
App.vue
<template>
<div id="app">
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader :addTodo="addTodo"></MyHeader>
<MyList :todos="todos" :checkTodo="checkTodo" :deleteTodo="deleteTodo"></MyList>
<MyFooter :todos="todos" :checkALLTodo="checkALLTodo" :cleanAllTodo="cleanAllTodo"></MyFooter>
</div>
</div>
</div>
</div>
</template>
<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
export default {
name: 'App',
components: {MyFooter, MyList, MyHeader},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || [] // 没有数据就是空数组,不加【】会变成null,无法使用length函数
}
},
watch:{
todos: {
deep: true, // 深度监视,数组里面集合内容发生变化也可以监视到
handler(value) { // 存储到本地
localStorage.setItem('todos',JSON.stringify(value))
}
}
},
methods: {
// 添加任务
addTodo(todoObj) { // 写一个方法,传递给子组件,然后子组件调用这个方法,传递值回来
this.todos.unshift(todoObj) // 添加任务到任务数组
},
// 完成或取消完成任务
checkTodo(id) { // 爷传父,再传子的方法,用于接收子传给爷的数据
this.todos.forEach((todo)=>{
if(todo.id == id) todo.completed = !todo.completed
})
},
// 删除任务
deleteTodo(id) {
if(confirm("确定要删除吗?")){
this.todos = this.todos.filter(todo => todo.id != id)
}
},
// 选中全部
checkALLTodo(done) {
this.todos.forEach((todo) => {
todo.completed = done
})
},
// 清除已完成任务
cleanAllTodo() {
this.todos = this.todos.filter(todo => !todo.completed)
}
}
}
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
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
MyHeader.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name: 'MyHeader',
props: ['addTodo'], // 接收父组件传过来的方法
data() {
return {
title: ''
}
},
methods: {
add() {
if(!this.title.trim()) {
alert("输入的任务不能为空")
return
}
const todoObj = {
id: nanoid(), // 生成一个唯一id
name: this.title,
completed: false
}
this.addTodo(todoObj) // 将生成的任务,传递回去给父组件
this.title = ''
}
}
}
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
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
MyList.vue
<template>
<ul class="todo-main">
<MyItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo = "todoObj"
:deleteTodo = "deleteTodo"
:checkTodo="checkTodo"
></MyItem>
</ul>
</template>
<script>
import MyItem from "@/components/MyItem";
export default {
name: 'MyList',
components: {MyItem},
props:['todos','checkTodo',"deleteTodo"]
}
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
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
MyItem.vue
<template>
<li>
<label>
<input type="checkbox" :checked="todo.completed" @change="handlerCheck(todo.id)"/>
<!--这种方式也能实现修改propos,但是vue检测不到,不推荐这样做,子组件不要直接修改父组件传过来的值-->
<!--<input type="checkbox" v-model="todo.completed" />-->
<span>{{ todo.name }}</span>
</label>
<button class="btn btn-danger" @click="handlerDelete(todo.id)">删除</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
props:['todo','checkTodo','deleteTodo'],
methods: {
handlerCheck(id) {
this.checkTodo(id) // 将勾选框发生变化的id传给爷爷
},
handlerDelete(id) {
this.deleteTodo(id);
}
}
}
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
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
MyFooter.vue
<template>
<div class="todo-footer" v-if="total">
<label>
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{doneTotal}}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="cleanAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
props: ['todos','checkALLTodo', 'cleanAllTodo'],
methods: {
cleanAll() {
this.cleanAllTodo()
}
},
computed: {
total() {
return this.todos.length
},
doneTotal(){
return this.todos.reduce((pre,todo) => {
return pre + (todo.completed == true ? 1: 0)
},0)
},
isAll: {
get() {
return this.total == this.doneTotal && this.total > 0
},
set(value) {
this.checkALLTodo(value)
}
}
}
}
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
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
# 利用全局事件总线和自定义事件版本
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App),
beforeCreate() {
Vue.prototype.$bus = this // 安装全局事件总线
}
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
12
13
14
App.vue
<template>
<div id="app">
<div id="root">
<div class="todo-container">
<div class="todo-wrap">
<MyHeader @addTodo="addTodo"></MyHeader>
<MyList :todos="todos"></MyList>
<MyFooter :todos="todos" @checkALLTodo="checkALLTodo" @cleanAllTodo="cleanAllTodo"></MyFooter>
</div>
</div>
</div>
<Fu></Fu>
</div>
</template>
<script>
import MyHeader from "@/components/MyHeader";
import MyList from "@/components/MyList";
import MyFooter from "@/components/MyFooter";
import Fu from "@/views/Fu";
export default {
name: 'App',
components: {MyFooter, MyList, MyHeader,Fu},
data() {
return {
todos: JSON.parse(localStorage.getItem('todos')) || [] // 没有数据就是空数组,不加【】会变成null,无法使用length函数
}
},
watch:{
todos: {
deep: true, // 深度监视,数组里面集合内容发生变化也可以监视到
handler(value) { // 存储到本地
localStorage.setItem('todos',JSON.stringify(value))
}
}
},
mounted() {
this.$bus.$on('checkTodo',this.checkTodo) // 这里不需要写参数
this.$bus.$on('deleteTodo',this.deleteTodo)
this.$bus.$on('updateTodo',this.updateTodo)
},
beforeDestroy() {
this.$bus.$off(['checkTodo','checkTodo','updateTodo'])
},
methods: {
// 添加任务
addTodo(todoObj) { // 写一个方法,传递给子组件,然后子组件调用这个方法,传递值回来
this.todos.unshift(todoObj) // 添加任务到任务数组
},
// 完成或取消完成任务
checkTodo(id) { // 爷传父,再传子的方法,用于接收子传给爷的数据
this.todos.forEach((todo)=>{
if(todo.id == id) todo.completed = !todo.completed
})
},
// 删除任务
deleteTodo(id) {
if(confirm("确定要删除吗?")){
this.todos = this.todos.filter(todo => todo.id != id)
}
},
updateTodo(id, name) {
this.todos.forEach( (todo) => {
if (todo.id == id) {
todo.name = name
}
})
},
// 选中全部
checkALLTodo(done) {
this.todos.forEach((todo) => {
todo.completed = done
})
},
// 清除已完成任务
cleanAllTodo() {
this.todos = this.todos.filter(todo => !todo.completed)
}
}
}
</script>
<style>
/*base*/
body {
background: #fff;
}
.btn {
display: inline-block;
padding: 4px 12px;
margin-bottom: 0;
font-size: 14px;
line-height: 20px;
text-align: center;
vertical-align: middle;
cursor: pointer;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.2), 0 1px 2px rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.btn-danger {
color: #fff;
background-color: #da4f49;
border: 1px solid #bd362f;
}
.btn-edit {
color: #fff;
background-color: skyblue;
border: 1px solid #0776a1;
margin-right: 5px;
}
.btn-danger:hover {
color: #fff;
background-color: #bd362f;
}
.btn:focus {
outline: none;
}
.todo-container {
width: 600px;
margin: 0 auto;
}
.todo-container .todo-wrap {
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
}
</style>
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
MyHeader.vue
<template>
<div class="todo-header">
<input type="text" placeholder="请输入你的任务名称,按回车键确认" v-model="title" @keyup.enter="add"/>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name: 'MyHeader',
// props: ['addTodo'], // 接收父组件传过来的方法
data() {
return {
title: ''
}
},
methods: {
add() {
if(!this.title.trim()) {
alert("输入的任务不能为空")
return
}
const todoObj = {
id: nanoid(), // 生成一个唯一id
name: this.title,
completed: false
}
// this.addTodo(todoObj) // 将生成的任务,传递回去给父组件
this.$emit('addTodo',todoObj)
this.title = ''
}
}
}
</script>
<style scoped>
/*header*/
.todo-header input {
width: 560px;
height: 28px;
font-size: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 7px;
}
.todo-header input:focus {
outline: none;
border-color: rgba(82, 168, 236, 0.8);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(82, 168, 236, 0.6);
}
</style>
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
MyList.vue
<template>
<ul class="todo-main">
<MyItem
v-for="todoObj in todos"
:key="todoObj.id"
:todo = "todoObj"
></MyItem>
</ul>
</template>
<script>
import MyItem from "@/components/MyItem";
export default {
name: 'MyList',
components: {MyItem},
// props:['todos','checkTodo',"deleteTodo"]
props:['todos']
}
</script>
<style scoped>
/*main*/
.todo-main {
margin-left: 0px;
border: 1px solid #ddd;
border-radius: 2px;
padding: 0px;
}
.todo-empty {
height: 40px;
line-height: 40px;
border: 1px solid #ddd;
border-radius: 2px;
padding-left: 5px;
margin-top: 10px;
}
</style>
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
MyItem.vue
<template>
<li>
<label>
<input type="checkbox" :checked="todo.completed" @change="handlerCheck(todo.id)"/>
<!--这种方式也能实现修改propos,但是vue检测不到,不推荐这样做,子组件不要直接修改父组件传过来的值-->
<!--<input type="checkbox" v-model="todo.completed" />-->
<span v-show="!todo.isEdit">{{ todo.name }}</span>
<input
v-show="todo.isEdit"
type="text"
:value="todo.name"
@blur="handlerBlur(todo,$event)"
ref="editInput"
>
</label>
<button class="btn btn-danger" @click="handlerDelete(todo.id)">删除</button>
<button v-show="!todo.isEdit" class="btn btn-edit" @click="handlerEdit(todo)">编辑</button>
</li>
</template>
<script>
export default {
name: 'MyItem',
//props:['todo','checkTodo','deleteTodo'],
props: ['todo'],
methods: {
handlerCheck(id) {
// this.checkTodo(id) // 将勾选框发生变化的id传给爷爷
this.$bus.$emit('checkTodo', id)
},
// 删除
handlerDelete(id) {
//this.deleteTodo(id);
this.$bus.$emit('deleteTodo', id)
},
// 编辑
handlerEdit(todo) {
if (todo.hasOwnProperty('isEdit')) { // 如果有这个属性
todo.isEdit = true
} else { // 没有这个
this.$set(todo, 'isEdit', true)
}
this.$nextTick(function (){
this.$refs.editInput.focus()
})
},
// 失去焦点回调,真正实现修改逻辑
handlerBlur(todo, e) {
todo.isEdit = false
if (!e.target.value.trim()) return alert("任务名称不能为空")
this.$bus.$emit('updateTodo', todo.id, e.target.value)
}
}
}
</script>
<style scoped>
/*item*/
li {
list-style: none;
height: 36px;
line-height: 36px;
padding: 0 5px;
border-bottom: 1px solid #ddd;
}
li label {
float: left;
cursor: pointer;
}
li label li input {
vertical-align: middle;
margin-right: 6px;
position: relative;
top: -1px;
}
li button {
float: right;
display: none;
margin-top: 3px;
}
li:before {
content: initial;
}
li:last-child {
border-bottom: none;
}
li:hover {
background-color: #ddd;
}
li:hover button {
display: block;
}
</style>
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
MyFooter.vue
<template>
<div class="todo-footer" v-if="total">
<label>
<input type="checkbox" v-model="isAll"/>
</label>
<span>
<span>已完成{{doneTotal}}</span> / 全部{{ total }}
</span>
<button class="btn btn-danger" @click="cleanAll">清除已完成任务</button>
</div>
</template>
<script>
export default {
name: 'MyFooter',
// props: ['todos','checkALLTodo', 'cleanAllTodo'],
props: ['todos'],
methods: {
cleanAll() {
// this.cleanAllTodo()
this.$emit('cleanAllTodo')
}
},
computed: {
total() {
return this.todos.length
},
doneTotal(){
return this.todos.reduce((pre,todo) => {
return pre + (todo.completed == true ? 1: 0)
},0)
},
isAll: {
get() {
return this.total == this.doneTotal && this.total > 0
},
set(value) {
//this.checkALLTodo(value)
this.$emit('checkALLTodo', value)
}
}
}
}
</script>
<style scoped>
/*footer*/
.todo-footer {
height: 40px;
line-height: 40px;
padding-left: 6px;
margin-top: 5px;
}
.todo-footer label {
display: inline-block;
margin-right: 20px;
cursor: pointer;
}
.todo-footer label input {
position: relative;
top: -1px;
vertical-align: middle;
margin-right: 5px;
}
.todo-footer button {
float: right;
margin-top: 5px;
}
</style>
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
# nextTick
- 语法:
his.$nextTick(回调函数)
- 作用:在下一次DOM更新结束后执行其指定的回调。
- 什么时候用:当改变数据后,要基于更新后的新DOM进行某些操作时,要在nextTick所指定的回调函数中执行.
# 消息订阅与发布(pubsub)
1.安装
npm install pubsub-js
2.引入
import pubsub from 'pubsub-js'
3.发布、订阅、取消
this.pubId = pubsub.subscribe('消息名','回调函数') // 订阅消息
this.unsubscribe(this.pubId) // 取消订阅
pubsub.publish('消息名',数据) // 发布消息
2
3
A.vue
import pubsub from 'pubsub-js'
export default {
name: 'A',
components: {},
props: {},
data() {
return {}
},
methods: {
demo(msgName, data) {
console.log("订阅hello消息,msgName是消息名,data是数据",msgName ,data)
}
},
mounted() {
// this.$bus.$on('hello',(data) => { // 这里也可以先在methods来定义方法,在这里回调
// console.log('接收到B组件的data:' + data)
// })
// this.pubId = pubsub.subscribe('hello',(msgName, data) => { // /订阅消息
// console.log("订阅hello消息,msgName是消息名,data是数据",msgName ,data)
// })
this.pubId = pubsub.subscribe('hello',this.demo)
},
beforeDestroy() {
// this.$bus.off('hello')
this.unsubscribe(this.pubId) // 取消订阅
}
}
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
B.vue
import pubsub from 'pubsub-js'
export default {
name: 'B',
components: {},
props: {},
data() {
return {}
},
methods: {
send() {
// this.$bus.$emit('hello',"我是B组件的数据")
pubsub.publish('hello',666) // 发布消息
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Vuex
- 官方解释:Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
- 它采用 集中式存储管理 应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
- Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
- 状态管理到底是什么?
- 状态管理模式、集中式存储管理这些名词听起来就非常高大上,让人捉摸不透。
- 其实,你可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。
- 然后,将这个对象放在顶层的Vue实例中,让其他组件可以使用。
- 那么,多个组件是不是就可以共享这个对象中的所有变量属性了呢?
- 等等,如果是这样的话,为什么官方还要专门出一个插件Vuex呢?难道我们不能自己封装一个对象来管理吗?
- 当然可以,只是我们要先想想VueJS带给我们最大的便利是什么呢?没错,就是响应式。
- 如果你自己封装实现一个对象能不能保证它里面所有的属性做到响应式呢?当然也可以,只是自己封装可能稍微麻烦一些。
- 不用怀疑,Vuex就是为了提供这样一个在多个组件间共享状态的插件,用它就可以了。
# 单界面状态管理
# 多界面状态管理
# Vuex状态管理图例
# 安装和导入Vuex
npm install vuex --save
src->store->index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
const store = new Vuex.Store({
state: {
},
mutations: {
},
actions: {
},
modules: {
}
})
export default store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
main.js
- 挂载到Vue实例中,让所有的Vue组件都可以使用这个store对象
- 来到main.js文件,导入store对象,并且放在new Vue中
- 这样,在其他Vue组件中,我们就可以通过this.$store的方式,获取到这个store对象了
import Vue from 'vue'
import App from './App.vue'
import store from './store'
Vue.config.productionTip = false
new Vue({
store,
render: h => h(App)
}).$mount('#app')
2
3
4
5
6
7
8
9
10
11
# 基本使用
APP.vue
<div id="app">
<h2>------APP组件------</h2>
<h2>{{this.$store.state.counter}}</h2>
<button @click="addCounter">+</button>
<button @click="subCounter">-</button>
<hello-vuex></hello-vuex>
</div>
2
3
4
5
6
7
import {INCREMENT, DECREMENT} from './store/motations-types'
methods: {
addCounter() {
this.$store.commit(INCREMENT)
},
subCounter() {
this.$store.commit(DECREMENT)
}
}
2
3
4
5
6
7
8
9
store->index.js
import Vue from 'vue'
import Vuex from 'vuex'
import mutations from './mutations'
Vue.use(Vuex)
const state = {
counter: 0
}
const store = new Vuex.Store({
state,
mutations,
actions: {
},
modules: {
}
})
export default store
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mutations.js
import { INCREMENT, DECREMENT } from "./motations-types.js"
export default {
[INCREMENT](state) { // 默认会传入state
state.counter++
},
[DECREMENT](state) {
state.counter--
}
}
2
3
4
5
6
7
8
9
10
motations-types.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
2
- 1.提取出一个公共的store对象,用于保存在多个组件中共享的状态
- 2.将store对象放置在new Vue对象中,这样可以保证在所有的组件中都可以使用到
- 3.在其他组件中使用store对象中保存的状态即可
- 通过
this.$store.state.属性
的方式来访问状态 - 通过
this.$store.commit('mutation中方法')
来修改状态
- 通过
- 注意事项:
- 我们通过提交mutation的方式,而非直接改变
store.state.count
。 - 这是因为Vuex可以更明确的追踪状态的变化,所以不要直接改变
store.state.count
的值。
# State
Vuex提出使用单一状态树, 什么是单一状态树呢? 英文名称是Single Source of Truth,也可以翻译成单一数据源。
# Getters
app.vue
<h2>------getters基本使用------</h2>
<h2>{{$store.getters.powerCounter}}</h2>
<p>{{$store.getters.more20Age}}</p>
<h2>年龄超过20的数量:{{$store.getters.more20AgeLength}}</h2>
<p>{{$store.getters.moreAgestu(12)}}</p>
2
3
4
5
getters.js
getters默认是不能传递参数的, 如果希望传递参数, 那么只能让getters本身返回另一个函数.
export default {
powerCounter(state) {
return state.counter * state.counter
},
more20Age(state) {
return state.students.filter(s => s.age >= 20)
},
more20AgeLength(state, getters){
return getters.more20Age.length
},
moreAgestu(state) {
// return function (age) {
// return state.students.filter(s => s.age > age)
// }
return age => {
return state.students.filter(s => s.age >= 20)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Mutation(同步函数)
- Vuex的store状态的更新唯一方式:提交Mutation
- Mutation主要包括两部分:
- 字符串的事件类型(type)
- 一个回调函数(handler),该回调函数的第一个参数就是state。
- mutation的定义方式:
import {INCREMENT, DECREMENT} from './store/motations-types'
methods: {
addCounter() {
this.$store.commit(INCREMENT)
},
subCounter() {
this.$store.commit(DECREMENT)
}
}
2
3
4
5
6
7
8
9
mutations.js
import { INCREMENT, DECREMENT } from "./motations-types.js"
export default {
[INCREMENT](state) { // 默认会传入state
state.counter++
},
[DECREMENT](state) {
state.counter--
}
}
2
3
4
5
6
7
8
9
10
motations-types.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
2
# Mutation传递参数
addCounter1(count) {
// payload: 负载
// 1.普通的提交封装
this.$store.commit('addCounter', count)
},
subCounter1(count) {
// // 2.特殊的提交封装
this.$store.commit({
type: 'subCounter',
count
})
}
2
3
4
5
6
7
8
9
10
11
12
mutation.js
import { INCREMENT, DECREMENT } from "./motations-types.js"
export default {
[INCREMENT](state) { // 默认会传入state
state.counter++
},
[DECREMENT](state) {
state.counter--
},
addCounter(state, n) {
state.counter += n
},
subCounter(state, payload) {
state.counter -= payload.count
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
updateInfo(state) {
state.info.name = 'modify'
// 错误的代码: 不能在这里进行异步操作
// setTimeout(() => {
// state.info.name = 'modify'
// }, 1000)
//state.info['address'] = '洛杉矶'
//Vue.set(state.info, 'address', '洛杉矶')
// 该方式做不到响应式
// delete state.info.age
//Vue.delete(state.info, 'age')
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- Vuex的store中的state是响应式的, 当state中的数据发生改变时, Vue组件会自动更新.
- 这就要求我们必须遵守一些Vuex对应的规则:
- 提前在store中初始化好所需的属性.
- 当给state中的对象添加新属性时, 使用下面的方式:
- 方式一: 使用Vue.set(obj, 'newProp', 123)
- 方式二: 用新对象给旧对象重新赋值
通常情况下, Vuex要求我们Mutation中的方法必须是同步方法. 主要的原因是当我们使用devtools时, 可以devtools可以帮助我们捕捉mutation的快照. 但是如果是异步操作, 那么devtools将不能很好的追踪这个操作什么时候会被完成.
# Action(异步函数)
- 我们强调, 不要再Mutation中进行异步操作.
- 但是某些情况, 我们确实希望在Vuex中进行一些异步操作, 比如网络请求, 必然是异步的. 这个时候怎么处理呢?
- Action类似于Mutation, 但是是用来代替Mutation进行异步操作的.
- Action的基本使用代码如下:
- context是什么?
- context是和store对象具有相同方法和属性的对象.
- 也就是说, 我们可以通过context去进行commit相关的操作, 也可以获取context.state等.
- 我们定义了actions, 然后又在actions中去进行commit, 这不是脱裤放屁吗?
- 事实上并不是这样, 如果在Vuex中有异步操作, 那么我们就可以在actions中完成了.
在Vue组件中, 如果我们调用action中的方法, 那么就需要使用dispatch
actions.js
export default {
// aUpdateInfo(context, payload) {
// setTimeout(() => {
// context.commit('updateInfo')
// console.log(payload.message)
// payload.success()
// },5000)
// }
aUpdateInfo(context, payload) {
return new Promise((resolve, reject) => {
setTimeout(() => {
context.commit('updateInfo');
console.log(payload);
resolve('1111111')
}, 1000)
})
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Info.vue
updateInfo() {
// this.$store.commit('updateInfo')
// this.$store.dispatch('aUpdateInfo', {
// message: '我是携带的信息',
// success: () => {
// console.log('里面已经完成了');
// }
// })
this.$store
.dispatch('aUpdateInfo', '我是携带的信息')
.then(res => {
console.log('里面完成了提交');
console.log(res);
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Module
我们在moduleA中添加state、mutations、getters mutation和getters接收的第一个参数是局部状态对象
虽然, 我们的doubleCount和increment都是定义在对象内部的.
但是在调用的时候, 依然是通过this.$store
来直接调用的.
<h2>{{$store.state.a.counter}}</h2>
<button @click="addInfoCounter">+</button>
2
addInfoCounter() {
this.$store.commit('add')
}
2
3
const module = {
state: {
counter: 0
},
mutations:{
add(state){
state.counter ++
}
}
}
2
3
4
5
6
7
8
9
10
# 项目结构
# 四个map方法的使用
使用前先引入
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
# mapState
用于帮助我们映射state中的数据为计算属性
computed: {
// 借助mapState生成计算属性: num、 name、region(对象写法)
// 对象写法可以映射$store.state里面的数据为一个新的名字
// ...mapState({num:'num',name:'name',region:'region'})
// 借助mapState生成计算属性: num、 name、region(数组写法)
// 和$store.state名字一样
...mapState(['num','name','region'])
},
2
3
4
5
6
7
8
9
# mapperGetters
用于帮助我们映射getters中的数据为计算属性
computed: {
// 借助mapGetters生成计算属性: bigNum(对象写法)
// ...mapGetters({bigNum:'bigNum'}),
// 借助mapGetters生成计算属性: bigNum(数组写法)
...mapGetters(['bigNum'])
},
2
3
4
5
6
# mapMutations
用于帮助我们生成与mutations对话的方法,即:包含 $store.commit(xxx)的函数
# mapActions
用于帮助我们生成actions对话的方法,即:包含$store.dispatch(xx)的函数
注:调用时候要传递参数:
<button @click="increment(n)">+</button> <button @click="decrement(n)">-</button>
1
2
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)
// },
// 对象写法
...mapMutations({increment:'JIA',decrement:'JIAN'}),
// 数组写法
// ...mapMutations(['JIA','JIAN']),
// 对象写法
...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'}),
// 数组写法
//...mapActions(['jiaOdd','jiaWait'])
},
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
完整代码:
<template>
<div>
<h1>当前求和为:{{num}}</h1>
<h1>放大10倍为:{{bigNum}}</h1>
<h1>我叫:{{name}},来自{{region}}</h1>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="increment(n)">+</button>
<button @click="decrement(n)">-</button>
<button @click="incrementOdd(n)">当前求和为奇数再加</button>
<button @click="incrementWait(n)">等一等再加</button>
</div>
</template>
<script>
import {mapState, mapGetters, mapMutations, mapActions} from 'vuex'
export default {
name:'Count',
data() {
return {
n:1, //用户选择的数字
}
},
computed: {
// 借助mapState生成计算属性: num、 name、region(对象写法)
// 对象写法可以映射$store.state里面的数据为一个新的名字
// ...mapState({num:'num',name:'name',region:'region'}),
// 借助mapState生成计算属性: num、 name、region(数组写法)
// 和$store.state名字一样
...mapState(['num','name','region']),
// 借助mapGetters生成计算属性: bigNum(对象写法)
// ...mapGetters({bigNum:'bigNum'}),
// 借助mapGetters生成计算属性: bigNum(数组写法)
...mapGetters(['bigNum'])
},
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)
// },
// 对象写法
...mapMutations({increment:'JIA',decrement:'JIAN'}),
// 数组写法
// ...mapMutations(['JIA','JIAN']),
// 对象写法
...mapActions({incrementOdd:'jiaOdd',incrementWait:'jiaWait'}),
// 数组写法
//...mapActions(['jiaOdd','jiaWait'])
},
}
</script>
<style lang="css">
button{
margin-left: 5px;
}
</style>
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
# 模块管理
完整代码:
store/count.js
//求和相关的配置
export default {
namespaced:true,
actions:{
jiaOdd(context,value){
console.log('actions中的jiaOdd被调用了')
if(context.state.sum % 2){
context.commit('JIA',value)
}
},
jiaWait(context,value){
console.log('actions中的jiaWait被调用了')
setTimeout(()=>{
context.commit('JIA',value)
},500)
}
},
mutations:{
JIA(state,value){
console.log('mutations中的JIA被调用了')
state.sum += value
},
JIAN(state,value){
console.log('mutations中的JIAN被调用了')
state.sum -= value
},
},
state:{
sum:0, //当前的和
school:'尚硅谷',
subject:'前端',
},
getters:{
bigSum(state){
return state.sum*10
}
},
}
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
store/person.js
//人员管理相关的配置
import axios from 'axios'
import { nanoid } from 'nanoid'
export default {
namespaced:true,
actions:{
addPersonWang(context,value){
if(value.name.indexOf('王') === 0){
context.commit('ADD_PERSON',value)
}else{
alert('添加的人必须姓王!')
}
},
addPersonServer(context){
axios.get('https://api.uixsj.cn/hitokoto/get?type=social').then(
response => {
context.commit('ADD_PERSON',{id:nanoid(),name:response.data})
},
error => {
alert(error.message)
}
)
}
},
mutations:{
ADD_PERSON(state,value){
console.log('mutations中的ADD_PERSON被调用了')
state.personList.unshift(value)
}
},
state:{
personList:[
{id:'001',name:'张三'}
]
},
getters:{
firstPersonName(state){
return state.personList[0].name
}
},
}
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
store/index.js
//该文件用于创建Vuex中最为核心的store
import Vue from 'vue'
//引入Vuex
import Vuex from 'vuex'
import countOptions from './count'
import personOptions from './person'
//应用Vuex插件
Vue.use(Vuex)
//创建并暴露store
export default new Vuex.Store({
modules:{
countAbout:countOptions,
personAbout:personOptions
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Count.vue
<template>
<div>
<h1>当前求和为:{{sum}}</h1>
<h3>当前求和放大10倍为:{{bigSum}}</h3>
<h3>我在{{school}},学习{{subject}}</h3>
<h3 style="color:red">Person组件的总人数是:{{personList.length}}</h3>
<select v-model.number="n">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<button @click="increment(n)">+</button>
<button @click="decrement(n)">-</button>
<button @click="incrementOdd(n)">当前求和为奇数再加</button>
<button @click="incrementWait(n)">等一等再加</button>
</div>
</template>
<script>
import {mapState,mapGetters,mapMutations,mapActions} from 'vuex'
export default {
name:'Count',
data() {
return {
n:1, //用户选择的数字
}
},
computed:{
//借助mapState生成计算属性,从state中读取数据。(数组写法)
...mapState('countAbout',['sum','school','subject']),
...mapState('personAbout',['personList']),
//借助mapGetters生成计算属性,从getters中读取数据。(数组写法)
...mapGetters('countAbout',['bigSum'])
},
methods: {
//借助mapMutations生成对应的方法,方法中会调用commit去联系mutations(对象写法)
...mapMutations('countAbout',{increment:'JIA',decrement:'JIAN'}),
//借助mapActions生成对应的方法,方法中会调用dispatch去联系actions(对象写法)
...mapActions('countAbout',{incrementOdd:'jiaOdd',incrementWait:'jiaWait'})
},
mounted() {
console.log(this.$store)
},
}
</script>
<style lang="css">
button{
margin-left: 5px;
}
</style>
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
Person.vue
<template>
<div>
<h1>人员列表</h1>
<h3 style="color:red">Count组件求和为:{{sum}}</h3>
<h3>列表中第一个人的名字是:{{firstPersonName}}</h3>
<input type="text" placeholder="请输入名字" v-model="name">
<button @click="add">添加</button>
<button @click="addWang">添加一个姓王的人</button>
<button @click="addPersonServer">添加一个人,名字随机</button>
<ul>
<li v-for="p in personList" :key="p.id">{{p.name}}</li>
</ul>
</div>
</template>
<script>
import {nanoid} from 'nanoid'
export default {
name:'Person',
data() {
return {
name:''
}
},
computed:{
personList(){
return this.$store.state.personAbout.personList
},
sum(){
return this.$store.state.countAbout.sum
},
firstPersonName(){
return this.$store.getters['personAbout/firstPersonName']
}
},
methods: {
add(){
const personObj = {id:nanoid(),name:this.name}
this.$store.commit('personAbout/ADD_PERSON',personObj)
this.name = ''
},
addWang(){
const personObj = {id:nanoid(),name:this.name}
this.$store.dispatch('personAbout/addPersonWang',personObj)
this.name = ''
},
addPersonServer(){
this.$store.dispatch('personAbout/addPersonServer')
}
},
}
</script>
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
# axios
npm install axios --save
http://httpbin.org/
http://123.207.32.32:8000/home/multidata
http://123.207.32.32:8000/home/data?type=sell&page=1
Vue中发送网络请求有非常多的方式, 那么, 在开发中, 如何选择呢?
选择一: 传统的Ajax是基于XMLHttpRequest(XHR)
- 为什么不用它呢?
- 非常好解释, 配置和调用方式等非常混乱.
- 编码起来看起来就非常蛋疼.
- 所以真实开发中很少直接使用, 而是使用jQuery-Ajax
- 为什么不用它呢?
选择二: 在前面的学习中, 我们经常会使用jQuery-Ajax 相对于传统的Ajax非常好用.
- 为什么不选择它呢?
- 首先, 我们先明确一点: 在Vue的整个开发中都是不需要使用jQuery了.
- 那么, 就意味着为了方便我们进行一个网络请求, 特意引用一个jQuery, 你觉得合理吗?
- jQuery的代码1w+行.
- Vue的代码才1w+行.
- 完全没有必要为了用网络请求就引用这个重量级的框架.
- 为什么不选择它呢?
选择三: 官方在Vue1.x的时候, 推出了Vue-resource.
Vue-resource的体积相对于jQuery小很多. 另外Vue-resource是官方推出的.
- 为什么不选择它呢?
- 在Vue2.0退出后, Vue作者就在GitHub的Issues中说明了去掉vue-resource, 并且以后也不会再更新.
- 那么意味着以后vue-reource不再支持新的版本时, 也不会再继续更新和维护.
- 对以后的项目开发和维护都存在很大的隐患.
选择四: 在说明不再继续更新和维护vue-resource的同时, 作者还推荐了一个框架: axios axios有非常多的优点, 并且用起来也非常方便.
# jsonp
- 在前端开发中, 我们一种常见的网络请求方式就是JSONP
- 使用JSONP最主要的原因往往是为了解决跨域访问的问题.
- JSONP的原理是什么呢?
- JSONP的核心在于通过
<script>
标签的src来帮助我们请求数据. - 原因是我们的项目部署在domain1.com服务器上时, 是不能直接访问domain2.com服务器上的资料的.
- 这个时候, 我们利用
<script>
标签的src帮助我们去服务器请求到数据, 将数据当做一个javascript的 - 函数来执行, 并且执行的过程中传入我们需要的json.
- 所以, 封装jsonp的核心就在于我们监听window上的jsonp进行回调时的名称.
- JSONP的核心在于通过
- JSONP的原理是什么呢?
# axios特点
- 在浏览器中发送 XMLHttpRequests 请求
- 在 node.js 中发送 http请求
- 支持 Promise API
- 拦截请求和响应
- 转换请求和响应数据
支持多种请求方式:
axios(config)
axios.request(config)
axios.get(url[, config])
axios.delete(url[, config])
axios.head(url[, config])
axios.post(url[, data[, config]])
axios.put(url[, data[, config]])
axios.patch(url[, data[, config]])
# 发送get请求
axios({
url: 'http://123.207.32.32:8000/home/multidata',
method: 'get' // 默认是get请求
}).then(res => {
console.log(res)
})
2
3
4
5
6
axios.get('http://123.207.32.32:8000/home/data',
{params: { type: 'shell', page: 1}})
.then(res => {
console.log(res)
})
2
3
4
5
使用axios.all, 可以放入多个请求的数组.
axios.all([])
返回的结果是一个数组,使用 axios.spread
可将数组 [res1,res2] 展开为 res1, res2
axios.all([axios.get('http://123.207.32.32:8000/home/multidata'),axios.get('http://123.207.32.32:8000/home/data',
{params: {type: 'shell', page: 1}})
]).then(axios.spread((res1, res2) =>{
console.log(res1)
console.log(res2)
}))
2
3
4
5
6
全局配置
axios.defaults.baseURL = ‘123.207.32.32:8000’
axios.defaults.headers.post[‘Content-Type’] = ‘application/x-www-form-urlencoded’;
2
axios.defaults.baseURL = "http://123.207.32.32:8000"
axios.all([axios.get('/home/multidata'),axios.get('/home/data',
{params: {type: 'shell', page: 1}})
]).then(axios.spread((res1, res2) =>{
console.log(res1)
console.log(res2)
}))
2
3
4
5
6
7
8
- 请求地址 url: '/user',
- 请求类型 method: 'get',
- 请根路径 baseURL: 'http://www.mt.com/api',
- 请求前的数据处理 transformRequest:[function(data){}],
- 请求后的数据处理 transformResponse: [function(data){}],
- 自定义的请求头 headers:{'x-Requested-With':'XMLHttpRequest'},
- URL查询对象 params:{ id: 12 },
- 查询对象序列化函数 paramsSerializer: function(params){ } request body data: { key: 'aa'},
- 超时设置s timeout: 1000,
- 跨域是否带Token withCredentials: false,
- 自定义请求处理 adapter: function(resolve, reject, config){},
- 身份验证信息 auth: { uname: '', pwd: '12'},
- 响应的数据格式 json / blob /document /arraybuffer / text / stream responseType: 'json',
# axios实例
- 为什么要创建axios的实例呢?
- 当我们从axios模块中导入对象时, 使用的实例是默认的实例.
- 当给该实例设置一些默认配置时, 这些配置就被固定下来了.
- 但是后续开发中, 某些配置可能会不太一样.
- 比如某些请求需要使用特定的baseURL或者timeout或者content-Type等.
- 这个时候, 我们就可以创建新的实例, 并且传入属于该实例的配置信息.
// 创建新的实例
const axiosInstance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000,
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
axiosInstance({
url:'/home/multidata',
method: 'get'
}).then(res => {
console.log(res)
}).catch(err => {
console.log(err)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# axios封装
src->network->request.js
import axios from 'axios'
export function request(config) {
// 1.创建axios的实例
const instance = axios.create({
baseURL: 'http://123.207.32.32:8000',
timeout: 5000,
})
// 2.axios的拦截器
// 2.1请求拦截的作用
// 发送真正的网络请求
instance.interceptors.request.use(config => {
// console.log(config);
// 1.比如config中的一些信息不符合服务器的要求
// 2.比如每次发送网络请求时, 都希望在界面中显示一个请求的图标
// 3.某些网络请求(比如登录(token)), 必须携带一些特殊的信息
return config
}, err => {
console.log(err)
})
// 2.2响应拦截
instance.interceptors.response.use(res => {
return res.data
}, err => {
console.log(err)
})
// 3.发送真正的网络请求
return instance(config)
}
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
main.js
import {request} from './network/request'
request({
url: '/home/multidata'
})
.then(res => {
console.log(res)
})
.catch(err => {
console.log(err)
})
2
3
4
5
6
7
8
9
10
11
# 拦截器
# 配置代理一
直接请求会存在跨域问题
vue.config.js文件配置代理服务器(配置完要重启)
module.exports = {
devServer: {
proxy: 'http://localhost:5000' //填写你需要代理的服务器的端口号
}
}
2
3
4
5
代理的服务还是8080,只不过发送请求时先往8080发,8080服务器向5000发,服务器向服务器发送请求就不会有跨域问题。
再次请求,往8080发送就可以了
问题:
1.只能配置一个代理
2.(不能灵活配置走不走代理)会优先去本地资源找,如果本地public里面有,则不会去往5000服务器发送
# 配置代理二
vue.config.js文件配置代理服务器(配置完要重启)
module.exports = {
/**开启代理服务器方式一
devServer: {
proxy: 'http://localhost:5000'
}
*/
//开启代理服务器方式二
devServer: {
proxy: {
'/api': { // 匹配所有以/api开头的请求,都会向服务器转发。这样就避免了优先匹配前端资源
target: 'http://localhost:5000', // 要代理的服务器地址
pathRewrite: {"^/api":""}, // 将/api替换为空,否则向服务器发送的是/api/test,其实我们想请求的是/test
ws: true, // 支持webscoket
changeOrigin: true // 如果为true,服务器那边收到的发送请求端口号为 5000(相当于欺骗)
// 如果为false,则服务器收到的请求端口号还是8080. 默认配置为true,所以注释掉也可以
// 即使为false,也可以发送请求,因为是8080服务器向5000服务器发,服务器之间没有跨域。
// 如果5000那边限制了请求的端口号啥的,就要设为true
},
'/demo': { // 配置第二个代理,请求以/demo为前缀都会走这里
target: 'http://localhost:5001',
pathRewrite: {"^/demo":""}
}
}
}
}
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
发送请求时,加上前缀就会去对应的代理服务器里请求
<template>
<div id="app">
<button @click="send">点击发送请求</button>
<button @click="send2">向服务器2发送请求</button>
</div>
</template>
<script>
import axios from "axios";
export default {
methods: {
send() { //加上前缀/api,就会向5000转发
axios.get('http://localhost:8080/api/students').then(
response => {
console.log('请求成功',response.data)
},
error => {
console.log('请求失败',error.message)
}
)
},
send2() {
axios.get('http://localhost:8080/demo/cars').then(
response => {
console.log('请求成功',response.data)
},
error => {
console.log('请求失败',error.message)
}
)
}
}
}
</script>
<style>
</style>
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
# Vue-Resource(已弃用)
- 下载
npm install vue-resource
引入和使用插件
main.js
import vueResource from 'vue-resource'
// 使用插件
Vue.use(vueResource) //会挂载在$http上
2
3
- 发送请求
this.$http.get('http://localhost:8080/api/students').then(
response => {
console.log('请求成功',response.data)
},
error => {
console.log('请求失败',error.message)
}
)
2
3
4
5
6
7
8
# Vue动画与过渡
<template>
<div>
<hr>
<button @click="isShow = !isShow">点击显示与隐藏</button>
<!--appear第一次出现就使用动画 ,加了name要用.hhhh-enter-active-->
<transition name="hhhh" :appear="true">
<div class="box" v-show="isShow">
<h2>你好啊</h2>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'MyAnimate',
data() {
return {
isShow: true
}
},
methods: {},
}
</script>
<style scoped>
.box {
background-color: skyblue;
}
/*
如果 translate没加name属性就用v,如果加了name就不能用v
.v-enter-active {
animation: danimate 0.5s linear;
}
.v-leave-active {
animation: danimate 0.5s linear reverse;
}
*/
.hhhh-enter-active {
animation: danimate 0.5s linear;
}
.hhhh-leave-active {
animation: danimate 0.5s linear reverse;
}
@keyframes danimate {
from {
transform: translateX(-100%);
}
to {
transform: translateX(0);
}
}
</style>
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
方式二:
<template>
<div>
<hr>
<button @click="isShow = !isShow">点击显示与隐藏</button>
<!--appear第一次出现就使用动画 ,加了name要用.hhhh-enter-active-->
<transition name="hhhh" :appear="true">
<div class="box" v-show="isShow">
<h2>你好啊</h2>
</div>
</transition>
</div>
</template>
<script>
export default {
name: 'MyAnimate2',
data() {
return {
isShow: true
}
},
methods: {},
}
</script>
<style scoped>
.box {
background-color: skyblue;
/*transition: 0.5s liner;也可以加载这里*/
}
/**进入的起点,和离开的终点*/
.hhhh-enter,.hhhh-leave-to {
transform: translateX(-100%);
}
.hhhh-enter-active,.hhhh-leave-active {
transition: 0.5s linear;
}
/**进入的终点,离开的起点*/
.hhhh-enter-to,.hhhh-leave {
transform: translateX(0);
}
</style>
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
如果想要加一组,要用transition-group,并且必须加key
<transition-group name="hhhh" :appear="true">
<h2 class="box" v-show="isShow" key="1">你好啊</h2>
<h2 class="box" v-show="!isShow" key="2">哈哈哈</h2>
</transition-group>
2
3
4
# 引入animate.css
1.安装
npm install animate.css --save
2.引入,在需要的组件script标签内引入
import 'animate.css';
3.使用
name="animate__animated animate__bounce" 必须加
enter-active-class="" //进入时的动画名称 leave-active-class="" //离开时的动画名称
<transition
:appear="true"
name="animate__animated animate__bounce"
enter-active-class="animate__shakeY"
leave-active-class="animate__backOutUp"
>
<div class="box" v-show="isShow">
<h2>你好啊</h2>
</div>
</transition>
2
3
4
5
6
7
8
9
10
完整代码:
<template>
<div>
<hr>
<button @click="isShow = !isShow">点击显示与隐藏</button>
<!--appear第一次出现就使用动画 ,加了name要用.hhhh-enter-active-->
<transition
:appear="true"
name="animate__animated animate__bounce"
enter-active-class="animate__shakeY"
leave-active-class="animate__backOutUp"
>
<div class="box" v-show="isShow">
<h2>你好啊</h2>
</div>
</transition>
</div>
</template>
<script>
import 'animate.css';
export default {
name: 'AnimateCss',
data() {
return {
isShow: true
}
},
}
</script>
<style scoped>
.box {
background-color: skyblue;
}
</style>
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