Vue 学习笔记

2020-12-04/2021-08-05

一、邂逅 Vuejs

Vue是一个渐进式的框架

  • 渐进式意味着你可以将 Vue 作为你应用的一部分嵌入其中,带来更丰富的交互体验。
  • 或者如果你希望将更多的业务逻辑使用 Vue 实现,那么 Vue 的核心库以及其生态系统,比如Core + Vue-router + Vuex ,可以满足你各种各样的需求。

Vue 有很多特点和 Web 开发中常见的高级功能

  • 解耦视图和数据
  • 可复用的组件
  • 前端路由技术
  • 状态管理
  • 虚拟DOM

1.1 Vue.js 安装

方式一:CDN 引入

1<!-- 开发环境版本,包含了有帮助的命令行警告 -->
2<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
3<!-- 生产环境版本,优化了尺寸和速度 -->
4<script src="https://cdn.jsdelivr.net/npm/vue"></script>

方式二:下载和引入
开发环境
生产环境

方式三:NPM 安装

1npm install vue

1.2 Vuejs 初体验

 1<div id="app">
 2    <div>{{message}}</div>  <!-- 你好呀 --> 
 3    <div>{{movies}}</div>   <!-- [ "电影 1", "电影 2", "电影 3" ] -->
 4    <ul>
 5        <li v-for="item in movies">{{item}}</li> 
 6        <!-- 
 7            * 电影 1
 8            * 电影 2
 9            * 电影 3
10            -->
11    </ul>
12</div>
13<script src="./vue.js"></script>
14<script>
15    const app = new Vue({
16        el: '#app', // 用于挂载要管理的数据
17        data: { // 定义数据
18            message: '你好呀',
19            movies: ['电影 1', '电影 2', '电影 3']
20        }
21    });
22</script>
  1. 创建 Vue 对象的时候,传入了一些 opitions:{}

    • el 属性:决定了这个 Vue 对象要挂载到哪一个元素上
    • data 属性:该属性通常会存储一个数据,可以是直接定义出来的,也可以是从服务器加载的
  2. 浏览器执行代码的流程

    • 先解析 html 代码,显示对应的 html
    • 创建 vue 实例后,对原 html 进行解析和修改

计数器案例

 1<div id="app">
 2    <h2>当前计数:{{count}}</h2>
 3    <button @click="increment">+</button>
 4    <button @click="decrement">-</button>
 5</div>
 6<script src="./vue.js"></script>
 7<script>
 8    const app = new Vue({
 9        el: '#app',
10        data: {
11            count: 0
12        },
13        methods: {
14            increment() {
15                this.count++;
16            },
17            decrement() {
18                this.count--;
19            }
20        }
21    });
22</script>
  • methods 属性:用于在 Vue 对象中定义方法
  • @click 指令:用于监听某个元素的点击事件

也可以这样:

 1<div id="app">
 2    <h2>当前计数:{{count}}</h2>
 3    <button @click="increment">+</button>
 4    <button @click="decrement">-</button>
 5</div>
 6<script src="./vue.js"></script>
 7<script>
 8    // proxy
 9    let obj = {
10        count: 0
11    };
12    const app = new Vue({
13        el: '#app',
14        data: obj,
15        methods: {
16            increment() {
17                this.count++;
18            },
19            decrement() {
20                this.count--;
21            }
22        }
23    });
24</script>

因为 vue 帮我们做了一个代理,obj 里面的东西会自动添加到 data 中,所以还需要使用 this.count

1.3 MVVM

mvvm

  • View 层

    • 视图层
    • 前端开发中的 DOM 层
    • 主要作用是给用户展示各种信息
  • Model 层

    • 数据层
    • 数据可能是我们固定死的数据,更多的是来自我们服务器,从网络上请求下来的数据
  • ViewModel 层

    • 视图模型层
    • 是 View 和 Model 沟通的桥梁
    • 它实现了 Data Binding,也就是数据绑定,将 Model 的改变实时反应到了 View 中
    • 它实现了 DOM Listener,也就是 DOM 监听,当 DOM 发生一些事件时,可以监听到,并且在需要的时候改变对应的 Data

1.4 opitions

官方文档:https://cn.vuejs.org/v2/api/#data

  • el :

    • 类型:string | HTMLElement
    • 作用:决定之后的 vue 实例会管理哪一个 DOM
  • data :

    • 类型:object | function(组件中 data 必须是一个函数)
    • 作用:vue 实例对应的数据对象
  • methods:

    • 类型:{ [key: string]: Function}
    • 作用:定义属于 vue 的一些方法,可以在其他地方调用,也可以在指令中调用

1.5 vue 生命周期

生命周期函数的使用

 1new Vue({
 2  data: {
 3    a: 1
 4  },
 5  created: function () {
 6    // `this` 指向 vm 实例
 7    console.log('a is: ' + this.a)
 8  }
 9})
10// => "a is: 1"

不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a) ,因为箭头函数并没有 this,this 会作为变量一直向上级词法作用域查找,直至找到为止,经常导致错误

生命周期图

lifecycle

二、Vue基础语法

2.1 插值语法

可以通过 Mustache 语法,也就是双大括号来进行插值,并且数据是响应式的。

只能在 content 中插入,不能在属性中插入。

双大括号中不仅仅可以直接写变量,也可以写简单表达式,比如 {{firstName + ' ' + lastName}}

2.2 v-once

让视图中的展示的数据不随着数据的改变而改变,仅仅第一次更新数据

 1<div id="app">
 2    <h2 v-once>{{message}}</h2>
 3</div>
 4<script src="./vue.js"></script>
 5<script>
 6    const app = new Vue({
 7        el: '#app',
 8        data: {
 9            message: 'haha'
10        }
11    });
12</script>

2.3 v-html

将变量解析为 html

 1<div id="app">
 2    <h2 v-html="url"></h2>
 3</div>
 4<script src="./vue.js"></script>
 5<script>
 6    const app = new Vue({
 7        el: '#app',
 8        data: {
 9            url: '<a href="http://www.baidu.com">百度</a>'
10        }
11    });
12</script>

2.4 v-text

 1<div id="app">
 2    <!-- 若 h2 标签中有东西,则会覆盖,不够灵活,一般不用 -->
 3    <h2 v-text="message"></h2>
 4</div>
 5<script src="./vue.js"></script>
 6<script>
 7    const app = new Vue({
 8        el: '#app',
 9        data: {
10            message: 'haha'
11        }
12    });
13</script>

2.5 v-pre

让内容原样展示

 1<div id="app">
 2  <h2 v-pre>{{message}}</h2>
 3</div>
 4<script src="./vue.js"></script>
 5<script>
 6  const app = new Vue({
 7    el: '#app',
 8    data: {
 9      message: 'hello'
10    }
11  });
12</script>
13<!-- {{message}} -->

2.6 v-cloak

在某些情况下,我们浏览器可能会直接显然出未编译的 Mustache 标签。

添加 v-cloak 属性,在 vue 解析之后,会自动删除此属性,用来防止显示未加载之前的数据。

 1<head>
 2  <meta charset="UTF-8">
 3  <title>Title</title>
 4  <style>
 5    [v-cloak] {
 6      display: none;
 7    }
 8  </style>
 9</head>
10
11<body>
12
13<div id="app" v-cloak>
14  <h2>{{message}}</h2>
15</div>
16
17<script src="../js/vue.js"></script>
18<script>
19  // 在vue解析之前, div中有一个属性v-cloak
20  // 在vue解析之后, div中没有一个属性v-cloak
21  setTimeout(function() {
22    const app = new Vue({
23      el: '#app',
24      data: {
25        message: '你好啊'
26      }
27    })
28  }, 1000)
29</script>
30
31</body>

2.7 v-bind

  • 作用:动态绑定属性
  • 缩写::
  • 预期:any (with argument) | Object (without argument)
  • 参数:attrOrProp (optional)
 1<div id="app">
 2  <a v-bind:href="aHref">百度一下</a>
 3  <!--语法糖的写法-->
 4  <a :href="aHref">百度一下</a>
 5</div>
 6
 7<script src="../js/vue.js"></script>
 8<script>
 9  const app = new Vue({
10    el: '#app',
11    data: {
12      aHref: 'http://www.baidu.com'
13    }
14  })
15</script>

绑定Class ,对象语法

 1<div id="app">
 2  <!--<h2 v-bind:class="{key1: value1, key2: value2}">{{message}}</h2>-->
 3  <!--<h2 v-bind:class="{类名1: true, 类名2: boolean}">{{message}}</h2>-->
 4  <!-- 会自动合并 class -->
 5  <h2 class="title" v-bind:class="{active: isActive, line: isLine}">{{message}}</h2>
 6  <h2 class="title" v-bind:class="getClasses()">{{message}}</h2>
 7  <button v-on:click="btnClick">按钮</button>
 8</div>
 9
10<script src="../js/vue.js"></script>
11<script>
12  const app = new Vue({
13    el: '#app',
14    data: {
15      message: '你好啊',
16      isActive: true,
17      isLine: true
18    },
19    methods: {
20      btnClick: function() {
21        this.isActive = !this.isActive
22      },
23      getClasses: function() {
24        return {
25          active: this.isActive,
26          line: this.isLine
27        }
28      }
29    }
30  })
31</script>

绑定Class, 数组语法

 1<div id="app">
 2  <!-- 这样写取得是字符串,不是变量 -->
 3  <!-- <h2 class="title" :class="['active', 'line']">{{message}}</h2> -->
 4  <h2 class="title" :class="[active, line]">{{message}}</h2>
 5  <h2 class="title" :class="getClasses()">{{message}}</h2>
 6</div>
 7
 8<script src="../js/vue.js"></script>
 9<script>
10  const app = new Vue({
11    el: '#app',
12    data: {
13      message: '你好啊',
14      active: 'aaaaaa',
15      line: 'bbbbbbb'
16    },
17    methods: {
18      getClasses: function() {
19        return [this.active, this.line]
20      }
21    }
22  })
23</script>

绑定 style,对象语法

 1<div id="app">
 2  <!--<h2 :style="{key(属性名): value(属性值)}">{{message}}</h2>-->
 3
 4  <!--'50px'必须加上单引号, 否则是当做一个变量去解析-->
 5  <!--<h2 :style="{fontSize: '50px'}">{{message}}</h2>-->
 6
 7  <!--fontSize 可以使用驼峰式或者 font-size,finalSize当成一个变量使用-->
 8  <h2 :style="{fontSize: finalSize + 'px', backgroundColor: finalColor}">{{message}}</h2>
 9  <h2 :style="getStyles()">{{message}}</h2>
10</div>
11
12<script src="../js/vue.js"></script>
13<script>
14  const app = new Vue({
15    el: '#app',
16    data: {
17      message: '你好啊',
18      finalSize: 100,
19      finalColor: 'red',
20    },
21    methods: {
22      getStyles: function() {
23        return {
24          fontSize: this.finalSize + 'px',
25          backgroundColor: this.finalColor
26        }
27      }
28    }
29  })
30</script>

绑定 style,数组语法

 1<div id="app">
 2  <h2 :style="[baseStyle, baseStyle1]">{{message}}</h2>
 3</div>
 4
 5<script src="../js/vue.js"></script>
 6<script>
 7  const app = new Vue({
 8    el: '#app',
 9    data: {
10      message: '你好啊',
11      baseStyle: {backgroundColor: 'red'},
12      baseStyle1: {fontSize: '100px'},
13    }
14  })
15</script>

2.8 computed 计算属性

我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示,这时候可以使用计算属性。

计算属性有缓存,不像方法一样每次都会计算。

简单使用

 1<script src="../js/vue.js"></script>
 2<script>
 3  const app = new Vue({
 4    el: '#app',
 5    data: {
 6      firstName: 'Lebron',
 7      lastName: 'James'
 8    },
 9    // computed: 计算属性()
10    computed: {
11      fullName: function() {
12        return this.firstName + ' ' + this.lastName
13      }
14    }
15  })
16</script>

复杂操作

 1<div id="app">
 2  <h2>总价格: {{totalPrice}}</h2>
 3</div>
 4
 5<script src="../js/vue.js"></script>
 6<script>
 7  const app = new Vue({
 8    el: '#app',
 9    data: {
10      books: [
11        {id: 110, name: 'Unix编程艺术', price: 119},
12        {id: 111, name: '代码大全', price: 105},
13        {id: 112, name: '深入理解计算机原理', price: 98},
14        {id: 113, name: '现代操作系统', price: 87},
15      ]
16    },
17    computed: {
18      totalPrice: function () {
19        let result = 0
20        for (let i=0; i < this.books.length; i++) {
21          result += this.books[i].price
22        }
23        return result;
24      }
25    }
26  })
27</script>

计算属性的 setter 和 getter

  • 计算属性有 get 和 set 方法
  • 计算属性的 set 方法一般省略
  • 计算属性的大括号和 get可以省略,直接写方法
  • 获取计算属性的时候,不需要加括号
 1<div id="app">
 2  <h2>{{fullName}}</h2>
 3</div>
 4
 5<script src="../js/vue.js"></script>
 6<script>
 7  const app = new Vue({
 8    el: '#app',
 9    data: {
10      firstName: 'Kobe',
11      lastName: 'Bryant'
12    },
13    computed: {
14      // 一般这样写
15      // fullName: function () {
16      //   return this.firstName + ' ' + this.lastName
17      // }
18
19      // 不省略的写法
20      fullName: {
21        // 计算属性一般是没有set方法, 只读属性.
22        // set: function(newValue) {
23        //   console.log('-----', newValue);
24        // },
25        get: function() {
26          return this.firstName + ' ' + this.lastName
27        }
28      }
29    }
30  })
31</script>

计算属性和 methods 的区别

  • 计算属性会做一个缓存,如果返回值没有改变,则不会去调用方法,返回值改变,就会再次调用方法
  • methods 每次都会执行
  • computed 效率比 methods 高

2.9 v-on 事件监听

  • 作用:绑定事件监听器
  • 缩写:@
  • 预期:Function | Inline Statement | Object
  • 参数:event

基础用法:

 1<div id="app">
 2  <h2>{{counter}}</h2>
 3  <!--<button v-on:click="counter++">+</button>-->
 4  <!--<button v-on:click="counter--">-</button>-->
 5  <!--<button v-on:click="increment">+</button>-->
 6  <!--<button v-on:click="decrement">-</button>-->
 7  <button @click="increment">+</button>
 8  <button @click="decrement">-</button>
 9</div>
10
11<script src="../js/vue.js"></script>
12<script>
13  const app = new Vue({
14    el: '#app',
15    data: {
16      counter: 0
17    },
18    methods: {
19      increment() {
20        this.counter++
21      },
22      decrement() {
23        this.counter--
24      }
25    }
26  })
27</script>

v-on 的参数

  • 不传参的时候,加不加括号都行,不加括号 vue 也会自动处理
  • 当调用时不传参数,但方法需要一个参数的时候,默认传入 event
  • 若我们需要手动传入 event,则通过 $event 取值传入
 1<div id="app">
 2  <!--1.事件调用的方法没有参数-->
 3  <button @click="btn1Click()">按钮1</button>
 4  <button @click="btn1Click">按钮1</button>
 5
 6  <!--2.在事件定义时, 写方法时省略了小括号, 但是方法本身是需要一个参数的, 这个时候, Vue会默认将浏览器生产的event事件对象作为参数传入到方法-->
 7  <!-- <button @click="btn2Click(123)">按钮2</button> -->
 8  <!--<button @click="btn2Click()">按钮2</button>-->
 9  <button @click="btn2Click">按钮2</button>
10
11  <!--3.方法定义时, 我们需要event对象, 同时又需要其他参数-->
12  <!-- 在调用方式, 如何手动的获取到浏览器参数的event对象: $event-->
13  <button @click="btn3Click(abc, $event)">按钮3</button>
14</div>
15
16<script src="../js/vue.js"></script>
17<script>
18  const app = new Vue({
19    el: '#app',
20    data: {
21      message: '你好啊',
22      abc: 123
23    },
24    methods: {
25      btn1Click() {
26        console.log("btn1Click");
27      },
28      btn2Click(event) {
29        console.log('--------', event);
30      },
31      btn3Click(abc, event) {
32        console.log('++++++++', abc, event);
33      }
34    }
35  })
36</script>

v-on 修饰符

Vue 提供了修饰符来帮助我们方便的处理一些事件:

  • .stop 阻止事件冒泡
  • .prevent 阻止默认事件,如表单中 submit 自动触发 form 的 action
  • .{keyCode | keyAlias} 只当事件是从特定键触发时才触发回调
  • .native 监听组件根元素的原生事件
  • .once 只触发一次事件的回调
 1<!-- 停止冒泡 -->
 2<button @click.stop="doThis"></button>
 3
 4<!-- 阻止默认行为 -->
 5<button @click.prevent="doThis"></button>
 6
 7<!-- 阻止默认行为,没有表达式 -->
 8<button @submit.prevent></button>
 9
10<!-- 串联修饰符 -->
11<button @click.stop.prevent="doThis"></button>
12
13<!-- 键修饰符,键别名 -->
14<button @keyup.enter="onEnter"></button>
15
16<!-- 键修饰符,键代码 -->
17<button @keyup.13="onEnter"></button>
18
19<!-- 点击回调只触发一次 -->
20<button @click.once="doThis"></button>

修饰符示例:

 1<div id="app">
 2  <!--1. .stop修饰符的使用-->
 3  <div @click="divClick">
 4    aaaaaaa
 5    <button @click.stop="btnClick">按钮</button>
 6  </div>
 7
 8  <!--2. .prevent修饰符的使用-->
 9  <br>
10  <form action="baidu">
11    <input type="submit" value="提交" @click.prevent="submitClick">
12  </form>
13
14  <!--3. .监听某个键盘的键帽-->
15  <input type="text" @keyup.enter="keyUp">
16
17  <!--4. .once修饰符的使用-->
18  <button @click.once="btn2Click">按钮2</button>
19</div>
20
21<script src="../js/vue.js"></script>
22<script>
23  const app = new Vue({
24    el: '#app',
25    data: {
26      message: '你好啊'
27    },
28    methods: {
29      btnClick() {
30        console.log("btnClick");
31      },
32      divClick() {
33        console.log("divClick");
34      },
35      submitClick() {
36        console.log('submitClick');
37      },
38      keyUp() {
39        console.log('keyUp');
40      },
41      btn2Click() {
42        console.log('btn2Click');
43      }
44    }
45  })
46</script>

2.10 条件判断

三个指令,v-if、v-else-if、v-else

原理:v-if 后面的条件为 false 时,对应的元素以及其子元素不会渲染。

 1<div id="app">
 2  <h2 v-if="score>=90">优秀</h2>
 3  <h2 v-else-if="score>=80">良好</h2>
 4  <h2 v-else-if="score>=60">及格</h2>
 5  <h2 v-else>不及格</h2>
 6
 7  <!-- 条件复杂的话使用计算属性 -->
 8  <h1>{{result}}</h1>
 9</div>
10
11<script src="../js/vue.js"></script>
12<script>
13  const app = new Vue({
14    el: '#app',
15    data: {
16      score: 99
17    },
18    computed: {
19      result() {
20        let showMessage = '';
21        if (this.score >= 90) {
22          showMessage = '优秀'
23        } else if (this.score >= 80) {
24          showMessage = '良好'
25        }
26        // ...
27        return showMessage
28      }
29    }
30  })
31</script>

条件判断中的小问题

先看一个例子:

 1<div id="app">
 2  <span v-if="isUser">
 3    <label for="username">用户账号</label>
 4    <!-- <input type="text" id="username" placeholder="用户账号" key="username"> -->
 5    <input type="text" id="username" placeholder="用户账号">
 6  </span>
 7  <span v-else>
 8    <label for="email">用户邮箱</label>
 9    <!-- <input type="text" id="email" placeholder="用户邮箱" key="email"> -->
10    <input type="text" id="email" placeholder="用户邮箱">
11  </span>
12  <button @click="isUser = !isUser">切换类型</button>
13</div>
14
15<script src="../js/vue.js"></script>
16<script>
17  const app = new Vue({
18    el: '#app',
19    data: {
20      isUser: true
21    }
22  })
23</script>

出现的问题:当我们在第一个 input 中输入了内容的时候,切换到第二个输入框,会发现内容并没有消失,输入框都不一样,内容为什么不消失了?

答案:这是因为 Vue 在进行 DOM 渲染时,会先创建一个虚拟 DOM,从虚拟 DOM 中取出内容,出于性能考虑,会尽可能的复用已经存在的元素,而不是重新创建新的元素。在上面的案例中,Vue 内部会发现原来的 input 元素不再使用,直接作为 else 中的 input 来使用了。

解决方法:给两个 input 设置不同的 key 属性,这样 vue 就不会再复用了,会重新创建一个 input。

2.11 v-show

v-show 的用法和 v-if 非常相似,也用于决定一个元素是否渲染。

v-if 和 v-show 的区别:
v-if 当条件为 false 时,压根不会有对应的元素在 DOM 中。
v-show 当条件为 false 时,仅仅是将元素的 display 属性设置为 none 而已。

开发中如何选择呢?
当需要在显示与隐藏之间切片很频繁时,使用 v-show
当只有一次切换时,通过使用 v-if。

 1<div id="app">
 2  <!--v-show: 当条件为false时, v-show只是给我们的元素添加一个行内样式: display: none-->
 3  <h2 v-show="isShow" id="bbb">{{message}}</h2>
 4</div>
 5
 6<script src="../js/vue.js"></script>
 7<script>
 8  const app = new Vue({
 9    el: '#app',
10    data: {
11      message: '你好啊',
12      isShow: true
13    }
14  })
15</script>

2.12 v-for

遍历数组:

 1<div id="app">
 2  <!--1.在遍历的过程中,没有使用索引值(下标值)-->
 3  <ul>
 4    <li v-for="item in names">{{item}}</li>
 5  </ul>
 6
 7  <!--2.在遍历的过程中, 获取索引值-->
 8  <ul>
 9    <li v-for="(item, index) in names">
10      {{index}}.{{item}}
11    </li>
12  </ul>
13</div>
14
15<script src="../js/vue.js"></script>
16<script>
17  const app = new Vue({
18    el: '#app',
19    data: {
20      names: ['why', 'kobe', 'james', 'curry']
21    }
22  })
23</script>

item 和 index 的顺序为默认,不能改变

遍历对象:

 1<div id="app">
 2  <!--1.在遍历对象的过程中, 如果只是获取一个值, 那么获取到的是value-->
 3  <ul>
 4    <li v-for="item in info">{{item}}</li>
 5  </ul>
 6  <!--2.获取key和value 格式: (value, key) -->
 7  <ul>
 8    <li v-for="(value, key) in info">{{value}}-{{key}}</li>
 9  </ul>
10  <!--3.获取key和value和index 格式: (value, key, index) -->
11  <ul>
12    <li v-for="(value, key, index) in info">{{value}}-{{key}}-{{index}}</li>
13  </ul>
14</div>
15
16<script src="../js/vue.js"></script>
17<script>
18  const app = new Vue({
19    el: '#app',
20    data: {
21      info: {
22        name: 'why',
23        age: 18,
24        height: 1.88
25      }
26    }
27  })
28</script>

value、key、index 的顺序不能改变

key 属性

官方推荐我们在使用 v-for 时,给对应的元素或组件添加上一个 :key 属性。
官方描述: v-for 的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key 来提供一个排序提示:

为什么需要这个key属性呢(了解)?
这个其实和 Vue 的虚拟 DOM 的 Diff 算法有关系。

当某一层有很多相同的节点时,也就是列表节点时,我们希望插入一个新的节点
我们希望可以在 B 和 C 之间加一个 F,Diff 算法默认执行起来是这样的。
即把 C 更新成 F,D 更新成 C,E 更新成 D,最后再插入 E,是不是很没有效率?

image.png

所以我们需要使用 key 来给每个节点做一个唯一标识
Diff 算法就可以正确的识别此节点
找到正确的位置区插入新的节点。
所以一句话,key 的作用主要是为了高效的更新虚拟 DOM。

key 需要保证与元素一一对应,即是元素的唯一标识,比如对象中的 id 属性,或者数组元素本身(数组没有重复元素时)。

key 不能为 index,index 不是元素的唯一标识。

重复的 key 会造成渲染错误。

 1<div id="app">
 2  <ul>
 3    <li v-for="item in letters" :key="item">{{item}}</li>
 4  </ul>
 5</div>
 6
 7<script src="../js/vue.js"></script>
 8<script>
 9  const app = new Vue({
10    el: '#app',
11    data: {
12      letters: ['A', 'B', 'C', 'D', 'E']
13    }
14  })
15</script>

2.13 响应式数组方法

因为 Vue 是响应式的,所以当数据发生变化时,Vue 会自动检测数据变化,视图会发生对应的更新。
Vue 中包含了一组观察数组编译的方法,使用它们改变数组也会触发视图的更新。

  • push()
  • pop()
  • shift()
  • unshift()
  • splice()
  • sort()
  • reverse()

直接通过数组坐标修改数组的值和直接修改对象属性的值是非响应式的,视图中的数据并不会发生改变。

vue 提供了全局 set 方法来进行响应式的数据修改,详见 Vue.set

 1<div id="app">
 2  <ul>
 3    <li v-for="item in letters">{{item}}</li>
 4  </ul>
 5  <button @click="btnClick">按钮</button>
 6</div>
 7
 8<script src="../js/vue.js"></script>
 9<script>
10  const app = new Vue({
11    el: '#app',
12    data: {
13      letters: ['a', 'b', 'c', 'd']
14    },
15    methods: {
16      btnClick() {
17        // 1.push方法
18        // this.letters.push('aaa')
19        // this.letters.push('aaaa', 'bbbb', 'cccc')
20
21        // 2.pop(): 删除数组中的最后一个元素
22        // this.letters.pop();
23
24        // 3.shift(): 删除数组中的第一个元素
25        // this.letters.shift();
26
27        // 4.unshift(): 在数组最前面添加元素
28        // this.letters.unshift()
29        // this.letters.unshift('aaa', 'bbb', 'ccc')
30
31        // 5.splice作用: 删除元素/插入元素/替换元素
32        // 删除元素: 第二个参数传入你要删除几个元素(如果没有传,就删除后面所有的元素)
33        // 替换元素: 第二个参数, 表示我们要替换几个元素, 后面是用于替换前面的元素
34        // 插入元素: 第二个参数, 传入0, 并且后面跟上要插入的元素
35        // splice(start)
36        // splice(start):
37        this.letters.splice(1, 3, 'm', 'n', 'l', 'x')
38        // this.letters.splice(1, 0, 'x', 'y', 'z')
39
40        // 5.sort()
41        // this.letters.sort()
42
43        // 6.reverse()
44        // this.letters.reverse()
45
46        // 注意: 通过索引值修改数组中的元素是非响应式的
47        // this.letters[0] = 'bbbbbb';   视图并没有发生改变
48
49        // 可以通过下面两种方式解决
50        // this.letters.splice(0, 1, 'bbbbbb')
51        // Vue.set(要修改的对象, 索引值, 修改后的值)
52        // Vue.set(this.letters, 0, 'bbbbbb')
53      }
54    }
55  })
56</script>

2.14 Vue.set

Vue.set( target, propertyName/index, value)

  • 参数:

    • {Object | Array} target
    • {string | number} propertyName/index
    • {any} value
  • 返回值:设置的值。

  • 用法:

    向响应式对象中添加一个 property,并确保这个新 property 同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新 property,因为 Vue 无法探测普通的新增 property (比如 this.myObject.newProperty = 'hi')

 1<div id="app">
 2  <ul>
 3    <li v-for="value in people">{{value}}</li>
 4  </ul>
 5  <button @click="btnClick">按钮</button>
 6</div>
 7
 8<script src="../js/vue.js"></script>
 9<script>
10  const app = new Vue({
11    el: '#app',
12    data: {
13      people: {
14        name: "zhangsan",
15        sex: "man"
16      }
17    },
18    methods: {
19      btnClick() {
20        Vue.set(this.people, "age", 18);
21        console.log(this.people);
22      }
23
24    }
25  })
26</script>

2.15 过滤器

过滤器可被用于一些常见的文本格式化。

过滤器可以用在两个地方:双花括号插值和 v-bind 表达式

 1<div id="app">
 2  <!-- 在双花括号中 -->
 3  {{ message | capitalize }}
 4
 5  <!-- 在 `v-bind` 中 -->
 6  <!-- <div v-bind:id="rawId | formatId"></div> -->
 7</div>
 8
 9<script src="../js/vue.js"></script>
10<script>
11  const app = new Vue({
12    el: '#app',
13    data: {
14      message: 'hello',
15    },
16    filters: {
17      capitalize: function(value) {
18        if (!value) return ''
19        value = value.toString()
20        return value.charAt(0).toUpperCase()
21      }
22    }
23  })
24</script>

过滤器可以串联:

1{{ message | filterA | filterB }}

过滤器可以接受参数:

1{{ message | filterA('arg1', arg2) }}

这里,filterA 被定义为接收三个参数的过滤器函数。其中 message 的值作为第一个参数,普通字符串 'arg1' 作为第二个参数,表达式 arg2 的值作为第三个参数。

2.16 v-model

Vue 中使用 v-model 指令来实现表单元素和数据的双向绑定。

 1<div id="app">
 2  <input type="text" v-model="message">
 3  {{message}}
 4</div>
 5
 6<script src="../js/vue.js"></script>
 7<script>
 8  const app = new Vue({
 9    el: '#app',
10    data: {
11      message: '你好啊'
12    }
13  })
14</script>

v-model 结合 radio

 1<div id="app">
 2  <label for="male">
 3    <input type="radio" id="male" value="男" v-model="sex"> 4  </label>
 5  <label for="female">
 6    <input type="radio" id="female" value="女" v-model="sex"> 7  </label>
 8  <h2>您选择的性别是: {{sex}}</h2>
 9</div>
10
11<script src="../js/vue.js"></script>
12<script>
13  const app = new Vue({
14    el: '#app',
15    data: {
16      message: '你好啊',
17      sex: '女'
18    }
19  })
20</script>

v-model 结合 checkbox

 1<div id="app">
 2  <!--1.checkbox单选框-->
 3  <!--<label for="agree">-->
 4    <!--<input type="checkbox" id="agree" v-model="isAgree">同意协议-->
 5  <!--</label>-->
 6  <!--<h2>您选择的是: {{isAgree}}</h2>-->
 7  <!--<button :disabled="!isAgree">下一步</button>-->
 8
 9  <!--2.checkbox多选框-->
10  <input type="checkbox" value="篮球" v-model="hobbies">篮球
11  <input type="checkbox" value="足球" v-model="hobbies">足球
12  <input type="checkbox" value="乒乓球" v-model="hobbies">乒乓球
13  <input type="checkbox" value="羽毛球" v-model="hobbies">羽毛球
14  <h2>您的爱好是: {{hobbies}}</h2>
15
16  <label v-for="item in originHobbies" :for="item">
17    <input type="checkbox" :value="item" :id="item" v-model="hobbies">{{item}}
18  </label>
19</div>
20
21<script src="../js/vue.js"></script>
22<script>
23  const app = new Vue({
24    el: '#app',
25    data: {
26      message: '你好啊',
27      isAgree: false, // 单选框
28      hobbies: [], // 多选框,
29      originHobbies: ['篮球', '足球', '乒乓球', '羽毛球', '台球', '高尔夫球']
30    }
31  })
32</script>

v-model 结合 select

 1<div id="app">
 2  <!--1.选择一个-->
 3  <select name="abc" v-model="fruit">
 4    <option value="苹果">苹果</option>
 5    <option value="香蕉">香蕉</option>
 6    <option value="榴莲">榴莲</option>
 7    <option value="葡萄">葡萄</option>
 8  </select>
 9  <h2>您选择的水果是: {{fruit}}</h2>
10
11  <!--2.选择多个-->
12  <select name="abc" v-model="fruits" multiple>
13    <option value="苹果">苹果</option>
14    <option value="香蕉">香蕉</option>
15    <option value="榴莲">榴莲</option>
16    <option value="葡萄">葡萄</option>
17  </select>
18  <h2>您选择的水果是: {{fruits}}</h2>
19</div>
20
21<script src="../js/vue.js"></script>
22<script>
23  const app = new Vue({
24    el: '#app',
25    data: {
26      message: '你好啊',
27      fruit: '香蕉',
28      fruits: []
29    }
30  })
31</script>

v-model 的修饰符

  • lazy: 可以让数据在失去焦点或者回车时才会更新
  • number: 默认情况下,在输入框中无论我们输入的是字母还是数字,都会被当做字符串类型进行处理。number 修饰符可以让在输入框中输入的内容自动转成数字类型
  • trim: 可以过滤内容左右两边的空格
 1<div id="app">
 2  <!--1.修饰符: lazy-->
 3  <input type="text" v-model.lazy="message">
 4  <h2>{{message}}</h2>
 5
 6
 7  <!--2.修饰符: number-->
 8  <input type="number" v-model.number="age">
 9  <h2>{{age}}-{{typeof age}}</h2>
10
11  <!--3.修饰符: trim-->
12  <input type="text" v-model.trim="name">
13  <h2>您输入的名字:{{name}}</h2>
14</div>
15
16<script src="../js/vue.js"></script>
17<script>
18  const app = new Vue({
19    el: '#app',
20    data: {
21      message: '你好啊',
22      age: 0,
23      name: ''
24    }
25  })
26</script>

2.17 watch 监听

  • 类型{ [key: string]: string | Function | Object | Array }

  • 详细

    一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个 property。当值被改变的时候调用回调函数。

  • 示例

     1var vm = new Vue({
     2  data: {
     3    a: 1,
     4    b: 2,
     5    c: 3,
     6    d: 4,
     7    e: {
     8      f: {
     9        g: 5
    10      }
    11    }
    12  },
    13  watch: {
    14    a: function (val, oldVal) {
    15      console.log('new: %s, old: %s', val, oldVal)
    16    },
    17    // 方法名
    18    b: 'someMethod',
    19    // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
    20    c: {
    21      handler: function (val, oldVal) { /* ... */ },
    22      deep: true
    23    },
    24    // 该回调将会在侦听开始之后被立即调用
    25    d: {
    26      handler: 'someMethod',
    27      immediate: true
    28    },
    29    // 你可以传入回调数组,它们会被逐一调用
    30    e: [
    31      'handle1',
    32      function handle2 (val, oldVal) { /* ... */ },
    33      {
    34        handler: function handle3 (val, oldVal) { /* ... */ },
    35        /* ... */
    36      }
    37    ],
    38    // watch vm.e.f's value: {g: 5}
    39    'e.f': function (val, oldVal) { /* ... */ }
    40  }
    41})
    42vm.a = 2 // => new: 2, old: 1
    

    注意,不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.updateAutocomplete 将是 undefined。

三、组件化开发

3.1 组件的基本使用

组件的使用分成三个步骤:

  • 创建组件构造器
  • 注册组件
  • 使用组件

image.png

 1<div id="app">
 2  <!--3.使用组件-->
 3  <my-cpn></my-cpn>
 4  <my-cpn></my-cpn>
 5  <my-cpn></my-cpn>
 6  <my-cpn></my-cpn>
 7</div>
 8
 9
10<script src="../js/vue.js"></script>
11<script>
12  // 1.创建组件构造器对象
13  const cpnC = Vue.extend({
14    template: `
15      <div>
16        <h2>我是标题</h2>
17        <p>我是内容, 哈哈哈哈</p>
18        <p>我是内容, 呵呵呵呵</p>
19      </div>`
20  })
21
22  // 2.注册组件
23  Vue.component('my-cpn', cpnC)
24
25  const app = new Vue({
26    el: '#app',
27    data: {
28      message: '你好啊'
29    }
30  })
31</script>
  • Vue.extend()

    • 调用Vue.extend() 创建的是一个组件构造器。
    • 通常在创建组件构造器时,传入 template 代表我们自定义组件的模板。
  • Vue.component()

    • 调用Vue.component() 是将刚才的组件构造器注册为一个组件,并且给它起一个组件的标签名称。
    • 所以需要传递两个参数:注册组件的标签名、组件构造器
  • 组件必须挂载在某个 Vue 实例下,否则它不会生效。

3.2 全局组件和局部组件

  • 当我们通过调用Vue.component() 注册组件时,组件的注册是全局的,这意味着该组件可以在任意 Vue 示例下使用。
  • 如果我们注册的组件是挂载在某个实例中, 那么就是一个局部组件

全局组件:

 1<div id="app">
 2  <cpn></cpn>
 3</div>
 4
 5<div id="app2">
 6    <cpn></cpn>
 7  </div>
 8<script src="../js/vue.js"></script>
 9<script>
10  // 1.创建组件构造器
11  const cpnC = Vue.extend({
12    template: `
13      <div>
14        <h2>我是标题</h2>
15        <p>我是内容,哈哈哈哈啊</p>
16      </div>
17    `
18  })
19
20  // 2.注册组件(全局组件, 意味着可以在多个Vue的实例下面使用)
21  Vue.component('cpn', cpnC)
22
23  const app = new Vue({
24    el: '#app',
25    data: {
26      message: '你好啊'
27    }
28  })
29
30  const app2 = new Vue({
31    el: '#app2'
32  })
33</script>

局部组件:

 1<div id="app">
 2  <cpn></cpn>
 3</div>
 4
 5<script src="../js/vue.js"></script>
 6<script>
 7  // 1.创建组件构造器
 8  const cpnC = Vue.extend({
 9    template: `
10      <div>
11        <h2>我是标题</h2>
12        <p>我是内容,哈哈哈哈啊</p>
13      </div>
14    `
15  })
16
17  const app = new Vue({
18    el: '#app',
19    data: {
20      message: '你好啊'
21    },
22    // 局部组件
23    components: {
24      // cpn使用组件时的标签名
25      cpn: cpnC
26    }
27  })
28</script>

注册组件的语法糖

注册组件的语法糖,主要是省去了调用 Vue.extend() 的步骤,而是可以直接使用一个对象来代替。

 1<div id="app">
 2  <cpn1></cpn1>
 3  <cpn2></cpn2>
 4</div>
 5
 6<script src="../js/vue.js"></script>
 7<script>
 8  // 1.全局组件注册的语法糖
 9  Vue.component('cpn1', {
10    template: `
11      <div>
12        <h2>我是标题1</h2>
13        <p>我是内容, 哈哈哈哈</p>
14      </div>
15    `
16  })
17
18  // 2.注册局部组件的语法糖
19  const app = new Vue({
20    el: '#app',
21    data: {
22      message: '你好啊'
23    },
24    components: {
25      'cpn2': {
26        template: `
27          <div>
28            <h2>我是标题2</h2>
29            <p>我是内容, 呵呵呵</p>
30          </div>
31    `
32      }
33    }
34  })
35</script>

3.3 父组件和子组件

子组件可以在父组件中进行注册,Vue 也称 root 组件,是最顶层的组件。

 1<div id="app">
 2  <cpn2></cpn2>
 3</div>
 4
 5<script src="../js/vue.js"></script>
 6<script>
 7  // 1.创建第一个组件构造器(子组件)
 8  const cpnC1 = Vue.extend({
 9    template: `
10      <div>
11        <h2>我是标题1</h2>
12        <p>我是内容, 哈哈哈哈</p>
13      </div>
14    `
15  })
16
17
18  // 2.创建第二个组件构造器(父组件)
19  const cpnC2 = Vue.extend({
20    template: `
21      <div>
22        <h2>我是标题2</h2>
23        <p>我是内容, 呵呵呵呵</p>
24        <cpn1></cpn1>
25      </div>
26    `,
27    components: {
28      cpn1: cpnC1
29    }
30  })
31
32  // root组件
33  const app = new Vue({
34    el: '#app',
35    data: {
36      message: '你好啊'
37    },
38    components: {
39      cpn2: cpnC2
40    }
41  })
42</script>

在 root 组件中可以用 cpnC2 组件,但不能使用 cpnC1组件,因为此组件没有在 root 组件中注册。

3.4 组件模板的分离写法

Vue 提供了两种方案来定义 HTML 模块内容:

  • 使用<script> 标签
  • 使用<template> 标签
 1<div id="app">
 2  <cpn></cpn>
 3  <cpn></cpn>
 4  <cpn></cpn>
 5</div>
 6<!--1.script标签, 注意:类型必须是text/x-template-->
 7<!--<script type="text/x-template" id="cpn">-->
 8<!--<div>-->
 9  <!--<h2>我是标题</h2>-->
10  <!--<p>我是内容,哈哈哈</p>-->
11<!--</div>-->
12<!--</script>-->
13
14<!--2.template标签-->
15<template id="cpn">
16  <div>
17    <h2>我是标题</h2>
18    <p>我是内容,呵呵呵</p>
19  </div>
20</template>
21
22<script src="../js/vue.js"></script>
23<script>
24
25  // 1.注册一个全局组件
26  Vue.component('cpn', {
27    template: '#cpn'
28  })
29
30  const app = new Vue({
31    el: '#app',
32    data: {
33      message: '你好啊'
34    }
35  })
36</script>

vuejs 2.x 版本要求,template 模板内容必须包裹在一个节点下面,比如 div,否则只显示一个元素节点

3.5 组件的数据存放

组件的 data 数据存储在 date 函数返回的对象中。

 1<div id="app">
 2  <cpn></cpn>
 3</div>
 4
 5<template id="cpn">
 6  <div>
 7    <h2>{{title}}</h2>
 8  </div>
 9</template>
10
11<script src="../js/vue.js"></script>
12<script>
13
14  // 1.注册一个全局组件
15  Vue.component('cpn', {
16    template: '#cpn',
17    data() {
18      return {
19        title: 'abc'
20      }
21    }
22  })
23
24  const app = new Vue({
25    el: '#app',
26    data: {
27      message: '你好啊'
28    }
29  })
30</script>

思考:为什么是函数返回对象而不是直接存储在对象中呢?

如果数据直接存储在对象中,当页面有多个一样的组件的时候,组件间的数据指向同一个内存地址,在第一个组件中修改完数据以后,第二个组件的数据也是修改后的,但是有函数返回的话,数据互不影响。

组件的方法放在 methods 中

 1<div id="app">
 2  <cpn></cpn>
 3  <cpn></cpn>
 4  <cpn></cpn>
 5</div>
 6
 7<template id="cpn">
 8  <div>
 9    <h2>当前计数: {{counter}}</h2>
10    <button @click="increment">+</button>
11    <button @click="decrement">-</button>
12  </div>
13</template>
14<script src="../js/vue.js"></script>
15<script>
16  const obj = {
17    counter: 0
18  }
19  Vue.component('cpn', {
20    template: '#cpn',
21    data() {
22      return {
23        counter: 0
24      }
25    },
26    methods: {
27      increment() {
28        this.counter++
29      },
30      decrement() {
31        this.counter--
32      }
33    }
34  })
35
36  const app = new Vue({
37    el: '#app'
38  })
39</script>

3.6 子父组件的通信

3.6.1 父组件向子组件传递值

方式:通过 props 向子组件传递数据

props 的值有两种方式:

  • 字符串数组,数组中的字符串就是传递时的名称。

     1<div id="app">
     2  <cpn :cmessage="message" :cmovies="movies"></cpn>
     3</div>
     4
     5<template id="cpn">
     6  <div>
     7    <ul>
     8      <li v-for="item in cmovies">{{item}}</li>
     9    </ul>
    10    <h2>{{cmessage}}</h2>
    11  </div>
    12</template>
    13
    14<script src="../js/vue.js"></script>
    15<script>
    16  // 父传子: props
    17  const cpn = {
    18    template: '#cpn',
    19    props: ['cmovies', 'cmessage'],
    20    data() {
    21      return {}
    22    },
    23    methods: {
    24
    25    }
    26  }
    27
    28  const app = new Vue({
    29    el: '#app',
    30    data: {
    31      message: '你好啊',
    32      movies: ['海王', '海贼王', '海尔兄弟']
    33    },
    34    components: {
    35      cpn
    36    }
    37  })
    38</script>
    
  • 对象,对象可以设置传递时的类型,也可以设置默认值等。

    • String
    • Number
    • Boolean
    • Array
    • Object
    • Date
    • Function
    • Symbol
     1<div id="app">
     2  <cpn :cmessage="message" :cmovies="movies"></cpn>
     3</div>
     4
     5
     6
     7<template id="cpn">
     8  <div>
     9    <ul>
    10      <li v-for="item in cmovies">{{item}}</li>
    11    </ul>
    12    <h2>{{cmessage}}</h2>
    13  </div>
    14</template>
    15
    16<script src="../js/vue.js"></script>
    17<script>
    18  // 父传子: props
    19  const cpn = {
    20    template: '#cpn',
    21    props: {
    22      // 1.类型限制
    23      // cmovies: Array,
    24      // cmessage: String,
    25
    26      // 2.提供一些默认值, 以及必传值
    27      cmessage: {
    28        type: String,
    29        default: 'aaaaaaaa',
    30        required: true
    31      },
    32      // 类型是对象或者数组时, 默认值必须是一个函数
    33      cmovies: {
    34        type: Array,
    35        default() {
    36          return []
    37        }
    38      }
    39    },
    40    data() {
    41      return {}
    42    },
    43    methods: {
    44
    45    }
    46  }
    47
    48  const app = new Vue({
    49    el: '#app',
    50    data: {
    51      message: '你好啊',
    52      movies: ['海王', '海贼王', '海尔兄弟']
    53    },
    54    components: {
    55      cpn
    56    }
    57  })
    58</script>
    

总结:

 1Vue.component('my-component', {
 2  props: {
 3    // 基础的类型检验('null'匹配任何类型)
 4    propA: Number,
 5    // 多个可能的类型
 6    propB: [String, Number],
 7    // 自定义类型
 8    propB2: People,
 9    // 必填的字符串
10    propC: {
11      type: String,
12      required: true
13    },
14    // 带有默认值的属性
15    propD: {
16      type: Number,
17      default: 100
18    },
19    // 带有默认值的对象
20    propE: {
21      type: Object,
22      // 对象或者数组默认值必须从一个工厂函数获取
23      default: function () {
24        return {
25          message: "hello"
26        }
27      }
28    },
29    // 自定义验证函数
30    propF: {
31      validator: function (value) {
32        // 这个值必须匹配下列字符串中的一个
33        return ['success', 'warning', 'danger'].indexOf(value) !== -1;
34      }
35    }
36
37  }
38})

标签中是不支持大写字母的,vue 支持在标签中用横杠拼接,在 prop 中用驼峰式接收属性,比如标签中写 my-data="hello",prop 中可以使用 myData 进行接收

3.6.2 子组件向父组件传递值

  • 子向父传递值通过自定义事件完成
  • 在子组件中,通过$emit() 来触发事件,第一个参数为要触发父组件中的自定义事件,第二个参数为要传递的值。
  • 在父组件中,通过 v-on 来监听子组件事件,在父组件中,会默认接收传递过来的参数,就类似于默认接受 event,因为此事件不是浏览器触发的,所以获取不到浏览数事件 event,能自动取到子组件传过来的值。所以父组件的 html 代码中,只需要写调用的方法名,不需要传值。
  • v-on不仅仅可以用于监听 DOM 事件,也可以用于组件间的自定义事件。

实例一

image20201118220136204.png

实例二

 1<!--父组件模板-->
 2<div id="app">
 3  <cpn @item-click="cpnClick"></cpn>
 4</div>
 5
 6<!--子组件模板-->
 7<template id="cpn">
 8  <div>
 9    <button v-for="item in categories"
10            @click="btnClick(item)">
11      {{item.name}}
12    </button>
13  </div>
14</template>
15
16<script src="../js/vue.js"></script>
17<script>
18
19  // 1.子组件
20  const cpn = {
21    template: '#cpn',
22    data() {
23      return {
24        categories: [
25          {id: 'aaa', name: '热门推荐'},
26          {id: 'bbb', name: '手机数码'},
27          {id: 'ccc', name: '家用家电'},
28          {id: 'ddd', name: '电脑办公'},
29        ]
30      }
31    },
32    methods: {
33      btnClick(item) {
34        // 发射事件: 自定义事件
35        this.$emit('item-click', item)
36      }
37    }
38  }
39
40  // 2.父组件
41  const app = new Vue({
42    el: '#app',
43    data: {
44      message: '你好啊'
45    },
46    components: {
47      cpn
48    },
49    methods: {
50      cpnClick(item) {
51        console.log('cpnClick', item);
52      }
53    }
54  })
55</script>

3.6.3 父子间通信案例

目的:从父组件传入值到子组件中,然后在子组件中对父组件的值进行修改。

错误的办法:在子组件中直接对 props 的值进行修改,Vue 不建议这样做。

正确的做法:在子组件中创建 data 变量( 或者计算属性 ),赋值为 props 传进来的值,然后对值进行修改,同时使用 emit 触发父组件中的事件,通过父组件中的方法对父组件的值进行修改。

 1<div id="app">
 2  <cpn :number1="num1"
 3       :number2="num2"
 4       @num1change="num1change"
 5       @num2change="num2change"/>
 6</div>
 7
 8<template id="cpn">
 9  <div>
10    <h2>props:{{number1}}</h2>
11    <h2>data:{{dnumber1}}</h2>
12    <input type="text" :value="dnumber1" @input="num1Input">
13    <h2>props:{{number2}}</h2>
14    <h2>data:{{dnumber2}}</h2>
15    <input type="text" :value="dnumber2" @input="num2Input">
16  </div>
17</template>
18
19<script src="../js/vue.js"></script>
20<script>
21  const app = new Vue({
22    el: '#app',
23    data: {
24      num1: 1,
25      num2: 0
26    },
27    methods: {
28      num1change(value) {
29        this.num1 = parseFloat(value)
30      },
31      num2change(value) {
32        this.num2 = parseFloat(value)
33      }
34    },
35    components: {
36      cpn: {
37        template: '#cpn',
38        props: {
39          number1: Number,
40          number2: Number
41        },
42        data() {
43          return {
44            dnumber1: this.number1,
45            dnumber2: this.number2
46          }
47        },
48        methods: {
49          num1Input(event) {
50            // 1.将input中的value赋值到dnumber中
51            this.dnumber1 = event.target.value;
52
53            // 2.为了让父组件可以修改值, 发出一个事件
54            this.$emit('num1change', this.dnumber1)
55
56            // 3.同时修饰dnumber2的值,让 dnumber2 的值为的number1 的 100 倍
57            this.dnumber2 = this.dnumber1 * 100;
58            this.$emit('num2change', this.dnumber2);
59          },
60          num2Input(event) {
61            this.dnumber2 = event.target.value;
62            this.$emit('num2change', this.dnumber2)
63
64            // 同时修饰dnumber1的值,让其值为 dnumber2的 1/100
65            this.dnumber1 = this.dnumber2 / 100;
66            this.$emit('num1change', this.dnumber1);
67          }
68        }
69      }
70    }
71  })
72</script>

3.6.4 watch 优化案例

用 v-model 双向绑定数据,用 watch 监听数据改变,刚数据改变时,使用 emit 调用父组件的方法。

 1<div id="app">
 2  <cpn :number1="num1"
 3       :number2="num2"
 4       @num1change="num1change"
 5       @num2change="num2change"/>
 6</div>
 7
 8<template id="cpn">
 9  <div>
10    <h2>props:{{number1}}</h2>
11    <h2>data:{{dnumber1}}</h2>
12    <input type="text" v-model="dnumber1">
13    <h2>props:{{number2}}</h2>
14    <h2>data:{{dnumber2}}</h2>
15    <input type="text" v-model="dnumber2">
16  </div>
17</template>
18
19<script src="../js/vue.js"></script>
20<script>
21  const app = new Vue({
22    el: '#app',
23    data: {
24      num1: 1,
25      num2: 0
26    },
27    methods: {
28      num1change(value) {
29        this.num1 = parseFloat(value)
30      },
31      num2change(value) {
32        this.num2 = parseFloat(value)
33      }
34    },
35    components: {
36      cpn: {
37        template: '#cpn',
38        props: {
39          number1: Number,
40          number2: Number,
41          name: ''
42        },
43        data() {
44          return {
45            dnumber1: this.number1,
46            dnumber2: this.number2
47          }
48        },
49        watch: {
50          dnumber1(newValue) {
51            this.dnumber2 = newValue * 100;
52            this.$emit('num1change', newValue);
53          },
54          dnumber2(newValue) {
55            this.number1 = newValue / 100;
56            this.$emit('num2change', newValue);
57          }
58        }
59      }
60    }
61  })
62</script>

3.7 父子组件的访问方式

3.7.1 父组件访问子组件

使用 $children$refs

  • $children, 可以获取子组件对象的数组

     1<div id="app">
     2  <cpn></cpn>
     3  <cpn></cpn>
     4  <button @click="btnClick">按钮</button>
     5</div>
     6
     7<template id="cpn">
     8  <div>我是子组件</div>
     9</template>
    10<script src="../js/vue.js"></script>
    11<script>
    12  const app = new Vue({
    13    el: '#app',
    14    methods: {
    15      btnClick() {
    16        // 1.$children
    17        console.log(this.$children);
    18        for (let c of this.$children) {
    19          console.log(c.name);
    20          c.showMessage();
    21        }
    22        console.log(this.$children[0].name);
    23      }
    24    },
    25    components: {
    26      cpn: {
    27        template: '#cpn',
    28        data() {
    29          return {
    30            name: '我是子组件的name'
    31          }
    32        },
    33        methods: {
    34          showMessage() {
    35            console.log('showMessage');
    36          }
    37        }
    38      },
    39    }
    40  })
    41</script>
    
  • $refs,根据 ref 属性访问子组件(常用)

     1<div id="app">
     2  <cpn ref="aaa"></cpn>
     3  <button @click="btnClick">按钮</button>
     4</div>
     5
     6<template id="cpn">
     7  <div>我是子组件</div>
     8</template>
     9<script src="../js/vue.js"></script>
    10<script>
    11  const app = new Vue({
    12    el: '#app',
    13    data: {
    14      message: '你好啊'
    15    },
    16    methods: {
    17      btnClick() {
    18        // 2.$refs => 对象类型, 默认是一个空的对象 ref='bbb'
    19        console.log(this.$refs.aaa.name);
    20      }
    21    },
    22    components: {
    23      cpn: {
    24        template: '#cpn',
    25        data() {
    26          return {
    27            name: '我是子组件的name'
    28          }
    29        }
    30      },
    31    }
    32  })
    33</script>
    

3.7.2 子组件访问父组件

使用 $parent,访问父组件对象,开发中一般不这样做

使用 $root 访问根组件对象

 1<div id="app">
 2  <cpn></cpn>
 3</div>
 4
 5<template id="cpn">
 6  <div>
 7    <h2>我是cpn组件</h2>
 8    <ccpn></ccpn>
 9  </div>
10</template>
11
12<template id="ccpn">
13  <div>
14    <h2>我是子组件</h2>
15    <button @click="btnClick">按钮</button>
16  </div>
17</template>
18
19<script src="../js/vue.js"></script>
20<script>
21  const app = new Vue({
22    el: '#app',
23    data: {
24      message: '你好啊'
25    },
26    components: {
27      cpn: {
28        template: '#cpn',
29        data() {
30          return {
31            name: '我是cpn组件的name'
32          }
33        },
34        components: {
35          ccpn: {
36            template: '#ccpn',
37            methods: {
38              btnClick() {
39                // 1.访问父组件$parent
40                console.log(this.$parent);
41                console.log(this.$parent.name);
42
43                // 2.访问根组件$root
44                console.log(this.$root);
45                console.log(this.$root.message);
46              }
47            }
48          }
49        }
50      }
51    }
52  })
53</script>

3.7.3 非父组件之间的通信

刚才我们讨论的都是父子组件间的通信,那如果是非父子关系呢?
非父子组件关系包括多个层级的组件,也包括兄弟组件的关系。
在 Vue1.x 的时候,可以通过 $dispatch$broadcast 完成
$dispatch 用于向上级派发事件
$broadcast 用于向下级广播事件
但是在 Vue2.x 都被取消了
在 Vue2.x 中,有一种方案是通过中央事件总线,也就是一个中介来完成。
但是这种方案和直接使用 Vuex 的状态管理方案还是逊色很多。
并且 Vuex 提供了更多好用的功能,所以这里我们暂且不讨论这种方案,后续我们专门学习 Vuex 的状态管理。

3.8 slot 插槽

插槽的基本使用

插槽默认值:定义插槽的时候,可以在 slot 中放入默认值,如果使用插槽的时候没有传值,便会使用这个默认值。

 1<!--
 21.插槽的基本使用 <slot></slot>
 32.插槽的默认值 <slot>button</slot>
 43.如果有多个值, 同时放入到组件进行替换时, 一起作为替换元素
 5-->
 6
 7<div id="app">
 8  <cpn></cpn>
 9
10  <cpn><span>哈哈哈</span></cpn>
11  <cpn><i>呵呵呵</i></cpn>
12  <cpn>
13    <i>呵呵呵</i>
14    <div>我是div元素</div>
15    <p>我是p元素</p>
16  </cpn>
17  <cpn></cpn>
18</div>
19
20
21<template id="cpn">
22  <div>
23    <h2>我是组件</h2>
24    <p>我是组件, 哈哈哈</p>
25    <slot><button>按钮</button></slot> 
26  </div>
27</template>
28
29<script src="../js/vue.js"></script>
30<script>
31  const app = new Vue({
32    el: '#app',
33    data: {
34      message: '你好啊'
35    },
36    components: {
37      cpn: {
38        template: '#cpn'
39      }
40    }
41  })
42</script>

具名插槽

当组件中有多个插槽的时候,直接在组件中加入内容会将所以插槽都给替换掉,当插槽有 name 属性的时候,则无法替换掉,只能通过使用插槽的时候指定 slot 来使用指定的插槽。

 1<div id="app">
 2  <cpn><span slot="center">标题</span></cpn>
 3  <cpn><button slot="left">返回</button></cpn>
 4  <cpn><span>test</span></cpn>
 5</div>
 6
 7
 8<template id="cpn">
 9  <div>
10    <slot name="left"><span>左边</span></slot>
11    <slot name="center"><span>中间</span></slot>
12    <slot name="right"><span>右边</span></slot>
13    <slot></slot>
14  </div>
15</template>
16
17<script src="../js/vue.js"></script>
18<script>
19  const app = new Vue({
20    el: '#app',
21    data: {
22      message: '你好啊'
23    },
24    components: {
25      cpn: {
26        template: '#cpn'
27      }
28    }
29  })
30</script>

作用域插槽

作用:在父组件使用子组件的时候,从子组件中取值

  1. 在组件内部 slot 标签上绑定变量
  2. 用 template 标签包裹要放入 slot 的内容
  3. 在 template 标签上加上 v-slot(缩写为#) 属性来绑定插槽
  4. 在 template 中的插槽内容中使用子组件的 prop
 1<div id="app">
 2  <cpn></cpn>
 3  <!--目的是获取子组件中的pLanguages-->
 4  <cpn>
 5    <!-- 绑定具名插槽 -->
 6    <template #slota="slotProps">
 7      <span>{{slotProps.languages.join(' * ')}}</span>
 8    </template>
 9
10  <!-- 绑定默认插槽,即没有名字的插槽 -->
11  <!-- <template #default="slotProps">
12    <span>{{slotProps.languages.join(' * ')}}</span>
13  </template> -->
14</cpn>
15
16  <!-- 当插槽没有名字时,可以化简 -->
17  <!-- <cpn #default="slotProps">
18    <span>{{slotProps.languages.join(' * ')}}</span>
19  </cpn> -->
20
21  <!-- 可以省略 default -->
22  <!-- <cpn v-slot="slotProps">
23    <span>{{slotProps.languages.join(' * ')}}</span>
24  </cpn> -->
25</div>
26
27<template id="cpn">
28  <div>
29    <slot :languages="pLanguages" name="slota">
30      <ul>
31        <li v-for="item in pLanguages">{{item}}</li>
32      </ul>
33    </slot>
34  </div>
35</template>
36<script src="../js/vue.js"></script>
37<script>
38  const app = new Vue({
39    el: '#app',
40    data: {
41      message: '你好啊'
42    },
43    components: {
44      cpn: {
45        template: '#cpn',
46        data() {
47          return {
48            pLanguages: ['JavaScript', 'C++', 'Java', 'C#', 'Python', 'Go', 'Swift']
49          }
50        }
51      }
52    }
53  })
54</script>

3.9 模块化

常见的模块化规范:CommonJS、AMD、CMD,也有 ES6 的 Modules。

在 html 中,想用模块化,需要在 script 标签加上,<script src="" type="module"></script>

3.9.1 commonJS 的导入与导出

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用;

Nodejs 用的是 commonjs

Commonjs 不适用于浏览器

 1// 导出 moduleA.js
 2module.exports = {
 3  flag: true,
 4  test(a, b) {
 5    return a + b;
 6  }
 7}
 8
 9// 导入
10// 方式一
11let temp = require('moduleA');
12console.log(temp.flag);
13// 方式二
14let { flag, test } = require('moduleA');

3.9.2 ES6 的导入与导出

ES6 用 export 导出,用 import 导入

ES6 模块输出的是值的引用

适用于浏览器

export

 1var name = '小明'
 2var age = 18
 3var flag = true
 4
 5function sum(num1, num2) {
 6  return num1 + num2
 7}
 8
 9if (flag) {
10  console.log(sum(20, 30));
11}
12
13// 1.导出方式一:
14export {
15  flag, sum
16}
17
18// 2.导出方式二:
19export var num1 = 1000;
20export var height = 1.88
21
22
23// 3.导出函数/类
24export function mul(num1, num2) {
25  return num1 * num2
26}
27
28export class Person {
29  run() {
30    console.log('在奔跑');
31  }
32}

export default

某些情况下,一个模块中包含某个的功能,我们并不希望给这个功能命名,而且让导入者可以自己来命名
这个时候就可以使用 export default

export default 在同一个模块中,不允许同时存在多个。

1// 1.export default
2// const address = '北京市'
3// export default address
4
5export default function (argument) {
6  console.log(argument);
7}

import

 1// 1.导入的{}中定义的变量
 2import {flag, sum} from "./aaa.js";
 3
 4if (flag) {
 5  console.log('小明是天才, 哈哈哈');
 6  console.log(sum(20, 30));
 7}
 8
 9// 2.直接导入export定义的变量
10import {num1, height} from "./aaa.js";
11
12console.log(num1);
13console.log(height);
14
15// 3.导入 export的function/class
16import {mul, Person} from "./aaa.js";
17
18console.log(mul(30, 50));
19
20const p = new Person();
21p.run()
22
23// 4.导入 export default中的内容,自定义名称,不需要加大括号
24import addr from "./aaa.js";
25
26addr('你好啊');
27
28// 5.统一全部导入
29// import {flag, num, num1, height, Person, mul, sum} from "./aaa.js";
30
31import * as aaa from './aaa.js'
32
33console.log(aaa.flag);
34console.log(aaa.height);

需要用 as 接收,不能直接 import * from

3.10 webpack

3.10.1 什么是 webpack

js 文件的打包

现在的 js 文件中使用了模块化的方式进行开发,他们可以直接使用吗?

不可以。因为如果直接在 index.html 引入这两个 js 文件,浏览器并不识别其中的模块化代码。
另外,在真实项目中当有许多这样的 js 文件时,我们一个个引用非常麻烦,并且后期非常不方便对它们进行管理。

我们应该怎么做呢?
使用 webpack 工具,对多个 js 文件进行打包。
webpack 就是一个模块化的打包工具,所以它支持我们代码中写模块化,可以对模块化的代码进行处理。
另外,如果在处理完所有模块之间的关系后,将多个 js 打包到一个 js 文件中,引入时就变得非常方便了。

什么是 webpack?

从本质上来讲,webpack 是一个现代的 JavaScript 应用的静态模块打包工具。

我们从两个点来解释上面这句话:模块打包

前端模块化:
webpack 其中一个核心就是让我们可能进行模块化开发,并且会帮助我们处理模块间的依赖关系。
而且不仅仅是 JavaScript 文件,我们的 CSS、图片、json 文件等等在 webpack 中都可以被当做模块来使用。
这就是 webpack 中模块化的概念。

打包:
理解了 webpack 可以帮助我们进行模块化,并且处理模块间的各种复杂关系后,打包的概念就非常好理解了。
就是将 webpack 中的各种资源模块进行打包合并成一个或多个包(Bundle)。
并且在打包的过程中,还可以对资源进行处理,比如压缩图片,将 scss 转成 css,将 ES6 语法转成 ES5 语法,将 TypeScript 转成JavaScript 等等操作。

grunt/gulp 和 webpack 的不同?
grunt/gulp 更加强调的是前端流程的自动化,模块化不是它的核心。
webpack 更加强调模块化开发管理,而文件压缩合并、预处理等功能,是他附带的功能。更强大。

3.10.2 使用方法

文件目录
dist 文件夹:用于存放之后打包的文件
src 文件夹:用于存放我们写的源文件
package.json:通过 npm init 生成的,npm 包管理的文件。

webpack 打包命令
webpack src/main.js dist/bundle.js

然后直接引入打包后的 bundle 即可,不需要在 script 标签中加上 type="module"

入口和出口

如果每次使用 webpack 的命令都需要写上入口和出口作为参数,就非常麻烦,有没有一种方法可以将这两个参数写到配置中,在运行时,直接读取呢?当然可以,就是创建一个 webpack.config.js 文件。

 1// npm 的模块,使用之前需要将项目 npm init
 2const path = require('path')
 3
 4module.exports = {
 5  entry: './src/main.js',
 6  output: {
 7    //path 只能使用绝对路径
 8    //__dirname是 npm 上下文中的东西,可以之前获取当前文件目录的绝对路径
 9    path: path.resolve(__dirname, 'dist'),
10    filename: 'bundle.js'
11  },
12}

然后使用 webpack 即可。

3.10.3 局部 webpack

我们上面在终端使用的为全局的 webpack,在项目中,我们一般为每个项目单独配置 webpack,然后使用局部的 webpack。

安装局部 webpack

1npm install webpack@3.6.0 --save-dev
2// -save-dev 表示只在开发时使用,--save,表示开发运行都使用

使用局部的 webpack

 1// 初始化 npm
 2npm init
 3//初始化完成以后,会出现 package.json 文件,内容大致如下
 4{
 5  "name": "meetwebpack",
 6  "version": "1.0.0",
 7  "description": "",
 8  "main": "index.js",
 9  "scripts": {
10    "test": "echo \"Error: no test specified\" && exit 1",
11    "build": "webpack"
12  },
13  "author": "",
14  "license": "ISC",
15  "devDependencies": {
16    "webpack": "^3.6.0"
17  }
18}
19// 在 scripts 中可定义脚本,定义完成以后,直接使用 npm run 脚本名 即可运行脚本,这时候使用的是本地的 webpack
20// npm 执行 script 脚本的时候,会寻找本地的 node_modules/.bin 路径中对应的命令。如果没有找到,会去全局的环境变量中寻找。
21npm run build
22
23// 如果初始化完成以后,想要直接在终端使用局部 webpack
24// 可以通过 node_modules/.bin/webpack 启动 webpack 打包

3.10.4 loader

loader 是 webpack 中一个非常核心的概念。
webpack 主要用来处理 js 代码,并且 webpack 会自动处理 js 之间相关的依赖。
但是,在开发中我们不仅仅有基本的 js 代码处理,我们也需要加载 css、图片,也包括一些高级的将 ES6 转成 ES5 代码,将 TypeScript 转成 ES5 代码,将 scss、less 转成 css,将 jsx、vue 文件转成 js 文件等等。
对于 webpack 本身的能力来说,对于这些转化是不支持的。这时候,给webpack扩展对应的loader就可以啦。

loader使用过程:

  1. 通过 npm 安装需要使用的 loader
  2. webpack.config.js 中的 modules 关键字下进行配置
  3. 大部分 loader 我们都可以在 webpack 的官网中找到,并且学习对应的用法。

webpack 处理 css 文件

  1. 书写 css 文件

  2. 在入口 js 文件中引用 css 文件,require('./css/normal.css')

  3. 安装 loader

    1npm install style-loader --save-dev //将模块的导出作为样式添加到 DOM 中
    2npm install --save-dev css-loader //解析 CSS 文件后,使用 import 加载,并且返回 CSS 代码
    
  4. webpack.config.js 中添加

     1module.exports = {
     2  module: {
     3    rules: [
     4      {
     5        test: /\.css$/,
     6        use: [ 'style-loader', 'css-loader' ]
     7      }
     8    ]
     9  }
    10}
    

    这次因为 webpack 在读取使用的 loader 的过程中,是按照从右向左的顺序读取的。所以 style-loader 需要放置在 css-loader 前面

  5. 然后只需在 html 中引用入口 js 文件即可

webpack 处理 less 文件

  1. 书写 less 文件

  2. 在入口 js 文件中引入

  3. 安装并配置 loader

     1npm install --save-dev less-loader less
     2
     3// webpack.config.js
     4module.exports = {
     5    ...
     6    module: {
     7        rules: [{
     8            test: /\.less$/,
     9            use: [{
    10                loader: "style-loader" // creates style nodes from JS strings
    11            }, {
    12                loader: "css-loader" // translates CSS into CommonJS
    13            }, {
    14                loader: "less-loader" // compiles Less to CSS
    15            }]
    16        }]
    17    }
    18};
    

webpack 处理图片文件

css 中引入了图片,直接使用 webpack 打包项目会出错,需要对图片资源做处理。

图片处理,我们使用 url-loader

 1npm install --save-dev url-loader
 2
 3module.exports = {
 4  module: {
 5    rules: [
 6      {
 7        test: /\.(png|jpg|gif)$/,
 8        use: [
 9          {
10            loader: 'url-loader',
11            options: {
12              limit: 8192
13            }
14          }
15        ]
16      }
17    ]
18  }
19}
20
21// 当图片小于 limit 限制的时候,在网页中加载的是 base64
22// 当图片大小大于 limit 限制的时候,需要使用 file-loader
23npm install --save-dev file-loader
24
25// 打包图片的时候 webpack 自动帮助我们给图片生成一个非常长的名字,这是一个32位hash值,目的是防止名字重复
26// 一般情况下,我们希望自命名,需要在 options 中添加如下选项
27name:'ima/[name].[hash:8].[ext]'
28// img:文件要打包到的文件夹
29// name:获取图片原来的名字,放在该位置
30// hash:8:为了防止图片名称冲突,依然使用hash,但是我们只保留8位
31// ext:使用图片原来的扩展名
32
33// 但是,我们发现图片并没有显示出来,这是因为图片使用的路径不正确
34// 默认情况下,webpack 会将生成的路径直接返回给使用者
35// 但是,我们整个程序是打包在 dist 文件夹下的,所以这里我们需要在出口 output 下添加
36publicPath:'dist/'

ES6 转 ES5

 1npm install --save-dev babel-loader@7 babel-core babel-preset-es2015
 2
 3module: {
 4  rules: [
 5    {
 6      test: /\.js$/,
 7      exclude: /(node_modules|bower_components)/,
 8      use: {
 9        loader: 'babel-loader',
10        options: {
11          presets: ['es2015']
12        }
13      }
14    }
15  ]
16}

3.10.5 npm 使用 vue

 1npm install --save vue
 2
 3// 在 js 中
 4import Vue from 'vue';
 5
 6// 若报 runtime-only 错误,在 webpack.config.js 中加入
 7resolve: {
 8  // alias: 别名
 9  extensions: ['.js', '.css', '.vue'],
10  alias: {
11    'vue$': 'vue/dist/vue.esm.js'
12  }
13}

3.11 template 和 el 的区别

1// 我们在 html 中定义一个空标签
2<div id="app"></div>
3
4// 在新建 vue 实例的时候,加上 template 属性,会自动替换 heml 中 el 定义的标签
5new Vue({
6  el: '#app',
7  template: '<h2>hello</h2>'
8});

3.12 Vue 的终极使用方案

  1. 新建 html 文件

     1<!DOCTYPE html>
     2<html lang="en">
     3<head>
     4  <meta charset="UTF-8">
     5  <title>Title</title>
     6</head>
     7<body>
     8
     9<div id="app"></div>
    10
    11<script src="./dist/bundle.js"></script>
    12</body>
    13</html>
    
  2. 新建入口 js 文件

     1import Vue from 'vue'
     2import App from './vue/App.vue'
     3
     4new Vue({
     5  el: '#app',
     6  template: '<App/>',
     7  components: {
     8    App
     9  }
    10})
    
  3. 新建 vue 树结构的根文件 App.vue

     1// html 代码
     2<template>
     3  <div>
     4    <h2 class="title">{{message}}</h2>
     5    <button @click="btnClick">按钮</button>
     6    <h2>{{name}}</h2>
     7    <Cpn/>
     8  </div>
     9</template>
    10
    11// js 代码
    12<script>
    13  export default {
    14    name: "App",
    15    data() {
    16      return {
    17        message: 'Hello Webpack',
    18        name: 'coderwhy'
    19      }
    20    },
    21    methods: {
    22      btnClick() {
    23
    24      }
    25    }
    26  }
    27</script>
    28
    29// css 代码
    30<style scoped>
    31  .title {
    32    color: green;
    33  }
    34</style>
    
  4. 对项目进行打包

webpack 处理 vue 文件

1npm install vue-loader vue-template-compiler --save-dev
2
3// 修改 webpack.config.js 的配置文件
4{
5    test: /\.vue$/,
6    use: ['vue-loader']
7}

3.13 webpack 插件

plugin 是插件的意思,通常是用于对某个现有的架构进行扩展。

webpack 中的插件,就是对 webpack 现有功能的各种扩展,比如打包优化,文件压缩等等。

loader 和plugin 区别
loader 主要用于转换某些类型的模块,它是一个转换器。
plugin 是插件,它是对 webpack 本身的扩展,是一个扩展器。

plugin 的使用过程:

  1. 通过 npm 安装需要使用的 plugins (某些 webpack 已经内置的插件不需要安装)
  2. webpack.config.js 中的 plugins 中配置插件。

3.13.1 添加版权的插件

BannerPlugin,属于 webpack 自带的插件。

1const webpack = require('webpack')
2
3module.exports = {
4  ...
5  plugins : [
6    new webpack.BannerPlugin('最终版权归 rainsheep 所有')
7  ]
8}

3.13.2 打包 html 的 plugin

发布项目的时候,我们需要将 index.html 放置在 dist 中,需要使用 HtmlWebpackPlugin 插件 。

HtmlWebpackPlugin 插件可以为我们做这些事情:
自动生成一个 index.html 文件(可以指定模板来生成)
将打包的 js 文件,自动通过 script 标签插入到 body 中。

1npm install html-webpack-plugin --save-dev
2
3plugins: [
4    new HtmlWebpackPlugin({
5      template: 'index.html'
6    })
7],

这里的 template 表示根据什么模板来生成 index.html
另外,我们需要删除之前在 output 中添加的 publicPath 属性,否则插入的 script 标签中的 src 可能会有问题

3.13.3 压缩 js 插件

在项目发布之前,我们必然需要对 js 等文件进行压缩处理。
我们使用一个第三方的插件 uglifyjs-webpack-plugin,并且版本号指定 1.1.1,和 CLI2 保持一致。

1npm install uglifyjs-webpack-plugin@1.1.1 --save-dev
2
3plugins: [
4    new UglifyjsWebpackPlugin()
5]

3.13.4 搭建本地服务器

webpack 提供了一个可选的本地开发服务器,这个本地服务器基于 node.js 搭建,内部使用 express 框架,可以实现我们想要的让浏览器自动刷新显示我们修改后的结果。不过它是一个单独的模块,在 webpack 中使用之前需要先安装它。

1npm install --save-dev webpack-dev-server@2.9.1
2
3// 在 webpack.config.js 添加如下内容
4devServer: {
5  contentBase: './dist',
6  inline: true
7}
  • contentBase:为哪一个文件夹提供本地服务,默认是根文件夹,我们这里要填写./dist
  • port:端口号
  • inline:页面实时刷新
  • historyApiFallback:在 SPA 页面中,依赖 HTML5 的 history 模式

然后在 package.json 中添加一个 scripts

1// --open 参数表示自动打开浏览器
2"dev": "webpack-dev-server --open"

配置分离

当我们有一个开发环境,有一个生产环境的时候,可以对两个环境的配置进行分离,即配置不同的配置文件。

安装插件

1npm install webpack-merge --save-dev
 1// 新建 ./build/base.config.js,保存公共配置
 2const path = require('path')
 3const webpack = require('webpack')
 4const HtmlWebpackPlugin = require('html-webpack-plugin')
 5const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
 6
 7module.exports = {
 8  entry: './src/main.js',
 9  output: {
10    // 配置输出目录
11    path: path.resolve(__dirname, '../dist'),
12    filename: 'bundle.js'
13  },
14  module: {
15    rules: [
16    ]
17  },
18  resolve: {
19  },
20  plugins: [
21  ]
22}
 1// 新建 ./build/prod.config.js,只写生产环境需要的配置
 2const UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')
 3const webpackMerge = require('webpack-merge')
 4const baseConfig = require('./base.config')
 5
 6// 跟基本篇配置合并
 7module.exports = webpackMerge(baseConfig, {
 8  plugins: [
 9    new UglifyjsWebpackPlugin()
10  ]
11})
 1// 新建 ./build/dev.config.js,只写开发环境需要的配置
 2const webpackMerge = require('webpack-merge')
 3const baseConfig = require('./base.config')
 4
 5// 跟基本篇配置合并
 6module.exports = webpackMerge(baseConfig, {
 7  devServer: {
 8    contentBase: './dist',
 9    inline: true
10  }
11})
1// 在 package.json 中配置脚本,手动指定配置文件
2"scripts": {
3  "build": "webpack --config ./build/prod.config.js",
4  "dev": "webpack-dev-server --open --config ./build/dev.config.js"
5}

四、Vue CLI洋解

4.1 什么是 Vue CLI

使用 Vue.js 开发大型应用时,我们需要考虑代码目录结构、项目结构和部署、热加载、代码单元测试等事情。
如果每个项目都要手动完成这些工作,那无疑效率比较低效,所以通常我们会使用一些脚手架工具来帮助完成这些事情。

CLI 是 Command-Line Interface, 翻译为命令行界面, 但是俗称脚手架.
Vue CLI 是一个官方发布 vue.js 项目脚手架
使用 vue-cli 可以快速搭建Vue开发环境以及对应的webpack配置.

使用脚手架需要已经安装 node 和 webpack

安装 Vue 脚手架

1// 安装 Vue CLI3
2npm i -g @vue/cli
3vue --version
4// 拉取 2.x 模板,既可以用 3,也可以用 2
5npm i -g @vant/cli-init

CLI2 和 CLI3 的区别

vue-cli 3 是基于 webpack 4 打造,vue-cli 2 还是 webapck 3
vue-cli 3 的设计原则是“0配置”,移除的配置文件根目录下的,build 和 config 等目录
vue-cli 3 提供了 vue ui 命令,提供了可视化配置,更加人性化
vue-cli 3 移除了static 文件夹,新增了 public 文件夹,并且 index.html 移动到 public 中

4.2 Vue CLI 创建项目

CLI2

vuecli2.png

CLI3

cli3.png

  • Babel:ES 代码转换
  • PWA:高级的 web app,目前用的较少(有本地存储和通知等新功能),不选择
  • css pre-processors:处理 less 等
  • Linter/Formatter:ESlint 对代码进行规范

怎么删除自己保存的配置?

删除用户目录下的 .vuerc 文件

4.3 CLI 目录结构介绍

4.3.1 CLI2 目录结构

dirdesc.png

build 文件夹:webpack 相关配置

config 文件夹: webpack 相关配置

node_modules 文件夹: 依赖的 node 相关的模块

src 文件夹: 写源码的地方

static 文件夹: 存放静态资源

.gitkeep: 文件为空,也需要上传 git

.babelrc:ES 代码相关转换配置,比如 ES6 转 ES5

.editconfig:项目的文本配置

.gitignore:git 仓库忽略文件

.postcssrc.js:css 相关转换配置

index.html:项目首页,vue 的承载文件

package-lock.json:存储 package 中包的版本信息

package.json:声明所使用的 node 包

README.md:项目声明信息

4.3.2 CLI3 目录结构

cli3.png

public 文件夹 : 相当于 CLI2 的 static,将 index.html 也放置在这下面

.browserslistrc:配置需要兼容的浏览器

4.4 vue 程序运行过程

vue 程序运行过程

vue程序运行过程.png

runtime-compiler 时程序运行过程

template(模板) -> ast(抽象语法树) -> render(渲染函数) -> vdom(虚拟 dom 树) -> UI(用户看到的界面)

main.js 中的写法

1new Vue({
2  el: '#app',
3  components: { App },
4  template: '<App/>'
5})

**runtime-only **时程序运行过程

render -> vdom -> UI

优点:1.性能更高 2.底层的代码量更少

main.js 中的写法

 1new Vue({
 2  el: '#app',
 3  render: function (createElement) {
 4    // 1.普通用法: createElement('标签', {标签的属性}, [''])
 5    // return createElement('h2',
 6    //   {class: 'box'},
 7    //   ['Hello World', createElement('button', ['按钮'])])
 8
 9    // 2.传入组件对象:
10    return createElement(App)
11  }
12})

问:那么 vue 文件中的 template 是由谁处理的?

答:由 vue-template-compiler 处理的。

总结:如果开发中,依然使用 template,就需要选择 Runtime-Compiler,如果之后的开发中,使用的是 vue 文件开发,那么可以选择 Runtime-only

官方文档

当使用 vue-loadervueify 的时候,*.vue 文件内部的模板会在构建时预编译成 JavaScript。你在最终打好的包里实际上是不需要编译器的,所以只用运行时版本即可

4.5 构建和运行

npm run dev 和 npm run build

npm run build:构建项目,将项目打包,包括打包第三方库和代码压缩操作等。

build.png

npm run dev: cli2 中将项目跑起来,可以在浏览器查看效果,不会进行代码压缩等操作。

npm run server:类似于 npm run dev,cli3 的命令

dev.png

最左边的生产环境参数为 config/dev.env.js ,NODE_ENV: "development"

CLI3 通过图形化界面配置项目

命令:vue ui

问题:那么 webpack 的配置去哪呢?

答:其实官方帮你隐藏起来了,在 node_modules/@vue/cli-server/webpack.config.js 中,在这个文件中,又导入了别的文件。

vue cli3 官方推荐为零配置,如果想自定义配置,在项目根目录下新建 vue.config.js 文件,书写配置,cli 会帮你自动合并。

1module.exports = {
2
3}

五、vue-router

5.1 前端路由

路由的发展可分为三个阶段:

后端渲染界面:

jsp、php 等语言写的,由后端来渲染界面,每次都去请求后端,然后后端返回渲染完之后的界面,直接展示。

前端渲染阶段:

就是前后端分离阶段,后端只负责提供数据,有前端来进行界面的渲染,前端用 ajax 去请求数据,然后通过 js 来渲染数据,一套 html+css+js 对应一个前端界面。

前端路由阶段:

单页面富应用阶段(SPA),最主要的特点就是在前后端分离的基础上加了一层前端路由,也就是前端来维护一套路由规则。改变URL,但是页面不进行整体的刷新,由前端路由的映射关系从已经请求的资源中提取出要展示的资源。

可以理解为整个网页只有一套 html+css+js,当用户点击改变界面的时候,由前端路由进行管理,从已经加载的一套代码中提取出需要的 html+css+js 来进行页面渲染。当 url 发生改变的时候,并不需要再去前端服务器就行请求资源。

5.2 前端修改 URL 的两种方式

URL 的 hash

URL 的 hash 也就是锚点(#), 本质上是改变 window.location 的 href 属性。
我们可以通过直接赋值 location.hash 来改变 href, 但是页面不发生刷新。

hash.png

HTML5 的 history 模式

history.pushState({}, '', '/foo') 来向页面栈中压入页面,history.pushState({}, '', '/foo') 来替换当前界面,history.back() 返回页面,history.forward() 前进页面,history.go(n) 来跳转页面。

replaceState.png
go.png

5.3 安装和使用 vue-router

安装 vue-router

1npm install vue-router --save

vue 项目集成 vue-router

在模块化工程中使用它(因为是一个插件, 所以可以通过 Vue.use() 来安装路由功能)

  1. 在 src 下新建router/index.js
  2. 导入路由对象,并且调用Vue.use(VueRouter)
  3. 创建路由实例,并且传入路由映射配置
  4. 在 Vue 实例中挂载创建的路由实例
 1// src/router/index.js
 2// 配置路由相关的信息
 3import VueRouter from 'vue-router'
 4import Home from '../views/Home.vue'
 5
 6// 1.通过Vue.use(插件), 安装插件
 7Vue.use(VueRouter)
 8
 9// 2.创建VueRouter对象
10const routes = [
11  {
12    path: '/',
13    component: Home
14  }
15]
16
17const router = new VueRouter({
18  routes
19})
20
21// 3.将router对象传入到Vue实例
22export default router
1// src/main.js
2import router from './router'
3
4new Vue({
5  el: '#app',
6  router,
7  render: h => h(App)
8})

使用 vue-router

  1. 创建路由组件
  2. 配置路由映射: 组件和路径映射关系
  3. 通过<router-link><router-view> (占位,路由内容显示位置)使用路由

router-link

  • 属性 to:指定跳转的路由
  • 属性 tag:指定渲染成什么标签,a、buttom、li 等
  • 属性 replace: replace 不会留下 history 记录, 所以指定 replace 的情况下, 后退键返回不能返回到上一个页面中
    • active-class: 当<router-link> 对应的路由匹配成功时, 会自动给当前元素设置一个router-link-active 的 class, 设置active-class 可以修改默认的名称。

全局修改默认激活 class

1// src/router/index.js
2const router = new VueRouter({
3  linkActiveClass: 'active'
4})

路由的默认路径

默认情况下, 进入网站的首页, 我们希望 <router-view> 渲染首页的内容。

我们在 routes 中配置了一个默认映射,path 配置的是根路径 /,redirect 是重定向, 也就是我们将根路径重定向到 /home 的路径下。

1const routes = [
2  {
3    path: '/',
4    // redirect重定向
5    redirect: '/home'
6  }
7]

使用 HTML5 的 History 模式

这种模式,URL 就没有 # 号了

1// src/router/index.js
2const router = new VueRouter({
3  routes,
4  mode: 'history'
5})

5.4 界面跳转

通过 js 代码进行路由跳转

1// vue-router 自动为每个对象添加了一个 $router 对象,调用其中的方法来进行页面跳转
2this.$router.push('/home')
3this.$router.replace('/home')

动态路由

/user/zhangsan/user/lisi,除了有前面的 /user 之外,后面还跟上了用户的 ID,这种 path 和 Component 的匹配关系,我们称之为动态路由(也是路由传递数据的一种方式)。

配置路由

1{
2   path: '/user/:id',
3   component: User
4}

跳转链接

1<router-link :to="'/user/'+userId">用户</router-link>

在组件内使用页面路由的参数

1<h2>{{$route.params.id}}</h2>

$router 是“路由实例”对象,即使用 new VueRouter 创建的实例,包括了路由的跳转方法,钩子函数等。

$route 是“路由信息对象”,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。

5.5 webpack 打包文件解析

1/dist/static/js/app.xxxx.js
2当前应用程序的业务代码
3
4/dist/static/js/manifest.xxxx.js
5为被打包的代码(导入导出等)做底层支撑
6
7/dist/static/js/vendor.xxxx.js
8第三方(vue/vue-router/axios)的代码会打包到这个文件里面

5.6 路由懒加载

当打包构建应用时,Javascript 包会变得非常大,影响页面加载。

路由懒加载的主要作用就是将路由对应的组件打包成一个个的 js 代码块。只有在这个路由被访问到的时候, 才加载对应的组件。

路由懒加载会把路由对应的每个组件打包成对应的 js 文件放置在 /dist/static/js/ 下。

路由懒加载的方式:

将 import 语句换成下面语句

方式一: 结合 Vue 的异步组件和 Webpack 的代码分析

1const Home = resolve => { require.ensure(['../components/Home.vue'], () => { resolve(require('../components/Home.vue')) })};

方式二: AMD写法

1const About = resolve => require(['../components/About.vue'], resolve);

方式三: 在 ES6 中, 我们可以有更加简单的写法来组织 Vue 异步组件和 Webpack 的代码分割,最常用

1const Home = () => import('../components/Home.vue');

总结:

1// 懒加载前
2import Home from '../components/Home'
3const routes = [
4  {
5    path: '/home',
6    component: Home
7   }
8]

懒加载前.png

1// 懒加载后
2const Home = () => import('../components/Home')
3const routes = [
4  {
5    path: '/home',
6    component: Home
7   }
8]

懒加载后.png

5.7 嵌套路由

比如在 home 页面中, 我们希望通过 /home/news/home/message 访问一些内容。
一个路径映射一个组件, 访问这两个路径也会分别渲染两个组件。

配置路由:

 1const HomeNews = () => import('../components/HomeNews')
 2const HomeMessage = () => import('../components/HomeMessage')
 3const routes = [
 4  {
 5    path: '/home',
 6    component: Home,
 7    meta: {
 8      title: '首页'
 9    },
10    children: [
11      // 默认路径
12      {
13        path: '',
14        redirect: 'news'
15      },
16      {
17        path: 'news',
18        component: HomeNews
19      },
20      {
21        path: 'message',
22        component: HomeMessage
23      }
24    ]
25  }
26]

注意二级路由 path 不需要加斜杠

在 Home 组件中使用

1<template>
2  <div>
3    <router-link to="/home/news">新闻</router-link>
4    <router-link to="/home/message">消息</router-link>
5    <router-view></router-view>
6    <h2>{{message}}</h2>
7  </div>
8</template>

5.8 路由跳转时参数的传递

方式一:动态路由方式(params 方式)

方式二:query 方式

传递的方式: 路径使用对象,对象中使用 query 的 key 作为传递方式
传递后形成的路径: /router?id=123&age=18

 1// html 跳转
 2<router-link :to="{path: '/profile', query: {name: 'why', age: 18, height: 1.88}}">
 3
 4// js 代码跳转
 5this.$router.push({
 6  path: "/profile",
 7  query: {
 8    name: "kobe",
 9    age: 19,
10    height: 1.87
11  },
12});

获取传递过来的参数:

1<h2>{{$route.query.name}}</h2>

5.9 route和router

$router 是“路由实例”对象,即使用 new VueRouter 创建的实例,包括了路由的跳转方法,钩子函数等。

$router 为 VueRouter 实例,想要导航到不同 URL,则使用 $router.push 方法。

所有的组件继承自 vue 的原型,之所以可以使用 $router,就是路由实例向 Vue.property 原型中添加了 $router 属性。

$route 是“路由信息对象”,包含当前活跃组件路由信息,包括 path,params,hash,query,fullPath,matched,name 等路由信息参数。

5.10 导航守卫

官方文档:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html#%E5%85%A8%E5%B1%80%E8%A7%A3%E6%9E%90%E5%AE%88%E5%8D%AB

我们来考虑一个需求: 在一个 SPA 应用中, 如何改变网页的标题呢?

网页标题是通过 <title> 来显示的, 但是 SPA 只有一个固定的 HTML, 切换不同的页面时, 标题并不会改变。
但是我们可以通过 JavaScript 来修改 <title> 的内容。window.document.title = '新的标题'

那么在 Vue 项目中, 在哪里修改? 什么时候修改比较合适呢?

普通的修改方式:
我们比较容易想到的修改标题的位置是每一个路由对应的组件 vue 文件中。
通过生命周期函数, 执行对应的代码进行修改即可。

但是当页面比较多时, 这种方式不容易维护(因为需要在多个页面执行类似的代码)。

有没有更好的办法呢? 使用导航守卫即可。

什么是导航守卫?

vue-router 提供的导航守卫主要用来监听监听路由的进入和离开的.
vue-router 提供了 beforeEach 和 afterEach 的钩子函数, 它们会在路由即将改变前和改变后触发。

导航守卫的使用

 1// src/router/index.js
 2// 定义路由的时候定义元数据
 3const routes = [
 4  {
 5    path: '/profile',
 6    component: Profile,
 7    meta: {
 8      title: '档案'
 9    }
10  }
11]
12
13// 利用导航守卫,修改我们的标题
14// to: 即将要进入的目标的路由对象.
15// from: 当前导航即将要离开的路由对象.
16// next: 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。
17// 前置守卫(guard)
18router.beforeEach((to, from, next) => {
19  // 从from跳转到to
20  document.title = to.matched[0].meta.title
21  next()
22})

全局后置钩子

1// 没有 next 参数
2router.afterEach((to, from) => {
3  // ...
4})

路由独享守卫

你可以在路由配置上直接定义 beforeEnter 守卫:

 1const router = new VueRouter({
 2  routes: [
 3    {
 4      path: '/foo',
 5      component: Foo,
 6      beforeEnter: (to, from, next) => {
 7        // ...
 8      }
 9    }
10  ]
11})

这些守卫与全局前置守卫的方法参数是一样的。

当全局守卫和路由独享守卫都有的时候,会先进入全局守卫,再进入独享守卫

组件内的守卫

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

5.11 keep-alive

keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,或避免重新渲染(创新 created)。

router-view 也是一个组件,如果直接被包在 keep-alive 里面,所有路径匹配到的视图组件都会被缓存。

如,在 App.vue

1<keep-alive>
2  <router-view/>
3</keep-alive>

则,所有子组件都会被缓存。

如果组件被缓存,则组件会多两个生命周期函数 activateddeactivated 来标记组件被激活,或者进入不活跃状态。

Keep-alive 有两个非常重要的属性:

  • include: 字符串或正则表达,只有匹配的组件会被缓存
  • exclude: 字符串或正则表达式,任何匹配的组件都不会被缓存
1// 字符串为组件的 name,vue 文件的 script 中的 name
2<keep-alive exclude="Profile,User">
3  <router-view/>
4</keep-alive>

5.12 路径别名

我们可以在 build/webpack.base.conf.js 中对路径起别名

 1module.exports = {
 2  resolve: {
 3    extensions: ['.js', '.vue', '.json'],
 4    alias: {
 5      '@': resolve('src'),
 6      'assets': resolve('src/assets'),
 7      'components': resolve('src/components')
 8    }
 9  }
10}

当我们配置完别名时,可以在组件中使用

 1<template>
 2  <tab-bar>
 3    <tab-bar-item path="/home" activeColor="pink">
 4      <img slot="item-icon" src="~assets/img/tabbar/home.svg" alt="">
 5      <img slot="item-icon-active" src="~assets/img/tabbar/home_active.svg" alt="">
 6      <div slot="item-text">首页</div>
 7    </tab-bar-item>
 8</template>
 9
10<script>
11  import TabBar from 'components/tabbar/TabBar'
12  import TabBarItem from 'components/tabbar/TabBarItem'
13</script>

在 html 中使用,则需要在前面加上 ~

六、Vuex 详解

6.1 什么是 Vuex

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。
它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。

状态管理到底是什么?
可以简单的将其看成把需要多个组件共享的变量全部存储在一个对象里面。然后,将这个对象放在顶层的 Vue 实例中,让其他组件可以使用。并且这个状态是响应式的。

我们一般用来存储用户的登录状态、用户名称、头像、地理位置信息、商品的收藏、购物车中的物品等等。

6.2 Vuex 的使用

 1// 安装
 2npm install vuex --save
 3
 4// 新建 src/store/index.js
 5import Vue from 'vue'
 6import Vuex from 'vuex'
 7// 安装插件
 8Vue.use(Vuex)
 9
10// 创建对象
11const state = {
12  count: 0
13}
14const store = new Vuex.Store({
15  state,
16  mutations: {
17    increment(state){
18      state.count++
19    },
20    decrement(state) {
21      state.count--
22    }
23  }
24//  actions,
25//  getters,
26//  modules
27})
28
29// 3.导出store独享
30export default store
1src/main.js 引入
2import store from './store'
3
4new Vue({
5  el: '#app',
6  store,
7  render: h => h(App)
8})
 1// 在组件中使用
 2<template>
 3  <div id="app">
 4    <p>{{count}}</p>
 5    <button @click="increment">+1</button>
 6    <button @click="decrement">-1</button>
 7  </div>
 8</template>
 9
10<script>
11export default {
12  name: 'App',
13  computed: {
14    count: function () {
15      return this.$store.state.count
16    }
17  },
18  method: {
19    increment: function () {
20      this.$store.commit('increment')
21    },
22    decrement: function () {
23      this.$store.commit('decrement')
24    }
25  }
26}
27</script>

为什么使用 mutations 来修改状态,而不是直接修改 store.state?

mutations.png

当我们使用 mutations 修改状态的时候,vue 提供的浏览器插件 devtools 可以跟踪到我们的修改,方便调试,官方推荐用这种方式来修改状态。

当我们使用 mutations 的时候,不能直接调用其中方法,需要通过 commit 来调用。

6.3 State 单一状态树

英文名称是 Single Source of Truth,也可以翻译成单一数据源。

如果你的状态信息是保存到多个 Store 对象中的,那么之后的管理和维护等等都会变得特别困难。
所以 Vuex 使用了单一状态树来管理应用层级的全部状态。
单一状态树能够让我们最直接的方式找到某个状态的片段,而且在之后的维护和调试过程中,也可以非常方便的管理和维护。

就是说一个项目仅使用一个 Store 对象。

对象展开运算符

 1import { mapState } from 'vuex'
 2computed: {
 3  // 使用对象展开运算符将此对象混入到外部对象中
 4  ...mapState({
 5    // 箭头函数可使代码更简练
 6    count: state => state.count,
 7
 8    // 传字符串参数 'count' 等同于 `state => state.count`
 9    countAlias: 'count',
10
11    // 为了能够使用 `this` 获取局部状态,必须使用常规函数
12    countPlusLocalState (state) {
13      return state.count + this.localCount
14    }
15  })
16}

6.4 getters

vuex 中的 getters 和 vue 中的计算属性类似。

 1const store = new Vuex.Store({
 2  state: {
 3    count: 1
 4  },
 5  getters: {
 6    countPow(state) {
 7      return state.count * state.count
 8    },
 9    // 可以使用第二个参数 getters
10    countPow2(state, getters) {
11      return getters.countPow + 1
12    }
13  }
14})

Getters 接收参数

getters 默认是不能传递参数的, 如果希望传递参数, 那么只能让 getters 本身返回另一个函数。

 1const store = new Vuex.Store({
 2  state: {
 3    count: 0
 4  },
 5  getters: {
 6    countPow(state) {
 7      return state.count * state.count
 8    },
 9    countPow3(state, getters) {
10      return num => {
11        return getters.countPow + num
12      }
13    }
14  }
15})
16// 调用
17<h2>{{$store.getters.countPow3(12)}}</h2>

mapGetter 辅助函数

 1import { mapGetters } from 'vuex'
 2
 3export default {
 4  // ...
 5  computed: {
 6  // 使用对象展开运算符将 getter 混入 computed 对象中
 7    ...mapGetters([
 8      'doneTodosCount',
 9      'anotherGetter',
10      // ...
11    ])
12  }
13}

6.5 Mutation 状态更新

Vuex 的 store 状态的更新唯一方式:Mutation

Mutation主要包括两部分:

  • 字符串的事件类型(type)
  • 一个回调函数(handler),该回调函数的第一个参数就是 state

Mutation 传递参数

在通过 mutation 更新数据的时候, 有可能我们希望携带一些额外的参数,参数被称为是 mutation 的载荷(Payload)

1// 传递一个参数
2incrementCount(state, count) {
3  state.counter += count
4}
5
6// 调用 
7this.$store.commit('incrementCount', 5)
 1// 传递多个参数的时候,只能传递一个对象,称之为 Payload
 2addStudent(state, stu) {
 3  state.students.push(stu)
 4}
 5
 6// 调用
 7addStudent() {
 8  const stu = {id: 114, name: 'alan', age: 35}
 9  this.$store.commit('addStudent', stu)
10}

Mutation 提交风格

上面的通过 commit 进行提交是一种普通的方式
Vue 还提供了另外一种风格, 它是一个包含 type 属性的对象

 1this.$store.commit({
 2  type: 'incrementCount',
 3  count: 5,
 4  count2: 10
 5})
 6
 7// 此时接收到的参数为一个对象
 8incrementCount(state, payload) {
 9  state.counter += payload.count
10}

Mutation 响应规则

当属性是在 store 中初始化好的,可以响应,当我们给 state 中的对象添加新属性时, 使用下面的方式才能响应:

方式一: 使用 Vue.set(obj, 'newProp', 123),删除的时候 Vue.delete(obj, 'prop')

方式二: 用新对象给旧对象重新赋值,state.info = {...state.info, newProp: 123}

Mutation 常量类型

为了避免我们在 commit 中写 mutation 中的方法名字写错,我们一般用一个常量来代替这个字符串,方式如下:

  1. 创建一个文件: mutation-types.js, 并且在其中定义我们的常量,使用这个常量来做为函数的名称

    1export const INCREMENT = 'increment'
    
  2. 在 mutation 中使用这个常量定义函数

    1import {INCREMENT} from "./mutations-types";
    2
    3export default {
    4  [INCREMENT](state) {
    5    state.counter++
    6  }
    7}
    
  3. 在 commit 中使用这个常量调用 mutation

    1import {
    2  INCREMENT
    3} from './store/mutations-types'
    4
    5this.$store.commit(INCREMENT)
    

mapMutations

 1import { mapMutations } from 'vuex'
 2
 3export default {
 4  // ...
 5  methods: {
 6    ...mapMutations([
 7      'increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`
 8
 9      // `mapMutations` 也支持载荷:
10      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`
11    ]),
12    ...mapMutations({
13      add: 'increment' // 将 `this.add()` 映射为 `this.$store.commit('increment')`
14    })
15  }
16}

6.6 actions

通常情况下, Vuex 要求我们 Mutation 中的方法必须是同步方法。
主要的原因是当我们使用 devtools 时, 可以 devtools 可以帮助我们捕捉 mutation 的快照。
但是如果是异步操作, 那么 devtools 将不能很好的追踪这个操作什么时候会被完成。

所以如果我们需要在 mutation 中使用异步操作的时候,比如发送网络请求,我们需要借助 actions。

 1const store = new Vuex.Store({
 2  state,
 3  mutations,
 4  actions: {
 5  	aUpdateInfo(context, payload) {
 6      console.log(payload);
 7      setTimeout(() => {
 8        context.commit('updateInfo');
 9      }, 1000)
10    }
11  }
12})
13
14// 调用 actions
15this.$store.dispatch('aUpdateInfo')

context 是什么?
context 是和 store 对象具有相同方法和属性的对象。
也就是说, 我们可以通过 context 去进行 commit 相关的操作, 也可以获取 context.state 等。

如果我们需要 actions 进行完之后通知调用者执行完了,则可以通过 Promise 来实现。

在组件中分发 Action

 1import { mapActions } from 'vuex'
 2
 3export default {
 4  // ...
 5  methods: {
 6    ...mapActions([
 7      'increment', // 将 `this.increment()` 映射为 `this.$store.dispatch('increment')`
 8
 9      // `mapActions` 也支持载荷:
10      'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`
11    ]),
12    ...mapActions({
13      add: 'increment' // 将 `this.add()` 映射为 `this.$store.dispatch('increment')`
14    })
15  }
16}

6.7 Module

Module 是模块的意思, 为什么在 Vuex 中我们要使用模块呢?

Vue 使用单一状态树,那么也意味着很多状态都会交给 Vuex 来管理。
当应用变得非常复杂时,store 对象就有可能变得相当臃肿。
为了解决这个问题, Vuex 允许我们将 store 分割成模块(Module), 而每个模块拥有自己的 state、mutations、actions、getters 等。

 1const moduleA = {
 2  state: {
 3    name: 'zhangsan'
 4  },
 5  mutations: {
 6    updateName(state, payload) {
 7      state.name = payload
 8    }
 9  },
10  getters: {
11    fullname(state) {
12      return state.name + '11111'
13    },
14    fullname2(state, getters) {
15      return getters.fullname + '2222'
16    },
17    // 可以获取根的 state
18    fullname3(state, getters, rootState) {
19      return getters.fullname2 + rootState.counter
20    }
21  },
22  actions: {
23    aUpdateName(context) {
24      console.log(context);
25      setTimeout(() => {
26        context.commit('updateName', 'wangwu')
27      }, 1000)
28    }
29  }
30}
31
32const store = new Vuex.Store({
33  state,
34  mutations,
35  actions,
36  getters,
37  modules: {
38    a: moduleA
39  }
40})
 1//  获取 moduleA 中的 state
 2<h2>{{$store.state.a.name}}</h2>
 3
 4// 获取 modeleA中的 getters,注意起名字的时候,不同 module 中的 getters 不要重复
 5<h2>{{$store.getters.fullname}}</h2>
 6
 7// 使用 moduleA 中的 mutations
 8this.$store.commit('updateName', 'lisi')
 9
10// 获取 moduleA 中的 actions
11this.$store.dispatch('aUpdateName')

可见,仅仅获取 state 的时候需要加模块名,其他的仅需注意定义时不同模块中的名字不要重复即可,module 中的 mutation、getters 接收的第一个 state 为局部状态对象,非全局的 state。

module 的 action 的 context 中包含 commit、dispatch、getters、state、rootGetters、rootState 等信息

6.8 Vuex 的目录结构

目录结构.png

vuex 建议我们将 getters、actions、mutation 抽离到单独的 js 文件中,将 module 相关的抽离到单独的文件夹,通过 export 导出,在 index.js 中导入,可以使代码结构更为清晰。

七、事件总线 bus

参考:Vue事件总线(EventBus)使用详细介绍

Vuex 出现后很少用到。用于处理组件间的数据通信问题。

局部使用

1// event-bus.js
2import Vue from 'vue'
3export const VueBus = new Vue()
4// 使用时导入

全局使用

1// main.js
2// 这种方式初始化的bus是一个全局的事件总线,下面以全局事件总线为例
3// 在 new Vue() 之前创建
4Vue.prototype.$bus = new Vue()

发送事件

1export default {
2  methods: {
3    sendMsg() {
4      this.$bus.$emit("aMsg", '来自A页面的消息');
5    }
6  }
7};

监听事件

1this.$bus.$on("aMsg", (msg) => {
2	// A发送来的消息
3	this.msg = msg;
4});

移除事件监听

1// 移除应用内所有对此某个事件的监听
2this.$bus.$off('aMsg')
3// 移除所有事件频道
4this.$bus.$off()

八、混入

官网介绍

8.1 混入的作用

提取多个界面的重复代码,使一份代码在多个界面复用

8.2 基本使用

 1// 新建文件 common/mixin.js,定义一个混入对象
 2export const myMixin = {
 3  created: function () {
 4    this.hello()
 5  },
 6  methods: {
 7    hello: function () {
 8      console.log('hello from mixin!')
 9    }
10  }
11}
12
13// 导入
14import {myMixin} from 'common/mixin'
15// 定义一个使用混入对象的组件
16var Component = Vue.extend({
17  mixins: [myMixin]
18})
19
20// 混入会与新组件自动合并
21var component = new Component() // => "hello from mixin!"

九、常用插件

Fastclick:解决移动端点击 300 ms 延迟问题

Vue-lazyload:图片懒加载插件

postcss-px-to-viewport:webpack px 单位转换插件

十、问题汇总

10.1 build 后资源找不到

在项目根目录下新建 vue.config.js

配置

1module.exports = {
2    publicPath: './'
3}

评论
发表评论
       
       
取消