浅谈单页应用路由

255 阅读6分钟

前言

本文更多是个人的一个见解,若有地方有疏漏,错误,请辛苦批评指正。

日常工作中,页面切换是在前端很常见的一个场景。以往的时候,我们可能用的大多都是多页面应用改变页面 URL 链接去更新我们渲染的页面。而现在,我们大多都用上了主流的前端框架,像 vue, react 等,这些我们一般结合了对应的路由工具,如 vue-router, react-router-dom 去进行路由管理。

本文会着重地从单页应用路由来讲解个人的看法。

页面应用模式

首先,我们先来了解,目前两种页面应用模式,前端常见的页面模式有两种,mpaspa

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, URLcookie或者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

image.png

修改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的处理,如。

  • 原生没有提供监听urlapi,得根据具体的路由模式进行监听。
  • 动态路由以及嵌套路由的处理。

当然,本文并不会去讨论这以上细节的处理,重点还是在于路由切换的大体实现。

下方,我们会讲如何不同路由模式下的url的监听变化, 而关于组件的匹配,这里并不会展开。

单页应用下路由模式

前面说过我们可以通过 historyhash 两种方式,做路由的跳转,从而对于路由工具来说也有了两种模式: history 模式 和 hash模式。

History 模式

借助了history API,实现url改变,渲染对应的组件。

url如何改变

前文提到是依赖于history对象上的 pushState / replaceState 的方法。(react-routervue-router依赖了history, 底层也是使用了window上的history对象)。

// 本质上都是依赖history
history.pushState(null, '', '/pathname')

如何进行监听

原生没有提供history跳转的路由监听的API,这也是一个痛点,不过目前也有解决方案。

方案一:重写window.history对象上方法做监听

这个是来源于社区的,主要的思路是

  1. 重写window.history对象pushState/replaceState方法,在执行完原生方法后,执行自己的监听回调函数。
const prevPushState = window.history.pushState;

window.history.pushState = function(...args) {
  const ret = prevPushState.apply(window.history, args);
  // 监听逻辑
  renderComponent();
  return ret;
}
  1. 监听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

  1. 可以去看看react-router-domvue-router中的路由工具的实现以及设计思路。
  2. 路由工具大多都依赖了history库,可以看看history库的实现。

参考资料