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下載到。

相關文章
相關標籤/搜索