写点什么

高级前端二面 vue 面试题(持续更新中)

作者:bb_xiaxia1998
  • 2023-02-13
    浙江
  • 本文字数:25355 字

    阅读完需:约 83 分钟

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就是resolveconst 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


  • 要实现一个Store存储全局状态

  • 要提供修改状态所需 API:commit(type, payload), dispatch(type, payload)


  1. 实现Store时,可以定义Store类,构造函数接收选项options,设置属性state对外暴露状态,提供commitdispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件

  2. 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.$storeVue.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,jsif (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. 缓存后如果要获取数据,解决方案可以有以下两种


  • 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. 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. 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> blockimport render from 'source.vue?vue&type=template'// import the <script> blockimport script from 'source.vue?vue&type=script'export * from 'source.vue?vue&type=script'// import <style> blocksimport 'source.vue?vue&type=style&index=1'script.render = renderexport 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 0count.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 barimport 'nprogress/nprogress.css'// progress bar styleimport { getToken } from '@/utils/auth' // getToken from cookie
NProgress.configure({ showSpinner: false })// NProgress Configuration
// permission judge functionfunction 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实现服务端渲染

四、小结

减少首屏渲染时间的方法有很多,总的来讲可以分成两大部分 :资源加载优化页面渲染优化


下图是更为全面的首屏优化的方案



大家可以根据自己项目的情况选择各种方式进行首屏渲染的优化


用户头像

bb_xiaxia1998

关注

还未添加个人签名 2022-09-01 加入

还未添加个人简介

评论

发布
暂无评论
高级前端二面vue面试题(持续更新中)_Vue_bb_xiaxia1998_InfoQ写作社区