最新一直在看关于 Vue 和 React 路由这块的知识,最终发现这些路由框架的模块功能的实现都是基于浏览器原生路由 API 的。本着追根溯源的初心,因而就想着将浏览器原生的路由 API 总体梳理一遍,以便更加顺畅的理解 Vue-Router 和 React-Router 的相关实现和原理。前端
浏览器的主要功能就是根据输入的 URL 在窗口加载对应的文档,与此同时,浏览器会记录一个 tab 窗口载入过的全部文档,同时会提供 "前进"、"后退" 和 "刷新" 的功能,以便用户能够在这些已经记录的文档之间进行切换浏览和重载当前页面获取最新的浏览信息。react
这些功能的实现最先是在服务器端实现的,由于那时候的引用都是先后端不分离的,页面内容也是动态生成的,因此这些页面的跳转、切换、刷新都是在服务端实现的。后来出现了 SPA(Single Page Application 单页应用),页面都是经过 JavaScript 动态生成和载入到页面的,而且能够在无刷新的状况下加载页面最新的状态信息,这时候若是要提供上述的功能就须要本身进行处理(由于此时的页面都是现实在同一个大的框架页面里面的,根本不存在页面的跳转切换),因此催生了各类框架对应的 Router 实现。后端
在浏览器中实现前端路由主要有两种方式:一个是咱们经常使用的 hash,另外一个是 HTML5 提供的 history。其实还有另一种利用 stack 实现的方式适用于 Node.js 服务器端,这里咱们着重说一下浏览器提供的 hash 和 history 吧,stack 具体怎么实现等咱们说到 x-Router 源码的时候再详细说一下。api
在浏览器 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 来定位文档。如今来验证一下咱们的猜测:微信
一、首先在新的 tab 窗口打开 react.docschina.org/docs/react-… 页面,而后在审核元素下找到上图所展现的 DOM 元素,修改其中的 h3 标签的 id 属性值为 reactlazy1,接着在 URL 地址栏追加 #reactlazy hash 值并按下回车键,此时页面并无定位到标题为 React.lazy 的文档片断,最后将 URL 地址栏的 #reactlazy hash 值改为 #reactlazy1 hash 值并按下回车键,此时页面并无定位到标题为 React.lazy 的文档片断,这一系列的表现说明 hash 定位仍是和元素的 id 属性值仍是有关联的;框架
二、依然是在新的 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 属性值仍是有关联的;
三、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,同时会在浏览器的历史记录中添加一条记录。
为了标准化管理浏览器历史管理,HTML5 定义了相对复杂的 API —— history。
history api
一、history 里面新增了两个 API,history.pushState() 和 history.replaceState()。这两个 API 都接受一样的参数:
它们之间的不一样之处是:history.pushState() 方法是将新状态添加到浏览器的历史记录中,也就是说还能够经过点击 "后退" 按钮,退到前一个页面;history.replaceState() 是用新的状态代替当前的历史状态,也就是说没有更多的历史记录,"后退" 按钮不能操做了,页面不能 "后退" 了。
⚠️注意:当执行这两个 API 时,浏览器的 URL 地址栏会变化,可是页面内容不会刷新!
为此,咱们能够利用语雀网站作一系列的实验:
window.history.pushState(null, null, "https://www.yuque.com/dashboard/?name=littleLane"); // result: https://www.yuque.com/dashboard/?name=littleLane window.history.pushState(null, null, "https://www.yuque.com/dashboard/name/littleLane"); //result: https://www.yuque.com/dashboard/name/littleLane window.history.pushState(null, null, "?name=littleLane"); //result: https://www.yuque.com/dashboard?name=littleLane window.history.pushState(null, null, "name=littleLane"); //result: https://www.yuque.com/dashboard/name=littleLane window.history.pushState(null, null, "/name/littleLane"); //result: https://www.yuque.com/dashboard/name/littleLane
在控制台中执行上面一系列语句时,浏览器的 URL 变化成了咱们备注的 result 的结果,可是页面并无发生重渲染,还有当咱们每次执行 pushState 时,浏览器历史都会添加一条记录,你们能够经过 "后退" 按钮进行查看。你们执行完上面的测试语句后,还能够将 pushState 替换成 replaceState 再次进行一轮测试,此时新的浏览记录都会代替当前的历史记录,仍是能够经过 "后退" 按钮进行查看。
⚠️注意:这里的 url 不支持跨域,当咱们把 www.yuque.com 换成 www.baidu.com 时就会报错。
二、除了上面新增的 API,history 对象上还有表示浏览历史列表数量的 length 属性,还定义了 back()、forward() 和 go() 进行浏览记录切换的方法。
History 对象的 back() 和 forward() 方法与浏览器的 "后退" 和 "前进" 按钮功能同样:它们可使浏览器在浏览历史中后退或前进跳转一格。而 go() 方法会接受一个整数,能够在浏览历史列表中向前(接受正整数参数)或向后(接受负整数参数)跳过任意多个页。好比 history.go(-1) 就会向后跳转一页,history.go(0) 就是刷新当前页,history.go(1) 就会向前跳转一页。
history 事件 - popstate
当用户经过 "前进" 和 "后退" 按钮浏览保存的历史状态时,浏览器会在 Window 对象上触发一个 popstate 事件。与该事件相关的事件对象有一个 state 属性,该属性包含传递给 pushState() 方法的状态对象的副本。
// 在 window 下监听 onpopstate 事件 window.onpopstate = function(state) { // 当 onpopstate 事件 (用户经过 "前进" 和 "后退" 按钮切换浏览记录) 触发时输出当前状态 console.log(state) }
Window 对象的 location 属性和 Document 对象的 location 属性引用的都是 Location 对象,它用来表示该窗口中当前显示的文档的 URL,并定义了方法来使窗口载入新的文档。
window.location === document.location // 老是返回 true
解析 URL
Location 对象的 href 属性是一个字符串,表示当前 URL 的完整文本。Location 对象的 toString() 方法返回 href 属性的值,所以在会隐式调用 toString() 的状况下,可使用 location 代替 location.href。
该对象的 protocol、host、hostname、port、pathname 和 search 分别表示 URL 的各个部分,它们所以被称为 URL 分解属性。通常咱们用的比较多的就是提取 URL 里面的参数了:
// 获取地址栏参数 const getUrlParame = (paramName) => { const urlParams = {}; let params = window.location.search.substring(1); if (!params) { return; } params = params.split('&'); for (let i = 0; i < params.length; i += 1) { let item = params[i]; item = item.split('='); urlParams[item[0]] = decodeURIComponent(item[1]); } if (paramName) { return urlParams[paramName]; } return urlParams; };
载入新文档
Location 对象的 assign() 方法可使窗口载入并显示指定的 url 中的文档。replace() 方法也有相似的功能,可是它会在新文档载入以前将当前文档从浏览历史中删除,就是说 "后退" 按钮并不会将浏览器带到原始的文档。
Location 对象还定义可 reload() 方法用来从新载入当前文档。
上述的内容咱们主要了解了在浏览器中支持的两种路由模式 —— hash 和 history,而后对它们各自的特性、api 和对应的事件作了详细的讲解,后面又说到了浏览器路由中相当重要的对象 —— Location,这一系列的内容为咱们后续理解 Vue-Router、React-Router 等路由系统的实现和阅读源码打下了坚实的基础。
做者:littleLane