乐趣区

关于vue.js:高级前端二面vue面试题持续更新中

action 与 mutation 的区别

  • mutation 是同步更新, $watch 严格模式下会报错
  • action 是异步操作,能够获取数据后调用 mutation 提交最终数据

MVVM的优缺点?

长处:

  • 拆散视图(View)和模型(Model),升高代码耦合,提⾼视图或者逻辑的重⽤性: ⽐如视图(View)能够独⽴于 Model 变动和批改,⼀个 ViewModel 能够绑定不同的 ”View” 上,当 View 变动的时候 Model 不能够不变,当 Model 变动的时候 View 也能够不变。你能够把⼀些视图逻辑放在⼀个 ViewModel ⾥⾯,让很多 view 重⽤这段视图逻辑
  • 提⾼可测试性: ViewModel 的存在能够帮忙开发者更好地编写测试代码
  • ⾃动更新 dom: 利⽤双向绑定, 数据更新后视图⾃动更新, 让开发者从繁琐的⼿动 dom 中解放

毛病:

  • Bug 很难被调试: 因为使⽤双向绑定的模式,当你看到界⾯异样了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得⼀个地位的 Bug 被疾速传递到别的地位,要定位原始出问题的地⽅就变得不那么容易了。另外,数据绑定的申明是指令式地写在 View 的模版当中的,这些内容是没方法去打断点 debug 的
  • ⼀个⼤的模块中 model 也会很⼤,尽管使⽤⽅便了也很容易保障了数据的⼀致性,过后⻓期持有,不开释内存就造成了破费更多的内存
  • 对于⼤型的图形应⽤程序,视图状态较多,ViewModel 的构建和保护的老本都会⽐较⾼。

形容下 Vue 自定义指令

在 Vue2.0 中,代码复用和形象的次要模式是组件。然而,有的状况下,你依然须要对一般 DOM 元素进行底层操作,这时候就会用到自定义指令。
个别须要对 DOM 元素进行底层操作时应用,尽量只用来操作 DOM 展现,不批改外部的值。当应用自定义指令间接批改 value 值时绑定 v -model 的值也不会同步更新;如必须批改能够在自定义指令中应用 keydown 事件,在 vue 组件中应用 change 事件,回调中批改 vue 数据;

(1)自定义指令根本内容

  • 全局定义:Vue.directive("focus",{})
  • 部分定义:directives:{focus:{}}
  • 钩子函数:指令定义对象提供钩子函数

    o bind:只调用一次,指令第一次绑定到元素时调用。在这里能够进行一次性的初始化设置。

    o inSerted:被绑定元素插入父节点时调用(仅保障父节点存在,但不肯定已被插入文档中)。

    o update:所在组件的 VNode 更新时调用,然而可能产生在其子 VNode 更新之前调用。指令的值可能产生了扭转,也可能没有。然而能够通过比拟更新前后的值来疏忽不必要的模板更新。

    o ComponentUpdate:指令所在组件的 VNode 及其子 VNode 全副更新后调用。

    o unbind:只调用一次,指令与元素解绑时调用。

  • 钩子函数参数
    o el:绑定元素

    o bing:指令外围对象,形容指令全副信息属性

    o name

    o value

    o oldValue

    o expression

    o arg

    o modifers

    o vnode 虚构节点

    o oldVnode:上一个虚构节点(更新钩子函数中才有用)

(2)应用场景

  • 一般 DOM 元素进行底层操作的时候,能够应用自定义指令
  • 自定义指令是用来操作 DOM 的。只管 Vue 推崇数据驱动视图的理念,但并非所有状况都适宜数据驱动。自定义指令就是一种无效的补充和扩大,不仅可用于定义任何的 DOM 操作,并且是可复用的。

(3)应用案例

高级利用:

  • 鼠标聚焦
  • 下拉菜单
  • 绝对工夫转换
  • 滚动动画

高级利用:

  • 自定义指令实现图片懒加载
  • 自定义指令集成第三方插件

Vue-Router 的懒加载如何实现

非懒加载:

import List from '@/components/list.vue'
const router = new VueRouter({
  routes: [{ path: '/list', component: List}
  ]
})

(1)计划一(罕用):应用箭头函数 +import 动静加载

const List = () => import('@/components/list.vue')
const router = new VueRouter({
  routes: [{ path: '/list', component: List}
  ]
})

(2)计划二:应用箭头函数 +require 动静加载

const router = new Router({
  routes: [
   {
     path: '/list',
     component: resolve => require(['@/components/list'], resolve)
   }
  ]
})

(3)计划三:应用 webpack 的 require.ensure 技术,也能够实现按需加载。这种状况下,多个路由指定雷同的 chunkName,会合并打包成一个 js 文件。

// r 就是 resolve
const List = r => require.ensure([], () => r(require('@/components/list')), 'list');
// 路由也是失常的写法  这种是官网举荐的写的 按模块划分懒加载 
const router = new Router({
  routes: [
  {
    path: '/list',
    component: List,
    name: 'list'
  }
 ]
}))

Vue 3.0 中的 Vue Composition API?

在 Vue2 中,代码是 Options API 格调的,也就是通过填充 (option) data、methods、computed 等属性来实现一个 Vue 组件。这种格调使得 Vue 绝对于 React 极为容易上手,同时也造成了几个问题:

  1. 因为 Options API 不够灵便的开发方式,使得 Vue 开发不足优雅的办法来在组件间共用代码。
  2. Vue 组件过于依赖 this 上下文,Vue 背地的一些小技巧使得 Vue 组件的开发看起来与 JavaScript 的开发准则相悖,比方在 methods 中的this 居然指向组件实例来不指向 methods 所在的对象。这也使得 TypeScript 在 Vue2 中很不好用。

于是在 Vue3 中,舍弃了 Options API,转而投向 Composition API。Composition API 实质上是将 Options API 背地的机制裸露给用户间接应用,这样用户就领有了更多的灵活性,也使得 Vue3 更适宜于 TypeScript 联合。

如下,是一个应用了 Vue Composition API 的 Vue3 组件:

<template>
  <button @click="increment">
    Count: {{count}}  </button>
</template>

<script>
// Composition API 将组件属性裸露为函数,因而第一步是导入所需的函数
import {ref, computed, onMounted} from 'vue'

export default {setup() {
// 应用 ref 函数申明了称为 count 的响应属性,对应于 Vue2 中的 data 函数
    const count = ref(0) 
// Vue2 中须要在 methods option 中申明的函数,当初间接申明
    function increment() {      count.value++} // 对应于 Vue2 中的 mounted 申明周期
    onMounted(() => console.log('component mounted!'))     return {count,      increment}  }}
</script>

不言而喻,Vue Composition API 使得 Vue3 的开发格调更靠近于原生 JavaScript,带给开发者更多地灵活性

SPA、SSR 的区别是什么

咱们当初编写的 VueReactAngular利用大多数状况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,因为良好的用户体验逐步成为支流的开发模式。但同时也会有首屏加载工夫长,SEO不敌对的问题,因而有了SSR,这也是为什么面试中会问到两者的区别

  1. SPA(Single Page Application)即单页面利用。个别也称为 客户端渲染(Client Side Render),简称 CSRSSR(Server Side Render)即 服务端渲染。个别也称为 多页面利用(Mulpile Page Application),简称 MPA
  2. SPA利用只会首次申请 html 文件,后续只须要申请 JSON 数据即可,因而用户体验更好,节约流量,服务端压力也较小。然而首屏加载的工夫会变长,而且 SEO 不敌对。为了解决以上毛病,就有了 SSR 计划,因为 HTML 内容在服务器一次性生成进去,首屏加载快,搜索引擎也能够很不便的抓取页面信息。但同时 SSR 计划也会有性能,开发受限等问题
  3. 在抉择上,如果咱们的利用存在首屏加载优化需要,SEO需要时,就能够思考SSR
  4. 但并不是只有这一种代替计划,比方对一些不常变动的动态网站,SSR 反而浪费资源,咱们能够思考预渲染(prerender)计划。另外 nuxt.js/next.js 中给咱们提供了 SSG(Static Site Generate) 动态网站生成计划也是很好的动态站点解决方案,联合一些 CI 伎俩,能够起到很好的优化成果,且能节约服务器资源

内容生成上的区别:

SSR

SPA

部署上的区别

参考 前端进阶面试题具体解答

delete 和 Vue.delete 删除数组的区别?

  • delete只是被删除的元素变成了 empty/undefined 其余的元素的键值还是不变。
  • Vue.delete间接删除了数组 扭转了数组的键值。
var a=[1,2,3,4]
var b=[1,2,3,4]
delete a[0]
console.log(a)  //[empty,2,3,4]
this.$delete(b,0)
console.log(b)  //[2,3,4]

说说你对 slot 的了解?slot 应用场景有哪些

一、slot 是什么

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

该占位符能够在前期应用本人的标记语言填充

举个栗子

<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 中,

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 地位),作为承载散发内容的进口

二、应用场景

通过插槽能够让用户能够拓展组件,去更好地复用组件和对其做定制化解决

如果父组件在应用到一个复用组件的时候,获取这个组件在不同的中央有大量的更改,如果去重写组件是一件不明智的事件

通过 slot 插槽向组件外部指定地位传递内容,实现这个复用组件在不同场景的利用

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

如果让你从零开始写一个 vuex,说说你的思路

思路剖析

这个题目很有难度,首先思考 vuex 解决的问题:存储用户全局状态并提供治理状态 API。

  • vuex需要剖析
  • 如何实现这些需要

答复范例

  1. 官网说 vuex 是一个状态管理模式和库,并确保这些状态以可预期的形式变更。可见要实现一个vuex
  2. 要实现一个 Store 存储全局状态
  3. 要提供批改状态所需 API:commit(type, payload), dispatch(type, payload)
  4. 实现 Store 时,能够定义 Store 类,构造函数接管选项 options,设置属性state 对外裸露状态,提供 commitdispatch批改属性 state。这里须要设置state 为响应式对象,同时将 Store 定义为一个 Vue 插件
  5. commit(type, payload)办法中能够获取用户传入 mutations 并执行它,这样能够按用户提供的办法批改状态。dispatch(type, payload)相似,但须要留神它可能是异步的,须要返回一个 Promise 给用户以解决异步后果

实际

Store的实现:

class Store {constructor(options) {this.state = reactive(options.state)
        this.options = options
    }
    commit(type, payload) {this.options.mutations[type].call(this, this.state, payload)
    }
}

vuex 简易版

/**
 * 1 实现插件,挂载 $store
 * 2 实现 store
 */

let Vue;

class Store {constructor(options) {
    // state 响应式解决
    // 内部拜访:this.$store.state.***
    // 第一种写法
    // this.state = new Vue({
    //   data: options.state
    // })

    // 第二种写法:避免外界间接接触外部 vue 实例,避免内部强行变更
    this._vm = new Vue({
      data: {$$state: options.state}
    })

    this._mutations = options.mutations
    this._actions = options.actions
    this.getters = {}
    options.getters && this.handleGetters(options.getters)

    this.commit = this.commit.bind(this)
    this.dispatch = this.dispatch.bind(this)
  }

  get state () {return this._vm._data.$$state}

  set state (val) {return new Error('Please use replaceState to reset state')
  }

  handleGetters (getters) {Object.keys(getters).map(key => {
      Object.defineProperty(this.getters, key, {get: () => getters[key](this.state)
      })
    })
  }

  commit (type, payload) {let entry = this._mutations[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this.state, payload)
  }

  dispatch (type, payload) {let entry = this._actions[type]
    if (!entry) {return new Error(`${type} is not defined`)
    }

    entry(this, payload)
  }
}

const install = (_Vue) => {
  Vue = _Vue

  Vue.mixin({beforeCreate () {if (this.$options.store) {Vue.prototype.$store = this.$options.store}
    },
  })
}


export default {Store, install}

验证形式

import Vue from 'vue'
import Vuex from './vuex'
// this.$store
Vue.use(Vuex)

export default new Vuex.Store({
  state: {counter: 0},
  mutations: {
    // state 从哪里来的
    add (state) {state.counter++}
  },
  getters: {doubleCounter (state) {return state.counter * 2}
  },
  actions: {add ({ commit}) {setTimeout(() => {commit('add')
      }, 1000)
    }
  },
  modules: {}})

Vue 中 v -html 会导致哪些问题

  • 可能会导致 xss 攻打
  • v-html 会替换掉标签外部的子元素
let template = require('vue-template-compiler'); 
let r = template.compile(`<div v-html="'<span>hello</span>'"></div>`) 

// with(this){return _c('div',{domProps: {"innerHTML":_s('<span>hello</span>')}})} 
console.log(r.render);

// _c 定义在 core/instance/render.js 
// _s 定义在 core/instance/render-helpers/index,js
if (key === 'textContent' || key === 'innerHTML') {if (vnode.children) vnode.children.length = 0 
    if (cur === oldProps[key]) continue // #6601 work around Chrome version <= 55 bug where single textNode // replaced by innerHTML/textContent retains its parentNode property 
    if (elm.childNodes.length === 1) {elm.removeChild(elm.childNodes[0]) 
    } 
}

v-model 实现原理

咱们在 vue 我的项目中次要应用 v-model 指令在表单 inputtextareaselect 等元素上创立双向数据绑定,咱们晓得 v-model 实质上不过是语法糖(能够看成是 value + input 办法的语法糖),v-model 在外部为不同的输出元素应用不同的属性并抛出不同的事件:

  • texttextarea 元素应用 value 属性和 input 事件
  • checkboxradio 应用 checked 属性和 change 事件
  • select 字段将 value 作为 prop 并将 change 作为事件

所以咱们能够 v -model 进行如下改写:

<input v-model="sth" />
<!-- 等同于 -->
<input :value="sth" @input="sth = $event.target.value" />

当在 input 元素中应用 v-model 实现双数据绑定,其实就是在输出的时候触发元素的 input 事件,通过这个语法糖,实现了数据的双向绑定

  • 这个语法糖必须是固定的,也就是说属性必须为value,办法名必须为:input
  • 晓得了 v-model 的原理,咱们能够在自定义组件上实现v-model
//Parent
<template>
  {{num}}
  <Child v-model="num">
</template>
export default {data(){
    return {num: 0}
  }
}

//Child
<template>
  <div @click="add">Add</div>
</template>
export default {props: ['value'], // 属性必须为 value
  methods:{add(){
      // 办法名为 input
      this.$emit('input', this.value + 1)
    }
  }
}

原理

会将组件的 v-model 默认转化成value+input

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<el-checkbox v-model="check"></el- checkbox>'); 

// 察看输入的渲染函数:// with(this) { 
//     return _c('el-checkbox', { 
//         model: {//             value: (check), 
//             callback: function ($$v) {check = $$v}, 
//             expression: "check" 
//         } 
//     }) 
// }
// 源码地位 core/vdom/create-component.js line:155

function transformModel (options, data: any) {const prop = (options.model && options.model.prop) || 'value' 
    const event = (options.model && options.model.event) || 'input' 
    ;(data.attrs || (data.attrs = {}))[prop] = data.model.value 
    const on = data.on || (data.on = {}) 
    const existing = on[event] 
    const callback = data.model.callback 
    if (isDef(existing)) {if (Array.isArray(existing) ? existing.indexOf(callback) === -1 : existing !== callback ) {on[event] = [callback].concat(existing) 
        } 
    } else {on[event] = callback 
    } 
}

原生的 v-model,会依据标签的不同生成不同的事件和属性

const VueTemplateCompiler = require('vue-template-compiler'); 
const ele = VueTemplateCompiler.compile('<input v-model="value"/>');

// with(this) { 
//     return _c('input', {//         directives: [{ name: "model", rawName: "v-model", value: (value), expression: "value" }], 
//         domProps: {"value": (value) },
//         on: {"input": function ($event) {//             if ($event.target.composing) return;
//             value = $event.target.value
//         }
//         }
//     })
// }

编译时:不同的标签解析出的内容不一样 platforms/web/compiler/directives/model.js

if (el.component) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime 
    return false 
} else if (tag === 'select') {genSelect(el, value, modifiers) 
} else if (tag === 'input' && type === 'checkbox') {genCheckboxModel(el, value, modifiers) 
} else if (tag === 'input' && type === 'radio') {genRadioModel(el, value, modifiers) 
} else if (tag === 'input' || tag === 'textarea') {genDefaultModel(el, value, modifiers) 
} else if (!config.isReservedTag(tag)) {genComponentModel(el, value, modifiers) // component v-model doesn't need extra runtime 
    return false 
}

运行时:会对元素解决一些对于输入法的问题 platforms/web/runtime/directives/model.js

inserted (el, binding, vnode, oldVnode) {if (vnode.tag === 'select') { // #6903 
    if (oldVnode.elm && !oldVnode.elm._vOptions) {mergeVNodeHook(vnode, 'postpatch', () => {directive.componentUpdated(el, binding, vnode) 
        }) 
    } else {setSelected(el, binding, vnode.context) 
    }
    el._vOptions = [].map.call(el.options, getValue) 
    } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { 
        el._vModifiers = binding.modifiers 
        if (!binding.modifiers.lazy) {el.addEventListener('compositionstart', onCompositionStart) 
            el.addEventListener('compositionend', onCompositionEnd) 
            // Safari < 10.2 & UIWebView doesn't fire compositionend when 
            // switching focus before confirming composition choice 
            // this also fixes the issue where some browsers e.g. iOS Chrome
            // fires "change" instead of "input" on autocomplete. 
            el.addEventListener('change', onCompositionEnd) /* istanbul ignore if */ 
            if (isIE9) {el.vmodel = true}
        }
    }
}

子组件能够间接扭转父组件的数据么,阐明起因

这是一个实际知识点,组件化开发过程中有个 单项数据流准则,不在子组件中批改父组件是个常识问题

思路

  • 讲讲单项数据流准则,表明为何不能这么做
  • 举几个常见场景的例子说说解决方案
  • 联合实际讲讲如果须要批改父组件状态应该如何做

答复范例

  1. 所有的 prop 都使得其父子之间造成了一个单向上行绑定:父级 prop 的更新会向下流动到子组件中,然而反过来则不行。这样会避免从子组件意外变更父级组件的状态,从而导致你的利用的数据流向难以了解。另外,每次父级组件产生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件外部扭转 prop。如果你这样做了,Vue 会在浏览器控制台中收回正告
const props = defineProps(['foo'])
// ❌ 上面行为会被正告, props 是只读的!
props.foo = 'bar'
  1. 理论开发过程中有两个场景会想要批改一个属性:

这个 prop 用来传递一个初始值;这个子组件接下来心愿将其作为一个本地的 prop 数据来应用。 在这种状况下,最好定义一个本地的 data,并将这个 prop 用作其初始值:

const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)

这个 prop 以一种原始的值传入且须要进行转换。 在这种状况下,最好应用这个 prop 的值来定义一个计算属性:

const props = defineProps(['size'])
// prop 变动,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
  1. 实际中如果的确想要扭转父组件属性应该 emit 一个事件让父组件去做这个变更。留神尽管咱们不能间接批改一个传入的对象或者数组类型的prop,然而咱们还是可能间接改内嵌的对象或属性

怎么缓存以后的组件?缓存后怎么更新

缓存组件应用 keep-alive 组件,这是一个十分常见且有用的优化伎俩,vue3keep-alive 有比拟大的更新,能说的点比拟多

思路

  • 缓存用keep-alive,它的作用与用法
  • 应用细节,例如缓存指定 / 排除、联合 routertransition
  • 组件缓存后更新能够利用 activated 或者beforeRouteEnter
  • 原理论述

答复范例

  1. 开发中缓存组件应用 keep-alive 组件,keep-alivevue 内置组件,keep-alive包裹动静组件 component 时,会缓存不流动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,避免反复渲染DOM
<keep-alive>
  <component :is="view"></component>
</keep-alive>
  1. 联合属性 includeexclude能够明确指定缓存哪些组件或排除缓存指定组件。vue3中联合 vue-router 时变动较大,之前是 keep-alive 包裹 router-view,当初须要反过来用router-view 包裹keep-alive
<router-view v-slot="{Component}">
  <keep-alive>
    <component :is="Component"></component>
  </keep-alive>
</router-view>
  1. 缓存后如果要获取数据,解决方案能够有以下两种
  2. beforeRouteEnter:在有 vue-router 的 我的项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
  next(vm=>{console.log(vm)
    // 每次进入路由执行
    vm.getData()  // 获取数据})
},
  • actived:在 keep-alive 缓存的组件被激活的时候,都会执行 actived 钩子
activated(){this.getData() // 获取数据
},
  1. keep-alive是一个通用组件,它外部定义了一个 map,缓存创立过的组件实例,它返回的渲染函数外部会查找内嵌的component 组件对应组件的 vnode,如果该组件在map 中存在就间接返回它。因为 componentis属性是个响应式数据,因而只有它变动,keep-aliverender 函数就会从新执行

Vue 中修饰符.sync 与 v -model 的区别

sync的作用

  • .sync修饰符能够实现父子组件之间的双向绑定,并且能够实现子组件同步批改父组件的值,相比拟与 v-model 来说,sync修饰符就简略很多了
  • 一个组件上能够有多个 .sync 修饰符
<!-- 失常父传子 -->
<Son :a="num" :b="num2" />

<!-- 加上 sync 之后的父传子 -->
<Son :a.sync="num" :b.sync="num2" />

<!-- 它等价于 -->
<Son 
  :a="num" 
  :b="num2" 
  @update:a="val=>num=val" 
  @update:b="val=>num2=val" 
/>

<!-- 相当于多了一个事件监听,事件名是 update:a, -->
<!-- 回调函数中,会把接管到的值赋值给属性绑定的数据项中。-->

v-model的工作原理

<com1 v-model="num"></com1>
<!-- 等价于 -->
<com1 :value="num" @input="(val)=>num=val"></com1>
  • 相同点

    • 都是语法糖,都能够实现父子组件中的数据的双向通信
  • 区别点

    • 格局不同:v-model="num", :num.sync="num"
    • v-model: @input + value
    • :num.sync: @update:num
    • v-model只能用一次;.sync能够有多个

Vue 组件之间通信形式有哪些

Vue 组件间通信是面试常考的知识点之一,这题有点相似于凋谢题,你答复出越多办法当然越加分,表明你对 Vue 把握的越纯熟。Vue 组件间通信只有指以下 3 类通信 父子组件通信 隔代组件通信 兄弟组件通信,上面咱们别离介绍每种通信形式且会阐明此种办法可实用于哪类组件间通信

组件传参的各种形式

组件通信罕用形式有以下几种

  • props / $emit 实用 父子组件通信

    • 父组件向子组件传递数据是通过 prop 传递的,子组件传递数据给父组件是通过$emit 触发事件来做到的
  • ref$parent / $children(vue3 废除) 实用 父子组件通信

    • ref:如果在一般的 DOM 元素上应用,援用指向的就是 DOM 元素;如果用在子组件上,援用就指向组件实例
    • $parent / $children:拜访拜访父组件的属性或办法 / 拜访子组件的属性或办法
  • EventBus($emit / $on) 实用于 父子、隔代、兄弟组件通信

    • 这种办法通过一个空的 Vue 实例作为地方事件总线(事件核心),用它来触发事件和监听事件,从而实现任何组件间的通信,包含父子、隔代、兄弟组件
  • $attrs / $listeners(vue3 废除) 实用于 隔代组件通信

    • $attrs:蕴含了父作用域中不被 prop 所辨认 (且获取) 的个性绑定 (classstyle 除外 )。当一个组件没有申明任何 prop时,这里会蕴含所有父作用域的绑定 (classstyle 除外 ),并且能够通过 v-bind="$attrs" 传入外部组件。通常配合 inheritAttrs 选项一起应用
    • $listeners:蕴含了父作用域中的 (不含 .native 润饰器的) v-on 事件监听器。它能够通过 v-on="$listeners" 传入外部组件
  • provide / inject 实用于 隔代组件通信

    • 先人组件中通过 provider 来提供变量,而后在子孙组件中通过 inject 来注入变量。provide / inject API 次要解决了跨级组件间的通信问题,不过它的应用场景,次要是子组件获取下级组件的状态,跨级组件间建设了一种被动提供与依赖注入的关系
  • $root 实用于 隔代组件通信 拜访根组件中的属性或办法,是根组件,不是父组件。$root 只对根组件有用
  • Vuex 实用于 父子、隔代、兄弟组件通信

    • Vuex 是一个专为 Vue.js 利用程序开发的状态管理模式。每一个 Vuex 利用的外围就是 store(仓库)。“store”基本上就是一个容器,它蕴含着你的利用中大部分的状态 (state )
    • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地失去高效更新。
    • 扭转 store 中的状态的惟一路径就是显式地提交 (commit) mutation。这样使得咱们能够不便地跟踪每一个状态的变动。

依据组件之间关系探讨组件通信最为清晰无效

  • 父子组件:props/$emit/$parent/ref
  • 兄弟组件:$parent/eventbus/vuex
  • 跨层级关系:eventbus/vuex/provide+inject/$attrs + $listeners/$root

上面演示组件之间通信三种状况: 父传子、子传父、兄弟组件之间的通信

1. 父子组件通信

应用 props,父组件能够应用props 向子组件传递数据。

父组件 vue 模板father.vue:

<template>
  <child :msg="message"></child>
</template>

<script>
import child from './child.vue';
export default {
  components: {child},
  data () {
    return {message: 'father message';}
  }
}
</script>

子组件 vue 模板child.vue:

<template>
    <div>{{msg}}</div>
</template>

<script>
export default {
  props: {
    msg: {
      type: String,
      required: true
    }
  }
}
</script>

回调函数(callBack)

父传子:将父组件里定义的 method 作为 props 传入子组件

// 父组件 Parent.vue:<Child :changeMsgFn="changeMessage">
methods: {changeMessage(){this.message = 'test'}
}
// 子组件 Child.vue:<button @click="changeMsgFn">
props:['changeMsgFn']

子组件向父组件通信

父组件向子组件传递事件办法,子组件通过 $emit 触发事件,回调给父组件

父组件 vue 模板father.vue:

<template>
    <child @msgFunc="func"></child>
</template>

<script>
import child from './child.vue';
export default {
    components: {child},
    methods: {func (msg) {console.log(msg);
        }
    }
}
</script>

子组件 vue 模板child.vue:

<template>
    <button @click="handleClick"> 点我 </button>
</template>

<script>
export default {
    props: {
        msg: {
            type: String,
            required: true
        }
    },
    methods () {handleClick () {
          //........
          this.$emit('msgFunc');
        }
    }
}
</script>

2. provide / inject 跨级拜访先人组件的数据

父组件通过应用 provide(){return{}} 提供须要传递的数据

export default {data() {
    return {
      title: '我是父组件',
      name: 'poetry'
    }
  },
  methods: {say() {alert(1)
    }
  },
  // provide 属性 可能为前面的后辈组件 / 嵌套的组件提供所须要的变量和办法
  provide() {
    return {
      message: '我是先人组件提供的数据',
      name: this.name, // 传递属性
      say: this.say
    }
  }
}

子组件通过应用 inject:[“参数 1”,”参数 2”,…] 接管父组件传递的参数

<template>
  <p> 曾孙组件 </p>
  <p>{{message}}</p>
</template>
<script>
export default {
  // inject 注入 / 接管先人组件传递的所须要的数据即可 
  // 接管到的数据 变量 跟 data 外面的变量一样 能够间接绑定到页面 {{}}
  inject: ["message","say"],
  mounted() {this.say();
  },
};
</script>

3. $parent + $children 获取父组件实例和子组件实例的汇合

  • this.$parent 能够间接拜访该组件的父实例或组件
  • 父组件也能够通过 this.$children 拜访它所有的子组件;须要留神 $children 并不保障程序,也不是响应式的
<!-- parent.vue -->
<template>
<div>
  <child1></child1>   
  <child2></child2> 
  <button @click="clickChild">$children 形式获取子组件值 </button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {data(){
    return {total: 108}
  },
  components: {
    child1,
    child2  
  },
  methods: {funa(e){console.log("index",e)
    },
    clickChild(){console.log(this.$children[0].msg);
      console.log(this.$children[1].msg);
    }
  }
}
</script>
<!-- child1.vue -->
<template>
  <div>
    <button @click="parentClick"> 点击拜访父组件 </button>
  </div>
</template>
<script>
export default {data(){
    return {msg:"child1"}
  },
  methods: {
    // 拜访父组件数据
    parentClick(){this.$parent.funa("xx")
      console.log(this.$parent.total);
    }
  }
}
</script>
<!-- child2.vue -->
<template>
  <div>
    child2
  </div>
</template>
<script>
export default {data(){
    return {msg: 'child2'}
  }
}
</script>

4. $attrs + $listeners 多级组件通信

$attrs 蕴含了从父组件传过来的所有 props 属性

// 父组件 Parent.vue:<Child :name="name" :age="age"/>

// 子组件 Child.vue:<GrandChild v-bind="$attrs" />

// 孙子组件 GrandChild
<p> 姓名:{{$attrs.name}}</p>
<p> 年龄:{{$attrs.age}}</p>

$listeners蕴含了父组件监听的所有事件

// 父组件 Parent.vue:<Child :name="name" :age="age" @changeNameFn="changeName"/>

// 子组件 Child.vue:<button @click="$listeners.changeNameFn"></button>

5. ref 父子组件通信

// 父组件 Parent.vue:<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){console.log(this.$refs.childComp.age);
    this.$refs.childComp.changeAge()}

// 子组件 Child.vue:data(){
    return{age:20}
},
methods(){changeAge(){this.age=15}
}

6. 非父子, 兄弟组件之间通信

vue2中废除了 broadcast 播送和散发事件的办法。父子组件中能够用 props$emit()。如何实现非父子组件间的通信,能够通过实例一个 vue 实例 Bus 作为媒介,要互相通信的兄弟组件之中,都引入 Bus,而后通过别离调用 Bus 事件触发和监听来实现通信和参数传递。Bus.js 能够是这样:

// Bus.js

// 创立一个地方工夫总线类  
class Bus {constructor() {this.callbacks = {};   // 寄存事件的名字  
  }  
  $on(name, fn) {this.callbacks[name] = this.callbacks[name] || [];  
    this.callbacks[name].push(fn);  
  }  
  $emit(name, args) {if (this.callbacks[name]) {this.callbacks[name].forEach((cb) => cb(args));  
    }  
  }  
}  

// main.js  
Vue.prototype.$bus = new Bus() // 将 $bus 挂载到 vue 实例的原型上  
// 另一种形式  
Vue.prototype.$bus = new Vue() // Vue 曾经实现了 Bus 的性能  
<template>
    <button @click="toBus"> 子组件传给兄弟组件 </button>
</template>

<script>
export default{
    methods: {toBus () {this.$bus.$emit('foo', '来自兄弟组件')
    }
  }
}
</script>

另一个组件也在钩子函数中监听 on 事件

export default {data() {
    return {message: ''}
  },
  mounted() {this.$bus.$on('foo', (msg) => {this.message = msg})
  }
}

7. $root 拜访根组件中的属性或办法

  • 作用:拜访根组件中的属性或办法
  • 留神:是根组件,不是父组件。$root只对根组件有用
var vm = new Vue({
  el: "#app",
  data() {
    return {rootInfo:"我是根元素的属性"}
  },
  methods: {alerts() {alert(111)
    }
  },
  components: {
    com1: {data() {
        return {info: "组件 1"}
      },
      template: "<p>{{info}} <com2></com2></p>",
      components: {
        com2: {
          template: "<p> 我是组件 1 的子组件 </p>",
          created() {this.$root.alerts()// 根组件办法
            console.log(this.$root.rootInfo)// 我是根元素的属性
          }
        }
      }
    }
  }
});

8. vuex

  • 实用场景: 简单关系的组件数据传递
  • Vuex 作用相当于一个用来存储共享变量的容器
  • state用来寄存共享变量的中央
  • getter,能够减少一个 getter 派生状态,(相当于 store 中的计算属性),用来取得共享变量的值
  • mutations用来寄存批改 state 的办法。
  • actions也是用来寄存批改 state 的办法,不过 action 是在 mutations 的根底上进行。罕用来做一些异步操作

小结

  • 父子关系的组件数据传递抉择 props$emit进行传递,也可抉择ref
  • 兄弟关系的组件数据传递可抉择 $bus,其次能够抉择$parent 进行传递
  • 先人与后辈组件数据传递可抉择 attrslisteners或者 ProvideInject
  • 简单关系的组件数据传递能够通过 vuex 寄存共享的变量

vue-loader 是什么?它有什么作用?

答复范例

  1. vue-loader是用于解决单文件组件(SFCSingle-File Component)的webpack loader
  2. 因为有了 vue-loader,咱们就能够在我的项目中编写SFC 格局的 Vue 组件,咱们能够把代码宰割为 <template><script><style>,代码会异样清晰。联合其余 loader 咱们还能够用 Pug 编写 <template>,用SASS 编写 <style>,用TS 编写 <script>。咱们的<style> 还能够独自作用以后组件
  3. webpack打包时,会以 loader 的形式调用vue-loader
  4. vue-loader被执行时,它会对 SFC 中的每个语言块用独自的 loader 链解决。最初将这些独自的块装配成最终的组件模块

原理

vue-loader会调用 @vue/compiler-sfc 模块解析 SFC 源码为一个描述符(Descriptor),而后为每个语言块生成 import 代码,返回的代码相似上面

// source.vue 被 vue-loader 解决之后返回的代码
​
// import the <template> block
import render from 'source.vue?vue&type=template'
// import the <script> block
import script from 'source.vue?vue&type=script'
export * from 'source.vue?vue&type=script'
// import <style> blocks
import 'source.vue?vue&type=style&index=1'
​
script.render = render
export default script

咱们想要 script 块中的内容被作为 js 解决(当然如果是 <script lang="ts"> 被作为 ts 理),这样咱们想要 webpack 把配置中跟 .js 匹配的规定都利用到形如 source.vue?vue&type=script 的这个申请上。例如咱们对所有 *.js 配置了babel-loader,这个规定将被克隆并利用到所在Vue SFC

import script from 'source.vue?vue&type=script

将被开展为:

import script from 'babel-loader!vue-loader!source.vue?vue&type=script'

相似的,如果咱们对 .sass 文件配置了style-loader + css-loader + sass-loader,对上面的代码

<style scoped lang="scss">

vue-loader将会返回给咱们上面后果:

import 'source.vue?vue&type=style&index=1&scoped&lang=scss'

而后 webpack 会开展如下:

import 'style-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'
  • 当解决开展申请时,vue-loader将被再次调用。这次,loader将会关注那些有查问串的申请,且仅针对特定块,它会选中特定块外部的内容并传递给前面匹配的loader
  • 对于 <script> 块,解决到这就能够了,然而 <template><style> 还有一些额定工作要做,比方

    • 须要用 Vue 模板编译器编译 template,从而失去render 函数
    • 须要对 <style scoped>中的 CSS 做后处理(post-process),该操作在 css-loader 之后但在 style-loader 之前

实现上这些附加的 loader 须要被注入到曾经开展的 loader 链上,最终的申请会像上面这样:

// <template lang="pug">
import 'vue-loader/template-loader!pug-loader!source.vue?vue&type=template'
​
// <style scoped lang="scss">
import 'style-loader!vue-loader/style-post-loader!css-loader!sass-loader!vue-loader!source.vue?vue&type=style&index=1&scoped&lang=scss'

diff 算法

工夫复杂度: 个树的齐全 diff 算法是一个工夫复杂度为 O(n*3),vue 进行优化转化成 O(n)

了解:

  • 最小量更新, key 很重要。这个能够是这个节点的惟一标识,通知 diff 算法,在更改前后它们是同一个 DOM 节点

    • 扩大 v-for 为什么要有 key,没有 key 会暴力复用,举例子的话轻易说一个比方挪动节点或者减少节点(批改 DOM),加 key 只会挪动缩小操作 DOM。
  • 只有是同一个虚构节点才会进行精细化比拟,否则就是暴力删除旧的,插入新的。
  • 只进行同层比拟,不会进行跨层比拟。

diff 算法的优化策略:四种命中查找,四个指针

  1. 旧前与新前(先比结尾,后插入和删除节点的这种状况)
  2. 旧后与新后(比结尾,前插入或删除的状况)
  3. 旧前与新后(头与尾比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧后之后)
  4. 旧后与新前(尾与头比,此种产生了,波及挪动节点,那么新前指向的节点,挪动到旧前之前)

vue3 中 watch、watchEffect 区别

  • watch是惰性执行,也就是只有监听的值发生变化的时候才会执行,然而 watchEffect 不同,每次代码加载 watchEffect 都会执行(疏忽 watch 第三个参数的配置,如果批改配置项也能够实现立刻执行)
  • watch须要传递监听的对象,watchEffect不须要
  • watch只能监听响应式数据:ref定义的属性和 reactive 定义的对象,如果间接监听 reactive 定义对象中的属性是不容许的(会报正告),除非应用函数转换一下。其实就是官网上说的监听一个getter
  • watchEffect如果监听 reactive 定义的对象是不起作用的,只能监听对象中的属性

看一下 watchEffect 的代码

<template>
<div>
  请输出 firstName:<input type="text" v-model="firstName">
</div>
<div>
  请输出 lastName:<input type="text" v-model="lastName">
</div>
<div>
  请输出 obj.text:<input type="text" v-model="obj.text">
</div>
 <div>【obj.text】{{obj.text}}
 </div>
</template>

<script>
import {ref, reactive, watch, watchEffect} from 'vue'
export default {
  name: "HelloWorld",
  props: {msg: String,},
  setup(props,content){let firstName = ref('')
    let lastName = ref('')
    let obj= reactive({text:'hello'})
    watchEffect(()=>{console.log('触发了 watchEffect');
      console.log(` 组合后的名称为:${firstName.value}${lastName.value}`)
    })
    return{
      obj,
      firstName,
      lastName
    }
  }
};
</script>

革新一下代码

watchEffect(()=>{console.log('触发了 watchEffect');
  // 这里咱们不应用 firstName.value/lastName.value,相当于是监控整个 ref, 对应第四点下面的论断
  console.log(` 组合后的名称为:${firstName}${lastName}`)
})
watchEffect(()=>{console.log('触发了 watchEffect');
  console.log(obj);
})

略微革新一下

let obj = reactive({text:'hello'})
watchEffect(()=>{console.log('触发了 watchEffect');
  console.log(obj.text);
})

再看一下 watch 的代码,验证一下

let obj= reactive({text:'hello'})
// watch 是惰性执行,默认初始化之后不会执行,只有值有变动才会触发,可通过配置参数实现默认执行
watch(obj, (newValue, oldValue) => {
  // 回调函数
  console.log('触发监控更新了 new',  newValue);
  console.log('触发监控更新了 old',  oldValue);
},{
  // 配置 immediate 参数,立刻执行,以及深层次监听
  immediate: true,
  deep: true
})
  • 监控整个 reactive 对象,从下面的图能够看到 deep 理论默认是开启的,就算咱们设置为 false 也还是有效。而且旧值获取不到。
  • 要获取旧值则须要监控对象的属性,也就是监听一个getter,看下图

总结

  • 如果定义了 reactive 的数据,想去应用 watch 监听数据扭转,则无奈正确获取旧值,并且 deep 属性配置有效,主动强制开启了深层次监听。
  • 如果应用 ref 初始化一个对象或者数组类型的数据,会被主动转成 reactive 的实现形式,生成 proxy 代理对象。也会变得无奈正确取旧值。
  • 用任何形式生成的数据,如果接管的变量是一个 proxy 代理对象,就都会导致 watch 这个对象时,watch回调里无奈正确获取旧值。
  • 所以当大家应用 watch 监听对象时,如果在不须要应用旧值的状况,能够失常监听对象没关系;然而如果当监听扭转函数外面须要用到旧值时,只能监听 对象.xxx` 属性 的形式才行

watch 和 watchEffect 异同总结

体验

watchEffect立刻运行一个函数,而后被动地追踪它的依赖,当这些依赖扭转时从新执行该函数

const count = ref(0)
​
watchEffect(() => console.log(count.value))
// -> logs 0
​
count.value++
// -> logs 1

watch侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数

const state = reactive({count: 0})
watch(() => state.count,
  (count, prevCount) => {/* ... */}
)

答复范例

  1. watchEffect立刻运行一个函数,而后被动地追踪它的依赖,当这些依赖扭转时从新执行该函数。watch侦测一个或多个响应式数据源并在数据源变动时调用一个回调函数
  2. watchEffect(effect)是一种非凡 watch,传入的函数既是依赖收集的数据源,也是回调函数。如果咱们不关怀响应式数据变动前后的值,只是想拿这些数据做些事件,那么watchEffect 就是咱们须要的。watch更底层,能够接管多种数据源,包含用于依赖收集的 getter 函数,因而它齐全能够实现 watchEffect 的性能,同时因为能够指定 getter 函数,依赖能够管制的更准确,还能获取数据变动前后的值,因而如果须要这些时咱们会应用watch
  3. watchEffect在应用时,传入的函数会立即执行一次。watch默认状况下并不会执行回调函数,除非咱们手动设置 immediate 选项
  4. 从实现上来说,watchEffect(fn)相当于watch(fn,fn,{immediate:true})

watchEffect定义如下

export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {return doWatch(effect, null, options)
}

watch定义如下

export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {return doWatch(source as any, cb, options)
}

很显著 watchEffect 就是一种非凡的 watch 实现。

vue 要做权限治理该怎么做?如果管制到按钮级别的权限怎么做

一、是什么

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

而前端权限归根结底是申请的发动权,申请的发动可能有上面两种模式触发

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

总的来说,所有的申请发动都触发自前端路由或视图

所以咱们能够从这两方面动手,对触发权限的源头进行管制,最终要实现的指标是:

  • 路由方面,用户登录后只能看到本人有权拜访的导航菜单,也只能拜访本人有权拜访的路由地址,否则将跳转 4xx 提醒页
  • 视图方面,用户只能看到本人有权浏览的内容和有权操作的控件
  • 最初再加上申请管制作为最初一道防线,路由可能配置失误,按钮可能忘了加权限,这种时候申请管制能够用来兜底,越权申请将在前端被拦挡

二、如何做

前端权限管制能够分为四个方面:

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

接口权限

接口权限目前个别采纳 jwt 的模式来验证,没有通过的话个别返回401,跳转到登录页面从新进行登录

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

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')
    }
})

路由权限管制

计划一

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

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
      }
    }]
  }]

这种形式存在以下四种毛病:

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

计划二

初始化的时候先挂载不须要权限管制的路由,比方登录页,404 等谬误页。如果用户通过 URL 进行强制拜访,则会间接进入 404,相当于从源头上做了管制

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

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
})

按需挂载,路由就须要晓得用户的路由权限,也就是在用户登录进来的时候就要晓得以后用户领有哪些路由权限

这种形式也存在了以下的毛病:

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

菜单权限

菜单权限能够了解成将页面与理由进行解耦

计划一

菜单与路由拆散,菜单由后端返回

前端定义路由信息

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

name字段都不为空,须要依据此字段与后端返回菜单做关联,后端返回的菜单信息中必须要有 name 对应的字段,并且做唯一性校验

全局路由守卫里做判断

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 动静挂载

这种形式的毛病:

  • 菜单须要与路由做一一对应,前端增加了新性能,须要通过菜单治理性能增加新的菜单,如果菜单配置的不对会导致利用不能失常应用
  • 全局路由守卫里,每次路由跳转都要做判断

计划二

菜单和路由都由后端返回

前端对立定义路由组件

const Home = () => import("../pages/Home.vue");
const UserInfo = () => import("../pages/UserInfo.vue");
export default {
    home: Home,
    userInfo: UserInfo
};

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

[
    {
        name: "home",
        path: "/",
        component: "home"
    },
    {
        name: "home",
        path: "/userinfo",
        component: "userInfo"
    }
]

在将后端返回路由通过 addRoutes 动静挂载之间,须要将数据处理一下,将 component 字段换为真正的组件

如果有嵌套路由,后端功能设计的时候,要留神增加相应的字段,前端拿到数据也要做相应的解决

这种办法也会存在毛病:

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

按钮权限

计划一

按钮权限也能够用 v-if 判断

然而如果页面过多,每个页面页面都要获取用户权限 role 和路由表里的meta.btnPermissions,而后再做判断

这种形式就不开展举例了

计划二

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

首先配置路由

{
    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']
        } // 页面须要的权限
    }]
}

自定义权限鉴定指令

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 指令

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

小结

对于权限如何抉择哪种适合的计划,能够依据本人我的项目的计划我的项目,如思考路由与菜单是否拆散

权限须要前后端联合,前端尽可能的去管制,更多的须要后盾判断

SPA 首屏加载速度慢的怎么解决

一、什么是首屏加载

首屏工夫(First Contentful Paint),指的是浏览器从响应用户输出网址地址,到首屏内容渲染实现的工夫,此时整个网页不肯定要全副渲染实现,但须要展现以后视窗须要的内容

首屏加载能够说是用户体验中 最重要 的环节

对于计算首屏工夫

利用 performance.timing 提供的数据:

通过 DOMContentLoad 或者 performance 来计算出首屏工夫

// 计划一:document.addEventListener('DOMContentLoaded', (event) => {console.log('first contentful painting');
});
// 计划二:performance.getEntriesByName("first-contentful-paint")[0].startTime

// performance.getEntriesByName("first-contentful-paint")[0]
// 会返回一个 PerformancePaintTiming 的实例,构造如下:{
  name: "first-contentful-paint",
  entryType: "paint",
  startTime: 507.80000002123415,
  duration: 0,
};

二、加载慢的起因

在页面渲染的过程,导致加载速度慢的因素可能如下:

  • 网络延时问题
  • 资源文件体积是否过大
  • 资源是否反复发送申请去加载了
  • 加载脚本的时候,渲染内容梗塞了

三、解决方案

常见的几种 SPA 首屏优化形式

  • 减小入口文件积
  • 动态资源本地缓存
  • UI 框架按需加载
  • 图片资源的压缩
  • 组件反复打包
  • 开启 GZip 压缩
  • 应用 SSR

1. 减小入口文件体积

罕用的伎俩是路由懒加载,把不同路由对应的组件宰割成不同的代码块,待路由被申请的时候会独自打包路由,使得入口文件变小,加载速度大大增加

vue-router 配置路由的时候,采纳动静加载路由的模式

routes:[ 
    path: 'Blogs',
    name: 'ShowBlogs',
    component: () => import('./components/ShowBlogs.vue')
]

以函数的模式加载路由,这样就能够把各自的路由文件别离打包,只有在解析给定的路由时,才会加载路由组件

2. 动态资源本地缓存

后端返回资源问题:

  • 采纳 HTTP 缓存,设置 Cache-ControlLast-ModifiedEtag 等响应头
  • 采纳 Service Worker 离线缓存

前端正当利用localStorage

3. UI 框架按需加载

在日常应用 UI 框架,例如 element-UI、或者antd,咱们经常性间接援用整个UI

import ElementUI from 'element-ui'
Vue.use(ElementUI)

但实际上我用到的组件只有按钮,分页,表格,输出与正告 所以咱们要按需援用

import {Button, Input, Pagination, Table, TableColumn, MessageBox} from 'element-ui';
Vue.use(Button)
Vue.use(Input)
Vue.use(Pagination)

4. 组件反复打包

假如 A.js 文件是一个罕用的库,当初有多个路由应用了 A.js 文件,这就造成了反复下载

解决方案:在 webpackconfig文件中,批改 CommonsChunkPlugin 的配置

minChunks: 3

minChunks为 3 示意会把应用 3 次及以上的包抽离进去,放进公共依赖文件,防止了反复加载组件

5. 图片资源的压缩

图片资源尽管不在编码过程中,但它却是对页面性能影响最大的因素

对于所有的图片资源,咱们能够进行适当的压缩

对页面上应用到的 icon,能够应用在线字体图标,或者雪碧图,将泛滥小图标合并到同一张图上,用以加重http 申请压力。

6. 开启 GZip 压缩

拆完包之后,咱们再用 gzip 做一下压缩 装置compression-webpack-plugin

cnmp i compression-webpack-plugin -D

vue.congig.js 中引入并批改 webpack 配置

const CompressionPlugin = require('compression-webpack-plugin')

configureWebpack: (config) => {if (process.env.NODE_ENV === 'production') {
            // 为生产环境批改配置...
            config.mode = 'production'
            return {
                plugins: [new CompressionPlugin({
                    test: /\.js$|\.html$|\.css/, // 匹配文件名
                    threshold: 10240, // 对超过 10k 的数据进行压缩
                    deleteOriginalAssets: false // 是否删除原文件
                })]
            }
        }

在服务器咱们也要做相应的配置 如果发送申请的浏览器反对 gzip,就发送给它gzip 格局的文件 我的服务器是用 express 框架搭建的 只有装置一下 compression 就能应用

const compression = require('compression')
app.use(compression())  // 在其余中间件应用之前调用

7. 应用 SSR

SSR(Server side),也就是服务端渲染,组件或页面通过服务器生成 html 字符串,再发送到浏览器

从头搭建一个服务端渲染是很简单的,vue利用倡议应用 Nuxt.js 实现服务端渲染

四、小结

缩小首屏渲染工夫的办法有很多,总的来讲能够分成两大部分:资源加载优化 页面渲染优化

下图是更为全面的首屏优化的计划

大家能够依据本人我的项目的状况抉择各种形式进行首屏渲染的优化

退出移动版