React 和 Vue 中都有 HashRouter 和 Router;本文用来理清路由的概念,并且使用原生 JS 实现 hash 路由 和history 路由;文章的最后是对 History 对象 和 window.history 的简要介绍,以便于理解 history 路由;
1、概述
在以前的多页应用(MPA)中,路由主要由后端实现,并且每次访问新内容都要重新刷新整个页面,这对服务端造成了性能压力,也影响浏览器上用户体验;
于是在现有的前端实践中,出现了前端路由的方案,前端路由使得每次获取新内容时只需要部分刷新页面,而不需要刷新整个页面,这也就构成了所谓单页应用(SPA)的概念;
在现有前端框架中都有 HashRouter 和 Router 两种前端路由实现方案,其中 HashRouter 是基于 hashchange
事件实现的(被称为hash路由),Router 是基于 history 对象实现的(被称为history路由);
hash路由在浏览器上的表现形式为 URL 为带 片段标识符(#
)的 URL ;
history 路由在浏览器上的表现形式为 URL 和普通的远程资源的 URL(不带锚点#
)的形式一致;
2、hash 路由的原生实现
hash URL 的特点:
- URL 上带有锚点(
#
); - hash URL变化时浏览器不向服务器发送请求,页面不会跳转;
- 通过 window.location.hash 可以获取和修改当前页面 URL 的 片段标识符;
实现 hash 路由的基本思路:
- 通过
hashchange
事件监听 hash 路由的变化; - 通过 xhr 对象获取服务器的资源;
- 将资源渲染到页面上;
hash 路由的原生 JS 实现:
1、html文件:
<!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>Hash Router 和 history router 的实现</title>
<style>
.navbar {
display:flex;
justify-content:space-around;
margin:0 auto;
width:300px;
}
#container {
width:400px;
text-align: center;
margin:0 auto;
margin-top:100px;
}
</style>
</head>
<body>
<div class="navbar">
<div class="navbar_item"><a href="#/page1" class="page1">page1</a></div>
<div class="navbar_item"><a href="#/page2" class="page2">page2</a></div>
<div class="navbar_item"><a href="#/page3" class="page3">page3</a></div>
</div>
<div id="container"></div>
<script type="text/javascript" src="./hashRouter.js"></script>
</body>
</html>
2、JS 文件:
// hashRouter.js
class HashRouter {
constructor(dom) {
this.hashObj = {};
window.addEventListener('hashchange',(e) => {
const path = e.newURL.split('#')[1];
if(path && this.hashObj[path]) {
const data = this.getData(path);
this.appendData(dom,data);
}
})
}
// 注册路由
register(...paths){
paths.forEach(path => {
this.hashObj[path] = path;
})
}
// 获取数据
getData(path) {
const url = `https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage${path.slice(-1)}data`
let xhr = new XMLHttpRequest();
xhr.open('GET',url,false)
xhr.send(null);
if(xhr.status === 200) {
return JSON.parse(xhr.responseText);
}
return {};
}
// 渲染数据
appendData(dom,data) {
let containerDom = document.querySelector(dom);
containerDom.innerHTML = '';
let titleDom = document.createElement('h1');
titleDom.innerText = data.title;
let dataDom = document.createElement('div');
dataDom.innerText = data.data;
let descDom = document.createElement('div');
descDom.innerText = data.desc;
containerDom.append(titleDom,dataDom,descDom);
}
}
const hashRouter = new HashRouter('#container');
hashRouter.register('/page1','/page2','/page3');
使用到的数据接口(由fastmock建立)
- https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage1data
- https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage2data
- https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage3data
3、history 路由的原生实现
普通的远程资源的 URL 的特点:
- URL 上不带有片段标识符(
#
); - URL 变化时浏览器会向服务器发送请求,重新渲染整个页面;
- 通过
window.history
属性指向的 History 对象可以控制浏览器展示变化的 URL;
实现 history 路由的基本思路:
- 监听 a 标签上访问普通URL的点击事件,阻止默认事件来阻止页面跳转和重新渲染;
- 通过
window.history
改变浏览器显示的 URL; - 通过 xhr 对象获取服务器的资源;
- 将资源渲染到页面上;
history 路由的原生 JS 实现:
1、html文件
<!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>Hash Router 的实现</title>
<style>
.navbar {
display:flex;
justify-content:space-around;
margin:0 auto;
width:300px;
}
#container {
width:400px;
text-align: center;
margin:0 auto;
margin-top:100px;
}
</style>
</head>
<body>
<div class="navbar">
<div class="navbar_item"><a href="/page1" class="page1">page1</a></div>
<div class="navbar_item"><a href="/page2" class="page2">page2</a></div>
<div class="navbar_item"><a href="/page3" class="page3">page3</a></div>
</div>
<div id="container"></div>
<script type="text/javascript" src="./historyRouter.js"></script>
</body>
</html>
2、JS 文件
// historyRouter.js
class HistoryRouter {
constructor(dom) {
this.historyObj = {};
this.dom = dom;
this.listenClickOfa();
this.listenPopstate();
}
// 注册路由
register(...paths){
paths.forEach(path => {
this.historyObj[path] = path;
})
}
// 监听 a 标签上的click 事件
listenClickOfa() {
window.addEventListener('click',(e) => {
const path = e.target.href;
if(e.target.tagName === 'A' && path && this.historyObj['/' + path.split('/').slice(-1)]) {
e.preventDefault();
// 变换 URL
this.updateUrl(path);
// 获取数据
const data = this.getData(path);
// 渲染数据
this.appendData(data);
}
})
}
// 获取数据
getData(path) {
const url = `https://www.fastmock.site/mock/ba90f15d75470108ed2c56997636d24d/routertest/api/getPage${path.slice(-1)}data`
let xhr = new XMLHttpRequest();
xhr.open('GET',url,false)
xhr.send(null);
if(xhr.status === 200) {
return JSON.parse(xhr.responseText);
}
return {};
}
// 通过 window.history 切换浏览器显示的 url
updateUrl(path) {
window.history.pushState({path:path},null,path);
}
// 渲染数据
appendData(data) {
let containerDom = document.querySelector(this.dom);
containerDom.innerHTML = '';
let titleDom = document.createElement('h1');
titleDom.innerText = data.title;
let dataDom = document.createElement('div');
dataDom.innerText = data.data;
let descDom = document.createElement('div');
descDom.innerText = data.desc;
containerDom.append(titleDom,dataDom,descDom);
}
// 通过监听 popstate 事件来处理 history.back()或 history.forward() 时的页面渲染
listenPopstate() {
window.addEventListener('popstate',e => {
const data = this.getData(e.state.path);
this.appendData(data);
})
}
}
const historyRouter = new HistoryRouter('#container');
historyRouter.register('/page1','/page2','/page3');
本文代码仓库:https://gitee.com/hotpotliuyu/myStudyForToBeAFrontendDeveloper.git
下面的内容是对 History 对象 和 window.history 的简要介绍;
4、History 对象
1、概述
window.history 属性指向一个 History 对象,它表示当前窗口的浏览历史;其中历史URL因为安全原因被浏览器设置为不可见,只显示 length
,scrollRestoration
,state
三个属性;
window.history instanceof History; // true
History 对象的结构如下:
History {
length: 5, // 此 History 对象有5个URL历史,但是浏览器默认对其隐藏
scrollRestoration: "auto",
state: {url: "66666"},
[[Prototype]]: History
}
2、属性
-
History.length
-
History.state
表示 History 中最上层网址的一些状态,使用
History.pushState()
和History.replaceState()
可更改 state;
eg:
window.history.length // 当前窗口访问过多少个网页
window.history.state // 当前窗口 History 对象的当前状态
3、方法
-
History.forward
-
History.back
-
History.go
eg:
window.history.forward(); // 当前窗口移动到上一个网页 window.history.back(); // 当前窗口移动到下一个网页 window.history.go(num); // 当前窗口跳转到指定网页
-
History.pushState(state,title,url)
用于在 History 对象中添加一条记录;
- state:一个与添加的记录相关联的的状态对象,会成为 History 对象中的 state 属性;
- title:修改文档的 title 属性,但是现在的浏览器大多没有使用这个参数;
- url:页面 url;
eg:
window.history.pushState({url:'http://hotpotliu.com'},null,'http://hotpotliu.com')
-
History.replaceState(state,title,url)
用来修改 History 对象的当前记录(History对象的最上层网页);
4、popstate 事件
每当同一个文档的浏览历史(即 history 对象)出现变化时,就会触发 popstate
事件;
页面第一次加载的时候,浏览器不会触发 popstate
事件;
eg:
window.addEventListener('popstate',function(event){
console.log(event.state);
// event.state 为history对象中对应URL的state;可以通过 History.pushState() 和 History.replaceState() 修改
})
参考资料: