摘要:最新一直在看关于和路由这块的知识,最终发现这些路由框架的模块功能的实现都是基于浏览器原生路由的。在浏览器中实现前端路由主要有两种方式一个是我们常用的,另一个是提供的。该对象的和分别表示的各个部分,它们因此被称为分解属性。
最新一直在看关于 Vue 和 React 路由这块的知识,最终发现这些路由框架的模块功能的实现都是基于浏览器原生路由 API 的。本着追根溯源的初心,于是就想着将浏览器原生的路由 API 整体梳理一遍,以便更加顺畅的理解 Vue-Router 和 React-Router 的相关实现和原理。
背景浏览器的主要功能就是根据输入的 URL 在窗口加载对应的文档,与此同时,浏览器会记录一个 tab 窗口载入过的所有文档,同时会提供 "前进"、"后退" 和 "刷新" 的功能,以便用户可以在这些已经记录的文档之间进行切换浏览和重载当前页面获取最新的浏览信息。
这些功能的实现最早是在服务器端实现的,因为那时候的引用都是前后端不分离的,页面内容也是动态生成的,所以这些页面的跳转、切换、刷新都是在服务端实现的。后来出现了 SPA(Single Page Application 单页应用),页面都是通过 JavaScript 动态生成和载入到页面的,并且可以在无刷新的情况下加载页面最新的状态信息,这时候如果要提供上述的功能就需要自己进行处理(因为此时的页面都是现实在同一个大的框架页面里面的,根本不存在页面的跳转切换),所以催生了各种框架对应的 Router 实现。
在浏览器中实现前端路由主要有两种方式:一个是我们常用的 hash,另一个是 HTML5 提供的 history。其实还有另外一种利用 stack 实现的方式适用于 Node.js 服务器端,这里我们着重说一下浏览器提供的 hash 和 history 吧,stack 具体怎么实现等我们说到 x-Router 源码的时候再详细说一下。
Hash在浏览器 URL 地址栏,我们总会发现像这样的地址:react.docschina.org/docs/react-… React 官网关于 lazy 的一个地址)。大家肯定发现:这串 URL 的最后有以 # 号开始的一串标识,那它到底是起着什么样的作用呢?肯定不会平白无故的出现吧。
hash 特性
你可以直接在浏览器中打开这个链接地址,你是不是发现页面会自动滚动到(页面顶部定位到)标题为 React.lazy 的部分文档。你再将页面往上滚动,肯定会发现上面还有部分的文档内容。此时,你修改地址栏的地址为 react.docschina.org/docs/react-… React.Suspense 部分。
在早些年,hash 作为 URL 的一部分主要用来定位文档中的文档片段。在上面的例子中,我们通过在 URL 后面添加 #reactlazy 和 #reactsuspense 定位到了文档对应标题为 React.lazy 和 React.Suspense 的部分。那他们到底是怎么做到的呢?
通过审核元素我们发现:在 React.lazy 和 React.Suspense 对应的标题部分分别都有一个 h3 标签,而且标签的 id 属性对应就是我们在 URL 地址栏输入的 hash 值部分(只是少了 # 号)。
hash 定位文档片段
可能有同学会有疑惑:为什么 hash 是通过元素上面的 id 属性来定位文档的?前面我们提到过,URL 中的 hash 部分是用来定位文档中的文档片段的。大家想想:所需要定位的文档片段肯定是唯一的,不然定位肯定是不准确了,那这个定位文档就有点鸡肋了,在文档中标识唯一的属性只有是 id 了,如果是我,我也会通过 hash 匹配元素的 id 来定位文档。现在来验证一下我们的猜想:
1、首先在新的 tab 窗口打开 react.docschina.org/docs/react-… 页面,然后在审核元素下找到上图所展示的 DOM 元素,修改其中的 h3 标签的 id 属性值为 reactlazy1,接着在 URL 地址栏追加 #reactlazy hash 值并按下回车键,此时页面并没有定位到标题为 React.lazy 的文档片段,最后将 URL 地址栏的 #reactlazy hash 值改成 #reactlazy1 hash 值并按下回车键,此时页面并没有定位到标题为 React.lazy 的文档片段,这一系列的表现说明 hash 定位还是和元素的 id 属性值还是有关联的;
2、依然是在新的 tab 窗口打开 react.docschina.org/docs/react-… 页面,然后将页面手动滚动到标题为 React.lazy 的文档片段,将鼠标放在标题上会出现一个锚点的图标,点击图标发现页面定位到了标题为 React.lazy 的文档片段并且 URL 地址栏变成了 react.docschina.org/docs/react-… #reactlazy hash 值。此时再回头看看我们前面给出的截图发现 id 属性值为 reactlazy 的 h3 标签中有一个 href 属性值为 #reactlazy 的 a 标签,其实我们在页面上看到的锚点图标就是这个 a 标签的展示。当我们点击锚点图标就是点击了 a 链接,然后将 url 定位到了 id 属性值为 reactlazy 的 h3 标签,还是很好的说明了 hash 定位还是和元素的 id 属性值还是有关联的;
3、MDN 官方定义如下:
MDN 官方文档上有明确的定义,但是我们还是通过两个方面来证明了我们的推论,乍一看好像说了很多没有用的东西,其实这样反复的推敲更有利于我们深刻的理解相关的知识点以及为什么是这样,而不是那样!
hash 路由
hash 的存在除了可以通过设置文档中元素的 ID 来定位文档片段之外,还可以设置为任意的字符串来表示路由。在 Vue、React 等现代前端框架中,为了实现功能完备的 SPA 应用都配备了对应的路由系统。在这些路由系统都会提供 hash 路由模式。
在 hash 模式下,hash 会支持任意的字符串来表示对应的 URL。这些路由系统针对 hash 模式的实现基本都是大同小异:在设置 location.hash 属性值后,应用就会想尽一切办法检测状态值变化,以便能够读取出存储在片段标识符中的状态并相应地更新自己的状态。支持 HTML5 的浏览器一旦发现片段标识符发生了变化,就会在 Window 对象上触发 hashchange 事件,这时就会触发对象的函数处理逻辑 —— 对 location.hash 的值进行解析,然后使用该值包含的状态信息重新渲染应用。
这里只是提到了一个基础的思路,路由系统的具体实现,后续会娓娓道来!
hash 事件
</>复制代码
// 在 window 下监听 hashchange 事件
window.onhashchange = function() {
// 当事件触发时输出当前的 hash 值
console.log(window.location.hash)
}
在不支持 HTML5 的浏览器中,我们可以通过 100ms 轮询监听 url 变化来模拟:
</>复制代码
(function(window){
// 如果浏览器不支持原生实现的事件,则开始模拟,否则退出。
if ( "onhashchange" in window.document.body ) return;
var location = window.location,
oldUrl = location.href,
oldHash = location.hash;
// 每隔 100ms 检查 hash 是否发生变化
setInterval(function() {
var newUrl = location.href,
newHash = location.hash;
// hash 发生变化且全局注册有 onhashchange 方法(这个名字是为了和模拟的事件名保持统一);
if (newHash !== oldHash && typeof window.onhashchange === "function" ) {
// 执行方法
window.onhashchange({
type: "hashchange",
oldURL: oldUrl,
newURL: newUrl
});
oldUrl = newUrl;
oldHash = newHash;
}
}, 100);
})(window)
⚠️注意:设置 location.hash 属性会更新显示在地址栏中的 URL,同时会在浏览器的历史记录中添加一条记录。
History为了标准化管理浏览器历史管理,HTML5 定义了相对复杂的 API —— history。
history api
1、history 里面新增了两个 API,history.pushState() 和 history.replaceState()。这两个 API 都接受同样的参数:
它们之间的不同之处是:history.pushState() 方法是将新状态添加到浏览器的历史记录中,也就是说还可以通过点击 "后退" 按钮,退到前一个页面;history.replaceState() 是用新的状态代替当前的历史状态,也就是说没有更多的历史记录,"后退" 按钮不能操作了,页面不能 "后退" 了。
⚠️注意:当执行这两个 API 时,浏览器的 URL 地址栏会变化,但是页面内容不会刷新!
状态对象(state