前端路由是現代SPA應用必備的功能,每一個現代前端框架都有對應的實現,例如vue-router、react-router。javascript
咱們不想探究vue-router或者react-router們的實現,由於不論是哪一種路由無外乎用兼容性更好的hash實現或者是H5 History實現,與框架幾個只須要作相應的封裝便可。html
提早聲明: 咱們沒有對傳入的參數進行及時判斷而規避錯誤,也沒有考慮兼容性問題,僅僅對核心方法進行了實現.前端
hash路由一個明顯的標誌是帶有#
,咱們主要是經過監聽url中的hash變化來進行路由跳轉。vue
hash的優點就是兼容性更好,在老版IE中都有運行,問題在於url中一直存在#
不夠美觀,並且hash路由更像是Hack而非標準,相信隨着發展更加標準化的History API會逐步蠶食掉hash路由的市場。 java
咱們用Class
關鍵字初始化一個路由.react
class Routers {
constructor() {
// 以鍵值對的形式儲存路由
this.routes = {};
// 當前路由的URL
this.currentUrl = '';
}
}
複製代碼
在初始化完畢後咱們須要思考兩個問題:git
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
}
// 將path路徑與對應的callback函數儲存
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 刷新
refresh() {
// 獲取當前URL中的hash路徑
this.currentUrl = location.hash.slice(1) || '/';
// 執行當前hash路徑的callback函數
this.routes[this.currentUrl]();
}
}
複製代碼
那麼咱們只須要在實例化Class
的時候監聽上面的事件便可.github
class Routers {
constructor() {
this.routes = {};
this.currentUrl = '';
this.refresh = this.refresh.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
this.routes[this.currentUrl]();
}
}
複製代碼
對應效果以下: 面試
完整示例vue-router
點擊這裏 hash router by 尋找海藍 (@xiaomuzhu) on CodePen.
上一節咱們只實現了簡單的路由功能,沒有咱們經常使用的回退與前進功能,因此咱們須要進行改造。
咱們在須要建立一個數組history
來儲存過往的hash路由例如/blue
,而且建立一個指針currentIndex
來隨着後退和前進功能移動來指向不一樣的hash路由。
class Routers {
constructor() {
// 儲存hash與callback鍵值對
this.routes = {};
// 當前hash
this.currentUrl = '';
// 記錄出現過的hash
this.history = [];
// 做爲指針,默認指向this.history的末尾,根據後退前進指向history中不一樣的hash
this.currentIndex = this.history.length - 1;
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
// 將當前hash路由推入數組儲存
this.history.push(this.currentUrl);
// 指針向前移動
this.currentIndex++;
this.routes[this.currentUrl]();
}
// 後退功能
backOff() {
// 若是指針小於0的話就不存在對應hash路由了,所以鎖定指針爲0便可
this.currentIndex <= 0
? (this.currentIndex = 0)
: (this.currentIndex = this.currentIndex - 1);
// 隨着後退,location.hash也應該隨之變化
location.hash = `#${this.history[this.currentIndex]}`;
// 執行指針目前指向hash路由對應的callback
this.routes[this.history[this.currentIndex]]();
}
}
複製代碼
咱們看起來實現的不錯,但是出現了Bug,在後退的時候咱們每每須要點擊兩下。
點擊查看Bug示例 hash router by 尋找海藍 (@xiaomuzhu) on CodePen.
問題在於,咱們每次在後退都會執行相應的callback,這會觸發refresh()
執行,所以每次咱們後退,history
中都會被push
新的路由hash,currentIndex
也會向前移動,這顯然不是咱們想要的。
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
// 將當前hash路由推入數組儲存
this.history.push(this.currentUrl);
// 指針向前移動
this.currentIndex++;
this.routes[this.currentUrl]();
}
複製代碼
如圖所示,咱們每次點擊後退,對應的指針位置和數組被打印出來
咱們必須作一個判斷,若是是後退的話,咱們只須要執行回調函數,不須要添加數組和移動指針。
class Routers {
constructor() {
// 儲存hash與callback鍵值對
this.routes = {};
// 當前hash
this.currentUrl = '';
// 記錄出現過的hash
this.history = [];
// 做爲指針,默認指向this.history的末尾,根據後退前進指向history中不一樣的hash
this.currentIndex = this.history.length - 1;
this.refresh = this.refresh.bind(this);
this.backOff = this.backOff.bind(this);
// 默認不是後退操做
this.isBack = false;
window.addEventListener('load', this.refresh, false);
window.addEventListener('hashchange', this.refresh, false);
}
route(path, callback) {
this.routes[path] = callback || function() {};
}
refresh() {
this.currentUrl = location.hash.slice(1) || '/';
if (!this.isBack) {
// 若是不是後退操做,且當前指針小於數組總長度,直接截取指針以前的部分儲存下來
// 此操做來避免當點擊後退按鈕以後,再進行正常跳轉,指針會停留在原地,而數組添加新hash路由
// 避免再次形成指針的不匹配,咱們直接截取指針以前的數組
// 此操做同時與瀏覽器自帶後退功能的行爲保持一致
if (this.currentIndex < this.history.length - 1)
this.history = this.history.slice(0, this.currentIndex + 1);
this.history.push(this.currentUrl);
this.currentIndex++;
}
this.routes[this.currentUrl]();
console.log('指針:', this.currentIndex, 'history:', this.history);
this.isBack = false;
}
// 後退功能
backOff() {
// 後退操做設置爲true
this.isBack = true;
this.currentIndex <= 0
? (this.currentIndex = 0)
: (this.currentIndex = this.currentIndex - 1);
location.hash = `#${this.history[this.currentIndex]}`;
this.routes[this.history[this.currentIndex]]();
}
}
複製代碼
查看完整示例 Hash Router by 尋找海藍 (@xiaomuzhu) on CodePen.
前進的部分就不實現了,思路咱們已經講得比較清楚了,能夠看出來,hash路由這種方式確實有點繁瑣,因此HTML5標準提供了History API供咱們使用。
咱們能夠直接在瀏覽器中查詢出History API的方法和屬性。
固然,咱們經常使用的方法實際上是有限的,若是想全面瞭解能夠去MDN查詢History API的資料。
咱們只簡單看一下經常使用的API
window.history.back(); // 後退
window.history.forward(); // 前進
window.history.go(-3); // 後退三個頁面
複製代碼
history.pushState
用於在瀏覽歷史中添加歷史記錄,可是並不觸發跳轉,此方法接受三個參數,依次爲:
state
:一個與指定網址相關的狀態對象,popstate
事件觸發時,該對象會傳入回調函數。若是不須要這個對象,此處能夠填null
。
title
:新頁面的標題,可是全部瀏覽器目前都忽略這個值,所以這裏能夠填null
。
url
:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。
history.replaceState
方法的參數與pushState
方法如出一轍,區別是它修改瀏覽歷史中當前紀錄,而非添加記錄,一樣不觸發跳轉。
popstate
事件,每當同一個文檔的瀏覽歷史(即history對象)出現變化時,就會觸發popstate事件。
須要注意的是,僅僅調用pushState
方法或replaceState
方法 ,並不會觸發該事件,只有用戶點擊瀏覽器倒退按鈕和前進按鈕,或者使用 JavaScript 調用back
、forward
、go
方法時纔會觸發。
另外,該事件只針對同一個文檔,若是瀏覽歷史的切換,致使加載不一樣的文檔,該事件也不會觸發。
以上API介紹選自history對象,能夠點擊查看完整版,咱們不想佔用過多篇幅來介紹API。
上一節咱們介紹了新標準的History API,相比於咱們在Hash 路由實現的那些操做,很顯然新標準讓咱們的實現更加方便和可讀。
因此一個mini路由實現起來其實很簡單
class Routers {
constructor() {
this.routes = {};
// 在初始化時監聽popstate事件
this._bindPopState();
}
// 初始化路由
init(path) {
history.replaceState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 將路徑和對應回調函數加入hashMap儲存
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 觸發路由對應回調
go(path) {
history.pushState({path: path}, null, path);
this.routes[path] && this.routes[path]();
}
// 監聽popstate事件
_bindPopState() {
window.addEventListener('popstate', e => {
const path = e.state && e.state.path;
this.routes[path] && this.routes[path]();
});
}
}
複製代碼
點擊查看H5路由 H5 Router by 尋找海藍 (@xiaomuzhu) on CodePen.
咱們大體探究了前端路由的兩種實現方法,在沒有兼容性要求的狀況下顯然符合標準的History API實現的路由是更好的選擇。
想更深刻了解前端路由實現能夠閱讀vue-router代碼,除去開發模式代碼、註釋和類型檢測代碼,核心代碼並很少,適合閱讀。
下期準備一篇關於雙向綁定的話題,由於許多人只知道Object.definedProperty
,禁不住深究:
Proxy
相比有何優劣?因爲涉及的框架和知識點過多,我開了一個頭已經小2000字了,,在考慮要不要分上下篇發出來,不過我相信它解決你對雙向綁定全部的疑問。