一个一百行内的现代的 JavaScript 路由

时下流行的单页的应用无处不在。有了这样的应用意味着你须要一个坚实的路由机制。像Emberjs框架是真正创建在一个路由器类的顶部。我真不知道,这是我喜欢的一个概念,但我绝对相信AbsurdJS应该有一个内置的路由器。并且,与一切都在这个小库,它应该是小的,简单的类。让咱们来看看这样的模块可能长什么样。正则表达式

要求

路由应该是:api

  • 在一百行之内。
  • 支持hash类型的 URLs如: like http://site.com#products/list.
  • 支持History API。
  • 提供易用的API.
  • 不自动运行。
  • 只在须要的状况下监听变化。

单列模式

建立一个路由实例多是一个糟糕的选择,由于项目可能须要几个路由,可是这是不寻常的应用程序。若是实现了单列模式,咱们将不须要从一个对象到另外一个对象传递路由,没必要担忧建立它。咱们但愿只有一个实例,因此可能会自动建立它。数组

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

这里有三个咱们所需的特性。浏览器

  • routes:保存当前已注册的路由。
  • mode: 显示“hash”或者“history”取决于咱们是否运用History API.
  • root: 应用的根路径,只在用pushState的状况下须要。

认证

咱们须要一个路由器的方法。将该方法添加进去并传递两个参数。app

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;
    }
}

mode至关于“history”只有当咱们要和固然只能是支持pushState。不然,咱们将在URL中的用hash。默认状况下,root设置为单斜线“/”。框架

得到当前URL

这是路由中的重要部分,由于它将告诉咱们当前所处的位置。咱们有两种模式,因此咱们须要一个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的根部。还应该删除全部GET参数,这是用一个正则表达式(/\?(.*)$/)完成。得到hash的值更加容易。注意clearSlashes功能的使用。它的任务是去掉从开始和字符串的末尾删除斜杠。这是必要的,由于咱们不但愿强迫开发者使用的URL的特定格式。无论他经过它转换为相同的值。this

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

添加和删除路由

在开发AbsurdJS时,我老是的给开发者尽量多的控制。在几乎全部的路由器实现的路由被定义为字符串。不过,我更喜欢直接传递一个正则表达式。它更灵活,由于咱们可能作的很是疯狂的匹配。url

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

该函数填充路由数组,若是只有一个函数传递,则它被认为是默认路由,这仅仅是一个空字符串的处理程序。请注意,大多数函数返回this。这将帮助咱们的连锁类的方法。code

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

删除只发生在经过一个传递匹配的正则表达式或传递handler参数给add方法。

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

有时,咱们可能须要从新初始化类。因此上面的flush方法能够在这种状况下被使用。

注册

好吧,咱们有添加和删除URLs的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。以后对路由进行一个正常的循环,并试图找到一个匹配。若是正则表达式不匹配,变量匹配该值为NULL。或者,它的值像下面

["products/12/edit/22", "12", "22", index: 1, input: "/products/12/edit/22"]

它的类数组对象包含全部的匹配字符串和子字符串。这意味着,若是咱们转移的第一个元素,咱们将获得的动态部分的数组。例如:

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"]

这就是咱们如何处理动态 URLs.

监测变化

固然,不能一直运行check方法。咱们须要一个逻辑,它会通知地址栏的变化。当发上改变,即便是点击后退按钮, URL改变将触发popstate 事件。不过,我发现一些浏览器调度此事件在页面加载。这与其余一些分歧让我想到了另外一种解决方案。由于我想有监控,即便模式设为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

在路由的最后须要一个函数,它改变了当前地址和触发路由的处理程序。

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;
}

一样,咱们作法不一样取决于咱们的mode属性。若是History API可用咱们能够用pushState,不然,用window.location就好了。

最终源代码

这个小例程是最终版本。

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.toString() === param.toString()) {
                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');

总结

这个路由仅90行左右,它支持hash类型的URLs和一个新的History API,它真的是有用的若是你不想由于路由而引用一整个框架。


原文参考:http://krasimirtsonev.com/blog/article/A-modern-JavaScript-router-in-1...

相关文章
相关标签/搜索