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

前端路由實現之 #hash

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

背景介紹

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

需求分析

咱們這裏只是簡單的實現一個路由輪子,基本的功能包含如下:前端

  1. 切換頁面vue

  2. 異步加載jshtml5

  3. 異步傳參java

實現步驟

  1. 切換頁面:路由的最大做用就是切換頁面,以日後臺的路由是直接改變了頁面的url方式促使頁面刷新。可是前端路由經過 # 號不能刷新頁面,只能經過 window 的監聽事件 hashchange 來監聽hash的變化,而後捕獲到具體的hash值進行操做git

    //路由切換
    window.addEventListener('hashchange',function(){
        //do something 
        this.hashChange()
    })
  2. 註冊路由:咱們須要把路由規則註冊到頁面,這樣頁面在切換的時候纔會有不一樣的效果。github

    //註冊函數
     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){
      ...
      })
  3. 異步加載js:通常單頁面應用爲了性能優化,都會把各個頁面的文件拆分開,按需加載,因此路由裏面要加入異步加載js文件的功能。異步加載咱們就採用最簡單的原生方法,建立script標籤,動態引入js。json

    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);
  4. 參數傳遞:在咱們動態引入單獨模塊的js以後,咱們可能須要給這個模塊傳遞一些單獨的參數。這裏借鑑了一下jsonp的處理方式,咱們把單獨模塊的js包裝成一個函數,提供一個全局的回調方法,加載完成時候再調用回調函數。api

    SPA_RESOLVE_INIT = function(transition) { 
        document.getElementById("content").innerHTML = '<p style="color:#F8C545;">當前異步渲染列表頁'+ JSON.stringify(transition) +'</p>'
        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<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經過回調傳遞參數
            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,//回調
                    fn:null //存儲異步文件狀態
                } 
            }else{
                console.trace('註冊'+path+'地址須要提供正確的的註冊回調')
            }
        },
        //切換以前一些處理
        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('路由切換後回調函數不正確')
            }
        },
        //路由異步懶加載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

以上僅是我我的的一些見解,若有疑問,感謝指導

相關文章
相關標籤/搜索