[TOC]

31-SPA、SSR的区别是什么

我们现在编写的Vue、React和Angular应用大多数情况下都会在一个页面中,点击链接跳转页面通常是内容切换而非页面跳转,由于良好的用户体验逐渐成为主流的开发模式。但同时也会有首屏加载时间长,SEO不友好的问题,因此有了SSR,这也是为什么面试中会问到两者的区别。

思路分析

  1. 两者概念
  2. 两者优缺点分析
  3. 使用场景差异
  4. 其他选择

回答范例

  1. SPA(Single Page Application)即单页面应用。一般也称为 客户端渲染(Client Side Render), 简称 CSR。SSR(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
    Vue经典面试题(三) - 图1
    SPA
    Vue经典面试题(三) - 图2
    部署上的区别
    Vue经典面试题(三) - 图3

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

    分析

    这是一道工具类的原理题目,相当有深度,具有不错的人才区分度。

    体验

    使用官方提供的SFC playground可以很好的体验vue-loader。
    sfc.vuejs.org
    有了vue-loader加持,我们才可以以SFC的方式快速编写代码。 ```javascript

<a name="dM24q"></a>
### 思路

- vue-loader是什么东东
- vue-loader是做什么用的
- vue-loader何时生效
- vue-loader如何工作
<a name="r8MHX"></a>
### 回答范例

1. **vue-loader**是用于处理**单文件组件**(**SFC,Single-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链处理**。最后将这些**单独的块装配成最终的组件模块。**
<a name="NGmdl"></a>
### 知其所以然

1. vue-loader会调用@vue/compiler-sfc模块解析SFC源码为一个描述符(Descriptor),然后为每个语言块生成import代码,返回的代码类似下面:
```javascript
// 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
  1. 我们想要script块中的内容被作为js处理(当然如果是

    我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:
    ```javascript
    // ...
    return (_ctx, _cache) => {
      return (_openBlock(), _createElementBlock(_Fragment, null, [
        // 从缓存获取vnode
        _cache[0] || (
          _setBlockTracking(-1),
          _cache[0] = _createElementVNode("h1", null, [
            _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
          ]),
          _setBlockTracking(1),
          _cache[0]
        ),
    // ...
    

    35-什么是递归组件?举个例子说明下?

    分析

    递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。

    体验

    组件通过组件名称引用它自己,这种情况就是递归组件。

    <template>
      <li>
        <div> {{ model.name }}</div>
        <ul v-show="isOpen" v-if="isFolder">
          <!-- 注意这里:组件递归渲染了它自己 -->
          <TreeItem
            class="item"
            v-for="model in model.children"
            :model="model">
          </TreeItem>
        </ul>
      </li>
    <script>
    export default {
      name: 'TreeItem',
      // ...
    }
    </script>
    

    思路

    • 下定义
    • 使用场景
    • 使用细节
    • 原理阐述

      回答范例

    1. 如果某个组件通过组件名称引用它自己,这种情况就是递归组件。
    2. 实际开发中类似TreeMenu这类组件,它们的节点往往包含子节点子节点结构和父节点往往是相同的。这类组件的数据往往也是树形结构,这种都是使用递归组件的典型场景。
    3. 使用递归组件时,由于我们并未也不能在组件内部导入它自己,所以设置组件name属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。
    4. 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给resolveComponent,这样实际获取的组件就是当前组件本身。

      知其所以然

      递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent(“Comp”, true)
      const _component_Comp = _resolveComponent("Comp", true)
      
      就是在传递maybeSelfReference
      export function resolveComponent(   name: string,   maybeSelfReference?: boolean ): ConcreteComponent | string {   return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name }
      
      resolveAsset中最终返回的是组件自身:
      if (!res && maybeSelfReference) {    
      // fallback to implicit self-reference 
      return Component 
      }
      
      github1s.com/vuejs/core/…
      github1s.com/vuejs/core/…
      sfc.vuejs.org/#eyJBcHAudn…

      36-异步组件是什么?使用场景有哪些?

      分析

      因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。

      体验

      大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
      import { defineAsyncComponent } from 'vue' // defineAsyncComponent定义异步组件 const AsyncComp = defineAsyncComponent(() => {   // 加载函数返回Promise   return new Promise((resolve, reject) => {     // ...可以从服务器加载组件     resolve(/* loaded component */)   }) }) // 借助打包工具实现ES模块动态导入 const AsyncComp = defineAsyncComponent(() =>   import('./components/MyComponent.vue') )
      

    思路

    1. 异步组件作用
    2. 何时使用异步组件
    3. 使用细节
    4. 和路由懒加载的不同

      范例

    5. 在大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们

    6. 我们不仅可以在路由切换时懒加载组件,还可以在页面组件中继续使用异步组件,从而实现更细的分割粒度。
    7. 使用异步组件最简单的方式是直接给defineAsyncComponent指定一个loader函数,结合ES模块动态导入函数import可以快速实现。我们甚至可以指定loadingComponenterrorComponent选项从而给用户一个很好的加载反馈。另外Vue3中还可以结合Suspense组件使用异步组件
    8. 异步组件容易和路由懒加载混淆,实际上不是一个东西。异步组件不能被用于定义懒加载路由上,处理它的是vue框架,处理路由组件加载的是vue-router。但是可以在懒加载路由组件中使用异步组件。

      知其所以然

      defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
      github1s.com/vuejs/core/…

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

      分析

      这是一个综合应用题目,在项目中我们常常需要将App的异常上报,此时错误处理就很重要了。
      这里要区分错误的类型,针对性做收集。
      然后是将收集的的错误信息上报服务器。

      思路

    9. 首先区分错误类型

    10. 根据错误不同类型做相应收集
    11. 收集的错误是如何上报服务器的

      回答范例

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

    13. 我们需要根据不同错误类型做相应处理:接口异常是我们请求后端接口过程中发生的异常,可能是请求失败,也可能是请求获得了服务器响应,但是返回的是错误状态。以Axios为例,这类异常我们可以通过封装Axios,在拦截器中统一处理整个应用中请求的错误。代码逻辑异常是我们编写的前端代码中存在逻辑上的错误造成的异常,vue应用中最常见的方式是使用全局错误处理函数app.config.errorHandler收集错误。
    14. 收集到错误之后,需要统一处理这些异常:分析错误,获取需要错误信息和数据。这里应该有效区分错误类型,如果是请求错误,需要上报接口信息,参数,状态码等;对于前端逻辑异常,获取错误名称和详情即可。另外还可以收集应用名称、环境、版本、用户信息,所在页面等。这些信息可以通过vuex存储的全局状态和路由信息获取。
    15. 实践

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

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

    思路分析

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

    • vuex需求分析
    • 如何实现这些需求

      回答范例

    1. 官方说vuex是一个状态管理模式和库,并确保这些状态以可预期的方式变更。可见要实现一个vuex:
      • 要实现一个Store存储全局状态
      • 要提供修改状态所需APIcommit(type, payload), dispatch(type, payload)
    2. 实现Store时,可以定义Store类构造函数接收选项options,设置属性state对外暴露状态,提供commit和dispatch修改属性state。这里需要设置state为响应式对象,同时将Store定义为一个Vue插件。
    3. 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中Store的实现:
      github1s.com/vuejs/vuex/…

      39-vuex中actions和mutations有什么区别?

      题目分析

      mutationsactions是vuex带来的两个独特的概念。新手程序员容易混淆,所以面试官喜欢问。
      我们只需记住修改状态只能是mutationsactions只能通过提交mutation修改状态即可。

      体验

      看下面例子可知,Action 类似于 mutation,不同在于:

      • Action 提交的是 mutation,而不是直接变更状态。
      • Action 可以包含任意异步操作。
        const store = createStore({
        state: {
        count: 0
        },
        mutations: {
        increment (state) {
         state.count++
        }
        },
        actions: {
        increment (context) {
         context.commit('increment')
        }
        }
        })
        

        答题思路

    4. 给出两者概念说明区别

    5. 举例说明应用场景
    6. 使用细节不同
    7. 简单阐述实现上差异

      回答范例

    8. 官方文档说:更改 Vuex 的 store 中的状态的唯一方法是提交 mutationmutation 非常类似于事件:每个 mutation 都有一个字符串的类型 (type)和一个 回调函数 (handler) Action 类似于 mutation,不同在于:Action可以包含任意异步操作但它不能修改状态需要提交mutation才能变更状态

    9. 因此,开发时,包含异步操作或者复杂业务组合时使用action;需要直接修改状态则提交mutation。但由于dispatchcommit是两个API,容易引起混淆,实践中也会采用统一使用dispatch action的方式。
    10. 调用dispatchcommit两个API时几乎完全一样,但是定义两者时却不甚相同,mutation的回调函数接收参数是state对象action则是与Store实例具有相同方法和属性的上下文context对象,因此一般会解构它为{commit, dispatch, state},从而方便编码。另外dispatch会返回Promise实例便于处理内部异步结果
    11. 实现上commit(type)方法相当于调用options.mutationstypedispatch(type)方法相当于调用options.actionstype,这样就很容易理解两者使用上的不同了。

      知其所以然

      我们可以像下面这样简单实现commit和dispatch,从而辨别两者不同:

      class Store {
       constructor(options) {
           this.state = reactive(options.state)
           this.options = options
       }
       commit(type, payload) {
           // 传入上下文和参数1都是state对象
           this.options.mutations[type].call(this.state, this.state, payload)
       }
       dispatch(type, payload) {
           // 传入上下文和参数1都是store本身
           this.options.actions[type].call(this, this, payload)
       }
      }
      

      40-使用vue渲染大量数据时应该怎么优化?说下你的思路!

      分析

      企业级项目中渲染大量数据的情况比较常见,因此这是一道非常好的综合实践题目。

      思路

    12. 描述大数据量带来的问题

    13. 分不同情况做不同处理
    14. 总结一下

      回答

    15. 在大型企业级项目中经常需要渲染大量数据,此时很容易出现卡顿的情况。比如大数据量的表格、树。

    16. 处理时要根据情况做不通处理:
      • 可以采取分页的方式获取,避免渲染大量数据
      • vue-virtual-scroller虚拟滚动方案,只渲染视口范围内的数据
      • 如果不需要更新,可以使用v-once方式只渲染一次
      • 通过v-memo可以缓存结果,结合v-for使用,避免数据变化时不必要的VNode创建
      • 可以采用懒加载方式,在用户需要的时候再加载数据,比如tree组件子树的懒加载
    17. 总之,还是要看具体需求,首先从设计上避免大数据获取和渲染;实在需要这样做可以采用虚表的方式优化渲染;最后优化更新,如果不需要更新可以v-once处理,需要更新可以v-memo进一步优化大数据更新性能。其他可以采用的是交互方式优化,无线滚动、懒加载等方案。

      41-怎么监听vuex数据的变化?

      分析

      vuex数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。
      既然状态都是响应式的,那自然可以watch,另外vuex也提供了订阅的API:store.subscribe()。

      思路

    • 总述知道的方法
    • 分别阐述用法
    • 选择和场景

      回答范例

    • 我知道几种方法:

      • 可以通过watch选项或者watch方法监听状态
      • 可以使用vuex提供的API:store.subscribe()
    • watch选项方式,可以以字符串形式监听$store.state.xxsubscribe方式,可以调用store.subscribe(cb),回调函数接收mutation对象和state对象,这样可以进一步判断mutation.type是否是期待的那个,从而进一步做后续处理。

      • watch方式简单好用,且能获取变化前后值,首选;subscribe方法会被所有commit行为触发,因此还需要判断mutation.type,用起来略繁琐,一般用于vuex插件中。

        实践

        watch方式
        const app = createApp({
        watch: {
         '$store.state.counter'() {
           console.log('counter change!');
         }
        }
        })
        
        subscribe方式:
        store.subscribe((mutation, state) => {
        if (mutation.type === 'add') {
         console.log('counter change in subscribe()!');
        }
        })
        

        42-router-link和router-view是如何起作用的?

        分析

        vue-router中两个重要组件router-linkrouter-view,分别起到导航作用内容渲染作用,但是回答如何生效还真有一定难度哪!

        思路

    • 两者作用

    • 阐述使用方式
    • 原理说明

      回答范例

    • vue-router中两个重要组件router-linkrouter-view,分别起到路由导航作用组件内容渲染作用

    • 使用中router-link默认生成一个a标签,设置to属性定义跳转path。实际上也可以通过custom插槽自定义最终的展现形式router-view是要显示组件的占位组件,可以嵌套,对应路由配置的嵌套关系,配合name可以显示具名组件,起到更强的布局作用。
    • router-link组件内部根据custom属性判断如何渲染最终生成节点,内部提供导航方法navigate,用户点击之后实际调用的是该方法,此方法最终会修改响应式的路由变量,然后重新去routes匹配出数组结果, 则根据其所处深度deep在匹配数组结果中找到对应的路由并获取组件,最终将其渲染出来。

      知其所以然

    • RouterLink定义

    github1s.com/vuejs/route…

    • RouterView定义

    github1s.com/vuejs/route…

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

    分析

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

    体验

    声明式导航

    <router-link to="/about">Go to About</router-link>
    

    编程导航

    // literal string path
    router.push('/users/eduardo')
    
    // object with path
    router.push({ path: '/users/eduardo' })
    
    // named route with params to let the router build the url
    router.push({ name: 'user', params: { username: 'eduardo' } })
    

    思路

    • 两种方式
    • 分别阐述使用方式
    • 区别和选择
    • 原理说明

      回答范例

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

    • 声明式导航方式使用router-link组件,添加to属性导航;编程方式导航更加灵活,可传递调用router.push(),并传递path字符串或者RouteLocationRaw对象,指定path、name、params等信息
    • 如果页面中简单表示跳转链接,使用router-link最快捷,会渲染一个a标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航
    • 实际上内部两者调用的导航函数是一样的

      知其所以然

      github1s.com/vuejs/route…
      routerlink点击跳转,调用的是navigate方法
      Vue经典面试题(三) - 图4
      navigate内部依然调用的push

      44-Vue3.0 性能提升体现在哪些方面?

      分析

      vue3在设计时有几个目标:更小、更快、更友好,这些多数适合性能相关,因此可以围绕介绍。

      思路

    • 总述和性能相关的新特性

    • 逐个说细节
    • 能说点原理更佳

      回答范例

    • 我分别从代码编译打包三方面介绍vue3性能方面的提升

    • 代码层面性能优化主要体现在全新响应式API基于Proxy实现初始化时间内存占用均大幅改进
    • 编译层面做了更多编译优化处理,比如静态提升、动态标记事件缓存,区块等可以有效跳过大量diff过程;
    • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快

      体验

      通过playground体验编译优化:sfc.vuejs.org

      知其所以然

      为什么基于Proxy更快了:初始化时懒处理,用户访问才做拦截处理,初始化更快:
      github1s.com/vuejs/core/…
      轻量的依赖关系保存:利用WeakMap、Map和Set保存响应式数据和副作用之间的依赖关系
      github1s.com/vuejs/core/…

      45-Vue3.0里为什么要用 Proxy 替代 defineProperty ?

      分析

      Vue3中最重大的更新之一就是响应式模块reactivity的重写。主要的修改就是Proxy替换defineProperty实现响应式。
      此变化主要是从性能方面考量。

      思路

    • 属性拦截的几种方式

    • defineProperty的问题
    • Proxy的优点
    • 其他考量

      回答范例

    • JS中做属性拦截常见的方式有三:: definePropertygetter/settersProxies.

    • Vue2中使用defineProperty的原因是,2013年时只能用这种方式。由于该API存在一些局限性,比如对于数组的拦截有问题,为此vue需要专门为数组响应式做一套实现。另外不能拦截那些新增、删除属性;最后defineProperty方案在初始化时需要深度递归遍历待处理的对象才能对它进行完全拦截,明显增加了初始化的时间。
    • 以上两点在Proxy出现之后迎刃而解,不仅可以对数组实现拦截,还能对Map、Set实现拦截;另外Proxy的拦截也是懒处理行为,如果用户没有访问嵌套对象,那么也不会实施拦截,这就让初始化的速度和内存占用都改善了。

    当然Proxy是有兼容性问题的,IE完全不支持,所以如果需要IE兼容就不合适

    知其所以然

    Proxy属性拦截的原理:利用get、set、deleteProperty这三个trap实现拦截

    function reactive(obj) {
        return new Proxy(obj, {
            get(target, key) {},
            set(target, key, val) {},
            deleteProperty(target, key){}
        })
    }
    

    Object.defineProperty属性拦截原理:利用getset这两个trap实现拦截

    function defineReactive(obj, key, val) {
        Object.defineReactive(obj, key, {
            get(key) {},
            set(key, val) {}
        })
    }
    

    很容易看出两者的区别!

    46-History模式和Hash模式有何区别????

    分析

    vue-router有3个模式,其中两个更为常用,那便是history和hash。
    两者差别主要在显示形式和部署上。

    体验

    vue-router4.x中设置模式已经变化:

    const router = createRouter({
      history: createWebHashHistory(), // hash模式
      history: createWebHistory(),     // history模式
    })
    

    用起来一模一样

    <router-link to="/about">Go to About</router-link>
    

    区别只在url形式

    // hash
       浏览器里的形态:http://xx.com/#/about
    // history
       浏览器里的形态:http://xx.com/about
    

    思路

    • 区别
    • 详细阐述
    • 实现

      回答范例

    • Hash:即地址栏中有#符号。比如这个URL: http://www.abc.com/#/hello,** hash的值为#/hello。它的特点在于:hash虽然出现在URL中, 但不会被包括在HTTP请求中, 对后端完全没有影响, 因此改变hash值**不会重新加载页面。

    hisroty:

    • history 是路由的另一种模式,在相应的 router 配置时将 mode 设置为 history 即可。
    • history 模式是通过调用 window.history 对象上的一系列方法来实现页面的无刷新跳转。
    • 利用了 HTML5 History Interface 中新增的 pushState() 和 replaceState() 方法。
    • 这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会向后端发送请求。

    特点:

    • 新的URL可以是与当前URL同源的任意 URL,也可以与当前URL一样,但是这样会把重复的一次操作记录到栈中。
    • 通过参数stateObject可以添加任意类型的数据到记录中。
    • 可额外设置title属性供后续使用。
    • 通过pushState、replaceState实现无刷新跳转的功能。
    • 路径直接拼接在端口号后面,后面的路径也会随着http请求发给服务器,因此前端的URL必须和向发送请求后端URL保持一致,否则会报404错误。
    • 由于History API的缘故,低版本浏览器有兼容行问题。

    生产环境存在问题
    因为 history 模式的时候路径会随着 http 请求发送给服务器,项目打包部署时,需要后端配置 nginx,当应用通过 vue-router 跳转到某个页面后,因为此时是前端路由控制页面跳转,虽然url改变,但是页面只是内容改变,并没有重新请求,所以这套流程没有任何问题。但是,如果在当前的页面刷新一下,此时会重新发起请求,如果 nginx 没有匹配到当前url,就会出现404的页面。

    知其所以然

    hash是一种特殊的history实现:
    github1s.com/vuejs/route…

    48-页面刷新后vuex的state数据丢失怎么解决?

    分析

    这是一道应用题目,很容易想到使用localStorage或数据库存储并还原状态。
    但是如何优雅编写代码还是能体现认知水平。


    体验

    可以从localStorage中获取作为状态初始值:

    const store = createStore({
      state () {
        return {
          count: localStorage.getItem('count')
        }
      }
    })
    

    业务代码中,提交修改状态同时保存最新值:虽说实现了,但是每次还要手动刷新localStorage不太优雅

    const store = createStore({
      state () {
        return {
          count: localStorage.getItem('count')
        }
      }
    })
    

    思路

    • 问题描述
    • 解决方法
    • 谈个人理解
    • 三方库原理探讨

      回答范例

    • vuex只是在内存保存状态刷新之后就会丢失,如果要持久化就要存起来。

    • localStorage就很合适,提交mutation的时候同时存入localStoragestore中把值取出作为state的初始值即可。
    • 这里有两个问题,不是所有状态都需要持久化;如果需要保存的状态很多,编写的代码就不够优雅,每个提交的地方都要单独做保存处理。这里就可以利用vuex提供的subscribe方法做一个统一的处理。甚至可以封装一个vuex插件以便复用。
    • 类似的插件有vuex-persistvuex-persistedstate,内部的实现就是通过订阅mutation变化做统一处理,通过插件的选项控制哪些需要持久化.

      知其所以然

      可以看一下vuex-persist内部确实是利用subscribe实现的
      github.com/championswi…

      49-你觉得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时变成了全局的,能同时触发多个子模块中同名mutation
      store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
      

      思路

    • 先夸再贬

    • 使用感受
    • 解决方案

      回答范例

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

    • 比如:访问state时要带上模块key,内嵌模块的话会很长,不得不配合mapState使用,加不加namespaced区别也很大,gettersmutationsactions这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。
    • 之前Vue2项目中用过vuex-module-decorators的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia出现之后使用体验好了很多,Vue3 + pinia会是更好的组合。

      知其所以然

      下面我们来看看vuex中store.state.x.y这种嵌套的路径是怎么搞出来的。
      首先是子模块安装过程:父模块状态parentState上面设置了子模块名称moduleName,值为当前模块state对象。放在上面的例子中相当于:store.state[‘x’] = moduleX.state。此过程是递归的,那么store.state.x.y安装时就是:store.state[‘x’][‘y’] = moduleY.state。

      if (!isRoot && !hot) {
        // 获取父模块state
        const parentState = getNestedState(rootState, path.slice(0, -1))
        // 获取子模块名称
        const moduleName = path[path.length - 1]
        store._withCommit(() => {
            // 把子模块state设置到父模块上
            parentState[moduleName] = module.state
        })
      }
      

      源码地址:github1s.com/vuejs/vuex/…

      50-Composition API 与 Options API 有什么不同

      分析

      Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要。
      Vue经典面试题(三) - 图5
      vuejs.org/guide/extra…

      体验

      Composition API能更好的组织代码,下面这个代码用options api实现
      Vue经典面试题(三) - 图6
      如果用composition api可以提取为useCount(),用于组合、复用
      Vue经典面试题(三) - 图7

      思路

    • 总述不同点

    • composition api动机
    • 两者选择

      回答范例

    • Composition API是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件。

    • Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options API中mixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对ts支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixins和provide/inject上。
    • Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益。

      可能的追问

    • Composition API能否和Options API一起使用?

      51-vue-router中如何保护路由?

      分析

      路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。

      体验

      全局守卫:
      const router = createRouter({ … }) router.beforeEach((to, from) => { // … // 返回 false 以取消导航 return false }) 复制代码
      路由独享守卫:
      const routes = [ { path: ‘/users/:id’, component: UserDetails, beforeEnter: (to, from) => { // reject the navigation return false }, }, ] 复制代码
      组件内的守卫:
      const UserDetails = { template: ..., beforeRouteEnter(to, from) { // 在渲染该组件的对应路由被验证前调用 }, beforeRouteUpdate(to, from) { // 在当前路由改变,但是该组件被复用时调用 }, beforeRouteLeave(to, from) { // 在导航离开渲染该组件的对应路由时调用 }, } 复制代码

      思路

    • 路由守卫的概念

    • 路由守卫的使用
    • 路由守卫的原理
    • vue-router中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。
    • 路由守卫有三个级别:全局,路由独享,组件级。影响范围由大到小,例如全局的router.beforeEach(),可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。
    • 用户的任何导航行为都会走navigate方法,内部有个guards队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。

      知其所以然

      runGuardQueue(guards)链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch流程取消原本导航。
      Vue经典面试题(三) - 图8

    作者:杨村长
    链接:https://juejin.cn/post/7115055320913117220
    来源:稀土掘金
    著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。