一個一百行內的現代的 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...

相關文章
相關標籤/搜索