跳到主要内容

路由

什么是路由?

路由的概念起源于服务端,在以前前后端不分离的时候,由后端来控制路由,当接收到客户端发来的 HTTP 请求,就会根据所请求的相应 URL,来找到相应的映射函数,然后执行该函数,并将函数的返回值发送给客户端。对于最简单的静态资源服务器,可以认为,所有 URL 的映射函数就是一个文件读取操作。对于动态资源,映射函数可能是一个数据库读取操作,也可能是进行一些数据的处理等等。然后根据这些读取的数据,在服务器端就使用相应的模板来对页面进行渲染后,再返回渲染完毕的页面。它的好处与缺点非常明显:

  • 好处:安全性好,SEO 好;
  • 缺点:加大服务器的压力,不利于用户体验,代码冗合不好维护;

也正是由于后端路由还存在着自己的不足,前端路由才有了自己的发展空间。**对于前端路由来说,路由的映射函数通常是进行一些 DOM 的显示和隐藏操作。**这样,当访问不同的路径的时候,会显示不同的页面组件。前端路由主要有以下两种实现方案:

  • Hash
  • History

当然,前端路由也存在缺陷:使用浏览器的前进,后退键时会重新发送请求,来获取数据,没有合理地利用缓存。但总的来说,现在前端路由已经是实现路由的主要方式了,前端主流框架都是基于前端路由进行开发

Hash模式

早期的前端路由的实现就是基于 location.hash 来实现的

image-20221113110207076

http://127.0.0.1:5500/index.html#download这个网址的哈希location.hash就是#download

hash 存在下面几个特性:

  • URLhash 值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash 部分不会被发送。
  • hash 值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash 的切换。
  • 我们可以使用 hashchange 事件来监听 hash 的变化。

我们可以通过两种方式触发 hash 变化,一种是通过 a 标签,并设置 href 属性,当用户点击这个标签后,URL 就会发生改变,也就会触发 hashchange 事件了

<a href="#search">search</a>

还有一种方式就是直接使用 JavaScript来对 loaction.hash 进行赋值,从而改变 URL,触发 hashchange 事件:

location.hash="#search"

另外使用location.replace替换当前的地址,hash变化后也能被hashchange监听到

全部测试代码如下:

<!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">
<link data-rh="true" rel="icon" href="/img/logo/favicon.ico">
<title>Document</title>
<style>
.link-box {
display: flex;
gap: 20px;
background-color: aliceblue;

}
</style>
</head>

<body>
<div>
<div class="link-box">
<div><a href="#">Home</a></div>
<div><a href="#guide">guide</a></div>
<div><a href="#download">download</a></div>
<div><a href="#any">any</a></div>
</div>
<hr />
<button onclick="History.go(-1)">后退</button> | <button onclick="History.go(1)">前进</button>
<hr />
<input id="hashAssignment" placeholder="hash赋值" type="text">
<input id="togglePage" placeholder="替换页面" type="text">
<hr />
<div id="content"></div>
</div>
<script>
class HashRouter {
constructor(list, ELEMENT) {
this.list = list;
this.ELEMENT = ELEMENT
this.handler();
// 监听 hashchange 事件
window.addEventListener('hashchange', e => {
this.handler();
});
}
render(state) {
let ele = this.list.find(ele => ele.path === state);
ele = ele ? ele : this.list.find(ele => ele.path === '*');
this.ELEMENT.innerText = ele.cmp;
}
// hash 改变时,重新渲染页面
handler() {
this.render(this.getState());
}
// 获取 hash 值
getState() {
const hash = window.location.hash;
return hash ? hash.slice(1) : '/';
}
// push 新的页面
push(path) {
window.location.hash = path;
}
// 获取 默认页 url
getUrl(path) {
const href = window.location.href;
const i = href.indexOf('#');
const base = i >= 0 ? href.slice(0, i) : href;
return base + '#' + path;
}
// 替换页面:通过指定URL替换当前缓存历史(客户端)的项目,因此当使用replace方法后,不能再使用“前进” “后退”来访问已经被替换的URL
replace(hash) {
window.location.replace(this.getUrl(hash));
}
// 赋值
assignment(hash) {
window.location.hash = `#${hash}`
}
// 前进 or 后退浏览历史
go(n) {
window.history.go(n);
}
}
const list = [{
path: "/",
cmp: "Home cmp"
}, {
path: "guide",
cmp: "guide cmp"
}, {
path: "download",
cmp: "download cmp"
}, {
path: "*",
cmp: "404"
}]
const History = new HashRouter(list, document.getElementById('content'))
document.getElementById('hashAssignment').oninput = e => History.assignment(e.target.value)
document.getElementById('togglePage').oninput = e => History.replace(e.target.value)
</script>
</body>

</html>

最终效果如下:

20221113-004036

History模式

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

window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);

history 存在下面几个特性:

  • pushStaterepalceState 的标题(title):一般浏览器会忽略,最好传入 null
  • 我们可以使用 popstate 事件来监听前进后退时 url 的变化;
  • history.pushState()history.replaceState() 不会触发 popstate 事件,这时我们需要手动触发页面渲染;

全部测试代码如下:

<!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">
<link data-rh="true" rel="icon" href="/img/logo/favicon.ico">
<title>Document</title>
<style>
.link-box {
display: flex;
gap: 20px;
background-color: aliceblue;

}
</style>
</head>

<body>
<div>
<div class="link-box">
<div><a href="/">Home</a></div>
<div><a href="/guide">guide</a></div>
<div><a href="/download">download</a></div>
<div><a href="/any">any</a></div>
</div>
<div class="link-box">
<div><a onclick="push('/')">Home</a></div>
<div><a onclick="push('/guide')">guide</a></div>
<div><a onclick="push('/download')">download</a></div>
<div><a onclick="push('/any')">any</a></div>
</div>
<hr />
<button onclick="History.go(-1)">后退</button> | <button onclick="History.go(1)">前进</button>
<hr>
<input id="addHistory" placeholder="新增History" type="text"><br>
<input id="replaceHistory" placeholder="替换History" type="text">
<hr />
<div id="content"></div>
</div>
<script>
class HistoryRouter {
constructor(list, ELEMENT) {
this.list = list;
this.ELEMENT = ELEMENT
this.handler();
// 监听 popstate 事件(前进和后退触发)
window.addEventListener('popstate', e => {
console.log('触发 popstate。。。', e);
this.handler();
});
}
render(state) {
let ele = this.list.find(ele => ele.path === state);
ele = ele ? ele : this.list.find(ele => ele.path === '*');
this.ELEMENT.innerText = ele.cmp;
}
// 渲染页面
handler() {
this.render(this.getState());
}
// 获取 url
getState() {
const path = window.location.pathname;
return path ? path : '/';
}
// push 页面
push(path) {
history.pushState(null, null, path || '/');
this.handler();
}
// replace 页面
replace(path) {
history.replaceState(null, null, path || '/');
this.handler();
}
// 前进 or 后退浏览历史
go(n) {
window.history.go(n);
}
}
const list = [{
path: "/",
cmp: "Home cmp"
}, {
path: "/guide",
cmp: "guide cmp"
}, {
path: "/download",
cmp: "download cmp"
}, {
path: "*",
cmp: "404"
}]
const History = new HistoryRouter(list, document.getElementById('content'))
document.getElementById('addHistory').oninput = e => History.push(e.target.value)
document.getElementById('replaceHistory').oninput = e => History.replace(e.target.value)
const push = (path) => {
History.push(path)
}
</script>
</body>

</html>

最终效果如下:

20221113-002953

服务端配置

如果使用Live Server,我们切换路由会找不到文件,这里是将index.html直接放到了服务器,配置nginx:try_files $uri $uri/ /index.html;,每次的匹配到请求重新返回index.html,上面的测试里使用pushState跳转页面无刷新,不会请求服务端,而使用a标签后页面会刷新并请求服务端

本地测试的一种方式是:利用脚手架自带的node服务,如vite,创建一个项目,然后拿以上代码替换项目的index.html就可以看到同样的测试结果了

image-20221113105909429

两种路由模式的对比

对比点Hash 模式History 模式
美观性带着 # 字符,较丑简洁美观
兼容性>= ie 8,其它主流浏览器>= ie 10,其它主流浏览器
实用性不需要对服务端做改动需要服务端对路由进行相应配合设置