4

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

要求

路由应该是:

  • 在一百行以内。
  • 支持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的情况下需要。

认证

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

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的特定格式。不管他通过它转换为相同的值。

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

添加和删除路由

在开发AbsurdJS时,我总是的给开发者尽可能多的控制。在几乎所有的路由器实现的路由被定义为字符串。不过,我更喜欢直接传递一个正则表达式。它更灵活,因为我们可能做的非常疯狂的匹配。

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

该函数填充路由数组,如果只有一个函数传递,则它被认为是默认路由,这仅仅是一个空字符串的处理程序。请注意,大多数函数返回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;
}

删除只发生在通过一个传递匹配的正则表达式或传递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...


sundway
1.2k 声望63 粉丝

Less is more...


« 上一篇
Flexbox详解