前端路由需要实现两个核心, 1. 修改 URL 而不引起页面刷新, 2. 检测 URL 的变化, 这两个核心取决于你采用的前端路由技术选择的方案, 即 hash 和 history, 当选择了技术后, 前端路由是怎样去实现修改的操作的呢?
前情提要
本篇文章以 vue-router 为例, 并对其中的 API 源码进行一定的分析, 并且只会介绍 history 模式, 前端路由修改 URL 的方式, 原因是 hash 是 2014 年以前普遍采用的方式, history 模式是 HTML5 标准的新方式, 而且在 vue-router4 中 hash 模式只是作为 history 模式的降级处理(当 history 模式无法修改时, 会采用 hash 模式的修改), 关于两种模式更详细的区别和介绍可以参考另一篇文章, vue-router 的不同的历史记录模式详解
改变 URL 的方式
在原生浏览器提供的 API 中,我们可以通过以下几种方式修改 URL
- 浏览器前进后退
- a 标签
- window.location
而在 vue-router4 中, Router 实例提供的修改 URL 或者说路由的导航方式有以下几种
- push
- replace
- go
- back
- forward
接下来会对上述方法对应的源码进行分析
编程式导航
编程式导航指的是在 vue/JS 中编写导航代码, 前面说了 Router 实例提供了 5 种修改 URL 的方法, 实际在源码中只实现了 3 种, 即 push, replace, go 而对于 back, forward 实际上是 go 的变种, 在 router/src/router.js 1141 的 Router 定义中 back, forward 实际是这样实现的, 把代码复用发挥到了极致了属于是
const router: Router = {
...
push,
replace,
go,
back: () => go(-1),
forward: () => go(1),
...
};
go
go/back/forward 有点接近浏览器前进后退的语义,那 go 的实现究竟和浏览器前进后退有没有关系呢?浏览器的前进后退对应的是浏览器的两个 API, window.history.back() 和 window.history.forward(), 而 vue-router4 中 go 的定义实际上也明说了
- window.history.back() == myHistory.go(-1)
- window.history.forward() == myHistory.go(1)
所以 go 的实现猜也猜得到了,它的实现在 router/src/history/html5.ts 320 (这里提一嘴 vs code 的代码导航的功能, 我查看 go 的实现的时候,它直接就给我跳了,实际上 go 的实现有多个,没有给我选择的空间,这里我手动流汗黄豆)
function go(delta: number, triggerListeners = true) {
if (!triggerListeners) historyListeners.pauseListeners();
history.go(delta);
}
直接是使用的浏览器管理历史记录的对象, history.go(), 我有点怀疑其实浏览器原生实现 history.back() 和 history.forward() 也是用的 history.go 了, 注意实现中的 triggerListeners, 它将决定你的操作是否触发前端路由的监听器, 注意,小细节,前端路由是需要检测 URL 变化的,而 history 模式 是通过 popState 事件实现的检测变化, window.history.go(), window.history.back(), window.history.forward() 调用的时候可不会触发 popState 事件
replace
replace 实际上是调用的 replaceState(), 这个东西可能就需要 MDN 的文档来了解一下了, 友情链接, 注意一个小细节, 它名字叫做 replaceState, 但实际上它和 pushState 一样都会在浏览器历史纪录上创建一个新的历史记录项, 比如你在当前页面的控制台(你用手机看的话当我没说)调用下面这个代码
history.pushState(null, "", "d");
history.replaceState(null, "", "a");
虽然传递的 URL 非常离谱, 但是不妨碍它能够无刷运行, 它是不会发出请求的, 而且最重要的是, 它会新增两条历史记录, 假设你在 https://www.baidu.com
调用的这个, 那么你看历史记录里面其实会有两条记录, 一条 https://www.baidu.com/a
一条 https://www.baidu.com/b
除此之外, 还有一个小细节, 如果你之前没有用过 pushState() 直接上的 replaceState, 那么无论调用多少次 replaceState 都只是修改当前页面的历史记录, 而不会新增
回到 vue-router4 的实现, 在 router/src/history/html5.js 240
function replace(to: HistoryLocation, data?: HistoryState) {
const state: StateEntry = assign(
{},
history.state,
buildState(
historyState.value.back,
// keep back and forward entries but override current position
to,
historyState.value.forward,
true
),
data,
{ position: historyState.value.position }
);
changeLocation(to, state, true);
currentLocation.value = to;
}
这里的 replace 做了三件事
- 创建状态对象, 这是 vue-router 自己的状态对象, 这个对象包含 window.history 自己的 state, 还有上一个历史记录和前一个历史记录, 以及编码时需要传递的数据
- 修改浏览器历史记录, 涉及到 replaceState() 的
changeLocation()
,changeLocation()
很重要, push, replace 和浏览器自身的历史记录关联起来就靠它 - 修改 Router 实例中 URL 的值
重点分析一下 changeLocation()
, 在 router/src/history/html5.js 203
function changeLocation(
to: HistoryLocation,
state: StateEntry,
replace: boolean
): void {
...
try {
history[replace ? 'replaceState' : 'pushState'](state, '', url)
historyState.value = state
} catch (err) {
...
location[replace ? 'replace' : 'assign'](url)
}
}
从这里可以看到 vue-router 里面 push 和 replace 实际上都是调用 changeLocation 修改浏览器历史记录的,而且当出现错误的时候会降级使用, window.location 的方式修改 URL (Hash 模式用的就是 window.location)
push
push 常用于栈之类的数据结构, 在 vue-router 里实际就是压入浏览器历史记录栈中的, 而在 vue-router 的源码实现中, push 和 replace 的实现是高度一致的,这也就是为什么先讲了 replace, 简简单单看一下源码
function push(to: HistoryLocation, data?: HistoryState) {
...
const currentState = assign(
{},
historyState.value,
history.state as Partial<StateEntry> | null,
{
forward: to,
scroll: computeScrollPosition(),
}
)
changeLocation(currentState.current, currentState, true)
const state: StateEntry = assign(
{},
buildState(currentLocation.value, to, null),
{ position: currentState.position + 1 },
data
)
changeLocation(to, state, false)
currentLocation.value = to
}
和 replace 实现的目的实际上是一样的, 但是有个小细节, 它是调用了两次 changeLocation, 又一个小细节!第一次是为了更新当前页面的历史记录状态对象信息, 更新信息包括当前页面滚动信息(当我们返回上一级路由时能够直接闪现到上一次访问时的位置), 以及 forward(前一个历史记录 URL), 因为这个页面要跳转到新的页面, 第二次 changeLocation 就是创建跳转页面的历史记录状态对象了
总结
前端路由(指 vue-router), 它们修改 URL 并不是说修改了就算了, 直接修改调用 replaceState 和 pushState 就行了, vue-router 在修改路由的同时, 用来大量的精力去维护状态对象, 状态对象中存储了一个历史记录条目的信息(小细节, 这个状态对象序列化后的大小不能超过 640kb), 这个状态对象是前端路由传递信息, 维护状态, 正确导航的核心
看来 vue-router 部分源码后,其实它们的很多实现也是依据浏览器提供的原生 API 实现的,并不是毫无根据的,说白了,你自己也能用这个 API 去捣鼓一个轮子出来,但是没必要,别人造的库,嵌套的那叫一个眼花缭乱,各式各样都给你考虑好了,所以说 珍爱生命,远离造轮