赞
踩
上一节了解了单页面,那么你知道前端单页面路由是怎么实现的吗?
说真的,我也不知道
接下来一起学习一下。
因为缺少路由系统的 SPA 框架有其存在的弊端:
主流的前端路由系统是通过 hash 或 history 来实现的,下面先看看 Hash 模式
hash 就是指 url 后的 # 号以及后面的字符。例如:https://blog.csdn.net/#kaimo313,那么 #kaimo313 就是 hash 值。
我们知道 url 上的 hash 本意是用来作锚点的,方便用户在一个很长的文档里进行上下的导航。
这是因为 hash 有一点很特殊:改变 url 的 hash 时,不会刷新页面,但是会触发相应的 hashchange 回调函数,使得 hash 能用来做路由控制。
下面我们实现一个功能:通过改变 hash,去改变 body 的背景色
效果如下:
实现的代码如下:
<!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</title> </head> <body> <ul> <li><a href="#/">turn white</a></li> <li><a href="#/blue">turn blue</a></li> <li><a href="#/green">turn green</a></li> </ul> <script> // 改变 body 颜色 function changeBodyColor(color) { document.body.style.backgroundColor = color; } function Router() { this.routes = {}; // routes 用来存放不同路由对应的回调函数 this.currentUrl = ""; } // 每个不同的hash值,对应不同的函数调用处理。 Router.prototype.route = function(path, callback) { this.routes[path] = callback || function() {}; } Router.prototype.refresh = function() { console.log('触发一次 hashchange,hash 值为:', location.hash); this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl](); } // init 用来初始化路由,在 load 事件发生后刷新页面,并且绑定 hashchange 事件,当 hash 值改变时触发对应回调函数 Router.prototype.init = function() { // 页面在第一次加载时并不会触发hashchang事件,只有改变了hash值后才会触发。 window.addEventListener("load", this.refresh.bind(this), false); window.addEventListener("hashchange", this.refresh.bind(this), false); } let hashRouter = new Router(); hashRouter.init(); hashRouter.route("/", function() { changeBodyColor("white"); }) hashRouter.route("/blue", function() { changeBodyColor("blue"); }) hashRouter.route("/green", function() { changeBodyColor("green"); }) </script> </body> </html>
或者使用类似发布订阅模式的方式,使用 ES6 的 class 实现:
<!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="#/">turn white</a></li> <li><a href="#/blue">turn blue</a></li> <li><a href="#/green">turn green</a></li> </ul> <script> class Router { constructor() { this.routes = {}; this.currentUrl = ""; } // 改变 body 颜色 changeBodyColor(color) { document.body.style.backgroundColor = color; } route(path, callback) { this.routes[path] = callback || function() {}; } refresh() { console.log('触发一次 hashchange,hash 值为:', location.hash); this.currentUrl = location.hash.slice(1) || '/'; this.routes[this.currentUrl](); } // init 里面需要添加 load hashchange 事件,触发刷新方法 init() { window.addEventListener("load", this.refresh.bind(this), false); window.addEventListener("hashchange", this.refresh.bind(this), false); } } let hashRouter = new Router(); hashRouter.init(); hashRouter.route("/", function() { hashRouter.changeBodyColor("white"); }) hashRouter.route("/blue", function() { hashRouter.changeBodyColor("blue"); }) hashRouter.route("/green", function() { hashRouter.changeBodyColor("green"); }) </script> </body> </html>
我们可以发现 hash 模式的情况下 url 上会有一个 # 号,很不美观,有没有什么解决方案?答案就是使用 history 模式。
那么什么是 history 模式?
我们可能使用过 history 的部分 api 比如:
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
在 HTML5 规范中,history 新增了以下几个 API
history.pushState(); // 添加新的状态到历史状态栈
history.replaceState(); // 用新的状态代替当前状态
history.state // 返回当前状态对象
下面具体了解一下 pushState,state,replaceState,popstate 事件。
History.pushState()
方法用于在历史中添加一条记录。
window.history.pushState(state, title, url)
举个例子:
假定当前网址是 history.html
,使用 pushState()
方法在浏览记录(History 对象)中添加一个新记录。
<!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>history1 页面</title>
</head>
<body>
<script>
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', 'history2.html');
</script>
</body>
</html>
我们发现添加新记录后,浏览器地址栏立刻显示 history2.html
,但并不会跳转到 history2.html
,甚至也不会检查 history2.html
是否存在,它只是成为浏览历史中的最新记录。
然后我们在 url 栏里输入 https://blog.csdn.net/kaimo313
我的博客地址,回车之后进入到我的博客之后,在点击返回上一次浏览的页面
我们会发现,此时的页面已经变成了 history2.html
。
总之,pushState()
方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会有反应。
注意:
如果 pushState 想要插入一个跨域的网址,导致报错。这样设计的目的是,防止恶意代码让用户以为他们是在另一个网站上,因为这个方法不会导致页面跳转。
使用 pushState 方法之后,就可以用 History.state
属性读出状态对象。
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', 'history2.html');
console.log('history.state:', history.state);
History.replaceState()
方法用来修改 History 对象的当前记录,其他都与 pushState()
方法一模一样。
我们来举个例子:假定当前网页是 kaimo.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>kaimo</title>
</head>
<body>
</body>
</html>
第1步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html?page=1
history.pushState({page: 1}, 'kaimo 1', '?page=1')
第2步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html?page=2
history.pushState({page: 2}, 'kaimo 2', '?page=2')
第3步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html?page=3
history.replaceState({page: 3}, 'kaimo 3', '?page=3')
第4步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html?page=1
,回退之后变成page=1了。
history.back()
第5步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html
,再次回退之后变成初始页了。
history.back()
第6步执行: URL 显示为 http://127.0.0.1:5500/kaimo.html?page=3
。
history.go(2) // 前进两页
每当同一个文档的浏览历史(即history对象)出现变化时,就会触发popstate事件。
注意,仅仅调用
pushState()
方法或replaceState()
方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()
、History.forward()
、History.go()
方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。
使用的时候,可以为popstate事件指定回调函数。
window.onpopstate = function (event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
};
// 或者
window.addEventListener('popstate', function(event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
});
举个例子:比如从 kaimo.html
跳转到 kaimo.html?page=1
<!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>kaimo</title> </head> <body> <script> window.addEventListener('popstate', function(event) { console.log('location: ' + document.location); console.log('state: ' + JSON.stringify(event.state)); console.log('state2: ' + JSON.stringify(history.state)); }); </script> </body> </html>
回调函数的参数是一个 event 事件对象,它的 state 属性指向 pushState 和 replaceState 方法为当前 URL 所提供的状态对象(即这两个方法的第一个参数)。这个 state 对象也可以直接通过 history.state
对象读取。
注意,页面第一次加载的时候,浏览器不会触发popstate事件。
通过上面的分析我们知道 history.pushState
或者 history.replaceState
也能做到改变 url 的同时,不会刷新页面。
hash 的改变会触发 onhashchange 事件,但是我们无法监听到 history 的改变事件。
那有没有什么方法实现?
答案是有的,如果我们能罗列出所有可能改变 history 的途径,然后在这些途径 一 一 进行拦截,也一样相当于监听了 history 的改变。
url 的改变只能由以下 3 种途径引起:
对于第 1 种,HTML5 规范中新增了一个 onpopstate
事件,通过它便可以监听到前进或者后退按钮的点击。
对于第 2 和 第 3 种,可以看成是一种,因为 a 标签的默认事件可以被禁止,进而调用 JS 方法。
思路图:
下面是我写的简易版本,实现了三种途径修改 url 渲染页面的效果
<!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>history 路由</title> </head> <body> <ul> <li><a href="/kaimo1">kaimo 1</a></li> <li><a href="/kaimo2">kaimo 2</a></li> <li><a href="/kaimo3">kaimo 3</a></li> </ul> <hr> <button onclick="kaimoBtnClick('/kaimo1')">kaimo 1</button> <button onclick="kaimoBtnClick('/kaimo2')">kaimo 2</button> <button onclick="kaimoBtnClick('/kaimo3')">kaimo 3</button> <hr> <div id="route-view"></div> <script> class HistoryRouter { constructor() { this.routeView = document.getElementById('route-view'); } init() { // 处理途径1 window.addEventListener("DOMContentLoaded",() => { // 获取所有带 href 属性的 a 标签节点 var aList = document.querySelectorAll('a[href]') // 遍历 a 标签节点数组,阻止默认事件,添加点击事件回调函数 aList.forEach(aNode => aNode.addEventListener('click', e => { e.preventDefault() // 阻止a标签的默认事件 var href = aNode.getAttribute('href') // 通过 history.pushState 手动修改地址栏 window.history.pushState(null, '', href) console.log('处理途径1'); this.forceUpdate(); })); }); // 处理途径3 window.addEventListener('popstate', () => { console.log('处理途径3'); this.forceUpdate(); }); } // 强制渲染 forceUpdate() { console.log('强制渲染', location.pathname); switch(location.pathname) { case '/kaimo2': this.routeView.innerHTML = '这里是 kaimo2 对应的页面' return case '/kaimo3': this.routeView.innerHTML = '这里是 kaimo3 对应的页面' return default: this.routeView.innerHTML = '这里是 kaimo1 对应的页面' return } } historyPush(path) { window.history.pushState({}, null, path); this.forceUpdate(); } } let historyRouter = new HistoryRouter(); historyRouter.init(); // 处理途径2 function kaimoBtnClick(path) { // 通过 history.pushState 手动修改地址栏 window.history.pushState(null, '', path); console.log('处理途径2'); historyRouter.forceUpdate(); } </script> </body> </html>
这里需要注意的是:不能在浏览器直接打开静态文件,需要通过 web 服务,启动端口去浏览网址。默认打开的协议是 file 协议,它是不会被 popstate 监听的。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。