基本使用
/**
* 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可以渲染一个html文本字符串,但是在使用v-html之后会覆盖掉该节点内部的子元素节点
* v-html存在XSS的风险
*/
<p v-html="rawHtml">
<span>有 XSS 的风险</span>
<span>【注意】使用v-html之后,将会覆盖掉子元素</span>
</p>
// ...
data() {
return {
rawHtml: "指令: v-html 加粗 斜体"
}
}
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属于是一个侦听器,它和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属性为true
deep: 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自定义事件时,会在他的事件中心去寻找对应的事件执行,但是因为回调函数时定义在父组件作用域中的,所以在其中可以更新父组件中的状态
// $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,没有则创建一个空数组push
if(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中判断条件可以是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: 会将所有的节点都加入到页面的DOM树中,不管是否匹配,如果是匹配的节点,则会被显示出来,如果不匹配,则会设置该节点的 display: none对该节点进行隐藏
对于两者的选择,如果页面中的内容只渲染一次,或者是页面的内容切换不频繁,那么选择v-if。 如果页面的内容频繁的切换,则选择v-show更加合适
- 父组件通过属性的形式将一些参数值传递给子组件,在子组件中使用Props对父组件传递的属性值进行接收。
- Props可以为一个数组形式或者为一个对象:当为数组形式时,数组的每个元素就时需要接收的参数名,但是此时父组件可以选择不传递一个参数,相当于这个参数值是没有限制的
- 当Props值为一个对象时,可以在对象中进行一些特殊的指定, 如比较常见的: type(props参数的类型), defaut(props参数的默认值,如果不传递就使用这个默认值), required(该参数是否不需要传递,为一个boolean值) 等等
// 父组件
<Chid :foo="100" />
//子组件中接收:
props: {
foo: {
type: Number,
required: true
}
}
- Props上面已经说过,是用于完成父组件向子组件的传值
- $emit()也在之前自定义事件的时候说过了,子组件通过触发父组件传递的自定义事件完成父组件中状态的更新(当然还有.sync修饰符)
- 这也算是目前比较常用的父子组件间通信的方式(例子这里不再介绍)
- 对于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是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")
}
}
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指令进行自定义
当父子组件间进行通信,需要子组件中使用输入框对父组件中的属性值进行操作等类似的需求,可以使用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: ""
}
}
- 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 = 1
let observer = new MutationObserver(nextTickHandler)
// 构造一个textNode并让其改变,使得每次都能触发observer的回调函数
let textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
// 让counter每次改变
counter = (counter + 1) % 2
textNode.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插槽中接收显示的是该组件使用时内部的子元素内容
// 子组件: 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>
// 这里以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是Vue的内置组件,能在组件切换的过程中将状态保留在内存中,防止重复的渲染DOM
- keep-alive用来包裹动态组件时,会缓存当前不活动的组件实例,而不会销毁它们。
- keep-alive是一个抽象组件,它自身不会渲染DOM元素,也不会出现在父组件的节点链中。
- 如果需要缓存路由渲染的组件时,可以在配置路由时配置meta属性,设置keepAlive值为true则会将组件缓存
routes: [
{
path: "/index",
component: index,
meta: {
keepAlive: true}
}
]
需要动态缓存router-view中的部分组件时(就是希望某些组件被缓存,某些组件不被缓存),可以使用以下两种方法:
- 使用 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属性来实现
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){
// 设置下一个路由的meta
to.meta.keepAlive = true;
next()
}
}
keep-alive组件的两个钩子函数(上面已经用到), 分别是activated()和deactivated(), 它们分别在组件被激活时和组件被隐藏缓存时被触发
- 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混入机制针对组件配置项的不同选项的混入机制存在差异,这里从源码层面依次展开:
// src/core/util/options.js
const strats = config.optionMergeStrategies
if(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
}
// 生命周期函数
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]
: parentVal
return res ? dedupeHooks(res) : res
}
strats.data = function(parentVal: any, childVal: any, vm?: Component) :?Function{
if(!vm){
// 如果子组件的data不是一个function,在dev环境下报出警告,同时返回父配置的data
if (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类型,则递归合并对象
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上查找,以此类推
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 = undefined
if (!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属性触发时,按照数组的顺序从头到尾调用触发,所以其触发顺序是: 全局混入 -> 实例混入 -> 组件混入
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){
// 继续合并childVal
extend(ret, childVal)
}
return ret;
}
如上可见:props, methods, computed, inject的合并策略和components相似,都是使用extend方法合并为一个对象,按照从子到父进行合并,所以在调用时child的优先级更高些
strats.provide = mergeDataOrFn // 这里就是之前在data合并中使用到的那个合并方法
mixin混合机制的优势和弊端:
优势: 当多个组件之间存在通用的可复用的组件逻辑时,可以使用mixin对项目进行抽离,提升代码的可复用性
弊端: mixin的混入可能会造成属性变量的污染,导致组件的渲染和实际的预期出现差异;变量来源不明确,造成代码的可读性不高; mixin和组件之间可能存在多对多的关系,导致代码的复杂度增加
这里将从插件使用,手写插件源码两层面记录
简介: 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)
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()方法会向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.push()类似,唯一不同是replace()不会向history中添加新的记录,而是替换掉当前的history记录,所以对于replace()api, 点击浏览器的前进和后退是无效的
router.replace()的参数使用和router.push()一样
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"
}
<router-link :to="{path: '/abc'}" ></router-link>
<router-link :to="{path: '/abc'}" replace></router-link>
// 使用append属性的坑: 使用append属性需要path指定的路径最开始不加 /, 否则append路由会失效
<router-link :to="{ path: 'b' }" append></router-link>
<router-link :to="{path: '/about'}" tag="button">About</router-link>
// 当路由切换到 /home, 该路由标签将会被添加上 active 的类
<router-link :to="/home" exact-active-class="active">
较为完整的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属性值)
base属性用于设置页面的基路由,及当前的所有的路由都在 /app/ 下, 可以设置base属性为: base: “/app”
Vue-router的生命周期:
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();
}
rourer.afterEach(function(to,from){
//路由跳转后,我们可以做一些操作
//例如: 根据path或者meta添加新属性,去判断,改变文档标题
if(to.path=='/'){
window.document.title = 'xxx'
}else if{
...
}else{
...
}
});
{
path:'xxx'
component:'xxx',
beforeEnter(to,from,next){
//只是针对xxx这个path,做操作
next(); //next必须执行,否则会卡住
}
},
//访问导航时候,执行这个路由执行的第一的函数
//这个时候组件还没有初始化,无法访问this
beforeRouteEnter(to,from,next){
console.log(this) //undefined
next(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.router
this.$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的执行触发页面render
this.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模式,使用pushState
window.location.hash = url;
}
}
简介: Vuex是一个专为vue.js应用程序开发得状态管理模式。采用集中式的存储管理应用的所有组件状态,并以相应的规则保证状态以一种可以预测的方式发生变化。
当需要构建大型的Vue单页应用,或者是应用的逻辑够复杂时, 选用Vuex进行组件的状态管理是一个好的选择。
安装使用:
npm install Vuex
import Vuex from "vuex"
Vue.use(Vuex)
// ... 创建store
new Vue({
el: "#app",
store
})
const store = new Vuex.Store({
state: {
// store中的状态
count: 0
},
mutations: {
// 修改状态的mutation: vuex中的状态值不能直接修改,只能通过触发mutation或者派发action去进行修改
increment (state) {
state.count++
}
}
})
computed: {
// 使用计算属性包装后的state状态值,会在state状态发生改变时触发重新计算获取新的状态值,从而完成组件的更新
count(){
return this.$store.state.count;
}
}
import {
mapState } from "vuex"
// ...
computed: mapState({
// 当参数属性值书写为函数时,可以接收到store中的state作为参数
count: state => state.count,
// 直接为参数写为字符串形式时,会等同于: state => state.count
countAlias: "count",
// 当需要在属性中使用到当前组件实例的this,则需要使用普通函数:
countPlusLocalState(state) {
return state.count + this.localCount
}
})
// 当映射的计算属性名称和state中的名称相同时,可以直接使用字符串数组的形式进行设置
// 如此即会生成相对应的state状态的计算属性
computed: mapState(["count"])
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中函数的返回值
getters: {
doneTodosCount: (state, getters) => {
return getters.doneTodos.length
}
}
// 在组件中的使用仍可以通过属性的形式进行使用: store.getters.getter属性名
getters: {
// 返回一个函数,在外部使用时就相当于调用这个函数
getTodoById: (state) => (id) => {
return state.todos.find(todo => todo.id === id)
}
}
// 外部使用: store.getters.getTodoById(1)
// 这里需要注意的是,getter通过方法访问时,每次状态更新都会重新调用,而不会缓存计算结果
import {
mapGetters } from "vuex"
export default {
// ...
computed: {
// mapGetters的参数和mapState的参数设置是一样的,可以是对象或者数组的形式
...mapGetters(["doneTodoCount"])
}
}
const store = new Vuex.Store({
state: {
count: 1
},
mutations: {
increment (state) {
// 变更状态
state.count++
}
}
})
store.commit("increment")
mutations: {
// mutation的第二个参数就是外部commit()提交时传入的参数
increment (state, n) {
state.count += n
}
}
// 外部使用时: store.commit("increment", 10), commit()的第二个参数将会作为mutation的第二个参数传递给相应的mutation handler执行函数
// 多数情况下,为了程序的可读性,常将多个payload参数封装成一个对象进行一次性传递
// 这里的amout将会作为mutation的payload传入,在mutation函数中可以通过payload参数获取到
store.commit({
type: 'increment',
amount: 10
})
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: {
// 或者可以使用es6的结构语法直接从context对象中结构出commit api
increment (context) {
context.commit('increment')
}
}
// 在组件中分发action: store.dispatch(), dispatch第一个参数仍旧是action的属性名
actions: {
incrementAsync ({
commit }) {
setTimeout(() => {
commit('increment')
}, 1000)
}
}
// 以载荷形式分发
store.dispatch('incrementAsync', {
amount: 10
})
// 以对象形式分发
store.dispatch({
type: 'incrementAsync',
amount: 10
})
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')`
})
}
}
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 的状态
modules: {
namespaced: true,
actions: {
someAction: {
root: true,
handler (namespacedContext, payload) {
... } // -> 'someAction'
}
}
}
// 这里的映射辅助函数的第一个参数都是命名空间的名称字符串
computed: {
...mapState('some/nested/module', {
a: state => state.a,
b: state => state.b
})
},
methods: {
...mapActions('some/nested/module', [
'foo', // -> this.foo()
'bar' // -> this.bar()
])
}
import {
createNamespacedHelpers } from 'vuex'
const {
mapState, mapActions } = createNamespacedHelpers('some/nested/module')
// 如此便可以正常使用这里的mapState和mapActions进行针对于命名空间some/nested/module映射
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 访问模块的状态。
理解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对应组合成一个数组,方便之后统一调用触发所有对应的mutationName
arr.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-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可以关注Webpack
productionSourceMap: 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"
}
}
}
}
module.exports = {
pages: {
index: {
// page页面的入口
entry: 'src/index/main.js',
// 页面的模板
template: '/public/index.html',
// 在打包目录dist中生成的文件名字
filename: "index.html",
// 打包生成的页面的title
title: "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脚手架工具的实现
commander: 定义脚手架工具的命令以及一些帮助信息显示等配置
inquirer: 定义命令行交互命令
chalk: 命令行界面输出带样式的提示信息
ora: 命令行显示进度条,用于表示脚手架工具的搭建流程
download-git-repo: 用于从git上拉去一些模板来初始化项目
fs-extra: 文件操作扩展,可对一些特定文件进行特定的操作,可用于处理项目模板的package.json这样的配置文件
关于服务端渲染(SSR)
// 创建项目: 在此之前需要先安装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 包管理配置文件
// 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'
}
]
}
// pages下的目录:
pages/
--| users/
-----| _id.vue
// 此时则会生成的配置如下:
router: {
routes: [
{
name: 'users-id',
path: '/users/:id?',
component: 'pages/users/_id.vue'
},
]
}
export default {
validate({
params}) {
// 检验params必须为number
return /^\d+$/.test(params.id)
}
}
// nuxt.config.js
module.exports = {
router: {
base: "/api/"
}
}
// 最基础的路由跳转
<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和vue.config.js文件类似,都需要导出一个配置对象
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'}
]
}
}
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'
})
]
}
}
module.exports = {
css: [ //该配置项用于定义应用的全局(所有页面均需引用的)样式文件、模块或第三方库。
'element-ui/lib/theme-chalk/index.css',//在创建项目的时候安装了elememt插件,这里自动引入插件的默认样式
'@/assets/css/reset.css', //引入assets下的reset.css全局标签重置样式
'@/assets/animation.css' //引入全局的动画文件
],
}
1. base: 配置应用的根URL
2. 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 }
}
}
}
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, // 从右到左设置进度条的方向。
}
}
start(): 路由更新(即浏览器地址变化)时调用, 请在该方法内显示组件。
finish(): 路由更新完毕(即asyncData方法调用完成且页面加载完)时调用,请在该方法内隐藏组件。
fail(error): 路由更新失败时调用(如asyncData方法返回异常)。
increase(num): 页面加载过程中调用, num 是小于 100 的整数。
module.exports = {
loading: '~/components/loading.vue'
}
// 1. 在package中的script中添加命令:
"analyze": "nuxt build --analyze"
// 2. 在nuxt.config.js中添加:
export default {
build: {
analyza: {
analyzeMode: 'static'
}
}
}
// 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'
}
}
// 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]'
})
)
}
}
}
}
// 1: 安装依赖:
npm i shrink-ray-current
// 2. 修改nuxt.config.js配置
export default {
render: {
http2: {
push: true
},
compressor: shrinkRay()
}
}
// 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);
// 1. 新建assets/js/global.js文件, 文件内容如下(如此可以使用ip6的屏幕尺寸)
if (process.browser) {
document.addEventListener('DOMContentLoaded', () => {
const html = document.querySelector('html')
let fontSize = window.innerWidth / 10
fontSize = fontSize > 50 ? 50 : fontSize
html.style.fontSize = fontSize + 'px'
})
}
// 在nuxt.config.js中进行配置:
plugins: [
'@/assets/js/global.js'
],
// 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()函数。。
服务端渲染简介:
客户端渲染不利于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)
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()
// 渲染器的renderToString方法可以将一个Vue实例渲染为html字符串,这是一个异步的方法,需要使用async和await
router.get("/", async ctx => {
ctx.body = await render.renderToString(vm)
})
/**
* 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
})
/**
* 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>
工具安装:
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专用配置文件
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")
})
]
}
// client-entry.js文件
import CreateApp from "./main"
const {
app } = createApp();
app.$mount("#app")
import createApp from "./main"
// 服务端需要调用当前这个文件产生一个新的vue的实例
// 服务端配置好后需要导出给node来使用
export default () => {
const {
app } = createApp()
return app;
}
// 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引入
})
]
})
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)
// 在server.js文件中加入:
const path = require("path")
// 进行静态目录托管,此时当客户端请求静态文件资源时可以到dist目录中进行查找返回
// 这里主要的目的是为了让服务端渲染的代码引入打包生成的客户端端代码文件
app.use(static(path.resolve(__dirname, "dist")))
<template>
<div id="app">
App.Vue
<Bar />
</div>
</template>
<script>
import Bar from "./components/Bar";
export default {
components: {
Bar
}
};
</script>
// 添加生成客户端打包的manifest.json的打包文件,这个插件是vue-server-render提供的
const ClientRenderPlugin = require("vue-server-renderer/client-plugin")
// ...在plugins中添加这个插件:
new ClientRenderPlugin()
const ServerRenderPlugin = require("vue-server-renderer/server-plugin")
// ... plugins中添加插件
new ServerRenderPlugin()
// 此时的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
});
(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 }
}
(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);
})
})
})
(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();
// 如果未匹配到路由,则返回404
if(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;
}
})
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数据处理好之后在进行渲染app
Promise.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)
})
}
(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()方法来获取数据解决这个问题
虽然 Vue 能够保证触发更新的组件最小化,但在单个组件内部依然需要遍历该组件的整个 vdom 树。
传统 vdom 的性能跟模版大小正相关,跟动态节点的数量无关。在一些组件整个模版内只有少量动态节点的情况下,这些遍历都是性能的浪费。
JSX 和手写的 render function 是完全动态的,过度的灵活性导致运行时可以用于优化的信息不足
将模版基于动态节点指令切割为嵌套的区块
每个区块内部的节点结构是固定的
每个区块只需要以一个 Array 追踪自身包含的动态节点
这里是我对于本人vue技术栈的内容整理记录,对于vue源码部分解析不写入到这里