Vue技术栈个人总结整理
Vue知识点整理
基本使用
- 插值, 表达式
/*** 1. 插值表达式: Vue中使用双大括号语法"{{ }}"* 2. 在{{ }}之间可以写变量和一些简单的js运算,但是不支持语句和流控制* 3. */<div>{{message}}</div><div>{{count / 10}}</div><script>export default{data() {return {message: "Hello World!",count: 100}}}</script>
- 指令,动态属性
由于指令内容过多,这里就不一一列举…
- v-html: 存在XSS风险,会覆盖子组件
/*** v-html可以渲染一个html文本字符串,但是在使用v-html之后会覆盖掉该节点内部的子元素节点* v-html存在XSS的风险*/<p v-html="rawHtml"><span>有 XSS 的风险</span><span>【注意】使用v-html之后,将会覆盖掉子元素</span></p>// ...data() {return {rawHtml: "指令: v-html 加粗 斜体"}}
- computed
computed计算属性:
当使用method方法时,只要将方法使用到了模板上,当每一次发生了变化,就会重新渲染试图,导致其性能开销比较大,computed计算属性是一个watcher,同时其具备缓存能力,只有当依赖的属性发生了改变的时候才会更新视图。
当页面中有属性需求计算的,不要使用函数的形式,可以使用computed计算属性来代替
computed和watch的原理:他们里面都是一个watcher来实现的,computed属性具备缓存,而watch是不具备缓存的
computed计算属性可以使用getter和setter对一个计算属性值进行获取和修改
data() {return {firstName: "chen",lastName: "GX"}
},
computed: {fullName: {get() {return `${this.firstName} ${this.lastName}`;}set(value) {const names = value.split(" ");this.firstName = names[0];this.lastName = names[1];}}
}
- watch
- watch属于是一个侦听器,它和computed属性的实现原理一样,都是一个watcher,只是watch不具备缓存
- watch一般监听单个变量或一个数组,对于监听基本的数据类型或进行浅度监听时,watch监听器何以得到一个oldValue和newValue值,但是进行深度监听时无法得到oldValue
- 使用watch进行深度监听:
data() {return {obj: {a: 1000}}
},
watch: {obj: {// 深度监听中handler执行函数中的oldValue获取不到值handler(oldValue, newValue) {console.log("深度监听: " oldValue, newValue);},// 进行深度监听加上deep属性为truedeep: true,// 加上immediate属性为true,则侦听的的handler会立即执行一次// immediate: true}
}
// 使用deep: true进行深度监听时,会将监听的引用类型的属性层层遍历,都加上监听事件,这样会导致性能开销增大,那么可以对引用数据类型内的单个属性进行监听
监听对象的单个属性
data() {return {obj: {a: 1000}}
},
watch: {'obj.a': function(newValue, oldValue){// 监听单个属性时可以取到oldValue值console.log(newValue, oldValue)}
}
- 循环列表渲染
- v-for对象遍历:v-for推荐使用 ‘item of list’ 形式
- 每个item使用 :key时,其值不推荐使用index索引作为唯一值,而是推荐从后台返回的唯一值,如id等,可有效的提高性能
- 对循环渲染的列表,直接改变数组的数据不会更新到页面上,可以使用push,pop,shift,unshift,splice,sort,reverse等方法来修改数组的内容实现页面的更新,或者是修改数组对象的引用
- 使用template标签来包裹列表,该标签不会被渲染到页面上
- 处理第三点对数组对象进行更新可以重新渲染视图外,Vue提供了一个Vue.set || vue. s e t ( ) 对 对 象 进 行 更 新 的 方 法 也 可 以 实 现 更 新 后 同 步 到 视 图 , 更 新 数 组 可 以 写 成 : v u e . set()对对象进行更新的方法也可以实现更新后同步到视图,更新数组可以写成: vue. set()对对象进行更新的方法也可以实现更新后同步到视图,更新数组可以写成:vue.set(arr, index, value)的形式
// 模板部分:这里为了简单就直接使用index作为key了
<div v-for="(item, index) in list" :key="index">{{ item }}</div>// 数据部分:
data() {return {list: [ 'a', 'b', 'c' ]}
}
v-for循环渲染列表需要添加key的原理是: Vue底层实现页面节点数据更新渲染的Diff算法需要保证节点的高可复用性,如果不添加key这个唯一标识,那么每次的组件更新都将会造成列表节点的重新渲染导致性能消耗明显增加,增加key作为节点的唯一标识可以减少节点不必要的渲染。而不推荐使用index的原因是在列表渲染时,在DIff的过程中,如果每次的更新列表导致索引变动也会造成不必要的更新。
v-for和v-if不能连用的情况: v-for的优先级会高于v-if,所以嵌套使用的话,每个v-for渲染的列表都会执行v-if,造成不必要的计算,影响整体性能,此时可以在v-for外层套一个template标签来处理v-if,或者使用computed计算属性来规避这个v-if
<template v-if={{showActive}}><div v-for="item in list" :key="item.id">{{ item.value }}</div>
</template>
- 事件
- vue的事件分为原生DOM事件和组件的自定义事件,其绑定的方法都类似: v-on: 或者使用 “@”
- 事件执行函数的参数传递: 如果直接在绑定事件后加上函数名(不加()执行函数),则这个监听函数可以直接接收到事件对象 e v e n t , 而 如 果 需 要 传 递 其 他 的 参 数 时 , 就 需 要 显 示 将 事 件 对 象 传 递 过 去 , 如 m e t h o d N a m e ( event, 而如果需要传递其他的参数时,就需要显示将事件对象传递过去, 如methodName( event,而如果需要传递其他的参数时,就需要显示将事件对象传递过去,如methodName(event, …其他参数)
- 添加事件修饰符可以完成一些特定的操作: .stop(取消冒泡), .prevent(取消默认事件) .capture(捕获阶段执行) .self(只有event.target就是当前元素才执行) .once(事件只执行一次后就被销毁) .passive(滚动事件允许默认行为和scroll不阻塞执行)
- vue提供按键修饰符实现更多交互: .enter(回车键触发), .tab(tan键触发) .delete(删除键触发) .esc(esc键触发) .space(空格键触发) …
- Vue中的原生事件是绑定在当前元素上的,和react中的合成事件采用事件委托的机制是不同的
- vue自定义事件: 自定义事件的写法和原生事件的写法相似,也是使用v-on或者时"@"进行绑定
- vue中的自定义事件主要用于父子组件间的通信: 子组件向父组件暴露消息,而在组件中触发自定义事件使用vue的$emit(“eventName”, arguments)的形式
- vue中提供了.sync语法糖用于实现父子组件的通信,其原理也是自定义事件,一般的 ‘:foo.sync=“bar”’ 会被扩展为: "@update:foo=“val => bart = val”"的形式,所以在更改变量值时仍需要写为: $emit(“update:foo”, newValue)的形似
// parent Component
<template><child-component @test='handleTest' />
</template>export default {methods: {handleTest(res) {console.log(res)}}
}// Child Component
<template><button @click='onClick'>btn</button>
</template>export default {methods: {onClick() {this.$emit('test', 'hello-vue~')}}
}
- 剖析自定义事件原理:
- 自定义事件用于实现父子组件间的通信,通过子组件调用this.$emit方法触发父组件定义的方法达到修改父组件中数据状态的目的来达到通信的目的。
- Vue的自定义事件系统,是会在组件实例时为每个组件添加上一个 _events属性,其值为一个Object, 其中存放了该组件的所有自定义事件
- 同时这个属性对象中提供了四个api对自定义事件进行操作: $on(), $emit(), $off, $once, 分别进行事件的添加,触发,和移除事件的操作
- 自定义事件的原理是在父组件经过模板编译后,会将自定义事件及其回调通过 o n ( ) 添 加 到 子 组 件 的 事 件 中 心 e v e n t s 中 , 当 子 组 件 通 过 on()添加到子组件的事件中心_events中, 当子组件通过 on()添加到子组件的事件中心events中,当子组件通过emit()触发test自定义事件时,会在他的事件中心去寻找对应的事件执行,但是因为回调函数时定义在父组件作用域中的,所以在其中可以更新父组件中的状态
- 简单实现 o n 和 on和 on和emit函数:
// $on()实现
Vue.prototype.$on = function (event, fn) {const hookRE = /^hook:/; // 检测事件名是否时hook:开头,这个这里可不管const vm = this;if(Array.isArray(event)){ //如果第一个参数是一个数组for(let i = 0; i < event.length; i++){this.$on(event[i], fn) // 递归将所有的事件进行绑定}}else{(vm._events[event] || (vm._events[event] = [])).push(fn)// 如果有对应的事件名就push,没有则创建一个空数组pushif(hookRE.test(event)){ // 对应hook开头的事件vm._hasHookEvent = true;}}return vm;
}// $emit()实现:
Vue.prototype.$emit = function(event){const vm = this;let cbs = vm._events[event] // 找到事件名对应的回调集合if(cbs) {// 将$emit()中传递的附加参数转化为数组const args = Array.from(arguments, 1).slice(1)// 挨个执行回调函数集合中的函数for(let i = 0; i < cbs.length; i++) {cbs[i].apply(vm, args)}}
}
- v-if v-else用法以及v-if和v-show的区别
v-if中判断条件可以是data中定义的变量,或者也可以是表达式
v-if 和 v-else指令的效果和 if … else… 语句的效果类似,根据条件判断相应的组件是否可以显示
<div><p v-if="type === 'a' ">A</p><p v-else-if="type === 'b' ">B</p><p v-else>C</p>
</div>
- v-if 和 v-show的区别:
v-if进行匹配的条件是:如果条件匹配,则页面的节点中就会将该节点渲染出来,如果不匹配,则页面的节点中不会存在这个节点内容
v-show: 会将所有的节点都加入到页面的DOM树中,不管是否匹配,如果是匹配的节点,则会被显示出来,如果不匹配,则会设置该节点的 display: none对该节点进行隐藏
对于两者的选择,如果页面中的内容只渲染一次,或者是页面的内容切换不频繁,那么选择v-if。 如果页面的内容频繁的切换,则选择v-show更加合适
- Vue组件 父子组件间传值: Props
- 父组件通过属性的形式将一些参数值传递给子组件,在子组件中使用Props对父组件传递的属性值进行接收。
- Props可以为一个数组形式或者为一个对象:当为数组形式时,数组的每个元素就时需要接收的参数名,但是此时父组件可以选择不传递一个参数,相当于这个参数值是没有限制的
- 当Props值为一个对象时,可以在对象中进行一些特殊的指定, 如比较常见的: type(props参数的类型), defaut(props参数的默认值,如果不传递就使用这个默认值), required(该参数是否不需要传递,为一个boolean值) 等等
// 父组件
<Chid :foo="100" />//子组件中接收:
props: {foo: {type: Number,required: true}
}
- Vue组件: 组件间通信
- 使用 Props + $emit进行通信
- Props上面已经说过,是用于完成父组件向子组件的传值
- $emit()也在之前自定义事件的时候说过了,子组件通过触发父组件传递的自定义事件完成父组件中状态的更新(当然还有.sync修饰符)
- 这也算是目前比较常用的父子组件间通信的方式(例子这里不再介绍)
- 使用 ref 完成组件组件通信
- 对于Ref而言,当将ref使用在原生的DOM元素上时,获取到的是一个原生DOM节点(所以ref需要在组件mounted时候才有效,因为此时才能够获取到组件的实例)
- 当ref作用在自定义的子组件上时,通过this.$refs在都组件中获取到的是一个子组件的实例,通过这个实例可能获取到子组件中定义的属性和方法并对它们进行操作,以此来达到父组件到子组件通信的目的
// 父组件:
<button @click="changeChildMsg">ref修改子组件的MSG</button>
<HelloWorld ref="hello" />methods: {// 通过子组件实例调用子组件的setMessage方法修改子组件的msg属性值changeChildMsg(){this.$refs.hello.setMessage("这是修改后的MSG")}}// 子组件: HelloWrold<h1>{{msg}}</h1>data(){return {msg: "这是修改前的MSG"}},methods: {setMessage(msg){this.msg = msg;}}
- 使用Vuex进行通信
Vuex是Vue框架的一个状态管理插件,将需要共享的状态(state)存入到Vuex的store中,需要使用的组件通过this.$store.state去获取其中的状态内容;通过dispatch去派发action修改state的值,进而Vuex会将修改后的最新的值通知给所有使用该state状态的组件进行视图更新,从而达到状态共享通信的目的(这些依赖于Vuex的单例数据源,响应式特性而实现)
关于Vuex之后会以源码的形式进行剖析,这里不做过多阐述
- 兄弟组件间的通信
- 兄弟组件间的通信使用状态提升的形式,将兄弟组件间都需要使用到的属性提升到公共的父组件,子组件通过$emit()的形式去触发父组件的自定义事件对父组件的状态进行更新,从而通知更新其兄弟组件的目的
- 通过自定义事件直接进行兄弟组件间的通信: EventBus, 在一个组间中使用this. o n ( ) 绑 定 一 个 自 定 义 事 件 ( 绑 定 自 定 义 事 件 后 记 得 在 b e f o r e D e s t r o y 钩 子 中 进 行 取 消 绑 定 : t h i s . on()绑定一个自定义事件(绑定自定义事件后记得在beforeDestroy钩子中进行取消绑定: this. on()绑定一个自定义事件(绑定自定义事件后记得在beforeDestroy钩子中进行取消绑定:this.off() ), 在兄弟组件中使用event.$emit()去触发这个自定义事件
- 这里自定义事件Vue已经帮我们实现了,所以可以不用我们再单独去实现一个EventBus, 我们可以直接使用一个Vue实例去完成, 如下所示
// 定义一个event.js, 导出一个Vue实例
import Vue from "vue"
export default new Vue()// 组件A: 只写有用的部分import event from "./event"mounted(){// 进行自定义事件的绑定event.$on("print", this.handler);},methods: {handler(){console.log("执行自定义事件")}},beforeDestroy(){// 在组件卸载前一定解绑自定义事件,避免发生内存泄漏event.$off("print", this.handler)}// 组件B:import event from "event"methods: {emit(){// 通过$emit()触发自定义事件event.$emit("print")}}
- 手写实现一个EventBus (面试题可能出现)
function EventBus(){this.msgQueues = {}
}
EventBus.prototype = {on: function(msgName, func){if(this.msgQueues.hasOwnProperty(msgName)){// 如果之前存在一个该类型的绑定回调,则现在给变成一个集合if(typeof this.msgQueues[msgName] === "function") {this.msgQueues[msgName] = [this.msgQueues[msgName], func]}else {this.msgQueues[msgName] = [...this.msgQueues[msgName], func]}} else {this.msgQueues[msgName] = func;}}// 触发事件执行函数emit: function(msgName){if(!this.msgQueues.hasOwnProperty(msgName)) {return}let args = Array.from(arguments).splice(0, 1)if(type this.msgQueues[msgName] === "function") {this.msgQueues[msgName](...args)}else {this.msgQueues[msgName].forEach((fn) => {fn(args)})}},// 删除自定义的事件,防止内存泄漏off: function(msgName){if(!this.msgQueues.hasOwnProperty(msgName)){return}delete this.msgQueues(msgName)}
}
- 跨组件间的通信
- 祖先组件通过provide提供可以别后代组件获取使用的状态属性
后代组件通过inject接收该组件需要的祖先组件提供的属性并在该组件中进行使用
// 祖先组件
provide() {return {test: 'hahhaha'}}// 后代组件:inject: ['test']// 此时在该组件中便可以使用this.test获取到祖先组件通提供的test值进行获取
- 组件的生命周期:
- 生命周期的三个阶段: 挂载阶段 -> 更新阶段 -> 卸载阶段
- 单组件的生命周期: new Vue()[new Vue实例] -> beforeCreate[初始化:事件 & 生命周期方法开始初始化] -> created[初始化完成: 此时数据,方法等都初始化完成] -> beforeMount[将要挂载组件(有el的情况下), 此时页面还没渲染] -> mounted[组件挂载完成,此时页面渲染完毕,使用ref也能获取到组件的实例了] -> beforeUpdate[将要更新,及组件将要被渲染,但此时还未被重新渲染] -> updated[组件重新渲染完成] -> beforeDestroy[组件将要被卸载,但是此时还未被卸载, 此时需要解除一些事件绑定,消除定时器等操作] -> destroyed[组件被卸载完成,生命周期结束]
- 父子组件的生命周期: 这里主要是父子组件的挂载先后顺序和卸载先后顺序: 【创建阶段】 父组件created -> 子组件created -> 子组件mounted -> 父组件mounted (及表示父组件实例之后完成子组件的实例,逐层向下;而组件的挂载(mount)则是先渲染挂载子组件后再渲染挂载父组件,由内向外的一个顺序) 【更新阶段】 父组件先beforeUpdate -> 子组件beforeUpdate -> 子组件updated -> 父组件updated (和挂载阶段类似,先从父组件开始beforeUpdate逐层向下,之后实际更新updated则由内向外进行) 【卸载过程】父组件beforeDestroy -> 子组件beforeDestroy -> 子组件destroyed -> 父组件destroyed
Vue高级特性
12. v-model原理以及自定义v-model
- v-model是双向数据绑定的语法糖,用于实现 数据<->视图 的双向绑定,常用于form表单元素的使用
- v-model通过实现v-bind属性绑定和v-on:input时间绑定实现的语法糖
<component :value="val" @input="val = $event.target.value"></component>
对于不是输入框而需要实现v-model效果的组件则可以使用v-bind和使用自定事件实现相应的效果或者对v-model指令进行自定义
- 使用model属性进行v-model的自定义实现:
当父子组件间进行通信,需要子组件中使用输入框对父组件中的属性值进行操作等类似的需求,可以使用v-model进行操作,此时需要使用model属性对v-model进行自定义:
// 父组件:
<p>{{name}}</p>
<Child v-model="name" />
data(){return {name: "chenSir"}
}
//...// 子组件中完成自定义v-model的相关操作:
<input type="text" :value="name" @input="$emit('change', $event.target.value)" />
model: {prop: "name", // 这个属性值需要和props中定义的那个接收父组件v-model传递值得名称相同event: "change" // 这个是在input中使用$emit()触发得事件,所以需要和$emit()的事件名相对应
},
props: {name: { // 这里的接收的名字自定义,都可以,只要和其他传递的props不重名即可type: String,default: ""}
}
- $nextTick
- vue. n e x t T i c k 和 V u e 组 件 的 异 步 更 新 有 关 , n e x t T i c k ( ) 是 在 下 一 次 D O M 更 新 循 环 结 束 之 后 执 行 延 迟 回 调 , 在 修 改 数 据 之 后 使 用 nextTick和Vue组件的异步更新有关,nextTick()是在下一次DOM更新循环结束之后执行延迟回调,在修改数据之后使用 nextTick和Vue组件的异步更新有关,nextTick()是在下一次DOM更新循环结束之后执行延迟回调,在修改数据之后使用nextTick,则可以在$nextTick()中获取到更新后的DOM
- n e x t T i c k 的 使 用 场 景 是 在 c r e a t e d 生 命 周 期 中 进 行 D O M 操 作 时 必 须 要 放 到 nextTick的使用场景是在created生命周期中进行DOM操作时必须要放到 nextTick的使用场景是在created生命周期中进行DOM操作时必须要放到nextTick的回调中执行,因为created执行时DOM并未渲染,此时操作DOM时无效的,所以需要放到$nextTick()中
- 在页面数据变化后需要获取到最新的DOM元素,则需要将操作放到$nextTick的回调中才能获取到最新的DOM
官方解释: (用于下面的源码理解)Vue异步执行DOM更新。只要观察到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有的数据变化。如果同一个Watcher被触发多次,则只会被推入队列一次,这种在缓冲时去除重复的数据对于避免不必要的计算和DOM操作是非常重要的。然后,在下一个事件循环tick中,Vue刷新队列并执行实际工作,Vue在内部尝试使用原生的Promise.then和MessageChannel实现,如果执行环境不支持则采用setTimeout(fn, 0)代替
// nickTick源码解析
export default nextTick = (function(){// 存储所有需要执行的回调函数const callbacks = []// 标志是否正在执行回调const pendinng = false// 用于触发执行回调函数let timerFunc;function nextTickHandler() {pendding = false;const copies = callbacks.slice(0)// 拷贝回调后将callbacks进行清空callbacks.length = 0;for(let i = 0; i < copies.length;i++){// 执行callbacks中的回调copies[i]()}}// 延迟执行,这里有限考虑是否原生支持Promise或者MutationObserver这两个微任务队列中执行的方法// 将延时放到微任务队列中执行是最合适的,他会在每次宏任务队列清空后被执行// 如果原生环境不支持这两种情况则换用setTimeout()代替if(typeof Promise !== "undefined" && isNative(Promise)){let p = Promise.resolve()let logError = err => {console.error(err)}timerFunc = () => {p.then(nextTickHandler).catch(logError)}}else if(typeof MutationObserver !== "undefined" && (isNative(MutationObserver) || MutationObserver.toString() === "[object MutationObserverConstructor]")){let counter = 1let observer = new MutationObserver(nextTickHandler)// 构造一个textNode并让其改变,使得每次都能触发observer的回调函数let textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {// 让counter每次改变counter = (counter + 1) % 2textNode.data = String(counter)}}else {timerFunc = () => {setTimeout(nextTickHandler, 0)}}// 导出为外部使用的函数,进行回调函数注入,在每次DOM更新结束之后调用// 其中ctx时当前回调函数执行的上下文对象return function queueNextTick(callback, ctx){let _resolve;callbacks.push(() => {if(callback){try{callbacks.call(ctx)}catch(err){console.error(err)}}else if(_resolve){_resolve(ctx)}})if(!pending){pendinng = true;// 将该函数调用放入异步延迟执行timerFunc()}// 若为传递callback同时支持Promise的情况下函数将返回一个Promise// 而在返回的Promise.then中注册的回调函数方法会在此次异步DOM更新完毕之后被触发调用 if(!callback && typeof Promise !== "undefined"){return new Promise((resolve) => {_resolve = resolve})}}
})()
- slot插槽
插槽的含义: 在引入子组件后,在子组件中元素中添加一些信息或者标签,使得这些信息或者标签可以在子组件中的指定的位置显示
让父组件中的内容和子组件的内容进行组合(这个过程成为内容分发),使用slot元素为原始内容的插槽。
使用插槽需要注意的是组件的作用域问题,在父组件中使用子组件写入插槽内容时,使用的内容的作用域应该时父组件的作用域
slot插槽中接收显示的是该组件使用时内部的子元素内容
- 普通插槽使用(单插槽使用)
// 子组件: child
<div class="child"><slot>如果父组件中没有插入内容,这将会作为默认内容显示</slot>
</div>// 父组件
<div class="parent"><child><p>这是插入到子组件插槽中的内容,此时子组件中插槽的默认内容不会显示</p></child>
</div>
- 使用具名插槽
// 子组件: child
// 使用name属性执行插槽的名字
<div class="child"><slot name="head">标题插槽</slot><p>这是正文内容</p><slot name="footer">底部插槽</slot>
</div>// 父组件使用
// 内容加上slot属性,属性值为子组件中需要插入位置的插槽的名字
<div class="parent"><child><p slot="header">这是子组件中的标题</p><p slot="footer">这是子组件中的底部</p></child>
</div>
/*** 具名插槽的使用写法还可以写为如下形式:*/// 这里的v-slot属性只能写在template组件上,这是2.6版本之后的更改
<child><template v-slot:header><p>这是子组件中的标题</p></template>
</child>
// 或者简写为: # + slot名字, 这种写法也是只能够写在template组件上
<child><template #header><p>这是子组件中的标题</p></template>
</child>
- 作用域插槽的使用(作用域插槽是一种特殊的slot, 在作用域插槽中可以在父组件写入插槽内容时获取使用子组件作用域中的元素)
// 这里以2.5版本之后的写法为例
// 子组件: child
<div class="child"><slot name="head" :text="message">标题插槽</slot>
</div>
data(){return {message: "这是来自子组件中的数据"}
}// 父组件
// 在父组件中写入插槽内容时,添加属性slot-scope可以获取到在子组件中对应slot组件上的属性值(除开name属性值)
<div class="parent"><child><p slot="header" slot-scope="prop">这里可以接收到自子组件中的数据: {{prop.text}}</p></child>
</div>
在子组件中可以使用this.$slots.插槽的name获取到对应插槽位置的内容
这里获取到的是一个插槽的实例,里面能够查询到插槽位置插入的节点元素信息等等
- 动态,异步组件
动态组件 & 异步组件的存在,可以使我们更方便的去控制首屏代码的加载体积,提升加载速度
- 动态组件:
- 当不同的组件之间进行切换的时候,使用动态组件会非常有用
- 动态组件使用一个特殊的属性 is 来实现组件间的切换,is属性的内容值可以为一个已经注册的组件的名字,或者是一个组件的选项对象,此时该组件会根据is属性指定的内容对组件进行渲染显示
<!-- 动态组件 -->
<component :is="ComponentName"></component>
- 解析DOM模板的时候需要注意,有些HTML元素,如ul, ol, table, select等,对于哪些元素可以出现在其内部是有严格要求的,而有些元素,如li,tr,option只能出现在某些特定元素的内部,这将会导致使用这些约束元素时出现问题,此时可以使用is这个特殊的属性动态对内容进行指定来解决
// 出错情况: 这里自定义组件child-component组件会被视为无效的内容被提升到外部,导致渲染的最终结果出错
<table><child-component></child-component>
</table>
// 对于以上的这种问题可以修改为以下所示即可解决
<table><tr :is="child-component"></tr>
</table>
这里的动态组件会经常和下面的keep-alive进行连用,使用keep-alive保存动态组件的状态,使得来回的频繁切换可以得到保存的值
- 异步组件
异步组件的目的主要式提升项目页面初始化加载的性能,通过使用import()函数异步加载初始化不需要渲染显示的组件,来将一些比较大的组件进行抽离,极大提升页面初始化渲染显示的性能
/* 异步组件加载 */
components: {ChildComponent: () => import("./ChildComponent")
}
- 普通组件从创建到页面展示经历的流程: 组件对象 -> compileFunctions编译后得到 render function -> 经过render 后得到VNode(虚拟DOM), 在update到浏览器页面得到真实的DOM
- 普通组件和异步组件的区别主要在组件createComponent这一步的时候,普通的Vue组件是直接使用开发者定义好的options,利用Vue.extend生成组件对应的构造函数,进而得到VNode, 而异步组件的createComponent:(如下所示)
Vue.component("async-component", function(resolve, reject){// 这里例用setTimeout模拟异步setTimeout(function(){reslve({// resolve出去的就和普通组件中的组件options是相似的template: `I am async component!`})}, 1000)
})
异步组件中的Ctor是一个function,在Vue源码实现中,对于异步组件会经过特定的处理,其中的function中接收到的resolve和reject方法实在resolveAsyncComponent中进行定义的,然后调用Ctor时传递给这个function. 源码中对于异步组件的处理:
let asyncFactory;
if(isUndef(Ctor.cid)){asyncFatory = Ctor;// resolveAsyncComponent的功能主要是定义Ctor所需要的resolve和reject方法// 在其中会调用Ctor执行将resolve和reject作为参数传递进去// 除此之外,在这个方法中还会定义一个forceRender,调用$forceUpdate()进行页面更新Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context)if(Ctor === undefined){return createAsyncPlaceholder(asyncFactory,data,context,children,tag)}
}// 这里看看reslve和reject的实现, 这里的once可以理解成就调用一次
const resolve = once(res: Object | Class<Component>) => {// 缓存resolved:由于resolve函数的主要功能是异步完成的,将得到的Ctor转化为构造函数,缓存在factory.resolved中factory.resolved = ensureCtor(res, baseCtor)// 强制渲染页面if(!sync){forceRender(true)}
}
const reject = once(reason => {process.env.NODE_ENV !== "production" && warn(`Failed to resolve async component: ${String(factory)}` +(reason ? `\nReason: ${reason}` : ''))if(isDef(factory.errorComp)){factory.error = true;forceUpdate(true)}
})
- keep-alive
- keep-alive是Vue的内置组件,能在组件切换的过程中将状态保留在内存中,防止重复的渲染DOM
- keep-alive用来包裹动态组件时,会缓存当前不活动的组件实例,而不会销毁它们。
- keep-alive是一个抽象组件,它自身不会渲染DOM元素,也不会出现在父组件的节点链中。
- 如果需要缓存路由渲染的组件时,可以在配置路由时配置meta属性,设置keepAlive值为true则会将组件缓存
routes: [{path: "/index",component: index,meta: {keepAlive: true}}
]
需要动态缓存router-view中的部分组件时(就是希望某些组件被缓存,某些组件不被缓存),可以使用以下两种方法:
- 使用 include/exclude来实现
- 使用include/exclude来实现的,每个组件需要加上name来进行匹配
- include: 表示只有匹配的组件会被缓存(这里的匹配支持字符串或者正则表达式的匹配)
- exclude: 表示任何匹配的组件都不会发生缓存 (支持字符串或者正则表达式)
// 只缓存以 in 开头的组件(使用正则表达式,需使用 v-bind)
<keep-alive :include="/^in.*/"><router-view/>
</keep-alive>
// 只缓存 name 为 index 的组件(字符串匹配)
<keep-alive include="index"><router-view/>
</keep-alive>// 不缓存 name 为 index 的组件
<keep-alive exclude="index"><router-view/>
</keep-alive>
- 配合router.meta属性来实现
- 配合router.meta属性实现主要是依赖beforeRouteLeave钩子函数动态设置meta.keepAlive
export default {name: "index"// 关于keep-alive组件相关的两个钩子函数activated(){console.log("组件被激活触发")},deactivated(){console.log("组件消失,被缓存时触发调用")},// beforeRouteLeave时vue-router的钩子函数,在路由切换时进行触发,常进行路由拦截路由守卫的作用// 这里通过这个钩子函数进行动态的设置组件的meta.keepAlive属性进行组件的缓存// 这个钩子函数的三个参数分别是: from当前路由,将要跳转到的路由: to,以及next()方法,需要继续向下执行就需要调用以下这个next()方法beforeRouteLeave(to, from, next){// 设置下一个路由的metato.meta.keepAlive = true;next()}
}
keep-alive组件的两个钩子函数(上面已经用到), 分别是activated()和deactivated(), 它们分别在组件被激活时和组件被隐藏缓存时被触发
- mixin (Vue 2.x版本)
- vue中,混入(mixin)是一种特殊的使用方式。一个混入对象可以包含任意的组件配置选项(data, props, components, watch,computed…)可以根据需求"封装"一些可复用的单元,并在使用时根据一定的策略合并到组件的选项中,使用时和组件自身的选项没有区别。
- mixin中比较重要的两个方法: Vue.extend() 和 extend()
- Vue.extend(): 是基础的Vue构造器,参数是一个包含组件选项的对象(组件选项包括data, props, computed, methods等等), 可以显示的扩展组件和混入对象, 如:
var Component = Vue.extend({mixins: [myMixin]
})
- extend(): 是对象的合并方法,参数是合并的源对象和目标对象,用于对象的合并,用法为: extend(target, source)表示source对象将合并到target对象上,作用类似Object.assign()方法
- mixin的合并有全局混入(调用Vue.mixin()进行合并)实例混入(在组件中配置: mixins: []选项,其中数组中的选项是组件配置选项的对象)
- 全局混入将对所有的Vue实例均有效,不推荐使用,但是在编写一些Vue插件的时候会用到,例如Vuex的源码中就使用了全局的mixin全局混入$store对象
- 实例混入只会在注册到了mixins配置项中的组件中才会生效。
- mixin混入机制针对组件配置项的不同选项的混入机制存在差异,这里从源码层面依次展开:
- (1). el, propsData 的合并方向: parentOptions -> childOptions; 源码实现如下:
// src/core/util/options.js
const strats = config.optionMergeStrategiesif(props.env.NODE_ENV !== "production"){// el 和 propsData的合并机制strats.el = strats.propsData = function(parent, child, vm, key){if(!vm){warn(`option "${key}" can only be used during instance ` +'creation with the `new` keyword.')}return defaultStrat(parent, child)}
}// defaultStrat: 默认合并策略比较简单,以childVal为主,若没有则使用parentVal
const defaultStrat = function(parentVal: any, childVal: any): any {return childVal === undefined ? parentVal : childVal
}
- (2). 生命周期钩子函数的合并方向: parentOptions -> childOptions, 源码解析如下:
// 生命周期函数
export const LIFECYCLE_HOOKS = ['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured','serverPrefetch'
]
// 为每个钩子函数设置合并方法
LIFECYCLE_HOOKS.forEach(hook => {strats[hook] = mergeHook
})function mergeHook(parentVal: ?Array<Function>, childVal: ?Function | ?Array<Function>) : ?Array<Function> {// 这里表示将每个hook合并为一个数组,按照从父到子开始一步一步链接合并为一个数组,parent在前,child在后。当钩子函数触发时,按照数组从头到尾的顺序调用触发,所以调用混入hook函数的顺序会是:// 全局混入hook执行 -> 实例混入hook执行 -> 组件实例的hook执行const res = childVal ? parentVal? parentVal.concat(childVal) //将子选项的值和父选项的值合并为一个数组: Array.isArray(childVal)? childVal: [childVal]: parentValreturn res ? dedupeHooks(res) : res
}
- (3). data数据的合并方向: parentOptions -> childOptions, 源码解析如下
strats.data = function(parentVal: any, childVal: any, vm?: Component) :?Function{if(!vm){// 如果子组件的data不是一个function,在dev环境下报出警告,同时返回父配置的dataif (childVal && typeof childVal !== 'function') {process.env.NODE_ENV !== 'production' && warn('The "data" option should be a function ' +'that returns a per-instance value in component ' +'definitions.',vm)return parentVal}return mergeDataOrFn(parentVal, childVal)}return mergeDataOrFn(parentVal, childVal, vm)
}// 这里vm组件实例有无仅用于对data方法调用的this指向处理上,若不传vm则在调用parentVal和childVal的时候直接将this指向当前调用的this
export function mergeDataOrFn(parentVal, childVal, vm): ?Function{if(!vm){if(!childVal){return parentVal;}if(!parentVal){return childVal;}//当parentVal和childVal都存在时,这里返回一个function, 在该函数中将parentVal和childVal进行合并返回合并后的值return function mergeDataFn(){// 这里parentVal作为from, childVal作为to, parentVal -> childVal 方向合并return mergeData(typeof childVal === "function" ? childVal.call(this, this) : childVal,typeof parentVal === "function" ? parentVal.call(this, this) : parentVal)}}else {return function mergedInstanceDataFn(){const instanceData = typeof childVal === "function" ? childVal.call(vm, vm) : childVal;const defaultData = typeof parentVal === "function" ? parentVal.call(vm, vm) : parentVal;if(instanceData){// parentVal作为from, childVal作为to, parent -> child方向进行合并return mergeData(instanceData, defaultData)} else {return defaultData;}}}
}// 主要的合并函数
function mergeData(to: Object, from: ?Object):Object {if(!from) return to;let key, toVal, fromVal;// 取得父options对象的key数组const keys = hasSymbol ? Reflect.ownKeys(from) : Object.keys(from)for(let i = 0; i < keys.length; i++){key = keys[i];if (key === "__ob__") continue;toVal = to[key] // childOptions配置fromVal = from[key] // parentOptions配置if(!hasOwn(to, key)){// 若childVal中没有这个数据,则添加set(to, key, fromVal)} else if (toVal !== fromVal && isPlainObject(toVal) && isPlainObject(fromVal)){//如果parent中和child中均有此key并且不相等,同时该key的值非对象,否则将递归进行深度合并mergeData(toVal, fromVal)}}return to;
}// 这里的set方法操作key并会添加响应式属性值,及我们调用的Vue.set()方法
// 由上可以看出data的合并由父到子开始递归进行合并,以child为主,比较key的规则如下:
// 1. 若child无此key,parent中存在,则直接合并此key
// 2. 若child和parent都有此key,且非object类型,忽略不作为,也就是表示使用了child中的值
// 3. 若child和parent都有此key,且为object类型,则递归合并对象
- (4). components, directives, filters的合并方向: parent <-- child: 源码解析如下:
export const ASSET_TYPES = ['component','directive','filter'
]
// 为这三个属性strats绑定合并方法
ASSET_TYPES.forEach(function (type) {strats[type + 's'] = mergeAssets
})function mergeAssets(parentVal: ?Object, childVal: ?Object, vm?: Component, key: string): Object {// 原型委托const res = Object.create(parentVal || null)if(childVal){process.env.NODE_ENV !== "production" && assertObjectType(key, childVal, vm)// 将childVal合并到parent上return extend(res, childVal)}else{return res;}
}
这里可以看出: components,directives, filters的合并策略比较简单,使用extend方法合并一个对象,按照从子到父进行合并
这里采用原型委托的方法在合并时把child属性委托在parent上,这样根据原型链查找的规则,现在child上查找,没有再到parent上查找,以此类推
- (5). watch侦听器的合并方向: parent -> child, 源码解析:
strats.watch = function(parentVal: ?Object, childVal: ?Object, vm?: Component, key: string): ?Object {// work around Firefox's Object.prototype.watch...if(parentVal === nativeWatch) parentVal = undefined;if (childVal === nativeWatch) childVal = undefinedif (!childVal) return Object.create(parentVal || null)if (process.env.NODE_ENV !== 'production') {assertObjectType(key, childVal, vm)}if (!parentVal) return childVal;const ret = {}//获取parent选项extend(ret, parentVal)for (const key in childVal) {//获取parent选项值let parent = ret[key]//获取child选项值const child = childVal[key]if (parent && !Array.isArray(parent)) {parent = [parent]}//每个wather选项合并为数组ret[key] = parent? parent.concat(child): Array.isArray(child) ? child : [child]}return ret
}
watch会将每个watcher合并成为一个数组,按照从父到子的顺序合并,再同名的watcher属性触发时,按照数组的顺序从头到尾调用触发,所以其触发顺序是: 全局混入 -> 实例混入 -> 组件混入
- (6). props, methods, computed, inject的合并方向: parent <-- child, 源码解析:
strats.props = strats.methods = strats.inject = strats.computed = function(parentVal: ?Object, childVal: ?Object, vm?: Component, key: string
): ?Object {if(childVal && process.env.NODE_ENV !== "production"){assertObjectType(key, childVal, vm)}if(!parentVal) return childVal;const ret = Object.create(null)// 将parentVal进行合并extend(ret, parentVal)if(childVal){// 继续合并childValextend(ret, childVal)}return ret;
}
如上可见:props, methods, computed, inject的合并策略和components相似,都是使用extend方法合并为一个对象,按照从子到父进行合并,所以在调用时child的优先级更高些
- provide的合并策略和data的合并策略类似: parent -> child:
strats.provide = mergeDataOrFn // 这里就是之前在data合并中使用到的那个合并方法
mixin混合机制的优势和弊端:
优势: 当多个组件之间存在通用的可复用的组件逻辑时,可以使用mixin对项目进行抽离,提升代码的可复用性
弊端: mixin的混入可能会造成属性变量的污染,导致组件的渲染和实际的预期出现差异;变量来源不明确,造成代码的可读性不高; mixin和组件之间可能存在多对多的关系,导致代码的复杂度增加
Vue周边插件: Vue-router && Vuex
这里将从插件使用,手写插件源码两层面记录
Vue-router
简介: vue-router的路由并非硬件路由器路由,而是SPA(单页应用)的路径管理器; vue-router和vue.js框架深度集成,适用于单页应用的构建; vue单页应用是基于路由和组件的,路由用于设定访问路径,并将路径和组件之间进行映射,完成组件间的路由切换。
vue-router的实现原理是: SPA(单页应用)程序仅有一个完整的页面,通过vue-router加载的页面,不会加载整个新的页面,而是更新某个指定容器中的内容,及更新视图而不重新请求页面。vue-router实现路由时提供了history模式和hash模式,通过mode参数来决定采用那种模式;
Hash模式:
vue-router默认为hash模式: 使用URL的hash来模拟一个完整的URL,于是当hash路由改变时,页面不会被重新加载,及单单改变#后面的内容,浏览器不会重新加载网页(也就是说,hash路由用来指导浏览器的动作,对服务器是无效的)。
每次改变#后的hash内容后,会在浏览器的访问历史中增加一个记录,可以使用前进和后退按钮返回到上一个位置;所以hash路由通过锚点值得改变,来完成浏览器指定DOM位置不同组件页面的渲染
History模式:
由于hash模式会在url中自带上#,是一个很丑的实现,而改用history模式,可以使得url切换和正常的url切换一样。这种模式充分利用了history.pushState API来完成URL的跳转而无需重更新加载页面。
但是使用history模式还需要服务器的支持,因为这个模式的路由切换回发起HTTP请求,所以history模式常用于SSR的项目中
使用:
- 安装:
npm install vue-router
- 开始使用:
import VueRouter from "vue-router"
Vue.use(VueRouter)
- 在页面容器中加入 组件,这个组件作为一个容器,使用vue-router路由渲染出的组件会渲染到这个容器中
- 基础编写配置:
const router = new VueRouter({routes: [{// 路由地址path: "/",// 该路径渲染的页面组件component: HomeComponent}]
})
new Vue({el: "#app",router
})
- 动态路由匹配:
{path: "/user/:id",component: UserComponent
}
// 动态路由传递的parmas参数(及这里的id)可以在跳转到的路由组件中通过: $route.params.id的方式取得参数值
- 嵌套路由: (也即是父子路由)
{path: "/layout",component: LayoutComponent,children: [ // children中的配置和routes中的路由配置是相似的{path: "/user/:id",component: UserComponent}] }
// 通过这样配置的页面的路由最终的效果是: /layout/user/:id 这样的嵌套路由
- 编程式导航:
存在这样的需求: 需要在也写js实现的逻辑中完成页面的跳转,vue-router提供给我们一些api来完成页面路由的切换:
- router.push() api:
router.push()方法会向history栈中添加一个新的记录,它的效果和使用组件实现的效果是一样的(组件内部也是调用router.push()api完成页面切换)
// 字符串的形式跳转: 跳转的 /home 页面
router.push("home")
// 对象形式跳转:
router.push({path: "home"})
// 命名的路由(及在定义路由规则时指定了name属性),并带params参数跳转:
router.push({name: "user", params: {userId: 123}})
// 带query查询参数跳转:
router.push({path: "user", query: {username: "zhangsan"}})
- router.replace() api:
router.replace()api 和 router.push()类似,唯一不同是replace()不会向history中添加新的记录,而是替换掉当前的history记录,所以对于replace()api, 点击浏览器的前进和后退是无效的
router.replace()的参数使用和router.push()一样
- router.go() api:
router.go()的参数是一个整数,代表在history记录中前进或者后退多少步的操作,类似于window.history.go(n)
// 在浏览器中前进一步: 等同于history.forword()
router.go(1)
// 在浏览器中后退一部: 等同于history.back()
router.go(-1)
- 命名路由:
有时通过一个名称来标识一个路由会显得更加方便些,特别在链接一些路由或者路由跳转的时候,使用命令路由可以简化一些工作:
{name: "home",path: "/home",component: Home}
// 使用router-link进行路由跳转时,可以这样书写:
<router-link :to="{name: 'home'}"></router-link>
- 命名视图:
当需要在当前浏览器页面中展示多个路由器页面时,可以使用多个进行处理,但是此时渲染路由时需要为每个指定name属性,如此,便可以实现在多个router-view路由视图中显示多个路由组件的效果
<router-view name="a" />
<router-view name="b" />
// 此时的路由配置:
routes = [{path: "/home",name: "home",components: {a: Layout, // 指定name="a"的视图显示的组件b: Home // 指定name="b"的视图显示的组件}}
]
- 路由重定向:
当在进行项目迁移和更新的时候,需要在用户访问之前的某些接口时进行重定向到新的接口,此时可以为路由匹配规则配置重定向:
{path: "/a",redirect: "/b" // 重定向到 /b 路由, 此时不需要指定component属性}
- 路由别名:
当需要多个路由匹配到的时一个路由规则,及不同的路由渲染的是同样的页面内容,可以为一个路由定义路由别名,当访问别名路由时,URL会保持访问路由不变,但是匹配时回去匹配另一个路由:
{path: "/a",componnet: A,alias: "/b"}
- 关于的props属性:
- to: 表示目标路由的链接,当被点击后,内部会将to属性的值传递到router.push()api进行执行,所以to的属性值可以参考router.push()api的参数值形式;
<router-link :to="{path: '/abc'}" ></router-link>
- replace: 为router-link标签添加replace属性后,点击跳转时使用的是router.replace()api而不是router.push()api,效果和调用router.replace()一样
<router-link :to="{path: '/abc'}" replace></router-link>
- append: append属性表示在当前(相对)路径前添加基路径;例如从 /a 导航到一个相对路径 /b,未设置append时路径为 /b, 设置之后为: /a/b
// 使用append属性的坑: 使用append属性需要path指定的路径最开始不加 /, 否则append路由会失效
<router-link :to="{ path: 'b' }" append></router-link>
- tag: 指定需要router-link标签最终被渲染成什么原生DOM标签,默认会渲染为 a 标签:
<router-link :to="{path: '/about'}" tag="button">About</router-link>
- exact-active-class: 该属性的默认值为router-link-exact-active(这里的默认选项可以通过路由构造函数的linkExactActiveClass进行全局配置),表示当前链接精确匹配时会被激活的class属性,用于在路由切换后标注当前选中的路由
// 当路由切换到 /home, 该路由标签将会被添加上 active 的类
<router-link :to="/home" exact-active-class="active">
- active-class: 该属性和exact-active-class相似,也是在匹配路由选中时为标签添加上class类,和exact-active-class的区别是: exact-active-class为精确匹配,而active-class为模糊匹配,及当路由为 /a 时, / 和 /a 都会被添加上相应的class类属性
- event: 目前在标签上添加click事件是不会被触发,因为标签本身会被解析成a标签,所以这里一般需要添加事件修饰符
较为完整的routes配置:
declare type RouteConfig = {path: string;component: component;name: string; // 命名路由components: { [name: string]: component };redirect: string | location | function;props: boolean | object | function; // 传递给跳转组件的props属性,会在路由组件的props属性中接收到alias: string | array; // 路由别名children: array; // 配置子路由beforeEnter: (to: route, from: route, next: function) => void;meta: any; // 提供开发者挂载一些自定义的数据caseSensitive: boolean; // 匹配规则是否大小写敏感(默认值false)pathToRegexpOptions: object; // 编译正则的选项
}
三种路由模式: (mode属性值)
- hash:使用URL hash值来作路由。支持所有浏览器。
- history:依赖HTML5 History API和服务器配置。
- abstract:支持所有js运行环境,如Node.js服务器端。如果发现没有浏览器的API,路由会自动强制进入这个模式。
路由配置的base属性:
base属性用于设置页面的基路由,及当前的所有的路由都在 /app/ 下, 可以设置base属性为: base: “/app”
Vue-router的生命周期:
- 全局钩子函数(在所有的路由进入,离开都会触发此钩子函数)
- router.beforeEach(): 该生命周期执行时,还未进入到路由内,所以该生命周期中不能获取到vue的实例,如果确实需要获取vue实例,可以使用router.app的方式去获取
let router = new VueRouter({...})router.beforeEach(function(to,from,next){console.log(to); // 目标导航console.log(from); // 当前导航(及当前离开的导航)// 执行则路由跳转函数,如果这里不调用next()函数,则路由到这里就结束了,无法完成跳转// 基于这个特性常用于实现一些路由守卫的效果next(); //实际例子,根据路由情况,判断是否需要登录//根据目标路径下的 自定义属性meta(如果你设置了),如
}...{path:'/xxx'component:'xxx',meta:{ //meta提供开发者挂载自定义数据needLogin:true //xxx页面需要登录}}...//如果跳转到xxx页面,则跳转到登录/login,否则路由正常跳转if(to.meta.needLogin){next('/login');}else{next();}
- afterEach: 路由跳转后可以在这个钩子函数中执行一些操作:
rourer.afterEach(function(to,from){//路由跳转后,我们可以做一些操作//例如: 根据path或者meta添加新属性,去判断,改变文档标题if(to.path=='/'){window.document.title = 'xxx'}else if{...}else{...}
});
- 局部钩子函数:
- beforeEnter: 针对某个路由进行处理,此时只对某个路由在将要跳转进入时执行
{path:'xxx'component:'xxx',beforeEnter(to,from,next){//只是针对xxx这个path,做操作next(); //next必须执行,否则会卡住} },
- 单文件组件中的钩子函数: (及在vue页面组件中的路由钩子函数)
//访问导航时候,执行这个路由执行的第一的函数
//这个时候组件还没有初始化,无法访问this
beforeRouteEnter(to,from,next){console.log(this) //undefinednext(function(vm){ //next中函数的参数,参数vm就是vue实例//这个回调函数会在组件创建完毕后之后,及会在组件的mounted之后才会被执行console.log(vm.msg); //'hello'})
},//这个是路由嵌套情况下,点击子导航触发
beforeRouteUpdata(to,from,next){console.log('beforeRouteUpdata');next(); //不写这个钩子函数不会继续执行console.log(this) //可以访问组件实例
},
//离开主导航(主组件)时候触发
beforeRouteLeave(to,from,next){// TODO: ..
}
通过手写简易版vue-router分析vue-router源码实现:
流程分析: 页面URL改变 -> 触发浏览器事件监听函数 -> 改变vue-router的当前值变量current -> 监听current的变动 -> 获取新的current相对应的组件(来自配置的routes参数) -> render新的组件
let Vue;
class VueRouter {// 开发Vue插件需要一个install方法,这个方法会在Vue.use()的时候调用,会将Vue传递到这个函数中static install(_Vue){Vue = _Vue;Vue.mixin({beforeCreate(){// 只有在根实例上才会有router选项,就是在new Vue()的时候将router作为参数传递了进去if(this.$options.router){Vue.prototype.$router = this.$options.routerthis.$options.router.init() // 初始化插件}}})}constructor(options){this.$options = options;// 缓存path和route的映射关系,方便之后查询组件this.routeMap = {}// 通过Vue实例来完成状态搜集,也就是实现监听current状态变化相应的操作,这也是vue-router和vue框架强相关的原因之一this.app = new Vue({data: {current: this.$options.base || "/" // 根据base配置来指定初始路由}})}// 插件初始化函数:init(){this.bindEvents();this.createRouteMap();// 创建vue-router提供的组件:this.initComponent()}// 创建hash路由和route的映射:createRouteMap(){this.$options.routes.forEach(route => {this.routeMap[route.path] = route;})}// 绑定事件监听:bindEvents(){// 监听浏览器路由变动window.addEventListener("hashchange", this.onHanshChange.bind(this), false)window.addEventListener("load", this.onHashChange.bind(this), false) // 这个是在初始化加载页面路由时触发的监听}onHashChange(e) {let hash = this.getHash()// 从routeMap中取出路由对应的组件配置let router = this.routeMap[hash]// 获取路由跳转的from, to值,用于传递给钩子函数beforeEnter执行let {from, to} = this.getFromAndTo(e)if(router.beforeEnter){router.beforeEnter(from, to, () => {// 这里时next函数,用于修改current的执行触发页面renderthis.app.current = hash;})}else{this.app.current = hash;}}// 获取当前的路由hash值: getHash(){return window.location.hash.slice(1) || '/'}// 获取from路由和to的hash路由:getFromAndTo(e){let from, to;if(e.newURL){from = e.oldURL.split('#')[1];to = e.newURL.split("#")[1];}else{from = "";to = this.getHash();}return {from, to}}// 创建router-view组件和router-link组件initComponent(){Vue.component("router-view", {render: h => {// 取得需要渲染的component组件const component = this.routeMap[this.app.current].component;return h(component)}})Vue.component("router-link", {props: {to: String},render(h){ return h('a', {attrs: {href: "#" + this.to}}, [this.$slots.default])}})}// 实现一个push api:push(url){// hash模式,直接赋值,如果时history模式,使用pushStatewindow.location.hash = url;}
}
Vuex插件:
简介: Vuex是一个专为vue.js应用程序开发得状态管理模式。采用集中式的存储管理应用的所有组件状态,并以相应的规则保证状态以一种可以预测的方式发生变化。
当需要构建大型的Vue单页应用,或者是应用的逻辑够复杂时, 选用Vuex进行组件的状态管理是一个好的选择。
安装使用:
- 安装包:
npm install Vuex
- 开始使用:
import Vuex from "vuex"
Vue.use(Vuex)
// ... 创建store
new Vue({el: "#app",store
})
- 创建一个简单的store:
const store = new Vuex.Store({state: { // store中的状态count: 0},mutations: { // 修改状态的mutation: vuex中的状态值不能直接修改,只能通过触发mutation或者派发action去进行修改increment (state) {state.count++}}
})
- Vuex的四个主要部分:
- State: 包含了store中存储的各个状态
- getter: 类似于vue.js中的计算属性,根据其他getter或者state计算返回相应的状态值
- mutation: 一组用于改变state状态的执行方法,只能够同步执行操作
- action: 一组用于改变state的执行方法,其中可以执行异步的操作
- 关于state:
- Vuex使用state来存储应用中需要共享的状态,其通过Vue的状态搜集机制来完成状态的更新和同步,在组件中可以从 $store.state 上获取到state中定义的状态值(对于进行了模块分割的,需要使用 $store.state.moduleName 的形式去获取各个模块的状态值)
- 为使一个状态被更好的在组件中使用,可以在组件中使用计算属性包装这个状态值:
computed: {// 使用计算属性包装后的state状态值,会在state状态发生改变时触发重新计算获取新的状态值,从而完成组件的更新count(){return this.$store.state.count;}}
- 当一个组件中需要多个状态时,为这些状态都声明计算属性会出现重复和冗余,因而可以使用vuex提供的辅助函数: mapState帮助我们生成计算属性,提高效率
import { mapState } from "vuex"
// ...computed: mapState({// 当参数属性值书写为函数时,可以接收到store中的state作为参数count: state => state.count,// 直接为参数写为字符串形式时,会等同于: state => state.countcountAlias: "count",// 当需要在属性中使用到当前组件实例的this,则需要使用普通函数:countPlusLocalState(state) {return state.count + this.localCount}})// 当映射的计算属性名称和state中的名称相同时,可以直接使用字符串数组的形式进行设置// 如此即会生成相对应的state状态的计算属性computed: mapState(["count"])
- 关于getter:
- 类似于vue.js的计算属性,getter的返回值会根据它的依赖被缓存起来,只有当其依赖的状态属性发生改变之后才会被重新计算.
- 当需要从state状态中派生出一些新的状态,例如对列表进行过滤等操作,使用getter计算属性是一个不错的选择
- getter接收state作为其第一个参数,用于生成相应的计算属性值:
const store = new Vuex.Store({state: {todos: [{ id: 1, text: '...', done: true },{ id: 2, text: '...', done: false }]},getters: {// 这里接收到的state参数值就是上面的state属性的值doneTodos: state => {return state.todos.filter(todo => todo.done)}}
})// 在组件中使用时可以通过 "store.getters.属性名" 的方式获取到getter中函数的返回值
- getter接收的第二个参数是其他的getter, 也就是说getter可以通过其他的getter继续生成新的计算状态
getters: {doneTodosCount: (state, getters) => {return getters.doneTodos.length}}// 在组件中的使用仍可以通过属性的形式进行使用: store.getters.getter属性名
- 当需要getter的计算依赖一些外部得条件状态时,需要为getter传递一些参数,此时可以将getter的返回值写成一个函数,在这个返回的函数中可以接收外部传值,此时调用getter就会变成调用返回的函数:
getters: {// 返回一个函数,在外部使用时就相当于调用这个函数getTodoById: (state) => (id) => {return state.todos.find(todo => todo.id === id)}}// 外部使用: store.getters.getTodoById(1)// 这里需要注意的是,getter通过方法访问时,每次状态更新都会重新调用,而不会缓存计算结果
- mapGetters辅助函数: 和mapState类似,当一个组件需要使用到多个getter值时,可以使用mapGetter将store中的getter映射到局部组件的计算属性中:
import { mapGetters } from "vuex"export default {// ...computed: {// mapGetters的参数和mapState的参数设置是一样的,可以是对象或者数组的形式...mapGetters(["doneTodoCount"])}
}
- 关于Mutation:
- 在vuex中修改store中的状态值的唯一的方式是提交mutation, 定义mutation类似于事件注册,mutation属性名类似注册的事件名,而在调用时通过属性名触发相应的handler函数
- 定义的mutation中的handler函数时,其第一个参数是state, 而在mutation中可以直接操作state进行状态更新
const store = new Vuex.Store({state: {count: 1},mutations: {increment (state) {// 变更状态state.count++}}
})
- 触发mutation更新状态时不能直接调用mutation的handler,而使用 commit() api去触发,commit()api的参数就是想要触发的mutation的属性名字符串:
store.commit("increment")
- 提交载荷(Payload): 也就是需要在commit()提交mutation的时候,为mutation的handler执行函数传递一些参数时,可以为mutation传入第二个参数:
mutations: {// mutation的第二个参数就是外部commit()提交时传入的参数increment (state, n) {state.count += n}
}
// 外部使用时: store.commit("increment", 10), commit()的第二个参数将会作为mutation的第二个参数传递给相应的mutation handler执行函数
// 多数情况下,为了程序的可读性,常将多个payload参数封装成一个对象进行一次性传递
- commit() api的对象形式参数: commit()除了使用多个参数的形式提交mutation外,还可以将参数封装成一个对象进行提交,这个对象必须包含一个type属性指定提交的mutation, 对象的其他属性将会作为payload传递:
// 这里的amout将会作为mutation的payload传入,在mutation函数中可以通过payload参数获取到
store.commit({type: 'increment',amount: 10
})
- 由于vuex中使用了vue的状态搜集机制,所以vuex中的store是响应式的,所以在使用mutation更新状态时,最好提前在store中将状态进行初始化,如果需要动态添加新的属性时,则需要使用Vue.set()方法去进行添加。同时推荐使用常量替代mutation的事件类型名;
- 关于Mutation的一条原则是,mutation中的handler必须是同步函数,这是因为异步函数中对于store状态的更新会造成状态的不可跟踪.
- mutation的映射辅助函数: mapMutations, 和前面的mapState和mapGetters类似,mapMutation是将组件中使用的一些mutation函数映射到methods方法中:
import { mapMutations } from 'vuex'
export default {methods: {...mapMutations(['increment', // 将 `this.increment()` 映射为 `this.$store.commit('increment')`// `mapMutations` 仍然支持载荷:'incrementBy' // 将 `this.incrementBy(amount)` 映射为 `this.$store.commit('incrementBy', amount)`]),...mapMutations({add: 'increment' // 为映射取别名: 将 `this.add()` 映射为 `this.$store.commit('increment')`})}
}
- 关于Actions:
- actions和mutation类似,都是完成store状态的变更,区别在于action中依旧是去提交mutation进行组件的更新操作(及实际的state更新仍旧是发生mutation的执行函数中); 其次,在action中可以包含任意的异步操作
- actions的方法的第一个参数是一个store的上下文对象(context),可以通过这个context去调用commit() api提交mutation
actions: {// 或者可以使用es6的结构语法直接从context对象中结构出commit apiincrement (context) {context.commit('increment')}}// 在组件中分发action: store.dispatch(), dispatch第一个参数仍旧是action的属性名
- 在actions中执行异步操作: (在action中可以执行异步操作,常被用于从服务端获取数据填充store的场景)
actions: {incrementAsync ({ commit }) {setTimeout(() => {commit('increment')}, 1000)}}
- actions中同样支持和Mutation类似的载荷传参,仍旧可以使用多个参数的形式或者是封装成一个对象的形式:
// 以载荷形式分发
store.dispatch('incrementAsync', {amount: 10
})// 以对象形式分发
store.dispatch({type: 'incrementAsync',amount: 10
})
- Actions的辅助函数: mapActions, 依旧和之前的mapState等类似,将store中的Action函数通过dispatch()的调用映射到methods方法中:
import { mapActions } from 'vuex'export default {methods: {...mapActions(['increment', // 将组件的`this.increment()` 映射为 `this.$store.dispatch('increment')`// `mapActions` 仍旧支持载荷:'incrementBy' // 将组件的`this.incrementBy(amount)` 映射为 `this.$store.dispatch('incrementBy', amount)`]),...mapActions({add: 'increment' // actions的别名: 将 `this.add()` 映射为 `this.$store.dispatch('increment')`})}
}
- 关于Module:
- 当将所有的状态state, mutations, actions等都集中到一个对象中,会造成对象的臃肿和难以维护,为解决这个问题,可以将store分割为多个模块(module),每个模块拥有自己的state, mutations, actions, getters, 甚至可以嵌套子模块
const moduleA = {state: () => ({ ... }),mutations: { ... },actions: { ... },getters: { ... }
}const moduleB = {state: () => ({ ... }),mutations: { ... },actions: { ... }
}const store = new Vuex.Store({modules: {a: moduleA,b: moduleB}
})store.state.a // -> moduleA 的状态
store.state.b // -> moduleB 的状态
- 对于模块内部的mutation和Getter,接收的第一个参数state在模块内将会变成模块内部的局部状态; 而对于模块内的actions, 局部状态通过context.state暴露出来,而根节点的状态则可以使用context.rootState获取; 对于模块内部的getter, 根节点的状态会作为getter的第三个参数(rootState)传递给getter函数
- 模块的命名空间: 默认情况下,模块内部的actions, mutation, getter都是注册在全局命名的,这样多个模块会对同一mutation和action做出响应; 当需要某些模块的mutation或者action等只单独响应,此时可以为模块配置 namespaced: true, 这样的方式会使模块成为带命名空间的模块
- 在带有命名空间的模块内需要访问全局的内容时,rootState和rootGetters会作为getter的第三和第四个参数传递给getter, 也会通过context对象传入action
- 在模块中需要在全局的空间分发action或者是提交mutation, 可以将 {root: true}作为dispatch或者commit的第三个参数传入; 同样在带命名空间的模块中需要将某些action注册为全局的action时,可以为action配置root: true
modules: {namespaced: true,actions: {someAction: {root: true,handler (namespacedContext, payload) { ... } // -> 'someAction'}}}
- 当需要使用映射完成命名空间内的State, getter, Actions, Mutations映射到组件中时,可以将模块的命名空间名称字符串作为第一个参数传递给以上的映射函数,这样所有的绑定都会自定将该模块作为上下文进行映射
// 这里的映射辅助函数的第一个参数都是命名空间的名称字符串
computed: {...mapState('some/nested/module', {a: state => state.a,b: state => state.b})
},
methods: {...mapActions('some/nested/module', ['foo', // -> this.foo()'bar' // -> this.bar()])
}
- 或者通过使用createNamespacedHelpers()方法创建基于某个命名空间的辅助函数,其返回一个对象,这个对象中包含了指定命名空间值上的辅助函数, 这个方法的参数就是模块命名空间的名称字符串:
import { createNamespacedHelpers } from 'vuex'
const { mapState, mapActions } = createNamespacedHelpers('some/nested/module')
// 如此便可以正常使用这里的mapState和mapActions进行针对于命名空间some/nested/module映射
- 动态注册模块: 在完成了模块注册之后,需要动态的添加模块,可以使用store下的registerModule进行动态的注册模块:
import Vuex from 'vuex'
const store = new Vuex.Store({ /* 选项 */ })// 注册模块 `myModule`
store.registerModule('myModule', {// ...
})
// 注册嵌套模块 `nested/myModule`
store.registerModule(['nested', 'myModule'], {// ...
})// 注册之后就可以通过 store.state.myModule 和 store.state.nested.myModule 访问模块的状态。
- 针对于动态注册模块而言,还可以进行动态的卸载模块,可以使用store.unregisterModule(moduleName)来对模块进行动态的卸载; 除此之外,和可以通过store.hasModule(moduleName)检测模块是否被注册到了store中
- 针对于模块重用(就是当一个模块多次被引用的情况),类似于vue中的data属性,当直接使用对象的形式时,由于这个状态对象会通过引用进行共享,导致状态对象被污染的问题,可以使用一个函数来声明模块的状态
理解Vuex源码: 手写Vuex源码核心
let Vue;
// 插件的安装函数, 在Vue.use()的时候会调用这个install方法
const install = (_Vue) => {Vue = _Vue;Vue.mixin({// 使用mixin将store对象混入到所有的组件中beforeCreate(){// 对于根组件,直接从$options中获取,而子组件则需要从父组件的实例上获取if(this.$options.store){this.$store = this.$options.store}else{this.$store = this.$parent.$store}}})
}
// 创建store类:
class Store{constructor(options){this.getters = {}this.mutations = {}this.actions = {};// 通过实例Vue,依赖于vue的状态搜集机制进行状态的监控this.vm = new Vue({data: {state: options.state}})// 通过ModuleCollection类完成store的模块收集this.modules = new ModuleCollection(options)// 将收集的模块进行安装installModule(this, this.state, [], this.modules.root)}// 定义getter函数,获取vm中监听的state状态get state(){return this.vm.state;}// 定义commit函数:commit = (mutationsName, payload) => {// 从this.mutations中取出对应的mutationsName进行执行// 这里直接调用只传递payload进入是因为在安装(installModule)的时候进行了封装处理this.mutations[mutationsName].forEach(fn => fn(payload))}// 定义dispatch函数:dispatch = (actionName, payload) => {// 这里直接调用的原因和上面commit的原因相同this.actions[actionName].forEach(fn => fn(payload))}}
// 定义模块搜集类:
class ModuleCollection{constructor(options){this.resgister([], options)}// register的path参数代表的是当前的module的层级状态:register(path, options){let rawModule = {_raw: options,_children: {},state: options.state}if (!this.root){this.root = rawModule;}else{// 递归,将当前分支所有的模块依次挂载到this.root上形成一棵树形结构let parentModule = path.slice(0, -1).reduce((root, current) => {return root._children[current]}, this.root)// 设置当前模块的路径parentModule._children[path[path.length - 1]] = rawModule;}if (rawModule._raw.modules){// 遍历,将所有的module均搜集到root上forEach(options.modules, (moduleName, value) => {this.reghister(path.concat(moduleName), value)})}}
}
// 自定义一个forEach方法:用于遍历对象
function forEach(obj, callback){Object.keys(obj).forEach((key) => {callback(kay, obj[key])})
}
// 安装模块方法:
function installModule(store, rootState, path, rawModule){let root = store.modules.root; // 获取到最终整个格式化之后的对象结果// 拼接各个模块对应的命名空间let namespace = path.reduce((str, current) => {root = root._children[current]; // 取得当前路径对应的模块str = str + (root._raw.namespaced ? current + "/" : "")return str;}, '')if(path.length > 0){let parentState = path.slice(0, -1).reduce((rootState, current) => {return rootState[current]}, rootState)// 给这个根状态定义当前模块的名字是path中的最后一项Vue.set(parentState, path[path.length - 1], rawModule.state)}// 处理getter: 为store上加上getters属性let getters = rawModule.raw.getters;if(getters){forEach(getters, (getterName, value) => {Object.defineProperty(store.getters, getterName, {get: () => {return value(store.state)}})})}// 处理mutations:let mutations = rawModule._raw.mutations;if(mutations){forEach(mutatioins, (mutationName, value) => {// 为mutationName拼接上命名空间let arr = store.mutations[namespace + mutationName] || (store.mutations[namespace + mutationName] = []);// 将mutationName对应组合成一个数组,方便之后统一调用触发所有对应的mutationNamearr.push((payload) => {value(rawModule.state, payload)})})}// 处理actions:let actions = rawModule._raw.actions;if(actions){forEach(actions, (actionName, value) => {let arr = store.actions[namespace + actionName] || (store.actions[namespace + actionName] = []);arr.push((payload) => {value(store, payload)})})}// 递归,将所有的子模块都依次安装到store上: 每次安装会将path拼接上当前模块名forEach(rawModule._children, (moduleName, module) => {installModule(store, rootState, path.concat(moduleName), module)})
}export default {Store,install
}
Vue脚手架工具: Vue-cli3
简介: Vue-cli是基于vue.js进行快速构建完整系统的工具,能够快速构建起符合实际项目要求的项目开发环境以及生产环境的打包实现。同时基于webpack-dev-server的开发服务帮助提高开发效率.
- 安装使用:
1. 安装:
npm install -g @vue/cli
// OR
yarn global add @vue/cli
2. 检测是否安装成功:
运行: vue --version 能否查看到vue-cli的版本
3. 创建一个项目:
vue create applicationName 之后根据提示选择自己需要的配置
// OR
vue ui 会启动一个服务端口,访问该端口可以以图形化的界面进行项目的创建
4. 在现有项目中安装插件: vue add presetName 如
vue add eslint --config airbnb --lintOn save 添加eslint代码校验
5. 启动项目:
npm run serve OR yarn serve 启动开发环境项目调试
npm run build OR yarn build 打包生产环境项目
由于Vue-cli3的配置内容太多,这里只写可能会常用到的Vue-cli的配置
2. vue-cli3的配置需要单独书写一个vue.config.js文件进行配置编写:
// vue.config.js
module.exports = {// 生产环境打包后项目文件的输出目录outputDir:"dist",// 放置生成的静态资源(js, css, img, fonts)的(相对于outputDir)目录assetsDir:"assets",// lintOnSave: Type: boolean | 'warning' | 'default' | 'error'// 是否在每次保存代码时使用eslint进行代码检查,这个配置对语法要求比较严格lintOnSave: false,// productionSourceMap: Type: boolean// 是否在打包时生成项目的来源映射(SourceMap),设置为false可以显著的减少打包的体积// 关于SourceMap可以关注WebpackproductionSourceMap: false,// publicPath: Type: string, Default: '/'// 配置项目打包的相对路径,这个配置在打包时经常会进行更改,否则会造成一些文件的引入错误publicPath: "./",css: {// extract: Type: boolean | Object Default: 生产环境下是 true,开发环境下是 false// 是否启用css分离插件, 如果不启用css样式分离插件,打包出来的css时通过内联样式的方式注入到DOM中extract: true,// sourceMap: Type: boolean Default: false// 是否启用css样式相关文件的sourceMap(文件来源映射)sourceMap: false,// 向css-loader解析器传递选项loaderOptions: {// 设置引入全局的sass样式文件sass: {data: @import "@/assets/styles/color.scss"}}},// 配置开发环境服务器devServer: {// 服务启动的域名host: "0.0.0.0"// 是否启动热模块加载,就是在每次代码更改时,是否需要重新刷新浏览器才能看到效果hot: true,// 服务启动的监听端口port: "8081",// 是否在项目启动完成后自动打开浏览器显示open: false,// proxy: 配置http代理proxy: {// 表示如果ajax请求的地址: http://api.yuming.com这个地址时,可以直接使用 /api的方式,这个路径会被代理到这个路由,但是在浏览器上看到的仍然时 http://localhost:8080/api/.."/api": {target: "http://api.yuming.com",// 是否允许跨域,这里是在开发环境会起作用,但在生产环境下,还是需要后台处理changeOrigin: true,pathRewrite: {// 这里定义重写规则,会将解析出来的接口地址中的多出来的 api 字符串替换为空串,目的是去穿多余的,否则地址上多个 api 时无法访问地址"/api": ""}}}},// 进行第三方插件的配置pluginOptions: {// 这里定义一个全局的less文件,把公共样式变量放入其中,这样每次使用的时候就不用重新引用了'style-resources-loader': {preProcessor: 'less',patterns: ['./src/assets/public.less']}},// 这里对webpack打包进行一些配置configureWebpack: {resolve: {alias: {// 起个别名,这里类似与使用vue-cli的 @ 指代 src目录一样的效果'views': "@/views"}}}
}
- 当需要更改vue-cli项目为多页面应用时,可以配置pages选项:
module.exports = {pages: {index: {// page页面的入口entry: 'src/index/main.js',// 页面的模板template: '/public/index.html',// 在打包目录dist中生成的文件名字filename: "index.html",// 打包生成的页面的titletitle: "Index Page",// 在这个页面中包含的块,默认情况下会包含// 提取出来的通用 chunk 和 vendor chunk。chunks: ['chunk-vender', 'chunk-comon', 'index']},// 当使用只有入口的字符串格式时,模板会被推导为 `public/subpage.html`,并且如果找不到的话,就回退到 `public/index.html`。// 输出文件名会被推导为 `subpage.html`。subpage: 'src/subpage/main.js'}
}
关于Vue-cli脚手架工具的实现
- 关于脚手架工具的实现是基于Node.js开发的,脚手架的模板基于webpack的打包配置,而node.js开发脚手架工具,会使用到的一些库:
commander: 定义脚手架工具的命令以及一些帮助信息显示等配置
inquirer: 定义命令行交互命令
chalk: 命令行界面输出带样式的提示信息
ora: 命令行显示进度条,用于表示脚手架工具的搭建流程
download-git-repo: 用于从git上拉去一些模板来初始化项目
fs-extra: 文件操作扩展,可对一些特定文件进行特定的操作,可用于处理项目模板的package.json这样的配置文件
- 当然这里只是简单列举了一些开发cli库需要的库,关于开发流程自查。对于脚手架模板的配置和webpack相关,不属于vue范畴,而webpack打包中解析打包vue文件相关的loader为 vue-loader, 更多信息自查。
Vue服务端渲染: (SSR) NUXT.js
关于服务端渲染(SSR)
- 服务端渲染简单理解就是服务器通过预渲染,将服务器数据渲染到页面组件或者页面上生成html字符串,在返回到浏览器渲染显示。此时由于数据和样式已经渲染写入到生成的html字符串中,所以浏览器无需再发送新的请求, 这样也就极大的提高了浏览器首次渲染页面的效率
服务端渲染的优势: -
- 更好的SEO: 浏览器搜索的原理是网络爬虫,只会抓取网页源码,不会执行网站的脚本。使用React或者Vue这样的MVVM框架,页面的多数DOM都是客户端根据js动态生成的,可供爬虫抓取的内容大大减少。浏览器爬虫不会等待页面数据加载渲染完毕之后才抓取页面,因而导致最终结果无法让爬虫匹配搜索内容。服务端渲染返回给客户端的是已经获取数据渲染好的html页面,无需再执行js加载数据渲染页面,这样网络爬虫能够抓取到站点的完整信息。
-
- 用户更友好的首屏渲染: 首屏渲染的页面是服务端发送过来的渲染好的html字符串,无需再加载js或者发送http请求渲染页面,这样用户能够更快的看到页面的内容。尤其针对于单页应用,打包后的文件体积较大,导致客户端渲染加载首屏的时间很长,首页会有很长一段的白屏时间。
服务端渲染的局限
- 用户更友好的首屏渲染: 首屏渲染的页面是服务端发送过来的渲染好的html字符串,无需再加载js或者发送http请求渲染页面,这样用户能够更快的看到页面的内容。尤其针对于单页应用,打包后的文件体积较大,导致客户端渲染加载首屏的时间很长,首页会有很长一段的白屏时间。
-
- 服务端压力增大: 原本通过客户端浏览器渲染的任务也统一交由服务端完成,尤其在高并发的情况下,会占用大量的CPU资源
-
- 开发条件受限: 在服务端渲染项目中,很多浏览器端的api功能无法使用,vue或者React这样的框架的很多能力也不可使用,导致开发受到很多的限制
-
- 增加成本: 开发服务端渲染项目涉及的技术内容增多,除了对webpack, react或vue熟悉外,还需对node服务端的内容有一定的掌握程度。相对于前后端分离的CSR而言,项目的构建,部署过程都会更加的复杂。
关于Nuxt:
- 增加成本: 开发服务端渲染项目涉及的技术内容增多,除了对webpack, react或vue熟悉外,还需对node服务端的内容有一定的掌握程度。相对于前后端分离的CSR而言,项目的构建,部署过程都会更加的复杂。
- Nuxt是一款用于构建vue服务端渲染的框架,帮助快速构建服务端渲染的项目。
- (这里直接以nuxt的脚手架构建项目为例进行记录,单独使用nuxt进行项目构建可自行查阅学习)
- 安装使用
// 创建项目: 在此之前需要先安装nuxt的脚手架工具: create-nuxt-app
npm init nuxt-app ProjectName
// OR
npx create-nuxt-app ProjectName
// OR
yarn create nuxt-app ProjectName// 运行启动项目:
cd ProjectName ->
npm run dev
// OR
yarn dev
关于Nuxt项目的目录结构:
.nuxt // Nuxt自动生成,临时的用于编辑的文件,build
assets // 用于组织未编译的静态资源如LESS、SASS或JavaScript,类似vue项目中的public目录,开发时可以直接访问到目录下的内容
components // 用于自己编写的Vue组件,比如分页组件,轮播组件等
layouts // 布局目录,用于组织应用的布局组件,不可更改
middleware // 用于存放中间件
pages // 用于存放写的页面,我们主要的工作区域
plugins // 用于存放JavaScript插件的地方,比如增加的element-ui组件库等就需要在这里面进行配置
static // 用于存放静态资源文件,比如图片
store // 用于组织应用的Vuex 状态管理
.editorconfig // 开发工具格式配置
.eslintrc.js // ESLint的配置文件,用于检查代码格式
.gitignore // 配置git不上传的文件
nuxt.config.json // 用于组织Nuxt.js应用的个性化配置,已覆盖默认配置
package-lock.json // npm自动生成,用于帮助package的统一设置的,yarn也有相同的操作
package.json // npm 包管理配置文件
- 关于Nuxt的路由:
- 使用create-nuxt-app创建的服务端渲染项目,无需单独配置路由,当需要添加新的页面时,直接在pages文件夹下创建页面文件即可(若是嵌套路由,则创建是也嵌套创建即可),所谓创建及配置,脚手架工会根据pages中的文件自动生成路由配置: 例如:
// pages目录下的文件结构如下:
pages/
--| user/
-----| index.vue
-----| one.vue
--| index.vue
// 此时nuxt自动生成的路由配置如下:routes: [{name: 'index',path: '/',component: 'pages/index.vue'},{name: 'user',path: '/user',component: 'pages/user/index.vue'},{name: 'user-one',path: '/user/one',component: 'pages/user/one.vue'}]}
- 当需要生成动态路由的配置,则须在相应的路由文件下创建相应的以下划线为前缀的vue文件或者目录, 例如需要 /users/:id 这样的动态路由时,创建如下:
// pages下的目录:
pages/
--| users/
-----| _id.vue
// 此时则会生成的配置如下:
router: {routes: [{name: 'users-id',path: '/users/:id?',component: 'pages/users/_id.vue'},]
}
- 关于路由的参数校验: 在创建的路由页面中,可以通过在validate方法中完成路由参数的校验,当校验方法返回false或者promise中的resolve解析为false或者reject掉,nuxt.js将自动加载显示404错误页面或者500错误页面; 以上面定义的 /pages/users/:id页面为例,在_id.vue文件中导出validate方法:
export default {validate({params}) {// 检验params必须为numberreturn /^\d+$/.test(params.id)}
}
- 设置根URL(BaseURL): 当页面的所有的路由都需要加上 /api 这样的前缀时,可以在nuxt.config.js文件中配置其router的base属性(这个文件和vue.vonfig.js类似)
// nuxt.config.js
module.exports = {router: {base: "/api/"}
}
- 页面间完成路由的跳转: 和router-link组件一样,nuxt中提供了一个NuxtLink组件完成路由间的跳转(当然也可以使用a标签,但是官方推荐的仍然是使用NuxtLink这个组件)
// 最基础的路由跳转
<template><NuxtLink to="/">Home Page</NuxtLink>
</template>
// 使用name路由名称跳转:
<template><NuxtLink :to="{name: 'index'}">Home Page</NuxtLink>
</template>
// 带params参数的路由跳转:
<template><NuxtLink :to="{name: 'users', params: {id: 000}}">Home Page</NuxtLink>
</template>
- 关于nuxt.config.js配置文件的配置: (只记录部分常用的)
nuxt.config.js和vue.config.js文件类似,都需要导出一个配置对象
-
- head配置: 在这个配置中可以配置页面全局的head内容(就是html页面的head部分信息)
module.exports = {head: {title: "Nuxt App", // 页面的标题// 页面的meta标签meta: [{charset: "utf-8"},{ name: 'viewport', content: 'width=device-width, initial-scale=1' },{ hid: 'description', name: 'description', content: 'Meta description' }],// 页面全局的link标签link: [//地址栏小图标的引入{ rel: 'icon', type: 'image/x-icon', href: '/logoicon.ico' }// 引入全局的样式文件{ rel: 'stylesheet', type: 'text/css', href: '/styles/color.css'},],// 页面的全局js脚本引入script: [{src: 'https://code.jquery.com/jquery-3.1.1.min.js'},{src: 'https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js'}]}
}
-
- build: 这个配置用来配置nuxt.js的项目构建规则,及webpack的构建配置,如通过vendor引入第三方模块,通过plugin字段配置webpack插件,通过loader配置webpack解析器等等:
module.exports = {build: {// 引入的第三发库,在这里配置后可以使配置只打包一次,能减少应用的bundle文件的体积vendor: ['core-js', 'axios'],// 配置额外的loader解析器loaders: [{test: /\.(scss|sass)$/,use: [{loader: "style-loader"}, {loader: "css-loader"}, {loader: "sass-loader"}]},{test: /\.(png|jpe?g|gif|svg)$/,loader: 'url-loader',query: {limit: 1000,name: 'img/[name].[hash:7].[ext]'}},{test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,loader: 'url-loader',query: {limit: 1000,name: 'fonts/[name].[hash:7].[ext]'}}],// 配置webpack插件plugins: [new webpack.ProvidePlugin({_: 'lodash'})]}
}
-
- css: 在这个配置项中,可以引入一些全局的css样式文件,配置后会在每个页面中都引入该样式
module.exports = {css: [ //该配置项用于定义应用的全局(所有页面均需引用的)样式文件、模块或第三方库。'element-ui/lib/theme-chalk/index.css',//在创建项目的时候安装了elememt插件,这里自动引入插件的默认样式'@/assets/css/reset.css', //引入assets下的reset.css全局标签重置样式'@/assets/animation.css' //引入全局的动画文件],
}
-
- router: 用于覆盖nuxt.js默认的路由配置
1. base: 配置应用的根URL2. extendRoutes: 扩展路由(添加自定义路由), 例如:
router: {// 扩展配置404页面, 这里的配置应该遵循vue-router的模式extendRoutes(routes, resolve){routes.push({name: "custom",path: "*",component: resolve(__dirname, 'pages/404.vue')})}
}3. linkActiveClass: 配置<NuxtLink>组件默认激活时的类名(也就是当NuxtLink组件被点击之后添加的类名)
router: {router: {linkActiveClass: 'active-link'}
}4. middleware: 为应用的每个页面设置默认的中间件, 及在每个页面渲染前都会先执行这里指定的中间件逻辑
module.exports = {router: {// 在每页渲染前运行 middleware/user-agent.js 中间件的逻辑middleware: 'user-agent'}
}
在middleware文件目录下定义的中间件文件导出一个函数,函数接收一个context应用全局上下文对象
export default function (context) {// 给上下文对象增加 userAgent 属性(增加的属性可在 `asyncData` 和 `fetch` 方法中获取)context.userAgent = process.server? context.req.headers['user-agent']: navigator.userAgent
}scrollBehavior: scrollBehavior 配置项用于个性化配置跳转至目标页面后的页面滚动位置。每次页面渲染后都会调用 scrollBehavior 配置的方法。
module.exports = {router: {scrollBehavior(to, from, savedPosition) {// 配置所有的页面在渲染后滚动到顶部return { x: 0, y: 0 }}}
}
-
- loading: nuxt.js提供了一套页面加载进度指示的组件,可以通过此配置项配置加载进度组件的颜色,是否禁用,或者是自定义加载组件
- 配置:
module.exports = {loading: {color: "blue", // 配置进度条颜色failedColor: "red", //页面加载失败时的颜色 (当 data 或 fetch 方法返回错误时)height: '10px', //进度条的高度 (在进度条元素的 style 属性上体现)。throttle: 200, // 在显示进度条之前等待指定的时间。用于防止条形闪烁。duration: 10000, // 进度条的最大显示时长,单位毫秒。Nuxt.js 假设页面在该时长内加载完毕。continuous: true, // 当加载时间超过duration时,保持动画进度条。css: true, // 设置为 false 以删除默认进度条样式(并添加自己的样式)。rtl: false, // 从右到左设置进度条的方向。}}- 自定义加载组件: 在components文件夹下定义好一个加载组件,然后配置loading指定这个组件的路径; 自定义组件需要实现以下方法:
start(): 路由更新(即浏览器地址变化)时调用, 请在该方法内显示组件。
finish(): 路由更新完毕(即asyncData方法调用完成且页面加载完)时调用,请在该方法内隐藏组件。
fail(error): 路由更新失败时调用(如asyncData方法返回异常)。
increase(num): 页面加载过程中调用, num 是小于 100 的整数。module.exports = {loading: '~/components/loading.vue' }
- 关于nuxt框架的一些项目配置处理:
- 生成nuxt项目打包分析: 添加分析后会在打包好的.nuxt/stats目录下生成一个html文件,浏览器打开该文件可以查看到此次打包的相关信息
// 1. 在package中的script中添加命令:
"analyze": "nuxt build --analyze"
// 2. 在nuxt.config.js中添加:
export default {build: {analyza: {analyzeMode: 'static'}}
}
- 为项目提供全局的sass文件:
方法1:
// 1: 安装相关依赖
npm i -S @nuxtjs/style-resources
npm i -D sass-loader node-sass
// 2. 修改nuxt.config.js配置:
export default {modules: ['@nuxtjs/style-resources',],// 这里指定全局引入的sass文件的路径styleResources: {scss: '~/assets/scss/variable.scss'}
}
方法2:
// 1: 安装依赖:
npm i -D nuxt-sass-resources-loader sass-loader node-sass
// 2. 修改nuxt.config.js配置:
export default {// 指定用loader解析sass文件modules: [['nuxt-sass-resources-loader', ['~/assets/scss/variable.scss']]],styleResources: {scss: '~/assets/scss/variable.scss'}
}
- 配置hard-source-webpack-plugin,提升打包效率(这个插件对于第一次打包构建没有提升,但是对于第二次的构建大概能提升90%的构建速率)
// 1: 安装依赖
npm i -D hard-source-webpack-plugin
// 修改nuxt.config.js配置
module.exports = {build: {extractCSS: true,extend(config, ctx) {if (ctx.isDev) {config.plugins.push(new HardSourceWebpackPlugin({cacheDirectory: '.cache/hard-source/[confighash]'}))}}}
}
- 进行Brotli压缩:
// 1: 安装依赖:
npm i shrink-ray-current
// 2. 修改nuxt.config.js配置
export default {render: {http2: {push: true},compressor: shrinkRay()}
}
- 按需导入element-ui组件库:
// 1. 安装element-ui库:
npm i -D babel-plugin-component
// or
yarn add -D babel-plugin-component
// 2. 修改nuxt.config.js配置:
module.exports = {plugins: ['@/plugins/element-ui'],build: {babel: {plugins: [['component',{ libraryName: 'element-ui', styleLibraryName: 'theme-chalk' }]]}},
}
// 3. 在plugins/element-ui.js文件中进行element-ui的按需导入配置:
import Vue from 'vue'
import {Button, Loading, Notification, Message, MessageBox
} from 'element-ui'
import lang from 'element-ui/lib/locale/lang/zh-CN'
import locale from 'element-ui/lib/locale'
// configure language
locale.use(lang)
//set
Vue.use(Loading.directive)
Vue.prototype.$loading = Loading
Vue.prototype.$msgbox = MessageBox
Vue.prototype.$notify = Notification
Vue.prototype.$message = Message// import components
Vue.use(Button);
- 配置全局remjs: 用于页面rem自适应:
// 1. 新建assets/js/global.js文件, 文件内容如下(如此可以使用ip6的屏幕尺寸)
if (process.browser) {document.addEventListener('DOMContentLoaded', () => {const html = document.querySelector('html')let fontSize = window.innerWidth / 10fontSize = fontSize > 50 ? 50 : fontSizehtml.style.fontSize = fontSize + 'px'})
}
// 在nuxt.config.js中进行配置:plugins: ['@/assets/js/global.js'],
- css样式处理配置: (主要是做一些全局的样式适应处理)
// 1. 配置nuxt.config.js
head:{// 禁止缩放{ name: 'viewport', content: 'width=device-width, initial-scale=1, minimum-scale=1.0,maximum-scale=1.0,user-scalable=no' },
}
modules: ['@nuxtjs/style-resources'
],
// 配置全局样式文件路径
styleResources: {scss: ['@/assets/css/global.scss', '@/assets/css/reset.scss']
},// 2. 新建/assets/css/global.scss文件:
@import "./reset";
$ratio: 375 / 10;
// px像素转换为rem尺寸
@function px2rem($px) {@return $px / $ratio + rem;
}// 3. 新建/assets/css/reset.scss文件:
html, body {user-select: none; //禁止用户长按font-family: 'PingFangSC-Light', 'PingFang SC', 'STHeitiSC-Light', 'Helvetica-Light', 'Arial', 'sans-serif';-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}// 4. 在之后的样式文件中就可以使用上面配置好的px2rem()函数。。
- 在nuxt脚手架工具中使用vuex: nuxt内置了vuex,所以无需再次安装,脚手架创建的项目中,已经有了store目录。还是那句话,在nuxt中创建及配置,在store目录下创建使用vuex有两种方式:
-
- 普通方式: 创建store/index.js文件,该文件返回vuex.Store的实例,和在vue项目中使用类似
-
- 模块方式: store目录下的每个js文件会被转化为状态树指定命名的子模块(index是跟模块) 也就是说store下文js文件需要按照vuex模块导出文件的形式导出相应的state, mutations, actions等信息
- 关于具体的配置细节前面的vuex部分已经说过,这里不再赘述,只需要在store文件夹下创建相关的文件后就相当于项目启动了vuex
Koa2 + Vue自制vue服务端渲染架构:(项目使用包管理工具yarn):
服务端渲染简介:
客户端渲染不利于SEO优化
服务端渲染是可以被爬虫抓取到的,客户端异步渲染则难以被爬虫抓取
SSR直接将渲染好的html字符传递给浏览器, 大大加快了首屏加载的时间
SSR会占用服务端更多的CPU和内存资源
一些常见的浏览器API将无法正常使用
在Vue中支持beforeCreate和created两个生命周期
-
安装服务端koa依赖: yarn add koa koa-router koa-static
-
安装vue相关的依赖: yarn add vue vue-router vue-server-renderer
-
初始化koa服务;
const Koa = require("koa")
const Router = require("koa-router")
const Static = require("koa-static")const app = new Koa();
const router = new Router()router.get("*", ctx => {ctx.body = 'hello world'
})
app.use(router.routes());
app.listen(3000)
- 在服务中引入vue相关的依赖,同时创建vue实例和创建vue的一个渲染器
const Vue = require("vue")
// 关于vueServerRender; 这是Vue提供的专门用于服务端渲染处理的包
const VueServerRender = require("vue-server-renderer")
// new 一个vue的实例:
const vm = new Vue({data() {return { msg: "hello 2" }},template: `{{msg}}`
})
// 创建一个渲染器
let render = VueServerRender.createRenderer()
- 使用渲染器在路由中渲染Vue实例进行返回:
// 渲染器的renderToString方法可以将一个Vue实例渲染为html字符串,这是一个异步的方法,需要使用async和await
router.get("/", async ctx => {ctx.body = await render.renderToString(vm)
})
- 构建网页html模板:
/*** 1. 创建一个template.html的文件(文件名随意)* 2. 在html文件的body中添加一个 <--vue-ssr-outlet-->,它有点类似于vue-router在html中添加的占位符,之后渲染时这个地方就将会被替换成渲染出的html字符串* 3. 在server.js服务文件(就是创建koa app实例的文件)中读取这个html文件的内容,然后作为参数传递给VueServerRender.createRenderer();这里的意思是vue-server-renderer将会使用哪个模板进行渲染。注意这里需要读取出模板字符串传递给它,而不是模板的路径*/
const fs = require("fs")
// 读取出html模板
const template = fs.readFileSync("./template.html", "utf8");
// 创建一个渲染器
let render = VueServerRender.createRenderer({template
})
- 文件拆分: 按照Vue-cli构建vue项目的样子,拆分出Vue的相关文件放到src目录下,将需要渲染的html文件放到public文件下(实际基本和Vue-cli创建的形式差不多)
/*** 7.1 创建src目录和public目录,src目录下创建main.js文件,App.vue文件,components组件文件夹,在public目录下创建一个index.html文件,并在其body中添加一个id为app的div容器*//*** 7.2 main.js文件:*/
import Vue from "vue"
import App from "./App";// 提供Vue的实例: 使用一个工厂的形式产生Vue实例
// 这样的好处是在服务端渲染的时候使得每个客户端都可以获得一个独立的vue实例,而不至于造成同一个vue实例多个用户的问题
export default () => {const app = new Vue({render: h => h(App)})return { app }
}
/*** App.vue文件(其实就是基本的App.vue文件)*/
<template><div>App.Vue<Bar /></div>
</template>
<script>import Bar from "./components/Bar";export default {components: {Bar}};
</script>
- 安装配置webpack用于打包客户端的代码,及刚创建的src下的内容打包为client代码用于后续处理:
工具安装:
yarn add webpack webpack-cli webpack-dev-server babel-loader @babel/preset-env @babel/core vue-style-loader css-loader vue-loader vue-template-compiler html-webpack-plugin webpack-merge
工具简单说明:
webpack: webpack打包文件
webpack-cli: webpack命令行解析工具,处理webpack相关的命令
webpack-dev-server: webpack提供的开发环境,用于启动一个服务等操作
babel-loader: 处理js语法
@babel/preset-env: 转化js高级语法
@babel/core: babel的核心库,使用babel这个是必须的
vue-style-loader: 解析css样式,使用vue-style-loader是它可以支持服务端渲染,其他和style-loader一样
css-loader: 对css样式进行处理
vue-loader: 解析vue文件
vue-template-compiler: 解析vue模板编译
html-webpack-plugin: 打包处理html文件
webpack-merge: 用于处理多个webpack文件的合并
- 初步配置webpack.config.js: 使值能够完成基本的客户端vue项目的打包:
// webpack专用配置文件
const path = require("path")
const VueLoader = require("vue-loader/lib/plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const resolve = (dir) => {return path.resolve(__dirname, dir)
}module.exports = {// webpack入口entry: resolve("./src/main.js"),output: {filename: "bundle.js",path: resolve("./dist")},resolve: {extensions: ['.js', '.vue']},module: {rules: [{test: /\.js$/,use: {loader: 'babel-loader',options: {presets: ["@babel/preset-env"]}},exclude: /node_modules/},{test: /\.css$/,use: ["vue-style-loader", "css-loader"]},{test: /\.vue$/,use: 'vue-loader'}]},plugins: [// 使用vue-loader中的插件处理 .vue文件new VueLoader(),new HtmlWebpackPlugin({filename: "index.html",template: resolve("./public/index.html")})]
}
- 分离: 打包两套代码,一套给客户端的请求使用,一套给服务端渲染使用:
- 客户端入口文件:(在src文件下新建一个client-entry.js客户端打包的入口文件)
// client-entry.js文件
import CreateApp from "./main"const { app } = createApp();
app.$mount("#app")
- 服务端入口文件: (在src文件夹下新建一个server.entry.js进行服务端文件打包)
import createApp from "./main"// 服务端需要调用当前这个文件产生一个新的vue的实例
// 服务端配置好后需要导出给node来使用
export default () => {const { app } = createApp()return app;
}
- 分离webpack打包文件: webpack.base.js && webpack.client.js && webpack.server.js
// webpack.base.js
// 基础的webpack配置,无论是客户端打包还是服务端打包都要基于这个
const path = require("path")
const VueLoader = require("vue-loader/lib/plugin")
const resolve = (dir) => {return path.resolve(__dirname, dir)
}module.exports = {output: {filename: "[name].bundle.js",path: resolve("../dist")},resolve: {extensions: ['.js', '.vue']},module: {rules: [{test: /\.js$/,use: {loader: 'babel-loader',options: {presets: ["@babel/preset-env"]}},exclude: /node_modules/},{test: /\.css$/,use: ["vue-style-loader", "css-loader"]},{test: /\.vue$/,use: 'vue-loader'}]},plugins: [// 使用vue-loader中的插件处理 .vue文件new VueLoader(),]
}
//webpack.client.js
const merge = require("webpack-merge")
const base = require("./webpack.base")
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const resolve = (dir) => {return path.resolve(__dirname, dir)
}
module.exports = merge(base, {// webpack入口entry: {client: resolve("../src/client-entry.js")},plugins: [new HtmlWebpackPlugin({filename: "index.html",template: resolve("../public/index.html")})]
})
const merge = require("webpack-merge")
const base = require("./webpack.base")
const path = require("path")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const resolve = (dir) => {return path.resolve(__dirname, dir)
}
module.exports = merge(base, {// webpack入口entry: {server: resolve("../src/server-entry.js")},target: "node", // 这里表示要给node来使用output: {libraryTarget: "commonjs2" // 将这个文件最终导出的结果放到module.exports上,让node来进行使用},plugins: [new HtmlWebpackPlugin({filename: "index.ssr.html",template: resolve("../public/index.ssr.html"),excludeChunks: ['server'] // 排除某个模块不被html引入})]
})
- 更新Koa服务端代码,让其渲染webpack打包生成的bundle文件生成渲染好的html返回:
const Koa = require("koa")
const Router = require("koa-router")
const Static = require("koa-static")
const app = new Koa();
const router = new Router()
const fs = require("fs")const VueServerRender = require("vue-server-renderer")
let serverBundle = fs.readFileSync("./dist/server.bundle.js", "utf8");
let template = fs.readFileSync("./dist/index.ssr.html", "utf8");
// 渲染webpack打包后的结果
const render = VueServerRender.createBundleRenderer(serverBundle, {template
});router.get("/", async ctx => {ctx.body = await new Promise((resolve, reject) => {render.renderToString((err, data) => {if (err) reject(err);resolve(data);})})
})app.use(router.routes());
app.listen(3000)
- 此时打包出的结果返回到客户端的是字符串,因为使用的是renderToString,所以此时客户端的页面是没法进行事件等操作的,所以此时需要将打包生成的客户端的js文件进行引入
// 在server.js文件中加入:
const path = require("path")
// 进行静态目录托管,此时当客户端请求静态文件资源时可以到dist目录中进行查找返回
// 这里主要的目的是为了让服务端渲染的代码引入打包生成的客户端端代码文件
app.use(static(path.resolve(__dirname, "dist")))
- 此时因为引入的客户端代码会将vue挂载在id为app的节点上,但是使用服务端打包的文件中并没有这个节点,所以客户端代码引入后仍没法挂载,此时需要在App.vue文件上将根节点设置为id为app的节点(Vue上管这个叫客户端激活)
<template><div id="app">App.Vue<Bar /></div>
</template>
<script>
import Bar from "./components/Bar";
export default {components: {Bar}
};
</script>
- 处理打包文件:(因为这里需要在服务端生成的html文件中引入客户端打包生成的js文件,为了手动去进行引入,这里对打包配置文件进行处理)
- 修改webpack.client.js配置文件:
// 添加生成客户端打包的manifest.json的打包文件,这个插件是vue-server-render提供的
const ClientRenderPlugin = require("vue-server-renderer/client-plugin")// ...在plugins中添加这个插件:
new ClientRenderPlugin()
- 修改webpack.server.js配置文件,生成服务端打包生成的bundle和相应文件的json文件
const ServerRenderPlugin = require("vue-server-renderer/server-plugin")
// ... plugins中添加插件
new ServerRenderPlugin()
- 修改server.js文件中createBundleRenderer()处理的内容
// 此时的bundle引入为打包生成的服务端的json文件的内容
const serverBundle = require("./dist/vue-ssr-server-bundle.json")
// 引入打包生成的客户端的manifest文件
const clientManifest = require("./dist/vue-ssr-client-manifest.json")
// createBundleRenderer()该函数处理改成这样的目的是为了读取到服务端渲染生成的文件中,
// 去插入从clientManifest文件中读取出的客户端的打包内容进行自动的引入
const render = VueServerRender.createBundleRenderer(serverBundle, {template,clientManifest
});
- 集成路由系统:
- vue ssr中的路由跳转的规则:
在浏览器首屏渲染的时候,以及在浏览器中输入url进行切换的时候将会走到服务端渲染处理路由,而单纯的在页面间进行路由的切换则将交由客户端引入的客户端vue文件中的路由进行接管,此时切换的就是客户端的路由,不走服务端的流程. 这样的好处的是降低服务端的开销,以及用户访问的体验。
(17.1) 创建vue路由文件,在其中进行路由的配置,如下代码所示:
import Vue from "vue"
import VueRouter from "vue-router"
import Bar from "./components/Bar"
Vue.use(VueRouter)// 导出的仍然需要是一个函数工厂,这样每个客户端就会生成各自的一套路由系统
export default () => {const router = new VueRouter({mode: "history",routes: [{path: "/",component: Bar},{path: "/foo",component: () => import("./components/Foo")}]})return router;
}
(17.2) 在main.js文件中创建vue实例的时候将router注册进去:
import createRouter from "./router"
// ...
export default () => {const router = createRouter();const app = new Vue({router,render: h => h(App)})// 导出router的目的是为了之后在服务端渲染的时候拿到这个路由进行匹配切换页面return { app, router }
}
- 这里需要注意: 此时客户端访问页面是没法进行路由切换的,这是因为此时访问的时候路由系统将会新进入到Koa中的路由匹配中去,而不是客户端的路由,所以此时需要在服务端进行路由的搜集处理
(17.3)处理server-entry.js文件中导出的方法:
import createApp from "./main"export default (context) => {// 服务端渲染时将会执行此方法const { app, router } = createApp()// 将context中传递过来的路由参数取出,调用push方法后进行路由切换,也就是渲染这个页面router.push(context.url)return app;
}
(17.4) 修改server.js文件:
router.get("/", async ctx => {ctx.body = await new Promise((resolve, reject) => {// 这里renderToString()中会调用执行的是server-entry.js中导出的方法,所以传递的参数{url: "/"}就会传递给server-entry.js中的函数render.renderToString({url: "/"}, (err, data) => {if (err) reject(err);resolve(data);})})
})// ...在路由匹配之后添加以下逻辑
// 如果匹配不到任何的路由,将会执行到此处进行处理
// 而如果服务器中没有路由匹配,则会渲染app.vue文件
app.use(async ctx => {ctx.body = await new Promise((resolve, reject) => {render.renderToString({url: ctx.url}, (err, data) => {if (err) reject(err);resolve(data);})})
})
- 这里补充提一下的是传递给renderToStrong()方法的第一个参数将会被传递给server-entry中导出的函数
(17.5) 当切换异步组件时,对server-entry中高勇promise进行处理,同时对不存在的路由进行错误处理:(这里主要通过监听路由跳转的ready事件进行异步路由组件的处理)
export default (context) => {return new Promise((resolve, reject) => {// 服务端将会执行此方法const { app, router } = createApp()// 将context中传递过来的路由参数取出,调用push方法后进行路由切换,也就是渲染这个页面router.push(context.url);// 涉及到异步组件的问题,在路由匹配跳转成功之后在resolve出app实例router.onReady(() => {// 获取当前跳转到的匹配的组件let matches = router.getMatchedComponents();// 如果未匹配到路由,则返回404if(matches.length === 0){reject({code: 404})}resolve(app);}, reject)})
}
(17.6) 在路由未在router中匹配时(就是在server-entry中通过判断是否有匹配到组件判断reject出的{code: 404}时的情况,需要在server.js文件中渲染时进行错误捕获来返回404)
// server.js文件:
app.use(async ctx => {// 通过try..catch来捕获路由错误try{ctx.body = await new Promise((resolve, reject) => {render.renderToString({url: ctx.url}, (err, data) => {if (err) reject(err);resolve(data);})})}catch(e){ctx.body = "404 Not Found"ctx.status = 404;}
})
- 这里理一下这个未匹配路由异常的流程: 路由匹配 -> 进入server-entry中的函数执行,如果检测到未匹配的路由,则reject({code: 404})掉 -> 返回到renderToString()方法中,其err参数有值,所以在此方法中再次reject抛出异常 -> 在catch中异常被捕获,返回404页面
- 集成Vuex:
- 注意思路: 创建好Vuex的store仓库后,主要需要注意的是在服务端的处理问题。在vue页面组件文件中可以使用一个asyncData()的方法,它接收一个store实例,可以在这个方法中实现在服务端渲染时获取数据处理,所以就需要在server-entry.js文件中处理这个方法:
(18.1) 在src/store目录下创建获取store实例的方法:
import Vuex from "vuex"
import Vue from "vue"
Vue.use(Vuex)export default () => {const store = new Vuex.Store({state: {name: "MrChenGX"},mutations: {changeName(state, payload){state.name = payload;}},actions: {changeName({commit}, payload){return new Promise((resolve, reject) => {// 这里使用Promise的目的是为了处理异步的请求setTimeout(() => {commit("changeName", payload),resolve()}, 1000)})}}})return store;
}
(18.2) 在main.js文件中将store注册到导出的vue实例上
import createStore from "./store"
// ...
export default () => {const router = createRouter();const store = createStore();const app = new Vue({router,store,render: h => h(App)})// 导出router和store的目的是为了在之后服务端中做相应的处理return { app, router, store }
}
(18.3) 在entry中匹配出路由页面组件时,判断组件中是否有设置asyncData方法,如果有,则调用其执行,并将store实例传递给这个方法处理:
export default (context) => {return new Promise((resolve, reject) => {const { app, router, store } = createApp()router.push(context.url);router.onReady(() => {// 获取当前跳转到的匹配的页面组件,当匹配到值时,这里的matches就是相应的组件内容let matches = router.getMatchedComponents();if(matches.length === 0){reject({code: 404})}// 这里处理vuex中数据获取:// 当匹配到路由组件跳转时,判断组件中是否要执行asyncData获取数据,如果需要,则调用这个asyncData方法去执行数据获取等操作,而在这个asyncData方法中一般返回的是一个Promise,同时对于匹配的页面组件可能是多个,所以这里应该是一个Promise的数组,所以就需要使用Promise.all()在所有的asyncData数据处理好之后在进行渲染appPromise.all(matches.map(component => {if(component.asyncData){return component.asyncData(store)}})).then(() => {// 在上面的all中将会改变store中的state,所以获取store中的新的state挂到context上下文中// 而context.state上的数据将会被处理挂载到window对象上作为一个window.__INITIAL_STATE__属性值,所以这里设置的context.state是固定的写法context.state = store.state;resolve(app);})}, reject)})
}
- 这里值得注意的就是在处理asyncData方法时,其返回值会是一个Promise(这个Promise就是在actions里面返回的对象),而且一个路由可能匹配的是一个组件数组,所以所有的asyncData()得到的就会是一个promise数组,所以这里使用的是Promise.all()方法让所有的Promise执行完.在Promise.all()中执行的逻辑将会改变store中的state
(18.4) 当在服务端调用asyncData获取数据更新store中的state后,处理了新的state放到了context.state上,通过renderToString的处理,会将这个state的值挂载到客户端的window对象上,所以在客户端获取store对象实例的时候,需要判断在window对象上是否有挂载上的__INITIAL_STATE__属性,如果有,表示在服务端已经获取了新的state数据,那么此时就需要取得这个__INITIAL_STATE__属性的数据替换掉生成的store对象中的state:
// 在创建store的文件中(store/index.js)的创建完store对象实例后添加如下内容//当在浏览器中执行获取store的时候,则需要判断window对象上是否有 window.__INITIAL_STATE__属性// 如果有,表明在服务端渲染的时候对store中的数据进行的更改,那么此时就需要取得 window.__INITIAL_STATE__的值替换掉state中的值if(typeof window !== "undefined" && window.__INITIAL_STATE__){store.replaceState(window.__INITIAL_STATE__)}
(18.5) 在一个组件中写一个asyncData进行测试实现:
<script>
export default {asyncData(store){ // asyncData这个方法只会在服务端执行,并只会在页面组件中执行return store.dispatch("changeName", "chenshao")},methods: {handleClick() {alert("hello world");}}
};
</script>
(18.6) 此时存在一个问题: 就是不走服务端的路由,也就是从其他页面通过客户端路由切换到需要使用asyncData的页面组件中时,里面的asyncData()方法就不会走服务端的流程,也就不会获得数据,那么此时可以试着在客户端的组件生命周期中也去调用asyncData()方法来获取数据解决这个问题
Vue3展望:(vue3的新特性)
- 目前vue3的beta版本已经出来了,但是具体完整的稳定版的时间和更新后的RFC的时间未知,这里对vue3中的一些新特性简单说明一下:
- 更小的代码压缩体积
- 响应式的重写: Object.defineProperty -> Proxy
- Object.defineProperty是一个相对比较昂贵的操作,因为它直接操作的是对象的属性,颗粒度更小。vue3将其使用Proxy代理进行拦截,在目标对象上架上一层拦截,代理的对象是对象而非属性,这样原本对属性的操作变为对对象的操作,颗粒度更大。 同时proxy不需要对原始对象做太多的操作,提高了可优化性
- 重构Virtual DOM:
- VDOM的本质是一个抽象层对象,用js对象描述界面渲染的样子。
- 传统vDOm的性能瓶颈:
虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vdom 树。
传统 vdom 的性能跟模版大小正相关,跟动态节点的数量无关。在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。
JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足
- vue的特点是底层为Virtual DOM,上层包含有大量静态信息的模版。为了兼容手写 render function,最大化利用模版静态信息,vue3.0采用了动静结合的解决方案,将vdom的操作颗粒度变小,每次触发更新不再以组件为单位进行遍历,主要更改如下
将模版基于动态节点指令切割为嵌套的区块
每个区块内部的节点结构是固定的
每个区块只需要以一个 Array 追踪自身包含的动态节点
- Composition API:
- 新版vue3取消了vue2的options配置形式的写法,而改用函数的形势进行书写,这样的好处在于更好的支持typeScript的类型推断,其次是提升代码的可复用性;除此之外,按需导入需要使用到的Vue的api,而不是像vue2一样从全局的vue中import进来;
- 碎片化的节点Fragment:
- 在vue3之前的组件中都必须存在一个根节点,vue3引入Fragment的概念,可以摒弃掉vue2的根节点的书写,减少一些无用的节点
这里是我对于本人vue技术栈的内容整理记录,对于vue源码部分解析不写入到这里
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
