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