面试官:您能说说vue的路由模式吗?
我:hash和history,hash相对于传统模式不够美观,是spa独有的类型,无法服务端渲染,history是传统的方式,更符合平时的使用直觉,大概是这样。
面试官:您能更详细的说说吗?
我:行。。。。你故意找茬是吧。
正文
这种问题其实算是很早之前刷过的面试题了,但是实际开发过程中基本没细研究过,除了在服务端渲染的时候稍微了解了一下,后续确实没有深入了解。
而这也是三年前端必须要掌握的基础功能了,我这里没有深入了解,确实显得基础不扎实了。
路由概念
在正式了解vue实现逻辑之前,我们先了解一下路由的一些概念。
后端路由(传统路由)
传统的 Web 应用都是由多个页面组成,页面之间通过链接的方式进行跳转。
每切换一个页面,就由浏览器发起页面请求,后端服务器接收到请求,对这个地址进行匹配,找到这个地址对应的逻辑,解析地址及参数,最终返回这个地址对应的页面 HTML。
这个对请求地址进行匹配的逻辑就被称为路由,它是在后端发生的,因此也叫后端路由。
对后端路由来说,每一个页面对于前端代码来说都是全新的,前端代码在页面加载的时候开始运行,在页面关闭或者跳到下一个页面的时候结束运行。
这种类型的路由便于网络爬虫抓取,便于搜索殷勤收录,所以这种路由模式更适合一些官网或展示类页面。
前端路由
前端路由的概念是随着单页面应用(Single Page Application)的流行而产生的。
单页面应用,是指整个 Web 应用只有一个页面。
由浏览器发起请求再由后端返回页面 HTML 这样的过程只会在第一次访问时发生。
在第一次请求完毕后,后面的页面逻辑都由前端进行控制。
好处是一旦用户完成第一次页面加载,在后续的使用中,用户都不会看到页面加载的过程,从而获得更流畅的使用体验,也减少了服务器的压力。
坏处在于,失去了 Web 应用最核心的特性 URL
。
一个典型的前端路由在单页面场景下大概需要关注以下几个问题:
- 定义路由表,即各种 URL 分别对应哪些逻辑(一般来说就是对应界面的渲染)。
- 获取当前访问的 URL,并根据路由表匹配中对应的逻辑并调用它(渲染对应的界面)。
- 处理链接跳转,如果链接地址是在单页面应用的范围内,则不能使用浏览器导航,而是直接完成新 URL 对应的界面的渲染,并将浏览器中显示的 URL 更新为新界面对应的 URL。
- 监视 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 | const route: Route = { |
值得一说的是,传入一个路由的时候,路由会被编译成一个正则表达式进行参数解析(rc/create-matcher.js/matchRoute)。
如传入一个路由 /foo/:bar:
1 | const keys = []; |
如果有参数则将参数从匹配的结果中取出,最终放到 Route 对象中的 params 属性中。
路由模式的实现
路由模式的处理源码在 src/history 目录下,hash 模式的处理逻辑在 hash.js, history 模式在 html5.js中,两个类的接口很相似。
路由模式的实现主要任务点在,更新浏览器 URL(ensureURL 方法) 和监视浏览器 URL 改变(setupListeners 方法)。
处理完之后回到VueRouter 类的 init 方法修改 _route, _route 是一个响应式数据,当它发生变更的时候,组件会重新渲染。
RouterView 和 RouterLink
RouterView
RouterView
的主要作用就是将当前匹配的路由的组件渲染出来,因为当前是哪个组件是会动态变化的,因此 Vue-Router 选择了使用 render() 方法来实现。
首先从路由中取出对应的组件,然后使用 h() 方法(即 createElement() 方法)返回组件的虚拟 DOM,后续跟 Vue 中的组件渲染一样。
RouterLink
RouterLink
主要是对链接的事件做了拦截,当点击链接的时候,会尝试调用 router.push() 或者 router.replace() 方法来完成导航,并阻止浏览器默认的导航,从而使这些链接也变成前端路由接管。
如果我们查看页面渲染的出来的H5源码,我们也会发现,routerLink
会完成
结语
vueRouter实现的很精妙,极大的方便了我们日常切换不同功能模块的需求。
如今,vueRouter几乎已经是开发中必须的插件,对其略作深入的了解也是必须的,虽然出在面试题中不算合理,但是也是考量了开发者的了解广度。
虽然,我仍然觉得意义不大,不过确实算是扩充了一些熟悉的知识。