100行代码实现现代版Router

 

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

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

需求java

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

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

我决定只用一个router实例。这个多是一个糟糕的选择,由于我曾经作过须要几个router的项目,可是反过来讲着毕竟不常见。若是咱们采用单例模式来实现咱们将不用在对象和对象之间传递router,同时咱们也不担忧如何建立它。咱们只须要一个实例,那咱们就天然而然这样建立了:github

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

这里有3个属性:正则表达式

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

配置api

咱们须要一个方法去启动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默认被设定为‘/’。浏览器

得到当前URLapp

这是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下载到。

相关文章
相关标签/搜索