浅析vue路由概念及实现逻辑
2024-07-02 11:40:03

面试官:您能说说vue的路由模式吗?

我:hash和history,hash相对于传统模式不够美观,是spa独有的类型,无法服务端渲染,history是传统的方式,更符合平时的使用直觉,大概是这样。

面试官:您能更详细的说说吗?

我:行。。。。你故意找茬是吧。

正文

这种问题其实算是很早之前刷过的面试题了,但是实际开发过程中基本没细研究过,除了在服务端渲染的时候稍微了解了一下,后续确实没有深入了解。

而这也是三年前端必须要掌握的基础功能了,我这里没有深入了解,确实显得基础不扎实了。

路由概念

在正式了解vue实现逻辑之前,我们先了解一下路由的一些概念。

后端路由(传统路由)

传统的 Web 应用都是由多个页面组成,页面之间通过链接的方式进行跳转。

每切换一个页面,就由浏览器发起页面请求,后端服务器接收到请求,对这个地址进行匹配,找到这个地址对应的逻辑,解析地址及参数,最终返回这个地址对应的页面 HTML。

这个对请求地址进行匹配的逻辑就被称为路由,它是在后端发生的,因此也叫后端路由

对后端路由来说,每一个页面对于前端代码来说都是全新的,前端代码在页面加载的时候开始运行,在页面关闭或者跳到下一个页面的时候结束运行。

这种类型的路由便于网络爬虫抓取,便于搜索殷勤收录,所以这种路由模式更适合一些官网或展示类页面。

前端路由

前端路由的概念是随着单页面应用(Single Page Application)的流行而产生的。

单页面应用,是指整个 Web 应用只有一个页面。

由浏览器发起请求再由后端返回页面 HTML 这样的过程只会在第一次访问时发生。

在第一次请求完毕后,后面的页面逻辑都由前端进行控制。

好处是一旦用户完成第一次页面加载,在后续的使用中,用户都不会看到页面加载的过程,从而获得更流畅的使用体验,也减少了服务器的压力。

坏处在于,失去了 Web 应用最核心的特性 URL

一个典型的前端路由在单页面场景下大概需要关注以下几个问题:

  1. 定义路由表,即各种 URL 分别对应哪些逻辑(一般来说就是对应界面的渲染)。
  2. 获取当前访问的 URL,并根据路由表匹配中对应的逻辑并调用它(渲染对应的界面)。
  3. 处理链接跳转,如果链接地址是在单页面应用的范围内,则不能使用浏览器导航,而是直接完成新 URL 对应的界面的渲染,并将浏览器中显示的 URL 更新为新界面对应的 URL。
  4. 监视 URL 的变更,当用户手工更改 URL 或者有其它逻辑更改了 URL 之后,需要重新进行路由匹配并完成界面的渲染。

一般来说,

(1)是纯计算逻辑,不需要什么特别的处理,(2)可以由 location 这个 API 进行获取,因此前端路由中值得关注的核心问题主要就是 (3)和 (4),简单地归纳就是更新浏览器 URL 和监视浏览器 URL 改变。

单页面应用因为不需要关注网络搜索引擎的收录,所以更适合用在管理系统中。

而且,相对于传统模式,这种前端路由不需要服务器进行渲染处理,所以可以大幅减少服务器压力,在现代开发中更推荐使用。

路由模式

这里的vue模式,主要是针对Vue-Router推出的两种模式做个总结,我个人对react不是很熟,所以不清楚react是否也有类似的路由组件。

hash 模式

对于一个 URL 如:/home#/hello/world,其中的 hash 部分就是#/hello/world

当我们在单页面应用中切换到另一个页面时,修改 hash 即可。

hash 部分在浏览器导航的时候并不会被传给后端服务器,也可以方便地用 JavaScript 修改,并且修改它时也不会发生重新导航的情况,因此对于单页面应用来说,非常适合用来作为前端路由的方案。

对于 hash 模式下 URL 的监听:

  • 老旧的浏览器,使用定时器,定时获取浏览器的 URL,并与之前的结果比对
  • 较新的浏览器提供了 hashchange 事件,直接监听这个事件即可
  • 更新的浏览器提供了 popstate 事件

hash 模式的缺点:

  • 不符合用户的固有认知,也不太美观
  • hash 部分不会被传递给后端服务器,导致没有办法进行服务端渲染,进而影响搜索引擎的收录
  • 和a元素的锚点跳转的功能冲突,导致a元素的锚点跳转无法使用

history 模式

在单页面应用下,这个模式的核心在于 history.pushState(state, title, url) 这个 API,它的含义是向浏览器的历史栈(即前进后退的栈)中压入一个新的状态,从逻辑上相当于跳转到了一个新的页面,但是并不真的重新加载或重新导航。

使用这个 API 很方便地修改浏览器中的 URL,并正确地处理前进 / 后退的问题。

该模式下对 URL 的监听使用 popstate 事件。

当用户进行导航动作(前进 / 后退等)或有 history.back()、history.forward() 之类的调用时,popstate 事件就会发生。

占位组件

Vue-Router 的作用不仅是管理路由,还需要配合 Vue 完成路由对应界面的渲染,Vue 本身是声明式渲染的,而 Vue-Router 通过声明组件(<router-view>)的方式来接管渲染。

当开发者使用 Vue-Router 时,<router-view> 组件会被全局注册,但它并没有具体的内容可渲染,当渲染到 <router-view> 时,就会由 Vue-Router 来决定这个组件的位置应该渲染哪个界面,从而实现从 URL 到路由匹配再到渲染对应界面的过程。

实现逻辑

这里主要讲vue-router源码的实现逻辑,这里就是简单看看,没必要深入。

插件安装

Vue.use() 是 Vue 提供的用来安装插件的方法,它要求参数提供一个 install() 方法,Vue 会调用这个 install() 方法完成安装。

vue-router 的 install() 方法位于一个单独的文件,即 src/install.js。

install() 方法主要做了这么几件事情:

  • 声明了 beforeCreate() 和 destroyed() 两个 mixin,这样在 Vue 实例的生命周期中能够处理 vue-router 相关的逻辑。
  • 声明了两个属性 router和router 和 router和route,分别指向了 this._routerRoot 对象上的_router 和_route。
  • 将_route 变成响应式数据,这样当它变更的时候就会触发组件的重新渲染。
  • 声明了两个全局组件 RouterView 和 RouterLink,这正是我们经常使用的 <router-view><router-link>

定义路由

声明 VueRouter 类,代码位于 src/index.js,new VueRouter() 时调用。

初始化时主要有这样几件事情:

  • 创建了用于进行 URL 匹配的路由表。路由表用来存储定义好的路由与对应的页面组件(用于在 <router-view> 中渲染)的关系。
  • 根据 mode 配置项决定使用 hash 模式还是 history 模式。
  • 根据对应的模式,选择负责管理历史记录和 URL 的 History 子类,初始化后赋值给 this.history。

VueRouter 类中 通过this.matcher = createMatcher(options.routes || [], this)创建路由表。

createMatcher方法源码位于 src/create-matcher.js 。

使用名称或 URL 匹配路由表中定义好的路由,参数解析(如解析 /foo/:bar),子路由处理,别名 alias 处理,都是通过 createMatcher 处理的。

最后会返回Route对象:

1
2
3
4
5
6
7
8
9
10
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}

值得一说的是,传入一个路由的时候,路由会被编译成一个正则表达式进行参数解析(rc/create-matcher.js/matchRoute)。

如传入一个路由 /foo/:bar:

1
2
3
4
const keys = [];
const regexp = pathToRegexp("/foo/:bar", keys);
// regexp = /^\/foo(?:\/([^\/#\?]+?))[\/#\?]?$/i
// keys = [{ name: 'bar', prefix: '/', suffix: '', pattern: '[^\\/#\\?]+?', modifier: '' }]

如果有参数则将参数从匹配的结果中取出,最终放到 Route 对象中的 params 属性中。

路由模式的实现

路由模式的处理源码在 src/history 目录下,hash 模式的处理逻辑在 hash.js, history 模式在 html5.js中,两个类的接口很相似。

路由模式的实现主要任务点在,更新浏览器 URL(ensureURL 方法) 和监视浏览器 URL 改变(setupListeners 方法)。

处理完之后回到VueRouter 类的 init 方法修改 _route, _route 是一个响应式数据,当它发生变更的时候,组件会重新渲染。

RouterView

RouterView的主要作用就是将当前匹配的路由的组件渲染出来,因为当前是哪个组件是会动态变化的,因此 Vue-Router 选择了使用 render() 方法来实现。

首先从路由中取出对应的组件,然后使用 h() 方法(即 createElement() 方法)返回组件的虚拟 DOM,后续跟 Vue 中的组件渲染一样。

RouterLink主要是对链接的事件做了拦截,当点击链接的时候,会尝试调用 router.push() 或者 router.replace() 方法来完成导航,并阻止浏览器默认的导航,从而使这些链接也变成前端路由接管。

如果我们查看页面渲染的出来的H5源码,我们也会发现,routerLink会完成

结语

vueRouter实现的很精妙,极大的方便了我们日常切换不同功能模块的需求。

如今,vueRouter几乎已经是开发中必须的插件,对其略作深入的了解也是必须的,虽然出在面试题中不算合理,但是也是考量了开发者的了解广度。

虽然,我仍然觉得意义不大,不过确实算是扩充了一些熟悉的知识。

参考

Vue-Router 前端路由原理