「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」
路由的概念
其实早先年没有前端路由的概念,只是随着前端的发展,才有了前端路由。(鲁迅说的)
最早做模版引擎开发的时候,会看到这样的页面url:
http://test.xxx.help.cn/bbs/sage.php 或者是http://home.xxx.cn/bbs/sage.html像这样以.php或者是.html的路径,就是通过服务器端渲染,直接返回一个页面。虽然这样利于SEO搜索,但是代码不方便维护,也会加大服务器压力。
ajax和 SPA
前端路由发展起来离不开ajax的普及。在http0.9(更早)时代都是直接返回一个html页面。用户的每次更新操作都需要重新刷新页面,及其影响交互体验。随着网络的发展,迫切需要一种方案来改善这种情况。 有了 Ajax 后,用户交互就不用每次都刷新页面,体验带来了极大的提升。随着就有了单页面应用SPA。
什么是SPA?
简单点来说;在SPA应用中,只有一个hTML页面,在HTML页面中包含一个app,也就是占位符,页面的切换就是app内的切换,也就是view的切换。如图:
通过view的切换可以起到页面的改变。但是有问题: 单单是通过js和html配合实现,页面url没有变化,对浏览器没有任何影响,浏览器无法记住用户对操作。用户的回退,前进操作都无效,很尴尬。而且url没有变化的话,一个页面只有一个url,也不利于SEO。蛋疼。。 为了解决以上问题,引出了前端路由的概念。其实只要做到可以监听url变化,同时不会发送请求,就可以完成一个简单的前端路由。 重点来啦,介绍实现前端路由的两种方式:hash和history。
hash模式
hash最早的路由解决方法,www.baidu/#/xx/xx 通过改变锚点#后面的hash值来实现url的改变,并且不会刷新页面。 通过window.location.hash可以获取到hash值,然后监听hashChange事件。当hasChange事件触发的时候更新对应的hash值,就可以实现前端路由功能。好的,不多bb。我们来实现一下:
实现hash
我们的想法是维护一个对象,用来储存path(用来匹配hash), view(匹配到hash的时候渲染)。
class MyHashRouter {
constructor() {
this.router = {}; //存储注册路由
}
window.addEventListener('hashChange', this.loadView.bind(this), false);
}
很简单,通过监听hashChange事件的改变,来触发loadView(通过路由匹配view)。注意一下router对象:
router {
path: "/XX", // 路由的路径
callBack: () => {} // 一个执行函数(用来显示view)
}
实现一下注册路由:
registerRouter(path, callBack = ()=>{}) {
this.router[path] = callBack;
// 一个路由对应一个view(一个萝卜一个坑)
}
要注意一个问题:path如果不存在的时候,我们要默认为/,也就是首页。
registerIndexRouter(callBack = () => {}) {
this.router['index'] = callBack; // index默认为首页
}
ok!简单的路由注册完成。现在我们要考虑一下路由切换view切换问题。通过a标签实现url的hash改变,在hash改变的同时我们获取到hash,通过hash来匹配到我们注册的路由。代码走起:!
loadView() {
let hash = window.location.hash.slice(1); // 去除#号
let cb; // 对应执行函数(view)
hash ? cb = this.router[hash] : cb = this.router.index;
// hash存在:找对应的view,否则对应index
cb.call(this); // 执行
}
这样一个简单的hash路由就是完成了。但是要注意个问题:如果匹配的路由没有匹配到,默认跳转到404页面。
hashNotFound() {
if (!this.router.hasOwnProperty(hash) {
cb = this.routers['404'] || function(){};
}
}
code如下:
class MyHashRouter {
constructor(){
this.router = {};
window.addEventListener('hashChange', this.loadView.bind(this), false);
}
registerIndexRouter(callBack = () => {}) {
this.router['index'] = callBack;
}
registerRouter(path, callBack = () => {}) {
this.router[path] = callBack;
}
registerNotFound(callback = () => {}){
this.routers['404'] = callback; // 注册404
}
loadView() {
let hash = window.location.hash.slice(1);
let cb;
if (!hash) {
cb = this.router.index;
}
else if (!this.router.hasOwnProperty(hash) && hash) {
cb = this.router['404'];
}
else {
cb = this.router.hash;
}
cb.call(this);
}
}
我们可以简单的玩一下:
let router = new MyHashRouter();
let app = document.getElementById('app');
//首页
router.registerIndexRouter(()=> app.innerHTML = '首页');
//注册其他视图
router.registerRouter('/path1',() => app.innerHTML = 'view1');
router.registerRouter('/path2',()=> app.innerHTML = 'view2');
router.registerRouter('/path3',()=> app.innerHTML = 'view3');
router.registerRouter('/path4',()=> app.innerHTML = 'view4');
router.registerNotFound(()=> app.innerHTML = '页面未找到'); // 404
router.loadView(); //加载视图
history模式
history api
DOM window 对象通过 history 对象提供了对浏览器的会话历史的访问(不要与 WebExtensions history搞混了)。它暴露了很多有用的方法和属性,允许你在用户浏览历史中向前和向后跳转,同时——从HTML5开始——提供了对history栈中内容的操作。 --MDN
window.history.back() === 浏览器回退
window.history.forward() === 浏览器前进
window.history.go(-1) === 向后移动一个页面 === back()
window.history.go(1) === 向前移动一个页面 === forward()
go(2/-2) === 跳转到 history 中指定的一个点
在HTML5中新增加属性history.pushState()和history.replaceState()它们分别可以添加和修改历史记录条目。注意:这个方法配合window.onpopstate使用。
history.pushState(state, title, url) 和 history.replaceState(state, title, url) 接收三个参数:
- state: 合法的 Javascript 对象,可以用在 popstate 事件中
- null(。。。)
- url:任意有效的 URL,用于更新浏览器的地址栏
pushState()会把现有的url保存在栈里。调用 pushState() 与 设置 window.location = "#foo" 类似,二者都会在当前页面创建并激活新的历史记录。但是相比之下pushState有如下的优点:
- 新的 URL 可以是与当前URL同源的任意URL。
- 如果你不想改URL,就不用改。相反,设置 window.location = "#foo";在当前哈希不是 #foo 时, 才能创建新的历史记录项。
replaceState() 会将历史记录中的当前页面历史替换为url。 ok!现在我们知道了,replaceState()和pushState()可以在修改url的同时,不进行页面刷新。所以我们可以使用history这两个属性实现前端路由。 这里有个问题:
在hash模式中,我们通过监听hashChange事件,来完成url改变,change对应的view。但是在history模式中,我们没有办法监听history。咋整呀!!!
可以这样处理,我们通过监听所有会引起history改变的方法,将这些方法拦截,监听。这样就可以做到变相拦截history了。
在HTML5中有以下的方式会触发history改变:
- a标签点击跳转
- 浏览器回退,前进事件。
- 触发了pushState()和replaceState()方法。
ok,现在我们来简单实现一下history模式。 注册router对象和之前的hash模式都是基本一样,这里就不过多BB了。主要是拦截可以触发history改变的方法。
通用处理路径和view
没什么好说的,path改变view改变。
commonDealPath(path) {
let view;
if(!this.router.hasOwnProperty(path)){
view = this.routers['404'] || function(){};
}
else{
view = this.routers[path]; //有对应path
}
view.call(this);
}
全局监听a链接
注意两点:1. 获取a标签的href属性的value。 2. 阻止a标签的默认事件。
listenerAClick() {
window.addEventListener('click', (e) => {
if (e && e.target) {
let target = e.target; // 获取click事件
if(target.tagName.toUpperCase() === 'A' && target.getAttribute('href')){
let path = target.getAttribute('href')); // 获取a标签的href属性。
history.pushState({path}, null, path); // 通过pushState改变url。
this.commonDealPath(path); // 触发commonDealPath
}
}
}
}
监听浏览器事件
监听浏览器前进和后退,也就是监听popState事件。
listenPop() {
window.addEventListener('popState', () => {
let state = e.state || {};
let path = state.path || "";
this.commonDealPath(path); // 触发commonDealPath
}, false)
}
拦截pushState()和replaceState()
单独处理一下,这两个方法。监听的同时触发view改变。
pathpush(path) {
history.pushState({path},null,path);
this.commonDealPath(path);
}
pathReplace(path) {
history.replaceState({path},null,path);
this.commonDealPath(path);
}
ok!简单的histroy模式就完成了!一句话:监听触发history改变的方法,然后change view。
兼容性
hash:
history:
hash的兼容性要好于hsitory的。
最后
简单的分析了一波hash和history。下期分享一下vue的router和react的router。感谢!!