第六部分:Vue组件基础
组件是可复用的 Vue 实例
组件化开发指将一个完整的页面,抽离成一个个独立的组件,最终,通过这一个个独立组件完成整个的页面(项目)的功能.
组件化开发的优势/作用 : 复用
1.组件的基本使用(先注册、再使用)
1.1全局组件注册和使用
注册全局组件
Vue.component('child', {
template: `<h1 class="red">这是child组件</h1>`
})
//或者
<template id="one">
<div>
<h1>hello,world</h1>
</div>
</template>
Vue.component('child', {
template: '#one'
})
/**
* 第一个参数 : 组件名(组件命名使用短横线连接(my-component)或者驼峰式(myComponent),使用时转化为短横线)
* 第二个参数 : 是一个配置对象, 该配置对象与 Vue 实例的配置对象几乎完全相同
* 也就是说,vue实例中用到的配置项,和组件中的配置项几乎相同,(el项换成template项)
*/
复制代码
其实这一步简化了一下:
// 1.创建组件构造器对象
const cnpc = Vue.extend({
template: `
<div>
<h1>我是标题</h1>
<P>我是内容</p>
</div>`
});
/*
2.注册组件:传入组件标签名和组件构造器对象
这种注册方法是全局注册的全局组件,可以在多个vue实例下面使用
*/
Vue.component("myComponentOne", cnpc);
复制代码
使用全局组件
<div id="app">
<!-- 3.使用组件 -->
<my-component-one"></my-component-one>
</div>
复制代码
1.2.局部注册和使用
每个组件实例中都有components属性,我们可以在里面注册局部组件。注册后的组件只有在该实例的作用域下有效。
const vm = new Vue({
el: "#app",
//语法糖注册局部组件
components: {
"my-component-two": {
template: `
<div>
<h1>我是标题</h1>
<P>我是内容</p>
</div>`,
},
},
});
复制代码
当然,可以把components里面的组件内容抽离出来定义在script标签里面。
如果组件里的template
里面的结构如果太过复杂,可以把它分离出来:在script外面定义。
<div id="app">
<my-button></my-button>
</div>
<template id="button">
<div>
{{msg}}
</div>
</template>
<script>
let myButton = {
data() {
return { msg: "aaa" };
},
template: "#button",
};
vm = new Vue({
el: "#app",
data: {},
components: {
myButton,
},
});
</script>
复制代码
使用局部组件
<div id="app">
<!-- 3.使用组件 -->
<my-component-two> </my-component-two>
</div>
复制代码
1.3 组件的data为啥是一个函数?
- 组件是可复用的vue实例,一个组件被创建好之后,就可能被用在各个地方,而组件不管被复用了多少次,组件中的data数据都应该是相互隔离,互不影响的。
- 如果组件的data是引用类型的对象,当复用组件时,都指向同一个堆内存,所有的复用组件操作同一个数据。
- 而组件的data如果是一个函数,且返回值是一个对象,每次复用组件都会调用data函数,形成独立的数据存储空间。
2.组件通信
2.1 父组件通过props
向子组件传递信息(数据)
- 子组件使用props来接收父组件传递过来的数据,在props中定义的属性,作用于组件本身,可以在template、computed、methods中直接使用。
- 具体步骤是:在父组件(father)里面的子组件(son)标签中通过v-bind绑定父组件中的数据;然后在子组件(son)的实例选项中通过props接收数据。
<div id="app">
<!--
第一步:父组件中通过v-bind绑定数据
组件使用中使用不能用驼峰命名
-->
<son :father-name="name"></son>
</div>
<template id="son">
<div>
<h1>我是子组件</h1>
<h2>{{fatherName}}</h2>
</div>
</template>
<script>
const son = {
template: "#son",
//第二步:子组件通过props接收数据
props: ["fatherName"],
};
// 跟组件可以看成父组件
const vm = new Vue({
el: "#app",
components: {
son,
},
data: {
name: "我是跟组件,也是父组件",
},
});
</script>
复制代码
props特点:
- 单向传递:Vue 使用单向数据流,就是父组件的数据变化时会传递给子组件,反过来则不行。这是为了不让子组件无意修改父组件的状态。
- 若果确实想要修改可以把props里面的属性,保存在自己的data里面。
- props的值有两种,一种是字符串,一种是对象
//第一种 props:['name','number','msg'] //第二种 props:{ msg:{ type:Object, default(){ return{} } } } 复制代码
- 在模板中使用props名称要换成短横线
2.2 子组件通过$emit()
向父组件触发事件(并携带数据)
- 子组件通过$emit()触发事件,父组件在子组件的自定义标签上用v-on监听子组件触发的自定义事件。
- 父组件使用子组件时,绑定父组件的方法(
<son @parentsay="say"></son>
),父组件的方法中接受子组件传递过来的数据.- 在子组件中绑定一个事件
<button @click="sonSay">儿子说</button>
,在子组件的事件函数中发送事件,并传递子组件的参数。
sonSay() { this.$emit("parentsay", this.msg); }, 复制代码
2.3 组件间通信-自定义事件(EventBus)
定义一个空的Vue实例作为中央事件总线(事件中心),用它来触发事件和监听事件,巧妙而轻量地实现了任何组件间的通信,包括父子、兄弟、跨级。当我们的项目比较大时,可以选择更好的状态管理解决方案vuex。
具体实现方式:
var Event=new Vue(); Event.$emit(事件名,数据); Event.$on(事件名,data => {}); 复制代码
<div id="app">
<my-a></my-a>
<my-b></my-b>
</div>
<template id="a">
<div>
<h3>A组件:{{name}}</h3>
<button @click="send">a组件将数据发送给b组件</button>
</div>
</template>
<template id="b">
<div>
<h3>B组件:{{name}}</h3>
</div>
</template>
<script>
//第一步:定义一个空的Vue实例,作为事件总线
var Event = new Vue();
var A = {
template: "#a",
data() {
return {
name: "tom",
};
},
methods: {
send() {
/*
第2步:将a组建的数据发给b,点击send就会发送一个名叫add的事件
第一个参数为事件名,第二个参数为当前组件的数据
*/
Event.$emit("add", this.name);
},
},
};
var B = {
template: "#b",
data() {
return {
name: "",
};
},
methods: {
addText(name) {
console.log(name);
this.name = name;
},
},
mounted() {
//第三步:在b组件的mounted事件中接收事件
//第一个参数是事件名,第二个是回调函数(为了方便解绑又重新命名了)
Event.$on("add", this.addText);
},
beforeDestroy() {
//第四步:解绑事件,防止内存泄漏
Event.$off("add", this.addText);
},
};
var vm = new Vue({
el: "#app",
components: {
"my-a": A,
"my-b": B,
},
});
</script>
复制代码
第一步:直接在项目中的 main.js 初始化 EventBus
// Vue.prototype.$EventBus = new Vue()
new Vue({
el: "#root",
render: (h) => h(App),
beforeCreate(){
Vue.prototype.$EventBus = this
}
});
利用他的三个方法:$on() $emit() $off()
第二步:通过$on()给vm绑定事件监听
第三步:通过$emit()来分发事件
复制代码
3. 组件的生命周期
单个组件的生命周期
分为挂载阶段、更新阶段、销毁阶段
挂载阶段
created
钩子函数执行表示实例初始化完成了,可以获得data数据,但Vue 实例使用的根 DOM 元素el还未初始化
beforeMount
执行时:data和el均已经初始化,但此时el并没有渲染进数据,el的值为“虚拟”的元素节点.
mounted
钩子函数执行表示界面已经渲染完毕并挂载到实例上,可以做一些ajax获取信息,绑定事件、dom操作等操作.更新阶段
只有数据变化才会调用beforeUpdata
和upDated
,beforeUpdate执行表示el中的数据已经跟新完了,而updated触发时,表示el中的数据已经渲染完成,组件dom被更新。销毁阶段
beforeDestroy
钩子函数表示销毁前准备,我们在之里面可以,解除自定义事件绑定、销毁子组件、事件监听器、定时器等所有的生命周期钩子自动绑定 this 上下文到实例中,所以不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos()),会导致this指向父级。
带有父子组件的生命周期
created创建阶段(实例初始化阶段),是先创建父组件,再创建子组件。
mounted渲染阶段是先保证子组件渲染完,再来渲染父组件父beforeCreate->父created->父beforeMount->子beforeCreate->子created->子beforeMount->子mounted->父mounted
更新阶段是:父组件先数据先被被修改(先触发更新),子组件再被修改(再触发更新),子组件更新完了(渲染),父组件才说自己更新完了(渲染)
父beforeUpdate->子beforeUpdate->子updated->父updated
销毁阶段是:
父beforeDestroy->子beforeDestroy->子destroyed->父destroyed
4. slot分发内容
- 父组件想要往子组件的标签中插入一些东西,就会用到slot插槽。
- slot 通俗的理解就是“占坑”,用插槽在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置)
基本使用
插槽的使用很简单,分两步:
第一步:在子组件的模板中写上插槽标签
<slot>默认内容</slot>
;
第二步:父组件使用子组件时,在子组件的标签中写上想插入的内容,如<one>hello,蔡徐坤</one>
作用域:父组件中的,slot分发的内容(给slot填坑的)作用域是父组件,他可以直接绑定父组件中的方法和数据
具名插槽
- 顾名思义,具名插槽就是具有名字的插槽,若果父组件想给子组件分发多个内容,需要在子组件中挖多个坑,这时候就需要用到具名插槽。
具名插槽使用步骤:
1.子组件中如果用到多个插槽可以用插槽标签的name给它取名字。并放在相应位置
2.父组件中写入想要插入的内容,在最外层标签(可以用template标签)写上:v-slot:插槽名
,v-slot:
可以简写为#
。如v-slot:header=’slotProps’可以简写为#header='slotProps'
作用域插槽
- 父组件插入子组件插槽的内容板块,只能访问父组件的数据和方法,若果想要访问子组件的数据和方法,需要用到作用于插槽。
步骤:
第一步:给子组件的slot插槽绑定要传输的属性
<slot name="footer" v-bind:user="name"></slot>
第二步:父组件中用v-slot设置一个值来定义我们提供插槽的名字:
v-slot:footer="slotProps"
第三步:通过
slotProps.user
就可以使用数据了
独占默认插槽缩写
当只使用一个默认插槽时,我们想要传子组件的数据,可以把子组件标签作为外层元素使用,写上v-slot='...'
即可
v-slot的其他写法(解构插槽)
注意看v-slot的写法:
<div id="app">
<one v-slot="{childName}">
{{childName}} {{name}}
</one>
</div>
<template id="one">
<div>
<header>
<slot :child-name="name"></slot>
<span>我是头部</span>
</header>
</div>
</template>
复制代码
<div id="app">
<one v-slot="{childName:aaa}">
{{aaa}} {{name}}
</one>
</div>
<template id="one">
<div>
<header>
<slot :child-name="name"></slot>
<span>我是头部</span>
</header>
</div>
</template>
复制代码
5.组件高级特性
5.1 自定义v-model
5.2 $nextTick()和refs
- Vue出于性能的考虑会进行异步渲染:就是当data改变之后,dom不会立刻渲染,而是等到数据不再变化的时候 一次性的 将 数据的改变更新到视图中。
- DOM 更新后,会执行
this.$nextTick(()=>{})
的回调函数,在回调函数里面进行一些相关操作,能让我们在操作数据之后,获取最新的dom节点。- 我们可以结合refs.xxx`获取dom节点,这是个只读属性。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<ul ref="ul1">
<li v-for="item in list">{{item}}</li>
</ul>
<button @click="addItem">添加1项</button>
</div>
<script>
vm = new Vue({
el: "#app",
data: {
list: ["a", "b", "c"],
},
methods: {
addItem() {
this.list.push(`${Date.now()}`);
this.list.push(`${Date.now()}`);
this.list.push(`${Date.now()}`);
// 获取dom元素
// 在一个元素中写上ref='xxx',就可以通过this.$refs获取dom元素
const ulEle = this.$refs.ul1;
console.log(ulEle.childNodes.length); //3 不会包括所有的,因为在data改变之后,dom不会立刻去变,要想拿到DOM所有节点就必须用法`this.$nextTick()`
//如下:
// 1.异步渲染,$nextTick待dom渲染完后再回调
// 2.页面渲染时,会将data的修改做整合,多次data修改只会渲染一次
this.$nextTick(() => {
const ulEles = this.$refs.ul1;
console.log("加了nextTick的", ulEles.childNodes.length);
console.log("加了nextTick的", ulEles);
});
},
},
});
</script>
</body>
</html>
复制代码
5.3 动态、异步组件
动态组件
Vue中自带一个
<component>
标签,这个标签就是用来动态显示组件的。它有一个is属性,通过v-bind:is="动态组件名"
属性,来选择要挂载的组件。
<div id="app">
<button @click="onChangeComponentClick(1)">To ComponentA</button>
<button @click="onChangeComponentClick(2)">To ComponentB</button>
<button @click="onChangeComponentClick(3)">To ComponentC</button>
<!-- 声明的区域 -->
<component :is="componentId"></component>
</div>
<!-- 定义组件模板内容 -->
<template id="component-a">
<div>component-a</div>
</template>
<template id="component-b">
<div>component-b</div>
</template>
<template id="component-c">
<div>component-c</div>
</template>
<script type="text/javascript">
// 注册组件A/B/C
var commponentA = {
template: "#component-a",
};
var commponentB = {
template: "#component-b",
};
var commponentC = {
template: "#component-c",
};
var vm = new Vue({
el: "#app",
data: function () {
return {
componentId: commponentA,
};
},
methods: {
// 通过一个点击事件传入不同的值,判断动态区块要挂载的组件
onChangeComponentClick: function (type) {
switch (type) {
case 1:
this.componentId = commponentA;
break;
case 2:
this.componentId = commponentB;
break;
case 3:
this.componentId = commponentC;
break;
}
},
},
});
</script>
复制代码
1.这样一种通过
<component>
来声明区域,通过v-bind:is来绑定组件(componentId),然后通过改变组件(componentId)指向来更改展示内容的方式就可以称为动态组件。2.我们每次去切换 component 的时候,模板(component 中的 template)总会去重新渲染,而我们知道每次的 DOM 渲染其实是很消耗性能的操作,那么如果想要避免这样反复渲染的话可以通过
<keep-alive>
标签来告诉 Vue,去缓存已经被渲染过的 component。3.
<keep-alive>
的使用非常简单,只需要使用<keep-alive>
来包裹住<component :is="componentId"></component>
异步组件
举个生活中常见的现象来看什么是异步: 你烧水,不等水烧开就去刷牙了,水烧开了会发出声音告诉你,然后你再处理水烧开之后的事情!看完这个再去理解官方的解释有点豁然开朗了!
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染。
异步加载最重要的一个功能就是可以加快页面访问速度,比如我们可以把一些非首屏的页面做成异步加载。
<div id="app">
<async-example></async-example>
</div>
<template id="demo">
<div>
我是一个异步组件
</div>
</template>
<script type="text/javascript">
// 注册组件
var resCom = {
template: "#demo"
}
Vue.component('async-example', function(resolve, reject){
setTimeout(function(){
// 向resolve回调传递组件定义
resolve(resCom);
}, 1000);
});
var vm = new Vue({
el: "#app"
})
</script>
复制代码
- 在上面的这段代码中,首先声明了一个变量 resCom,并给它指定了一个 template 指向 async-example,这和声明局部组件时所进行的操作是一致的。
- 然后通过 Vue.component(‘async-example’, function (resolve, reject){} 来创建了一个工厂函数,这个函数包含 resolve、reject 两个参数。这两个参数表示两个回调方法,我们可以通过 resolve(resCom) 使程序去异步加载定义的 resCom 组件,也可以使用 reject(‘加载失败描述内容’); 来表示加载失败。这里的 setTimeout 仅作为模拟异步操作使用的。当然上面的代码也可以通过局部组件的方式使用:
var vm = new Vue({
el: '#app',
components: {
'async-example': function (resolve, reject) {
setTimeout(function () {
resolve(resCom);
// reject('加载失败描述内容');
}, 1000);
}
}
});
复制代码
修改一下:
// 注册组件
var resCom = {
template: "#async-example"
};
var promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(resCom)
}, 1000);
});
var vm = new Vue({
el: "#app",
components:{
'async-example':function(){
return promise
}
}
})
复制代码
在webpack中可以这样构建:
// 全局注册
Vue.component(
'async-webpack-example',
// 这个 `import` 函数会返回一个 `Promise` 对象。
() => import('./my-async-component')
)
// 局部注册
new Vue({
// ...
components: {
'my-component': () => import('./my-async-component')
}
})
复制代码
高级的异步注册:
<script type="text/javascript">
// 注册组件
var resCom = {
template: "#async-example"
};
var promise = new Promise(function(resolve, reject){
setTimeout(function(){
resolve(resCom)
}, 2000);
});
var LoadingComponent = {
template: '<div>加载中显示的组件</div>'
};
var ErrorComponent = {
template: '<div>异步组件加载失败</div>'
};
const AsyncComponent = function(){
return {
// 需要加载的组件 (应该是一个 `Promise` 对象)
component: promise,
// 异步组件加载时使用的组件
loading: LoadingComponent,
// 加载失败时使用的组件
error: ErrorComponent,
// 展示加载时组件的延时时间。默认值是 200 (毫秒)
delay: 200,
// 如果提供了超时时间且组件加载也超时了,
// 则使用加载失败时使用的组件。默认值是:`Infinity`
// PS: 组件加载超时时间,超时表示加载失败,会展示ErrorComponent。
// 比如在这里当我们把 Promise 中的 setTimeout 改为 4000的时候,则会展示 ErrorComponent
timeout: 3000
}
}
var vm = new Vue({
el: "#app",
// 注意这里与之前的写法不同之处,是因为我们把这个方法提出去赋值给了AsyncComponent的变量
components:{
'async-example': AsyncComponent
}
})
</script>
复制代码
通过注册路由方法注册异步组件:
// 注册路由的时候会有这么一条语句,实际上是注册异步组件,
// `import` 函数会返回一个 `Promise` 对象
// 将异步组件和 webpack 的 code-splitting 功能一起配合使用
component: () => import(/* webpackChunkName: "index" */ './views/Index.vue')
复制代码
5.4 keep-alive缓存组件
1、啥是 keep-alive?
- keep-alive 是 Vue 的内置组件,当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们(
主要目的是保存组件状态,避免重新渲染
)。和 transition 相似,keep-alive 是一个抽象组件:它自身不会渲染成一个 DOM 元素,也不会出现在父组件链中。看看具体的使用场景:
- 第一种比较普遍的场景,当我们从首页–>列表页–>商详页–>再返回,这时候列表页应该是需要keep-alive的。
- 第二种,当我们从首页–>列表页–>商详页–>返回到列表页(需要缓存)–>返回到首页(需要缓存)–>再次进入列表页(不需要缓存),这时候就是按需来控制页面的keep-alive了。
2.keep-alive的Props
- include – 字符串或正则表达式。只有名称匹配的组件会被缓存。
- exclude – 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
- max – 数字。最多可以缓存多少组件实例。
3.生命周期函数
在被keep-alive包裹的组件或路由中,会多出两个钩子函数:activated 和 deactivated。当组件在 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。
使用 exclude 排除之后,就算被包裹在 keep-alive 中,这两个钩子函数依然不会被调用!另外,在服务端渲染时,此钩子函数也不会被调用。
- 1.activated:在 keep-alive 组件激活时调用,该钩子函数在服务器端渲染期间不被调用.
- 使用 keep-alive 会将数据保留在内存中,如果要在每次进入页面的时候获取最新的数据,需要在 activated 阶段获取数据,承担原来 created 钩子函数中获取数据的任务。
- 2.deactivated:在 keep-alive 组件停用时调用,该钩子在服务器端渲染期间不被调用
在动态组件中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
复制代码
在vue-router中的应用
缓存所有页面(在 App.vue 里面)
<template>
<div id="app">
<keep-alive>
<router-view/>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
复制代码
根据条件缓存页面(在 App.vue 里面)
//include定义缓存白名单,keep-alive会缓存命中的组件;
//exclude定义缓存黑名单,被命中的组件将不会被缓存;
//max定义缓存组件上限,超出上限使用LRU的策略置换缓存数据。
<template>
<div id="app">
// 1. 将缓存 name 为 test 的组件
<keep-alive include='test'>
<router-view/>
</keep-alive>
// 2. 将缓存 name 为 a 或者 b 的组件,结合动态组件使用
<keep-alive include='a,b'>
<router-view/>
</keep-alive>
// 3. 使用正则表达式,需使用 v-bind
<keep-alive :include='/a|b/'>
<router-view/>
</keep-alive>
// 5.动态判断
<keep-alive :include='includedComponents'>
<router-view/>
</keep-alive>
// 5. 将不缓存 name 为 test 的组件
<keep-alive exclude='test'>
<router-view/>
</keep-alive>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
复制代码
结合Router,缓存部分页面(在 router 目录下的 index.js 文件里)
使用 vue-router 提供的 meta 对象,给需要缓存如首页、列表页、商详等添加一个字段,用来判断用户是前进还是后退以及是否需要 keep-alive
import Vue from 'vue'
import Router from 'vue-router'
const Home = resolve => require(['@/components/home/home'], resolve)
const Goods = resolve => require(['@/components/home/goods'], resolve)
const Ratings = resolve => require(['@/components/home/ratings'], resolve)
const Seller = resolve => require(['@/components/home/seller'], resolve)
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home,
redirect: 'goods',
children: [
{
path: 'goods',
name: 'goods',
component: Goods,
meta: {
keepAlive: false // 不需要缓存
}
},
{
path: 'ratings',
name: 'ratings',
component: Ratings,
meta: {
keepAlive: true // 需要缓存
}
},
{
path: 'seller',
name: 'seller',
component: Seller,
meta: {
keepAlive: true // 需要缓存
}
}
]
}
]
})
复制代码
在app.vue里面($route.meta.keepAlive)
<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive"></router-view>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
复制代码
现在针对场景按需设置组件缓存
router.js
// 首页
{
path: '*',
name: 'Home',
// 路由懒加载:
component: () => import(/* webpackPreload: true */ '@/views/home'),
meta: {
keepAlive: true,
deepth: 1
}
},
// 商品列表
{
path: '/product',
name: 'Product',
component: () => import('@/views/product'),
meta: {
keepAlive: true,
deepth: 2
}
},
// 商品详情
{
path: '/detail',
name: 'Detail',
component: () => import('@/views/detail'),
meta: {
keepAlive: true,
deepth: 3
}
},
复制代码
app.vue下
<template>
<div id="app">
<keep-alive :include="include">
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
</div>
</template>
export default {
data() {
return {
include: []
};
},
watch: {
$route(to, from) {
// 如果要to(进入)的页面是需要keepAlive缓存的,把name push进include数组中
if (to.meta.keepAlive) {
!this.include.includes(to.name) && this.include.push(to.name);
}
// 如果 要 form(离开) 的页面是 keepAlive缓存的,
// 再根据 deepth 来判断是前进还是后退
// 如果是后退:
if (from.meta.keepAlive && to.meta.deepth < from.meta.deepth) {
const index = this.include.indexOf(from.name);
index !== -1 && this.include.splice(index, 1);
}
}
}
};
复制代码
利用meta.keeAlive和key值
首先我们肯定还是要利用meta.keeAlive字段来进行判断的,但是不用定义deepth深度了。
进入到app.vue页面中我们为
<router-view>
添加一个key,这个key就像是我们使用v-for循环所定义的一样,大家都知道,key的作用就是一个标识对吧,作用于vue在虚拟 dom 进行diff算法,提高渲染效率。
<template>
<div id="app">
<keep-alive>
<router-view v-if="$route.meta.keepAlive" :key="key" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" :key="key" />
</div>
</template>
<script>
export default {
computed: {
key() {
return this.$route.fullPath;
}
}
};
</script>
复制代码
//然后我们对其需要强制刷新的页面参数里加个时间戳,这样就可以实现按需keep-alive了。
onClick() {
this.$router.push({
path: '/product',
query: {
t: +new Date()
}
})
}
复制代码
5.5 mixin组件抽离公共逻辑
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
- 1.当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项;
- 2.数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先,同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用;
- 3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对;
- 4.请谨慎使用全局混入,因为它会影响每个单独创建的 Vue 实例 (包括第三方组件)。
基本使用
第一步:src/mixin/demo.js
export default {
data(){
return {
msg:"这是mixin的数据",
mixinMsg:"这是mixin的数据",
}
},
created(){
console.log(123)
},
methods:{
onClick(){
console.log('触发了mixin中的onClick')
}
}
}
复制代码
第二步:在组件中引入并使用,组件中只使用mixin。
<template>
<div class='container'>
<div>{{msg}}</div>
<div>{{mixinMsg}}</div>
<div @click="onClick"> 点一下 </div>
</div>
</template>
<script>
import mixin from '@/mixin/demo.js';
export default {
mixins:[mixin],
//1.data中的属性在键值发生冲突的时候,会以组件中的数据优先
data () {
return {
msg: '组件中的数据'
}
},
//2.同名钩子函数将被合并为一个数组,会依次调用,混合对象的钩子函数将在组件滋生钩子函数之前调用
created(){
console.log('组件内的created')
},
//3.值为对象的选项,例如 methods、components 和 directives,将被合并为同一个对象。两个对象键名冲突的时候,组件对象的键值优先。
methods: {
onClick(){
console.log('触发了组件中的onClick')
}
},
}
</script>
复制代码
全局组件混入
在初始化Vue之前调用Vue.mixin()进行全局混入,可以在main.js中使用:
Vue.mixin({
data(){
return {
$_globalMsg:"全局mixin数据"
}
},
created(){
console.log('触发全局mixin的Created')
},
methods:{
$_globalMixin(){
console.log('$_globalMixin')
}
}
})
复制代码
案例-混入懒加载图片功能
给组件中的所有图片添加懒加载
第一步:src/mixin/demo.js
import logo from '@/assets/logo.png'
export default {
data() {
return {
baseImg: logo,
$_timer: null
}
},
mounted() {
// 首屏懒加载一次
this.$_lazyLoadImage();
// 监听croll事件
window.addEventListener('scroll', this.$_handelScroll);
// 移除croll事件
this.$on('hook:beforeDestroy', () => {
window.removeEventListener('scroll', this.$_handelScroll)
})
},
methods: {
$_handelScroll() {
clearTimeout(this.$_timer);
this.$_timer = setTimeout(() => {
this.$_lazyLoadImage();
}, 20);
},
// 懒加载图片
$_lazyLoadImage() {
const imgList = this.$_getNeedLoadingImg();
if(imgList.length <= 0 ) return;
// 判断图片是否展示
imgList.forEach(img => {
if (this.$_imgInView(img)) {
this.$_showImg(img)
}
})
},
// 获取需要加载的图片
$_getNeedLoadingImg() {
let images = Array.from(document.querySelectorAll('img[data_src]'));
images = images.filter(ele => {
return !ele.getAttribute('isloaded')
})
return images
},
// 计算图片位置,判断图片是否展示
$_imgInView(img) {
return window.innerHeight + document.documentElement.scrollTop >= img.offsetTop
},
// 展示图片
$_showImg(img) {
const image = new Image();
const src = img.getAttribute('data_src')
image.src = src;
image.onload = () => {
img.src = src;
// 标记已加载完成
img.setAttribute('isloaded', true);
}
}
}
}
复制代码
第二步:在需要的组件中使用
<template>
<div class='container'>
<div><img :src="baseImg" alt=""></div>
<div v-for="(item,index) in imgSrc" :key="index" ><img :src="baseImg" :data_src="item" alt=""></div>
</div>
</template>
<script>
import mixin from '@/mixin/demo.js';
export default {
mixins:[mixin],
data(){
return {
imgSrc:['放图片链接']
}
}
}
</script>
<style lang="scss" scoped>
img{
width: 200px;
height: 200px;
}
</style>
复制代码
第七部分:自定义指令及过滤器
自定义指令(v-xxx)
在 Vue,除了核心功能默认内置的指令 ( v-model 和 v-show ),Vue 也允许注册自定义指令。用自定义指令对普通 DOM 元素进行底层操作,以达到复用的目的。
注册指令分为全局注册和局部注册
自定义全局指令
一般在在main.js文件里面直接注册,在组件中就可以使用指令
//伪代码:
vue.directive('自定义指令名称', {
生命周期名称: function (el) {
指令业务逻辑代码
}
});
//举例:v-color
Vue.directive("color", {
// 这里的el就是被绑定指令的那个元素
bind: function (el) {
el.style.color = "red";
}
});
<p v-color>我是段落</p>
复制代码
自定义局部指令
- 给创建Vue实例时传递的对象添加directives
- 只能在自定义的那个Vue实例中使用
let vm= new Vue({
el: '#app',
directives: {
"color": {
bind: function (el, obj) {
el.style.color = obj.value;
}
}
}
});
复制代码
自定义指令生命周期钩子函数
bind
当把指令绑定到dom元素身上的时候就会执行,由于只绑定一次,所以只执行一次,在这里可以进行一次性的初始化设置。
inserted
被绑定的元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
update
所在组件的VNode更新时调用,但是可能发生在其子VNode更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新.
componentUpdated
指令所在组件的VNode及其子VNode全部更新后调用。
unbind
指令与dom元素解绑时调用(被绑定的Dom元素被Vue移除),只调用一次。
#### 3.自定义指令传参
- 例如: v-model=”name”,在我们自定义的指令中我们也可以传递传递。
- 在执行自定义指令对应的方法的时候, 除了会传递el给我们, 还会传递一个对象给我们
这个对象中就保存了指令传递过来的参数。
<div id="app">
<p v-color="curColor">我是段落</p>
</div>
<script>
Vue.directive("color", {
// 这里的el就是被绑定指令的那个元素
bind: function (el, obj) {
el.style.color = obj.value;
}
});
let vue = new Vue({
el: '#app',
data: {
curColor: 'green'
},
methods: {
}
});
</script>
复制代码
过滤器
- Vue.js 允许你自定义过滤器,可被用于一些常见的文本格式化。过滤器可以用在两个地方:双花括号插值和 v-bind 表达式 (后者从 2.1.0+ 开始支持)。过滤器应该被添加在 JavaScript 表达式的尾部,由“管道”符号指示:
- 过滤器和函数和计算属性一样都是用来处理数据的,但是过滤器一般用于格式化插入的文本数据。
//左边的值交给右边的过滤器处理
<!-- 在双花括号中 -->
{{ message | capitalize }}
<!-- 在 `v-bind` 中 -->
<div v-bind:id="rawId | formatId"></div>
复制代码
- 默认情况下处理数据的函数接收一个参数, 就是当前要被处理的数据,过滤器可以连续使用.
<div id="app">
<!--Vue会把name交给指定的过滤器处理之后,再把处理之后的结果插入到指定的元素中渲染出来-->
<p>{{name | formartStr2}}</p>
</div>
<script>
Vue.filter("formartStr2", function (value) {
console.log(value); //清华大学, 麻省理工大学, 帝国理工大学
//正则表达式`/大学/g`是把学院全局匹配,然后替换成学院
value = value.replace(/大学/g, "学院");
console.log(value); //清华五道口学院, 麻省理工学院, 帝国理工学院,史莱克学院
return value;
});
let vue = new Vue({
el: "#app",
data: {
name: "清华五道口大学, 麻省理工大学, 帝国理工大学,史莱克大学",
},
});
</script>
复制代码
第八部分:渲染函数和JSX语法
1.虚拟DOM
- 虚拟DOM是使用普通的JavaScript 对象来描述的 ==DOM ==元素,在Vue 中,每一个虚拟节点都是一 VNode 的实例。而Vue.js 之所以执行性能高,就是因为采用了 虚拟 DOM机制。
- 虚拟DOM对象就是普通的JavaScript对象,访问JavaScript对象要比访问真实的DOM 要快的多,Vue 在更新真实的 DOM 前,会比较更新前后 虚拟DOM 结构中有差异的部分,然后采用异步更新队列的方式将差异部分更新到真实的 DOM。
真实的 DOM 结构:
<div id="app">
<h1>hello,vue</h1>
</div>
复制代码
Vue.js的虚拟 DOM 创建的JavaScript对象:
var vNode = {
tag:'div',
data:{
attrs:{
id:'app'
}
},
children:{
//h1节点
}
}
复制代码
2.render函数
概念:
- Vue提供的render()函数是就是让我们使用JavaScript编程的方式生成html模板。而在大多数情况下,我们在 Vue 实例中使用模板template来构建HTML。
- render 函数 跟 template 一样都是创建 html 模板的,但是有些场景中用 template 实现起来代码冗长繁琐而且有大量重复,这时候就可以用 render 函数。
参数:
- render函数接受一个函数参数createElement。
- render()函数的返回值一定是createElement()方法,用于创建一个虚拟节点 (virtual node)。
createElement() 有三个参数:
- 1.第一个参数是一个 HTML 标签字符串,组件选项对象,或者解析上述任何一种的一个 async 异步函数。类型:{String | Object | Function}。
必需。
- 2.第二个参数是一个包含模板相关属性的数据对象你可以在 template 中使用这些特性。类型:{Object}。可选。简单点就是元素的属性集合(包括普通属性、porp、事件属性、自定义指令等);
- 第三个参数是子节点的信息,以数组形式输出,如果该元素只有文本子节点,那么直接以字符串形式给出,如果还有其他子元素,则继续调用 createElement 函数。
- 组件树中的所有 VNode 必须是唯一的,如果想要重复很多次的元素/组件,可以使用工厂函数来实现.
render: function (createElement) {
return createElement('div',
Array.apply(null, { length: 20 }).map(function () {
return createElement('p', 'hi')
})
)
}
复制代码
- JavaScript 代替模板功能
在之前使用模板构建 HTML 的时候,我们可以使用指令。而通过 render 函数创建模板时,就无法提供这些指令,所以我们只能自己编写JavaScript 来实现。
//v-if 和 v-for
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
复制代码
想要通过 render 函数完成上述 HTML页面的构建,过程如下所示:
props: ['items'],
render: function (createElement) {
if (this.items.length) {
return createElement('ul', this.items.map(function (item) {
return createElement('li', item.name)
}))
} else {
return createElement('p', 'No items found.')
}
}
//判断参数 item 的长度,如果长度为 0 ,则返回一个 p 标签,如果长度不为 0 ,则返回一个 ul 标签,并且循环创建 li 子节点
复制代码
- v-model
v-model 的本质是把 value的值作为prop,同时监听 input 事件,而上述代码就是按照 v-model的逻辑实现的。
props: ['value'],
render: function (createElement) {
var self = this
return createElement('input', {
domProps: {
value: self.value
},
on: {
input: function (event) {
self.$emit('input', event.target.value)
}
}
})
}
复制代码
3.render:h => h(App)
- render函数是渲染一个视图,然后提供给el挂载,如果没有render那页面什么都不会出来
- 1.Vue 实例选项对象的 render 方法作为一个函数,接受传入的参数 h 函数,返回 h(App) 的函数调用结果。
- 2.其次,Vue 在创建 Vue 实例时,通过调用 render 方法来渲染实例的 DOM 树。
- 3.最后,Vue 在调用 render 方法时,会传入一个 createElement 函数作为参数,也就是这里的 h 的实参是 createElement 函数,然后 createElement 会以 APP 为参数进行调用.
render: h => h(App) 是下面内容的缩写:
render: function (createElement) {
return createElement(App);
复制代码
4.JSX
render 函数虽然解决了我们的问题,但实在是太麻烦了。这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
new Vue({
el: '#demo',
render: function (h) {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
复制代码
第九部分:Vue中的过渡
- 在Vue项目中,我们可以使用Vue封装的transition组件,这样就可以为任意元素和组件添加过度和动画
- 一般搭配v-if、v-show、动态组件、组件根节点来使用。v-if、v-show显示隐藏组件时添加动画;组件切换实现页面切换,添加过渡动画,淡入淡出效果增强用户体验。
包括以下工具:
- 在 CSS 过渡和动画中自动应用 class
- 可以配合使用第三方 CSS 动画库,如 Animate.css
- 在过渡钩子函数中使用 JavaScript 直接操作 DOM
- 可以配合使用第三方 JavaScript 动画库,如 Velocity.js
1.过渡类名
动画进入:
- v-enter:动画进入之前的初始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。
- v-enter-to:动画进入之后的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter 被移除),在过渡/动画完成之后移除。
- v-enter-active:动画进入的时间段。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。
PS:第一、第二个是时间点;第三个是时间段。
动画离开:
- v-leave:动画离开之前的初始状态。在离开过渡被触发时立刻生效,下一帧被移除。
- v-leave-to:动画离开之后的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave 被删除),在过渡/动画完成之后移除。
- v-leave-active:动画离开的时间段。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。
PS:第一、第二个是时间点;第三个是时间段。
注意对于这些在过渡中切换的类名来说,如果你使用一个没有名字的
<transition>
,则 v- 是这些类名的默认前缀。如果你使用了<transition name="my-transition">
,那么 v-enter 会替换为 my-transition-enter。
原理说明:
- 1.首先最基础的一点在于 如果你想要在单元素/单个组件之中实现过渡动画 那么 你需要在元素/组件所在的HTML标签之外包裹一层 标签,无论是过渡还是动画 都需要这个包起来。
- 2.上面提到了当元素/组件被标签包裹的时候Vue会自动的构建动画流程 也就是自动的在某个时间节点添加/删除对应的CSS类名 Vue其实提供了6个对应的类名 ,如上图所示。
2.过渡
具体实现过程:
<style>
/* 可以设置不同的进入和离开动画 */
/* 设置持续时间和动画函数 */
.v-enter{
transform: translateX(10px);
opacity: 0;
}
.v-enter-active {
transition: all .3s ease;
}
/*v-enter-to和v-leave是一样的可以只写一个,也可以省略,只写v-enter和v-leave-to和进入和离开阶段的函数 */
.v-enter-to{}
.v-leave{}
.v-leave-active {
transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0);
}
.v-leave-to{
transform: translateX(10px);
opacity: 0;
}
</style>
<transition>
<p v-if="show">hello</p>
</transition>
复制代码
进入:
- 在过渡开始前,p标签会被添加类v-enter-active、类v-enter;当过渡开始时,类v-enter被移除,添加类v-enter-to。进入过渡完成后:类v-enter-active、类v-enter-to都被移除。
离开:
- 离开之前理论上会被添加类v-leave-active、类v-leave;在过渡开始时,类v-leave被移除,添加类v-leave-to。因为理论上类v-leave是在瞬间进行添加、移除的,所以我未观察到;根据实际效果我觉得类v-leave并未起作用。
3.CSS 动画
//第一步:写入关键帧函数
@keyframes bounce-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.5);
}
100% {
transform: scale(1);
}
}
//第二步:写上进入和离开的状态
.v-enter-active {
animation: bounce-in .5s;
}
.v-leave-active {
animation: bounce-in .5s reverse;
}
//第三步:样式中使用
<transition >
<p v-if="show">Lorem ipsum dolor sit amet</p>
</transition>
复制代码
4. transition-group列表过渡
transition-group会生成一个真实的dom节点,默认为span,通过tag=”ul”可实现默认切换
transition-group必须要有key值。
5. css动画:Animate.css库
1.1导入Animate.css库
方式一:npm install animate.css --save
方式二:
<head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.1/animate.min.css"
/>
</head>
复制代码
1.2在执行过程中的属性上绑定需要的类名
<h1 class="animate__animated animate__bounce">An animated element</h1>
//或者
.my-element {
display: inline-block;
margin: 0 0.5rem;
animation: bounce; /* referring directly to the animation's @keyframe declaration */
animation-duration: 2s; /* don't forget to set a duration! */
}
复制代码
第十部分:Vue-Router
- vue的单页面应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件映射起来。传统的页面应用,是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中,则是路径之间的切换,实际上就是组件的切换。路由就是SPA(单页应用)的路径管理器。
- 在vue中实现路由还是相对简单的。因为我们页面中所有内容都是组件化的,我们只要把路径和组件对应起来就可以了,然后在页面中把组件渲染出来。
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页面应用变得易如反掌。包含的功能有:
- 嵌套的路由/视图表
- 模块化的、基于组件的路由配置
- 路由参数、查询、通配符
- 基于 Vue.js 过渡系统的视图过渡效果
- 细粒度的导航控制带有自动激活的 CSS class 的链接
- HTML5 历史模式或 hash 模式,在 IE9 中自动降级
- 自定义的滚动条行为
1.怎么使用vue-router?
- 第一步:安装vue-router
npm install vue-router -S
复制代码
- 第二步:在router文件下index.js中使用Vue.use()加载VueRouter插件
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
复制代码
- 第三步:配置路由(定义路由和实例化路由对象)
//1.定义路由规则
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
//2.实例化路由对象
const router = new VueRouter({
routes
})
//3.导出路由实例对象
export default router
复制代码
- 第四步:把路由实例对象挂载根实例(main.js)
import App from './App.vue'
import router from './router'
new Vue({
router,
render: h => h(App)
}).$mount('#app')
复制代码
- 第五步:设置跳转导航
点击之后跳转到对应的路由组件(表现为url的变化)
<template>
<div id="app">
<div id="nav">
<!--跳转导航-->
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<router-view/>
</div>
</template>
复制代码
- 第六步:根组件模板设置路由出口(使用路由)
根据url路径展示当前路由组件内容
<template>
<div id="app">
<div id="nav">
<router-link to="/">Home</router-link> |
<router-link to="/about">About</router-link>
</div>
<!-- 路由出口 -->
<router-view/>
</div>
</template>
复制代码
2.路由模式(哈希 h5 history)
前端路由:就是在保证只有一个 HTML 页面,且与用户交互时不刷新和跳转页面的同时,为 SPA 中的每个视图展示形式匹配一个特殊的 url。在刷新、前进、后退和SEO时均通过这个特殊的 url 来实现。为实现这一目标,我们需要做到以下二点:
- 改变 url 且不让浏览器像服务器发送请求。
- 可以监听到 url 的变化
vue-router的model有两种模式:hash模式和history模式。
hash模式
- Hash模式就是通过改变#后面的值,实现浏览器渲染指定的组件,vue默认的就是hash模式。
说明:
- 这里的 hash 就是指 url 后的 # 号以及后面的字符。
- window.location.hash获取。
- hash 模式下,仅 hash 符号之前的内容会被包含在请求中, hash 值的变化不会导致浏览器像服务器发送请求。
- hash发生变化时,url都会被浏览器记录下来,这样你就可以使用浏览器的后退了.
- 改变 hash(浏览器的前进后退) 不会重新加载页面,会触发 hashchange 事件。
hash模式下用到的api:
window.location.hash = 'hash字符串'; // 用于设置 hash 值
let hash = window.location.hash; // 获取当前 hash 值
// 监听hash变化,点击浏览器的前进后退会触发
window.addEventListener('hashchange', function(event){
let newURL = event.newURL; // hash 改变后的新 url
let oldURL = event.oldURL; // hash 改变前的旧 url
},false)
复制代码
在 HTML5 之前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面的跳转:
history —— 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。(需要特定浏览器支持)这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
history模式
History模式就是通过pushState()方法来对浏览器的浏览记录进行修改,来达到不用请求后端来渲染的效果.不过建议,实际项目还是使用history模式.
const router = new VueRouter({
mode: 'history', //如果这里不写,路由默认为hash模式
routes: [...]
})
复制代码
在 HTML5 之前,浏览器就已经有了 history 对象。但在早期的 history 中只能用于多页面
的跳转:
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
复制代码
在 HTML5 的规范中,history 新增了以下几个 API:
window.history.pushState(state, title, url)
/*
// 添加新的状态到历史状态栈在,保留现有历史记录的同时,将 url 加入到历史记录中。
state:需要保存的数据,合法的 Javascript 对象,这个数据在触发popstate事件时,可以在event.state里获取
title:标题,基本没用,一般传 null
url:设定新的历史记录的 url。新的 url 与当前 url 的 origin 必须是一樣的,否则会抛出
错误。url可以是绝对路径,也可以是相对路径。
如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
*/
window.history.replaceState(state, title, url)
/*
// 用新的状态代替当前状态,会将历史记录中的当前页面历史替换为 url。
与 pushState 基本相同,但她是修改当前历史记录,而 pushState 是创建新的历史记录
*/
window.addEventListener("popstate", function() {
// 返回当前状态对象
// 监听浏览器前进后退事件,pushState 与 replaceState 方法不会触发
});
复制代码
MDN解释:
HTML5引入了 history.pushState() 和 history.replaceState() 方法,它们分别可以添加和修改历史记录条目。这些方法通常与window.onpopstate 配合使用。由于 history.pushState() 和 history.replaceState() 可以改变 url 同时,不会刷新页面,所以在 HTML5 中的 histroy 具备了实现前端路由的能力。history 的改变并不会触发任何事件,所以我们无法直接监听 history 的改变而做出相应的改变。所以,我们需要换个思路,我们可以罗列出所有可能触发 history 改变的情况,并且将这些方式一一进行拦截,变相地监听 history 的改变。
.对于单页应用的 history 模式而言,url 的改变只能由下面四种方式引起:
- 点击浏览器的前进或后退按钮
- 点击 a 标签
- 在 JS 代码中触发 history.pushState 函数
- 在 JS 代码中触发 history.replaceState 函数
总结:
- pushState()方法可以改变URL地址且不会发送请求,replaceState()方法可以读取历史记录栈,还可以对浏览器记录进行修改。这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。
- 在history下,你可以自由的修改path,当刷新时,如果服务器中没有相应的响应或者资源,则会刷新出来404页面。history 模式改变 url 的方式会导致浏览器向服务器发送请求,这不是我们想看到的,我们需要在服务器端做处理:如果匹配不到任何静态资源,则应该始终返回同一个 html 页面。
3.动态路由匹配
我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果:
3.1 params传参
const User = {
//第三步:在当前组件就可以使用参数
template: '<div>{{$route.params.name}}</div>'
}
const router = new VueRouter({
routes: [
// 第一步:动态路径参数 以冒号开头
{ path: '/user/:name', component: User }
]
})
//在某一个路由链接中
<router-link to="/user/:jack">用户</router-link>
//这两部设置了相当于:对应的值都会设置到 $route.params 中。例如:{ name: 'jack' }
复制代码
<router-link :to="{name:'User',params:{name:'蔡徐坤',age:10}}">用户</router-link>
复制代码
3.2 query进行配置传参
const User = {
//第三步:在当前组件就可以使用参数
template: '<div>{{this.$route.query.id}}</div>'
}
const router = new VueRouter({
routes: [
// 第一步:query传参,路由配置不变
{ path: '/user', component: User }
]
})
//在某一个路由链接中传入,问号开头
<router-link to="/user?id=111">用户</router-link>
复制代码
vue-route会自动将?后的id=foo封装进this.route.query.id值为’foo’.
==除了通过router-link的to属性. query也可以通过后面讲到的编程式导航进行传参==
3.3 监听路有变化
- 使用动态路由配置之后,所有的路由对应同一个组件,例如给:配置路由时user/:id,设置路由跳转时分别设置
/user/foo
和/user/bar
,在使用路由参数时,复用的都是User组件.此时组件的生命周期钩子不会再被调用。如果你想路径切换时,进行一些初始化操作时,可以用以下两种解决办法:
- 在组件内watch(监测变化) $route 对象:
const User = {
template: '...',
watch: {
'$route' (to, from) {
// 对路由变化作出响应...
}
}
}
复制代码
- 或者.beforeRouteUpdate 导航守卫如果目的地和当前路由相同,只有参数发生了改变 (比如从一个用户资料到另一个 /users/1 -> /users/2),你需要使用 beforeRouteUpdate来响应这个变化 (比如抓取用户信息)。
const User = {
template: '...',
beforeRouteUpdate (to, from, next) {
// react to route changes...
// don't forget to call next()
}
}
复制代码
4.router,routes,route傻傻分不清?
- 1.router:一般指的就是路由实例.如$router.
- 2.routes:指router路由实例的routes API.用来配置多个route路由对象.(路由配置数组)
- 3.route:指的就是路由对象.例如;$route指的就是当前路由对象.
4.1 router提供的方法可用于编程式导航
router:路由器对象,包含一些操作路由的功能函数,来实现编程式导航。一般指的是在任何组件内访问路由。如:
路由编程式导航的$router.push()
该方法的参数可以是一个字符串路径,或者一个描述地址的对象.例如:
// 字符串
router.push('home')
// 对象
router.push({ path: 'home' })
// 命名的路由
router.push({ name: 'user', params: { userId: '123' }})
// 带查询参数,变成 /register?plan=private
router.push({ path: 'register', query: { plan: 'private' }})
复制代码
注意:如果提供了 path,params 会被忽略,你需要提供路由的 name 或手写完整的带有参数的 path:
const userId = '123'
router.push({ name: 'user', params: { userId }}) // -> /user/123
router.push({ path: `/user/${userId}` }) // -> /user/123
// 这里的 params 不生效
router.push({ path: '/user', params: { userId }}) // -> /user
复制代码
router.replace方法
router.replace和router.push很像,写法一样.但实际效果不一样.push是向history里添加新记录.而replace是直接将当前浏览器history记录替换掉!
那最直接的后果是什么呢? 举个例子:
- 用push方法,页面1跳转到页面2,你使用浏览器的后退可以回到页面1
- 用replace方法,页面1被替换成页面2,你使用浏览器的后退,此时你回不到页面1,只能回到页面1的前一页,页面0.
那什么时候会用到replace呢?
- 当你不想让用户回退到之前的页面时,常见于权限验证,验证后就不让用户回退到登录页重复验证.
router.go(n)方法
这个方法的参数就是一个整数,意思是在history记录中前进或后退多少步.类似window.history.go(n).这样就能控制页面前进或者后退多少步.
4.2 routes
routes创建vue-router路由实例的配置项。用来配置多个route路由对象
4.3 route
5. 嵌套路由
一个对应展示的就是一个组件,因此实现嵌套路由有两个要点:
- 路由对象中定义子路由(嵌套子路由)
- 组件内
<router-view/>
的使用.
//第一步:路由对象中定义子路由(嵌套子路由)
{
path: "/user",
component: User,
name: "user",
//嵌套路由就写在children配置中,写法和routes一样.
children: [
{
path: "",//这里让他默认显示在user视图里
component: UserDefault,
name: "default",
},
{ path: "foo", component: UserFoo, name: "foo" },
//直接写子路由路径,此时path等同于'/user/foo',子路由会继承父路由的路径.
],
},
//第二步:组件内<router-view/>的使用.
<template>
<div>
<div>user组件</div>
<router-link to>默认用户</router-link>
<router-link to="/user/foo">foo用户</router-link>
<router-view></router-view>
</div>
</template>
复制代码
6.命名视图(让路由规则里面的同级普通组件显示)
我们知道点击当前路由路径跳转链接就会显示对应的路由组件,但是如果我们想让让他跳转的同时也把他的兄弟非路由组件显示出来,就需要在同级默认路由出口的地方增加有名字的路由出口,供同级非路由组件显示。看图就明白了。
7.路由重定向和别名
7.1路由重定向
重定向其实就是通过路由拦截path,然后替换url跳转到redirect所指定的路由上. 重定向是通过 routes 配置来完成,
//从 /a 重定向到 /b
const router = new VueRouter({
routes:[
{path:'/a',redirect:'/b'}
]
})
///从 /a 重定向到 命名为'foo'的路由
const router = new VueRouter({
routes: [
{ path: '/a', redirect: { name: 'foo' }}
]
})
//甚至是一个方法,动态返回重定向目标:
const router = new VueRouter({
routes: [
{ path: '/a', redirect: to => {
// 方法接收 目标路由 作为参数
// return 重定向的 字符串路径/路径对象
const { hash, params, query } = to
//这里使用了ES6的解构写法,分别对应了to的hash模式,params,query参数.这里解构就不具体说明了.
if (query.to === 'foo') {
return { path: '/foo', query: null }
}
if (hash === '#baz') {
return { name: 'baz', hash: '' }
}
if (params.id) {
return '/with-params/:id'
} else {
return '/bar'
}
}}
]
})
复制代码
7.2 别名
- “重定向”的意思是,当用户访问 /a时,URL 将会被替换成 /b,然后匹配路由为 /b,那么“别名”又是什么呢?
- /a 的别名是 /b,意味着,当用户访问 /b 时,URL 会保持为 /b,但是路由匹配则为 /a,就像用户访问 /a 一样。
- 那别名就是一个路由有两个路径.两个路径都能跳转到该路由.
//别名是在rutes里的alias进行配置:
const router = new VueRouter({
routes: [
{ path: '/a', component: A, alias: '/b' }
]
})
//使用别名
<router-link to="/b">About</router-link>|
复制代码
8.路由组件传参
- 路由传参,可以通过前面介绍的params和query进行传参.但这两种传参方式,本质上都是把参数放在url上,通过改变url进行的.这样就会造成参数和组件的高度耦合.
- 如果我想传参的时候,可以更自由,摆脱url的束缚.这时就可以使用rute的props进行解耦.提高组件的复用,同时不改变url.
//1.组件接收
const Hello = {
props: ['name'],
//使用rute的props传参的时候,对应的组件一定要添加props进行接收,否则根本拿不到传参
template: '<div>Hello {{ $route.params}}和{{this.name}}</div>'
//如果this.name有值,那么name已经成功成为组件的属性,传参成功
}
//2.路由配置:
const router = new VueRouter({
mode: 'history',
routes: [
// 情况一:没有传参 所以组件什么都拿不到
{ path: '/', component: Hello },
//情况二:布尔模式: props 被设置为 true,此时route.params (即此处的name)将会被设置为组件属性。
{ path: '/hello/:name', component: Hello, props: true },
// 对象模式: 此时就和params没什么关系了.此时的name将直接传给Hello组件.注意:此时的props需为静态!
{ path: '/static', component: Hello, props: { name: 'world' }},
/* 函数模式:
1,这个函数可以默认接受一个参数即当前路由对象.
2,这个函数返回的是一个对象.
3,在这个函数里你可以将静态值与路由相关值进行处理.
*/
{ path: '/dynamic/:years', component: Hello, props: dynamicPropsFn },
{ path: '/attrs', component: Hello, props: { name: 'attrs' }}
]
})
function dynamicPropsFn (route) {
return {
name: (new Date().getFullYear() + parseInt(route.params.years)) + '!'
}
}
new Vue({
router,
el: '#app'
})
<!--html部分-->
<div id="app">
<h1>Route props</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/hello/you">/hello/you</router-link></li>
<li><router-link to="/static">/static</router-link></li>
<li><router-link to="/dynamic/1">/dynamic/1</router-link></li>
<li><router-link to="/attrs">/attrs</router-link></li>
</ul>
<router-view></router-view>
</div>
复制代码
9. 路由懒加载
- vue主要用于单页面应用,此时webpack会打包大量文件,这样就会造成首页需要加载资源过多,首屏时间过长,给用户一种不太友好的体验.
- 如果使用路由懒加载,仅在你路由跳转的时候才加载相关页面.这样首页加载的东西少了,首屏时间也减少了.
- vueRouter的懒加载主要是靠Vue 的异步组件和 Webpack 的代码分割功能,轻松实现路由组件的懒加载。只需要将组件以promise形式引入即可.
{
path: "/about",
name: "About",
alias: "/a1111",
component: () => import("../views/About.vue"),
},
或者
const Foo = () => import('./Foo.vue')
routes: [
{
path: '/foo',
component: Foo
}
]
//或者把组件按组分块
const Foo = () => import(/* webpackChunkName: "group-foo" */ './Foo.vue')
const Bar = () => import(/* webpackChunkName: "group-foo" */ './Bar.vue')
const Baz = () => import(/* webpackChunkName: "group-foo" */ './Baz.vue')
复制代码
10. 导航守卫(路由钩子)
路由导航守卫,通俗点说就是路由钩子.作用也和生命周期钩子类似,在路由跳转过程进行操作控制.
路由守卫一般用作路由跳转的一些验证,比如登录鉴权(没有登录不能进入个人中心页)等等等
10.1 导航守卫分类
- 1.全局前置守卫
beforeEach
:当一个导航触发时,全局前置守卫按照创建顺序调用。守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中。
//1.可以在main.js 或者在单独的路由配置文件router.js中进行设置
router.beforeEach((to, from, next) => {
...
next();
});
//2,也可以在组件内部设置
this.$router.beforeEach((to, from, next) => {
...
next();
});
//3,对函数及next()的详细使用说明
router.beforeEach((to, from, next) => {
next();//使用时,千万不能漏写next!!!, 表示直接进入下一个钩子.
//next(false) 中断当前导航
//next('/path路径')或者对象形式next({path:'/path路径'}) 跳转到path路由地址
//next({path:'/shotcat',name:'shotCat',replace:true,query:{logoin:true}...}) 这种对象的写法,可以往里面添加很多.router-link 的 to prop 和 router.push 中的选项(具体可以查看api的官方文档)全都是可以添加进去的,再说明下,replace:true表示替换当前路由地址,常用于权限判断后的路由修改.
//next(error)的用法,(需2.4.0+)
}).catch(()=>{
//跳转失败页面
next({ path: '/error', replace: true, query: { back: false }})
})
//如果你想跳转报错后,再回调做点其他的可以使用 router.onError()
router.onError(callback => {
console.log('出错了!', callback);
});
复制代码
- 2.全局解析守卫
beforeResolve
在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被调用。- 3.全局后置守卫afterEach:这些钩子不会接受 next 函数也不会改变导航本身:
- 4.路由独享的守卫:可以在路由配置上直接定义 beforeEnter 守卫:
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
// 使用方法和上面的beforeEach一毛一样
}
}
]
})
复制代码
- 5.组件内的守卫:
beforeRouteEnter
、beforeRouteUpdate (2.2 新增)
、beforeRouteLeave
你可以在路由组件内直接定义的路由导航守卫:
const Foo = {
template: `...`,
beforeRouteEnter(to, from, next) {
// 在渲染该组件的对应路由被 confirm 前调用
// 不!能!获取组件实例 `this`
// 因为当守卫执行前,组件实例还没被创建
//不过,你可以通过传一个回调给 next来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数。
next(vm => {
// 通过 `vm` 访问组件实例
})
},
beforeRouteUpdate(to, from, next) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,
// 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 可以访问组件实例 `this`
this.name = to.params.name
},
beforeRouteLeave(to, from, next) {
// 导航离开该组件的对应路由时调用
// 离开当前路由,此时可以用来保存数据,或数据初始化,或关闭定时器等等
// 可以访问组件实例 `this`
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (answer) {
next()
} else {
next(false)
}
}
}
复制代码
10.2 完整的导航解析流程
导航被触发。
在失活的组件里调用 beforeRouteLeave 守卫。
调用全局的 beforeEach 守卫。
在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
在路由配置里调用 beforeEnter。
解析异步路由组件。
在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve 守卫 (2.5+)。
导航被确认。
调用全局的 afterEach 钩子。
触发 DOM 更新。
调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。
复制代码
10.3导航守卫参数
每个守卫方法接收三个参数:
- to: Route: 即将要进入的目标 路由对象
- from: Route: 当前导航正要离开的路由对象
- next: Function: 一定要调用该方法来 resolve 这个钩子。执行效果依赖 next 方法的调用参数。
- next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是confirmed (确认的)。
- next(false): 中断当前的导航。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器后退按钮),那么 URL 地址会重置到 from 路由对应的地址。
- next(‘/’) 或者 next({ path: ‘/’ }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: ‘home’ 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。
- next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止且该错误会被传递给 router.onError() 注册过的回调。
11. 路由元信息
接在路由配置的时候,给每个路由添加一个自定义的meta对象,在meta对象中可以设置一些状态,来进行一些操作。用它来做登录校验再合适不过了.
要优雅要隐性地传递信息,就使用meta对象吧!
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar,
// a meta field
meta: { requiresAuth: true }
}
]
}
]
})
复制代码
/*
$route.matched: 一个数组,共含当前路由的所有嵌套路径片段的路由记录
*/
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
//数组some方法,如果meta.requiresAuth为ture,则返回true.此时,说明进入该路由前需要判断用户是否已经登录
if (!auth.loggedIn()) { //如果没登录,则跳转到登录页
next({
path: '/login',
query: { redirect: to.fullPath } //官方例子的这个小细节很好,通过query将要跳转的路由路径保存下来,待完成登录后,就可以直接获取该路径,直接跳转到登录前要去的路由
})
} else {
next()
}
} else {
next() // 确保一定要调用 next()
}
})
复制代码
- 我们可以通过在meta里设置的状态,来判断是否需要进行登录验证.如果meta里的requiresAuth为true,则需要判断是否已经登录,没登录就跳转到登录页.如果已登录则继续跳转.
- 此时,可能会有同学说,前面说的path,params,query都可以存储信息,作为登录验证的状态标记.的确,它们也可以达到同样的效果.如果是少量单个的验证,使用它们问题不大.
- 但如果是多个路由都需要进行登录验证呢?path,params,query是把信息显性地存储在url上的.并且多个路径都把一个相同的状态信息加在url上.这样就使url不再单纯,并且也很不优雅美观.
所以要优雅要隐性地传递信息,就使用meta对象吧!