VueRouter的hash和history模式

VueRouter的hash和history模式

vue-router 在实现单页面前端路由时,提供了两种方式:Hash 模式和 History 模式

vue2 是根据 mode 参数来决定采用哪一种方式

vue3 则是 history 参数

hash

简述

localhost:8080/#/home

  • vue-router 默认 hash 模式 —— 使用 URL 的 hash 来模拟一个完整的 URL,于是当 URL 改变时,页面不会重新加载。 hash(#)是 URL 的锚点,代表的是网页中的一个位置,单单改变 # 后的部分,浏览器只会滚动到相应位置,不会重新加载网页,也就是说 # 是用来指导浏览器动作的,对服务器端完全无用,HTTP 请求中也不会不包括 # ,同时每一次改变 # 后的部分,都会在浏览器的访问历史中增加一个记录,使用 “后退” 按钮,就可以回到上一个位置,所以说 hash 模式通过锚点值的改变,根据不同的值,渲染指定 DOM 位置的不同数据。
  • # 符号本身以及它后面的字符称之为 hash,可通过 window.location.hash 属性读取。

对 SEO 不友好

实现

==hash 通过 window.onhashchange 的方式,来监听 hash 的改变,根据 hash 的改变值来渲染对应路由。==

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
38
39
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="#/home">首页</a>
</li>
<li>
<a href="#/about">关于</a>
</li>
</ul>

<!-- 渲染对应的 UI -->
<div id="routerView"></div>
<script>
let routerView = document.getElementById('routerView')
window.addEventListener('hashchange', onHashChange)

// 控制渲染对应的 ui
function onHashChange() {
switch(location.hash) {
case '#/home':
routerView.innerHTML = '首页'
break
case '#/about':
routerView.innerHTML = '关于'
break
default: return
}
}
</script>
</body>
</html>

稍复杂一些的实现

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta
content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no,viewport-fit=cover"
name="viewport"
/>
<title>实现简单的hash路由</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
height: 100%;
}
#content {
height: calc(100vh - 50px);
display: flex;
align-items: center;
justify-content: center;
font-size: 3em;
}
#nav {
height: 50px;
position: fixed;
left: 0;
bottom: 0;
width: 100%;
display: flex;
}
#nav a {
width: 25%;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid black;
}
#nav a:not(:last-of-type) {
border-right: none;
}
</style>
</head>
<body>
<main id="content"></main>
<nav id="nav">
<a href="#/">首页</a>
<a href="#/shop">商城</a>
<a href="#/shopping-cart">购物车</a>
<a href="#/mine">我的</a>
</nav>
</body>
<script>
class VueRouter {
constructor(routes = []) {
this.routes = routes; // 路由映射
this.currentHash = ""; // 当前的hash
this.refresh = this.refresh.bind(this);
window.addEventListener("load", this.refresh, false);
window.addEventListener("hashchange", this.refresh, false);
}

getUrlPath(url) {
// 获取hash
return url.indexOf("#") >= 0 ? url.slice(url.indexOf("#") + 1) : "/";
}

refresh(event) {
// URL hash发生改变的时候,拿到当前的hash
let newHash = "",
oldHash = null;
if (event.newURL) {
oldHash = this.getUrlPath(event.oldURL || "");
newHash = this.getUrlPath(event.newURL || "");
} else {
newHash = this.getUrlPath(window.location.hash);
}
this.currentHash = newHash;
this.matchComponent();
}

matchComponent() {
let curRoute = this.routes.find(
(route) => route.path === this.currentHash
);
if (!curRoute) {
// 当前URL中的hash不存在的时候,默认取第一个,当然真实场景下,可能会有各种情况,取决于业务逻辑
curRoute = this.routes.find((route) => route.path === "/");
}
const { component } = curRoute;
document.querySelector("#content").innerHTML = component;
}
}

const router = new VueRouter([
{
path: "/",
name: "home",
component: "<div>首页内容</div>"
},
{
path: "/shop",
name: "shop",
component: "<div>商城内容</div>"
},
{
path: "/shopping-cart",
name: "shopping-cart",
component: "<div>购物车内容</div>"
},
{
path: "/mine",
name: "mine",
component: "<div>我的内容</div>"
}
]);
</script>
</html>

设置hash模式

vue2 中默认为 mode:hash

vue3 中使用 createWebHashHistory

1
2
3
4
5
6
import { createWebHashHistory } from 'vue-router'

const router = createRouter({
history: createWebHashHistory(),
routes
});

history

简述

localhost:8080/home

history 是路由的另一种模式,由于 hash 模式会在 url 中带#,如果不想要带 #的话,我们可以使用路由的 history 模式,只需要在响应的 router 配置规则时加上即可,vue 的路由默认是 hash 模式。

利用了HTML5 History Interface中新增的 pushState() 和 replaceState() 方法。

这两个方法应用于浏览器的历史记录栈,在当前已有的 back、forward、go 的基础之上,它们提供了对历史记录进行修改的功能。只是当它们执行修改时,虽然改变了当前的 URL,但浏览器不会立即向后端发送请求。

history api

window.history.go 可以跳转到浏览器会话历史中的指定的某一个记录页

window.history.forward 指向浏览器会话历史中的下一页,跟浏览器的前进按钮相同

window.history.back 返回浏览器会话历史中的上一页,跟浏览器的回退按钮功能相同

window.history.pushState 可以将给定的数据压入到浏览器会话历史栈中

window.history.replaceState 将当前的会话页面的url替换成指定的数据

优势

  1. 美观,没有#

  2. pushState() 相比于直接修改 hash 的优势

pushState()设置的新 URL 可以是与当前 URL 同源的任意 URL;而hash只可修改#
后面的部分,因此只能设置与当前 URL 同文档的 URL;

pushState()设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中;
而hash设置的新值必须与原来不一样才会触发动作将记录添加到栈中;

pushState()通过stateObject参数可以添加任意类型的数据到记录中;
而hash只可添加短字符串;

pushState()可额外设置title属性供后续使用。

缺陷

需要后端配置(nginx代理转发,找不到就重定向到项目首页)

否则刷新页面会报404,因为会重新发请求,而此时的 url 已经改变。

具体的说:当我们把 history 项目部署到服务器中后,此时我们在浏览器输入一个网址(比如是 www.test.com ), 此时会经过 dns 解析,拿到 ip 地址后根据 ip 地址向该服务器发起请求,服务器接受到请求后,然后返回相应的结果(html,css,js)。如果我们在前端设置了重定向,此时页面会进行跳转到 www.test.com/home ,在前端会进行匹配对应的组件然后将其渲染到页面上。此时如果我们刷新页面的话,浏览器会发送新的请求 www.test.com/home, 如果后端服务器没有 /home 对应的接口,那么就会返回404。

hash模式不会404,因为 #及之后的内容并不会放入请求

注意:开发环境没有这个问题,因为 webpack 处理好了。生产环境才会有问题。

实现

HTML5提供了History API来实现URL的变化,其中最主要的两个API有以下两个 history.pushState()和history.replaceState()。这两个API可以在不进行刷新的情况下,操作浏览器的历史记录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录

而 history 对比 hash 不同的是通过阻塞浏览器 url 地址栏更改之后的跳转,然后将其 pathname 拼接上去,实现路由,具体代码实现如下:

==每当激活的历史记录发生变化时,都会触发popstate事件。==

理论上我们可以通过监听popstate来控制路由渲染。

但是pushstate和replacestate是不会触发popstate事件的;go/back/forward可以触发.

所以可以重写 pushstate 和 replaceState

1
2
3
4
5
6
7
8
9
10
11
12
13
let _wr = function(type) {
let orig = history[type]
return function() {
let rv = orig.apply(this, arguments)
let e = new Event(type)
e.arguments = arguments
window.dispatchEvent(e)
return rv
}
}

history.pushState = _wr('pushState')
history.replaceState = _wr('replaceState')

或者手动触发?

页面加载成功时触发一次popstate,并给所有可能的路由跳转按钮加上点击事件,点击事件,调用pushState,后调用一次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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul>
<li>
<a href="#/home">首页</a>
</li>
<li>
<a href="#/about">关于</a>
</li>
</ul>

<!-- 渲染对应的 UI -->
<div id="routerView"></div>
<script>
let routerView = document.getElementById('routerView')

window.addEventListener('DOMContentLoaded', onLoad)
function onLoad() {
PoPState()
let links = document.querySelectorAll('li a[href]')
links.forEach(a => {
a.addEventListener('click', (e) => {
e.preventDefault()
history.pushState(null, '', a.getAttribute('href'))
PoPState()
})
})
}
onLoad()
function PoPState() {
console.log(location.pathname)
switch(location.pathname) {
case '/home':
routerView.innerHTML = '<h2>home page</h2>'
return
case '/about':
routerView.innerHTML = '<h2>about page</h2>'
return
default:
return
}
}
</script>
</body>
</html>

设置history模式

vue2 中设置 mode:history

vue3 中设置 createWebHistory

1
2
3
4
5
6
import { createWebHistory } from 'vue-router'

const router = createRouter({
history: createWebHistory(),
routes
});

实际项目中对两种模式的使用

我们看 vue3 的官网

点击左侧的章节,中间部分会被整体替换,使用的是 history 模式。url 如下

https://cn.vuejs.org/guide/introduction.html

点击右侧的本页目录,中间部分不会替换,而是滚动到对应位置,使用的是 hash 模式。url 如下

https://cn.vuejs.org/guide/typescript/composition-api.html#typing-component-props

注意:上面写的是错的,这里其实根本不是 hash,而是 html 自带的锚点。

现在感觉大部分的网页都使用的是 history,可能是因为hash对 SEO 不友好?

参考

https://juejin.cn/post/7116336664540086286#heading-7

https://juejin.cn/post/7096034733649297421

https://juejin.cn/post/7127143415879303204#heading-5


VueRouter的hash和history模式
http://example.com/2022/08/28/VueRouter的hash和history模式/
Author
John Doe
Posted on
August 28, 2022
Licensed under