JS原生一步步实现前端路由和单页面应用

前端路由实现之 #hash

先上demo项目地址: spa-routers
运行效果图

router.jpg

背景介绍

用了许多前端框架来做spa应用,比如说backbone,angular,vue他们都有各自的路由系统,管理着前端的每一个页面切换,想要理解其中路由的实现,最好的方法就是手动实现一个。
前端路由有2种实现方式,一种是html5推出的historyapi,我们这里说的是另一种hash路由,就是常见的 # 号,这种方式兼容性更好。

需求分析

我们这里只是简单的实现一个路由轮子,基本的功能包含以下:

  1. 切换页面
  2. 异步加载js
  3. 异步传参

实现步骤

  1. 切换页面:路由的最大作用就是切换页面,以往后台的路由是直接改变了页面的url方式促使页面刷新。但是前端路由通过 # 号不能刷新页面,只能通过 window 的监听事件 hashchange 来监听hash的变化,然后捕获到具体的hash值进行操作

    //路由切换
    window.addEventListener('hashchange',function(){
        //do something 
        this.hashChange()
    })
    
  2. 注册路由:我们需要把路由规则注册到页面,这样页面在切换的时候才会有不同的效果。

    //注册函数
     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。

    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包装成一个函数,提供一个全局的回调方法,加载完成时候再调用回调函数。

    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

以上仅是我个人的一些看法,如有疑问,感谢指导

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容

  • 本文首发于TalkingCoder,一个有逼格的程序员社区。转载请注明出处和作者。 写在前面 本文为系列文章,总共...
    Aresn阅读 9,509评论 0 42
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,394评论 25 707
  • 我第一次发简书后收到周围很多同事朋友的赞赏和关注,也收获了许多不知名的简友的认同。于是我还不老的心脏又开始砰砰乱跳...
    活着不易阅读 276评论 6 5
  • 这一周只有两天不是休息日 现在我就为大家讲述我的我的旅行故事: 星期三我们坐高铁去北京 我们整整七个小时才到!...
    王姝怡阅读 540评论 1 4
  • 我被人窥探心事 年少无知荒唐的梦 盖世英雄 丑恶中没有什么美丽 魔鬼在神圣的大堂讲演正义 毒蛇跟蛤蟆在看同一张旧报...
    以梦为马所到所栖阅读 122评论 0 0