Jiahonzheng's Blog

前端路由实现方式

字数统计: 1.3k阅读时长: 5 min
2020/02/03 Share

在 Web SPA 中,前端路由描述的 URL 与 UI 之间的单向映射关系,即 URL 变化引起 UI 页面的更新(无需刷新页面)。

核心问题

上面我们提到,在前端路由中,当 URL 发生变化时,我们需要在不刷新页面的情况下,触发 UI 页面的更新。因此,在实现前端路由时,我们需要解决以下两个核心的问题。

  • 如何检测 URL 是否变化?
  • 如何改变 URL 却不引起页面刷新?

我们可以从 Hash 和 History 两种实现方式回答上述两个问题。

  • 在 Hash 方式中,我们可以通过 hashchange 事件监听 URL 的变化,以下场景会触发 hashchange 事件:通过浏览器前进后退改变 URL 、通过标签改变 URL 、通过 window.location 改变 URL 。Hash 是 URL 中 # 及后面的部分,改变 URL 中的 Hash 部分不会引起页面刷新。
  • 在 History 方式中,我们可以通过 popstate 事件监听 URL 的变化。我们可通过调用 pushStatereplaceState 两种方法,改变 URL 而不引起页面刷新。值得注意的是,通过浏览器前进后退改变 URL 时会触发 popstate 事件,而通过 pushStatereplaceState 或标签改变 URL 并不会触发 popstate 事件,因此我们需要手动拦截。

实现

代码地址:GitHub Gist 前端路由实现方式

为便于测试,我们使用了 github.com/zeit/serve 作为页面服务器,相关命令如下。

1
2
3
4
5
6
7
8
# 安装依赖。
yarn

# 对 Vanilla Hash 进行演示。
yarn vanilla.hash

# 对 Vanilla History 进行演示。
yarn vanilla.history

Vanilla

现在,我们使用原生 HTML/JS 实现 Hash 和 History 两种模式的前端路由,不依赖任何框架。

Hash

页面 vanilla.hash.html 的具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Hash Route - Vanilla</title>
</head>
<body>
<ul>
<!-- 定义路由 -->
<li><a href="#/home">home</a></li>
<li><a href="#/about">about</a></li>

<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
<script src="hash.js"></script>
</body>
</html>

vanilla.hash.js 中,我们通过对 hashchange 事件进行监听,从而检测 URL 是否变化。当 URL 变化时,我们调用 onHashChange 函数,通过修改元素的 innerHTML 属性,在页面无刷新的情况下,实现页面视图的更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 维护 UI 页面。
let routerView = null;

// 路由变化时,根据路由渲染对应 UI 页面。
function onHashChange() {
switch (window.location.hash) {
case '':
case '#/home':
routerView.innerHTML = 'Home';
return;
case '#/about':
routerView.innerHTML = 'About';
break;
default:
}
}

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件。
window.addEventListener('DOMContentLoaded', () => {
routerView = document.querySelector('#routeView');
onHashChange();
});

// 监听路由变化。
window.addEventListener('hashchange', onHashChange)

History

页面 vanilla.history.html 的具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>History Route - Vanilla</title>
</head>
<body>
<ul>
<!-- 定义路由 -->
<li><a href="/home">home</a></li>
<li><a href="/about">about</a></li>

<!-- 渲染路由对应的 UI -->
<div id="routeView"></div>
</ul>
<script src="vanilla.history.js"></script>
</body>
</html>

vanilla.history.js 中,我们通过对 popstate 事件进行监听,从而检测 URL 是否变化。当 URL 变化时,我们调用 onPopState 函数,通过修改元素的 innerHTML 属性,在页面无刷新的情况下,实现页面视图的更新。

由于通过 pushStatereplaceState 和标签改变 URL 时,并不会触发 popstate 事件,我们需要手动拦截。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 维护 UI 页面。
let routerView = null;

// 路由变化时,根据路由渲染对应 UI 页面。
function onPopState() {
switch (window.location.pathname) {
case '/':
case '/home':
routerView.innerHTML = 'Home';
return;
case '/about':
routerView.innerHTML = 'About';
break;
default:
}
}

// 页面加载完不会触发 hashchange,这里主动触发一次 hashchange 事件。
window.addEventListener('DOMContentLoaded', () => {
routerView = document.querySelector('#routeView');
// 刷新页面。
onPopState();

// 拦截 <a> 标签点击事件默认行为, 点击时使用 pushState 修改 URL并更新手动 UI,从而实现点击链接更新 URL 和 UI 的效果。
const links = document.querySelectorAll('a[href]');
links.forEach(el =>
el.addEventListener('click', function handler(e) {
e.preventDefault();
// 手动拦截。
window.history.pushState(null, '', el.getAttribute('href'));
onPopState();
}),
);
});

// 监听路由变化。
window.addEventListener('popstate', onPopState);

在使用 History 模式时,当我们刷新页面,会出现 404 页面。为解决这个问题,我们需要 rewrite 请求。

在 Nginx 中,我们需要添加以下设置:对于所有的请求,我们都响应相同的页面,让页面来接管路由。

1
2
3
location / {
try_files $uri $uri/ /vanilla.history.html;
}

serve 中,我们只需定义 rewrites 数组即可。

1
2
3
4
5
6
7
8
{
"rewrites": [
{
"source": "**",
"destination": "vanilla.history.html"
}
]
}

参考资料

CATALOG
  1. 1. 核心问题
  2. 2. 实现
    1. 2.1. Vanilla
      1. 2.1.1. Hash
      2. 2.1.2. History
  3. 3. 参考资料