从 history 到 SPA


前后端分离目前算是一种共识了,特别是前端 MV* 框架的兴起,前端更倾向于按照框架写代码,之后打包再交由后端 render。因此,如果传统的根据路由打包为多个入口会比较蛋疼,单一入口的路由管理算是 SPA 的一个关键部分了,而这里离不开 history


history

比较有意思的是,互联网根基不应该是光纤或者电脑之类,应该是 URL,任何互联网资源都有唯一的 URL(说的不严谨,请不要深究,下同),所以可以说 URL 是互联网资源的身份证。在资源跳转的时候可以轻松地前进后退,因为浏览器里有个 history 结构管理着这一切。history 的实现有点类似于栈,点击链接是 push,后退是 pop,但是也不是栈,因为你也可以前进,或者直接手输 URL 进行 replace。

但是,不管是如何转换(push/pop/replace/…)说白了都是对资源的一种替换(这里资源我们暂时当作 HTML 页面吧)以及资源依赖的一些其它资源(可以理解为 js/css/images 等),每次页面的切换(URL变动)都是资源的加载过程。这逻辑上当然没有什么问题,但是如果切换的两个页面有 90% 的内容和资源都一样呢,在没有正确缓存的情况下,全局刷新是对资源的一种极大的浪费。

从浏览器角度来说,它是没有优雅的方式去判断两个页面之间的差异然后只加载新内容的,因为页面的差异情景太多,这锅不应该让浏览器背。但是,换种思路呢,如果 history 的切换变得可编程,这一切就都不是事了,因为写代码的人肯定很清楚差异是什么。

三种 history

为了更好的对 history 进行编程,著名的 history 对此做了个封装,其提供了三种形式的 history,对外暴露出相同的基础操作,底层实现是不同的,适用场景也不一样:

  • browserHistory 是基于原生 HTML5 history API 的封装,这更接近我们大多数时候看得 URL,用 path 来区分,这更利于 SEO,并且在使用(如分享,收藏等)的时候坑更少。虽然对浏览器版本有点要求,但是仍然是首选
  • hashHistory 可以兼容性更高,这个更像我们平时说的单页,因为它的 path 不变,用的是 # 号去区分资源,如:example.com/#/some/path
  • memoryHistory 并不是为浏览器准备的,因为它的 url 并不是从地址栏读取的,其实根本没有地址栏的概念,自身在内存里模拟一个 history 环境,主要用于 React-Native

SPA 里的路由

说完 history 了,可以说 SPA 了,使用上 SPA 体验并不会是单页的感觉,大多也是很多页,甚至很多你根本感知不出是否是单页,因为 SPA 更多的是指实现方式的一个入口,而不是表现上的。引入 MV* 思想后,这里页会被当作模块,所以如果可以控制在不同的 URL 时展示不同的模块,就可以实现一个单页系统。

所以,一个 SPA 可以简单到:

return `{#if path = '/home'}<Home />{#else}<Error />{/if}`

表现上就是访问 /home 就显示首页,访问其它页面就显示错误页面,这不就是我们过去几十年用浏览器的方式吗?所以我说 SPA 并不是表现上的单入口。

由于 hashHistory 放弃了用 path 去定位资源,所以其有着天然 SPA 的基因,使用上并不会有太多疑问。browserHistory 应用于单页系统的时候需要配置下服务端,可以对任何 path 都返回同一个页面。接下来的过程可以想象下,主要三种场景:

  • 手动改变地址栏:这种没什么特别过程,就是一个全新加载过程,资源加载完后应用根据 URL 渲染出合适组件(页面)
  • 通过导航前进后退:这会抛出 popstate 等事件,应用根据事件更新 history 栈(不精确)信息(地址栏 URL 也会被改变),最后渲染出正确组件
  • 页面链接过去:在单页应用里,应用内链接并不会真的写 <a href=""> 链接,会把点击事件转为设置 history 操作,同样 URL 跟随改变,最后渲染出目标页面

结论

  • SPA 在使用上并不会单页
  • 优先使用 browserHistory,除非你无服务端的控制权(如把静态页面放在 CDN 上)
  • 浏览器如果暴露更多可编程接口,会极大丰富开发模式