所以前端web路由需要实现以下目标:
(1)能根据页面URL来获取不同的模块,但不发起新的页面请求;
(2)能监听URL的变化。
而hash和history这两种模式便是其实现原理。
URL的hash属性是一个可读可写的字符串,该字符串是URL的锚部分(即#后面的部分)。例如http://abc.com/#fragment,fragment便是hash值。
'#'是用来指导浏览器动作的,对服务器完全无用,其值的改变不会导致浏览器发起http请求,也不会引起页面的重载。但每次hash值的改变,都会在浏览器的访问历史栈里增加一个记录,使用'后退'键便能返回上一个位置。在H5的history模式出现之前,hash是前端路由的实现方式。
核心API:
1、window.location.hash
是个可读可写属性,读取时可以校验hash的变化,写入时可以不重载页面修改浏览器记录
2、onhashchange事件
这是一个H5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。
实现方式如下:
window.addEventListener('onhashchange', func, false);
当浏览器不兼容时,可以用setInterval监控location.hash的变化。
核心功能的简单实现:
首先要实现一个router对象来管理页面的回调,
class HashRouter{
constructor(routeArr = []){
// 管理页面的回调
this.routers = {};
routeArr.forEach(item => this.register(item));
}
// 注册
register(item){
const {name, content} = item;
this.routers[name] = typeof content === 'function' ? content : function(){};
}
}
然后添加hashchange事件的监听,定义事件触发时的回调函数,
class HashRouter{
constructor(routeArr = []){
// 管理页面的回调
this.routers = {};
routeArr.forEach(item => this.register(item));
window.addEventListener('hashchange',this.load.bind(this),false);
}
// 注册
register(item){
const {name, content} = item;
this.routers[name] = typeof content === 'function' ? content : function(){};
}
load(){
let hash = location.hash.slice(1),
handler = this.routers[hash];
// 执行注册的回调函数
try{
handler.apply(this);
}catch(e){
console.error(e);
}
}
}
最后添加上对象的初始化和页面内容,
const container = document.getElementById('container');
const routeArr = [
{name: 'index', content: ()=> container.innerHTML = '这是首页'},
{name: 'about', content: ()=> container.innerHTML = '这是关于页'},
{name: 'detail', content: ()=> container.innerHTML = '这是详情页'}
];
cosnt router = new HashRouter(routeArr);
<body>
<div id="header">
<a href="#index">index</a>
<a href="#about">about</a>
<a href="#detail">detail</a>
</div>
<div id="container"></div>
</body>
当点击页面上的按钮时,页面内容便会变换,这样就基本介绍了hash模式下路由的实现原理。接下来介绍一下history模式。
history接口允许操作浏览器曾经在标签页或者框架里访问的会话历史记录。在H5之前其实存在history接口了,但只是用于页面的跳转,比如:
history.go(-1); // 后退一页
history.go(2); // 前进两页
history.forward(); // 前进一页
history.back(); // 后退一页
在H5规范中引入了三个新的API,
// 按指定的名称和URL(如果提供该参数)将数据push进会话历史栈
history.pushState();
// 按指定的数据,名称和URL(如果提供该参数),更新历史栈上最新的入口
history.replaceState();
// 返回当前状态对象
history.state
因为pushState和replaceState都可以改变URL的同时,不引起页面重载,所以history符合了目标一的条件。
回顾hash模式,在hash被改变时会触发hashchange事件,而window上也有一个popstate事件。当活动历史记录条目更改时,将触发popstate事件。然而调用history.pushState()/history.replaceState()不会触发popstate事件,只有在做出浏览器动作时,才会触发该事件,比如用户点击浏览器的回退/前进按钮,或者在JS代码中调用history.back()/history.forward()方法。
既然pushState和replaceState不会触发事件,那么我们需要换个思路来监听URL的变化。在单页应用中能改变URL的操作其实可以归为以下几种:
1. 点击浏览器的前进或后退按钮;
2. 点击 a 标签;
3. 在JS代码中触发history.pushState函数;
4. 在JS代码中触发history.replaceState函数;
只要我们能控制以上的操作,就可以实现history模式的路由管理了。核心功能的简单实现如下:
首先创建一个router对象,并添加popstate事件监听,
class HistoryRouter{
constructor(routeArr = []){
// 管理页面的回调
this.routers = {};
routeArr.forEach(item => this.register(item));
this.listenPopState();
}
// 注册
register(item){
const {path, content} = item;
this.routers[path] = typeof content === 'function' ? content : function(){};
}
// 监听popstate事件,点击浏览器的前进后退按钮触发
listenPopState(){
window.addEventListener('popstate',(e)=>{
let state = e.state || {},
path = state.path || '';
this.load(path);
},false)
}
load(path){
let handler = this.routers[path];
// 执行注册的回调函数
try{
handler.apply(this);
}catch(e){
console.error(e);
}
}
}
然后添加对a标签的劫持,
// 全局监听a标签的点击事件
listenALink(){
window.addEventListener('click',(e)=>{
let dom = e.target;
if(dom.tagName.toUpperCase() === 'A' && dom.getAttribute('href')){
e.preventDefault(); // 阻止原生事件
this.push(dom.getAttribute('href'));
}
},false)
}
再添加pushState和replaceState的实现,
// 跳转到path
push(path){
history.pushState({path},null,path);
this.load(path); // 需要手动加载页面回调
}
// 替换为path
replace(path){
history.replaceState({path},null,path);
this.load(path);
}
最后添加上对象的初始化和页面内容,
const container = document.getElementById('container');
const routeArr = [
{path: '/index', content: ()=> container.innerHTML = '这是首页'},
{path: '/about', content: ()=> container.innerHTML = '这是关于页'},
{path: '/detail', content: ()=> container.innerHTML = '这是详情页'}
];
cosnt router = new HistoryRouter(routeArr);
document.getElementById('push_btn').onclick = () => router.push('/detail');
document.getElementById('replace_btn').onclick = () => router.replace('/detail');
<body>
<div id="header">
<a href="/index">index</a>
<a href="/about">about</a>
<a href="/detail">detail</a>
</div>
<div id="container"></div>
<div id="push_btn"></div>
<div id="replace_btn"></div>
</body>
最后提一点,由于history是通过改变URL来进行路由的,当刷新页面时浏览器会向服务器访问当前地址,而服务器上不存在该页面,所以会出现404。为解决这个问题,我们需要修改web服务器的配置,让其在匹配不到页面时返回单页应用的页面。
const routes = {
"/index": '这是首页',
"/about": '这是关于页',
"/detail": '这是详情页',
};
const container = document.getElementById('container');
function route() {
let href = window.localStorage.getItem('cur-route');
if (!href) {
href = "/index";
}
// 展示内容
container.innerHTML = routes[href];
}
// 获取到所有class为link的a标签
const allA = document.querySelectorAll('a.link');
// 遍历a标签
for (let a of allA) {
a.addEventListener('click', (e) => {
e.preventDefault();
const href = a.getAttribute('href');
window.localStorage.setItem('cur-route', href);
// 通知变化
onStateChange();
});
}
function onStateChange() {
route();
}
// 初始化
route();
下面总结一下几种方式的优缺点:
hash模式兼容性更好,且不需要服务器配合修改,但SEO不友好,并且hash模式的地址比较丑陋。
history模式对于SEO更友好,但需要服务端进行配置,并且IE8及以下不支持。
memeory模式的路由信息保存在内存中,浏览器的前进后退操作无效,更适合运用在单机应用中。
以上便是web路由管理的几种常见实现方式,实现过程比较粗糙,希望能有助于大家在使用现代优秀的路由组件,如vue-router、react-router时能更好的运用在项目中。
至此,我们了解到了web路由是如何去实现路由管理的,那么,就请期待我们下一篇文章《大前端开发中的路由管理之三:Android篇》吧,下篇文章将为大家揭秘Android端是如何去做路由管理的。
QQ音乐招聘 Android / iOS 客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com