Tiny'Wo | 小窝

网络中的一小块自留地

面试官:请描述下你对vue生命周期的理解?在created和mounted这两个生命周期中请求数据有什么区别呢�?

一、生命周期是什�?

生命周期(Life Cycle)的概念应用很广泛,特别是在政治、经济、环境、技术、社会等诸多领域经常出现,其基本涵义可以通俗地理解为“从摇篮到坟墓”(Cradle-to-Grave)的整个过程在Vue中实例从创建到销毁的过程就是生命周期,即指从创建、初始化数据、编译模板、挂载Dom→渲染、更新→渲染、卸载等一系列过程我们可以把组件比喻成工厂里面的一条流水线,每个工人(生命周期)站在各自的岗位,当任务流转到工人身边的时候,工人就开始工作PS:在Vue生命周期钩子会自动绑�?this 上下文到实例中,因此你可以访问数据,�?property 和方法进行运算这意味着*你不能使用箭头函数来定义一个生命周期方�? (例如 created: () => this.fetchTodos())

二、生命周期有哪些

Vue生命周期总共可以分为8个阶段:创建前后, 载入前后,更新前后,销毁前销毁后,以及一些特殊场景的生命周期

生命周期 描述
beforeCreate 组件实例被创建之�?
created 组件实例已经完全创建
beforeMount 组件挂载之前
mounted 组件挂载到实例上去之�?
beforeUpdate 组件数据发生变化,更新之�?
updated 组件数据更新之后
beforeDestroy 组件实例销毁之�?
destroyed 组件实例销毁之�?
activated keep-alive 缓存的组件激活时
deactivated keep-alive 缓存的组件停用时调用
errorCaptured 捕获一个来自子孙组件的错误时被调用

三、生命周期整体流�?

Vue生命周期流程�?

具体分析

beforeCreate -> created

  • 初始化vue实例,进行数据观�?
    created

  • 完成数据观测,属性与方法的运算,watchevent事件回调的配�?- 可调用methods中的方法,访问和修改data数据触发响应式渲染dom,可通过computedwatch完成数据计算

  • 此时vm.$el 并没有被创建

created -> beforeMount

  • 判断是否存在el选项,若不存在则停止编译,直到调用vm.$mount(el)才会继续编译

  • 优先级:render > template > outerHTML

  • vm.el获取到的是挂载DOM�?
    beforeMount

  • 在此阶段可获取到vm.el

  • 此阶段vm.el虽已完成DOM初始化,但并未挂载在el选项�?
    beforeMount -> mounted

  • 此阶段vm.el完成挂载,vm.$el生成的DOM替换了el选项所对应的DOM

mounted

  • vm.el已完成DOM的挂载与渲染,此刻打印vm.$el,发现之前的挂载点及内容已被替换成新的DOM

beforeUpdate

  • 更新的数据必须是被渲染在模板上的(eltemplaterender之一�?- 此时view层还未更�?- 若在beforeUpdate中再次修改数据,不会再次触发更新方法

updated

  • 完成view层的更新

  • 若在updated中再次修改数据,会再次触发更新方法(beforeUpdateupdated�?
    beforeDestroy

  • 实例被销毁前调用,此时实例属性与方法仍可访问

destroyed

  • 完全销毁一个实例。可清理它与其它实例的连接,解绑它的全部指令及事件监听器
  • 并不能清除DOM,仅仅销毁实�?

使用场景分析

生命周期 描述
beforeCreate 执行时组件实例还未创建,通常用于插件开发中执行一些初始化任务
created 组件初始化完毕,各种数据可以使用,常用于异步数据获取
beforeMount 未执行渲染、更新,dom未创�?
mounted 初始化结束,dom已创建,可用于获取访问数据和dom元素
beforeUpdate 更新前,可用于获取更新前各种状�?
updated 更新后,所有状态已是最�?
beforeDestroy 销毁前,可用于一些定时器或订阅的取消
destroyed 组件已销毁,作用同上

四、题外话:数据请求在created和mouted的区�?

created是在组件实例一旦创建完成的时候立刻调用,这时候页面dom节点并未生成;mounted是在页面dom节点渲染完毕之后就立刻执行的。触发时机上created是比mounted要更早的,两者的相同点:都能拿到实例对象的属性和方法�?讨论这个问题本质就是触发的时机,放在mounted中的请求有可能导致页面闪动(因为此时页面dom结构已经生成),但如果在页面加载前完成请求,则不会出现此情况。建议对页面内容的改动放在created生命周期当中�?

参考文�?

面试官VUE系列总进度:4�?3

面试官:说说你对vue的理解?

面试官:说说你对SPA(单页应用)的理解?

面试官:说说你对双向绑定的理解?

面试官:说说你对vue的mixin的理解,有什么应用场景?

一、mixin是什�?

Mixin是面向对象程序设计语言中的类,提供了方法的实现。其他类可以访问mixin类的方法而不必成为其子类

Mixin类通常作为功能模块使用,在需要该功能时“混入”,有利于代码复用又避免了多继承的复�?

Vue中的mixin

先来看一下官方定�?> mixin(混入),提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能�?
本质其实就是一个js对象,它可以包含我们组件中任意功能选项,如datacomponentsmethods createdcomputed等等

我们只要将共用的功能以对象的方式传入 mixins选项中,当组件使�?mixins对象时所有mixins对象的选项都将被混入该组件本身的选项中来

Vue中我们可�?*局部混�?�?全局混入

局部混�?

定义一个mixin对象,有组件optionsdatamethods属�?

1
2
3
4
5
6
7
8
9
10
var myMixin = {
created: function () {
this.hello()
},
methods: {
hello: function () {
console.log('hello from mixin!')
}
}
}

组件通过mixins属性调用mixin对象

1
2
3
Vue.component('componentA',{
mixins: [myMixin]
})

该组件在使用的时候,混合了mixin里面的方法,在自动执行created生命钩子,执行hello方法

全局混入

通过Vue.mixin()进行全局的混�?

1
2
3
4
5
Vue.mixin({
created: function () {
console.log("全局混入")
}
})

使用全局混入需要特别注意,因为它会影响到每一个组件实例(包括第三方组件)

PS:全局混入常用于插件的编写

注意事项�?

当组件存在与mixin对象相同的选项的时候,进行递归合并的时候组件的选项会覆盖mixin的选项

但是如果相同选项为生命周期钩子的时候,会合并成一个数组,先执行mixin的钩子,再执行组件的钩子

二、使用场�?

在日常的开发中,我们经常会遇到在不同的组件中经常会需要用到一些相同或者相似的代码,这些代码的功能相对独立

这时,可以通过Vuemixin功能将相同或者相似的代码提出�?
举个例子

定义一个modal弹窗组件,内部通过isShowing来控制显�?

1
2
3
4
5
6
7
8
9
10
11
12
13
const Modal = {
template: '#modal',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}

定义一个tooltip提示框,内部通过isShowing来控制显�?

1
2
3
4
5
6
7
8
9
10
11
12
13
const Tooltip = {
template: '#tooltip',
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}

通过观察上面两个组件,发现两者的逻辑是相同,代码控制显示也是相同的,这时候mixin就派上用场了

首先抽出共同代码,编写一个mixin

1
2
3
4
5
6
7
8
9
10
11
12
const toggle = {
data() {
return {
isShowing: false
}
},
methods: {
toggleShow() {
this.isShowing = !this.isShowing;
}
}
}

两个组件在使用上,只需要引入mixin

1
2
3
4
5
6
7
8
9
const Modal = {
template: '#modal',
mixins: [toggle]
};

const Tooltip = {
template: '#tooltip',
mixins: [toggle]
}

通过上面小小的例子,让我们知道了Mixin对于封装一些可复用的功能如此有趣、方便、实�?

三、源码分�?

首先从Vue.mixin入手

源码位置�?src/core/global-api/mixin.js

1
2
3
4
5
6
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}

主要是调用merOptions方法

源码位置�?src/core/util/options.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export function mergeOptions (
parent: Object,
child: Object,
vm?: Component
): Object {

if (child.mixins) { // 判断有没有mixin 也就是mixin里面挂mixin的情�?有的话递归进行合并
for (let i = 0, l = child.mixins.length; i < l; i++) {
parent = mergeOptions(parent, child.mixins[i], vm)
}
}

const options = {}
let key
for (key in parent) {
mergeField(key) // 先遍历parent的key 调对应的strats[XXX]方法进行合并
}
for (key in child) {
if (!hasOwn(parent, key)) { // 如果parent已经处理过某个key 就不处理�? mergeField(key) // 处理child中的key 也就parent中没有处理过的key
}
}
function mergeField (key) {
const strat = strats[key] || defaultStrat
options[key] = strat(parent[key], child[key], vm, key) // 根据不同类型的options调用strats中不同的方法进行合并
}
return options
}

从上面的源码,我们得到以下几点:

  • 优先递归处理 mixins

  • 先遍历合并parent 中的key,调用mergeField方法进行合并,然后保存在变量options

  • 再遍�?child,合并补�?parent 中没有的key,调用mergeField方法进行合并,保存在变量options

  • 通过 mergeField 函数进行了合�?
    下面是关于Vue的几种类型的合并策略

  • 替换�?- 合并�?- 队列�?- 叠加�?

替换�?

替换型合并有propsmethodsinjectcomputed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
strats.props =
strats.methods =
strats.inject =
strats.computed = function (
parentVal: ?Object,
childVal: ?Object,
vm?: Component,
key: string
): ?Object {
if (!parentVal) return childVal // 如果parentVal没有值,直接返回childVal
const ret = Object.create(null) // 创建一个第三方对象 ret
extend(ret, parentVal) // extend方法实际是把parentVal的属性复制到ret�? if (childVal) extend(ret, childVal) // 把childVal的属性复制到ret�? return ret
}
strats.provide = mergeDataOrFn

同名的propsmethodsinjectcomputed会被后来者代�?

合并�?

和并型合并有:data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
strats.data = function(parentVal, childVal, vm) {    
return mergeDataOrFn(
parentVal, childVal, vm
)
};

function mergeDataOrFn(parentVal, childVal, vm) {
return function mergedInstanceDataFn() {
var childData = childVal.call(vm, vm) // 执行data挂的函数得到对象
var parentData = parentVal.call(vm, vm)
if (childData) {
return mergeData(childData, parentData) // �?个对象进行合�?
} else {
return parentData // 如果没有childData 直接返回parentData
}
}
}

function mergeData(to, from) {
if (!from) return to
var key, toVal, fromVal;
var keys = Object.keys(from);
for (var i = 0; i < keys.length; i++) {
key = keys[i];
toVal = to[key];
fromVal = from[key];
// 如果不存在这个属性,就重新设�? if (!to.hasOwnProperty(key)) {
set(to, key, fromVal);
}
// 存在相同属性,合并对象
else if (typeof toVal =="object" && typeof fromVal =="object") {
mergeData(toVal, fromVal);
}
}
return to
}

mergeData函数遍历了要合并�?data 的所有属性,然后根据不同情况进行合并�?

  • 当目�?data 对象不包含当前属性时,调�?set 方法进行合并(set方法其实就是一些合并重新赋值的方法�?- 当目�?data 对象包含当前属性并且当前值为纯对象时,递归合并当前对象值,这样做是为了防止对象存在新增属�?

队列�?

队列性合并有:全部生命周期和watch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function mergeHook (
parentVal: ?Array<Function>,
childVal: ?Function | ?Array<Function>
): ?Array<Function> {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal
}

LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook
})

// watch
strats.watch = function (
parentVal,
childVal,
vm,
key
) {
// work around Firefox's Object.prototype.watch...
if (parentVal === nativeWatch) { parentVal = undefined; }
if (childVal === nativeWatch) { childVal = undefined; }
/* istanbul ignore if */
if (!childVal) { return Object.create(parentVal || null) }
{
assertObjectType(key, childVal, vm);
}
if (!parentVal) { return childVal }
var ret = {};
extend(ret, parentVal);
for (var key$1 in childVal) {
var parent = ret[key$1];
var child = childVal[key$1];
if (parent && !Array.isArray(parent)) {
parent = [parent];
}
ret[key$1] = parent
? parent.concat(child)
: Array.isArray(child) ? child : [child];
}
return ret
};

生命周期钩子和watch被合并为一个数组,然后正序遍历一次执�?

叠加�?

叠加型合并有:componentdirectivesfilters

1
2
3
4
5
6
7
8
9
10
11
12
13
14
strats.components=
strats.directives=

strats.filters = function mergeAssets(
parentVal, childVal, vm, key
) {
var res = Object.create(parentVal || null);
if (childVal) {
for (var key in childVal) {
res[key] = childVal[key];
}
}
return res
}

叠加型主要是通过原型链进行层层的叠加

小结�?

  • 替换型策略有propsmethodsinjectcomputed,就是将新的同名参数替代旧的参数
  • 合并型策略是data, 通过set方法进行合并和重新赋�?- 队列型策略有生命周期函数和watch,原理是将函数存入一个数组,然后正序遍历依次执行
  • 叠加型有componentdirectivesfilters,通过原型链进行层层的叠加

参考文�?

面试官:Vue常用的修饰符有哪些有什么应用场�?

一、修饰符是什�?

在程序世界里,修饰符是用于限定类型以及类型成员的声明的一种符�?
Vue中,修饰符处理了许多DOM事件的细节,让我们不再需要花大量的时间去处理这些烦恼的事情,而能有更多的精力专注于程序的逻辑处理

vue中修饰符分为以下五种�?

  • 表单修饰�?- 事件修饰�?- 鼠标按键修饰�?- 键值修饰符
  • v-bind修饰�?

二、修饰符的作�?

表单修饰�?

在我们填写表单的时候用得最多的是input标签,指令用得最多的是v-model

关于表单的修饰符有如下:

  • lazy
  • trim
  • number

lazy

在我们填完信息,光标离开标签的时候,才会将值赋予给value,也就是在change事件之后再进行信息同�?

1
2
<input type="text" v-model.lazy="value">
<p>{{value}}</p>

trim

自动过滤用户输入的首空格字符,而中间的空格不会过滤

1
<input type="text" v-model.trim="value">

number

自动将用户的输入值转为数值类型,但如果这个值无法被parseFloat解析,则会返回原来的�?

1
<input v-model.number="age" type="number">

事件修饰�?

事件修饰符是对事件捕获以及目标进行了处理,有如下修饰符:

  • stop
  • prevent
  • self
  • once
  • capture
  • passive
  • native

stop

阻止了事件冒泡,相当于调用了event.stopPropagation方法

1
2
3
4
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
//只输�?

prevent

阻止了事件的默认行为,相当于调用了event.preventDefault方法

1
<form v-on:submit.prevent="onSubmit"></form>

self

只当�?event.target 是当前元素自身时触发处理函数

1
<div v-on:click.self="doThat">...</div>

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,�?v-on:click.prevent.self 会阻�?*所有的点击**,�?v-on:click.self.prevent 只会阻止对元素自身的点击

once

绑定了事件以后只能触发一次,第二次就不会触发

1
<button @click.once="shout(1)">ok</button>

capture

使事件触发从包含这个元素的顶层开始往下触�?

1
2
3
4
5
6
7
8
9
10
11
12
13
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输出结构: 1 2 4 3

passive

在移动端,当我们在监听元素滚动事件的时候,会一直触发onscroll事件会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰�?

1
2
3
4
<!-- 滚动事件的默认行�?(即滚动行�? 将会立即触发 -->
<!-- 而不会等�?`onScroll` 完成 -->
<!-- 这其中包�?`event.preventDefault()` 的情�?-->
<div v-on:scroll.passive="onScroll">...</div>

不要�?.passive �?.prevent 一起使�?因为 .prevent 将会被忽略,同时浏览器可能会向你展示一个警告�?>
passive 会告诉浏览器你不想阻止事件的默认行为

native

让组件变成像html内置标签那样监听根元素的原生事件,否则组件上使用 v-on 只会监听自定义事�?

1
<my-component v-on:click.native="doSomething"></my-component>

使用.native修饰符来操作普通HTML标签是会令事件失效的

鼠标按钮修饰�?

鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:

  • left 左键点击
  • right 右键点击
  • middle 中键点击
1
2
3
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>

键盘修饰�?

键盘修饰符是用来修饰键盘事件(onkeyuponkeydown)的,有如下�?
keyCode存在很多,但vue为我们提供了别名,分为以下两种:

  • 普通键(enter、tab、delete、space、esc、up…�?- 系统修饰键(ctrl、alt、meta、shift…�?
    1
    2
    // 只有按键为keyCode的时候才触发
    <input type="text" @keyup.keyCode="shout()">

还可以通过以下方式自定义一些全局的键盘码别名

1
Vue.config.keyCodes.f2 = 113

v-bind修饰�?

v-bind修饰符主要是为属性进行操作,用来分别有如下:

  • async
  • prop
  • camel

async

能对props进行一个双向绑�?

1
2
//父组�?<comp :myMessage.sync="bar"></comp> 
//子组�?this.$emit('update:myMessage',params);

以上这种方法相当于以下的简�?

1
2
3
4
5
6
7
8
9
//父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){
this.bar = e;
}
//子组件js
func2(){
this.$emit('update:myMessage',params);
}

使用async需要注意以下两点:

  • 使用sync的时候,子组件传递的事件名格式必须为update:value,其中value必须与子组件中props中声明的名称完全一�?
  • 注意带有 .sync 修饰符的 v-bind 不能和表达式一起使�?
  • �?v-bind.sync 用在一个字面量的对象上,例�?v-bind.sync=”{ title: doc.title }”,是无法正常工作�?

props

设置自定义标签属性,避免暴露数据,防止污染HTML结构

1
<input id="uid" title="title1" value="1" :index.prop="index">

camel

将命名变为驼峰命名法,如将 view-Box属性名转换�?viewBox

1
<svg :viewBox="viewBox"></svg>

三、应用场�?

根据每一个修饰符的功能,我们可以得到以下修饰符的应用场景�?

  • .stop:阻止事件冒�?- .native:绑定原生事�?- .once:事件只执行一�?- .self :将事件绑定在自身身上,相当于阻止事件冒�?- .prevent:阻止默认事�?- .caption:用于事件捕�?- .once:只触发一�?- .keyCode:监听特定键盘按�?- .right:右�?

参考文�?

面试官:Vue实例挂载的过�?

一、思�?

我们都听过知其然知其所以然这句�?
那么不知道大家是否思考过new Vue()这个过程中究竟做了些什么?

过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等�?

一、分�?

首先找到vue的构造函�?
源码位置:src\core\instance\index.js

1
2
3
4
5
6
7
8
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}

options是用户传递过来的配置项,如data、methods等常用的方法

vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定定义了很多初始化方�?

1
2
3
4
initMixin(Vue);     // 定义 _init
stateMixin(Vue); // 定义 $set $get $delete $watch �?eventsMixin(Vue); // 定义事件 $on $once $off $emit
lifecycleMixin(Vue);// 定义 _update $forceUpdate $destroy
renderMixin(Vue); // 定义 _render 返回虚拟dom

首先可以看initMixin方法,发现该方法在Vue原型上定义了_init方法

源码位置:src\core\instance\init.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}

// a flag to avoid this being observed
vm._isVue = true
// merge options
// 合并属性,判断初始化的是否是组件,这里合并主要�?mixins �?extends 的方�? if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else { // 合并vue属�? vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
// 初始化proxy拦截�? initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
// 初始化组件生命周期标志位
initLifecycle(vm)
// 初始化组件事件侦�? initEvents(vm)
// 初始化渲染方�? initRender(vm)
callHook(vm, 'beforeCreate')
// 初始化依赖注入内容,在初始化data、props之前
initInjections(vm) // resolve injections before data/props
// 初始化props/data/method/watch/methods
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
// 挂载元素
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}

仔细阅读上面的代码,我们得到以下结论�?

  • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到

  • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

  • 挂载方法是调用vm.$mount方法

initState方法是完成props/data/method/watch/methods的初始化

源码位置:src\core\instance\state.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export function initState (vm: Component) {
// 初始化组件的watcher列表
vm._watchers = []
const opts = vm.$options
// 初始化props
if (opts.props) initProps(vm, opts.props)
// 初始化methods方法
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
// 初始化data
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

我们和这里主要看初始化data的方法为initData,它与initState在同一文件�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function initData (vm: Component) {
let data = vm.$options.data
// 获取到组件上的data
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
data = {}
process.env.NODE_ENV !== 'production' && warn(
'data functions should return an object:\n' +
'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
vm
)
}
// proxy data on instance
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// 属性名不能与方法名重复
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
// 属性名不能与state名称重复
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
`The data property "${key}" is already declared as a prop. ` +
`Use prop default value instead.`,
vm
)
} else if (!isReserved(key)) { // 验证key值的合法�? // 将_data中的数据挂载到组件vm�?这样就可以通过this.xxx访问到组件上的数�? proxy(vm, `_data`, key)
}
}
// observe data
// 响应式监听data是数据的变化
observe(data, true /* asRootData */)
}

仔细阅读上面的代码,我们可以得到以下结论�?

  • 初始化顺序:propsmethodsdata

  • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

关于数据响应式在这就不展开详细说明

上文提到挂载方法是调用vm.$mount方法

源码位置�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取或查询元�? el = el && query(el)

/* istanbul ignore if */
// vue 不允许直接挂载到body或页面文档上
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}

const options = this.$options
// resolve template/el and convert to render function
if (!options.render) {
let template = options.template
// 存在template模板,解析vue模板文件
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template)
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && !template) {
warn(
`Template element not found or is empty: ${options.template}`,
this
)
}
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
if (process.env.NODE_ENV !== 'production') {
warn('invalid template option:' + template, this)
}
return this
}
} else if (el) {
// 通过选择器获取元素内�? template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
/**
* 1.将temmplate解析ast tree
* 2.将ast tree转换成render语法字符�? * 3.生成render方法
*/
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
return mount.call(this, el, hydrating)
}

阅读上面代码,我们能得到以下结论�?

  • 不要将根元素放到body或者html�?
  • 可以在对象中定义template/render或者直接使用templateel表示元素选择�?
  • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

template的解析步骤大致分为以下几步:

  • html文档片段解析成ast描述�?
  • ast描述符解析成字符�?
  • 生成render函数

生成render函数,挂载到vm上后,会再次调用mount方法

源码位置:src\platforms\web\runtime\index.js

1
2
3
4
5
6
7
8
9
// public mount method
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
// 渲染组件
return mountComponent(this, el, hydrating)
}

调用mountComponent渲染组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
vm.$el = el
// 如果没有获取解析的render函数,则会抛出警�? // render是解析模板文件生成的
if (!vm.$options.render) {
vm.$options.render = createEmptyVNode
if (process.env.NODE_ENV !== 'production') {
/* istanbul ignore if */
if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
vm.$options.el || el) {
warn(
'You are using the runtime-only build of Vue where the template ' +
'compiler is not available. Either pre-compile the templates into ' +
'render functions, or use the compiler-included build.',
vm
)
} else {
// 没有获取到vue的模板文�? warn(
'Failed to mount component: template or render function not defined.',
vm
)
}
}
}
// 执行beforeMount钩子
callHook(vm, 'beforeMount')

let updateComponent
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
updateComponent = () => {
const name = vm._name
const id = vm._uid
const startTag = `vue-perf-start:${id}`
const endTag = `vue-perf-end:${id}`

mark(startTag)
const vnode = vm._render()
mark(endTag)
measure(`vue ${name} render`, startTag, endTag)

mark(startTag)
vm._update(vnode, hydrating)
mark(endTag)
measure(`vue ${name} patch`, startTag, endTag)
}
} else {
// 定义更新函数
updateComponent = () => {
// 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
vm._update(vm._render(), hydrating)
}
}
// we set this to vm._watcher inside the watcher's constructor
// since the watcher's initial patch may call $forceUpdate (e.g. inside child
// component's mounted hook), which relies on vm._watcher being already defined
// 监听当前组件状态,当有数据变化时,更新组件
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
// 数据更新引发的组件更�? callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
hydrating = false

// manually mounted instance, call mounted on self
// mounted is called for render-created child components in its inserted hook
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
return vm
}

阅读上面代码,我们得到以下结论:

  • 会触发beforeCreate钩子
  • 定义updateComponent渲染页面视图的方�?- 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

updateComponent方法主要执行在vue初始化时声明的renderupdate方法

render的作用主要是生成vnode

源码位置:src\core\instance\render.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 定义vue 原型上的render方法
Vue.prototype._render = function (): VNode {
const vm: Component = this
// render函数来自于组件的option
const { render, _parentVnode } = vm.$options

if (_parentVnode) {
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
)
}

// set parent vnode. this allows render functions to have access
// to the data on the placeholder node.
vm.$vnode = _parentVnode
// render self
let vnode
try {
// There's no need to maintain a stack because all render fns are called
// separately from one another. Nested component's render fns are called
// when parent component is patched.
currentRenderingInstance = vm
// 调用render方法,自己的独特的render方法�?传入createElement参数,生成vNode
vnode = render.call(vm._renderProxy, vm.$createElement)
} catch (e) {
handleError(e, vm, `render`)
// return error render result,
// or previous vnode to prevent render error causing blank component
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
try {
vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
} catch (e) {
handleError(e, vm, `renderError`)
vnode = vm._vnode
}
} else {
vnode = vm._vnode
}
} finally {
currentRenderingInstance = null
}
// if the returned array contains only a single node, allow it
if (Array.isArray(vnode) && vnode.length === 1) {
vnode = vnode[0]
}
// return empty vnode in case the render function errored out
if (!(vnode instanceof VNode)) {
if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
warn(
'Multiple root nodes returned from render function. Render function ' +
'should return a single root node.',
vm
)
}
vnode = createEmptyVNode()
}
// set parent
vnode.parent = _parentVnode
return vnode
}

_update主要功能是调用patch,将vnode转换为真实DOM,并且更新到页面�?
源码位置:src\core\instance\lifecycle.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
const vm: Component = this
const prevEl = vm.$el
const prevVnode = vm._vnode
// 设置当前激活的作用�? const restoreActiveInstance = setActiveInstance(vm)
vm._vnode = vnode
// Vue.prototype.__patch__ is injected in entry points
// based on the rendering backend used.
if (!prevVnode) {
// initial render
// 执行具体的挂载逻辑
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
} else {
// updates
vm.$el = vm.__patch__(prevVnode, vnode)
}
restoreActiveInstance()
// update __vue__ reference
if (prevEl) {
prevEl.__vue__ = null
}
if (vm.$el) {
vm.$el.__vue__ = vm
}
// if parent is an HOC, update its $el as well
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
vm.$parent.$el = vm.$el
}
// updated hook is called by the scheduler to ensure that children are
// updated in a parent's updated hook.
}

三、结�?

  • new Vue的时候调用会调用_init方法

    • 定义 $set $get$delete$watch 等方�? - 定义 $on$off$emit$off 等事�? - 定义 _update$forceUpdate$destroy生命周期
  • 调用$mount进行页面的挂�?- 挂载的时候主要是通过mountComponent方法

  • 定义updateComponent更新函数

  • 执行render生成虚拟DOM

  • _update将虚拟DOM生成真实DOM结构,并且渲染到页面�?

参考文�?

面试官:Vue中的$nextTick有什么作用?

一、NextTick是什�?

官方对其的定�?

在下�?DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后�?DOM

什么意思呢�?
我们可以理解成,Vue 在更�?DOM 时是异步执行的。当数据发生变化,Vue将开启一个异步更新队列,视图需要等队列中所有数据变化完成之后,再统一进行更新

举例一�?
Html结构

1
<div id="app"> {{ message }} </div>

构建一个vue实例

1
2
3
4
5
6
const vm = new Vue({
el: '#app',
data: {
message: '原始�?
}
})

修改message

1
2
3
this.message = '修改后的�?'
this.message = '修改后的�?'
this.message = '修改后的�?'

这时候想获取页面最新的DOM节点,却发现获取到的是旧�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
console.log(vm.$el.textContent) // 原始�?```

这是因为`message`数据在发现变化的时候,`vue`并不会立刻去更新`Dom`,而是将修改数据的操作放在了一个异步操作队列中

如果我们一直修改相同数据,异步操作队列还会进行去重

等待同一事件循环中的所有数据变化完成之后,会将队列中的事件拿来进行处理,进行`DOM`的更�?
#### 为什么要有nexttick

举个例子
```js
{{num}}
for(let i=0; i<100000; i++){
num = i
}

如果没有 nextTick 更新机制,那�?num 每次更新值都会触发视图更�?上面这段代码也就是会更新10万次视图),有了nextTick机制,只需要更新一次,所以nextTick本质是一种优化策�?

二、使用场�?

如果想要在修改数据后立刻得到更新后的DOM结构,可以使用Vue.nextTick()

第一个参数为:回调函数(可以获取最近的DOM结构�?
第二个参数为:执行函数上下文

1
2
3
4
// 修改数据
vm.message = '修改后的�?
// DOM 还没有更�?console.log(vm.$el.textContent) // 原始的�?Vue.nextTick(function () {
// DOM 更新�? console.log(vm.$el.textContent) // 修改后的�?})

组件内使�?vm.$nextTick() 实例方法只需要通过this.$nextTick(),并且回调函数中�?this 将自动绑定到当前�?Vue 实例�?

1
2
3
4
5
this.message = '修改后的�?
console.log(this.$el.textContent) // => '原始的�?
this.$nextTick(function () {
console.log(this.$el.textContent) // => '修改后的�?
})

$nextTick() 会返回一�?Promise 对象,可以是用async/await完成相同作用的事�?

1
2
3
4
this.message = '修改后的�?
console.log(this.$el.textContent) // => '原始的�?
await this.$nextTick()
console.log(this.$el.textContent) // => '修改后的�?

三、实现原�?

源码位置:/src/core/util/next-tick.js

callbacks也就是异步操作队�?
callbacks新增回调函数后又执行了timerFunc函数,pending是用来标识同一个时间只能执行一�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve;

// cb 回调函数会经统一处理压入 callbacks 数组
callbacks.push(() => {
if (cb) {
// �?cb 回调函数执行加上�?try-catch 错误处理
try {
cb.call(ctx);
} catch (e) {
handleError(e, ctx, 'nextTick');
}
} else if (_resolve) {
_resolve(ctx);
}
});

// 执行异步延迟函数 timerFunc
if (!pending) {
pending = true;
timerFunc();
}

// �?nextTick 没有传入函数参数的时候,返回一�?Promise 化的调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve;
});
}
}

timerFunc函数定义,这里是根据当前环境支持什么方法则确定调用哪个,分别有�?
Promise.thenMutationObserversetImmediatesetTimeout

通过上面任意一种方法,进行降级操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
//判断1:是否原生支持Promise
const p = Promise.resolve()
timerFunc = () => {
p.then(flushCallbacks)
if (isIOS) setTimeout(noop)
}
isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
isNative(MutationObserver) ||
MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
//判断2:是否原生支持MutationObserver
let counter = 1
const observer = new MutationObserver(flushCallbacks)
const textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
timerFunc = () => {
counter = (counter + 1) % 2
textNode.data = String(counter)
}
isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
//判断3:是否原生支持setImmediate
timerFunc = () => {
setImmediate(flushCallbacks)
}
} else {
//判断4:上面都不行,直接用setTimeout
timerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}

无论是微任务还是宏任务,都会放到flushCallbacks使用

这里将callbacks里面的函数复制一份,同时callbacks置空

依次执行callbacks里面的函�?

1
2
3
4
5
6
7
8
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}

*小结�?

  1. 把回调函数放入callbacks等待执行
  2. 将执行函数放到微任务或者宏任务�?3. 事件循环到了微任务或者宏任务,执行函数依次执行callbacks中的回调

参考文�?

面试官:Vue.observable你有了解过吗?说说看

一、Observable 是什�?

Observable 翻译过来我们可以理解�?可观察的*

我们先来看一下其在Vue中的定义

Vue.observable,让一个对象变成响应式数据。Vue 内部会用它来处理 data 函数返回的对�?
返回的对象可以直接用于渲染函数和计算属性内,并且会在发生变更时触发相应的更新。也可以作为最小化的跨组件状态存储器

1
Vue.observable({ count : 1})

其作用等同于

1
new vue({ count : 1})

�?Vue 2.x 中,被传入的对象会直接被 Vue.observable 变更,它和被返回的对象是同一个对�?
�?Vue 3.x 中,则会返回一个可响应的代理,而对源对象直接进行变更仍然是不可响应�?

二、使用场�?

在非父子组件通信时,可以使用通常的bus或者使用vuex,但是实现的功能不是太复杂,而使用上面两个又有点繁琐。这时,observable就是一个很好的选择

创建一个js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 引入vue
import Vue from 'vue
// 创建state对象,使用observable让state对象可响�?export let state = Vue.observable({
name: '张三',
'age': 38
})
// 创建对应的方�?export let mutations = {
changeName(name) {
state.name = name
},
setAge(age) {
state.age = age
}
}

.vue文件中直接使用即�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<template>
<div>
姓名:{{ name }}
年龄:{{ age }}
<button @click="changeName('李四')">改变姓名</button>
<button @click="setAge(18)">改变年龄</button>
</div>
</template>
import { state, mutations } from '@/store
export default {
// 在计算属性中拿到�? computed: {
name() {
return state.name
},
age() {
return state.age
}
},
// 调用mutations里面的方法,更新数据
methods: {
changeName: mutations.changeName,
setAge: mutations.setAge
}
}

三、原理分�?

源码位置:src\core\observer\index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 判断是否存在__ob__响应式属�? if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 实例化Observer响应式对�? ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}

Observer�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data

constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
// 实例化对象是一个对象,进入walk方法
this.walk(value)
}
}

walk函数

1
2
3
4
5
6
walk (obj: Object) {
const keys = Object.keys(obj)
// 遍历key,通过defineReactive创建响应式对�? for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}

defineReactive方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
// 接下来调用Object.defineProperty()给对象定义响应式属�? Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 对观察者watchers进行通知,state就成了全局响应式对�? dep.notify()
}
})
}

参考文�?

面试官:vue要做权限管理该怎么做?如果控制到按钮级别的权限怎么做?

一、是什�?

权限是对特定资源的访问许可,所谓权限控制,也就是确保用户只能访问到被分配的资源

而前端权限归根结底是请求的发起权,请求的发起可能有下面两种形式触�?

  • 页面加载触发
  • 页面上的按钮点击触发

总的来说,所有的请求发起都触发自前端路由或视�?
所以我们可以从这两方面入手,对触发权限的源头进行控制,最终要实现的目标是�?

  • 路由方面,用户登录后只能看到自己有权访问的导航菜单,也只能访问自己有权访问的路由地址,否则将跳转 4xx 提示�?

  • 视图方面,用户只能看到自己有权浏览的内容和有权操作的控件

  • 最后再加上请求控制作为最后一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候请求控制可以用来兜底,越权请求将在前端被拦�?

二、如何做

前端权限控制可以分为四个方面�?

  • 接口权限
  • 按钮权限
  • 菜单权限
  • 路由权限

接口权限

接口权限目前一般采用jwt的形式来验证,没有通过的话一般返回401,跳转到登录页面重新进行登录

登录完拿到token,将token存起来,通过axios请求拦截器进行拦截,每次请求的时候头部携带token

1
2
3
4
5
6
7
8
axios.interceptors.request.use(config => {
config.headers['token'] = cookie.get('token')
return config
})
axios.interceptors.response.use(res=>{},{response}=>{
if (response.data.code === 40099 || response.data.code === 40098) { //token过期或者错�? router.push('/login')
}
})

路由权限控制

方案一

初始化即挂载全部路由,并且在路由上标记相应的权限信息,每次路由跳转前做校�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const routerMap = [
{
path: '/permission',
component: Layout,
redirect: '/permission/index',
alwaysShow: true, // will always show the root menu
meta: {
title: 'permission',
icon: 'lock',
roles: ['admin', 'editor'] // you can set roles in root nav
},
children: [{
path: 'page',
component: () => import('@/views/permission/page'),
name: 'pagePermission',
meta: {
title: 'pagePermission',
roles: ['admin'] // or you can only set roles in sub nav
}
}, {
path: 'directive',
component: () => import('@/views/permission/directive'),
name: 'directivePermission',
meta: {
title: 'directivePermission'
// if do not set roles, means: this page does not require permission
}
}]
}]

这种方式存在以下四种缺点�?

  • 加载所有的路由,如果路由很多,而用户并不是所有的路由都有权限访问,对性能会有影响�?
  • 全局路由守卫里,每次路由跳转都要做权限判断�?
  • 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编�?
  • 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

*方案�?

初始化的时候先挂载不需要权限控制的路由,比如登录页�?04等错误页。如果用户通过URL进行强制访问,则会直接进�?04,相当于从源头上做了控制

登录后,获取用户的权限信息,然后筛选有权限访问的路由,在全局路由守卫里进行调用addRoutes添加路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import router from './router'
import store from './store'
import { Message } from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css'// progress bar style
import { getToken } from '@/utils/auth' // getToken from cookie

NProgress.configure({ showSpinner: false })// NProgress Configuration

// permission judge function
function hasPermission(roles, permissionRoles) {
if (roles.indexOf('admin') >= 0) return true // admin permission passed directly
if (!permissionRoles) return true
return roles.some(role => permissionRoles.indexOf(role) >= 0)
}

const whiteList = ['/login', '/authredirect']// no redirect whitelist

router.beforeEach((to, from, next) => {
NProgress.start() // start progress bar
if (getToken()) { // determine if there has token
/* has token*/
if (to.path === '/login') {
next({ path: '/' })
NProgress.done() // if current page is dashboard will not trigger afterEach hook, so manually handle it
} else {
if (store.getters.roles.length === 0) { // 判断当前用户是否已拉取完user_info信息
store.dispatch('GetUserInfo').then(res => { // 拉取user_info
const roles = res.data.roles // note: roles must be a array! such as: ['editor','develop']
store.dispatch('GenerateRoutes', { roles }).then(() => { // 根据roles权限生成可访问的路由�? router.addRoutes(store.getters.addRouters) // 动态添加可访问路由�? next({ ...to, replace: true }) // hack方法 确保addRoutes已完�?,set the replace: true so the navigation will not leave a history record
})
}).catch((err) => {
store.dispatch('FedLogOut').then(() => {
Message.error(err || 'Verification failed, please login again')
next({ path: '/' })
})
})
} else {
// 没有动态改变权限的需求可直接next() 删除下方权限判断 �? if (hasPermission(store.getters.roles, to.meta.roles)) {
next()//
} else {
next({ path: '/401', replace: true, query: { noGoBack: true }})
}
// 可删 �? }
}
} else {
/* has no token*/
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login') // 否则全部重定向到登录�? NProgress.done() // if current page is login will not trigger afterEach hook, so manually handle it
}
}
})

router.afterEach(() => {
NProgress.done() // finish progress bar
})

按需挂载,路由就需要知道用户的路由权限,也就是在用户登录进来的时候就要知道当前用户拥有哪些路由权�?
这种方式也存在了以下的缺点:

  • 全局路由守卫里,每次路由跳转都要做判�?- 菜单信息写死在前端,要改个显示文字或权限信息,需要重新编�?- 菜单跟路由耦合在一起,定义路由的时候还有添加菜单显示标题,图标之类的信息,而且路由不一定作为菜单显示,还要多加字段进行标识

菜单权限

菜单权限可以理解成将页面与理由进行解�?

方案一

菜单与路由分离,菜单由后端返�?
前端定义路由信息

1
2
3
4
5
{
name: "login",
path: "/login",
component: () => import("@/pages/Login.vue")
}

name字段都不为空,需要根据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有name对应的字段,并且做唯一性校�?
全局路由守卫里做判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function hasPermission(router, accessMenu) {
if (whiteList.indexOf(router.path) !== -1) {
return true;
}
let menu = Util.getMenuByName(router.name, accessMenu);
if (menu.name) {
return true;
}
return false;

}

Router.beforeEach(async (to, from, next) => {
if (getToken()) {
let userInfo = store.state.user.userInfo;
if (!userInfo.name) {
try {
await store.dispatch("GetUserInfo")
await store.dispatch('updateAccessMenu')
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
//Util.toDefaultPage([...routers], to.name, router, next);
next({ ...to, replace: true })//菜单权限更新完成,重新进一次当前路�? }
}
catch (e) {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
} else {
if (to.path === '/login') {
next({ name: 'home_index' })
} else {
if (hasPermission(to, store.getters.accessMenu)) {
Util.toDefaultPage(store.getters.accessMenu,to, routes, next);
} else {
next({ path: '/403',replace:true })
}
}
}
} else {
if (whiteList.indexOf(to.path) !== -1) { // 在免登录白名单,直接进入
next()
} else {
next('/login')
}
}
let menu = Util.getMenuByName(to.name, store.getters.accessMenu);
Util.title(menu.title);
});

Router.afterEach((to) => {
window.scrollTo(0, 0);
});

每次路由跳转的时候都要判断权限,这里的判断也很简单,因为菜单的name与路由的name是一一对应的,而后端返回的菜单就已经是经过权限过滤�?
如果根据路由name找不到对应的菜单,就表示用户有没权限访问

如果路由很多,可以在应用初始化的时候,只挂载不需要权限控制的路由。取得后端返回的菜单后,根据菜单与路由的对应关系,筛选出可访问的路由,通过addRoutes动态挂�?
这种方式的缺点:

  • 菜单需要与路由做一一对应,前端添加了新功能,需要通过菜单管理功能添加新的菜单,如果菜单配置的不对会导致应用不能正常使�?- 全局路由守卫里,每次路由跳转都要做判�?

方案�?

菜单和路由都由后端返�?
前端统一定义路由组件

1
2
3
4
5
6
const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
home: Home,
userInfo: UserInfo
};

后端路由组件返回以下格式

1
2
3
4
5
6
7
8
9
10
11
12
[
{
name: "home",
path: "/",
component: "home"
},
{
name: "home",
path: "/userinfo",
component: "userInfo"
}
]

在将后端返回路由通过addRoutes动态挂载之间,需要将数据处理一下,将component字段换为真正的组�?
如果有嵌套路由,后端功能设计的时候,要注意添加相应的字段,前端拿到数据也要做相应的处�?
这种方法也会存在缺点�?

  • 全局路由守卫里,每次路由跳转都要做判�?- 前后端的配合要求更高

按钮权限

方案一

按钮权限也可以用v-if判断

但是如果页面过多,每个页面页面都要获取用户权限role和路由表里的meta.btnPermissions,然后再做判�?
这种方式就不展开举例�?

方案�?

通过自定义指令进行按钮权限的判断

首先配置路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
path: '/permission',
component: Layout,
name: '权限测试',
meta: {
btnPermissions: ['admin', 'supper', 'normal']
},
//页面需要的权限
children: [{
path: 'supper',
component: _import('system/supper'),
name: '权限测试�?,
meta: {
btnPermissions: ['admin', 'supper']
} //页面需要的权限
},
{
path: 'normal',
component: _import('system/normal'),
name: '权限测试�?,
meta: {
btnPermissions: ['admin']
} //页面需要的权限
}]
}

自定义权限鉴定指�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Vue from 'vue'
/**权限指令**/
const has = Vue.directive('has', {
bind: function (el, binding, vnode) {
// 获取页面按钮权限
let btnPermissionsArr = [];
if(binding.value){
// 如果指令传值,获取指令参数,根据指令参数和当前登录人按钮权限做比较�? btnPermissionsArr = Array.of(binding.value);
}else{
// 否则获取路由中的参数,根据路由的btnPermissionsArr和当前登录人按钮权限做比较�? btnPermissionsArr = vnode.context.$route.meta.btnPermissions;
}
if (!Vue.prototype.$_has(btnPermissionsArr)) {
el.parentNode.removeChild(el);
}
}
});
// 权限检查方�?Vue.prototype.$_has = function (value) {
let isExist = false;
// 获取用户按钮权限
let btnPermissionsStr = sessionStorage.getItem("btnPermissions");
if (btnPermissionsStr == undefined || btnPermissionsStr == null) {
return false;
}
if (value.indexOf(btnPermissionsStr) > -1) {
isExist = true;
}
return isExist;
};
export {has}

在使用的按钮中只需要引用v-has指令

1
<el-button @click='editClick' type="primary" v-has>编辑</el-button>

小结

关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分�?
权限需要前后端结合,前端尽可能的去控制,更多的需要后台判�?

参考文�?

面试官:v-show和v-if有什么区别?使用场景分别是什么?

一、v-show与v-if的共同点

我们都知道在 vue �?v-show �?v-if 的作用效果是相同�?不含v-else),都能控制元素在页面是否显示

在用法上也是相同�?

1
2
<Model v-show="isShow" />
<Model v-if="isShow" />
  • 当表达式为true的时候,都会占据页面的位�?- 当表达式都为false时,都不会占据页面位�?

二、v-show与v-if的区�?

  • 控制手段不同
  • 编译过程不同
  • 编译条件不同

控制手段:v-show隐藏则是为该元素添加css--display:nonedom元素依旧还在。v-if显示隐藏是将dom元素整个添加或删�?
编译过程:v-if切换有一个局部编�?卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show只是简单的基于css切换

编译条件:v-if是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染

  • v-showfalse变为true的时候不会触发组件的生命周期

  • v-iffalse变为true的时候,触发组件的beforeCreatecreatebeforeMountmounted钩子,由true变为false的时候触发组件的beforeDestorydestoryed方法

性能消耗:v-if有更高的切换消耗;v-show有更高的初始渲染消耗;

三、v-show与v-if原理分析

具体解析流程这里不展开讲,大致流程如下

  • 将模板template转为ast结构的JS对象
  • ast得到的JS对象拼装renderstaticRenderFns函数
  • renderstaticRenderFns函数被调用后生成虚拟VNODE节点,该节点包含创建DOM节点所需信息
  • vm.patch函数通过虚拟DOM算法利用VNODE节点创建真实DOM节点

v-show原理

不管初始条件是什么,元素总是会被渲染

我们看一下在vue中是如何实现�?
代码很好理解,有transition就执行transition,没有就直接设置display属�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}

v-if原理

v-if在实现上比v-show要复杂的多,因为还有else else-if 等条件需要处理,这里我们也只摘抄源码中处�?v-if 的一小部�?
返回一个node节点,render函数通过表达式的值来决定是否生成DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)

四、v-show与v-if的使用场�?

v-if �?v-show 都能控制dom元素在页面的显示

v-if 相比 v-show 开销更大的(直接操作dom节点增加与删除)

如果需要非常频繁地切换,则使用 v-show 较好

如果在运行时条件很少改变,则使用 v-if 较好

参考文�?

面试官:说说你对slot的理解?slot使用场景有哪些?

一、slot是什�?

在HTML�?slot 元素 ,作�?Web Components 技术套件的一部分,是Web组件内的一个占位符

该占位符可以在后期使用自己的标记语言填充

举个栗子

1
2
3
4
5
6
7
8
9
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>

template不会展示到页面中,需要用先获取它的引用,然后添加到DOM中,

1
2
3
4
5
6
7
8
9
10
11
customElements.define('element-details',
class extends HTMLElement {
constructor() {
super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})

Vue中的概念也是如此

Slot 艺名插槽,花名“占坑”,我们可以理解为solt在组件模板中占好了位置,当使用该组件标签时候,组件标签里面的内容就会自动填坑(替换组件模板中slot位置),作为承载分发内容的出�?
可以将其类比为插卡式的FC游戏机,游戏机暴露卡槽(插槽)让用户插入不同的游戏磁条(自定义内容)

放张图感受一�?

二、使用场�?

通过插槽可以让用户可以拓展组件,去更好地复用组件和对其做定制化处�?
如果父组件在使用到一个复用组件的时候,获取这个组件在不同的地方有少量的更改,如果去重写组件是一件不明智的事�?
通过slot插槽向组件内部指定位置传递内容,完成这个复用组件在不同场景的应用

比如布局组件、表格列、下拉选、弹框显示内容等

三、分�?

slot可以分来以下三种�?

  • 默认插槽
  • 具名插槽
  • 作用域插�?

默认插槽

子组件用<slot>标签来确定渲染的位置,标签里面可以放DOM结构,当父组件使用的时候没有往插槽传入内容,标签内DOM结构就会显示在页�?
父组件在使用的时候,直接在子组件的标签内写入内容即可

子组件Child.vue

1
2
3
4
5
<template>
<slot>
<p>插槽后备的内�?/p>
</slot>
</template>

父组�?

1
2
3
<Child>
<div>默认插槽</div>
</Child>

具名插槽

子组件用name属性来表示插槽的名字,不传为默认插�?
父组件中在使用时在默认插槽的基础上加上slot属性,值为子组件插槽name属性�?
子组件Child.vue

1
2
3
4
<template>
<slot>插槽后备的内�?/slot>
<slot name="content">插槽后备的内�?/slot>
</template>

父组�?

1
2
3
4
5
<child>
<template v-slot:default>具名插槽</template>
<!-- 具名插槽⽤插槽名做参�?-->
<template v-slot:content>内容...</template>
</child>

作用域插�?

子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件v-slot接受的对象上

父组件中在使用时通过v-slot:(简写:#)获取子组件的信息,在内容中使用

子组件Child.vue

1
2
3
4
5
<template> 
<slot name="footer" testProps="子组件的�?>
<h3>没传footer插槽</h3>
</slot>
</template>

父组�?

1
2
3
4
5
6
7
8
9
<child> 
<!-- 把v-slot的值指定为作⽤域上下⽂对象 -->
<template v-slot:default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
<template #default="slotProps">
来⾃⼦组件数据:{{slotProps.testProps}}
</template>
</child>

小结�?

  • v-slot属性只能在<template>上使用,但在只有默认插槽时可以在组件标签上使�?- 默认插槽名为default,可以省略default直接写v-slot
  • 缩写为#时不能不写参数,写成#default
  • 可以通过解构获取v-slot={user},还可以重命名v-slot="{user: newName}"和定义默认值v-slot="{user = '默认�?}"

四、原理分�?

slot本质上是返回VNode的函数,一般情况下,Vue中的组件要渲染到页面上需要经过template -> render function -> VNode -> DOM 过程,这里看看slot如何实现�?
编写一个buttonCounter组件,使用匿名插�?

1
2
3
Vue.component('button-counter', {
template: '<div> <slot>我是默认内容</slot></div>'
})

使用该组�?

1
2
3
4
5
new Vue({
el: '#app',
template: '<button-counter><span>我是slot传入内容</span></button-counter>',
components:{buttonCounter}
})

获取buttonCounter组件渲染函数

1
2
3
4
(function anonymous(
) {
with(this){return _c('div',[_t("default",[_v("我是默认内容")])],2)}
})

_v表示穿件普通文本节点,_t表示渲染插槽的函�?
渲染插槽函数renderSlot(做了简化)

1
2
3
4
5
6
7
8
9
10
11
12
13
function renderSlot (
name,
fallback,
props,
bindObject
) {
// 得到渲染插槽内容的函�?
var scopedSlotFn = this.$scopedSlots[name];
var nodes;
// 如果存在插槽渲染函数,则执行插槽渲染函数,生成nodes节点返回
// 否则使用默认�? nodes = scopedSlotFn(props) || fallback;
return nodes;
}

name属性表示定义插槽的名字,默认值为defaultfallback表示子组件中的slot节点的默认�?
关于this.$scopredSlots是什么,我们可以先看看vm.slot

1
2
3
4
5
function initRender (vm) {
...
vm.$slots = resolveSlots(options._renderChildren, renderContext);
...
}

resolveSlots函数会对children节点做归类和过滤处理,返回slots

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
function resolveSlots (
children,
context
) {
if (!children || !children.length) {
return {}
}
var slots = {};
for (var i = 0, l = children.length; i < l; i++) {
var child = children[i];
var data = child.data;
// remove slot attribute if the node is resolved as a Vue slot node
if (data && data.attrs && data.attrs.slot) {
delete data.attrs.slot;
}
// named slots should only be respected if the vnode was rendered in the
// same context.
if ((child.context === context || child.fnContext === context) &&
data && data.slot != null
) {
// 如果slot存在(slot="header") 则拿对应的值作为key
var name = data.slot;
var slot = (slots[name] || (slots[name] = []));
// 如果是tempalte元素 则把template的children添加进数组中,这也就是为什么你写的template标签并不会渲染成另一个标签到页面
if (child.tag === 'template') {
slot.push.apply(slot, child.children || []);
} else {
slot.push(child);
}
} else {
// 如果没有就默认是default
(slots.default || (slots.default = [])).push(child);
}
}
// ignore slots that contains only whitespace
for (var name$1 in slots) {
if (slots[name$1].every(isWhitespace)) {
delete slots[name$1];
}
}
return slots
}

_render渲染函数通过normalizeScopedSlots得到vm.$scopedSlots

1
2
3
4
5
vm.$scopedSlots = normalizeScopedSlots(
_parentVnode.data.scopedSlots,
vm.$slots,
vm.$scopedSlots
);

作用域插槽中父组件能够得到子组件的值是因为在renderSlot的时候执行会传入props,也就是上述_t第三个参数,父组件则能够得到子组件传递过来的�?

参考文�?

面试官:你对SPA单页面的理解,它的优缺点分别是什么?如何实现SPA应用�?

一、什么是SPA

SPA(single-page application),翻译过来就是单页应用SPA是一种网络应用程序或网站的模型,它通过动态重写当前页面来与用户交互,这种方法避免了页面之间切换打断用户体验在单页应用中,所有必要的代码(HTMLJavaScriptCSS)都通过单个页面的加载而检索,或者根据需要(通常是为响应用户操作)动态装载适当的资源并添加到页面页面在任何时间点都不会重新加载,也不会将控制转移到其他页面举个例子来讲就是一个杯子,早上装的牛奶,中午装的是开水,晚上装的是茶,我们发现,变的始终是杯子里的内容,而杯子始终是那个杯子结构如下�?

我们熟知的JS框架如react,vue,angular,ember都属于SPA

二、SPA和MPA的区�?

上面大家已经对单页面有所了解了,下面来讲讲多页应用MPA(MultiPage-page application),翻译过来就是多页应用在MPA中,每个页面都是一个主页面,都是独立的当我们在访问另一个页面的时候,都需要重新加载htmlcssjs文件,公共文件则根据需求按需加载如下�?

单页应用与多页应用的区别

单页面应用(SPA�? 多页面应用(MPA�?
组成 一个主页面和多个页面片�? 多个主页�?
刷新方式 局部刷�? 整页刷新
url模式 哈希模式 历史模式
SEO搜索引擎优化 难实现,可使用SSR方式改善 容易实现
数据传�? 容易 通过url、cookie、localStorage等传�?
页面切换 速度快,用户体验良好 切换加载资源,速度慢,用户体验�?
维护成本 相对容易 相对复杂

单页应用优缺�?

优点�?

  • 具有桌面应用的即时性、网站的可移植性和可访问�?- 用户体验好、快,内容的改变不需要重新加载整个页�?- 良好的前后端分离,分工更明确

缺点�?
- 不利于搜索引擎的抓取
- 首次渲染速度相对较慢

三、实现一个SPA

原理

  1. 监听地址栏中hash变化驱动界面变化
  2. pushsate记录浏览器的历史,驱动界面发送变�?

实现

hash 模式

核心通过监听url中的hash来进行路由跳�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 定义 Router  
class Router {
    constructor () {
        this.routes = {}; // 存放路由path及callback
        this.currentUrl = '';
        
        // 监听路由change调用相对应的路由回调
        window.addEventListener('load'this.refreshfalse);
        window.addEventListener('hashchange'this.refreshfalse);
    }
    
    route(path, callback){
        this.routes[path] = callback;
    }
    
    push(path) {
        this.routes[path] && this.routes[path]()
    }
}

// 使用 router
window.miniRouter = new Router();
miniRouter.route('/'() => console.log('page1'))
miniRouter.route('/page2'() => console.log('page2'))

miniRouter.push('/'// page1
miniRouter.push('/page2'// page2
history模式

history 模式核心借用 HTML5 history apiapi 提供了丰富的 router 相关属性先了解一个几个相关的api

  • history.pushState 浏览器历史纪录添加记�? - history.replaceState修改浏览器历史纪录中当前纪录
  • history.popState �?history 发生变化时触�?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 定义 Router  
class Router {
    constructor () {
        this.routes = {};
        this.listerPopState()
    }
    
    init(path) {
        history.replaceState({path: path}, null, path);
        this.routes[path] && this.routes[path]();
    }
    
    route(path, callback){
        this.routes[path] = callback;
    }
    
    push(path) {
        history.pushState({path: path}, null, path);
        this.routes[path] && this.routes[path]();
    }
    
    listerPopState () {
        window.addEventListener('popstate' , e => {
            const path = e.state && e.state.path;
            this.routers[path] && this.routers[path]()
        })
    }
}

// 使用 Router

window.miniRouter = new Router();
miniRouter.route('/'()=> console.log('page1'))
miniRouter.route('/page2'()=> console.log('page2'))

// 跳转
miniRouter.push('/page2')  // page2

四、题外话:如何给SPA做SEO

下面给出基于VueSPA如何实现SEO的三种方�?

  1. *SSR服务端渲�?

将组件或页面通过服务器生成html,再返回给浏览器,如nuxt.js

  1. 静态化

目前主流的静态化主要有两种:�?)一种是通过程序将动态页面抓取并保存为静态页面,这样的页面的实际存在于服务器的硬盘中�?)另外一种是通过WEB服务器的 URL Rewrite的方式,它的原理是通过web服务器内部模块按一定规则将外部的URL请求转化为内部的文件地址,一句话来说就是把外部请求的静态地址转化为实际的动态页面地址,而静态页面实际是不存在的。这两种方法都达到了实现URL静态化的效�?
3. 使用Phantomjs针对爬虫处理

原理是通过Nginx配置,判断访问来源是否为爬虫,如果是则搜索引擎的爬虫请求会转发到一个node server,再通过PhantomJS来解析完整的HTML,返回给爬虫。下面是大致流程�?

参考文�?

0%