前言
本文更多是个人的一个见解,若有地方有疏漏,错误,请辛苦批评指正。
日常工作中,页面切换是在前端很常见的一个场景。以往的时候,我们可能用的大多都是多页面应用改变页面 URL 链接去更新我们渲染的页面。而现在,我们大多都用上了主流的前端框架,像 vue, react 等,这些我们一般结合了对应的路由工具,如 vue-router, react-router-dom 去进行路由管理。
本文会着重地从单页应用路由来讲解个人的看法。
页面应用模式
首先,我们先来了解,目前两种页面应用模式,前端常见的页面模式有两种,mpa 及 spa。
MPA
MPA 指的是多页应用,也是以前常用的一种页面模式。一般来说,当我们切换页面路由,或跳转的时候,多页应用都会去重新请求新的html文档,以及相关的资源,从而渲染页面。
sequenceDiagram
Client-)Client: 触发页面跳转
Client->>Serve: 发起请求,加载资源
Serve-->>Client: 返回资源
Client-)Client: 页面渲染
SPA
SPA(Single Page application) 指的是单页应用。一般来说,我们只有首次进入的时候,需要去请求页面的主要资源,后续当我们切换页面应用内路由的时候,并不会重新加载html以及静态资源,但可能会动态加载额外资源。
sequenceDiagram
Client-)Client: 监听路由变化,js动态渲染页面
对比
| 多页应用模式MPA | 单页应用模式SPA | ||
|---|---|---|---|
| 应用构成 | 由多个完整页面构成 | 一个页面和多个页面片段构成 | |
| 跳转方式 | 页面之间的跳转是从一个页面跳转到另一个页面 | 通过借助history,hash的api,监听路由变化,切换渲染的组件,本质没有重载页面 | |
| 跳转后公共资源是否重新加载 | 是 | 否 (但不排除动态加载额外资源) | |
| 用户体验 | 页面间切换加载慢,不流畅 | 页面片段间的切换快 | |
| 页面间传递数据 | 依赖页面以及浏览器API, URL、cookie或者localstorage等。 | 页面周期未销毁内,变量在同一内存下,不销毁,就可以通信如状态管理工具redux, mbox, vuex. |
这里只是简单对两种方式做了对比,这里做对比并不代表,哪种应用模式就绝对是优的,不同的场景下,可能会有不同的取舍。同时,像 SEO, 首屏渲染等都没有进行对比,有兴趣的朋友可以研究下。
下面,我们着重讲下单页应用路由。
单页应用路由切换原理
单页应用下(如 vue, react),我们切换页面的 url,此时我们的页面会渲染对应新的内容,但这个过程中,其实并没有重新加载页面。
实际上,路由工具跳转使用了history对象或改变hash值,同时做了url监听,在url变化的时候,会根据url的信息,去匹配对应的组件,进行老组件的销毁,新组件的挂载,从而进行页面的变化。
url 更新
功能: 路由工具跳转使用了history对象或改变hash值, 这两种情况都可以做到改变页面的url, 但不会重新刷新/加载页面。
history对象
借助了history对象上的 pushState / replaceState 的方法。
history.pushState(null, '', '/pathname')
改变hash值
修改hash, 即修改上图fragment,也是不会重新刷新/加载页面。
监听url 变化
功能: 这里主要做url监听,进行组件渲染即可。
(下方是一个伪代码,在实际情况下,是无法运行的,只是给一个代码思路。毕竟,url的监听还要考虑该应用使用的路由模式。)
handleUrlChange(() => {
const matchedComponent = matchComponentByLocaltion(routeMap, localtion);
render(matchedComponent);
});
组件的匹配
功能: 这里主要做页面 url 信息,从而匹配对应的组件,大多单页面的页面切换,实质上是组件渲染替换的过程。
// 伪代码
const matchComponentByLocaltion = (routeMap: RouteMap, location: Location) => {
let matchedComponent = null;
// 根据 routeMap, location 找到对应组件从而赋值。
return matchedComponent;
}
大体的思路是这样,但实际上,这里面需要考虑的细节还很多,路由工具也做了很多细节,以及边界case的处理,如。
- 原生没有提供监听
url的api,得根据具体的路由模式进行监听。 - 动态路由以及嵌套路由的处理。
当然,本文并不会去讨论这以上细节的处理,重点还是在于路由切换的大体实现。
下方,我们会讲如何不同路由模式下的url的监听变化, 而关于组件的匹配,这里并不会展开。
单页应用下路由模式
前面说过我们可以通过 history 和 hash 两种方式,做路由的跳转,从而对于路由工具来说也有了两种模式: history 模式 和 hash模式。
History 模式
借助了history API,实现url改变,渲染对应的组件。
url如何改变
前文提到是依赖于history对象上的 pushState / replaceState 的方法。(react-router和vue-router依赖了history, 底层也是使用了window上的history对象)。
// 本质上都是依赖history
history.pushState(null, '', '/pathname')
如何进行监听
原生没有提供history跳转的路由监听的API,这也是一个痛点,不过目前也有解决方案。
方案一:重写window.history对象上方法做监听
这个是来源于社区的,主要的思路是
- 重写
window.history对象pushState/replaceState方法,在执行完原生方法后,执行自己的监听回调函数。
const prevPushState = window.history.pushState;
window.history.pushState = function(...args) {
const ret = prevPushState.apply(window.history, args);
// 监听逻辑
renderComponent();
return ret;
}
- 监听
popState事件,考虑用户手动点击浏览器前进,后退操作。
window.addEventListener('popstate', () => {
renderComponent();
})
可参考笔者之前的文章 单页应用history路由监听,但是觉得这种方案过于hack。
方案二:使用history库的提供的监听API
这种方式就比较方便了,如果路由工具中跳转使用了history库,直接使用了history库的listen函数,可能成本会更低。
history.listen(callback);
Hash 模式
url如何改变
手动修改hash值,调用history库的api都可以,实质上是改变了url的hash部分。
// 手动修改
localtion.hash = 'nextHash'
// 调用history api, 实质上也是通过window.history对象的api修改hash部分。
history.push('')
如何进行监听
hash模式的监听,比较简单,有原生的监听事件hashChange,我们直接使用就行了。
window.addEventListener('hashchange', function() {
renderComponent();
});
或者可以直接用history库的listen,进行监听。
以上,就是不同模式下路由监听的实现
总结 & 思考
本文讲解了以下几点
- 页面应用模式
- 单页应用路由原理
- 单页应用下路由模式
本文可能由于笔者经验尚浅,同时夹带一些个人见解,若有地方有疏漏,错误,请辛苦批评指正。希望这篇文章大家对单页应用路由有一些新知识点的输入吧。
TIP:
- 可以去看看
react-router-dom和vue-router中的路由工具的实现以及设计思路。 - 路由工具大多都依赖了
history库,可以看看history库的实现。