vue路由传参数 使用query方法传入的参数使用this.$route.query接受 使用params方...">26 Vue路由相关 | 前端面试题集锦 vue路由传参数 使用query方法传入的参数使用this.$route.query接受 使用params方...">

26 Vue路由相关

Vue-router基本使用

mode

  • hash
  • history

跳转

  • 编程式(js跳转)this.$router.push('/')
  • 声明式(标签跳转) <router-link to=""></router-link>

vue路由传参数

  • 使用query方法传入的参数使用this.$route.query接受
  • 使用params方式传入的参数使用this.$route.params接受

占位

    <router-view></router-view>

vue-router 动态路由是什么

我们经常需要把某种模式匹配到的所有路由,全都映射到同个组件。例如,我们有一个 User 组件,对于所有 ID 各不相同的用户,都要使用这个组件来渲染。那么,我们可以在 vue-router 的路由路径中使用“动态路径参数”(dynamic segment) 来达到这个效果

    const User = {
      template: "<div>User</div>",
    };
    
    const router = new VueRouter({
      routes: [
        // 动态路径参数 以冒号开头
        { path: "/user/:id", component: User },
      ],
    });

问题: vue-router 组件复用导致路由参数失效怎么办?

解决方法:

  1. 通过 watch 监听路由参数再发请求
    watch: { //通过watch来监听路由变化
     "$route": function(){
      this.getData(this.$route.params.xxx);
     }
    }
  1. :key 来阻止“复用”
    <router-view :key="$route.fullPath" />

回答范例

  1. 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由
  2. 例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router中,我们可以在路径中使用一个动态字段来实现,例如:{ path: '/users/:id', component: User },其中:id就是路径参数
  3. 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。
  4. 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query$route.hash

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

分析

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

回答范例

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

声明式导航

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

编程式导航

    // literal string path
    router.push('/users/1')

    // object with path
    router.push({ path: '/users/1' })

    // named route with params to let the router build the url
    router.push({ name: 'user', params: { username: 'test' } })

回答范例

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

Vue-router 路由模式有几种

vue-router3 种路由模式:hashhistoryabstract,对应的源码如下所示

    switch (mode) {
        case 'history':
        this.history = new HTML5History(this, options.base)
    	break
        case 'hash':
        this.history = new HashHistory(this, options.base, this.fallback)
    	  break
        case 'abstract':
        this.history = new AbstractHistory(this, options.base)
    	  break
      default:
        if (process.env.NODE_ENV !== 'production') {
          assert(false, `invalid mode: ${mode}`)
        }
    }

其中,3 种路由模式的说明如下:

  • hash: 使用 URL hash 值来作路由,支持所有浏览器。 缺点:SEO不友好
  • history : 依赖 HTML5 History API 和服务器配置。后端将所有前端路由都渲染同一页面,但不能是404页面。缺点:IE8不支持
  • abstract : 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。缺点:没有URL,只对单机有效。前面两种都是把路径存到URL上面。memory模式不放在URL里面,前端一般放在localstorage里面。

注意:v4之前叫做abstract history,之后叫做MemoryHistory

Vue路由hash模式和history模式

1.hash模式

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL# 后面的内容。比如下面这个网站,它的 location.hash 的值为 '#search'

    https://interview.poetries.top#search

hash 路由模式的实现主要是基于下面几个特性

  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URLhash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URLhash 值;
  • 我们可以使用 hashchange 事件来监听 hash 值的变化,从而对页面进行跳转(渲染)
    window.addEventListener("hashchange", funcRef, false);

每一次改变 hashwindow.location.hash),都会在浏览器的访问历史中增加一个记录利用 hash 的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了

特点 :兼容性好但是不美观

2.history模式

history采用HTML5的新特性;且提供了两个新方法: pushState()replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更

    window.history.pushState(null, null, path);
    window.history.replaceState(null, null, path);

这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL 改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。

history 路由模式的实现主要基于存在下面几个特性:

  • pushStaterepalceState 两个 API 来操作实现 URL 的变化 ;
  • 我们可以使用 popstate 事件来监听 url 的变化,从而对页面进行跳转(渲染);
  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。

特点 :虽然美观,但是刷新会出现 404 需要后端进行配置

了解history有哪些方法吗?说下它们的区别

history 这个对象在html5的时候新加入两个api history.pushState()history.repalceState() 这两个API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。

从参数上来说:

    window.history.pushState(state,title,url)
    //state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
    //title:标题,基本没用,一般传null
    //url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
    //如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
    //执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
    
    window.history.replaceState(state,title,url)
    //与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录

另外还有:

  • window.history.back() 后退
  • window.history.forward()前进
  • window.history.go(1) 前进或者后退几步

从触发事件的监听上来说:

  • pushState()replaceState()不能被popstate事件所监听
  • 而后面三者可以,且用户点击浏览器前进后退键时也可以

如何监听 pushState 和 replaceState 的变化呢?

利用自定义事件new Event()创建这两个事件,并全局监听:

    <body>
      <button onclick="goPage2()">去page2</button>
      <div>Page1</div>
      <script>
        let count = 0;
        function goPage2 () {
          history.pushState({ count: count++ }, `bb${count}`,'page1.html')
          console.log(history)
        }
        // 这个不能监听到 pushState
        // window.addEventListener('popstate', function (event) {
        //   console.log(event)
        // })
        function createHistoryEvent (type) {
          var fn = history[type]
          return function () {
            // 这里的 arguments 就是调用 pushState 时的三个参数集合
            var res = fn.apply(this, arguments)
            let e = new Event(type)
            e.arguments = arguments
            window.dispatchEvent(e)
            return res
          }
        }
        history.pushState = createHistoryEvent('pushState')
        history.replaceState = createHistoryEvent('replaceState')
        window.addEventListener('pushState', function (event) {
          // { type: 'pushState', arguments: [...], target: Window, ... }
          console.log(event)
        })
        window.addEventListener('replaceState', function (event) {
          console.log(event)
        })
      </script>
    </body>

Vue路由的钩子函数

首页可以控制导航跳转,beforeEachafterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。

  • beforeEach主要有3个参数tofromnext
  • toroute即将进入的目标路由对象。
  • fromroute当前导航正要离开的路由。
  • nextfunction一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转

$route$router的区别

  • $route是“路由信息对象”,包括pathparamshashqueryfullPathmatchedname等路由信息参数。
  • $router是“路由实例”对象包括了路由的跳转方法,钩子函数等

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 有哪几种导航守卫

  • 全局守卫
  • 路由独享守卫
  • 路由组件内的守卫

全局守卫

vue-router全局有三个守卫

  • router.beforeEach 全局前置守卫 进入路由之前
  • router.beforeResolve 全局解析守卫(2.5.0+) 在beforeRouteEnter调用之后调用
  • router.afterEach 全局后置钩子 进入路由之后
    // 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 全局后置钩子');
    });

路由独享守卫

如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫

    const router = new VueRouter({
      routes: [
        {
          path: '/foo',
          component: Foo,
          beforeEnter: (to, from, next) => { 
            // 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
            // ...
          }
        }
      ]
    })

路由组件内的守卫

  • beforeRouteEnter 进入路由前, 在路由独享守卫后调用 不能 获取组件实例 this,组件实例还没被创建
  • beforeRouteUpdate (2.2) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例 this
  • beforeRouteLeave 离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例 this

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销毁,防止离开之后,定时器还在调用。
      }
    }

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流程取消原本导航

    // 源码
    runGuardQueue(guards)
      .then(() => {
        // check global guards beforeEach
        guards = []
        for (const guard of beforeGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)
    
        return runGuardQueue(guards)
      })
      .then(() => {
        // check in components beforeRouteUpdate
        guards = extractComponentsGuards(
          updatingRecords,
          'beforeRouteUpdate',
          to,
          from
        )
    
        for (const record of updatingRecords) {
          record.updateGuards.forEach(guard => {
            guards.push(guardToPromiseFn(guard, to, from))
          })
        }
        guards.push(canceledNavigationCheck)
    
        // run the queue of per route beforeEnter guards
        return runGuardQueue(guards)
      })
      .then(() => {
        // check the route beforeEnter
        guards = []
        for (const record of to.matched) {
          // do not trigger beforeEnter on reused views
          if (record.beforeEnter && !from.matched.includes(record)) {
            if (isArray(record.beforeEnter)) {
              for (const beforeEnter of record.beforeEnter)
                guards.push(guardToPromiseFn(beforeEnter, to, from))
            } else {
              guards.push(guardToPromiseFn(record.beforeEnter, to, from))
            }
          }
        }
        guards.push(canceledNavigationCheck)
    
        // run the queue of per route beforeEnter guards
        return runGuardQueue(guards)
      })
      .then(() => {
        // NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
    
        // clear existing enterCallbacks, these are added by extractComponentsGuards
        to.matched.forEach(record => (record.enterCallbacks = {}))
    
        // check in-component beforeRouteEnter
        guards = extractComponentsGuards(
          enteringRecords,
          'beforeRouteEnter',
          to,
          from
        )
        guards.push(canceledNavigationCheck)
    
        // run the queue of per route beforeEnter guards
        return runGuardQueue(guards)
      })
      .then(() => {
        // check global guards beforeResolve
        guards = []
        for (const guard of beforeResolveGuards.list()) {
          guards.push(guardToPromiseFn(guard, to, from))
        }
        guards.push(canceledNavigationCheck)
    
        return runGuardQueue(guards)
      })
      // catch any navigation canceled
      .catch(err =>
        isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
          ? err
          : Promise.reject(err)
      )

源码位置 (opens new window)

怎么实现路由懒加载呢

这是一道应用题。当打包应用时,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问时才加载对应组件,这样就会更加高效

    // 将
    // import UserDetails from './views/UserDetails'
    // 替换为
    const UserDetails = () => import('./views/UserDetails')

    const router = createRouter({
      // ...
      routes: [{ path: '/users/:id', component: UserDetails }],
    })

回答范例

  1. 当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段
  2. 一般来说,对所有的路由都使用动态导入 是个好主意
  3. component选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:{ path: '/users/:id', component: () => import('./views/UserDetails') }
  4. 结合注释 () => import(/* webpackChunkName: "group-user" */ './UserDetails.vue') 可以做webpack代码分块

Vue要做权限管理该怎么做?控制到按钮级别的权限怎么做?

分析

  • 综合实践题目,实际开发中经常需要面临权限管理的需求,考查实际应用能力。
  • 权限管理一般需求是两个:页面权限和按钮权限,从这两个方面论述即可。

思路

  • 权限管理需求分析:页面和按钮权限
  • 权限管理的实现方案:分后端方案和前端方案阐述
  • 说说各自的优缺点

回答范例

  1. 权限管理一般需求是页面权限和按钮权限的管理
  2. 具体实现的时候分后端和前端两种方案:
  • 前端方案 会把所有路由信息在前端配置,通过路由守卫要求用户登录,用户登录后根据角色过滤出路由表。比如我会配置一个asyncRoutes数组,需要认证的页面在其路由的meta中添加一个roles字段,等获取用户角色之后取两者的交集,若结果不为空则说明可以访问。此过滤过程结束,剩下的路由就是该用户能访问的页面,最后通过router.addRoutes(accessRoutes)方式动态添加路由即可
  • 后端方案 会把所有页面路由信息存在数据库中,用户登录的时候根据其角色查询得到其能访问的所有页面路由信息返回给前端,前端再通过addRoutes动态添加路由信息
  • 按钮权限的控制通常会实现一个指令,例如v-permission,将按钮要求角色通过值传给v-permission指令,在指令的moutned钩子中可以判断当前用户角色和按钮是否存在交集,有则保留按钮,无则移除按钮
  1. 纯前端方案的优点是实现简单,不需要额外权限管理页面,但是维护起来问题比较大,有新的页面和角色需求就要修改前端代码重新打包部署;服务端方案就不存在这个问题,通过专门的角色和权限管理页面,配置页面和按钮权限信息到数据库,应用每次登陆时获取的都是最新的路由信息,可谓一劳永逸!

可能的追问

  1. 类似Tabs这类组件能不能使用v-permission指令实现按钮权限控制?
    <el-tabs> 
      <el-tab-pane label="⽤户管理" name="first">⽤户管理</el-tab-pane> 
    	<el-tab-pane label="⻆⾊管理" name="third">⻆⾊管理</el-tab-pane>
    </el-tabs>
  1. 服务端返回的路由信息如何添加到路由器中?
    // 前端组件名和组件映射表
    const map = {
      //xx: require('@/views/xx.vue').default // 同步的⽅式
      xx: () => import('@/views/xx.vue') // 异步的⽅式
    }
    // 服务端返回的asyncRoutes
    const asyncRoutes = [
      { path: '/xx', component: 'xx',... }
    ]
    // 遍历asyncRoutes,将component替换为map[component]
    function mapComponent(asyncRoutes) {
      asyncRoutes.forEach(route => {
        route.component = map[route.component];
        if(route.children) {
          route.children.map(child => mapComponent(child))
        }
    	})
    }
    mapComponent(asyncRoutes)

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

思路分析:

首先思考vue路由要解决的问题:用户点击跳转链接内容切换,页面不刷新。

  • 借助hash或者history api实现url跳转页面不刷新
  • 同时监听hashchange事件或者popstate事件处理跳转
  • 根据hash值或者state值从routes表中匹配对应component并渲染

回答范例:

一个SPA应用的路由需要解决的问题是页面跳转内容改变同时不刷新 ,同时路由还需要以插件形式存在,所以:

  1. 首先我会定义一个createRouter函数,返回路由器实例,实例内部做几件事
  • 保存用户传入的配置项
  • 监听hash或者popstate事件
  • 回调里根据path匹配对应路由
  1. router定义成一个Vue插件,即实现install方法,内部做两件事
  • 实现两个全局组件:router-linkrouter-view,分别实现页面跳转和内容显示
  • 定义两个全局变量:$route$router,组件内可以访问当前路由和路由器实例
Last Updated:
Contributors: leeguooooo