前端路由實現(xiàn)之 #hash
先上demo項目地址: spa-routers
運行效果圖

背景介紹
用了許多前端框架來做spa應用,比如說backbone,angular,vue他們都有各自的路由系統(tǒng),管理著前端的每一個頁面切換,想要理解其中路由的實現(xiàn),最好的方法就是手動實現(xiàn)一個。
前端路由有2種實現(xiàn)方式,一種是html5推出的historyapi,我們這里說的是另一種hash路由,就是常見的 # 號,這種方式兼容性更好。
需求分析
我們這里只是簡單的實現(xiàn)一個路由輪子,基本的功能包含以下:
- 切換頁面
- 異步加載js
- 異步傳參
實現(xiàn)步驟
-
切換頁面:路由的最大作用就是切換頁面,以往后臺的路由是直接改變了頁面的url方式促使頁面刷新。但是前端路由通過 # 號不能刷新頁面,只能通過 window 的監(jiān)聽事件 hashchange 來監(jiān)聽hash的變化,然后捕獲到具體的hash值進行操作
//路由切換 window.addEventListener('hashchange',function(){ //do something this.hashChange() }) -
注冊路由:我們需要把路由規(guī)則注冊到頁面,這樣頁面在切換的時候才會有不同的效果。
//注冊函數(shù) map:function(path,callback){ path = path.replace(/\s*/g,"");//過濾空格 //在有回調(diào),且回調(diào)是一個正確的函數(shù)的情況下進行存儲 以 /name 為key的對象 {callback:xx} if(callback && Object.prototype.toString.call(callback) === '[object Function]' ){ this.routers[path] ={ callback:callback,//回調(diào) fn:null //存儲異步文件狀態(tài),用來記錄異步的js文件是否下載,下文有提及 } }else{ //打印出錯的堆棧信息 console.trace('注冊'+path+'地址需要提供正確的的注冊回調(diào)') } } //調(diào)用方式 map('/detail',function(transition){ ... }) -
異步加載js:一般單頁面應用為了性能優(yōu)化,都會把各個頁面的文件拆分開,按需加載,所以路由里面要加入異步加載js文件的功能。異步加載我們就采用最簡單的原生方法,創(chuàng)建script標簽,動態(tài)引入js。
var _body= document.getElementsByTagName('body')[0], scriptEle= document.createElement('script'); scriptEle.type= 'text/javascript'; scriptEle.src= xxx.js; scriptEle.async = true; scriptEle.onload= function(callback){ //為了避免重復引入js,我們需要在這里記錄一下已經(jīng)加載過的文件,對應的 fn需要賦值處理 callback() } _body.appendChild(scriptEle); -
參數(shù)傳遞:在我們動態(tài)引入單獨模塊的js之后,我們可能需要給這個模塊傳遞一些單獨的參數(shù)。這里借鑒了一下jsonp的處理方式,我們把單獨模塊的js包裝成一個函數(shù),提供一個全局的回調(diào)方法,加載完成時候再調(diào)用回調(diào)函數(shù)。
SPA_RESOLVE_INIT = function(transition) { document.getElementById("content").innerHTML = '<p style="color:#F8C545;">當前異步渲染列表頁'+ JSON.stringify(transition) +'</p>' console.log("首頁回調(diào)" + JSON.stringify(transition)) }
擴展:以上我們已經(jīng)完成了基本功能,我們再對齊進行擴展,在頁面切換之前beforeEach和切換完成afterEach的時候增加2個方法進行處理。思路是,注冊了這2個方法之后,在切換之前就調(diào)用beforeEach,切換之后,需要等待下載js完成,在onload里面進行調(diào)用 afterEach
//切換之前一些處理
beforeEach:function(callback){
if(Object.prototype.toString.call(callback) === '[object Function]'){
this.beforeFun = callback;
}else{
console.trace('路由切換前鉤子函數(shù)不正確')
}
},
//切換成功之后
afterEach:function(callback){
if(Object.prototype.toString.call(callback) === '[object Function]'){
this.afterFun = callback;
}else{
console.trace('路由切換后回調(diào)函數(shù)不正確')
}
},
通過以上的思路分析,再加以整合,我們就完成了一個簡單的前端路由,并且可以加到頁面進行實際的SPA開發(fā),不過還是非常簡陋。
完整代碼
/*
*author:https://github.com/kliuj
**使用方法
* 1:注冊路由 : spaRouters.map('/name',function(transition){
//異步加載js
spaRouters.asyncFun('name.js',transition)
//或者同步執(zhí)行回調(diào)
spaRouters.syncFun(function(transition){},transition)
})
2:初始化 spaRouters.init()
3:跳轉(zhuǎn) href = '#/name'
*/
(function() {
var util = {
//獲取路由的路徑和詳細參數(shù)
getParamsUrl:function(){
var hashDeatail = location.hash.split("?"),
hashName = hashDeatail[0].split("#")[1],//路由地址
params = hashDeatail[1] ? hashDeatail[1].split("&") : [],//參數(shù)內(nèi)容
query = {};
for(var i = 0;i<params.length ; i++){
var item = params[i].split("=");
query[item[0]] = item[1]
}
return {
path:hashName,
query:query
}
}
}
function spaRouters(){
this.routers = {};//保存注冊的所有路由
this.beforeFun = null;//切換前
this.afterFun = null;
}
spaRouters.prototype={
init:function(){
var self = this;
//頁面加載匹配路由
window.addEventListener('load',function(){
self.urlChange()
})
//路由切換
window.addEventListener('hashchange',function(){
self.urlChange()
})
//異步引入js通過回調(diào)傳遞參數(shù)
window.SPA_RESOLVE_INIT = null;
},
refresh:function(currentHash){
var self = this;
if(self.beforeFun){
self.beforeFun({
to:{
path:currentHash.path,
query:currentHash.query
},
next:function(){
self.routers[currentHash.path].callback.call(self,currentHash)
}
})
}else{
self.routers[currentHash.path].callback.call(self,currentHash)
}
},
//路由處理
urlChange:function(){
var currentHash = util.getParamsUrl();
if(this.routers[currentHash.path]){
this.refresh(currentHash)
}else{
//不存在的地址重定向到首頁
location.hash = '/index'
}
},
//單層路由注冊
map:function(path,callback){
path = path.replace(/\s*/g,"");//過濾空格
if(callback && Object.prototype.toString.call(callback) === '[object Function]' ){
this.routers[path] ={
callback:callback,//回調(diào)
fn:null //存儲異步文件狀態(tài)
}
}else{
console.trace('注冊'+path+'地址需要提供正確的的注冊回調(diào)')
}
},
//切換之前一些處理
beforeEach:function(callback){
if(Object.prototype.toString.call(callback) === '[object Function]'){
this.beforeFun = callback;
}else{
console.trace('路由切換前鉤子函數(shù)不正確')
}
},
//切換成功之后
afterEach:function(callback){
if(Object.prototype.toString.call(callback) === '[object Function]'){
this.afterFun = callback;
}else{
console.trace('路由切換后回調(diào)函數(shù)不正確')
}
},
//路由異步懶加載js文件
asyncFun:function(file,transition){
var self = this;
if(self.routers[transition.path].fn){
self.afterFun && self.afterFun(transition)
self.routers[transition.path].fn(transition)
}else{
console.log("開始異步下載js文件"+file)
var _body= document.getElementsByTagName('body')[0];
var scriptEle= document.createElement('script');
scriptEle.type= 'text/javascript';
scriptEle.src= file;
scriptEle.async = true;
SPA_RESOLVE_INIT = null;
scriptEle.onload= function(){
console.log('下載'+file+'完成')
self.afterFun && self.afterFun(transition)
self.routers[transition.path].fn = SPA_RESOLVE_INIT;
self.routers[transition.path].fn(transition)
}
_body.appendChild(scriptEle);
}
},
//同步操作
syncFun:function(callback,transition){
this.afterFun && this.afterFun(transition)
callback && callback(transition)
}
}
//注冊到window全局
window.spaRouters = new spaRouters();
})()
簡單的單頁面在github上有完整的demo
spa-routers
以上僅是我個人的一些看法,如有疑問,感謝指導