一、Vue 概述
渐进式 JavaScript 框架
声明式渲染 -> 组件系统 -> 客户端路由 -> 集中式状态管理 -> 项目构建。
- 易用:熟悉 HTML、CSS、JavaScript 知识后,可快速上手 Vue
- 灵活:在一个库和一套完整框架之间自如伸缩
- 高效:20kb运行大小,超快虚拟 DOM
二、Vue 基本使用
<body>
<div id="app">
<div>{{msg}}</div>
</div>
<script src="js/vue.js"></script>
<script>
/*
Vue的基本使用步骤
1.需要提供标签用于填充数据
2.引入 vue.js 库文件
3.可以使用 vue 的语法做功能
4.把vue提供的数据填充到标签里面
*/
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello Vue!'
}
})
</script>
</body>
- el:元素的挂载位置(值可以是 CSS 选择器或者 DOM 元素)
- data:模型数据(值是一个对象)
- {{msg}}:插值表达式
- 将数据填充到 HTML 标签中
- 插值表达式支持基本的计算操作
Vue 代码运行原理
- 编译过程(Vue语法 -> 原生语法)
三、Vue 模板语法
3.1 模板语法概述
-
前端渲染
把数据填充到 HTML 标签中。
-
前端渲染方式
-
原生 js 拼接字符串
var d = data.weather; var info = document.getElementById('info'); info.innerHTML = ''; for (var i = 0; i < d.length; i++) { var date = d[i].date; var day = d[i].info.day; var night = d[i].info.night; var tag = ''; tag += '<span>日期:' + date + '</span><ul>'; tag += '<li>白天天气:' + day[1] + '</li>'; tag += '<li>白天温度:' + day[2] + '</li>'; tag += '<li>白天风向:' + day[3] + '</li>'; tag += '<li>白天风速:' + day[4] + '</li>'; tag += '</ul>'; var div = document.createElement('div'); div.innerHTML = tag; info.appendChild(div); }
- 缺点:不同开发人员的代码风格差别很大,随着业务的复杂,后期的维护变得越来越困难。
-
使用前端模板引擎
下面的代码是基于模板引擎 art-template 的代码,与拼接字符串相比,代码明显规范很多,它拥有自己的一套模板语法规则。
<script id="abc" type="text/html"> {{if isAdmin}} <h1>{{title}}</h1> <ul> {{each list as value i}} <li>索引 {{i + 1}} :{{value}}</li> {{/each}} </ul> {{/if}} </script>
- 优点:大家都遵循同样的规则编写代码,代码可读性明显提高,方便后期的维护
- 缺点:没有专门提供事件机制
-
使用 vue 特有的模板语法
- 差值表达式
- 指令
- 事件绑定
- 属性绑定
- 样式绑定
- 分支循环结构
-
3.2 指令
什么是指令?
指令的格式:以 v-
开始(比如 v-block)。
3.2.1 v-cloak
- 插值表达式存在的问题:“闪动”,使用 v-cloak 指令解决
- 解决原理:先隐藏,替换好值后再显示最终的值。
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>v-cloak</title>
<style>
[v-cloak] {
display: none;
}
</style>
</head>
<body>
<div id="app">
<div v-cloak>{{msg}}</div>
</div>
<script src="js/vue.js"></script>
<script>
/*
v-cloak指令的用法
1、提供样式
[v-cloak] {
display: none;
}
2、在插值表达式所在的标签添加 v-cloak 指令
原理:先通过样式隐藏内容,再在内存中进行值的替换,替换好之后再显示最终的结果
*/
var vm = new Vue({
el: '#app',
data: {
msg: 'hello Vue'
}
});
</script>
</body>
3.2.2 数据绑定指令
- v-text 填充纯文本
- 相比插值表达式更加简洁
- v-html 填充 HTML 片段
- 存在安全问题
- 本网站内部数据可以用使用,来自第三方的数据不可以用
- v-pre 填充原始信息
- 显示原始信息,跳过编译过程(分析编译过程)
<body>
<div id="app">
<!-- 原样输出 -->
<div v-text="msg1"></div>
<!-- 解析 html 标签 -->
<div v-html="msg2"></div>
<!-- 跳过编译输出 -->
<div v-pre>{{msg1}}</div>
</div>
<script src="js/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg1: '<h1>v-text<h1>',
msg2: '<h1>v-html<h1>'
}
});
</script>
</body>
3.2.3 数据响应式
- 如何理解响应式
- html5 中的响应式(屏幕尺寸的变化导致样式的变化)
- 数据的响应式(数据的变化导致页面内容的变化)
- 什么是数据绑定
- 数据绑定:将数据填充到标签中
- v-once 只编译一次
- 显示内容之后不再具有响应式功能
<body>
<div id="app">
<div v-once>{{msg}}</div>
</div>
<script src="js/vue.js"></script>
<script>
/*
v-once的应用场景:如果显示的信息后续不需要再修改,就可以使用 v-once,提高性能。
*/
var vm = new Vue({
el: '#app',
data: {
msg: 'v-once'
}
});
</script>
</body>
3.2.4 双向数据绑定
- v-model 指令
<body>
<div id="app">
<div>{{msg}}</div>
<input type="text" v-model="msg">
</div>
<script src="js/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'v-model'
}
});
</script>
</body>
MVVM 设计思想:分而治之
- M(model)数据
- V(view)视图,模板
- VM(view-model)
注意:v-model 不是任何标签都可以设置的。
因为 span 标签不能在页面上修改内容,是不应该有双向绑定的,这里报错也是可以理解。
v-model 的底层实现原理:
<input type="text" v-bind:value="msg" v-on:input="msg=$event.target.value" />
使用 v-bind 绑定属性,在数据修改后,内容自动修改;使用 v-on 事件绑定,在输入域内容发生改变,修改数据模型。
<body>
<div id="app">
<div>{{msg}}</div>
<!-- <input type="text" v-bind:value="msg" v-on:input="handle" /> -->
<input type="text" v-bind:value="msg" v-on:input="msg=$event.target.value" />
</div>
<script src="js/vue.js"></script>
<script>
/*
v-model 指令的本质
*/
var vm = new Vue({
el: '#app',
data: {
msg: 'hello'
}
// methods: {
// handle: function(event) {
// // 使用输入域中最新的数据覆盖原来的数据
// this.msg = event.target.value;
// }
// }
});
</script>
</body>
3.2.5 事件绑定
-
v-on 指令
<input type="button" v-on:click="num++" />
简写形式:
<input type="button" @click="num++" />
<body> <div id="app"> <div>{{num}}</div> <button v-on:click="num++">点击</button> </div> <script src="js/vue.js"></script> <script> /* 事件绑定 */ var vm = new Vue({ el: '#app', data: { num: 0 } }) </script> </body>
-
事件函数的调用方式
-
直接绑定函数名称
<button v-on:click="say">点击</button>
-
调用函数
<button v-on:click="say()">点击</button>
<script> /* 事件绑定 */ var vm = new Vue({ el: '#app', data: { num: 0 }, methods: { say: function() { // 这里的 this 是 Vue 的实例对象 this.num++; } } }) </script>
-
-
事件函数参数传递
-
如果事件直接绑定函数名称,那么默认会传递事件对象作为事件函数的第一个参数;
-
如果事件绑定函数调用,那么事件对象必须作为最后一个参数显示传递,并且事件对象的名称是
$event
。<button v-on:click="say('hi', $event)">Say hi</button>
-
-
事件修饰符
-
.stop 阻止冒泡
<a v-on:click.stop="handle">跳转</a>
-
.prevent 阻止默认行为
<a v-on:click.prevent="handle">跳转</a>
-
-
按键修饰符
-
.enter 按下回车键
<input type="text" v-on:keyup.enter="submit"/>
-
.delete 按下删除键
<input type="text" v-on:keyup.delete="delete"/>
-
-
自定义按键修饰符
- 全局 config.keyCodes 对象
<body> <div id="app"> <input type="text" v-on:keyup.aaa="handle" v-model="info" /> </div> <script src="js/vue.js"></script> <script> /* 事件绑定-自定义按键修饰符 规则:自定义按键修饰符名字是自定义的,但是对应的值必须是按键对应 event.keyCode 值 */ Vue.config.keyCodes.aaa = 65; var vm = new Vue({ el: '#app', data: { info: '' }, methods: { handle: function(event) { console.log(event.keyCode); } } }); </script> </body>
3.2.6 属性绑定
-
v-bind 指令
<a v-bind:href='url'>跳转</a>
-
缩写形式
<a :href='url'>跳转</a>
<body>
<div id="app">
<a v-bind:href="url" v-text='title'></a>
<a :href="url2">简写</a>
<button v-on:click="handle">切换</button>
</div>
<script src="js/vue.js"></script>
<script>
/* 属性绑定 */
var vm = new Vue({
el: '#app',
data: {
title: '百度',
url: 'https://www.baidu.com',
url2: 'https://www.baidu.com'
},
methods: {
handle: function(){
// 修改 url
this.title = '新浪';
this.url = 'https://www.sina.com';
}
}
});
</script>
</body>
3.2.7 样式绑定
-
class 样式处理
-
对象语法
<div v-bind:class="{ active: isActive }"></div>
-
数组语法
<div v-bind:class="{ activeClass, errorClass }"></div>
<body> <div id="app"> <div v-bind:class="{ active: isActive, error: isError }">文字</div> <!-- <div v-bind:class="[ activeClass, errorClass ]">文字</div> --> <button v-on:click="handle">切换</button> </div> <script src="js/vue.js"></script> <script> /* 样式绑定 */ var vm = new Vue({ el: '#app', data: { isActive: true, isError: true, activeClass: 'active', errorClass: 'error' }, methods: { handle: function() { this.isActive = !this.isActive; this.isError = !this.isError; this.activeClass = this.activeClass == '' ? 'active' : ''; this.errorClass = this.errorClass == '' ? 'error' : ''; } } }); </script> </body>
对象语法和数组语法可以结合使用。
<div v-bind:class="[activeClass, { error: isError }]">文字</div>
class 可以简化操作:
<div v-bind:class="arrClasses">文字</div> <div v-bind:class="objClasses">文字</div> <script> var vm = new Vue({ data: { arrClasses: ['active', 'error'], objClasses: { active: true, error: true } } }); </script>
标签默认的样式会保留:
<div class="base" v-bind:class='[ activeClass, errorClass ]'>文字</div>
-
-
style 样式处理
-
对象语法
<div v-bind:style="{ color: activeColor, fontSize: fontSize }"></div>
-
数组语法
<div v-bind:style="{ baseStyles, overridingStyles }"></div>
-
3.3 分支循环结构
-
分支结构
- v-if
- v-else
- v-else-if
- v-show
<body> <div id="app"> <div v-if="score >= 90">优秀</div> <div v-else-if="score < 90 && score >= 80">良好</div> <div v-else-if="score < 80 && score > 60">一般</div> <div v-else>比较差</div> <div v-show='flag'>show</div> <button v-on:click="handle">点击</button> </div> <script src="js/vue.js"></script> <script> /* 分支结构 v-show 的原理:控制元素样式是否显示 display: none */ var vm = new Vue({ el: '#app', data: { score: 88, flag: false }, methods: { handle: function() { this.flag = !this.flag; } } }); </script> </body>
v-if 与 v-show 的区别:
- v-if 控制元素是否渲染到页面
- v-show 控制元素是否显示(已经渲染到了页面上)
-
循环结构
-
v-for 遍历数组
<li v-for="item in list">{{item}}</li> <li v-for="(item, index) in list">{{item + '---' + index}}</li>
-
key 的作用:帮助 Vue 区分不同的元素,从而提高性能
<li :key="item.id" v-for="(item, index) in list">{{item + '---' + index}}</li>
-
v-for 遍历对象
<div v-for="(value, key, index) in object"></div>
-
v-if 和 v-for 结合使用
<div v-if="value==12" v-for="(value, key, index) in object"></div>
<body> <div id="app"> <div>水果列表</div> <ul> <li v-for="item in fruits">{{item}}</li> <li v-for="(item, index) in fruits">{{item + '---' + index}}</li> <li :key="item.id" v-for="(item, index) in myFruits"> <span>{{item.ename}}</span> <span>---</span> <span>{{item.cname}}</span> </li> </ul> </div> <script src="js/vue.js"></script> <script> /* 循环结构 */ var vm = new Vue({ el: '#app', data: { fruits: [ 'apple', 'orange', 'banana' ], myFruits: [{ id: 1, ename: 'apple', cname: '苹果' }, { id: 2, ename: 'orange', cname: '橙子' }, { id: 3, ename: 'banana', cname: '香蕉' }] } }); </script> </body>
-
四、Vue 常用特性
4.1 常用特性概览
- 表单操作
- 自定义指令
- 计算属性
- 过滤器
- 侦听器
- 生命周期
4.2 表单操作
-
基于 Vue 的表单操作
- input 单行文本
- textarea 多行文本
- select 下拉多选
- radio 单选框
- checkbox 多选框
<body> <div id="app"> <form> <div> <span>姓名:</span> <span> <input type="text" v-model="username"> </span> </div> <div> <span>性别:</span> <span> <input type="radio" id="man" value="1" v-model="gender"> <label for="man">男</label> <input type="radio" id="woman" value="2" v-model="gender"> <label for="woman">女</label> </span> </div> <div> <span>爱好:</span> <input type="checkbox" name="" id="ball" value="1" v-model="hobby"> <label for="ball">篮球</label> <input type="checkbox" name="" id="sing" value="2" v-model="hobby"> <label for="sing">唱歌</label> <input type="checkbox" name="" id="code" value="3" v-model="hobby"> <label for="code">写代码</label> </div> <div> <span>职业:</span> <select v-model="job"> <option value="0">请选择职业</option> <option value="1">教师</option> <option value="2">软件工程师</option> <option value="3">律师</option> </select> </div> <div> <span>个人简介:</span> <textarea v-model="desc"></textarea> </div> <div> <input type="submit" value="提交" v-on:click.prevent="handle"> </div> </form> </div> <script src="js/vue.js"></script> <script> /* 表单基本操作 */ var vm = new Vue({ el: '#app', data: { username: '', gender: 2, hobby: ['1', '2'], job: 0, desc: '' }, methods: { handle: function() { console.log(this.username); console.log(this.gender); console.log(this.hobby.toString()); console.log(this.job); console.log(this.desc); } } }); </script> </body>
-
表单域修饰符
- number:转换为数值
- trim:去掉开始和结尾的空格
- lazy:将 input 事件切换为 change 事件
<body> <div id="app"> <input type="text" v-model.number="age"> <input type="text" v-model.trim="info"> <!-- input 事件每次修改内容时触发,change 事件在失去焦点时触发 --> <input type="text" v-model.lazy="msg"> <div>{{msg}}</div> <button @click="handle">点击</button> </div> <script src="js/vue.js"></script> <script> var vm = new Vue({ el: "#app", data: { age: '', info: '', msg: '' }, methods: { handle: function() { console.log(this.age + 12); console.log('info.length = ' + this.info.length); } } }); </script> </body>
4.3 自定义指令
内置指令不满足需求时使用。
-
自定义指令的语法规则(获取元素焦点)
Vue.directive('focus', { // el 表示指令所绑定的元素 inserted: function(el) { // 获取元素的焦点 el.focus(); } })
用法:
<body> <div id="app"> <input type="text" v-focus> </div> <script src="js/vue.js"></script> <script> /* 自定义指令 */ Vue.directive('focus', { inserted: function(el) { // el 表示指令所绑定的元素 el.focus(); } }); var vm = new Vue({ el: '#app' }); </script> </body>
-
带从参数的自定义指令(改变元素背景色)
Vue.directive('color', { inserted: function(el, binding) { el.style.backgroundColor = binding.value.color; } });
用法:
<input type="text" v-color="{color: 'orange'}" />
-
局部指令
var vm = new Vue({ el: '#app', data: { }, directives: { focus: { // 指令的定义 inserted: function(el) { el.focus(); } }, color: { bind: function(el, binding) { el.style.backgroundColor = binding.value.color; } } } })
4.4 计算属性
表达式的计算逻辑可能会比较复杂,使用计算属性可以使模板内容更加简洁。
-
计算属性的用法
<body> <div id="app"> <div>{{msg}}</div> <div>{{reversedMessage}}</div> </div> <script src="js/vue.js"></script> <script> /* 计算属性 */ var vm = new Vue({ el: '#app', data: { msg: 'Nihao' }, computed: { reversedMessage: function() { return this.msg.split('').reverse().join(''); } } }); </script> </body>
-
计算属性和方法的区别
- 计算属性是基于它们的依赖进行缓存的
- 方法不进行缓存
<body> <div id="app"> <div>{{msg}}</div> <div>{{reverseMessage}}</div> <div>{{reverseMessage}}</div> <div>{{reverseMessage}}</div> <div>{{reverseString()}}</div> <div>{{reverseString()}}</div> <div>{{reverseString()}}</div> </div> <script src="js/vue.js"></script> <script> /* 计算属性 */ var vm = new Vue({ el: '#app', data: { msg: 'Nihao' }, methods: { reverseString: function() { console.log('reverseString run!'); return this.msg.split('').reverse().join(''); } }, computed: { reverseMessage: function() { console.log('reverseMessage run!'); return this.msg.split('').reverse().join(''); } } }); </script> </body>
方法执行了三次,而计算属性在 msg 未改变的情况下只执行了一次,优化性能。
4.5 侦听器
-
侦听器的应用场景
数据变化时执行异步或开销较大的操作。
-
侦听器的用法
<body> <div id="app"> <div> <span>名:</span> <span> <input type="text" v-model="firstName" /> </span> </div> <div> <span>姓:</span> <span> <input type="text" v-model="lastName" /> </span> </div> <div>{{fullName}}</div> </div> <script src="js/vue.js"></script> <script> /* 侦听器 */ var vm = new Vue({ el: '#app', data: { firstName: 'Lebron', lastName: 'James', fullName: 'Lebron James' }, watch: { // firstName 与上面的数据名相同 firstName: function(val) { // val 表示变化之后的值 this.fullName = val + ' ' + this.lastName; }, lastName: function(val) { this.fullName = this.firstName + ' ' + val; } } }); </script> </body>
4.6 过滤器
-
过滤器的作用
格式化数据,比如将字符串格式化为首字母大写,将日期格式化为指定的格式等。
-
自定义过滤器
Vue.filter('过滤器名称', function(value) { // 过滤器业务逻辑 });
使用:
<div>{{msg | upper}}</div> <div>{{msg | upper | lower}}</div> <div v-bind:id="id | formatId"></div>
-
局部过滤器
var vm = new Vue({ filters: { upper: function(value) { return value.chatAt(0).toUpperCase() + value.slice(1); } } });
<body> <div id="app"> <input type="text" v-model="msg"/> <div>{{msg | upper}}</div> <div>{{msg | lower}}</div> <!-- 级联操作,先进行首字母大写,再进行首字母小写 --> <div>{{msg | upper | lower}}</div> </div> <script src="js/vue.js"></script> <script> /* 过滤器 */ // 首字母大写 Vue.filter('upper', function(value) { return value.charAt(0).toUpperCase() + value.slice(1); }); // 首字母小写 Vue.filter('lower', function(value) { return value.charAt(0).toLowerCase() + value.slice(1); }) var vm = new Vue({ el: '#app', data: { msg: '' }, // 局部过滤器 // filters: { // upper: function(value) { // return value.charAt(0).toUpperCase + value.slice(1); // } // } }); </script> </body>
-
带参数的过滤器
Vue.filter('format', function(value, arg) { // arg 就是过滤器传递过来的参数 });
使用:
<div>{{date | format('yyyy-MM-dd')}}</div>
4.7 生命周期
- 主要阶段
- 挂载(初始化相关属性)
- beforeCreate:在 Vue 实例初始化之后,数据观测和事件配置之前被调用
- created:在 Vue 实例创建完成后被立即调用
- beforeMount:在挂载开始之前被调用
- mounted:el 被新创建的
vm.$el
替换,并挂载到 Vue 实例上之后调用该钩子
- 更新(元素或组件的变更操作)
- beforeUpdate:数据更新时调用,发生在虚拟 DOM 打补丁之前
- updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
- 销毁(销毁相关属性)
- beforeDestory:Vue 实例销毁之前调用
- destoryed:Vue 实例销毁后调用
- 挂载(初始化相关属性)
<body>
<div id="app">
<div>{{msg}}</div>
<button @click="update">更新</button>
<button @click="destroy">销毁</button>
</div>
<script src="js/vue.js"></script>
<script>
/*
vue 生命周期
*/
var vm = new Vue({
el: '#app',
data: {
msg: '生命周期'
},
methods: {
update: function() {
this.msg = '更新';
},
destroy: function() {
this.$destroy();
},
},
beforeCreate: function() {
console.log('beforeCreate');
},
created: function() {
console.log('created');
},
beforeMount: function() {
console.log('beforeMount');
},
mounted: function() {
console.log('mounted');
},
beforeUpdate: function() {
console.log('beforeUpdate');
},
updated: function() {
console.log('updated');
},
beforeDestroy: function() {
console.log('beforeDestroy');
},
destroyed: function() {
console.log('destroyed');
}
})
</script>
</body>
五、数组响应式变化
- Vue.set(vm.items, indexOfItem, newValue)
- vm.$set(vm.items, indexOfItem, newValue)
- 参数 1:表示要处理的数组名称
- 参数 2:表示要处理的数组的索引
- 参数 3:表示要处理的数组的值
var vm = new Vue({
el: '#app',
data: {
list: ['apple', 'orange', 'banana'],
info: {
name: 'Tom'
}
}
});
//vm.list[1] = 'lemon'; // 不是响应式的
//Vue.set(vm.list, 2, 'lemon');
vm.$set(vm.list, 2, 'lemon');
// 也可以处理对象
//vm.info.gender = 'man'; // 不是响应式的
vm.$set(vm.info, 'gender', 'man'); // 随后再修改 gender 值会同步更新页面
六、组件化开发
6.1 组件化开发思想
- 标准
- 分治
- 重用
- 组合
不同的功能封装到不同的组件中,然后组件通过组合的方式形式一个完整的应用。
组件化规范:Web Components
自己实现组件时的问题:
- 尽可能多的重用代码
- 自定义组件的方式不太容易(html、css 和 js)
- 多次使用组件可能导致冲突
Web Components 通过创建封装好功能的定制元素解决上述问题。
Vue 部分实现了此规范。
6.2 组件注册
-
全局组件注册语法
Vue.component(组件名称, { data: 组件数据, template: 组件模板内容 })
// 定义一个名为 button-counter 的新组件 Vue.component('button-counter', { data: function() { return { count: 0 } }, template: '<buttton v-on:click="count++">点击了{{count}}次。</button>' })
-
组件用法
<div id="app"> <button-counter></button-counter> </div>
完整示例:
<body> <div id="app"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter> </div> <script src="js/vue.js"></script> <script> /* 组件注册 */ Vue.component('button-counter', { data: function () { return { count: 0 } }, template: '<button v-on:click="handle">点击了{{count}}次</button>', methods: { handle: function () { this.count += 2; } }, }) var vm = new Vue({ el: '#app', data: { } }) </script> </body>
-
组件注册注意事项
-
data 必须是一个函数
-
组件模板内容必须是单个根元素
template: '<button v-on:click="handle">点击了{{count}}次</button><button>测试</button>',
只有两个同级的 button 按钮,没有统一的父元素,将会报错。
-
组件模板内容可以是模板字符串
- 模板字符串需要浏览器提供支持(ES6语法)
- 在模板内容很长时,使用模板字符串提高可读性
template: ` <div> <button v-on:click="count++">点击了{{count}}次</button> <button>测试</button> </div> `,
-
组件命名方式
- 短横线方式
Vue.component('my-component', {})
- 驼峰方式
Vue.component('MyComponent', {})
- 短横线方式
-
-
局部组件注册
var ComponentA = { data = function() { return { msg: '' } }, template: '<div>{{msg}}</div>' } var ComponentB = { ... } var ComponentC = { ... } new Vue({ el: '#app', components: { 'component-a': ComponentA, 'component-b': ComponentB, 'component-c': ComponentC, } })
局部组件只能在注册它的父组件中使用。
6.3 组件间数据交互
6.3.1 父组件向子组件传值
-
组件内部通过 props 接收传递过来的值
Vue.component('menu-item', { props: ['title'], template: '<div>{{ title }}</div>' })
-
父组件通过属性将值传递给子组件
<menu-item title="来自父组件的数据"></menu-item> <menu-item :title="title"></menu-item> <!--动态绑定属性值-->
-
props 属性名规则
- 在 props 中使用驼峰形式,模板中需要使用短横线的形式
- 字符串形式的模板中没有这个限制
<!-- 在 html 中短横线形式的 --> <menu-item menu-title="nihao"></menu-item> <script> Vue.component('menu-item', { // 在 JavaScript 中是驼峰形式 props: ['menuTitle'], template: '<div>{{ menuTitle }}</div>' }) </script>
-
props 属性值类型
- 字符串 String
- 数值 Number
- 布尔值 Boolean
- 数组 Array
- 对象 Object
<div id="app"> <!-- pnum 是字符串类型,pbool 是字符串类型 --> <menu-item :pstr='pstr' pnum='12' pbool='true'></menu-item> <!-- pnum 是数值类型,pbool 是布尔类型 --> <menu-item :pstr='pstr' :pnum='12' :pbool='true' :parr='parr' :pobj='pobj'></menu-item> </div> <script> Vue.component('menu-item', { props: ['pstr', 'pnum', 'pbool', 'parr', 'pobj'], template: ` <div> <div>{{ pstr }}</div> <div>{{ typeof pnum }}</div> <div>{{ typeof pbool }}</div> <ul> <li :key='index' v-for='(item, index) in parr'>{{ item }}</li> </ul> <div> <span>{{ pobj.msg }}</span> <div> </div> ` }); var vm = new Vue({ el: '#app', data: { pstr: 'hello', parr: ['apple', 'orange'], pobj: { msg: '' } } }) </script>
6.3.2 子组件向父组件传值
-
props 传递数据原则:单向数据流。只允许父组件向子组件传递数据,而不允许子组件操作 props 中的数据。
<body> <div id="app"> <menu-item :parr="parr"></menu-item> </div> <script> /* 能够修改 parr 的值,从而影响父组件的内容,但是不推荐这样做 */ Vue.component('menu-item', { props: ['parr'], template: ` <div> <ul> <li :key='index' v-for='(item, index) in parr'>{{ item }}</li> </ul> <button @click='parr.push("lemon")'>点击</button> </div> ` }); var vm = new Vue({ el: '#app', data: { pmsg: '父组件中的内容', parr: ['apple', 'orange'] } }) </script> </body>
-
子组件通过自定义事件向父组件传递信息
<button v-on:click='$emit("enlarge-text", 0.1)'>放大字体</button>
-
父组件监听子组件的事件
<menu-item v-on:enlarge-text="fontSize += $event"></menu-item> <menu-item @enlarge-text="fontSize += $event"></menu-item>
6.3.3 非父子组件间传值
-
单独的事件中心管理组件间的通信
var eventHub = new Vue();
-
监听事件与销毁事件
eventHub.$on('add-todo', addTodo); eventHub.$off('add-todo');
-
触发事件
eventHub.$emit('add-todo', id);
示例代码:
<body>
<div id="app">
<div>父组件</div>
<div>
<button @click='handle'>销毁事件</button>
</div>
<test-tom></test-tom>
<test-jerry></test-jerry>
</div>
<script src='js/vue.js'></script>
<script>
/*
兄弟组件之间传值
*/
// 提供事件中心
var hub = new Vue();
Vue.component('test-tom', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>TOM:{{num}}</div>
<div>
<button @click='handle'>点击</button>
</div>
</div>
`,
methods: {
handle: function() {
// 触发兄弟组件的事件
hub.$emit('jerry-event', 2);
}
},
mounted: function() {
// 监听事件
hub.$on('tom-event', (val) => {
this.num += val;
});
}
});
Vue.component('test-jerry', {
data: function () {
return {
num: 0
}
},
template: `
<div>
<div>JERRY:{{num}}</div>
<div>
<button @click='handle'>点击</button>
</div>
</div>
`,
methods: {
handle: function() {
// 触发兄弟组件的事件
hub.$emit('tom-event', 1);
}
},
mounted: function() {
// 监听事件
hub.$on('jerry-event', (val) => {
this.num += val;
});
}
});
var vm = new Vue({
el: '#app',
data: {
},
methods: {
handle: function() {
hub.$off('tom-event');
hub.$off('jerry-event');
}
}
})
</script>
</body>
6.4 组件插槽
6.4.1 组件插槽的作用
-
父组件向子组件传递内容
6.4.2 组件插槽基本用法
-
插槽位置
Vue.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> ` })
-
插槽内容
<alert-box>Something bad happened.</alert-box>
示例代码:
<body> <div id="app"> <alert-box>有bug发生</alert-box> <alert-box></alert-box> </div> <script src="js/vue.js"></script> <script> /* 组件插槽 */ Vue.component('alert-box', { template: ` <div> <strong>ERROR:</strong> <slot>默认内容</slot> </div> ` }); var vm = new Vue({ el: '#app', data: { } }) </script> </body>
-
具名插槽
-
插槽定义
Vue.component('base-layout',{ template: ` <div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div> ` });
-
插槽内容
<base-layout> <template slot="header"> <p>标题内容1</p> <p>标题内容2</p> </template> <p>主要内容1</p> <p>主要内容2</p> <template slot="footer"> <p>底部内容1</p> <p>底部内容2</p> </template> </base-layout>
-
-
作用域插槽
- 应用场景:父组件对子组件的内容进行加工处理。
- 插槽定义
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>作用域插槽</title> <style> .current { color: orange; } </style> </head> <body> <div id="app"> <fruit-list :list="list"> <template slot-scope="slotProps"> <strong v-if="slotProps.info.id==2" class="current">{{slotProps.info.name}}</strong> <span v-else>{{slotProps.info.name}}</span> </template> </fruit-list> </div> <script src="js/vue.js"></script> <script> /* 作用域插槽 */ Vue.component('fruit-list', { props: ['list'], template: ` <ul> <li :key="item.id" v-for="(item, index) in list"> <slot :info='item'>{{item.name}}</slot> </li> </ul> ` }) var vm = new Vue({ el: '#app', data: { list: [{ id: 1, name: 'apple' }, { id: 2, name: 'orange' }, { id: 3, name: 'banana' }] } }) </script> </body>
七、前后端交互
7.1 前后端交互模式
- 原生 ajax
- 基于 jQuery 的 ajax
- fetch
- axios
7.2 Promise 用法
- 异步调用
- 定时任务
- Ajax
- 事件函数
- 多次异步调用的依赖分析
- 多次异步调用的结果顺序不确定
- 异步调用结果如果尺寸在依赖需要嵌套
Promise 是异步编程的一种解决方案,从语法上来讲,Promise 是一个对象,从它可以获取异步操作的消息。
使用 promise 主要有以下好处:
- 可以避免多层异步调用嵌套问题(回调地狱)
- Promise 对象提供了简洁的 API,使得控制异步操作更加容易。
使用步骤:
- 实例化 Promise 对象,构造函数中传递函数,该函数中用于处理异步任务
- resolve 和 reject 两个参数用于处理成功和失败两种情况,并通过 p.then 获取处理结果。
var p = new Promise(function(resolve, reject) {
// 成功时调用 resolve()
// 失败时调用 reject()
});
p.then(function(ret) {
// 从 resolve 得到正常数据
}, function(ret) {
// 从 reject 得到错误信息
});
基于 Promise 处理 Ajax 请求:
-
处理原生 Ajax
function queryData(url) { return new Promise(function(resolve, reject) { var xhr = new XMLHttpRequest(); xhr.onreadystatechange = function() { if (xhr.readyState != 4) return; if (xhr.status == 200) { resolve(xhr.responseText); } else { reject('出错了'); } } xhr.open('get', url); xhr.send(null); }); return p; } queryData('http://localhost:3000/data') .then(function(data) { console.log(data); return queryData('http://localhost:3000/data1'); }, function(info) { console.log(info); }) .then(function(data) { console.log(data); return queryData('http://localhost:3000/data2') }) .then(function(data) { console.log(data); });
then 参数中的函数返回值:
- 返回 Promise 实例对象
- 返回的该实例对象会调用下一个 then
- 返回普通值
- 返回的普通值会直接传递给下一个 then,通过 then 参数中函数的参数接收该值
- 原因:会产生一个默认的 Promise 对象来调用下一个 then
Promise 常用的 API:
-
实例方法
- p.then() 得到异步任务的正确结果
- p.catch() 获得异步的结果
- p.finally() 成功与否都会执行
queryData().then(function(data) { console.log(data); }) .catch(function(data) { console.log(data); }) .finally(function() { console.log('finished'); })
-
对象方法
- Promise.all() 并发处理多个异步任务,所有任务都执行完成才能得到结果
- Promise.race() 并发处理多个异步任务,只要有一个任务完成就能得到结果
Promise.all([p1, p2, p3]).then((result) => { console.log(result); }) Promise.race([p1, p2, p3]).then((result) => { console.log(result); })
7.3 接口调用 -fetch 用法
-
基本特性
- 更加简单的数据获取方式,功能更强大、更灵活,可以看作是 xhr 的升级版
- 基于 Promise 实现
-
语法结构
fetch('/abc').then(data => { // text() 方法属于 fetchAPI 的一部分,它返回一个 Promise 实例对象,用于获取后台返回的数据 return data.text(); }).then(ret => { // 这里才是最终的数据 console.log(ret); }).catch(function(data) { });
-
fetch 请求参数
-
常用配置选项
- method(String):HTTP 请求方法,默认为 GET
- body(String):HTTP 的请求参数
- headers(Object):HTTP 的请求头,默认为 {}
fetch('/abc', { method: 'get' }).then(data => { return data.text(); }).then(ret => { // 这里才是最终的数据 console.log(ret); });
-
GET 请求方式的参数传递
fetch('/abc?id=123').then(data => { return data.text(); }).then(ret => { // 这里才是最终的数据 console.log(ret); });
Restful 风格:
fetch('/abc/123', { method: 'get' }).then(data => { return data.text(); }).then(ret => { // 这里才是最终的数据 console.log(ret); });
-
DELETE 请求方式的参数传递与 GET 相似。
-
POST 请求方式的参数传递
fetch('/books', { method: 'post', body: 'username=Tom&age=13', headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }).then(data => { return data.text(); }).then(ret => { console.log(ret); })
fetch('/books', { method: 'post', body: JSON.stringify({ username: 'Tom', age: 13 }), headers: { 'Content-Type': 'application/json' } }).then(data => { return data.text(); }).then(ret => { console.log(ret); })
-
PUT 请求方式的参数传递与 POST 相似。
-
-
fetch 响应结果
响应数据格式
- text():将返回体处理成字符串类型
- json():返回结果和 JSON.parse(responseText) 一样
fetch('/abc').then(data => { return data.json(); }).then(ret => { console.log(ret); });
7.4 接口调用 -axios 用法
-
axios 的基本特性
axios 是一个基于 Promise 用于浏览器和 node.js 的 HTTP 客户端。
它具有以下特征:
- 支持浏览器和 node.js
- 支持 promise
- 能拦截请求和响应
- 自动转换 JSON 数据
-
axios 的基本用法
axios.get('/data').then(ret => { // data 属性名称是固定的,用于获取后台响应的数据 console.log(ret.data); })
-
axios 的常用 API
- get:查询数据
- post:添加数据
- put:修改数据
- delete:删除数据
-
axios 的参数传递
-
通过 url 传递参数
axios.get('http://localhost:3000/axios?id=123').then(function(ret) { console.log(ret.data); }) axios.get('http://localhost:3000/axios/123').then(function(ret) { console.log(ret.data); })
-
通过 params 选项传递参数
// GET axios.get('http://localhost:3000/axios', { params: { id: 123 } }).then(function(ret) { console.log(ret.data); }) // POST axios.post('http://localhost:3000/axios', { username: 'Tom', password: 123456 }).then(function(ret) { console.log(ret.data); }) // PUT axios.put('http://localhost:3000/axios/123', { username: 'Tom', password: 123456 }).then(function(ret) { console.log(ret.data); })
-
通过 URLSearchParams 传递参数(application/x-www-form-urlencoded)
const params = new URLSearchParams(); params.append('username', 'Tom'); params.append('password', '123456'); axios.post('http://localhost:3000/axios', params).then(ret => { console.log(ret.data); })
-
-
axios 的响应结果
响应结果的主要属性:
- data:实际响应回来的数据
- headers:响应头信息
- status:响应状态码
- statusText:响应状态信息
-
全局配置
- axios.defaults.timeout = 3000; 超时时间
- axios.defaults.baseURL = 'http://localhost:3000/app'; 默认地址
- axios.defaults.headers['mytoken'] = 'aqweeqweqewqew23a12w2'; 设置请求头
-
axios 拦截器
-
请求拦截器
在请求发出之前设置一些信息。
// 添加一个请求拦截器 axios.interceptors.request.use(function(config) { // 在请求发出之前进行一些信息设置 config.headers.mytoken = 'nihao'; return config; }, function(error) { console.log(error); })
-
响应拦截器
在获取数据之前对数据做一些加工处理。
// 添加一个响应拦截器 axios.interceptors.response.use(function(res) { console.log(res); // data 是实际的数据 var data = res.data; return data; }, function(error) { console.log(error); })
-
7.5 接口调用 async/await 用法
- async/await 是 ES7 引入的新语法,可以更加方便的进行异步操作
- async 关键字用于函数上(async 函数的返回值是 Promise 实例对象)
- await 关键字用于 async 函数当中(await 可以得到异步的结果)
async function queryData(id) {
const rsp = await axios.get('/data');
return rsp;
}
queryData.then(rsp => {
console.log(rsp);
})
八、Vue 前端路由
8.1 路由
路由是一个比较广义和抽象的概念,路由的本质就是对应关系。
在开发中,路由分为:
- 后端路由
- 前端路由
-
后端路由
- 概念:根据不同的用户 URL 请求,返回不同的内容
- 本质:URL 请求地址与服务器资源之间的对应关系
-
SPA(Single Page Application)
- 后端渲染(存在性能问题)
- Ajax 前端渲染(前端渲染提高性能,但是不支持浏览器的前进后退操作)
- SPA(Single Page Application)单页面应用程序:整个网站只有一个页面,内容的变化通过 Ajax 局部更新实现,同时支持浏览器地址栏的前进和后退操作
- SPA 实现原理之一:基于 URL 地址的 hash(hash 的变化会导致浏览器记录访问历史的变化,但 hash 的变化不会触发新的 URL 请求)
- 在实现 SPA 过程中,最核心的技术点是前端路由
-
前端路由
- 概念:根据不同的用户事件,显示不同的页面内容。
- 本质:用户事件与事件处理函数之间的对应关系
-
实现简易前端路由
- 基于 URL 中的 hash 实现(点击菜单的时候改变 URL 的 hash,根据 hash 的变化控制组件的切换)
// 监听 window 的 onhashchange 事件,根据获取到的最新 hash 值,切换要显示的组件的名称 window.onhashchange = function() { // 通过 location.hash 获取到最新的 hash 值 }
<body> <div id="app"> <a href="#/a">页面A</a> <a href="#/b">页面B</a> <a href="#/c">页面C</a> <a href="#/d">页面D</a> <component :is="componentName"></component> </div> <script src="js/vue.js"></script> <script> const PageA = { template: `<h1>页面A</h1>` } const PageB = { template: `<h1>页面B</h1>` } const PageC = { template: `<h1>页面C</h1>` } const PageD = { template: `<h1>页面D</h1>` } /* 简易前端路由 */ var vm = new Vue({ el: '#app', data: { componentName: 'pageA' }, // 注册私有组件 components: { pageA: PageA, pageB: PageB, pageC: PageC, pageD: PageD } }) window.onhashchange = function() { console.log(location.hash.slice(1)); switch(location.hash.slice(1)) { case '/a': vm.componentName = 'pageA'; break; case '/b': vm.componentName = 'pageB'; break; case '/c': vm.componentName = 'pageC'; break; case '/d': vm.componentName = 'pageD'; break; } } </script> </body>
8.2 Vue Router
Vue Router(官网:https://router.vuejs.org/zh/) 是 Vue.js 官方的路由管理器。
它和 Vue.js 的核心深度集成,可以非常方便的用于 SPA 应用程序的开发。
Vue Router 包含的功能有:
- 支持 HTML 历史模式或 hash 模式
- 支持嵌套路由
- 支持路由参数
- 支持编程式路由
- 支持命名路由
8.2.1 基本使用步骤
-
引入相关的库文件
<!-- 导入 vue 文件,为全局 window 对象挂载 Vue 构造函数 --> <script src="js/vue.js"></script> <!-- 导入 vue-router 文件,为全局 window 对象挂载 VueRouter 构造函数 --> <script src="js/vue-router_3.0.2.js"></script>
-
添加路由链接
<!-- router-link 是 vue 中提供的标签,默认会被渲染为 a 标签 --> <!-- to 属性默认会被渲染为 href 属性 --> <!-- to 属性的值默认会被渲染为 # 开头的 hash 地址 --> <router-link to="/user">User</router-link> <router-link to="/register">Register</router-link>
-
添加路由填充位
<!-- 路由填充位(也叫路由占位符) --> <!-- 将来通过路由规则匹配到的组件,将会被渲染到 router-view 所在的位置 --> <router-view></router-view>
-
定义路由组件
const User = { template: '<div>User组件</div>' } const Register = { template: '<div>Register组件</div>' }
-
配置路由规则并创建路由实例
// 创建路由实例对象 var router = new VueRouter({ // routes 是路由规则数组 routes: [ // 每个路由规则都是一个配置对象,其中至少包含 path 和 component 两个属性 // path:表示当前路由规则匹配的 hash 地址 // component:表示当前路由规则对应要展示的组件 { path: '/User', component: User }, { path: '/Register', component: Register } ] })
-
把路由挂载到 Vue 根实例中
new Vue({ el: '#app', // 为了能够让路由规则生效,必须把路由对象挂载到 vue 实例对象上 // router: router // 在 ES6中变量名和变量值名称相同可以简写 router })
8.2.2 路由重定向
路由重定向:用户在访问地址 A 时,强制用户跳转到地址 C,从而展示特定的组件页面;通过路由规则的 redirect
属性,指定一个新的路由地址,可以很方便地设置路由的重定向。
var router = new VueRouter({
routes: [
// path 表示需要被重定向的原地址;redirect 表示将要被重定向到的新地址
{ path: '/', redirect:'login' },
{ path: '/login', component: Login },
{ path: '/user', component: User },
{ path: '/register', component: Register }
]
})
8.2.3 嵌套路由用法
-
嵌套路由功能分析
- 点击父级路由链接显示模板内容
- 模板内容中又有子级路由链接
- 点击子级路由链接显示自己模板内容
-
父路由组件模板
- 父级路由链接
- 父组件路由填充位
-
子级路由模板
- 子级路由链接
- 子级路由填充位
const Register = { template: ` <div> <div>Register 组件</div> <hr/> <!-- 子路由链接 --> <router-link to='/register/tab1'>Tab1</router-link> <router-link to='/register/tab2'>Tab2</router-link> <!-- 子路由占位符 --> <router-view></router-view> </div> ` }
-
嵌套路由配置
- 父级路由通过 children 属性配置子级路由
const router = new VueRouter({ routes: [ { path: '/usr', component: User }, { path: '/register', component: Register, // 通过 children 属性,为 /register 添加子路由规则 children: [ { path: '/register/tab1', component: Tab1 }, { path: '/register/tab2', component: Tab2 } ] } ] })
8.2.4 动态路由匹配
应用场景:通过动态路由参数的模式进行路由匹配
const User = {
// 路由组件中通过 $route.params 获取路由参数
template: '<div>User {{ $route.params.id }}</div>'
}
var router = new VueRouter({
routes: [
// 动态路径参数以冒号(:)开头
{ path: '/usr/:id', component: User }
]
})
8.2.5 路由组件传递参数
$route
与对应路由形成高度耦合,不够灵活,所以可以使用 props
将组件和路由解耦。
-
props 的值为布尔类型
const router = new VueRouter({ routes: [ // 如果 props 被设置为 true,route.params 将会被设置为组件属性 { path: '/user/:id', component: User, props: true } ] }) const User = { // 使用 props 接收路由参数 props: ['id'], // 使用路由参数 template: '<div>用户Id:{{ id }}</div>' }
-
props 的值为对象类型
const router = new VueRouter({ routes: [ // 如果 props 是一个对象,它会被按原样设置为组件属性 { path: '/user/:id', component: User, props: { username: 'Tom', password: '123456' } } ] }) const User = { // 使用 props 接收路由参数 props: ['id', 'username', 'password'], // 使用路由参数 // 这样是获取不到 id 值的,因为传递过来的 props 中没有 id template: '<div>用户信息:{{ id + '---' + username + '---' + password }}</div>' }
-
props 的值为函数类型
const router = new VueRouter({ routes: [ // 如果 props 是一个函数,则这个函数接收 route 对象为自己的形参 { path: '/user/:id', component: User, props: route => ({ username: 'Tom', age: 20, id: route.params.id }) } ] }) const User = { props: ['username', 'age', 'id'], template: '<div>用户信息:{{ id + '---' + username + '---' + password }}</div>' }
8.2.6 命名路由的配置规则
为了更加方便的表示路由的路径,可以给路由规则起一个别名,即为“命令路由”。
const router = new VueRouter({
routes: [
{
path: '/user/:id',
// 命名路由
name: 'user',
component: User
}
]
})
<!-- name 表示要跳转到的路由,params 表示路由要传递的参数 -->
<router-link :to="{ name: 'user', params: { id: 123 }}">User</router-link>
router.push({ name: 'user', params: { id: 123 }})
8.2.7 路由导航守卫控制访问权限
如果用户没有登录,但是直接通过 URL 访问特定页面,需要重新导航到登录页面。
// 为路由对象添加 beforeEach 导航守卫
router.beforeEach((to, from, next) => {
// 如果用户访问的登录页,直接放行
if (to.path === '/login') return next()
// 从 sessionStorage 中获取到保存的 token 值
const tokenStr = window.sessionStorage.getItem('token')
// 没有 token,强制跳转到登录页
if (!tokenStr) return next('/login')
next()
})
8.3 编程式导航
8.3.1 页面导航的两种方式
- 声明式导航:通过点击链接实现导航的方式,称为声明式导航
- 编程式导航:通过调用 JavaScript 形式的 API 实现导航的方式,称为编程式导航。例如:普通网页的 location.href
常用的编程式导航 API 如下:
- this.$router.push('hash地址')
- this.$router.go(n)
const User = {
template: `
<div>
<button @click="goRegister">跳转到注册页面</button>
<button @click="goBack">后退</button>
</div>
`,
methods: {
goRegister: function() {
// 用编程的方式控制路由跳转
this.$router.push('/register');
},
goBack: function() {
// 后退
this.$router.go(-1);
}
}
}
8.3.2 编程式导航参数规则
router.push() 方法的参数规则
// 字符串(路径名称)
router.push('/home')
// 对象
router.push({ path: '/home' })
// 命名的路由(传递参数)
router.push({ name: '/user', params: { id: 123 }})
// 带查询参数,变成 /register?username=Tom
router.push({ path: '/register', query: { username: 'Tom' }})
九、前端工程化
9.1 模块化相关规范
9.1.1 模块化概述
传统开发模式的主要问题:
- 命名冲突
- 文件依赖
通过模块化解决上述问题:
- 模块化就是把单独的一个功能封装到一个模块(文件)中,模块之间相互隔离,但是可以通过特定的接口公开内部成员,也可以依赖别的模块。
- 模块化开发的好处:方便代码的重用,从而提高开发效率,并且方便后期的维护
9.1.2 浏览器端模块化规范
-
AMD
Require.js(http://www.requirejs.cn/)
-
CMD
9.1.3 服务器端模块化规范
- CommonJS
- 模块分为单文件模块和包
- 模块成员导出:module.exports 和 exports
- 模块成员导入:require('模块标识符')
9.1.4 ES6 模块化
在 ES6 模块化规范之前,JavaScript 社区开始尝试并提出了 AMD、CDM、CommonJS 等模块化规范。
但是,这些社区提出的模块化标准,还是存在一定的差异性与局限性、并不是浏览器与服务器通用的模块化标准,例如:
- AMD 和 CMD 适用于浏览器端的 JavaScript 模块化
- CommonJS 适用于服务器端的 JavaScript 模块化
因此,ES6 语法规范中,在语言从层面上定义了 ES6 模块化规范,是浏览器与服务器端通用的模块化开发规范。
ES6 模块化规范中定义:
- 每个 js 文件都是一个独立的模块
- 导入模块成员使用
import
关键字 - 暴露模块成员使用
export
关键字
-
Node.js 中通过 babel 体验 ES6 模块化
- npm install --save-dev @babel/core @babel/cli @babel/preset-env @babel/node
- npm install --save @babel/polyfill
- 项目根目录创建 babel.config.js
- babel.config.js 文件内容如下:
const presets = [ ["@babel/env", { targets: { edge: "17", firefox: "60", chrome: "67", safari: "11.1" } }] ]; module.exports = { presets };
- 通过 npx babel-node index.js 执行代码
9.1.5 ES6 模块化的基本语法
-
默认导出与默认导入
-
默认导出语法:export default 默认导出的成员
// 定义私有成员 let a = 10; let c = 20; let d = 30; function show() {} // 将本模块的私有成员暴露出去,供其它模块使用 export default { a, c, show }
-
默认导入语法:import 接收名称 from '模块标识符'
// 导入模块成员 import m1 from './m1.js' console.log(m1); // 打印输出的结果:{ a: 10, c: 20, show: [Function: show] }
注意:每个模块中,只允许使用唯一的一次 export default,否则会报错~
-
-
按需导出与按需导入
-
按需导出语法:export let s1 = 10
// 向外按需导出变量 s1、s2 export let s1 = 'aaa'; export let s2 = 'ccc'; // 向外按需导出方法 say export function say = function() {}
-
按需导入语法:import { s1 } from '模块标识符'
// 导入模块成员 import { s1, s2 as ss2, say } from './m1.js' console.log(s1); console.log(s2); console.log(say); // [Function: show]
注意:每个模块中,可以使用多次按需导出。
-
-
直接导入并执行模块代码
只想单纯执行某个模块中的代码,并不需要得到模块中向外暴露的成员,此时,可以直接导入并执行模块代码。
// m2.js // 在当前模块中执行一个 for 循环操作 for(let i = 0; i < 10; i++) { console.log(i); }
// 直接导入并执行模块代码 import './m2.js'
9.2 webpack
9.2.1 web 开发面临的困境
- 文件依赖关系错综复杂
- 静态资源请求效率低
- 模块化支持不友好
- 浏览器对高级 JavaScript 特性兼容程度较低
- ...
9.2.2 webpack 概述
webpack 是一个流行的前端项目构建工具(打包工具),可以解决当前 web 开发中所面临的困境。
webpack 提供了友好的模块化支持,以及代码压缩混淆、处理 js 兼容问题、性能优化等强大的功能,从而让程序员把工作的重心放到具体的功能实现上,提高了开发效率和项目的可维护性。
目前绝大多数企业中的前端项目,都是基于 webpack 进行打包构建的。
9.2.3 webpack 的基本使用
-
项目初始化的一些操作
- 新建项目空白目录,运行
npm init -y
命令,初始化包管理配置文件 package.json - 新建
src
源代码目录 - 新建
src -> index.html
首页 - 初始化首页基本的结构
- 运行
npm install jquery -S
命令,安装 jQuery。
- 新建项目空白目录,运行
-
在项目中安装和配置 webpack
-
运行
npm install webpack webpack-cli -D
命令,安装 webpack 相关的包 -
在项目根目录中,创建名为
webpack.config.js
的 webpack 配置文件 -
在 webpack 的配置文件中,初始化如下基本配 置:
module.exports = { mode: 'development' // mode 指定构建模式 }
-
在 package.json 配置文件中的
scripts
节点中,新增 dev 脚本如下:"scripts": { "dev": "webpack" // script 节点下的脚本,可以通过 npm run 运行 }
-
在终端运行
npm run dev
命令,启动 webpack 进行项目打包。
-
-
配置打包的入口和出口
webpack 的 4.x 版本中默认约定:
- 打包的入口文件为 src -> index.js
- 打包的输出文件为 dis -> main.js
如果要修改打包的入口和出口,可以在 webpack.config.js 中新增如下配置信息:
// 导入 node.js 中专门操作路径的模块 const path = require('path'); module.exports = { // 打包入口文件的路径 entry: path.join(__dirname, './src/index.js'); output: { // 输出文件的存放路径 path: path.join(__dirname, './dist'), // 输出文件的名称 filename: 'bundle.js' } }
-
配置 webpack 的自动打包功能
-
运行
npm install webpack-dev-server -D
命令,安装支持项目自动打包的工具 -
修改 package.json -> scripts 中的 dev 命令如下:
"scripts": { "dev": "webpack-dev-server" // script 节点下的脚本,可以通过 npm run 运行 }
-
将 src -> index.html 中,script 脚本的引用路径,修改为 "/bundle.js"
-
运行
npm run dev
命令,重新进行打包 -
在浏览器中访问 http://localhost:8080 地址,查看自动打包效果
注意:webpack、webpack-cli、webpack-server 的版本兼容问题。下面是可用的版本信息:
"devDependencies": { "webpack": "^4.43.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.10.3" }
注意:webpack-dev-server 会启动一个实时打包的 http 服务器,引入的 /bundle.js 并不在工程目录中,而是在内存中。
-
-
配置 html-webpack-plugin 生成预览页面
-
运行
npm install html-webpack-plugin -D
命令,安装生成预览页面的插件 -
修改 webpack.config.js 文件头部区域,添加如下配置信息:
// 导入生成预览页面的插件,得到一个构造函数 const HtmlWebpackPlugin = require('html-webpack-plugin'); // 创建插件的实例对象 const htmlPlugin = new HtmlWebpackPlugin({ // 指定要用到的模板文件 template: './src/index.html', // 指定生成的文件名称,该文件存在于内存中,在目录中不显示 filename: 'index.html' })
-
修改 webpack.config.js 文件中向外暴露的配置对象,新增如下配置节点:
module.exports = { plugins: [ htmlPlugin ] // plugins 数组是 webpack 打包期间会用到的一些插件列表 }
-
-
配置自动打包相关的参数
// package.json 中的配置 // --open 打包完成后自动打开浏览器页面 // --host 配置 IP 地址 // --port 配置端口 "scripts": { "dev": "webpack-dev-serveer --open --host 127.0.0.1 --port 8888" }
9.2.4 webpack 中的加载器
-
通过 loader 打包非 js 模块
在实际开发过程中,webpack 默认只能打包处理以 .js 后缀名结尾的模块,其他非 .js 后缀名结尾的模块,webpack 默认处理不了,需要调用 loader 加载器才可以正常打包,否则会报错。
loader 加载器可以协助 webpack 打包处理特定的文件模块,比如:
- less-loader 可以打包处理 .less 相关的文件
- sass-loader 可以打包处理 .scss 相关的文件
- url-loader 可以打包处理 css 中与 url 路径相关的文件
-
loader 的调用过程
9.2.5 webpack 中加载器的基本使用
-
打包处理 css 文件
-
运行
npm i style-loader css-loader -D
命令,安装处理 css 文件的 loader -
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
// 所有第三方文件模块的匹配规则 module: { rules: [ { // 匹配的文件类型 test: /.css$/, // 对应要调用的 loader use: ['style-loader', 'css-loader'] } ] }
注意:
- use 数组中指定的 loader 顺序是固定的
- 多个 loader 的调用顺序是:从后往前调用。先将 css 文件交给 css-loader 处理,再将处理后的结果提交给 style-loader 处理。
-
-
打包处理 less 文件
-
运行
npm i less-loader less -D
命令 -
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
// 所有第三方文件模块的匹配规则 module: { rules: [ { test: /.less$/, use: ['style-loader', 'css-loader', 'less-loader'] } ] }
-
-
打包处理 scss 文件
-
运行
npm i sass-loader node-sass -D
命令 -
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
module: { rules: [ { test: /.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] } ] }
-
-
配置 postCSS 自动添加 css 的兼容前缀
-
运行
npm i postcss-loader autoprefixer -D
命令 -
在项目根目录中创建 postcss 的配置文件 postcss.config.js,并初始化如下配置:
// 导入自动添加前缀的插件 const autoprefixer = require('autoprefixer') module.exports = { // 挂载插件 plugins: [ autoprefixer ] }
-
在 webpack.config.js 的 module -> rules 数组中,修改 css 的 loader 规则如下:
module: { rules: [ { test: /.css$/, use: ['style-loader', 'css-loader', 'postcss-loader'] } ] }
-
-
打包样式表中的图片和字体文件
-
运行
npm i url-loader file-loader -D
命令 -
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
module: { rules: [ { test: /.jpg|png|gif|bmp|ttf|eot|svg|woff|woff2$/, use: 'url-loader?limit=16940' } ] }
其中 ? 之后的是 loader 的参数项。
limit 用来指定图片的大小,单位是字节(byte),只有小于 limit 大小的图片,才会被转为 base64 图片。
-
-
打包处理 js 文件中的高级语法
-
安装 babel 转换器相关的包
npm i babel-loader @babel/core @babel/runtime -D
-
安装 babel 语法插件相关的包:
npm i @babel/preset-env @babel/plugin-transform-runtime @babel/plugin-proposal-class-properties -D
-
在项目根目录中,创建 babel 配置文件 babel.config.js 并初始化基本配置如下:
module.exports = { presets: [ '@babel/preset-env' ], plugins: [ '@babel/plugin-transform-runtime', '@babel/plugin-proposal-class-properties' ] }
-
在 webpack.config.js 的 module -> rules 数组中,添加 loader 规则如下:
// exclude 为排除项,表示 babel-loader 不需要处理 node_modules 中的 js 文件 { test: /.js$/, use: 'babel-loader', exclude: /node_modules/ }
-
9.3 Vue 单文件组件
9.3.1 传统组件的问题和解决方案
-
问题
- 全局定义的组件必须保证组件的名称不重复
- 字符串模板缺乏语法高亮,在 HTML 有多行的时候,需要用到丑陋的反斜杠
- 不支持 CSS 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏
- 没有构建步骤限制,只能使用 HTML 和 ES5 JavaScript,而不能使用预处理器(如:babel)
-
解决方案
针对传统组件的问题,Vue 提供了一个解决方案:使用 Vue 单文件组件。
9.3.2 Vue 单文件组件的基本用法
单文件组件的组成结构
- template 组件的模板区域
- script 业务逻辑区域
- style 样式区域
<template>
<!-- 这里用于定义 Vue 组件的模板内容 -->
</template>
<script>
// 这里用于定义 Vue 组件的业务逻辑
export default {
data: () { return {} }, // 私有数据
methods: {} // 处理函数
// ... 其它业务逻辑
}
</script>
<style scoped>
/* 这里用于定义组件的样式 */
</style>
9.3.3 webpack 中配置 vue 组件的加载器
-
运行
npm i vue-loader vue-template-compiler -D
命令 -
在 webpack.config.js 配置文件中,添加 vue-loader 的配置项如下:
const VueLoaderPlugin = require('vue-loader/lib/plugin') module.exports = { module: { // ... 其它规则 rules: [ { test: /.vue$/, loader: 'vue-loader' } ] }, plugins: [ // ... 其它插件 new VueLoaderPlugin() // 请确保引入这个插件 ] }
9.3.4 在 webpack 项目中使用 vue
- 运行
npm i vue -s
安装 vue; - 在 src -> index.js 入口文件中,通过
import Vue from 'vue'
来导入 vue 构造函数; - 通过 vue 的实例对象,并指定要控制的 el 区域;
- 通过 render 函数渲染 App 根组件
// 1. 导入 Vue 构造函数
import Vue from 'vue'
// 2. 导入 App 根组件
import App from './components/App.vue'
const vm = new Vue({
// 3. 指定 vm 实例要控制的页面区域
el: '#app',
// 4. 通过 render 函数,把指定的组件渲染到 el 区域中
render: h => h(App)
})
9.3.5 webpack 打包发布
上线之前需要通过 webpack 将应用进行整体打包,可以通过 package.json 文件配置打包命令:
// 在 package.json 文件中配置 webpack 打包命令
// 该命令默认加载项目根目录中的 webpack.config.js 配置文件
"scripts": {
// 用于打包的命令
"build": "webpack -p",
// 用于开发调试的命令
"dev": "webpack-dev-server --open --host 127.0.0.1 --port 3000",
},
9.4 Vue 脚手架
9.4.1 Vue 脚手架的基本用法
Vue 脚手架用于快速生成 Vue 项目基础框架。官网地址:https://cli.vuejs.org/zh/
使用步骤:
-
安装 Vue 脚手架
npm install -g @vue/cli
-
脚手架创建 vue 项目
// 1. 基于交互式命令行的方式,创建新版 vue 项目 vue create my-project // 2. 基于图形化界面的方式,创建新版 vue 项目 vue ui // 3. 基于 2.x 的旧模板,创建旧版 vue 项目 npm install -g @vue/cli-init vue init webpack my-project
9.4.2 Vue 脚手架生成的项目结构分析
9.4.3 Vue 脚手架的自定义配置
-
通过 package.json 配置项目
// 必须是符合规范的 json 语法 "vue": { "devServer": { "port": "8888", "open": true } }
注意:不推荐使用这种配置方式。因为 package.json 主要用来管理包的配置信息;为了方便维护,推荐将 vue 脚手架相关的配置,单独定义到 vue.config.js 配置文件中。
-
通过单独的配置文件配置项目
- 在项目的根目录中创建文件 vue.config.js
- 在该文件中进行相关配置,从而覆盖默认配置
// vue.config.js module.exports = { devServer: { port: 8888 } }
9.5 Element-UI 的基本使用
Element-UI:一套为开发者、设计师和产品经理准备的基于 Vue 2.0 的桌面端组件库。
官网地址:http://element-cn.eleme.io/#/zh-CN
-
基于命令行方式手动安装
- 安装依赖包
npm i element-ui -S
- 导入 Element-UI 相关资源
// 导入组件库 import ElementUI from 'element-ui'; // 导入组件相关样式 import 'element-ui/lib/theme-chalk/index.css'; // 配置 Vue 插件 Vue.use(ElementUI);
- 安装依赖包
-
基于图形化界面自动安装
- 运行
vue ui
命令,打开图形化界面 - 通过 Vue 项目管理器,进入具体的项目配置面板
- 点击 插件 -> 添加插件,进入插件查询面板
- 搜索 vue-cli-plugin-element 并安装
- 配置插件,实现按需导入,从而减少打包后项目的体积
- 运行
十、项目优化
10.1 项目优化策略
- 生成打包报告
- 第三方库启用 CDN
- Element-UI 组件按需加载
- 路由懒加载
- 首页内容定制
10.2 生成打包报告
打包时,为了直观地发现项目中存在的问题,可以在打包时生成报告。
生成报告的方式有两种:
-
通过命令行参数的形式生成报告
// 通过 vue-cli 的命令选项可以生成打包报告 // --report 选项可以生成 report.html 以帮助分析包内容 vue-cli-service build --report
-
通过可视化的 UI 面板直接查看报告
在可视化的 UI 面板中,通过控制台和分析面板,可以方便的看到项目中存在的问题。
10.3 通过 vue.config.js 修改 webpack 的默认配置
通过 vue-cli 3.0 工具生成的项目,默认隐藏了所有 webpack 的配置项,目的是为了屏蔽项目的配置过程,让程序员把工作的重心,放到具体功能和业务逻辑的实现上。
如果程序员有修改 webpack 默认配置的需求,可以在项目根目录中,按需创建 vue.config.js 这个配置文件,从而对项目的打包发布过程做自定义的配置(具体配置参考 https://cli.vuejs.org/zh/config/#vue-config-js)
// vue.config.js
// 导出一个包含了自定义配置选项的对象
module.exports = {
}
-
为开发模式和发布模式指定不同的打包入口
默认情况下,Vue 项目的开发模式和发布模式,共用同一个打包的入口文件(即 src/main.js)。为了将项目的开发过程和发布过程分离,我们可以为两种模式,各自指定打包的入口文件,即:
- 开发模式的入口文件为 src/main-dev.js
- 发布模式的入口文件为 src/main-prd.js
-
configureWebpack 和 chainWebpack
在 vue.config.js 导出的配置对象中,新增 configureWebpack 或 chainWebpack 节点,来自定义 webpack 的打包配置。
configureWebpack 和 chainWebpack 的作用相同,唯一的区别就是它们修改 webpack 配置的方式不同:
- chainWebapck 通过链式编程的形式,来修改默认的 webpack 配置
- configureWebpack 通过操作对象的形式,来修改默认的 webpack 配置
两者具体的使用差异,可参考如下网址:https://cli.vuejs.org/zh/guide/webpack.html#webpack-相关
-
通过 chainWebpack 自定义打包入口
module.exports = { chainWebpack: config => { config.when(process.env.NODE_ENV === 'production', config => { config.entry('app').clear().add('./src/main-prd.js') }) config.when(process.env.NODE_ENV === 'development', config => { config.entry('app').clear().add('./src/main-dev.js') }) } }
10.4 通过 externals 加载外部 CDN 资源
默认情况下,通过 import 语法导入的第三方依赖包,最终会被打包合并到同一个文件中,从而导致打包成功后,单文件体积过大的问题。
为了解决上述问题,可以通过 webpack 的 externals 节点,来配置并加载外部的 CDN 资源。凡是声明在 externals 中的第三方依赖包,都不会被打包。
config.set('externals', {
vue: 'vue',
'vue-router': 'VueRouter',
axios: 'axios',
lodash: '_',
echarts: 'echarts',
nprogress: 'NProgress',
'vue-quill-editor': 'VueQuillEditor'
})
同时,需要在 public/index.html 文件的头部,添加如下的 CDN 资源引用:
<!-- nprogress 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.css" />
<!-- 富文本编辑器的样式文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.core.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.snow.min.css" />
<link rel="stylesheet" href="https://cdn.staticfile.org/quill/1.3.4/quill.bubble.min.css" />
<script src="https://cdn.staticfile.org/vue/2.5.22/vue.min.js"></script>
<script src="https://cdn.staticfile.org/vue-router/3.0.1/vue-router.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script src="https://cdn.staticfile.org/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdn.staticfile.org/echarts/4.1.0/echarts.min.js"></script>
<script src="https://cdn.staticfile.org/nprogress/0.2.0/nprogress.min.js"></script>
<!-- 富文本编辑器的 js 文件 -->
<script src="https://cdn.staticfile.org/quill/1.3.4/quill.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-quill-editor@3.0.4/dist/vue-quill-editor.js"></script>
10.5 通过 CDN 优化 ElementUI 的打包
虽然在开发阶段,启用了 element-ui 组件的按需加载,尽可能的减少了打包的体积,但是那些被按需加载的组件,还是占用了较大的文件体积。此时,我们可以将 element-ui 中的组件,也通过 CDN 的形式来加载,这样能够进一步减小打包后的文件体积。
具体操作流程:
- 在 main-prd.js 中,注释掉 element-ui 按需加载的代码
- 在 index.html 的头部区域中,通过 CDN 加载 element-ui 的 js 和 css 样式
<!-- element-ui 的样式表文件 -->
<link rel="stylesheet" href="https://cdn.staticfile.org/element-ui/2.8.2/theme-chalk/index.css" />
<!-- element-ui 的 js 文件 -->
<script src="https://cdn.staticfile.org/element-ui/2.8.2/index.js"></script>
10.6 首页内容自定制
不同的打包环境下,首页内容可能会有所不同。我们可以通过插件的方式进行定制,插件配置如下:
chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
config.plugin('html').tap(args => {
args[0].isPrd = true
return args
})
})
config.when(process.env.NODE_ENV === 'development', config => {
config.plugin('html').tap(args => {
args[0].isPrd = false
return args
})
})
}
在 public/index.html
首页中,可以根据 isPrd
的值来决定如何渲染页面结构:
<!-- 按需渲染页面的标题 -->
<title><%= htmlWebpackPlugin.options.isPrd ? '' : 'dev - ' %>电商后台管理系统</title>
<!-- 按需加载外部的 CDN 资源 -->
<% if (htmlWebpackPlugin.options.isPrd) { %>
<!-- 通过 externals 加载的外部 CDN 资源 -->
<% } %>
10.7 路由懒加载
当打包构建项目时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就更加高效了。
使用步骤:
-
安装
@bebel/plugin-syntax-dynamic-import
包 -
在 babel.config.js 配置文件中声明该插件
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins: [ ... '@babel/plugin-syntax-dynamic-import' ] }
-
将路由改为按需加载的形式,实例代码如下:
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue') const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue') const Baz = () => import(/* webpackChunkName: "group-boo" */ './Baz.vue')
解释:
'./Foo.vue'
是路由组件存放的位置/* webpackChunkName: "group-foo" */
是路由的分组
在上面的实例代码中,Foo 和 Bar 被打包进了同一个 js 文件,在请求 Foo 组件时,也会把 Bar 组件请求到。
关于路由懒加载的详细文档,参考以下链接:https://router.vuejs.org/zh/guide/advanced/lazy-loading.html
十一、项目上线
11.1 项目上线相关配置
- 通过 node 创建 web 服务器
- 开启 gzip 配置
- 配置 https 服务
- 使用 pm2 管理应用
11.2 通过 node 创建 web 服务器
创建 node 项目,并安装 express,通过 express 快速创建 web 服务器,将 vue 打包生成的 dist 文件夹,托管为静态资源即可,关键代码如下:
const express = require('express')
// 创建 web 服务器
const app = express()
// 托管静态资源
app.use(express.static('./dist'))
// 启动 web 服务器
app.listen(3000, () => {
console.log('web server running at http://127.0.0.1:3000')
})
11.3 开启 gzip 配置
使用 gzip 可以减小文件体积,是传输速度更快。
可以通过服务器使用 express 做 gzip 压缩,配置如下:
// 安装包
npm install compression -D
// 导入包
const compression = require('compression')
// 启用中间件
app.use(compression)
11.4 配置 HTTPS 服务
- 传统的 HTTP 协议传输的数据都是明文,不安全
- 采用 HTTPS 协议对传输的数据进行了加密处理,可以防止数据被中间人窃取,使用更安全。
申请 SSL 证书(https://freessl.org)
- 进入 https://freessl.cn/ 官网,输入要申请的域名并选择品牌
- 输入自己的邮箱并选择相关选项
- 验证 DNS(在域名管理后台添加 TXT 记录)
- 验证通过之后,下载 SSL 证书(full_chain.pem 公钥;private.key 私钥)
在后台项目中导入证书
const https = require('https')
const fs = require('fs')
const options = {
cert: fs.readFileSync('./full_chain.pem'),
key: fs.readFileSync('./private.key')
}
https.createServer(options, app).listen(443)
11.5 使用 pm2 管理应用
- 在服务器中安装 pm2:
npm i pm2 -g
- 启动项目:
pm2 start 脚本 --name 自定义名称
如pm2 start app.js --name web_vueshop
- 查看运行项目:
pm2 ls
- 重启项目:
pm2 restart 自定义名称
- 停止项目:
pm2 stop 自定义名称
- 删除项目:
pm2 delete 自定义名称
十二、Vuex
12.1 Vuex 概述
-
Vue组件间共享数据的方式
- 父向子传值:v-bind 属性绑定
- 子向父传值:v-on 事件绑定
- 兄弟组件之间共享数据:EventBus
- 接收数据的那个组件使用 $on
- 发送数据的那个组件使用 $emit
-
Vuex 是什么
Vuex 是实现组件全局状态(数据)管理的一种机制,可以方便的实现组件之间数据的共享。
-
使用 Vuex 统一管理状态的好处
- 能够在 vuex 中集中管理共享的数据,易于开发和后期维护;
- 能够高效地实现组件之间的数据共享,提高开发效率
- 存储在 vuex 中的数据都是响应式的,能够实时保持数据与页面的同步
-
什么样的数据适合存储在 Vuex 中
一般情况下,只有组件之间共享的数据,才有必要存储在 vuex 中;对于组件中的私有数据,依旧存储在组件自身的 data 中即可。
12.2 Vuex 的基本使用
-
安装 vuex 依赖包
npm install vuex --save
-
导入 vuex 包
import Vuex from 'vuex' Vue.use(Vuex)
-
创建 store 对象
const store = new Vuex.Store({ // state 中存放的就是全局共享的数据 state: { count: 0 } })
-
将 store 对象挂载到 vue 实例中
new Vue({ el: '#app', render: h => h(app), router, // 将创建的共享数据对象挂载到 Vue 实例中 // 所有的组件,就可以直接从 store 中获取全局的数据了 store })
12.3 Vuex 的核心概念
Vuex 中的主要核心概念:
- State
- Mutation
- Action
- Getter
12.3.1 State
State 提供唯一的公共数据源,所有共享的数据都要统一放到 Store 的 State 中进行存储。
// 创建 store 数据源,提供唯一公共数据
const store = new Vuex.Store({
state: { count: 0 }
})
组件访问 State 中数据的第一种方式:
// this.$store.state.全局数据名称
this.$store.state.count
组件访问 State 中数据的第二种方式:
// 1. 从 vuex 中按需导入 mapState 函数
import { mapState } from 'vuex'
通过刚才导入的 mapState 函数,将当前组件需要的全局数据,映射为当前组件的 computed 计算属性:
// 2. 将全局数据映射为当前组件的计算属性
computed: {
...mapState(['count'])
}
12.3.2 Mutation
Mutation 用于变更 Store 中的数据。
- 只能通过 mutation 变更 Store 数据,不可以直接操作 Store 中的数据
- 通过这种方式虽然操作起来稍微繁琐一些,但是可以集中监控所有数据的变化。
// 定义 Mutation
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
add(state) {
// 变更状态
state.count++
}
}
})
组件中触发 mutation
methods: {
btnClicked() {
// 触发 mutations 的第一种方式
this.$store.commit('add')
}
}
可以在触发 mutations 时传递参数:
// 定义 mutations
const store = new Vuex.Store({
state: {
count: 0
},
mutations: {
addN(state, step) {
// 变更状态
state.count += step
}
}
})
组件中触发 mutation
methods: {
btnClicked() {
// 在调用 commit 函数触发 mutations 时携带参数
this.$store.commit('addN', 3)
}
}
this.$store.commit()
是触发 mutations 的第一种方式,触发 mutation 的第二种方式:
// 1. 从 vuex 中按需导入 mapMutations 函数
import { mapMutations } from 'vuex'
通过刚才导入的 mapMutations 函数,将需要的 mutations 函数映射为当前组件的 methods 方法:
// 2. 将指定的 mutations 函数映射为当前组件的 methods 函数
methods: {
...mapMutations(['add', 'addN'])
}
不要在 mutations 函数中执行异步操作
12.3.3 Action
Action 用于处理异步任务。
如果通过异步操作变更数据,必须通过 Action,而不能使用 Mutation,但是在 Action 中还是要通过触发 Mutation 的方法间接变更数据。
// 定义 Action
const store = new Vuex.Store({
...
mutatios: {
add(state) {
state.count++
}
},
actions: {
addAsync(context) {
setTimeout(() => {
context.commit('add')
}, 1000)
}
}
})
组件中触发 Action:
methods: {
btnClicked() {
// 触发 actions 的第一种方式
this.$store.dispatch('addAsync')
}
}
触发 actions 异步任务时携带参数:
const store = new Vuex.store({
...
mutations: {
addN(state, step) {
state.count += step
}
},
actions: {
addNAsync(context, step) {
setTimeout(() => {
context.commit('addN', step)
}, 1000)
}
}
})
组件中触发 Action:
methods: {
btnClicked() {
this.$store.dispatch('addNAsync', 2)
}
}
触发 actions 的第二种方式:
// 1. 从 vuex 中按需导入 mapActions 函数
import { mapActions } from 'vuex'
通过刚导入的 mapActions 函数,将需要的 actions 函数映射为当前组件的 methods 方法:
// 2. 将指定的 actions 函数映射为当前组件的 methods 函数
methods: {
...mapActions(['addAsync', 'addNAsync'])
}
12.3.4 Getter
Getter 用于对 Store 中的数据进行加工处理形成新的数据。
- Getter 可以对 Store 中已有的数据加工处理之后形成新的数据,类似 Vue 的计算属性。不会修改原数据。
- Store 中数据发生变化,Getter 的数据也会跟着变化。
const store = new Vuex.Store({
state: {
count: 0
},
getters: {
showNum: state => {
return '当前最新的数量是【' + state.count + '】'
}
}
})
使用 getters 的第一种方式:
// this.$store.getters.名称
this.$store.getters.showNum
使用 getters 的第二种方式:
import { mapGetters } from 'vuex'
computed: {
...mapGetters(['showNum'])
}