03-面试之前端 Vue-Router

vue-router 路由模式有几种?

vue-router 有 3 种路由模式:hash、history、abstract,对应的源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器;
  • history : 依赖 HTML5 History API 和服务器配置。具体可以查看 HTML5 History 模式;
  • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

区别:

  1. hash 模式较丑,history 模式较优雅
  2. pushState 设置的新 URL 可以是与当前 URL 同源的任意 URL;而 hash 只可修改 # 后面的部分,故只可设置与当前同文档的 URL
  3. pushState 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;而 hash 设置的新值必须与原来不一样才会触发记录添加到栈中
  4. pushState 通过 stateObject 可以添加任意类型的数据到记录中;而 hash 只可添加短字符串
  5. pushState 可额外设置 title 属性供后续使用
  6. hash 兼容 IE8 以上,history 兼容 IE10 以上
  7. history 模式需要后端配合将所有访问都指向 index.html,否则用户刷新页面,会导致 404 错误

vue-router 中常用的 hash 和 history 路由模式实现原理

(1)hash 模式的实现原理

早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是 URL 中 # 后面的内容。比如 https://www.word.com#search,它的 location.hash 的值为 #search

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

  • URL 中 hash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送;
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制 hash 的切换;
  • 可以通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 的 hash 值会发生改变;或者使用 JavaScript 来对 loaction.hash 进行赋值,改变 URL 的 hash 值;
  • hash 模式的工作原理是 hashchange 事件,在 window 监听 hash 的变化。我们在 url 后面随便添加一个 #xx 即可触发这个事件。vue-router hash 模式 使用 URL 的 hash 来模拟一个完整的 URL,当 # 后面的 hash 发生变化,不会导致浏览器向服务器发出请求,浏览器不发出请求就不会刷新页面,但是会触发 hashchange 这个事件,从而对页面进行跳转(渲染)。

(2)history 模式的实现原理

HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState()history.repalceState()。这两个 API 可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:

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

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

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

使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
// hash路由原理 ***************************
// 监听hashchange方法
window.addEventListener('hashchange',()=>{
div.innerHTML = location.hash.slice(1)
});

// history路由原理 ************************
// 利用html5的history的pushState方法结合window.popstate事件(监听浏览器前进后退)
function routerChange (pathname){
history.pushState(null,null,pathname);
div.innerHTML = location.pathname
}
window.addEventListener('popstate',()=>{
div.innerHTML = location.pathname
})
</script>

Vue router 哪个模式不会请求服务器

  • hash 模式:使用 URL 的 hash 来模拟一个完整的 URL,仅改变 hash 部分不会向服务器发送请求。

  • history 模式:利用 HTML5 History API 来管理路由,需要服务器支持。当使用 history 模式时,URL 会像正常网页 URL 一样,但在使用该模式时,服务器需要被配置为返回你的 index.html 页面,以确保所有路径都可以被正确处理。

  • abstract 模式:主要用于 Node.js 服务端渲染 (SSR),不需要请求服务器。

所以,hash 和 abstract 模式不会请求服务器,而 history 模式在正确配置服务器后不会请求服务器。

router 和 route 的区别

  • router 为 VueRouter 的实例,相当于一个全局的路由器对象,里面含有很多属性和子对象,例如 history 对象。。。经常用的跳转链接就可以用 this.$router.push,和 router-link 跳转一样。

  • route 相当于当前正在跳转的路由对象。可以从里面获取 name,path,params,query 等。

vue-router 路由跳转方式

声明式(标签跳转)

1
2
<router-link :to="{name:'home'}"></router-link>
<router-link :to="{path:'/home'}"></router-link>

编程式(js 跳转)

1
2
3
this.$router.push('/home')
this.$router.push({name:'home'})
this.$router.push({path:'/home'})

vue-router 路由传参

vue-router 有几种钩子函数(导航守卫)

简单的说,导航守卫就是路由跳转过程中的一些钩子函数。路由跳转是一个大的过程,这个大的过程分为跳转前中后等等细小的过程,在每一个过程中都有一些函数,这个函数能让你操作一些其他事的时机,这就是导航守卫。

  1. 全局路由。全局导航钩子主要有两种钩子:前置守卫(beforeEach)、后置守卫(afterEach)

  2. 路由独享的钩子。

    • router.beforeEnter(to,from,next) 单个路由独享的导航钩子,它是在路由配置上直接进行定义的
      详细知识点可以查看路由导航守卫
  3. 组件导航守卫。

    • router.beforerRouterEnter(to,from,next)
    • router.beforerRouterupdate(to,from,next)
    • router.beforerRouterupLeave(to,from,next)

全局前置守卫 router.beforeEach

1
2
3
4
const router = new VueRouter({ ... });
router.beforeEach((to, from, next) => {
// ...
})

当一个导航开始时,全局前置守卫按照注册顺序调用。守卫是异步链式调用的,导航在最后的一层当中。

1
2
3
4
5
6
7
8
9
new Promise((resolve, reject) => {
resolve('第一个全局前置守卫')
}.then(() => {
return '第二个全局前置守卫'
}.then(() => {
...
}.then(() => {
console.log('导航终于开始了') // 导航在最后一层中
})

每个守卫方法接收三个参数(往后的守卫都大同小异):

  1. to: Route: 即将要进入的目标路由对象

  2. from: Route: 当前导航正要离开的路由对象

  3. next: Function: 一定要调用该方法将控制权交给下一个守卫,执行效果依赖 next 方法的参数。

    • next(): 进入下一个守卫。如果全部守卫执行完了。则导航的状态就是 confirmed (确认的)。

    • next(false): 中断当前的导航(把小明腿打断了)。如果浏览器的 URL 改变了 (可能是用户手动或者浏览器 后退按钮),那么 URL 地址会重置到 from 路由对应的地址。

    • next('/')next({ path: '/' }): 跳转到一个不同的地址。当前的导航被中断,然后进行一个新的导航(小明被打断腿并且送回家了)。你可以向 next 传递任意位置对象,且允许设置诸如 replace: true、name: 'home' 之类的选项以及任何用在 router-link 的 to prop 或 router.push 中的选项。

    • next(error): (2.4.0+) 如果传入 next 的参数是一个 Error 实例,则导航会被终止 且 该错误会被传递 router.onError() 注册过的回调。

    注意:永远不要使用两次 next,这会产生一些误会。

全局解析守卫 router.beforeResolve

router.beforeResolve 注册一个全局守卫。这和 router.beforeEach 类似,因为它在 每次导航时都会触发,但是确保在导航被确认之前,同时在所有组件内守卫和异步路由组件被解析之后,解析守卫就被正确调用

全局解析守卫总是被放在最后一个执行。

全局后置守卫 router.afterEach

导航已经确认了的,小明已经到了外婆家了,你打断他的腿他也是在外婆家了。

1
2
3
4
router.afterEach((to, from) => {
// 你并不能调用next
// ...
})

路由独享的守卫

在路由内写的守卫

1
2
3
4
5
6
7
8
9
10
11
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// ...
}
}
]
})

组件内的守卫

组件内的导航钩子主要有这三种:beforeRouteEnterbeforeRouteUpdate (2.2 新增)beforeRouteLeave。他们是直接在路由组件内部直接进行定义的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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`
}
}

导航全过程

  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 实现懒加载

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

实现:结合 Vue 的异步组件和 Webpack 的代码分割功能,可以实现路由组件的懒加载

  1. 首先,可以将异步组件定义为返回一个 Promise 的工厂函数 (该函数返回的 Promise 应该 resolve 组件本身):

    1
    const Foo = () => Promise.resolve({ /* 组件定义对象 */ })
  2. 在 Webpack 2 中,我们可以使用动态 import语法来定义代码分块点 (split point):

    1
    import('./Foo.vue') // 返回 Promise

    结合这两者,这就是如何定义一个能够被 Webpack 自动代码分割的异步组件。

    1
    const Foo = () => import('./Foo.vue')

    在路由配置中什么都不需要改变,只需要像往常一样使用Foo:

    1
    2
    3
    4
    5
    const router = new VueRouter({
    routes: [
    { path: '/foo', component: Foo }
    ]
    })

js 是如何监听 HistoryRouter 的变化的

通过浏览器的地址栏来改变切换页面,前端实现主要有两种方式:

  1. 通过 hash 改变,利用 window.onhashchange 监听。

  2. HistoryRouter: 通过 history 的改变,进行 js 操作加载页面,然而 history 并不像 hash 那样简单,因为 history 的改变,除了浏览器的几个前进后退(使用 history.back(), history.forward()history.go() 方法来完成在用户历史记录中向后和向前的跳转。)等操作会主动触发 popstate 事件,pushState,replaceState 并不会触发 popstate 事件,要解决 history 监听的问题,方法是:

    首先完成一个订阅-发布模式,然后重写 history.pushStatehistory.replaceState 并添加消息通知,这样一来只要 history 的无法实现监听函数就被我们加上了事件通知,只不过这里用的不是浏览器原生事件,而是通过我们创建的 event-bus 来实现通知,然后触发事件订阅函数的执行。

具体操作如下:

  1. 订阅-发布模式示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    class Dep {                  // 订阅池
    constructor(name){
    this.id = new Date() //这里简单的运用时间戳做订阅池的ID
    this.subs = [] //该事件下被订阅对象的集合
    }
    defined(){ // 添加订阅者
    Dep.watch.add(this);
    }
    notify() { //通知订阅者有变化
    this.subs.forEach((e, i) => {
    if(typeof e.update === 'function'){
    try {
    e.update.apply(e) //触发订阅者更新函数
    } catch(err){
    console.warr(err)
    }
    }
    })
    }
    }
    Dep.watch = null;
    class Watch {
    constructor(name, fn){
    this.name = name; //订阅消息的名称
    this.id = new Date(); //这里简单的运用时间戳做订阅者的ID
    this.callBack = fn; //订阅消息发送改变时->订阅者执行的回调函数
    }
    add(dep) { //将订阅者放入dep订阅池
    dep.subs.push(this);
    }
    update() { //将订阅者更新方法
    var cb = this.callBack; //赋值为了不改变函数内调用的this
    cb(this.name);
    }
    }
  2. 重写 history 方法,并添加 window.addHistoryListener 事件机制。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var addHistoryMethod = (function(){
    var historyDep = new Dep();
    return function(name) {
    if(name === 'historychange'){
    return function(name, fn){
    var event = new Watch(name, fn);
    Dep.watch = event;
    historyDep.defined();
    Dep.watch = null; //置空供下一个订阅者使用
    }
    } else if(name === 'pushState' || name === 'replaceState') {
    var method = history[name];
    return function(){
    method.apply(history, arguments);
    historyDep.notify();
    }
    }
    }
    }());
    window.addHistoryListener = addHistoryMethod('historychange');
    history.pushState = addHistoryMethod('pushState');
    history.replaceState = addHistoryMethod('replaceState');

路由跳转和 location.href 的区别?

  • 使用 location.href='/url' 来跳转,简单方便,但是刷新了页面;

  • 使用路由方式跳转,无刷新页面,静态跳转;

params 和 query 的区别

  • 用法:query 要用 path 来引入,params 要用 name 来引入
  • 接收参数都是类似的,分别是 this.$route.query.namethis.$route.params.name
  • url 地址显示:query 会把参数显示在地址栏上,params 不会把参数显示在 url
  • 刷新:query 刷新不会丢失 query 里面的数据,params 刷新会丢失 params 里面的数据

路由守卫进行判断登录

在 vue 项目中,切换路由时肯定会碰到需要登录的路由,其原理就是在切换路径之前进行判断,你不可能进入页面再去判断有无登录重新定向到 login,那样的话会导致页面已经渲染以及它的各种请求已经发出。

如需要登录的路由可在 main.js中统一处理(全局前置守卫)。使用 router.beforeEach 方法,不懂得可以打印 to,from 的参数就 ok,requireAuth 可以随意换名的,只要 man.js 里面跟配置路由的 routes 里面的字段保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import router from './router'
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireAuth)){ // 判断该路由是否需要登录权限
if(!sessionStorage.getItem('token') && !localStorage.getItem('token')){
next({
path: '/login',
query: {redirect: to.fullPath} // 将跳转的路由path作为参数,登录成功后跳转到该路由
})
}else{
next();
}
}else {
next();
}
});
new Vue({
el: '#app',
router,
render: h => h(App)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default new Router({
routes: [
{
path: '/',
name: 'home',
redirect: '/home'
},
{
path: '/home',
component: Home,
meta: {
title: '',
requireAuth: true, // 添加该字段,表示进入这个路由是需要登录的
}
},
{
path:'/login',
name:'login',
component:Login
},
{
path:'/register',
name:'register',
component:Register
}
]
})

后端主动通知前端的方式

  • 轮训
  • 定时器
  • websocket

03-面试之前端 Vue-Router
https://flepeng.github.io/interview-20-开发语言类-21-frontend-03-Vue-03-面试之前端-Vue-Router/
作者
Lepeng
发布于
2023年8月8日
许可协议