1、Web路由需要实现的目标
上一篇文章中我们谈到了SPA(Single-page application)的出现,但SPA的应用有个需要解决的问题,就是浏览器只加载记录了一个html,所以当刷新浏览器时js会重新执行,当前页面的内容便会丢失;页面跳转时浏览器不会向服务器发出新的页面请求,浏览器也就无法前进、后退页面。
所以前端web路由需要实现以下目标:
(1)能根据页面URL来获取不同的模块,但不发起新的页面请求;
(2)能监听URL的变化。
而hash和history这两种模式便是其实现原理。
2、 hash模式
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模式。
3、 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); // 需要手动加载页面回调}// 替换为pathreplace(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服务器的配置,让其在匹配不到页面时返回单页应用的页面。
4、memory模式
SPA的路由模式还有一种叫memory的模式,其特点是内容变化,但URL始终不变。由于其不符合上述的目标,所以这里只是简单介绍其实现原理。实现方式就是利用window.localstorage保存当前的路径,根据路径映射出页面内容。合适的使用场景比如react-native。
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();
5、 结语
下面总结一下几种方式的优缺点:
- hash模式兼容性更好,且不需要服务器配合修改,但SEO不友好,并且hash模式的地址比较丑陋。
- history模式对于SEO更友好,但需要服务端进行配置,并且IE8及以下不支持。
- memeory模式的路由信息保存在内存中,浏览器的前进后退操作无效,更适合运用在单机应用中。
以上便是web路由管理的几种常见实现方式,实现过程比较粗糙,希望能有助于大家在使用现代优秀的路由组件,如vue-router、react-router时能更好的运用在项目中。
至此,我们了解到了web路由是如何去实现路由管理的,那么,就请期待我们下一篇文章《大前端开发中的路由管理之三:Android篇》吧,下篇文章将为大家揭秘Android端是如何去做路由管理的。
QQ音乐招聘 Android / iOS 客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com
文末为大家推荐一个技术号《腾讯音乐天琴实验室》,TME天琴实验室致力于对业内前沿科技如AI等方向进行相关研发,持续推出新技术提升TME旗下QQ音乐等平台的音乐视听体验,对音视频相关AI研发感兴趣的同仁们一起交流学习起来吧!!!
↓ ↓ ↓