zoukankan      html  css  js  c++  java
  • 100行代码实现现代版Router

     

    原文:http://www.html-js.com/article/JavaScript-version-100-lines-of-code-to-achieve-a-modern-version-of-Router

    当前到处可见单页应用,而对于单页应用来说我们必须有一个有效的路由机制。像Emberjs就是建立在一个Router类上的框架。虽然我不是太确信这个是不是我喜欢的东东,但是肯定的是AbsurdJS必须有一个内置的Router。和这个框架中的其他功能一样,这个Router应该非常小巧简单的。让我们看下这个模块应该是什么样子呢。编辑:github 原文链接:A modern JavaScript router in 100 lines

    需求

    这里设计的router应该是这样的:

    • 少于100行代码
    • 支持散列输入的URL,比如http://site.com#products/list
    • 能够支持History API
    • 提供简单可用的接口
    • 不会自动运行
    • 可以监听变化
    • 采用单例模式

    我决定只用一个router实例。这个可能是一个糟糕的选择,因为我曾经做过需要几个router的项目,但是反过来说着毕竟不常见。如果我们采用单例模式来实现我们将不用在对象和对象之间传递router,同时我们也不担心如何创建它。我们只需要一个实例,那我们就自然而然这样创建了:

    var Router = {
        routes: [],
        mode: null,
        root: '/'
    }
    

    这里有3个属性:

    • routes-它用来保存当前已经注册的路由。
    • mode-取值有hash和history两个选项,用来判断是否使用History API
    • root-应用的根路径。只有当我们使用pushState我们才需要它。

    配置

    我们需要一个方法去启动router。虽然只要设定两个属性,但是我们最好还是使用一个方法来封装下。

    var Router = {
        routes: [],
        mode: null,
        root: '/',
        config: function(options) {
            this.mode = options && options.mode && options.mode == 'history' 
                        && !!(history.pushState) ? 'history' : 'hash';
            this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
            return this;
        }
    }
    

    只有在支持pushState的情况下才会支持history模式,否则我们就运行于hash模式下。root默认被设定为‘/’。

    获得当前URL

    这是router中非常重要的一部分,因为它告诉我们当前我们在哪里。因为我们有两个模式,所以我们要一个if判断。

    getFragment: function() {
        var fragment = '';
        if(this.mode === 'history') {
            fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
            fragment = fragment.replace(/?(.*)$/, '');
            fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
        } else {
            var match = window.location.href.match(/#(.*)$/);
            fragment = match ? match[1] : '';
        }
        return this.clearSlashes(fragment);
    }
    

    两种条件下我们都是用了全局对象window.location。在history模式下我们需要删除掉URL中的root部分,同时还需要通过正则(/?(.*)$/)去删除所有get的参数。hash模式下比较简单。注意下方法clearSlashes,它是用来删除斜线的。这非常有必要,因为我们不想强制开发者使用固定格式的URL。所有他传递进去后都转换为一个值。

    clearSlashes: function(path) {
        return path.toString().replace(//$/, '').replace(/^//, '');
    }
    

    增加和删除route

    设计AbsurdJS的时候,我是尽量把控制权交给开发者。在大多数router实现中,路由一般被设计成字符串,但是我倾向于正则表达式。这样通过传递疯狂的正则表达式,可以使系统的可扩展性更强。

    add: function(re, handler) {
        if(typeof re == 'function') {
            handler = re;
            re = '';
        }
        this.routes.push({ re: re, handler: handler});
        return this;
    }
    

    这个方法用来填充routes数组。如果只传递了一个方法,那我们就把它当成一个默认路由处理器,并且把它当成一个空字符串。注意这里大多数方法返回了this,这是为了方便级联调用。

    remove: function(param) {
        for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
            if(r.handler === param || r.re === param) {
                this.routes.splice(i, 1); 
                return this;
            }
        }
        return this;
    }
    

    如果我们传递一个合法的正则表达式或者handler给删除方法,那就可以执行删除了。

    flush: function() {
        this.routes = [];
        this.mode = null;
        this.root = '/';
        return this;
    }
    

    有时候我们需要重置类,那我们就需要一个flush方法来执行重置。

    Check-in

    当前我们已经有增加和删除URL的API了,同时也要可以获得当前的地址。那么接下来的逻辑就是去比对注册了的实体。

    check: function(f) {
        var fragment = f || this.getFragment();
        for(var i=0; i<this.routes.length; i++) {
            var match = fragment.match(this.routes[i].re);
            if(match) {
                match.shift();
                this.routes[i].handler.apply({}, match);
                return this;
            }           
        }
        return this;
    }
    

    我们使用getFragment来创建fragment或者直接把函数的参数赋值给fragment。然后我们使用了一个循环来查找这个路由。如果没有匹配上,那match就为null,否则match的只应该是下面这样的[”products/12/edit/22”, “12”, “22”, index: 1, input: ”/products/12/edit/22”]。他是一个对象数组,包含了匹配上的字符串和子字符串。这意味着如果我们能够匹配第一个元素的话,我们就可以通过正则匹配动态的URL。例如:

    Router
    .add(/about/, function() {
        console.log('about');
    })
    .add(/products/(.*)/edit/(.*)/, function() {
        console.log('products', arguments);
    })
    .add(function() {
        console.log('default');
    })
    .check('/products/12/edit/22');
    

    脚本输出:

    products [”12”, “22”]

    这就是我们能够处理动态URL的原因。

    监控变化

    我们不能一直运行check方法。我们需要一个在地址栏发生变化的时候通知我们的逻辑。我这里说的变化包括触发浏览器的返回按钮。如果你接触过History API的话你肯定会知道这里有个popstate 事件。它是当URL发生变化时候执行的一个回调。但是我发现一些浏览器在页面加载时候不会触发这个事件。这个浏览器处理不同让我不得不去寻找另一个解决方案。及时在mode被设定为hash的时候我也去执行监控,所以我决定使用setInterval。

    listen: function() {
        var self = this;
        var current = self.getFragment();
        var fn = function() {
            if(current !== self.getFragment()) {
                current = self.getFragment();
                self.check(current);
            }
        }
        clearInterval(this.interval);
        this.interval = setInterval(fn, 50);
        return this;
    }
    

    我需要保存一个最新的URL用于执行比较。

    改变URL

    在我们router的最后,我们需要一个方法可以改变当前的地址,同时也可以触发路由的回调。

    navigate: function(path) {
        path = path ? path : '';
        if(this.mode === 'history') {
            history.pushState(null, null, this.root + this.clearSlashes(path));
        } else {
            window.location.href.match(/#(.*)$/);
            window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
        }
        return this;
    }
    

    同样,我么能这对不同的模式做了分支判断。如果History API当前可用的话,我们就是用pushState,否则我们我们就是用window.location。

    最终代码

    下面是最终版本的router,并附了一个小例子:

    var Router = {
        routes: [],
        mode: null,
        root: '/',
        config: function(options) {
            this.mode = options && options.mode && options.mode == 'history' 
                        && !!(history.pushState) ? 'history' : 'hash';
            this.root = options && options.root ? '/' + this.clearSlashes(options.root) + '/' : '/';
            return this;
        },
        getFragment: function() {
            var fragment = '';
            if(this.mode === 'history') {
                fragment = this.clearSlashes(decodeURI(location.pathname + location.search));
                fragment = fragment.replace(/?(.*)$/, '');
                fragment = this.root != '/' ? fragment.replace(this.root, '') : fragment;
            } else {
                var match = window.location.href.match(/#(.*)$/);
                fragment = match ? match[1] : '';
            }
            return this.clearSlashes(fragment);
        },
        clearSlashes: function(path) {
            return path.toString().replace(//$/, '').replace(/^//, '');
        },
        add: function(re, handler) {
            if(typeof re == 'function') {
                handler = re;
                re = '';
            }
            this.routes.push({ re: re, handler: handler});
            return this;
        },
        remove: function(param) {
            for(var i=0, r; i<this.routes.length, r = this.routes[i]; i++) {
                if(r.handler === param || r.re === param) {
                    this.routes.splice(i, 1); 
                    return this;
                }
            }
            return this;
        },
        flush: function() {
            this.routes = [];
            this.mode = null;
            this.root = '/';
            return this;
        },
        check: function(f) {
            var fragment = f || this.getFragment();
            for(var i=0; i<this.routes.length; i++) {
                var match = fragment.match(this.routes[i].re);
                if(match) {
                    match.shift();
                    this.routes[i].handler.apply({}, match);
                    return this;
                }           
            }
            return this;
        },
        listen: function() {
            var self = this;
            var current = self.getFragment();
            var fn = function() {
                if(current !== self.getFragment()) {
                    current = self.getFragment();
                    self.check(current);
                }
            }
            clearInterval(this.interval);
            this.interval = setInterval(fn, 50);
            return this;
        },
        navigate: function(path) {
            path = path ? path : '';
            if(this.mode === 'history') {
                history.pushState(null, null, this.root + this.clearSlashes(path));
            } else {
                window.location.href.match(/#(.*)$/);
                window.location.href = window.location.href.replace(/#(.*)$/, '') + '#' + path;
            }
            return this;
        }
    }
    
    // configuration
    Router.config({ mode: 'history'});
    
    // returning the user to the initial state
    Router.navigate();
    
    // adding routes
    Router
    .add(/about/, function() {
        console.log('about');
    })
    .add(/products/(.*)/edit/(.*)/, function() {
        console.log('products', arguments);
    })
    .add(function() {
        console.log('default');
    })
    .check('/products/12/edit/22').listen();
    
    // forwarding
    Router.navigate('/about');
    

      

    总结

    router类大概有90行代码。它支持散列输入的RUL和History API。如果你不想用整个框架的话我想这个还是非常有用的。

    这个类是AbsurdJS类的一部分,你可以在这里查看到这个类的说明。

    源代码可以在github下载到。

  • 相关阅读:
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
    LeetCode
  • 原文地址:https://www.cnblogs.com/gaoxue/p/4789185.html
Copyright © 2011-2022 走看看