前端路由
为什么会出现前端路由
传统的多页面应用 MPA 是指访问服务端时,由服务端返回该页面的所有 dom 结构。每当切换路由时,都需要服务端生成对应路由的 dom 结构并返回前端。此时,并没有前端路由的概念。
随着技术的发展,单页面应用 SPA 越来越流行。以 React、VUE 为例,用户在访问某个 url 时,服务端返回一个 html 文件,html 文件内的 script 标签有 js 文件,浏览器加载对应的 js 文件并执行,生成对应的 dom 结构,即客户端渲染。在后续的交互过程中,都不会再重新请求 html 文件了,全部依赖 js 代码的执行更新页面。
服务端返回的 html 大体如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/static/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>网站title</title>
<script type="module" crossorigin src="/assets/index-F1SL953E.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DdzCGrS2.css" />
</head>
<body>
<div id="root"></div>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
此时,每个 url 所对应的页面内容,均由浏览器端处理,即前端路由
前端路由解决了什么问题
既然前端页面结构是浏览器生成的,那如果我有十几个页面要互相跳转切换,咋整!!?? 这时候 前端路由 应运而生,它的出现就是为了解决单页面网站,通过切换浏览器地址路径,来匹配相对应的页面组件。我们通过一张图片来理解这个过程
前端路由 会根据浏览器地址栏 pathname 的变化,去匹配相应的页面组件。然后将其通过创建 DOM 节点的形式,塞入根节点
。 这就达到了无刷新页面切换的效果,从侧面也能说明正因为无刷新,所以 React 、 Vue 、 Angular 等现代框架在创建页面组件的时候,每个组件都有自己的 生命周期。前端路由的实现原理
Hash 模式
a 标签锚点大家应该不陌生,而浏览器地址上 # 后面的变化,是可以被监听的,浏览器为我们提供了原生监听事件 hashchange ,它可以监听到如下的变化:
- 点击 a 标签,改变了浏览器地址
- 浏览器的前进后退行为
- 通过 window.location 方法,改变浏览器地址
- history.replaceState(undefined, undefined, "#/some"),history.pushState(undefined, undefined, "#/some"),
实现
const pathView = {
path1: Component1,
path2: Component2,
};
window.addEventListener(
'hashchange',
function (e) {
const currentUrl = location.hash.slice(1) || '/';
renderView(pathView[currentUrl]);
},
false
);
2
3
4
5
6
7
8
9
10
11
12
13
在上面的代码中,当触发 hashchange 事件时,获取当前的 url,进行页面渲染即可。 同样,也可以在 hashchange 事件中,做 pvuv 的上报
结论
- hash 模式所有的工作都是在前端完成的,不需要后端服务的配合
- hash 模式的实现方式就是通过监听 URL 中 hash 部分的变化,从而做出对应的渲染逻辑
- hash 模式下,URL 中会带有#,看起来不太美观
History 模式
history 略微复杂, 参考 hash 模式,可能有以下行为可以修改浏览器地址:
- 点击 a 标签,改变了浏览器地址,重新发起请求
- 点击浏览器的前进后退按钮,触发前进后退行为
- 调用
history.back()
,history.forward()
,history.go()
方法,触发前进后退行为 - 调用
history.pushState()
,history.replaceState()
, 此时只更新地址栏,页面不更新 - 通过 window.location 方法,改变浏览器地址,此时浏览器会重新发起请求
在此,先解释下history.pushState()
, history.replaceState()
这两个方法
相同点
- 都会更新浏览器地址栏
- 页面都不刷新
- 携带的参数一致
不同点
- pushState 是新增一条纪录,replaceState 是替换最新的一条纪录
针对上面的 5 种情况,详细说明一下:
- 点击 a 标签,改变了浏览器地址,重新发起请求。既然已经重新发起请求,也就意味着前后两条纪录不是同一个执行上下文,没有关系了,不会触发 popState 事件
- 点击浏览器的前进后退按钮,如果接下来的历史纪录是
history.pushState()
,history.replaceState()
这两个方法更新的,则触发 popState 事件,否则,浏览器重新发起请求 - 调用
history.back()
,history.forward()
,history.go()
,如果接下来的历史纪录是history.pushState()
,history.replaceState()
这两个方法更新的,则触发 popState 事件,否则,浏览器重新发起请求 - 调用
history.pushState()
或者history.replaceState()
不会触发 popstate 事件 - window.location 改变浏览器地址,不会触发 popstate 事件
针对上面的情况,需要对应的处理:
点击 a 标签,可以通过拦截 click 事件
jsconst aList = document.querySelectorAll('a[href]'); aList.forEach((aNode) => aNode.addEventListener('click', function (e) { // 需要判断 _target,如果是新开页面,不需要拦截 // 也需要判断 a 标签的其他属性,比如 download e.preventDefault(); //阻止a标签的默认事件 const href = aNode.getAttribute('href'); // 手动修改浏览器的地址栏 history.pushState(null, '', href); // 通过 history.pushState 手动修改地址栏, // 此时不会触发 popState 事件,所以此处需要手动执行回调函数 renderView(); // 可以主动创建一个事件 // 如果重写了 history.pushState,不需要主动dispatchEvent,否则会触发两次 let event = new Event('pushState'); event.arguments = [null, '', href]; window.dispatchEvent(e); }) );
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20点击浏览器的前进后退按钮,监听 popstate 事件即可,在第 4 点详细说明
调用
history.back()
,history.forward()
,history.go()
,监听 popstate 事件即可,在第 4 点详细说明调用
history.pushState()
或者history.replaceState()
, 需要主动触发事件jslet _wr = function (type) { let orig = history[type]; return function () { let rv = orig.apply(this, arguments); // 其实这里可以增加 pushState,replaceState 两个事件 let e = new Event('popstate'); e.arguments = arguments; window.dispatchEvent(e); return rv; }; }; history.pushState = _wr('pushState'); history.replaceState = _wr('replaceState');
1
2
3
4
5
6
7
8
9
10
11
12
13
14这样可以给 history.pushState 和 history.replaceState 添加回调事件
接下来解释一下第 2 点和第 3 点: 在业务交互中,修改(新增)前端路由的主要方法就是 a 标签和 history 的两个方法了。 这两种类型都添加了 popstate 事件。在使用 history 纪录时,点击浏览器的前进后退按钮和调用
history.back()
,history.forward()
,history.go()
,因为历史纪录是来自 history.pushState 和 history.replaceState(a 标签通过点击事件重写了),也会触发 popState 事件,即只监听 popState 就可以了使用 window.location.href 改变浏览器地址, 会触发浏览器重新加载(spa 会重新加载 html),不是同一个文档,自然也不会触发 popstate.因为 location 是一个只读对象,不能重写,所以拦截不住了. 即使通过 proxy 或者 Object.defineProperties 添加 set 逻辑,依旧会触发浏览器重新加载页面,因为不是同一个文档而拦截无效