开局面试官就让我设计一个路由
前言
- 常网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,路由会自动强制进入这个模式。
因为在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;
}
四、路由导航守卫有哪些?
🙋面试官心想:前面都难不倒你,那你说一下路由导航守卫有哪些?
我就不信,你能把三个都说出来。
🙋🏻♂️路由导航守卫一共有三种,分别是全局、路由独享、组件内。
全局
-
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(离开页面后)
钩子函数内有三个参数
- 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
等。
可能有些初学者还是不太能理解。我再举个例子。
场景:详情页(文章、商品)
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);
面试官:有点底子的啊。
六、给你出道实战题
面试官说:不把你难倒,我就对不起我刁难官的称号。
需求:我们在H5想通过手机左滑、右滑和机身自带的返回键,让打开的弹出框隐藏。
第一想法就是监听左滑事件,让弹出框隐藏。
在安卓/IOS 端可以通过监听物理返回事件去关闭弹窗,但是在H5,不好意思,是没有这一事件。
阿,你这,和路由有啥关系。
面试官心里想:你小子,终于不会了是吧。就想要你这个表情。
🙋回去等通知吧。
面试官摸了摸胡子,今日KPI已达标。
🙋🏻♂️让我输个明白吧,告诉我吧。
🙋想知道?那就告诉你吧。
其实这种返回在H5实际上只是返回上一页的功能,也就是回退到上个历史记录。
因此我们可以在弹窗打开时,添加一个不会改变当前页面的历史记录,如 ?popyp=true(或 #popup),在触发物理返回键后,浏览器会后退一个历史记录并且自动清除?popyp=true(或 #popup),而页面不会发生跳转和刷新,最后通过监听url变化,识别出url中 ?popyp=true 被清除则关闭弹窗。
这也是路由的一个小应用了,学以致用。
原来如此。学废了。
那我回家做饭了。
刚刚和你开玩笑的,你前面回答得这么好,在我心里已经是过了的,明天来报道。
哦,那明天见,我回家做饭了😁😁😁。
祝君在每一次面试中,都能收割到Offer。
后记
我们再来总结一下回答:
🙋🏻♂️我个人主要从三个方面回答
-
我们可以定义一个createRouter函数,返回路由实例:
- 保存我们传入的配置项。
- 监听hash || popstate事件。
- 回调里根据path匹配对应路由。
-
然后要把他定义成插件,即实现
install
方法。-
实现两个全局组件:router-link页面跳转、router-view内容显示。
- 实现router-view:内容要想渲染,就会涉及到路由模式,因为我们根据路由变化来渲染不同的组件页面。
- hash模式,最常用的模式,监听hashchange事件
- history模式,需要后端配合的模式,监听popstate事件
- router-link其实就是个
a
标签。
-
定义两个全局变量:router,组件内可以访问当前路由和路由器实例route。
-
-
如果可以的话,再聊聊路由模式的不同点,基本就大功告成。
其实按照这个思路回答不算难,贵在自己理解,才能抵挡住面试官的猛攻。(可反复多看几次)
回答面试题的时候,尽量形成一个结构化的回答,大体,再细化,由浅而深。
👍 如果对您有帮助,您的点赞是我前进的润滑剂。
以往推荐
原文链接
转载自:https://juejin.cn/post/7170888152461082637