版权声明:本文为吴孔云博客原创文章,转载请注明出处并带上链接,谢谢。 https://cloud.tencent.com/developer/article/1347525
前端路由:客户端浏览器可以不依赖服务端,根据不同的URL渲染不同的视图页面。
我们经常在 url 中看到 #,这个 # 有两种情况,一个是我们所谓的锚点,比如典型的回到顶部按钮原理、Github 上各个标题之间的跳转等,路由里的 # 不叫锚点,我们称之为 hash,大型框架的路由系统大多都是哈希实现的。
同样我们需要一个根据监听哈希变化触发的事件 —— hashchange 事件
我们用 window.location 处理哈希的改变时不会重新渲染页面,而是当作新页面加到历史记录(session history)中,这样我们跳转页面就可以在 hashchange 事件中注册 ajax 从而改变页面内容。
利用hash值前端路由的简单实现
//index.html
<ul>
<li><a href='#blue'>blue</a></li>
<li><a href='#yellow'>yellow</a></li>
<li><a href='#red'>red</a></li>
</ul>
//js
function Router() {
this.routes = {};
//初始化load一次
window.addEventListener('load', this.refresh.bind(this), false);
window.addEventListener('hashchange', this.refresh.bind(this), false);
}
Router.prototype.route = function (path, callback) {
this.routes[path] = callback || function () {};
};
Router.prototype.refresh = function () {
this.currentHash = location.hash.slice(1) || '/';
typeof this.routes[this.currentHash] === 'function' && this.routes[this.currentHash]();
};
var router = new Router();
router.route('blue', function () {
document.body.style.backgroundColor = 'blue';
});
router.route('yellow', function () {
document.body.style.backgroundColor = 'yellow';
});
router.route('red', function () {
document.body.style.backgroundColor = 'red';
})
对于低版本的浏览器,例如IE6,7等等,不支持 hashchange 事件。这个时候我们只能通过 setInterval 设置心跳的方式去模拟 hashchange。
(function(window) {
// 如果浏览器不支持原生实现的事件,则开始模拟,否则退出。
if ( "onhashchange" in window.document.body ) { return; }
var location = window.location,
oldURL = location.href,
oldHash = location.hash;
// 每隔100ms检查hash是否发生变化
setInterval(function() {
var newURL = location.href,
newHash = location.hash;
// hash发生变化且全局注册有onhashchange方法(这个名字是为了和模拟的事件名保持统一);
if ( newHash != oldHash && typeof window.onhashchange === "function" ) {
// 执行方法
window.onhashchange({
type: "hashchange",
oldURL: oldURL,
newURL: newURL
});
oldURL = newURL;
oldHash = newHash;
}
}, 100);
})(window);
history 的方法可以查看这篇文章:http://blog.csdn.net/wkyseo/article/details/51699770
重点说其中的两个新增的API history.pushState 和 history.replaceState
这两个 API 都接收三个参数,分别是
相同之处是两个 API 都会操作浏览器的历史记录,而不会引起页面的刷新。
不同之处浏览器针对每个页面维护一个History栈。执行pushState函数可压入设定的url至栈顶,同时修改当前指针;当执行back操作时,history栈大小并不会改变(history.length不变),仅仅移动当前指针的位置;若当前指针在history栈的中间位置(非栈顶),此时执行pushState会改变history栈的大小。总结pushState的规律,可发现当前指针在history栈顶部时执行pushState,会增加history栈大小;若current指针不在栈顶则会在当前指针所在位置添加项。执行back操作并不修改history栈大小,因此可以通过back和forward在当前大小的history栈中自由移动。replaceState则仅会替换当前的历史记录。
改变history栈的current指针都会触发popstate事件,但是pushstate不会触发popstate事件,虽然current指针会在栈顶,并且改变history的length大小,但是切记pushstate不会触发popstate事件,之前写代码的时候逻辑混乱,导致触发不了。
写了个demo,点击不同的导航,内容区相应切换,并且history推入一条记录,可实现浏览器的后退和书签保存功能。
//html
<ul class="header">
<li><a href="#nav1">nav1</a></li>
<li><a href="#nav2">nav2</a></li>
<li><a href="#nav3">nav3</a></li>
<li><a href="#nav4">nav4</a></li>
<li><a href="#nav5">nav5</a></li>
</ul>
<div class="content">
<div style="display: none">nav1内容区域</div>
<div style="display: none">nav2内容区域</div>
<div style="display: none">nav3内容区域</div>
<div style="display: none">nav4内容区域</div>
<div style="display: none">nav5内容区域</div>
</div>
//js
var eleMenus = document.querySelector('.header');
function FrontRouter() {
window.addEventListener('load', this.refresh, false);
if(history.pushState) {
window.addEventListener('popstate', this.refresh, false);
}
}
FrontRouter.prototype.refresh = function () {
var hash = location.hash;
var eleTarget;
if(!hash) {
eleTarget= eleMenus.querySelector('li a');
history.replaceState(null, null, location.href + '#' + eleTarget.href.split('#')[1]);
eleTarget.click();
}else {
var eleLinks = eleMenus.querySelectorAll('li a');
for(let i=0, len=eleLinks.length; i<len; i++) {
if(eleLinks[i].getAttribute('href') === hash) {
eleTarget = eleLinks[i];
}
}
//未找到重新遍历,递归自身
if(!eleTarget) {
history.replaceState(null, null, location.href.split("#")[0]);
FrontRouter.prototype.refresh();
}else {
eleTarget.click();
}
}
};
var router = new FrontRouter();
/*
* Node原型小方法
*/
Object.defineProperties(Node.prototype, {
//取元素的当前索引
n_getIndex: {
value: function () {
if (this.parentElement) {
var list = this.parentElement.children;
for (var i = 0; i < list.length; i++) {
if (list[i] === this) {
return i;
}
}
}
}
},
//取当前元素的同辈元素, 是个数组
n_siblings: {
value: function () {
var par = this.parentElement;
if (par) {
var list = par.children,
len = list.length,
arr = [];
for (var i = 0; i < len; i++) {
if (list[i] !== this) {
arr.push(list[i]);
}
}
return arr;
}
}
},
//判断是否含有class
n_hasClass: {
value: function (name) {
name = ' ' + name.trim() + ' ';
return (new RegExp(name, 'm')).test(' ' + this.className + ' ');
}
},
//添加class
n_addClass: {
value: function (name) {
var curNames = this.className.trim();
name = name.trim();
if (curNames) {
if ((' ' + curNames + ' ').indexOf(' ' + name + ' ') === -1) {
this.className = curNames + ' ' + name;
}
} else {
this.className = name;
}
return this;
}
},
//移除class
n_removeClass: {
value: function (name) {
var curNames = this.className.trim();
if (curNames) {
name = ' ' + name.trim() + ' ';
this.className = (' ' + curNames + ' ').replace(new RegExp(name, 'gm'), ' ').trim();
}
return this;
}
}
});
//nav的点击切换函数
eleMenus.addEventListener('click', function (e) {
if(e.target.tagName.toUpperCase() === 'A') {
var eleTarget = e.target, liTarget = eleTarget.parentElement;
var targetIndex = liTarget.n_getIndex();
if(!liTarget.n_hasClass('selected')) {
liTarget.n_addClass('selected');
var _siblings = liTarget.n_siblings();
for(let i=0, len=_siblings.length; i<len; i++) {
_siblings[i].n_removeClass('selected');
}
var contentLists = document.querySelectorAll('.content div');
for(let i=0, len = contentLists.length; i<len; i++) {
contentLists[i].style.display = 'none';
}
contentLists[targetIndex].style.display = 'block';
}
//push history stack
if(history.pushState && location.hash !== eleTarget.getAttribute('href')) {
var title = e.target.innerHTML;
history.pushState({title: title}, title, location.href.split('#')[0] + '#' +eleTarget.getAttribute('href').substring(1));
}
e.preventDefault();
}
}, false);
//css
<style>
*{margin: 0;padding: 0}
.header{
width: 500px;
height: 50px;
margin: 50px auto 0;
border: 1px solid #CCCCCC;
border-bottom: none;
font-size: 0;
}
.header li {
display: inline-block;
width: 100px;
box-sizing: border-box;
border-right: 1px solid #CCCCCC;
border-bottom: 1px solid #CCCCCC;
font-size: 14px;
line-height: 50px;
text-align: center;
cursor: pointer;
}
.header li a {
display: inline-block;
width: 100%;
height: 100%;
}
.header li:last-child{
border-right: none;
}
.header li.selected{
border-bottom: none;
}
.header li.selected a {
color: blue;
}
.content {
width: 500px;
height: 300px;
margin: 0 auto;
border: 1px solid #CCCCCC;
border-top: none;
}
.content>div{
padding: 50px;
}
</style>
前端路由主要应用在SPA单页面上,现在很多框架比如Angular、React、Vue都有router插件,大体都是利用这两个方法来实现,当然会复杂很多。个人倾向于hash的方式。history的兼容性要IE9及以上。
这个链接的 demo 含有判断方法:http://sandbox.runjs.cn/show/… 。同时给出 Github 仓库地址: minrouter,推荐大家读下源码,仅仅 117 行,精辟!