JS原生一步步實(shí)現(xiàn)前端路由和單頁(yè)面應(yīng)用

前端路由實(shí)現(xiàn)之 #hash

先上demo項(xiàng)目地址: spa-routers
運(yùn)行效果圖

router.jpg

背景介紹

用了許多前端框架來(lái)做spa應(yīng)用,比如說(shuō)backbone,angular,vue他們都有各自的路由系統(tǒng),管理著前端的每一個(gè)頁(yè)面切換,想要理解其中路由的實(shí)現(xiàn),最好的方法就是手動(dòng)實(shí)現(xiàn)一個(gè)。
前端路由有2種實(shí)現(xiàn)方式,一種是html5推出的historyapi,我們這里說(shuō)的是另一種hash路由,就是常見(jiàn)的 # 號(hào),這種方式兼容性更好。

需求分析

我們這里只是簡(jiǎn)單的實(shí)現(xiàn)一個(gè)路由輪子,基本的功能包含以下:

  1. 切換頁(yè)面
  2. 異步加載js
  3. 異步傳參

實(shí)現(xiàn)步驟

  1. 切換頁(yè)面:路由的最大作用就是切換頁(yè)面,以往后臺(tái)的路由是直接改變了頁(yè)面的url方式促使頁(yè)面刷新。但是前端路由通過(guò) # 號(hào)不能刷新頁(yè)面,只能通過(guò) window 的監(jiān)聽(tīng)事件 hashchange 來(lái)監(jiān)聽(tīng)hash的變化,然后捕獲到具體的hash值進(jìn)行操作

    //路由切換
    window.addEventListener('hashchange',function(){
        //do something 
        this.hashChange()
    })
    
  2. 注冊(cè)路由:我們需要把路由規(guī)則注冊(cè)到頁(yè)面,這樣頁(yè)面在切換的時(shí)候才會(huì)有不同的效果。

    //注冊(cè)函數(shù)
     map:function(path,callback){
       path = path.replace(/\s*/g,"");//過(guò)濾空格
       //在有回調(diào),且回調(diào)是一個(gè)正確的函數(shù)的情況下進(jìn)行存儲(chǔ) 以 /name 為key的對(duì)象 {callback:xx}
       if(callback && Object.prototype.toString.call(callback) === '[object Function]' ){
           this.routers[path] ={
                callback:callback,//回調(diào)
                fn:null //存儲(chǔ)異步文件狀態(tài),用來(lái)記錄異步的js文件是否下載,下文有提及
            } 
        }else{
        //打印出錯(cuò)的堆棧信息
            console.trace('注冊(cè)'+path+'地址需要提供正確的的注冊(cè)回調(diào)')
        }
     }
     
     //調(diào)用方式
     map('/detail',function(transition){
      ...
    })
    
  3. 異步加載js:一般單頁(yè)面應(yīng)用為了性能優(yōu)化,都會(huì)把各個(gè)頁(yè)面的文件拆分開(kāi),按需加載,所以路由里面要加入異步加載js文件的功能。異步加載我們就采用最簡(jiǎn)單的原生方法,創(chuàng)建script標(biāo)簽,動(dòng)態(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){ 
        //為了避免重復(fù)引入js,我們需要在這里記錄一下已經(jīng)加載過(guò)的文件,對(duì)應(yīng)的 fn需要賦值處理
        callback()
    } 
    _body.appendChild(scriptEle);   
    
  4. 參數(shù)傳遞:在我們動(dòng)態(tài)引入單獨(dú)模塊的js之后,我們可能需要給這個(gè)模塊傳遞一些單獨(dú)的參數(shù)。這里借鑒了一下jsonp的處理方式,我們把單獨(dú)模塊的js包裝成一個(gè)函數(shù),提供一個(gè)全局的回調(diào)方法,加載完成時(shí)候再調(diào)用回調(diào)函數(shù)。

    SPA_RESOLVE_INIT = function(transition) { 
        document.getElementById("content").innerHTML = '<p style="color:#F8C545;">當(dāng)前異步渲染列表頁(yè)'+ JSON.stringify(transition) +'</p>'
        console.log("首頁(yè)回調(diào)" + JSON.stringify(transition))
    }
    

擴(kuò)展:以上我們已經(jīng)完成了基本功能,我們?cè)賹?duì)齊進(jìn)行擴(kuò)展,在頁(yè)面切換之前beforeEach和切換完成afterEach的時(shí)候增加2個(gè)方法進(jìn)行處理。思路是,注冊(cè)了這2個(gè)方法之后,在切換之前就調(diào)用beforeEach,切換之后,需要等待下載js完成,在onload里面進(jìn)行調(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ù)不正確')
            }
        },

通過(guò)以上的思路分析,再加以整合,我們就完成了一個(gè)簡(jiǎn)單的前端路由,并且可以加到頁(yè)面進(jìn)行實(shí)際的SPA開(kāi)發(fā),不過(guò)還是非常簡(jiǎn)陋。

完整代碼

/*
*author:https://github.com/kliuj
**使用方法
*       1:注冊(cè)路由 : 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 = {
        //獲取路由的路徑和詳細(xì)參數(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 = {};//保存注冊(cè)的所有路由
        this.beforeFun = null;//切換前
        this.afterFun = null;
    }
    spaRouters.prototype={
        init:function(){
            var self = this;
            //頁(yè)面加載匹配路由
            window.addEventListener('load',function(){
                self.urlChange()
            })
            //路由切換
            window.addEventListener('hashchange',function(){
                self.urlChange()
            })
            //異步引入js通過(guò)回調(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{
                //不存在的地址重定向到首頁(yè)
                location.hash = '/index'
            }
        },
        //單層路由注冊(cè)
        map:function(path,callback){
            path = path.replace(/\s*/g,"");//過(guò)濾空格
            if(callback && Object.prototype.toString.call(callback) === '[object Function]' ){
                this.routers[path] ={
                    callback:callback,//回調(diào)
                    fn:null //存儲(chǔ)異步文件狀態(tài)
                } 
            }else{
                console.trace('注冊(cè)'+path+'地址需要提供正確的的注冊(cè)回調(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("開(kāi)始異步下載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)
        }

    }
    //注冊(cè)到window全局
    window.spaRouters = new spaRouters();
})()

簡(jiǎn)單的單頁(yè)面在github上有完整的demo
spa-routers

以上僅是我個(gè)人的一些看法,如有疑問(wèn),感謝指導(dǎo)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容