开局面试官就让我设计一个路由


theme: fancy
highlight: arduino-light


本文正在参加「金石计划 . 瓜分6万现金大奖」

前言

  • 常网IT戳我呀!
  • 常网IT源码上线啦!
  • 如果是海迷,RED红发剧场版有需要可关注我主页公众号回“海贼王red”领取。
  • 本篇录入吊打面试官专栏,希望能祝君拿下Offer一臂之力,各位看官感兴趣可移步🚶。
  • 这段时间面了很多家公司,被问到的题我感觉不重复不止100道,将会挑选觉得常见且有意义的题目进行分析及回答。
  • 有人说面试造火箭,进去拧螺丝;其实个人觉得问的问题是项目中涉及的点 || 热门的技术栈都是很好的面试体验,不要是旁门左道冷门的知识,实际上并不会用到的。
  • 请问如果让你设计Vue中的路由,你有哪些思路?

他明白,他明白,他~
之前秋招信誓旦旦说dog都不去,现在:dog不去, 我去。

若面试着急用,最后面有回答示例~

一、问题剖析

设计Vue中的路由,你有哪些思路?

每一个技术的诞生,都是要出来解决问题的。

那么路由解决什么问题?

用户点击跳转链接,页面内容切换,页面不刷新。

我们要想设计他,首先得知道他是怎么用的?

知己知彼,百战不殆。

  • 我们会引入vue-router,然后Vue.use(VueRouter) — 哦,他是一个插件🏎。

  • 将我们的路由数组传入VueRouter实例中,然后导出暴露出来 。

  • 然后将VueRouter实例挂载到Vue实例中。

进行分析

知道如何用,接下来我们进行分析一下:

  • 根据hash值或者state值从routes表中匹配对应component并渲染之。

  • 借助hash或者history api实现url跳转页面不刷新。

  • 监听hashchange事件或者popstate事件处理跳转。

那我们还可以说说router它内部的实现原理。

因为这道题是一道开发性的题目,我们可以聊聊路由有关的点,比如:导航守卫、动态路由…

二、回

分析题目之后,我们进行语言组织回答。

我们可以定义一个createRouter函数,返回路由实例:

  • 保存我们传入的配置项。

  • 监听hash || popstate事件。

  • 回调里根据path匹配对应路由。

然后要把他定义成插件,即实现install方法。

  • 实现两个全局组件:router-link页面跳转、router-view内容显示。

  • 定义两个全局变量:router,组件内可以访问当前路由和路由器实例route。

2.1 createRouter函数

第一步:我们来大概写一下createRouter函数。

  • 保存了我们传入的配置项options
  • init函数,主要做一些初始化工作,比如:初始化时执行一次根据路由渲染组件。
  • 我们还实现了install方法,把路由做成插件。
// src/router.js

class VueRouter {
    constructor(options) {}
    init(app) {}
}

VueRouter.install = (Vue) => {}

export default VueRouter

2.2 install:关于把路由定义成插件

我们要把路由做成插件,在Vue中,其实就是实现install方法。

我们会引入vue-router,然后Vue.use(VueRouter)

在Vue.use(XXX)时,会执行XXX的install方法,并将Vue当做参数传入install方法。

// src/router.js

let _Vue
VueRouter.install = (Vue) => {
    _Vue = Vue
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件
                // this 是 根组件本身
                this._routerRoot = this

                // this.$options.router就是挂在根组件上的VueRouter实例
                this.$router = this.$options.router

                // 执行VueRouter实例上的init方法,初始化
                this.$router.init(this)
            } else {
                // 非根组件,也要把父组件的_routerRoot保存到自身身上
                this._routerRoot = this.$parent && this.$parent._routerRoot
                // 子组件也要挂上$router
                this.$router = this._routerRoot.$router
            }
        }
    })
}

2.3 路由模式

我们上面回答到,会监听hash || popstate这两个事件。

首先得先了解一下路由的模式。

路由有三种模式

  • hash模式,最常用的模式
  • history模式,需要后端配合的模式
  • abstract模式,非浏览器环境的模式

怎么去设置路由模式呢?

通过options的mode字段传进去(上面的createRouter函数初始化传进的options)

export default new VueRouter({
    mode: 'hash' // 设置模式
    routes
})

mode字段如果不传的话,默认路由模式是hash模式。

// src/router.js

import HashHistory from "./hashHistory"

class VueRouter {
    constructor(options) {
        
        this.options = options
        
        // 如果不传mode,默认为hash
        this.mode = options.mode || 'hash'

        // 判断模式是哪种
        switch (this.mode) {
            case 'hash':
                this.history = new HashHistory(this)
                break
            case 'history':
                this.history = new HTML5History(this, options.base)
                break
            case 'abstract':

        }
    }
    init(app) { }
}

2.4 HashHistory

hash模式原理:监听浏览器url中的hash值变化,然后切换对应的组件。即监听hashchange事件。

class HashHistory {
    constructor(router) {

        // 将传进来的VueRouter实例保存
        this.router = router

        // 如果url没有 # ,自动填充 /#/ 
        ensureSlash()
        
        // 监听hash变化
        this.setupHashLister()
    }
    // 监听hash的变化
    setupHashLister() {
        window.addEventListener('hashchange', () => {
            // 传入当前url的hash,并触发跳转
            this.transitionTo(window.location.hash.slice(1))
        })
    }

    // 跳转路由时触发的函数
    transitionTo(location) {
        console.log(location) // 每次hash变化都会触发,可以自己在浏览器修改试试
        // 比如 http://localhost:8080/#/home/child1 最新hash就是 /home/child1
    }
}

// 如果浏览器url上没有#,则自动补充/#/
function ensureSlash() {
    if (window.location.hash) {
        return
    }
    window.location.hash = '/'
}

export default HashHistory

2.5 实现全局组件:router-view内容显示

我们要根据hash变化来渲染不同的组件页面。

我们会接收传入的路由数组,然后根据hash值去获取对应的组件渲染它。

我们要注意一点,就是手动刷新的时候,是不会触发hashchange事件,所以我们要做路由初始化渲染组件。(手动调一次)

还记得我们在上面的createRouter函数写了一个init(app) {}的方法吗?

// src/router.js

class VueRouter {

    // ...原先代码
    
    init(app) {
        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ...原先代码
}

2.6 $route的由来

思考一个问题?

当你hash值改变,确实能拿到最新的组件数组,但组件不会进行渲染。

因为Vue的组件重新渲染只能通过某个数据的响应式变化来触发。

所以hash值改变要让他变成一个响应式,即调用Object.defineProperty

hash变化 –> 定义一个值来保存这个组件数组,这个值在Vue中就是$route

hash变化 –> $route值变化 –> 组件渲染。

所以知道为什么每个组件内都有$route对象了吧。

// src/router.js

class VueRouter {

    // ...原先代码
    
    init(app) {
        // 把回调传进去,确保每次current更改都能顺便更改_route触发响应式
        this.history.listen((route) => app._route = route)
        
        // 初始化时执行一次,保证刷新能渲染
        this.history.transitionTo(window.location.hash.slice(1))
    }

    // ...原先代码
}

VueRouter.install = (Vue) => {
    _Vue = Vue
    // 使用Vue.mixin混入每一个组件
    Vue.mixin({
        // 在每一个组件的beforeCreate生命周期去执行
        beforeCreate() {
            if (this.$options.router) { // 如果是根组件

                // ...原先代码
                
                // 相当于存在_routerRoot上,并且调用Vue的defineReactive方法进行响应式处理
                Vue.util.defineReactive(this, '_route', this.$router.history.current)
            } else {
                // ...原先代码
            }


        }
    })
    
    // 访问$route相当于访问_route
    Object.defineProperty(Vue.prototype, '$route', {
        get() {
            return this._routerRoot._route
        }
    })
}

还是借助Vue.mixin给每一个组件的beforeCreate生命周期去绑定其defineReactive响应式。

说一说:route和router的区别?
router是通过“Vue.use(VueRouter)”和VueRouter构造函数得到一个实例对象,包括了路由的跳转方法,钩子函数等,它是一个全局的对象。
而route是一个跳转的路由对象,包括path,params,hash,query,name等路由信息参数,每一个路由都会有一个route对象,是一个局部的对象。

2.7 实现全局组件:router-link页面跳转

router-link其实就是个a标签。

const myLink = {
    props: {
        to: {
            type: String,
            required: true,
        },
    },
    // 渲染
    render(h) {

        // 使用render的h函数渲染
        return h(
            // 标签名
            'a',
            // 标签属性
            {
                domProps: {
                    href: '#' + this.to,
                },
            },
            // 插槽内容
            [this.$slots.default]
        )
    },
}

export default myLink

其实,上面回答之后,这个问题就告一段落了,但也有面试官抓着问题不放,继续追问下去。

三、面试题:Hash 和 History 模式有何区别?

我相信聊到路由,面试官必问的一题。

之前在面视源的时候就被问到,当时回答得不好。。

前端路由本质上其实是监听url变化。

一般主流的模式有:

Hash 模式和 History 模式,无需刷新页面就能重新加载相应的页面。(JSP永远无法get到的点)

hash

  • Hash url 的格式为zheng.cn/#/,当#后的哈希值发生变化时,通过 hashchange 事件监听,然后页面跳转。
  • 通过 location.hash 跳转路由。

  • 通过 hashchange event 监听路由变化。

history API

  • 通过history.pushState和history.replaceState改变 url

  • 通过 history.pushState() 跳转路由。

  • 通过 popstate event 监听路由变化,但无法监听到 history.pushState() 时的路由变化。

回:两种模式的区别

  • hash 只能改变#后的值,而 history 模式可以随意设置同源 url;

  • hash 只能添加字符串类的数据,而 history 可以通过 API 添加多种类型的数据;

  • hash 的历史记录只显示之前的www.a.com而不会显示 hash 值,而 history 的每条记录都会进入到历史记录;

  • hash 无需后端配置且兼容性好,而 history 需要配置index.html用于匹配不到资源的情况。(打包后切断自测要使用hash,如果使用history会出现空白页404,可以通过配置nginx重定向跳转)

  • 跳转请求:

    • history:http://localhost:8080/id ==> 发送请求
    • hash:http://localhost:8080/#/id ==>不会发送请求
  • hash值变化不会让浏览器向服务器请求;而history则会。

当然啦,路由还有第三种模式叫:abstract,我们很少用,它支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式。

vue-router使用history模式,部署时为什么会出现404?

因为在history模式下,只是动态的通过js操作window.history来改变浏览器地址栏里的路径,并没有发起http请求。

当直接在浏览器里输入这个地址的时候,就一定要对服务器发起http请求,但是这个目标在服务器上又不存在,所以会返回404。

通俗易懂的说:history是通过js显示组件,但直接输入url,不是通过js就走http,但服务器并没有这个资源,所以404。

所以要在Ngnix中将所有请求都转发到index.html上就可以了。

location / {
    try_files  $uri $uri/ @router index index.html;
}
location @router {
    rewrite ^.*$ /index.html last;
}

四、路由导航守卫有哪些?

🙋面试官心想:前面都难不倒你,那你说一下路由导航守卫有哪些?

我就不信,你能把三个都说出来。

2.jpg

🙋🏻‍♂️路由导航守卫一共有三种,分别是全局、路由独享、组件内。

全局

  • beforeEach:用的最多,是否登录,有则正常next,否push到login页面(是在路由跳转之前调用)

  • beforeResolve:解析守卫

  • afterEach:在路由跳转之后调用

路由独享

顾名思义:每个路由独自享受钩子函数。

是指在单个路由配置的时候也可以设置的钩子函数,路由独享守卫其实就是路由里面的导航守卫,只有跳转到对应的路由里面才会触发

和全局一样的钩子函数:beforeEnter、beforeResolve、afterEach。

可以在路由配置上直接定义beforeEnter守卫。

const router = new VueRouter({
  routes:[
    {
      path: '/ze',
      component: Home,
      beforeEnter:(to, from, next) =>{
        // ...
      },
      beforeResolve:(to, from, next) =>{
        // ...
      },
      afterEach:(to, from, next) =>{
        // ...
      },
    }
  ]
})

组件内

我们可以在组件内写路由的钩子函数。

  • beforeRouteEnter(进入页面前 有三个参数)
  • beforeRouteUpdate(更新时)
  • beforeRouteLeave(离开页面后)

4.png

钩子函数内有三个参数

  • to:从哪里来
  • form:到哪里去
  • next:进入下一个。(必须调用,不然路由跳转不过去)

我们使用到路由守卫的场景:一般是判断是否登录,如果登录就next否则就跳转到登录页面。

怎么在组件中监听路由参数的变化?

有两种方法可以监听路由参数的变化,但是只能用在包含<router-view />的组件内。

// 第一种
watch: {
    '$route'(to, from) {
        //这里监听
    },
},

// 第二种:就是我们说到的路由守卫
beforeRouteUpdate (to, from, next) {
    //这里监听
},

面试官说:不错不错,我以为你会说在router.js那里定义全局守卫而已,没想到还知其两种。

五、聊聊动态路由?

🙋面试官心有不甘,从来没一个候选人从我最拿捏的路由通关过。

🙋🏻‍♂️soga。

问题剖析

其实这个问题在项目实战中很常见,面试中我们怎么去回答比较好✍,我个人觉得可以从以下四个方面去和面试官聊这个题。

  • 什么是动态路由?

  • 什么时候使用动态路由,怎么定义动态路由?

  • 参数如何获取?

  • 一些细节、注意事项。

  • 很多时候,我们需要将给定匹配模式的路由映射到同一个组件,这种情况就需要定义动态路由。

  • 例如,我们可能有一个 User 组件,它应该对所有用户进行渲染,但用户 ID 不同。在 Vue Router 中,我们可以在路径中使用一个动态字段id来实现,例如:{ path: ‘/users/:id’, component: User },其中:id就是路径参数。

  • 路径参数 用冒号 : 表示。当一个路由被匹配时,它的 params 的值将在每个组件中以 this.$route.params 的形式暴露出来。

  • 参数还可以有多个,例如/users/:username/posts/:postId;除了 $route.params 之外,$route 对象还公开了其他有用的信息,如 $route.query、$route.hash 等。

可能有些初学者还是不太能理解。我再举个例子。

场景:详情页(文章、商品)

6.png

router.js配置

{
  path: '/list',
  name: 'List',
  component:() => import("List.vue")
    children:[
      {
        path: '/list',
        name: 'List',
        component:() => import("Details.vue")
      }
    ],
}

List.vue组件

<div>
  <router-link to = "/list/123">123</router-link>
  <router-link to = "/list/456">456</router-link>
  <router-link to = "/list/789">789</router-link>

  <router-view></router-view>
</div>
怎么动态加载路由?

使用Router的实例方法addRoutes来实现动态加载路由,一般用来实现菜单权限。

vm.$router.options.routes.push(...routes); 
vm.$router.addRoutes(routes);

面试官:有点底子的啊。

六、给你出道实战题

面试官说:不把你难倒,我就对不起我刁难官的称号。

4.jpg

需求:我们在H5想通过手机左滑、右滑和机身自带的返回键,让打开的弹出框隐藏。

第一想法就是监听左滑事件,让弹出框隐藏。

在安卓/IOS 端可以通过监听物理返回事件去关闭弹窗,但是在H5,不好意思,是没有这一事件。

8.jpg

阿,你这,和路由有啥关系。

面试官心里想:你小子,终于不会了是吧。就想要你这个表情。

🙋回去等通知吧。

面试官摸了摸胡子,今日KPI已达标。

🙋🏻‍♂️让我输个明白吧,告诉我吧。

🙋想知道?那就告诉你吧。

其实这种返回在H5实际上只是返回上一页的功能,也就是回退到上个历史记录。

因此我们可以在弹窗打开时,添加一个不会改变当前页面的历史记录,如 ?popyp=true(或 #popup),在触发物理返回键后,浏览器会后退一个历史记录并且自动清除?popyp=true(或 #popup),而页面不会发生跳转和刷新,最后通过监听url变化,识别出url中 ?popyp=true 被清除则关闭弹窗。

这也是路由的一个小应用了,学以致用。

原来如此。学废了。

那我回家做饭了。

刚刚和你开玩笑的,你前面回答得这么好,在我心里已经是过了的,明天来报道。

哦,那明天见,我回家做饭了😁😁😁。

9.png

祝君在每一次面试中,都能收割到Offer。

后记

我们再来总结一下回答:

🙋🏻‍♂️我个人主要从三个方面回答

  • 我们可以定义一个createRouter函数,返回路由实例:

    • 保存我们传入的配置项。
    • 监听hash || popstate事件。
    • 回调里根据path匹配对应路由。
  • 然后要把他定义成插件,即实现install方法。

    • 实现两个全局组件:router-link页面跳转、router-view内容显示。

      • 实现router-view:内容要想渲染,就会涉及到路由模式,因为我们根据路由变化来渲染不同的组件页面。
      • hash模式,最常用的模式,监听popstate事件
      • history模式,需要后端配合的模式,监听hashchange事件
      • router-link其实就是个a标签。
    • 定义两个全局变量:router,组件内可以访问当前路由和路由器实例route。

  • 如果可以的话,再聊聊路由模式的不同点,基本就大功告成。

其实按照这个思路回答不算难,贵在自己理解,才能抵挡住面试官的猛攻。(可反复多看几次)

回答面试题的时候,尽量形成一个结构化的回答,大体,再细化,由浅而深。

我是Dignity_呱,如果想跟我一起讨论和学习前端可以关注我深夜末班车,咱们交朋友,一起进步。

👍 如果对您有帮助,您的点赞是我前进的润滑剂。

以往推荐

Vue3的响应式到底比Vue2优雅在哪

靓仔,说一下keep-alive缓存组件后怎么更新及原理?

如何回答new Vue阶段做了什么?

优雅回答watch和computed的区别以及选择?

多图详解,一次性啃懂原型链(上万字)

Vue-Cli3搭建组件库

VuePress搭建项目组件文档

原文链接

https://juejin.cn/post/7170888152461082637/

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容