(一)Vue 模板语法
一、指令
1.1 什么是指令?
指令的本质就是自定义属性;指令的格式:以v-开始(比如: v-cloak)
1.2 v-cloak指令用法
- 插值表达式存在的问题:“闪动”
- 如何解决:使用v-cloak指令
- 解决的原理:先隐藏,替换好值之后再显示最终的值
1.3 数据绑定指令
- v-text:填充纯文本,相比插值表达式更加简洁
- v-html:填充HTML片段,但是会存在安全问题,本网站内部数据可以使用该指令,来自第三方的数据不可以用
- v-pre:填充原始信息,显示原始信息,跳过编译过程(用于分析编译过程)
1.4 数据响应式
- 如何理解响应式:1. html5中的响应式(屏幕尺寸的变化导致样式的变化);2. 数据的响应式(数据的变化导致页面内容的变化)
- 什么是数据绑定?数据绑定是将数据填充到标签中
- v-once指令:只编译一次,显示内容之后不再具有响应式功能,其应用场景为当数据上传之后不需要再修改时可以提高编译效率
1.5 双向数据绑定(应用场景:表单输入)
二、 事件绑定
2.1 绑定事件
注意:
1)如果事件直接绑定函数名称,那么默认会传递事件对象作为事件函数的第一个参数;
2)如果事件绑定函数调用,那么事件对象必须作为最后一个参数显示传递,并且事件对象的名称必须是$event。
2.2 事件修饰符
<!-- .stop 阻止冒泡,相当于event.stopPropagation() -->
<a v-on:click.stop="handle">跳转</a>
<!-- .prevent 阻止默认行为,相当于event.preventDefault() -->
<a v-on:click.prevent="handle">跳转</a>
2.3 键盘事件
<!-- .enter 回车键 -->
<input v-on:keyup.enter="submit">
<!-- .delete 删除键 -->
<input v-on:keyup.delete="submit">
2.4 自定义按键修饰符
// 全局config.keyCodes对象
Vue.config.keyCodes.f1 = 112
Vue.config.keyCodes.名称 = event.keyCode
三、属性绑定
3.1 动态处理属性
- v-bind 指令用法
<a v-bind:href="url">跳转</a>
<!-- 缩写形式 -->
<a :href="url">跳转</a>
<body>
<div id="app">
<a v-bind:href="url">百度</a>
<a :href="url">百度1</a>
<button v-on:click='handle'>切换</button>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
属性绑定
*/
var vm = new Vue({
el: '#app',
data: {
url: 'http://www.baidu.com'
},
methods: {
handle: function(){
// 修改URL地址
this.url = 'http://itcast.cn';
}
}
});
</script>
</body>
3.2 v-model的底层实现原理
v-model的底层相当于使用了v-bind和v-on
<input v-bind: value="msg" v-on:input="msg=$event.target.value">
// 相当于在Vue对象中使用
this.msg = event.target.value;
四、样式绑定
4.1 class样式处理
- 对象语法
<!-- isActive是控制active类名是否有效的变量,布尔类型 -->
<div v-bind:class="{ active: isActive }"></div>
- 数组语法
<div v-bind:class="[ activeClass, errorClass ]"></div>
<div id="app">
<div v-bind:class="{active: isActive}">测试</div>
<button v-on:click="handle">切换</button>
</div>
<script src="js/vue.js"></script>
<script>
// 样式绑定
var vm = new Vue({
el: '#app',
data: {
isActive: true
},
methods: {
handle: function () {
this.isActive = !this.isActive;
}
}
});
</script>
<div id="app">
<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: {
activeClass: 'active',
errorClass: 'error'
},
methods: {
handle: function () {
this.activeClass = '';
this.errorClass = '';
}
}
});
</script>
对比: 数组绑定无法实现切换,用于添加多个类名的场景。
语法细节:
1、对象绑定和数组绑定可以结合使用
<div v-bind:class="[activeClass, errorClass,{test: isTest}]">测试1</div>
<div v-bind:class="arrClasses">测试2</div>
<div v-bind:class="objClasses">测试3</div>
data: {
activeClass: 'active',
errorClass: 'error',
isTest: true,
arrClasses: ['active','error'],
objClasses: {
active: true,
error: true
}
methods: {
handle: function () {
// this.isTest = false;
this.objClasses.error = false;
}
}
2、class绑定的值可以简化操作
3、默认的class如何处理? ---- 默认的class样式会被保留
4.2 style样式处理
- 对象语法
<!-- "-"要用驼峰写法代替 -->
<div v-bind:style="{ color:activeColor, fontSize: fontSize}"></div>
- 数组语法
<!-- 一般数组里面放置样式的对象 -->
<div v-bind:style="[ baseStyles, overridingStyles]"></div>
<div id="app">
<div v-bind:style="{border: borderStyle, width: widthStyle, height: heightStyle}"></div>
<div v-bind:style="[objStyle,overrideStyles]"></div>
<div v-bind:style="objStyle"></div>
<button v-on:click="handle">切换</button>
</div>
<script src="js/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
borderStyle: '1px solid blue',
widthStyle: '100px',
heightStyle: '200px',
objStyle: {
border: '1px solid blue',
width: '200px',
height: '100px'
},
overrideStyles: {
border: '5px solid orange',
backgroundColor: 'blue'
}
},
methods: {
handle: function () {
this.heightStyle = '100px';
this.objStyle.width = '100px';
}
}
});
</script>
五、分支循环结构
5.1 v-show
原理:控制元素样式是否显示,操控display属性。
v-show与v-if的区别:
v-show会将结构渲染到页面,若不显示则display:none,而v-if的原理是是否渲染元素到页面。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dOBGZP8X-1649484576768)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618197098743.png)]
<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">测试</div>
5.2 v-for
// 使用原生js遍历对象
var obj = {
uname: 'lisi',
age: 12,
gender: 'male'
}
for(var key in obj) {
console.log(key, obj[key])
}
<div id="app">
<div v-if='v==13' v-for='(v,k,i) in obj'>{{v + '---' + k + '---' + i}}</div>
</div>
var vm = new Vue({
el: '#app',
data: {
obj: {
uname: 'zhangsan',
age: 13,
gender: 'female'
}
}
});
5.3 声明式编程
- 模板的结构和最终显示的效果基本一致
(二)Vue常用特性
六、表单操作
6.1 基于Vue的表单操作
<div id="app">
<form action="http://itcast.cn">
<div>
<span>姓名:</span>
<span>
<input type="text" v-model="uname">
</span>
</div>
<div>
// 设置value值区分不同选项,通过传值选中该选项框
<span>性别:</span>
<span>
<input type="radio" id="male" value="1" v-model="gender">
<label for="male">男</label>
<input type="radio" id="female" value="2" v-model="gender">
<label for="female">女</label>
</span>
</div>
<div>
<span>爱好:</span>
<input type="checkbox" id="ball" value="1" v-model="hobby">
<label for="ball">篮球</label>
<input type="checkbox" id="sing" value="2" v-model="hobby">
<label for="sing">唱歌</label>
<input type="checkbox" id="code" value="3" v-model="hobby">
<label for="code">写代码</label>
</div>
<div>
<span>职业:</span>
<select v-model="occupation" multiple>
<option value="0">请选择职业...</option>
<option value="1">教师</option>
<option value="2">软件工程师</option>
<option value="3">律师</option>
</select>
</div>
<div>
<span>个人简介:</span>
// Vue不允许在标签中间写文本内容
<textarea v-model="desc"></textarea>
</div>
<div>
<!-- 禁止表单默认提交行为 -->
<input type="submit" value="提交" @click.prevent="handle">
</div>
</form>
</div>
<script type="text/javascript" src="js/vue.js"></script>
<script type="text/javascript">
/*
表单基本操作
*/
var vm = new Vue({
el: '#app',
data: {
uname: 'lisi',
gender: 2,
hobby: ['2', '3'],
// occupation: 3
occupation: ['2', '3'],
desc: 'nihao'
},
methods: {
handle: function () {
// console.log(this.uname);
// 输出数据
console.log(this.hobby.toString());
}
}
});
</script>
6.2 表单域修饰符
input事件和change事件的区别: change事件在失去焦点时触发,input事件在文本发生改变时触发。
七、自定义指令
带参数的自定义指令:
与全局指令的区别: 局部指令只能在本组件(关于组件的概念在后面章节)中使用,全局指令则没有限制。
八、计算属性(基于data中的数据,数据改变,计算属性对应发生变化)
<div id="app">
<div>{{msg}}</div>
<div>{{reverseString}}</div>
</div>
<script src="js/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'Hello'
},
computed: {
reverseString: function () {
return this.msg.split('').reverse().join('');
}
}
})
</script>
九、侦听器
9.1 应用场景
数据变化时执行异步或开销较大的操作。
在其他场景(开销小)下,用计算属性也能实现相同的功能,且更方便。
9.2 用法
十、过滤器
10.1 过滤器作用
10.2 自定义过滤器
过滤器也支持级联效果(链式编程)
<div id="app">
<input type="text" v-model='msg'>
<div>
{{msg | upper}}
</div>
</div>
<script>
Vue.filter('upper',function(val){
// 记得返回值,否则没有数据
return val.charAt(0).toUpperCase() + val.slice(1);
});
var vm = new Vue({
el: '#app',
data: {
msg: ''
}
});
</script>
10.3 过滤器的使用
10.4 局部过滤器
10.5 带参数的过滤器
<div id="app">
<div>{{date | format('yyyy-MM-dd hh:mm:ss')}}</div>
</div>
<script src="js/vue.js"></script>
<script>
Vue.filter('format', function (value, arg) {
function dateFormat(date, format) {
if (typeof date === "string") {
var mts = date.match(/(\/Date\((\d+)\)\/)/);
if (mts && mts.length >= 3) {
date = parseInt(mts[2]);
}
}
date = new Date(date);
if (!date || date.toUTCString() == "Invalid Date") {
return "";
}
var map = {
"M": date.getMonth() + 1, //月份
"d": date.getDate(), //日
"h": date.getHours(), //小时
"m": date.getMinutes(), //分
"s": date.getSeconds(), //秒
"q": Math.floor((date.getMonth() + 3) / 3), //季度
"S": date.getMilliseconds() //毫秒
};
format = format.replace(/([yMdhmsqS])+/g, function (all, t) {
var v = map[t];
if (v !== undefined) {
if (all.length > 1) {
v = '0' + v;
v = v.substr(v.length - 2);
}
return v;
} else if (t === 'y') {
return (date.getFullYear() + '').substr(4 - all.length);
}
return all;
});
return format;
}
return dateFormat(value, arg);
});
var vm = new Vue({
el: '#app',
data: {
date: new Date()
},
methods: {
}
});
</script>
十一、生命周期
11.1 主要阶段
11.2 Vue实例过程
11.3 Vue的数组方法(数组的响应式变化)
// 通过 list[1] = 'lemon' 和 obj.gender = 'male' 添加的值不具有响应式,即修改数组或对象不发生变化
要用 vm.$set(vm.items,1,'lemon') 和 vm.$set(vm.items,'gender','male') 进行添加响应式数据
(三)组件
一、组件化开发思想
二、组件注册
2.1 全局组件注册语法
2.2 用法
2.3 注意事项
1、data必须是一个函数:为了形成一个闭包,否则会报错
2、组件模板内容必须是单个根元素
3、组件模板内容可以是模板字符串
- 模板字符串需要浏览器提供支持(ES6语法)
template: `
<div>
<button @click="count++">点击了{{count}}次</button>
<button>测试</button>
</div>
`
2.4 组件命名方式
2.5 局部组件
!局部组件只能在注册它的父组件中使用
三、组件间数据交互
3.1 父组件向子组件传值
1、数值和布尔值不做属性绑定时,数据类型会变成string。
2、props传递数据原则:单向数据流
3.2 子组件向父组件传值
<div id="app">
<div :style="{fontSize: fontSize + 'px'}">{{pmsg}}</div>
<menu-item :parr="parr" @enlarge-text="handle($event)"></menu-item>
</div>
<script src="js/vue.js"></script>
<script>
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>
<button @click='$emit("enlarge-text",5)'>扩大父组件中的字体大小</button>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
pmsg: '父组件中内容',
parr: ['apple', 'orange', 'banana'],
fontSize: 10
},
methods: {
handle: function (val) {
// 扩大字体大小
this.fontSize += val;
}
}
});
</script>
3.3 非父子组件间传值(兄弟组件传值)
<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>
3.4 组件插槽
作用:
<slot>默认内容</slot>
3.5 具名插槽
3.6 作用域插槽
<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: `
<div>
<li :key='item.id' v-for='item in list'>
<slot :info="item">{{item.name}}</slot>
</li>
</div>
`
});
var vm = new Vue({
el: '#app',
data: {
list: [{
id: 1,
name: 'apple'
}, {
id: 2,
name: 'banana'
}, {
id: 3,
name: 'orange'
}]
}
});
</script>
(四)前后端交互
一、前后端交互模式
1.1 URL地址格式
二、Promise 用法
2.1 异步调用
注:会出现回调地狱现象。
2.2 Promise概述(Promise本身是函数,函数也是对象)
var p = new Promise(function (resolve, reject) {
// 这里用于实现异步任务
setTimeout(function () {
var flag = true;
if (flag) {
// 正常情况
resolve('hello');
} else {
// 异常情况
reject('error');
}
}, 100);
});
p.then(function (data) {
console.log(data);
}, function (info) {
console.log(info);
});
2.3 基于Promise处理Ajax请求
function queryData(url) {
var p = new Promise(function (resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function () {
if (xhr.readyState != 4) return;
if (xhr.readyState == 4 && 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);
}, function (info) {
console.log(info);
});
发送多次Ajax请求并且保证顺序:
queryData('http://localhost:3000/data')
.then(function (data) {
console.log(data);
return queryData('http://localhost:3000/data1');
})
.then(function (data) {
console.log(data);
return queryData('http://localhost:3000/data2');
})
.then(function (data) {
console.log(data);
});
2.4 then参数中的函数返回值
2.5 Promise常用的API
三、接口调用-fetch用法
3.1 fetch 概述
3.2 fetch 基本用法
fetch('http://localhost:3000/fdata').then(function (data) {
// text(方法属于fetch API的一部分,它返回一个Promise实例对象,用于获取后台返回的数据
return data.text();
}).then(function (data) {
console.log(data);
});
3.3 fetch 请求参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kEmODRmh-1649487890482)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618626921540.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NGjbEmnm-1649487890483)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618626969205.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pxbvGkvC-1649487890483)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627098248.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vs9FhTwA-1649487890484)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627161070.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-d6OypgHh-1649487890485)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627212143.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G4Y1bLgv-1649487890485)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627366962.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NnofX2JN-1649487890486)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627598592.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wq44k8Nr-1649487890486)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618627649256.png)]
! 允许跨域访问设置
// 设置允许跨域访问该服务
app.all('*', function (req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header('Access-Control-Allow-Methods', 'PUT, GET, POST, DELETE, OPTIONS');
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
res.header('Access-Control-Allow-Headers', 'Content-Type');
// 开启认证请求
res.header('Access-Control-Allow-Credentials',true);
// 内容类型:如果是post请求必须指定这个属性
res.header('Content-Type','application/json;charset=utf-8');
next();
});
3.4 fetch 响应结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dk3kR8J6-1649484576796)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618628920123.png)]
res.json()
https://www.expressjs.com.cn/4x/api.html#res.json
res.json({
uname: 'lisi',
pwd: '123'
});
四、接口调用-axios用法
4.1 axios 的基本特性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yo6sWDrZ-1649484576796)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618629778322.png)]
4.2 axios 的基本用法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9HviIVx6-1649484576797)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618629934924.png)]
4.3 axios 参数传递
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jtjenFEU-1649484576797)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630565043.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K7sfTndm-1649484576798)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630585083.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ph16O1b7-1649484576798)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630659929.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-clFVGf06-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618630849934.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1f5R1LgJ-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631025745.png)]
4.4 axios的响应结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vVm7kpsv-1649484576799)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631119083.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LJJesPHK-1649484576800)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631135460.png)]
4.5 axios的全局配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jji52oJx-1649484576800)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631256346.png)]
res.header('Access-Control-Allow-Headers', 'mytoken');
/* 全局配置 */
// 配置请求的基准URL地址,会自动拼接上baseURL
axios.defaults.baseURL = 'http://localhost:3000/';
// 配置请求头信息
axios.defaults.headers['mytoken'] = 'hello';
axios.get('axios-json').then(function (ret) {
console.log(ret.data.uname);
});
4.6 axios拦截器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jI2QSmST-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618631825359.png)]
axios.interceptors.request.use(function (config) {
// 可用于让某些路由设置请求头
console.log(config.url);
config.headers.mytoken = 'nihao';
return config;
}, function (err) {
console.log(err);
});
axios.get('http://localhost:3000/adata').then(function (data) {
console.log(data.data);
})
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ow6EzhPX-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619357634795.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IhBGXybn-1649484576801)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618632212976.png)]
/* 响应拦截器 */
axios.interceptors.response.use(function (res) {
// res 和下面的 data是完全一样的对象
var data = res.data;
return res;
}, function (err) {
console.log(err);
});
axios.get('http://localhost:3000/adata').then(function (data) {
// 这里就可以直接打印data了
console.log(data);
})
4.7 axios带token下载文件
exportFile(event) {
event.preventDefault()
//使a自带的方法失效,即无法调整到href中的URL
this.$http({
method: 'get',
url: 'http://anjude.cn.utools.club/cashout/export',
responseType: 'blob',
})
.then((res) => {
const link = document.createElement('a')
let blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
console.log(res)
//获取heads中的filename文件名
var fileName = '订单列表'
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.catch((error) => {
console.log(error)
})
},
五、接口调用-async/await用法
5.1 async/await 的基本用法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2phlqZIk-1649484576802)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618632544516.png)]
async function queryData() {
var ret = await axios.get('http://localhost:3000/adata');
// console.log(ret.data);
return ret.data;
}
queryData().then(function (data) {
console.log(data);
});
// 后面跟Promise对象用法
async function queryData() {
var ret = await new Promise(function(resolve,reject){
setTimeout(function(){
resolve('nihao');
},1000);
});
return ret;
}
queryData().then(function (data) {
console.log(data);
});
5.2 async/await 处理多个异步请求
多个异步请求场景
/* 处理多个异步请求 */
axios.defaults.baseURL = 'http://localhost:3000/';
async function queryData() {
var info = await axios.get('async1');
var ret = await axios.get('async2?info=' + info.data);
return ret.data;
}
queryData().then(function (data) {
console.log(data);
});
(四)Vue路由
一、路由的基本概念
1.1 路由
分为前端路由和后端路由。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-i6W4BubP-1649484576802)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650271050.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m5Yugl3M-1649484576803)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650312273.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S4KGgUPz-1649484576803)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650345982.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4WJNLo1u-1649484576804)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650433704.png)]
哈希值:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SKcqMYs1-1649484576804)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650828716.png)]
!component占位符:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uCbjzXwe-1649484576805)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618650971320.png)]
1.2 Vue Router
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-355D6kve-1649484576805)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651092964.png)]
二、Vue-Router基本使用
2.1 步骤
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tDLBPTTb-1649484576806)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651178627.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-y1OU2mA5-1649484576806)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651812492.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9d4Zjayr-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651824881.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cTQMvnUX-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651844666.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ckAUrw1u-1649484576807)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651855902.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbn2tJa1-1649484576808)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618651892739.png)]
2.2 路由重定向
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRD8Gbur-1649484576808)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618652096405.png)]
三、嵌套路由
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xGKcLGvX-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658428745.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8bU8AlWC-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658443648.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V43kHNet-1649484576809)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658457022.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eo3fXfdH-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618658467142.png)]
四、动态路由匹配
4.1 动态匹配路由的基本用法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-O0HMCbh5-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618659053019.png)]
4.2 路由组件传递参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ePlwx0Es-1649484576810)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618659196060.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ka7jzfXb-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661147571.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zXPCwJxk-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661177961.png)]
五、命名路由
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RY2njlB5-1649484576811)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661495136.png)]
<router-link :to="{name: 'user',params: {id: 3}}">User3</router-link>
六、编程式导航
6.1 页面导航的两种方式
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yRZbBR8Q-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661716064.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VjLxHe4i-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618661757078.png)]
6.2 router.push()参数
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g5HzSrm1-1649484576812)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1618662701277.png)]
6.3 路由导航守卫控制访问权限
6.3.1 全局前置守卫
1. 旧版导航守卫(使用next),这里的next用法和router.push()用法一样。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AG6xdKa4-1649484576813)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619353195850.png)]
2. 新版导航守卫根据return值判断导航是否有效:(当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于等待中)
a. false: 取消当前的导航。如果浏览器的URL改变了(可能是用户手动或者浏览器后退按钮),那么URL地址会重置到from路由对应的地址。
b. 一个路由地址: 通过一个路由地址跳转到一个不同的地址,就像调用router.push()方法一样,可以设置诸如 replace: true或name: 'home’之类的配置。当前导航被中断,然后进行一个新的导航,就和from一样。
c. undefined 或 true: 导航是有效的,并调用下一个导航守卫。
d. 意料之外的情况,可能会抛出一个error。这会取消导航并且调用 router.onError()注册过的回调。
router.beforeEach(async(to,from) => {
// canUserAccess()返回 true 或 false
return await canUserAccess(to)
})
6.3.2 全局解析守卫
你可以用 router.beforeResolve
注册一个全局守卫。这和 router.beforeEach
类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用。这里有一个例子,确保用户可以访问自定义 meta 属性 requiresCamera
的路由:
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
6.3.3 全局后置钩子
全局后置钩子和守卫不同的是,这些钩子不会接受 next
函数也不会改变导航本身,它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})
它们也反映了 navigation failures 作为第三个参数:
router.afterEach((to, from, failure) => {
if (!failure) sendToAnalytics(to.fullPath)
})
6.3.4 路由独享守卫
a. 可以直接在路由配置上定义 beforeEnter
守卫:(用法类似beforeEach)
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
beforeEnter
守卫 只在进入路由时触发,不会在 params
、query
或 hash
改变时触发。例如,从 /users/2
进入到 /users/3
或者从 /users/2#info
进入到 /users/2#projects
。它们只有在 从一个不同的 路由导航时,才会被触发。
b. 可以将一个函数数组传递给 beforeEnter
,这在为不同的路由重用守卫时很有用:
function removeQueryParams(to) {
if (Object.keys(to.query).length)
return { path: to.path, query: {}, hash: to.hash }
}
function removeHash(to) {
if (to.hash) return { path: to.path, query: to.query, hash: '' }
}
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: [removeQueryParams, removeHash],
},
{
path: '/about',
component: UserDetails,
beforeEnter: [removeQueryParams],
},
]
6.3.5 路由组件钩子
beforeRouteEnter
beforeRouteUpdate
beforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
beforeRouteEnter
守卫 不能 访问 this
,因为守卫在导航确认前被调用,因此即将登场的新组件还没被创建。
不过,你可以通过传一个回调给 next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
注意 beforeRouteEnter
是支持给 next
传递回调的唯一守卫。对于 beforeRouteUpdate
和 beforeRouteLeave
来说,this
已经可用了,所以** 不支持 传递回调,因为没有必要了:
beforeRouteUpdate (to, from) {
// just use `this`
this.name = to.params.name
}
这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false
来取消。
beforeRouteLeave (to, from) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (!answer) return false
}
6.4 基于token的退出登录功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FJeqUTXI-1649484576813)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619353614277.png)]
6.5 Vue-Router注意点
- 使用带有参数(/users/:username)的路由时需要注意的是,相同的组件实例被重复使用,比起销毁再创建,复用则显得更加高效。**不过,这也意味着组件的生命周期钩子不会被调用。**可用以下方法解决:
// 1. watch监听$route对象上的任意属性,比如监听$route.params
const User = {
template: '...',
created(){
this.$watch(
() => this.$route.params,
(toParams,previousParams) => {
// 对路由变化做出响应
}
)
/*
// watch 路由的参数,以便再次获取数据
this.$watch(
() => this.$route.params,
() => {
this.fetchData()
},
// 组件创建完后获取数据,
// 此时 data 已经被 observed 了
{ immediate: true }
)
*/
}
}
// 2. 利用导航守卫
const User = {
template: '...',
async beforeRouteUpdate(to, from){
// 对路由变化做出响应
this.userData = await fetchUser(to.params.id)
}
}
- 嵌套路由时,当想在子路由未匹配时也渲染一些东西呈现时,可以提供一个空的嵌套路径:
const routes = [
{
path: '/user/:id',
component: User,
children: [
// 当 /user/:id 匹配成功
// UserHome 将被渲染到User的<router-view>内部
{
path: '', component: UserHOme
},
// ...其他子路由
]
}
]
- 想要导航到不同的URL,可以使用router.push(this.$router.push)方法,这个方法会向history栈添加一个新的记录,所以当用户点击浏览器后退按钮时,会回到之前的URL。
- router.replace()的作用与router.push()类似,唯一的不同是,它在导航时不会向history添加新记录。
// 直接在传递给router.push的routeLocation中增加一个属性replace: true 相当于router.replace()
router.push({ path: '/home', replace: true })
// 相当于
router.replace({ path: 'home' })
- 横跨历史
window.history.go(n) // 该方法采用一个整数作为参数,表示在历史堆栈中前进或后退多少步
// 向前移动一条记录,与 router.forward() 相同
router.go(1)
// 返回一条记录,与router.back() 相同
router.go(-1)
// 前进 3 条记录
router.go(3)
// 如果没有那么多记录,静默失败
router.go(-100)
router.go(100)
- 路由别名
- 路由的props
a. 将props传递给路由组件
const User = {
template: '<div>User {{ $route.params.id }}</div>'
}
const routes = [{ path: '/user/:id', component: User }]
// ========== 替换为 ===========
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const routes = [{ path: '/user/:id', component: User, props: true }] // 当 props 设置为 true 时,route.params 将被设置为组件的 props。
b. 对于有命名视图的路由,必须为每个命名视图定义props配置:
const routes = [
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
c. 当props是一个对象时,它将原样设置为组件props。当props是静态的时候很有用。
const routes = [
{
path: '/promotion/from-newsletter',
component: Promotion,
props: { newsletterPopup: false }
}
]
d. 你可以创建一个返回 props 的函数。这允许你将参数转换为其他类型,将静态值与基于路由的值相结合等等。
const routes = [
{
path: '/search',
component: SearchUser,
props: route => ({ query: route.query.q })
}
]
**注意:**请尽可能保持 props
函数为无状态的,因为它只会在路由发生变化时起作用。如果你需要状态来定义 props,请使用包装组件,这样 vue 才可以对状态变化做出反应。
- 推荐使用这个模式:用
createWebHistory()
创建 HTML5 模式的历史模式。
6.6 完整导航解析流程
- 导航被触发。
- 在失活的组件里调用 beforeRouteLeave 守卫。
- 调用全局的 beforeEach 守卫。
- 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
- 在路由配置里调用 beforeEnter。
- 解析异步路由组件。
- 在被激活的组件里调用 beforeRouteEnter。
- 调用全局的 beforeResolve 守卫(2.5+)。
- 导航被确认。
- 调用全局的 afterEach 钩子。
- 触发 DOM 更新。
- 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
6.7 路由元信息
const routes = [
{
path: '/posts',
component: PostsLayout,
children: [
{
path: 'new',
component: PostsNew,
// 只有经过身份验证的用户才能创建帖子
meta: { requiresAuth: true }
},
{
path: ':id',
component: PostsDetail
// 任何人都可以阅读文章
meta: { requiresAuth: false }
}
]
}
]
如何访问meta属性?
router.beforeEach((to, from) => {
// 而不是去检查每条路由记录
// to.matched.some(record => record.meta.requiresAuth)
if (to.meta.requiresAuth && !auth.isLoggedIn()) {
// 此路由需要授权,请检查是否已登录
// 如果没有,则重定向到登录页面
return {
path: '/login',
// 保存我们所在的位置,以便以后再来
query: { redirect: to.fullPath },
}
}
})
TypeScript
// typings.d.ts or router.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
// 是可选的
isAdmin?: boolean
// 每个路由都必须声明
requiresAuth: boolean
}
}
6.8 滚动行为
使用前端路由,当切换到新路由时,想要页面滚到顶部,或者是保持原先的滚动位置,就像重新加载页面那样。 vue-router 能做到,而且更好,它让你可以自定义路由切换时页面如何滚动。
注意: 这个功能只在支持 history.pushState 的浏览器中可用。
当创建一个 Router 实例,你可以提供一个 scrollBehavior
方法:
const router = createRouter({
history: createWebHashHistory(), // 推荐使用这个模式:用createWebHistory()创建 HTML5 模式的历史模式
routes: [...],
scrollBehavior(to,from,savedPosition){
// return 期望滚动到哪个位置
}
})
scrollBehavior
函数接收 to
和 from
路由对象,如 Navigation Guards。第三个参数 savedPosition
,只有当这是一个 popstate
导航时才可用(由浏览器的后退/前进按钮触发)。
该函数可以返回一个 ScrollToOptions
位置对象:
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终滚动到顶部
return { top: 0 }
},
})
也可以通过 el
传递一个 CSS 选择器或一个 DOM 元素。在这种情况下,top
和 left
将被视为该元素的相对偏移量。
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
// 始终在元素 #main 上方滚动 10px
return {
// 也可以这么写
// el: document.getElementById('main'),
el: '#main',
top: -10,
}
},
})
如果返回一个 falsy 的值,或者是一个空对象,那么不会发生滚动。
返回 savedPosition
,在按下 后退/前进 按钮时,就会像浏览器的原生表现那样:
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { top: 0 }
}
},
})
如果你要模拟 “滚动到锚点” 的行为:
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
return {
el: to.hash,
}
}
},
})
如果你的浏览器支持滚动行为,你可以让它变得更流畅:
const router = createRouter({
scrollBehavior(to, from, savedPosition) {
if (to.hash) {
return {
el: to.hash,
behavior: 'smooth',
}
}
}
})
延迟滚动
有时候,需要在页面中滚动之前稍作等待。例如,当处理过渡时,我们希望等待过渡结束后再滚动。要做到这一点,你可以返回一个 Promise,它可以返回所需的位置描述符。下面是一个例子,我们在滚动前等待 500ms:
const router = createRouter({
scrollBehavior(to,from,savedPosition){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve({left: 0, top: 0})
},500)
})
},
})
可以将其与页面级过渡组件的事件挂钩,以使滚动行为与你的页面过渡很好地结合起来,但由于使用场景可能存在的差异和复杂性,这里只是提供了这个基础来实现特定的用户场景。
6.9 路由懒加载
// 将 import UseDetails = () => import('./views/UserDetails') 替换成
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{ path: '/users/:id', component: UserDetails }],
})
component
(和 components
) 配置接收一个返回 Promise 组件的函数,Vue Router 只会在第一次进入页面时才会获取这个函数,然后使用缓存数据。这意味着你也可以使用更复杂的函数,只要它们返回一个 Promise :
const UserDetails = () =>
Promise.resolve({
/* 组件定义 */
})
一般来说,对所有的路由都使用动态导入是个好主意。
注意: 不要在路由中使用异步组件。异步组件仍然可以在路由组件中使用,但路由组件本身就是动态导入的。
如果你使用的是 webpack 之类的打包器,它将自动从代码分割中受益。
如果你使用的是 Babel,你将需要添加 syntax-dynamic-import 插件,才能使 Babel 正确地解析语法。
6.10 把组件按组分块
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):
const UserDetails = () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
const UserDashboard = () => import(/* webpackChunkName: "group-user" */ './UserDashboard.vue')
const UserProfileEdit = () => import(/* webpackChunkName: "group-user" */ './UserProfileEdit.vue')
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
6.11 导航故障
当使用 router-link
组件时,Vue Router 会自动调用 router.push
来触发一次导航。虽然大多数链接的预期行为是将用户导航到一个新页面,但也有少数情况下用户将留在同一页面上:
- 用户已经位于他们正在尝试导航到的页面
- 一个导航守卫通过调用
return false
中断了这次导航 - 当前的导航守卫还没有完成时,一个新的导航守卫会出现了
- 一个导航守卫通过返回一个新的位置,重定向到其他地方 (例如,
return '/login'
) - 一个导航守卫抛出了一个
Error
6.11.1 检测导航故障
如果导航被阻止,导致用户停留在同一个页面上,由 router.push
返回的 Promise
的解析值将是 Navigation Failure。否则,它将是一个 falsy 值(通常是 undefined
)。这样我们就可以区分我们导航是否离开了当前位置:
const navigationResult = await router.push('/my-profile')
if(navigationResult){
// 导航被阻止
} else {
// 导航成功(包括重新导航的情况)
this.isMenuOpen = false // 示例,关闭菜单栏
}
6.11.2 鉴别导航故障
import { NavigationFailureType, isNavigationFailure } from 'vue-router'
// 试图离开未保存的编辑文本界面
const failure = await router.push('/articles/2')
if (isNavigationFailure(failure, NavigationFailureType.aborted)) {
// 给用户显示一个小通知
showToast('You have unsaved changes, discard and leave anyway?')
}
6.11.3 导航故障的属性
// 正在尝试访问 admin 页面
router.push('/admin').then(failure => {
if(isNavigationFailure(failure, NavigationFailureType.redirected)){
failure.to.path // '/admin'
failure.from.path // '/'
}
})
6.11.4 检测重定向
当在导航守卫中返回一个新的位置时,我们会触发一个新的导航,覆盖正在进行的导航。与其他返回值不同的是,重定向不会阻止导航,而是创建一个新的导航。因此,通过读取路由地址中的 redirectedFrom
属性,对其进行不同的检查:
await router.push('/my-profile')
if (router.currentRoute.value.redirectedFrom) {
// redirectedFrom 是解析出的路由地址,就像导航守卫中的 to和 from
}
6.12 动态路由
对路由的添加通常是通过 routes
选项来完成的,但是在某些情况下,你可能想在应用程序已经运行的时候添加或删除路由。具有可扩展接口(如 Vue CLI UI )这样的应用程序可以使用它来扩展应用程序。
6.12.1 添加路由
动态路由主要通过两个函数实现。router.addRoute()
和 router.removeRoute()
。它们只注册一个新的路由,也就是说,如果新增加的路由与当前位置相匹配,就需要你用 router.push()
或 router.replace()
来手动导航,才能显示该新路由。我们来看一个例子:
想象一下,只有一个路由的以下路由:
const router = createRouter({
history: createWebHistory(),
routes: [{ path: '/:articleName', component: Article }],
})
进入任何页面,/about
,/store
,或者 /3-tricks-to-improve-your-routing-code
最终都会呈现 Article
组件。如果我们在 /about
上添加一个新的路由:
router.addRoute({ path: '/about', component: About })
页面仍然会显示 Article
组件,我们需要手动调用 router.replace()
来改变当前的位置,并覆盖我们原来的位置(而不是添加一个新的路由,最后在我们的历史中两次出现在同一个位置):
router.addRoute({ path: '/about', component: About })
// 我们也可以使用 this.$route 或 route = useRoute() (在 setup 中)
router.replace(router.currentRoute.value.fullPath)
记住,如果你需要等待新的路由显示,可以使用 await router.replace()
。
6.12.2 在导航守卫中添加路由
如果你决定在导航守卫内部添加或删除路由,你不应该调用 router.replace()
,而是通过返回新的位置来触发重定向:
router.beforeEach(to => {
if (!hasNecessaryRoute(to)) {
router.addRoute(generateRoute(to))
// 触发重定向
return to.fullPath
}
})
上面的例子有两个假设:第一,新添加的路由记录将与 to
位置相匹配,实际上导致与我们试图访问的位置不同。第二,hasNecessaryRoute()
在添加新的路由后返回 false
,以避免无限重定向。
因为是在重定向中,所以我们是在替换将要跳转的导航,实际上行为就像之前的例子一样。而在实际场景中,添加路由的行为更有可能发生在导航守卫之外,例如,当一个视图组件挂载时,它会注册新的路由。
6.12.3 删除路由
有几个不同的方法来删除现有的路由:
-
通过添加一个名称冲突的路由。如果添加与现有途径名称相同的途径,会先删除路由,再添加路由:
router.addRoute({ path: '/about', name: 'about', component: About }) // 这将会删除之前已经添加的路由,因为他们具有相同的名字且名字必须是唯一的 router.addRoute({ path: '/other', name: 'about', component: Other })
-
通过调用
router.addRoute()
返回的回调:const removeRoute = router.addRoute(routeRecord) removeRoute() // 删除路由如果存在的话
当路由没有名称时,这很有用。
-
通过使用
router.removeRoute()
按名称删除路由:router.addRoute({ path: '/about', name: 'about', component: About }) // 删除路由 router.removeRoute('about')
需要注意的是,如果你想使用这个功能,但又想避免名字的冲突,可以在路由中使用
Symbol
作为名字。TIP: 当路由被删除时,所有的别名和子路由也会被同时删除。
6.12.4 添加嵌套路由
要将嵌套路由添加到现有的路由中,可以将路由的 name 作为第一个参数传递给 router.addRoute()
,这将有效地添加路由,就像通过 children
添加的一样:
router.addRoute({ name: 'admin', path: '/admin', component: Admin })
router.addRoute('admin', { path: 'settings', component: AdminSettings })
这等效于:
router.addRoute({
name: 'admin',
path: '/admin',
component: Admin,
children: [{ path: 'settings', component: AdminSettings }],
})
6.12.5 查看现有路由
Vue Router 提供了两个功能来查看现有的路由:
router.hasRoute()
:检查路由是否存在。router.getRoutes()
:获取一个包含所有路由记录的数组。
七、token原理分析
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HCs13OKM-1649484576814)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1619336782848.png)]
八、.prettierrc.json
{
// 解决与ESlint的冲突
"semi":false,
"singleQuote": true,
// 换行设置
"printWidth": 200
}
九、Element-UI
1、el-row
一行有24格,通过 :span=“6” 添加每一列所占格数,使所有列格数相加等于24。
2、阻止启动生产消息
Vue.config.productionTip = false
3、改变el-input的宽高
/deep/ .el-input__inner {
width: 400px;
height: 60px;
}
改变prefix-icon大小:
/deep/.el-icon-user:before {
font-size: 18px;
}
4、$nextTick
当页面上元素被重新渲染之后,才会执行回调函数中的代码。
this.$nextTick((_) => {
this.$refs.saveTagInput.$refs.input.focus()
})
5、上传图片时携带token
// axios
// 确认上传功能
submitUpload() {
this.$refs.videoFormRef.validate(async (valid) => {
if (!valid || !this.imgInfo.name || !this.videoInfo.name) {
this.$message.error('请填写完整的表单数据')
} else {
let params = 'source=' + this.videoForm.uploadPathValue + '&videoName=' + this.videoForm.fileName
// 创建空的formData表单对象
var formData = new FormData()
// 将用户选择的文件追加到formData表单对象中
formData.append('file', this.imgInfo)
formData.append('video', this.videoInfo)
const { data: res } = await this.$http.put('file/video?' + params, formData)
if (res.status !== 200) return this.$message.error('上传视频失败,请重新登录后尝试')
this.$message.success('上传视频成功')
this.$router.push('/video')
}
})
},
// 自定义上传路径
this.$http({
method: 'put',
url: 'https://acfly.cn/fibreboard/message/poster',
data: formData,
})
.then((res) => {
if (res.status !== 200) return this.$message.error('上传海报失败')
this.$message.success('上传海报成功')
})
.catch((error) => {
console.log(error)
})
6、自行注册Element组件
// element.js
import Timeline from './timeline/index.js'
import TimelineItem from './timeline-item/index.js'
Vue.use(Timeline)
Vue.use(TimelineItem)
// 使用的.vue文件
<style lang="less" scoped>
@import '../../plugins/timeline/timeline.css';
@import '../../plugins/timeline-item/timeline-item.css';
</style>
7、echarts
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gVZbsa2a-1649484576814)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1623144196310.png)]
// 1.导入echarts
import * as echarts from 'echarts'
let echarts from 'echarts'
// 2.<!-- 为ECharts准备一个具备大小(宽高)的Dom -->
<div id="main" style="width: 600px; height: 400px"></div>
// 3.基于准备好的dom,初始化echarts实例
mounted() {
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'))
},
const { data: res } = await this.$http.get(`reports/type/1`)
// 4.准备数据和配置项
const result = _.merge(res.data, this.options)
// 5.展示数据
myChart.setOption(result)
// 配置项
options: {
title: {
text: '用户来源'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#E9EEF3'
}
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: [
{
boundaryGap: false
}
],
yAxis: [
{
type: 'value'
}
]
}
十、vue项目初始化
1、vue ui
2、创建新项目 --> 选择文件夹 --> 填写项目名、init project
3、安装插件:vue-cli-plugin-element --> 配置插件: import on demand
4、依赖:axios --> less@4.1.1 --> less-loader@5.0.0 --> .prettierrc.json
5、添加公钥:设置–> SSH公钥 ssh-keygen -t rsa -C “xxxxx@xxxxx.com” --> 黏贴生成SSH --> ssh -T git@gitee.com
6、新建仓库:去掉使用Readme文件对勾 -->
git config --global user.name "AhhC"
git config --global user.email "2388639967@qq.com"
–> 根据仓库指令绑定仓库
git多公钥ssh连接多仓库
1、本地新建创建多个ssh公钥
通过git bash打开命令行
进入的ssh公钥配置目录
cd ~/.ssh
2、新建新的ssh公钥
//新建demo@gmail.com的demo公钥
ssh-keygen -t rsa -C “demo@gmail.com” -f demo
注意:实际的邮件地址和-f后面的公钥名称demo根据自己的情况命名,邮件地址跟ssh私钥的能对应就OK了
3、新建config
//新建config
touch config
4、编辑config
config配置信息如下:
默认的git配置,如gitee.com,Host是主机可自行定义名称
Host gitee.com
HostName gitee.com
User deme1@gmail.com
IdentityFile ~/.ssh/id_rsa
Host gitee.demo.com
HostName gitee.com
User demo2@gmail.com
IdentityFile ~/.ssh/demo
5、把配置的ssh公钥复制到git对应后台生成私钥
git地址的变化
git地址,如:git@gitee.com:demo/test.git
按照config的配置:Host:gitee.com,git clone命令如下:
git clone git@gitee.com:demo/test.git
其中Host:gitee.demo.com的git地址如下:
git clone git@gitee.demo.com:demo/test.git
注:第二个git的地址,跟你实际获得的git地址有所不同,主要根据config配置文件的Host来遍历具体的git地址及用户和公钥等信息
注意:vue-cli的项目名称不能包含大写
十二、项目优化
12.1 项目优化策略
- 生成打包报告
- 第三方库启用CDN
- ElementUI组件按需加载
- 路由懒加载
- 首页内容定制
12.2 nprogress
1、vue-cli下载运行依赖
2、main.js导入文件
import NProgress from 'nprogress'
import 'nprogress/nprogress.css'
3、// 在request拦截器中,展示进度条
axios.interceptors.request.use(config=>{
NProgress.start()
config.headers.Authorization = window.sessionStorage.getItem('token');
return config;
})
// 在response拦截器中,隐藏进度条
axios.interceptors.response.use(config=>{
NProgress.done()
return config
})
12.3 去除console语句
安装开发依赖:babel-plugin-transform-remove-console
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uaGUDPSa-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620719853313.png)]
// 由于babel.config.js是全局配置,所以不论开发环境还是生产环境都会启用依赖,所以进行优化:
// 这是项目发布阶段需要用到的babel插件
const prodPlugins = []
if(process.env.NODE_ENV === 'production'){
prodPlugins.push('transform-remove-console')
}
module.exports = {
"presets": [
"@vue/app"
],
"plugins": [
[
"component",
{
"libraryName": "element-ui",
"styleLibraryName": "theme-chalk"
}
],
// 发布产品时候的插件数组
...prodPlugins
]
}
12.4 生产打包报告
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-p9oPepVJ-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720317690.png)]
12.5 通过 vue.config.js 修改Webpack的默认配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWQSZYaF-1649484576815)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720536402.png)]
12.6 为开发模式与发布模式指定不同的打包入口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l1VStHcP-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720628257.png)]
configureWebpack和chainWebpack:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3kUfKr7A-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620720807316.png)]
12.7 通过chainWebpack自定义打包入口
module.exports = {
lintOnSave: false,
chainWebpack: config => {
config.when(process.env.NODE_ENV === 'production', config => {
config.entry('app').clear().add('./src/main-prod.js')
})
config.when(process.env.NODE_ENV === 'development', config => {
config.entry('app').clear().add('./src/main-dev.js')
})
}
}
12.8 通过externals加载外部CDN资源
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKJqihzt-1649484576816)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1620999997546.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mbdjKQGV-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000014104.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gd4HELO3-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000026720.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RronYpLq-1649484576817)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1621000034961.png)]
12.9 通过CDN优化ElementUI的打包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FoVIzUAe-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624158584055.png)]
<!-- 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>
12.10 自定义首页内容
chainWebpack: config => {
// 发布模式
config.when(process.env.NODE_ENV === 'production', config => {
config.entry('app').clear().add('./src/main-prod.js')
config.plugin('html').tap(args => {
args[0].isProd = true
return args
})
})
// 开发模式
config.when(process.env.NODE_ENV === 'development', config => {
config.entry('app').clear().add('./src/main-dev.js')
config.plugin('html').tap(args => {
args[0].isProd = false
return args
})
})
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D8LY44Bi-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624159357320.png)]
12.11 实现路由懒加载
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsjLuG9G-1649484576818)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624159760135.png)]
关于路由懒加载的详细文档,可参考如下链接:
https://router.vuejs.org/zh/guide/advanced/lazy-loading.html
// babel.config.js
"plugins": ['@babel/plugin-syntax-dynamic-import']
// router.js
// 同一个chunkName表示会打包到同一个JS文件中
const Login = () => import(/* webpackChunkName: "login_home_welcome" */ './components/Login.vue')
十三、常用方法
13.1 自定义时间格式处理过滤器
Vue.filter('dataFormat',function(originVal){
const dt = new Date(originVal)
const y = dt.getFullYear()
const m = (dt.getMonth() + 1 + '').padStart(2,'0')
const d = (dt.getDate() + '').padStart(2,'0')
const hh = (dt.getHours() + '').padStart(2,'0')
const mm = (dt.getMinutes() + '').padStart(2,'0')
const ss = (dt.getSeconds() + '').padStart(2,'0')
return `${y}-${m}-${d} ${hh}:${mm}:${ss}`
})
注:或者用moment插件
13.2 下载Excel文件
exportFile(event) {
event.preventDefault() //使a自带的方法失效,即无法调整到href中的URL
this.$http({
method: 'get',
url: 'https://www.jingyun.club/background/message/export',
responseType: 'blob',
})
.then((res) => {
const link = document.createElement('a')
let blob = new Blob([res.data], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
console.log(res)
//获取heads中的filename文件名
var fileName = '信息收集列表'
link.style.display = 'none'
link.href = URL.createObjectURL(blob)
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
})
.catch((error) => {
console.log(error)
})
},
13.3 a标签携带token
//方式1
$(".a_post").on("click",function(event){
event.preventDefault();//使a自带的方法失效,即无法调整到href中的URL
var url='http://localhost:8050/file/export/snapEventVO'; //请求的URl
var xhr = new XMLHttpRequest(); //定义http请求对象
xhr.open("POST", url, true);
xhr.setRequestHeader("Content-type","application/x-www-form-urlencoded");
xhr.send();
xhr.responseType = "blob"; // 返回类型blob
xhr.onload = function() { // 定义请求完成的处理函数,请求前也可以增加加载框/禁用下载按钮逻辑
if (this.status===200) {
var blob = this.response;
//alert(this.readyState);
//alert(xhr.getAllResponseHeaders());
console.log(xhr.getResponseHeader("content-disposition"))
let temp = xhr.getResponseHeader("content-disposition").split(";")[1].split("filename=")[1];
var fileName = decodeURIComponent(temp);
//var hh = xhh.getResponseHeader("fileName");
//var fileName = this.response.headers["content-disposition"].split(";")[1].split("filename=")[1];
//console.log("fileName="+fileName)
//console.log(xhr.getResponseHeader("content-disposition"))
var reader = new FileReader();
reader.readAsDataURL(blob); // 转换为base64,可以直接放入a标签href
reader.οnlοad=function (e) {
console.log(e); //查看有没有接收到数据流
// 转换完成,创建一个a标签用于下载
var a = document.createElement('a');
a.download=fileName+".xlsx"; //自定义下载文件名称
a.href = e.target.result;
$("body").append(a); // 修复firefox中无法触发click
a.click();
//$(a).remove();
}
}
else{
alert("出现了未知的错误!");
}
}
});
//方式2
$(".a_post").on("click",function(event){
event.preventDefault();//使a自带的方法失效,即无法调整到href中的URL
axios({
method: 'post',
url: "http://localhost:8050/file/export/snapEventVO",
responseType: 'blob'
}).then((res) => {
const link = document.createElement('a')
let blob = new Blob([res.data],{type: 'application/vnd.ms-excel'});
//获取heads中的filename文件名
var aa = res.headers["content-disposition"]
let temp = res.headers["content-disposition"].split(";")[1].split("filename=")[1];
var fileName = decodeURIComponent(temp);
console.log(fileName)
link.style.display = 'none'
link.href = URL.createObjectURL(blob);
link.setAttribute('download', fileName)
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}).catch(error => {
console.log(error)
})
});
十四、vue-quill-editor
安装:npm install vue-quill-editor --save
main.js文件中
// 导入富文本编辑器
import VueQuillEditor from 'vue-quill-editor'
// require styles
import 'quill/dist/quill.core.css'
import 'quill/dist/quill.snow.css'
import 'quill/dist/quill.bubble.css'
// 将富文本编辑器注册为全局可用的组件
Vue.use(VueQuillEditor)
// ---------------使用: 客户端SPA--------------
<!-- 富文本编辑器组件 -->
<quill-editor v-model="addForm.goods_introduce"></quill-editor>
// 在全局样式表中更改样式,局部样式更改无效
.ql-editor {
min-height: 300px;
}
十五、lodash
// 在所需组件中引用
<script>
import _ from 'lodash'
// 1.深拷贝
const form = _.cloneDeep(this.addForm)
// 2.对象合并
const result = _.merge(res.data, this.options)
</script>
// JSON方法,只要不是null和undefined就不会报错
JSON.parse(JSON.stringify(this.addForm))
十六、项目上线
16.1 通过node创建web服务器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dC9AVbir-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624160978907.png)]
16.2 开启 gzip 配置
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-l9aCOCOI-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161155639.png)]
16.3 配置HTTPS服务(默认端口 443)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RtvZUkgi-1649484576819)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161273229.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KmPNDsT-1649484576820)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161310207.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-manv9AHQ-1649484576821)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161425842.png)]
16.4 使用pm2管理应用(关闭服务器终端后仍可以访问)
在服务器中写
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7V0SLPG2-1649484576821)(C:\Users\LENOVO\AppData\Roaming\Typora\typora-user-images\1624161748906.png)]
十七、Vue-Router:vue2迁移到vue3(即vue-router3到vue-router4)
1、new Router 变成 createRouter
// 以前是
// import Router from 'vue-router'
import { createRouter } from 'vue-router'
// const router = new Router({})
const router = createRouter({
// ...
})
2、新的history
配置代替mode
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory(),
routes: []
})
在 SSR 上使用时,你需要手动传递相应的 history:
// router.js
let history = isServer ? createMemoryHistory() : createWebHistory()
let router = createRouter({ routes, history })
// 在你的server-entry.js 中的某个地方
router.push(req.url) // 请求url
router.isReady().then(()=>{
// 处理请求
})
原因:为未使用的 history 启用摇树,以及为高级用例(如原生解决方案)实现自定义 history。
3、移动了base配置
// 原来
new Router({
mode: 'history', // 访问路径不带#号
base: '/page/aa', // 配置单页应用的基路劲
})
// 这时,页面访问 http://localhost:8080/page/aa 和 http://localhost:8080/的效果是一样的。
import { createRouter, createWebHistory } from 'vue-router'
createRouter({
history: createWebHistory('/base-directory/'),
routes: [],
})
4、删除了 RouterOptions 中的 fallback属性
创建路由时不再支持fallback属性:
-new VueRouter({
+createRouter({
- fallback: false,
// other options...
})
原因: Vue支持的所有浏览器都支持 HTML5 History API,因此我们不再需要使用 location.hash
,而可以直接使用 history.pushState()
。
5、删除了*通配符路由
现在必须使用自定义的 regex 参数来定义所有路由(*
、/*
):在参数中自定义正则
onst routes = [
// pathMatch 是参数的名称,例如,跳转到 /not/found 会得到
// { params: { pathMatch: ['not', 'found'] } }
// 这要归功于最后一个 *,意思是重复的参数,如果你
// 打算直接使用未匹配的路径名称导航到该路径,这是必要的
{ path: '/:pathMatch(.*)*', name: 'not-found', component: NotFound },
// 如果你省略了最后的 `*`,在解析或跳转时,参数中的 `/` 字符将被编码
{ path: '/:pathMatch(.*)', name: 'bad-not-found', component: NotFound },
]
// 如果使用命名路由,不好的例子:
router.resolve({
name: 'bad-not-found',
params: { pathMatch: 'not/found' },
}).href // '/not%2Ffound'
// 好的例子:
router.resolve({
name: 'not-found',
params: { pathMatch: ['not', 'found'] },
}).href // '/not/found'
原因:Vue Router 不再使用 path-to-regexp
,而是实现了自己的解析系统,允许路由排序并实现动态路由。由于我们通常在每个项目中只添加一个通配符路由,所以支持 *
的特殊语法并没有太大的好处。参数的编码是跨路由的,无一例外,让事情更容易预测。
6、将 onReady 改为 isReady
现有的 router.onReady()
函数已被 router.isReady()
取代,该函数不接受任何参数并返回一个 Promise:
// 将
router.onReady(onSuccess, onError)
// 替换成
router.isReady().then(onSuccess).catch(onError)
// 或者使用 await:
try {
await router.isReady()
// 成功
} catch (err) {
// 报错
}
7、<router-view>
、<keep-alive>
和<transition>
transition
和 keep-alive
现在必须通过 v-slot
API 在 RouterView
内部使用:
<router-view v-slot="{ Component }">
<transition>
<keep-alive>
<component :is="Component" />
</keep-alive>
</transition>
</router-view>
原因: 这是一个必要的变化。
8、删除 <router-link>
中的event
和tag
属性
<router-link>
中的 event
和 tag
属性都已被删除。你可以使用 v-slot
API 来完全定制 <router-link>
:
将
<router-link to="/about" tag="span" event="dblclick">About Us</router-link>
替换成
<router-link to="/about" custom v-slot="{ navigate }">
<span @click="navigate" @keypress.enter="navigate" role="link">About Us</span>
</router-link>
原因:这些属性经常一起使用,以使用与 <a>
标签不同的东西,但这些属性是在 v-slot
API 之前引入的,并且没有足够的使用,因此没有足够的理由为每个人增加 bundle 包的大小。
9、忽略mixins中的导航守卫
目前不支持mixins中的导航守卫,可以在 vue-router#454 追踪它的支持情况。
10、所有的导航都是异步的
所有的导航,包括第一个导航,现在都是异步的,这意味着,如果你使用一个 transition
,你可能需要等待路由 ready 好后再挂载程序:
app.use(router)
// 注意:在服务器端,你需要手动跳转到初始地址。
router.isReady().then(() => app.mount('#app'))
否则会有一个初始过渡,就像你提供了 appear
属性到 transition
一样,因为路由会显示它的初始地址(什么都没有),然后显示第一个地址。
请注意,如果在初始导航时有导航守卫,你可能不想阻止程序渲染,直到它们被解析,除非你正在进行服务器端渲染。否则,在这种情况下,不等待路由准备好挂载应用会产生与 Vue2 中相同的结果。
11、将内容传递给路由组件的 <slot>
之前可以直接传递一个模板,通过嵌套在 <router-view>
组件下,由路由组件的 <slot>
来渲染:
<router-view>
<p>In Vue Router 3, I render inside the route component</p>
</router-view>
由于 <router-view>
引入了 v-slot
API,你必须使用 v-slot
API 将其传递给 <component>
:
<router-view v-slot="{ Component }">
<component :is="Component">
<p>In Vue Router 3, I render inside the route component</p>
</component>
</router-view>
12、不存在的命名路由 || 命名路由缺少必要的params
- 跳转或解析不存在的命名路由会产生错误:
// 哎呀,我们的名字打错了
router.push({ name: 'homee' }) // 报错
router.resolve({ name: 'homee' }) // 报错
原因:以前,路由会导航到 /
,但不显示任何内容(而不是主页)。抛出一个错误更有意义,因为我们不能生成一个有效的 URL 进行导航。
- 在没有传递所需参数的情况下跳转或解析命名路由,会产生错误:
// 给与以下路由:
const routes = [{ path: '/users/:id', name: 'user', component: UserDetails }]
// 缺少 `id` 参数会失败
router.push({ name: 'user' })
router.resolve({ name: 'user' })
十八、Provide / Inject
1. 概述
对于这种情况,我们可以使用一对 provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这些数据。
例如我们有这样的层次结构:
Root
└─ TodoList
├─ TodoItem
└─ TodoListFooter
├─ ClearTodosButton
└─ TodoListStatistics
如果要将 todo-items 的长度直接传递给 TodoListStatistics
,我们可以用provide/inject的方式,直接执行以下操作:
const app = Vue.createApp({})
app.component('todo-list', {
data(){
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
user: 'John Doe'
},
template: `
<div>
{{ todos.length }}
</div>
`
})
app.component('todo-list-statistics', {
inject: ['user'],
created() {
console.log(`Injected property: ${this.user}`) // > 注入的 property: John Doe
}
})
但是,如果尝试在此处 provide 一些组件的实例 property,这将是不起作用的:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide: {
todoLength: this.todos.length // 将会导致错误 `Cannot read property 'length' of undefined`
},
template: `
...
`
})
要访问组件实例property,我们需要将provide转换为返回对象的函数:
app.component('todo-list', {
data() {
return {
todos: ['Feed a cat', 'Buy tickets']
}
},
provide() {
return {
todoLength: this.todos.length
}
},
template: `
...
`
})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。
2. 处理响应性
在上面的例子中,如果我们更改了 todos
的列表,这个变化并不会反映在 inject 的 todoLength
property 中。这是因为默认情况下,provide/inject
绑定并不是响应式的。我们可以通过传递一个 ref
property 或 reactive
对象给 provide
来改变这种行为。在我们的例子中,如果我们想对祖先组件中的更改做出响应,我们需要为 provide 的 todoLength
分配一个组合式 API computed
property:
app.component('todo-list', {
// ...
provide() {
return {
todoLength: Vue.computed(() => this.todos.length)
}
}
})
app.component('todo-list-statistics', {
inject: ['todoLength'],
created() {
console.log(`Injected property: ${this.todoLength.value}`) // > 注入的 property: 5
}
})
在这种情况下,任何对 todos.length
的改变都会被正确地反映在注入 todoLength
的组件中。
十九、渲染函数
1. 概述
假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
锚点标题的使用非常频繁,我们应该创建一个组件:
<anchored-heading :level="1">Hello world!</anchored-heading>
1
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:
const { createApp } = Vue
const app = createApp({})
app.component('anchored-heading', {
template: `
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
`,
props: {
level: {
type: Number,
required: true
}
}
})
这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>
。当我们添加锚元素时,我们必须在每个 v-if/v-else-if
分支中再次重复它。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
const { createApp, h } = Vue
const app = createApp({})
app.component('anchored-heading', {
render() {
return h(
'h' + this.level, // 标签名
{}, // prop 或 attribute
this.$slots.default() // 包含其子节点的数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
render()
函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。
2. 虚拟 DOM 树
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return h('h1', {}, this.blogTitle)
h()
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
// @returns {VNode}
h(
// {String | Object | Function} tag
// 一个 HTML 标签名、一个组件、一个异步组件、或
// 一个函数式组件。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 这会在模板中用到。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 VNode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
如果没有 prop,那么通常可以将 children 作为第二个参数传入。如果会产生歧义,可以将 null
作为第二个参数传入,将 children 作为第三个参数传入。
约束
VNodes必须唯一,若要创建重复很多次的元素/组件,可以使用工厂函数来实现,例如:
render(){
return h('div',
Array.from({length: 20}).map(()=> {
return h('p', 'hi')
})
)
}
3. 创建组件VNode
要为某个组件创建一个 VNode,传递给 h
的第一个参数应该是组件本身。
render() {
return h(ButtonCounter)
}
如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent
:
const { h, resolveComponent } = Vue
// ...
render() {
const ButtonCounter = resolveComponent('ButtonCounter')
return h(ButtonCounter)
}
resolveComponent
是模板内部用来解析组件名称的同一个函数。
4. 使用 JavaScript 代替模板功能
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 prop:
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
v-on
我们必须为事件处理程序提供一个正确的 prop 名称,例如,要处理 click
事件,prop 名称应该是 onClick
。
render(){
return h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}
事件修饰符
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
render() {
return h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop |
event.stopPropagation() |
.prevent |
event.preventDefault() |
.self |
if (event.target !== event.currentTarget) return |
按键: .enter , .13 |
if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta |
if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
这里是一个使用所有修饰符的例子:
render() {
return h('input', {
onKeyUp: event => {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果向上键不是回车键,则终止
// 没有同时按下按键 (13) 和 shift 键
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
})
}
插槽
你可以通过 this.$slots 访问静态插槽的内容,每个插槽都是一个VNode数组:
render() {
// `<div><slot></slot></div>`
return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
// `<div><slot :text="message"></slot></div>`
return h('div', {}, this.$slots.default({
text: this.message
}))
}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
const { h, resolveComponent } = Vue
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return h('div', [
h(
resolveComponent('child'),
{},
// 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
{
default: (props) => h('span', props.text)
}
)
])
}
插槽以函数的形式传递,允许子组件控制每个插槽内容的创建。任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。相反,对 resolveComponent
的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
// 应该是在插槽函数外面调用 resolveComponent。
const Button = resolveComponent('MyButton')
const Icon = resolveComponent('MyIcon')
return h(
Button,
null,
{
// 使用箭头函数保存 `this` 的值
default: (props) => {
// 响应式 property 应该在插槽函数内部读取,
// 这样它们就会成为 children 渲染的依赖。
return [
h(Icon, { name: this.icon }),
this.text
]
}
}
)
}
如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件。
render() {
return h(Panel, null, this.$slots)
}
也可以根据情况单独传递或包裹住。
render() {
return h(
Panel,
null,
{
// 如果我们想传递一个槽函数,我们可以通过
header: this.$slots.header,
// 如果我们需要以某种方式对插槽进行操作,
// 那么我们需要用一个新的函数来包裹它
default: (props) => {
const children = this.$slots.default ? this.$slots.default(props) : []
return children.concat(h('div', 'Extra child'))
}
}
)
}
<component>
和<is>
在底层实现里,模板使用 resolveDynamicComponent
来实现 is
attribute。如果我们在 render
函数中需要 is
提供的所有灵活性,我们可以使用同样的函数:
const { h, resolveDynamicComponent } = Vue
// ...
// `<component :is="name"></component>`
render() {
const Component = resolveDynamicComponent(this.name)
return h(Component)
}
就像 is
, resolveDynamicComponent
支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。
通常这种程度的灵活性是不需要的。通常 resolveDynamicComponent
可以被换做一个更直接的替代方案。
例如,如果我们只需要支持组件名称,那么可以使用 resolveComponent
来代替。
如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h
:
// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
return h(this.bold ? 'strong' : 'em')
}
同样,如果传递给 is
的值是一个组件选项对象,那么不需要解析什么,可以直接作为 h
的第一个参数传递。
与 <template>
标签一样,<component>
标签仅在模板中作为语法占位符需要,当迁移到 render
函数时,应被丢弃。
自定义指令
可以使用 withDirectives
将自定义指令应用于 VNode:
const { h, resolveDirective, withDirectives } = Vue
// ...
// <div v-pin:top.animate="200"></div>
render () {
const pin = resolveDirective('pin')
return withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
}
resolveDirective
是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。
内置组件
诸如 <keep-alive>
、<transition>
、<transition-group>
和 <teleport>
等内置组件默认并没有被全局注册。这使得打包工具可以 tree-shake,因此这些组件只会在被用到的时候被引入构建。不过这也意味着我们无法通过 resolveComponent
或 resolveDynamicComponent
访问它们。
在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。当我们编写自己的 render
函数时,需要自行导入它们:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
return h(Transition, { mode: 'out-in' }, /* ... */)
}
5. 渲染函数的返回值
render
函数返回的是单个根 VNode。但其实也有别的选项。
返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素:
render() {
return 'Hello world!'
}
我们也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment):
// 相当于模板 `Hello<br>world!`
render() {
return [
'Hello',
h('br'),
'world!'
]
}
可能是因为数据依然在加载中的关系,组件不需要渲染,这时它可以返回 null
。这样我们在 DOM 中会渲染一个注释节点。
6. JSX
如果你写了很多渲染函数,可能会觉得下面这样的代码写起来很痛苦:
h(
'anchored-heading',
{
level: 1
},
{
default: () => [h('span', 'Hello'), ' world!']
}
)
特别是对应的模板如此简单的情况下:
<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
app.mount('#demo')
7. 函数式组件
函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。
我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 render
函数。而因为函数式组件里没有 this
引用,Vue 会把 props
当作第一个参数传入:
const FunctionalComponent = (props, context) => {
// ...
}
第二个参数 context
包含三个 property:attrs
、emit
和 slots
。它们分别相当于实例的 $attrs
、$emit
和 $slots
这几个 property。
大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 props
和 emits
作为 property 加入,以达到定义它们的目的:
FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']
如果这个 props
选项没有被定义,那么被传入函数的 props
对象就会像 attrs
一样会包含所有 attribute。除非指定了 props
选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。
函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h
,它将会被当作一个函数式组件来对待。