JS原生一步步實現前端路由和單頁面應用

前端路由實現之 #hash

先上github項目地址: spa-routers
運行效果圖
[運行效果圖]

背景介紹

用瞭許多前端框架來做spa應用,比如說backbone,angular,vue他們都有各自的路由系統,管理著前端的每一個頁面切換,想要理解其中路由的實現,最好的方法就是手動實現一個。
前端路由有2種實現方式,一種是html5推出的historyapi,我們這裡說的是另一種hash路由,就是常見的 # 號,這種方式兼容性更好。

需求分析

我們這裡隻是簡單的實現一個路由輪子,基本的功能包含以下:

切換頁面 異步加載js 異步傳參

實現步驟

切換頁面:路由的最大作用就是切換頁面,以往後臺的路由是直接改變瞭頁面的url方式促使頁面刷新。但是前端路由通過 # 號不能刷新頁面,隻能通過 window 的監聽事件 hashchange 來監聽hash的變化,然後捕獲到具體的hash值進行操作

//路由切換
window.addEventListener('hashchange',function(){
    //do something 
    this.hashChange()
})

註冊路由:我們需要把路由規則註冊到頁面,這樣頁面在切換的時候才會有不同的效果。

//註冊函數
 map:function(path,callback){
   path = path.replace(/\s*/g,"");//過濾空格
   //在有回調,且回調是一個正確的函數的情況下進行存儲 以 /name 為key的對象 {callback:xx}
   if(callback && Object.prototype.toString.call(callback) === '[object Function]' ){
       this.routers[path] ={
            callback:callback,//回調
            fn:null //存儲異步文件狀態,用來記錄異步的js文件是否下載,下文有提及
        } 
    }else{
    //打印出錯的堆棧信息
        console.trace('註冊'+path+'地址需要提供正確的的註冊回調')
    }
 }

 //調用方式
 map('/detail',function(transition){
  ...
})

異步加載js:一般單頁面應用為瞭性能優化,都會把各個頁面的文件拆分開,按需加載,所以路由裡面要加入異步加載js文件的功能。異步加載我們就采用最簡單的原生方法,創建script標簽,動態引入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,我們需要在這裡記錄一下已經加載過的文件,對應的 fn需要賦值處理
    callback()
} 
_body.appendChild(scriptEle);   

參數傳遞:在我們動態引入單獨模塊的js之後,我們可能需要給這個模塊傳遞一些單獨的參數。這裡借鑒瞭一下jsonp的處理方式,我們把單獨模塊的js包裝成一個函數,提供一個全局的回調方法,加載完成時候再調用回調函數。

SPA_RESOLVE_INIT = function(transition) { 
    document.getElementById("content").innerHTML = '

當前異步渲染列表頁'+ JSON.stringify(transition) +'

' console.log("首頁回調" + JSON.stringify(transition)) }

擴展:以上我們已經完成瞭基本功能,我們再對齊進行擴展,在頁面切換之前beforeEach和切換完成afterEach的時候增加2個方法進行處理。思路是,註冊瞭這2個方法之後,在切換之前就調用beforeEach,切換之後,需要等待下載js完成,在onload裡面進行調用 afterEach

        //切換之前一些處理
        beforeEach:function(callback){
            if(Object.prototype.toString.call(callback) === '[object Function]'){
                this.beforeFun = callback;
            }else{
                console.trace('路由切換前鉤子函數不正確')
            }
        },
        //切換成功之後
        afterEach:function(callback){
            if(Object.prototype.toString.call(callback) === '[object Function]'){
                this.afterFun = callback;
            }else{
                console.trace('路由切換後回調函數不正確')
            }
        },

通過以上的思路分析,再加以整合,我們就完成瞭一個簡單的前端路由,並且可以加到頁面進行實際的SPA開發,不過還是非常簡陋。

完整代碼

/*
*author:https://github.com/kliuj
**使用方法
*       1:註冊路由 : spaRouters.map('/name',function(transition){
                        //異步加載js 
                        spaRouters.asyncFun('name.js',transition)
                        //或者同步執行回調
                        spaRouters.syncFun(function(transition){},transition)
                    })
        2:初始化      spaRouters.init()
        3:跳轉  href = '#/name'           
*/
(function() {
    var util = {
        //獲取路由的路徑和詳細參數
        getParamsUrl:function(){
            var hashDeatail = location.hash.split("?"),
                hashName = hashDeatail[0].split("#")[1],//路由地址
                params = hashDeatail[1] ? hashDeatail[1].split("&") : [],//參數內容
                query = {};
            for(var i = 0;i<cke:params.length ;="" i++){="" var="" item="params[i].split(&quot;=&quot;);" query[item[0]]="item[1]" }="" return="" {="" path:hashname,="" query:query="" function="" sparouters(){="" this.routers="{};//保存註冊的所有路由" this.beforefun="null;//切換前" this.afterfun="null;" sparouters.prototype="{" init:function(){="" self="this;" 頁面加載匹配路由="" window.addeventlistener('load',function(){="" self.urlchange()="" })="" 路由切換="" window.addeventlistener('hashchange',function(){="" 異步引入js通過回調傳遞參數="" window.spa_resolve_init="null;" },="" refresh:function(currenthash){="" if(self.beforefun){="" self.beforefun({="" to:{="" path:currenthash.path,="" query:currenthash.query="" next:function(){="" self.routers[currenthash.path].callback.call(self,currenthash)="" }else{="" 路由處理="" urlchange:function(){="" currenthash="util.getParamsUrl();" if(this.routers[currenthash.path]){="" this.refresh(currenthash)="" 不存在的地址重定向到首頁="" location.hash="/index" 單層路由註冊="" map:function(path,callback){="" path="path.replace(/\s*/g,&quot;&quot;);//過濾空格" if(callback="" &&="" object.prototype.tostring.call(callback)="==" '[object="" function]'="" ){="" this.routers[path]="{" callback:callback,="" 回調="" fn:null="" 存儲異步文件狀態="" console.trace('註冊'+path+'地址需要提供正確的的註冊回調')="" 切換之前一些處理="" beforeeach:function(callback){="" if(object.prototype.tostring.call(callback)="==" function]'){="" console.trace('路由切換前鉤子函數不正確')="" 切換成功之後="" aftereach:function(callback){="" console.trace('路由切換後回調函數不正確')="" 路由異步懶加載js文件="" asyncfun:function(file,transition){="" if(self.routers[transition.path].fn){="" self.afterfun="" self.afterfun(transition)="" self.routers[transition.path].fn(transition)="" console.log("開始異步下載js文件"+file)="" _body="document.getElementsByTagName('body')[0];" 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.routers[transition.path].fn="SPA_RESOLVE_INIT;" _body.appendchild(scriptele);="" 同步操作="" syncfun:function(callback,transition){="" this.afterfun(transition)="" callback="" && callback(transition)="" 註冊到window全局="" window.sparouters="new" sparouters();="" })()="" 

簡單的單頁面在github上有完整的demo
spa-routers

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *