写点什么

滴滴前端必会 vue 面试题汇总

作者:bb_xiaxia1998
  • 2023-05-19
    浙江
  • 本文字数:17485 字

    阅读完需:约 57 分钟

watch 原理

watch 本质上是为每个监听属性 setter 创建了一个 watcher,当被监听的属性更新时,调用传入的回调函数。常见的配置选项有 deepimmediate,对应原理如下


  • deep:深度监听对象,为对象的每一个属性创建一个 watcher,从而确保对象的每一个属性更新时都会触发传入的回调函数。主要原因在于对象属于引用类型,单个属性的更新并不会触发对象 setter,因此引入 deep 能够很好地解决监听对象的问题。同时也会引入判断机制,确保在多个属性更新时回调函数仅触发一次,避免性能浪费。

  • immediate:在初始化时直接调用回调函数,可以通过在 created 阶段手动调用回调函数实现相同的效果

对虚拟 DOM 的理解?

从本质上来说,Virtual Dom 是一个 JavaScript 对象,通过对象的方式来表示 DOM 结构。将页面的状态抽象为 JS 对象的形式,配合不同的渲染工具,使跨平台渲染成为可能。通过事务处理机制,将多次 DOM 修改的结果一次性的更新到页面上,从而有效的减少页面渲染的次数,减少修改 DOM 的重绘重排次数,提高渲染性能。


虚拟 DOM 是对 DOM 的抽象,这个对象是更加轻量级的对 DOM 的描述。它设计的最初目的,就是更好的跨平台,比如 Node.js 就没有 DOM,如果想实现 SSR,那么一个方式就是借助虚拟 DOM,因为虚拟 DOM 本身是 js 对象。 在代码渲染到页面之前,vue 会把代码转换成一个对象(虚拟 DOM)。以对象的形式来描述真实 DOM 结构,最终渲染到页面。在每次数据发生变化前,虚拟 DOM 都会缓存一份,变化之时,现在的虚拟 DOM 会与缓存的虚拟 DOM 进行比较。在 vue 内部封装了 diff 算法,通过这个算法来进行比较,渲染时修改改变的变化,原先没有发生改变的通过原先的数据进行渲染。


另外现代前端框架的一个基本要求就是无须手动操作 DOM,一方面是因为手动操作 DOM 无法保证程序性能,多人协作的项目中如果 review 不严格,可能会有开发者写出性能较低的代码,另一方面更重要的是省略手动 DOM 操作可以大大提高开发效率。

Vue3 的设计目标是什么?做了哪些优化

1、设计目标


不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题


  • 随着功能的增长,复杂组件的代码变得越来越难以维护

  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制

  • 类型推断不够友好

  • bundle的时间太久了


Vue3 经过长达两三年时间的筹备,做了哪些事情?


我们从结果反推


  • 更小

  • 更快

  • TypeScript 支持

  • API 设计一致性

  • 提高自身可维护性

  • 开放更多底层功能


一句话概述,就是更小更快更友好了


更小


  • Vue3移除一些不常用的 API

  • 引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了


更快


主要体现在编译方面:


  • diff算法优化

  • 静态提升

  • 事件监听缓存

  • SSR优化


更友好


vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力


这里代码简单演示下:


存在一个获取鼠标位置的函数


import { toRefs, reactive } from 'vue';function useMouse(){    const state = reactive({x:0,y:0});    const update = e=>{        state.x = e.pageX;        state.y = e.pageY;    }    onMounted(()=>{        window.addEventListener('mousemove',update);    })    onUnmounted(()=>{        window.removeEventListener('mousemove',update);    })
return toRefs(state);}
复制代码


我们只需要调用这个函数,即可获取xy的坐标,完全不用关注实现过程


试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高


同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示


2、优化方案


vue3从很多层面都做了优化,可以分成三个方面:


  • 源码

  • 性能

  • 语法 API


源码


源码可以从两个层面展开:


  • 源码管理

  • TypeScript


源码管理


vue3整个源码是通过 monorepo的方式维护的,根据功能将不同的模块拆分到packages目录下面不同的子目录中



这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性


另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue


TypeScript


Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导


性能


vue3是从什么哪些方面对性能进行进一步优化呢?


  • 体积优化

  • 编译优化

  • 数据劫持优化


这里讲述数据劫持:


vue2中,数据劫持是通过Object.defineProperty,这个 API 有一些缺陷,并不能检测对象属性的添加和删除


Object.defineProperty(data, 'a',{  get(){    // track  },  set(){    // trigger  }})
复制代码


尽管Vue为了解决这个问题提供了 setdelete实例方法,但是对于用户来说,还是增加了一定的心智负担


同时在面对嵌套层级比较深的情况下,就存在性能问题


default {  data: {    a: {      b: {          c: {          d: 1        }      }    }  }}
复制代码


相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到


同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归


语法 API


这里当然说的就是composition API,其两大显著的优化:


  • 优化逻辑组织

  • 优化逻辑复用


逻辑组织


一张图,我们可以很直观地感受到 Composition API在逻辑组织方面的优势



相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一块


逻辑复用


vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰


而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可


同样是上文的获取鼠标位置的例子


import { toRefs, reactive, onUnmounted, onMounted } from 'vue';function useMouse(){    const state = reactive({x:0,y:0});    const update = e=>{        state.x = e.pageX;        state.y = e.pageY;    }    onMounted(()=>{        window.addEventListener('mousemove',update);    })    onUnmounted(()=>{        window.removeEventListener('mousemove',update);    })
return toRefs(state);}
复制代码


组件使用


import useMousePosition from './mouse'export default {    setup() {        const { x, y } = useMousePosition()        return { x, y }    }}
复制代码


可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题

Vue-router 除了 router-link 怎么实现跳转

声明式导航


<router-link to="/about">Go to About</router-link>
复制代码


编程式导航


// literal string pathrouter.push('/users/1')// object with pathrouter.push({ path: '/users/1' })// named route with params to let the router build the urlrouter.push({ name: 'user', params: { username: 'test' } })
复制代码


回答范例


  • vue-router导航有两种方式:声明式导航和编程方式导航

  • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定pathnameparams等信息

  • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个 a 标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航

  • 实际上内部两者调用的导航函数是一样的

vue-router 守卫

导航守卫 router.beforeEach 全局前置守卫


  • to: Route: 即将要进入的目标(路由对象)

  • from: Route: 当前导航正要离开的路由

  • next: Function: 一定要调用该方法来 resolve 这个钩子。(一定要用这个函数才能去到下一个路由,如果不用就拦截)

  • 执行效果依赖 next 方法的调用参数。

  • next(): 进行管道中的下一个钩子。如果全部钩子执行完了,则导航的状态就是 confirmed (确认的)。

  • next(false):取消进入路由,url 地址重置为 from 路由地址(也就是将要离开的路由地址)


// main.js 入口文件import router from './router'; // 引入路由router.beforeEach((to, from, next) => {   next();});router.beforeResolve((to, from, next) => {  next();});router.afterEach((to, from) => {  console.log('afterEach 全局后置钩子');});
复制代码


路由独享的守卫 你可以在路由配置上直接定义 beforeEnter 守卫


const router = new VueRouter({  routes: [    {      path: '/foo',      component: Foo,      beforeEnter: (to, from, next) => {        // ...      }    }  ]})
复制代码


组件内的守卫你可以在路由组件内直接定义以下路由导航守卫


const Foo = {  template: `...`,  beforeRouteEnter (to, from, next) {    // 在渲染该组件的对应路由被 confirm 前调用    // 不!能!获取组件实例 `this`    // 因为当守卫执行前,组件实例还没被创建  },  beforeRouteUpdate (to, from, next) {    // 在当前路由改变,但是该组件被复用时调用    // 举例来说,对于一个带有动态参数的路径 /foo/:id,在 /foo/1 和 /foo/2 之间跳转的时候,    // 由于会渲染同样的 Foo 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。    // 可以访问组件实例 `this`  },  beforeRouteLeave (to, from, next) {    // 导航离开该组件的对应路由时调用,我们用它来禁止用户离开    // 可以访问组件实例 `this`    // 比如还未保存草稿,或者在用户离开前,    将setInterval销毁,防止离开之后,定时器还在调用。  }}
复制代码

如果让你从零开始写一个 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: { }})
复制代码


参考 前端进阶面试题详细解答

你知道哪些 Vue3 新特性?

官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)



  • Composition API

  • SFC Composition API语法糖

  • Teleport传送门

  • Fragments片段

  • Emits选项

  • 自定义渲染器

  • SFC CSS变量

  • Suspense


以上这些是 api 相关,另外还有很多框架特性也不能落掉


回答范例


  1. api层面Vue3新特性主要包括:Composition APISFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense

  2. 另外,Vue3.0在框架层面也有很多亮眼的改进:


  • 更快

  • 虚拟DOM重写,diff算法优化

  • 编译器优化:静态提升、patchFlags(静态标记)、事件监听缓存

  • 基于Proxy的响应式系统

  • SSR优化

  • 更小 :更好的摇树优化 tree shakingVue3移除一些不常用的 API

  • 更友好vue3在兼顾vue2options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力

  • 更容易维护TypeScript + 模块化

  • 更容易扩展

  • 独立的响应化模块

  • 自定义渲染器

你有对 Vue 项目进行哪些优化?

(1)代码层面的优化


  • v-if 和 v-show 区分使用场景

  • computed 和 watch 区分使用场景

  • v-for 遍历必须为 item 添加 key,且避免同时使用 v-if

  • 长列表性能优化

  • 事件的销毁

  • 图片资源懒加载

  • 路由懒加载

  • 第三方插件的按需引入

  • 优化无限列表性能

  • 服务端渲染 SSR or 预渲染


(2)Webpack 层面的优化


  • Webpack 对图片进行压缩

  • 减少 ES6 转为 ES5 的冗余代码

  • 提取公共代码

  • 模板预编译

  • 提取组件的 CSS

  • 优化 SourceMap

  • 构建结果输出分析

  • Vue 项目的编译优化


(3)基础的 Web 技术的优化


  • 开启 gzip 压缩

  • 浏览器缓存

  • CDN 的使用

  • 使用 Chrome Performance 查找性能瓶颈

vue 的优点

轻量级框架:只关注视图层,是一个构建数据的视图集合,大小只有几十 kb;


简单易学:国人开发,中文文档,不存在语言障碍 ,易于理解和学习;


双向数据绑定:保留了 angular 的特点,在数据操作方面更为简单;


组件化:保留了 react 的优点,实现了 html 的封装和重用,在构建单页面应用方面有着独特的优势;


视图,数据,结构分离:使数据的更改更为简单,不需要进行逻辑代码的修改,只需要操作数据就能完成相关操作;


虚拟 DOM:dom 操作是非常耗费性能的,不再使用原生的 dom 操作节点,极大解放 dom 操作,但具体操作的还是 dom 不过是换了另一种方式;


运行速度更快:相比较与 react 而言,同样是操作虚拟 dom,就性能而言,vue 存在很大的优势。

Vue 组件间通信有哪几种方式?

​ Vue 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。Vue 组件间通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。


(1)props / $emit 适用 父子组件通信


这种方法是 Vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。


(2)ref$parent / $children 适用 父子组件通信


  • ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例

  • $parent / $children:访问父 / 子实例


(3)EventBus ($emit / $on) 适用于 父子、隔代、兄弟组件通信


这种方法通过一个空的 Vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。


(4)$attrs/$listeners 适用于 隔代组件通信


  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 ( class 和 style 除外 )。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 ( class 和 style 除外 ),并且可以通过 v-bind="$attrs" 传入内部组件。通常配合 inheritAttrs 选项一起使用。

  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件


(5)provide / inject 适用于 隔代组件通信


祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。 provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。


(6)Vuex 适用于 父子、隔代、兄弟组件通信


Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。每一个 Vuex 应用的核心就是 store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state )。


  • Vuex 的状态存储是响应式的。当 Vue 组件从 store 中读取状态的时候,若 store 中的状态发生变化,那么相应的组件也会相应地得到高效更新。

  • 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。

Vue 组件如何通信?

Vue 组件通信的方法如下:


  • props/$emit+v-on: 通过 props 将数据自上而下传递,而通过 $emit 和 v-on 来向上传递信息。

  • EventBus: 通过 EventBus 进行信息的发布与订阅

  • vuex: 是全局数据管理库,可以通过 vuex 管理全局的数据流

  • $attrs/$listeners: Vue2.4 中加入的$attrs/$listeners可以进行跨级的组件通信

  • provide/inject:以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效,这成为了跨组件通信的基础


还有一些用 solt 插槽或者 ref 实例进行通信的,使用场景过于有限就不赘述了。

在 Vue 中使用插件的步骤

  • 采用ES6import ... from ...语法或CommonJSrequire()方法引入插件

  • 使用全局方法Vue.use( plugin )使用插件,可以传入一个选项对象Vue.use(MyPlugin, { someOption: true })

Vue 项目本地开发完成后部署到服务器后报 404 是什么原因呢

如何部署

前后端分离开发模式下,前后端是独立布署的,前端只需要将最后的构建物上传至目标服务器的web容器指定的静态目录下即可


我们知道vue项目在构建后,是生成一系列的静态文件


常规布署我们只需要将这个目录上传至目标服务器即可




复制代码


web容器跑起来,以nginx为例


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; }}
复制代码


配置完成记得重启nginx


// 检查配置是否正确nginx -t 
// 平滑重启nginx -s reload
复制代码


操作完后就可以在浏览器输入域名进行访问了


当然上面只是提到最简单也是最直接的一种布署方式


什么自动化,镜像,容器,流水线布署,本质也是将这套逻辑抽象,隔离,用程序来代替重复性的劳动,本文不展开

404 问题

这是一个经典的问题,相信很多同学都有遇到过,那么你知道其真正的原因吗?


我们先还原一下场景:


  • vue项目在本地时运行正常,但部署到服务器中,刷新页面,出现了 404 错误


先定位一下,HTTP 404 错误意味着链接指向的资源不存在


问题在于为什么不存在?且为什么只有history模式下会出现这个问题?


为什么 history 模式下有问题


Vue是属于单页应用(single-page application)


SPA是一种网络应用程序或网站的模型,所有用户交互是通过动态重写当前页面,前面我们也看到了,不管我们应用有多少页面,构建物都只会产出一个index.html


现在,我们回头来看一下我们的nginx配置


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; }}
复制代码


可以根据 nginx 配置得出,当我们在地址栏输入 www.xxx.com 时,这时会打开我们 dist 目录下的 index.html 文件,然后我们在跳转路由进入到 www.xxx.com/login


关键在这里,当我们在 website.com/login 页执行刷新操作,nginx location 是没有相关配置的,所以就会出现 404 的情况


为什么 hash 模式下没有问题


router hash 模式我们都知道是用符号 #表示的,如 website.com/#/login, hash 的值为 #/login


它的特点在于:hash 虽然出现在 URL 中,但不会被包括在 HTTP 请求中,对服务端完全没有影响,因此改变 hash 不会重新加载页面


hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 website.com/#/login 只有 website.com 会被包含在请求中 ,因此对于服务端来说,即使没有配置location,也不会返回404错误

解决方案

看到这里我相信大部分同学都能想到怎么解决问题了,


产生问题的本质是因为我们的路由是通过 JS 来执行视图切换的,


当我们进入到子路由时刷新页面,web容器没有相对应的页面此时会出现404


所以我们只需要配置将任意页面都重定向到 index.html,把路由交由前端处理


nginx配置文件.conf修改,添加try_files $uri $uri/ /index.html;


server {  listen  80;  server_name  www.xxx.com;
location / { index /data/dist/index.html; try_files $uri $uri/ /index.html; }}
复制代码


修改完配置文件后记得配置的更新


nginx -s reload
复制代码


这么做以后,你的服务器就不再返回 404 错误页面,因为对于所有路径都会返回 index.html 文件


为了避免这种情况,你应该在 Vue 应用里面覆盖所有的路由情况,然后在给出一个 404 页面


const router = new VueRouter({  mode: 'history',  routes: [    { path: '*', component: NotFoundComponent }  ]})
复制代码

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>
复制代码


小结


关于权限如何选择哪种合适的方案,可以根据自己项目的方案项目,如考虑路由与菜单是否分离


权限需要前后端结合,前端尽可能的去控制,更多的需要后台判断

你觉得 vuex 有什么缺点

分析


相较于reduxvuex已经相当简便好用了。但模块的使用比较繁琐,对ts支持也不好。


体验


使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持


const store = createStore({  modules: {    a: moduleA  }})store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutationstore.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
复制代码


回答范例


  1. vuex利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档

  2. 比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的 path 来匹配,使用模式不统一,容易出错;对 ts 的支持也不友好,在使用模块时没有代码提示。

  3. 之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合


原理


下面我们来看看vuexstore.state.x.y这种嵌套的路径是怎么搞出来的


首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state['x'] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state['x']['y'] = moduleY.state


//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115if (!isRoot && !hot) {    // 获取父模块state    const parentState = getNestedState(rootState, path.slice(0, -1))    // 获取子模块名称    const moduleName = path[path.length - 1]    store._withCommit(() => {        // 把子模块state设置到父模块上        parentState[moduleName] = module.state    })}
复制代码

你是怎么处理 vue 项目中的错误的?

分析


  • 这是一个综合应用题目,在项目中我们常常需要将 App 的异常上报,此时错误处理就很重要了。

  • 这里要区分错误的类型,针对性做收集。

  • 然后是将收集的的错误信息上报服务器。


思路


  • 首先区分错误类型

  • 根据错误不同类型做相应收集

  • 收集的错误是如何上报服务器的


回答范例


  1. 应用中的错误类型分为"接口异常"和“代码逻辑异常

  2. 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误

  3. 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过vuex存储的全局状态和路由信息获取


实践


axios拦截器中处理捕获异常:


// 响应拦截器instance.interceptors.response.use(  (response) => {    return response.data;  },  (error) => {    // 存在response说明服务器有响应    if (error.response) {      let response = error.response;      if (response.status >= 400) {        handleError(response);      }    } else {      handleError(null);    }    return Promise.reject(error);  },);
复制代码


vue中全局捕获异常:


import { createApp } from 'vue'const app = createApp(...)app.config.errorHandler = (err, instance, info) => {  // report error to tracking services}
复制代码


处理接口请求错误:


function handleError(error, type) {  if(type == 1) {    // 接口错误,从config字段中获取请求信息    let { url, method, params, data } = error.config    let err_data = {       url, method,       params: { query: params, body: data },       error: error.data?.message || JSON.stringify(error.data),    })  }}
复制代码


处理前端逻辑错误:


function handleError(error, type) {  if(type == 2) {    let errData = null    // 逻辑错误    if(error instanceof Error) {      let { name, message } = error      errData = {        type: name,        error: message      }    } else {      errData = {        type: 'other',        error: JSON.strigify(error)      }    }  }}
复制代码

Vue 模版编译原理知道吗,能简单说一下吗?

简单说,Vue 的编译过程就是将template转化为render函数的过程。会经历以下阶段:


  • 生成 AST 树

  • 优化

  • codegen


首先解析模版,生成AST语法树(一种用 JavaScript 对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。


Vue 的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的 DOM 也不会变化。那么优化过程就是深度遍历 AST 树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对,对运行时的模板起到很大的优化作用。


编译的最后一步是将优化后的AST树转换为可执行的代码

vue-router 路由钩子函数是什么 执行顺序是什么

路由钩子的执行流程, 钩子函数种类有:全局守卫、路由守卫、组件守卫


完整的导航解析流程:


  1. 导航被触发。

  2. 在失活的组件里调用 beforeRouteLeave 守卫。

  3. 调用全局的 beforeEach 守卫。

  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。

  5. 在路由配置里调用 beforeEnter。

  6. 解析异步路由组件。

  7. 在被激活的组件里调用 beforeRouteEnter。

  8. 调用全局的 beforeResolve 守卫 (2.5+)。

  9. 导航被确认。

  10. 调用全局的 afterEach 钩子。

  11. 触发 DOM 更新。

  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

Vue-router 路由有哪些模式?

一般有两种模式: (1)hash 模式:后面的 hash 值的变化,浏览器既不会向服务器发出请求,浏览器也不会刷新,每次 hash 值的变化会触发 hashchange 事件。 (2)history 模式:利用了 HTML5 中新增的 pushState() 和 replaceState() 方法。这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

Vue 中 computed 和 watch 有什么区别?

计算属性 computed: (1)支持缓存,只有依赖数据发生变化时,才会重新进行计算函数; (2)计算属性内不支持异步操作; (3)计算属性的函数中都有一个 get(默认具有,获取计算属性)和 set(手动添加,设置计算属性)方法; (4)计算属性是自动监听依赖值的变化,从而动态返回内容。


侦听属性 watch: (1)不支持缓存,只要数据发生变化,就会执行侦听函数; (2)侦听属性内支持异步操作; (3)侦听属性的值可以是一个对象,接收 handler 回调,deep,immediate 三个属性; (3)监听是一个过程,在监听的值变化时,可以触发一个回调,并做一些其他事情


用户头像

bb_xiaxia1998

关注

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

还未添加个人简介

评论

发布
暂无评论
滴滴前端必会vue面试题汇总_Vue_bb_xiaxia1998_InfoQ写作社区