1. 程式人生 > >前端路由&vue-router部分解讀

前端路由&vue-router部分解讀

前端路由

由於SPA單頁應用以及前後端分離的思想的出現,前端路由在當今的前端開發中是必不可少的。一些流行框架vue、react都有對應的實現vue-router、react-router等。

框架路由的實現都是對原生路由進行了封裝。原生路由我們通常採用hash實現或者H5的history實現。

hash路由

hash路由:一個明顯的標誌就是帶有#,我們主要是通過監聽url中的hash變化來進行路由跳轉。 hash的優勢就是具備好的相容性,唯一的不好就是url中會一直存在#不夠美觀,看起來更像是hack實現。

下面基於hash實現一個簡單的路由。(採用ES6的語法)

class Routers {
    constructor() {
        // 以鍵值對形式儲存路由
        this.routes = {};
        // 當前路由的URL
        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); } // 將path路徑與對應的callback函式儲存 route(path, callback) { this
.routes[path] = callback || function() {}; } // 重新整理 refresh() { // 獲取當前URL中的hash路徑 this.currentUrl = location.hash.slice(1) || '/'; if(!this.isBack) { if (this.currentIndex < this.history.length - 1) { this.history = this.history.slice(0, this.currentIndex + 1); } this.history.push(this.currentUrl); this.currentIndex++; } // 執行當前hash路徑的callback函式 this.routes[this.currentUrl](); this.isBack = false; } // 後退 backOff() { // 後退操作設定為true this.isBack = true; // 如果指標小於0的話就不存在對應的hash路由 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]](); } }
    // 呼叫
    Router.route('/',function() {

    });

HTML5 history API路由

部分API介紹

window.history.back(); // 後退
window.history.forward(); // 前進
window.history.go(-3); // 後退3個頁面


// history.pushState用於在瀏覽歷史中新增歷史記錄,但是不觸發跳轉

// 三個引數

/*
state: 一個與指定網址相關的狀態物件,popstate事件觸發時,該物件會傳入回撥函式。如果不需要這個物件,此處可以填null。

title: 新頁面的標題,但是所有瀏覽器目前都忽略這個值,因此可以填null。

url:新的網址,必須與當前頁面處在同一個域。瀏覽器的位址列將顯示這個網址。
*/


// history.replaceState方法的引數與pushState方法一模一樣,區別是它修改瀏覽歷史中當前記錄,而非新增記錄,同樣不觸發跳轉。

/*
popstate事件,每當同一個文件的瀏覽歷史(即history物件)出現變化時,就會觸發popstate事件。僅僅呼叫pushState和replaceState方法,並不會觸發該事件,只有使用者點選瀏覽器倒退按鈕和前進按鈕,或者使用JavaScript呼叫back、forward、go方法時才會觸發。另外該事件只針對同一個文件,如果瀏覽歷史切換導致載入不同的文件,該事件也不會觸發。
*/

下面基於新的標準實現一個簡單路由

    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]();
            });
        }
    }

vue-router部分實現


    // 建立路由
    const routes = [
        {// 首頁
            path: '/',
            component: () => import('src/pages/home/index.vue')
        },
        {// 首頁更多功能
            path: '/functionManage',
            component: () => import('src/pages/home/functionManage.vue')
        }
    ]
    // router路由管理
    const router = new VueRouter({
        routes,
    })

    // router例項注入根例項
    window.vm = new Vue({
        router
    })

通過mode引數控制路由的實現模式。

    const router = new VueRouter({
    // HTML5 history 模式
    mode: 'history',
    base: process.env.NODE_ENV === 'production' ? process.env.PROXY_PATH : '',
    routes,
})

在入口檔案中需要例項化一個 VueRouter 的例項物件 ,然後將其傳入 Vue 例項的 options 中。

    var VueRouter = function VueRouter(options) {
        // match匹配函式
        this.matcher = createMatcher(options.routes || [], this);
        // 根據mode例項化具體的History,預設為'hash'模式
        var mode = options.mode || 'hash';
        // 通過supportsPushState判斷瀏覽器是否支援'history'模式
        // 如果設定的是'history'但是如果瀏覽器不支援的話,'history'模式會退回到'hash'模式
        // fallback 是當瀏覽器不支援 history.pushState 控制路由是否應該回退到 hash 模式。預設值為 true。
        this.fallback = mode === 'history' && !supportsPushState &&   options.fallback !== false;
        if (this.fallback) {
            mode = 'hash';
        }
        this.mode = mode;
        // 根據不同模式選擇例項化對應的History類
        switch (mode) {
            case 'history':
                this.history = new HTML5History(this, options.base);
                break;
            case 'hash': 
                this.history = new HashHistory(this, options.base, this.fallback);
                break;
        }
    }


    //作為引數傳入的字串屬性mode只是一個標記,用來指示實際起作用的物件屬性history的實現類,兩者對應關係:
    modehistory: 
        'history': HTML5History;
        'hash': HashHistory;
        'abstract': AbstractHistory;

在初始化對應的history之前,會對mode做一些校驗:若瀏覽器不支援HTML5History方式(通過supportsPushState變數判斷),則mode設為hash;若不是在瀏覽器環境下執行,則mode設為abstract; VueRouter類中的onReady(),push()等方法只是一個代理,實際是呼叫的具體history物件的對應方法,在init()方法中初始化時,也是根據history物件具體的類別執行不同操作

HashHistory

1、hash雖然出現在url中,但不會被包括在http請求中,它是用來指導瀏覽器動作的,對伺服器沒有影響,改變hash不會重新載入頁面。 2、監聽hash window.addEventListener(‘hashchagne’, function, false) 3、每一次改變hash(window.location.hash),都會在瀏覽器訪問歷史中增加一個記錄。 Vue作為漸進式的前端框架,本身的元件定義中應該是沒有有關路由內建屬性_route,如果元件中要有這個屬性,應該是在外掛載入的地方,即VueRouter的install()方法中混入Vue物件的,install.js的原始碼:

function install (Vue) {

  ...

  Vue.mixin({
    beforeCreate: function beforeCreate () {
      if (isDef(this.$options.router)) {
        this._routerRoot = this;
        this._router = this.$options.router;
        this._router.init(this);
        Vue.util.defineReactive(this, '_route', this._router.history.current);
      } else {
        this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
      }
      registerInstance(this, this);
    },
    destroyed: function destroyed () {
      registerInstance(this);
    }
  });
}

通過Vue.mixin()方法,全域性註冊一個混合,影響註冊之後所有建立的每個Vue例項,該混合在beforeCreate鉤子中通過Vue.util.defineReactive()定義了響應式的_route屬性。所謂響應式屬性,即當_route值改變時,會自動呼叫Vue例項的render()方法,更新檢視。

$router.push()–>HashHistory.push()–>History.transitionTo()–>History.updateRoute()–>{app._route=route}–>vm.render()

HashHistory.push()方法:將路由新增到瀏覽器訪問歷史棧頂(直接對window.location.hash賦值) HashHistory.replace()方法:替換掉當前路由(呼叫window.location.replace)

監聽位址列: 上面的VueRouter.push()和VueRouter.replace()是可以在vue元件的邏輯程式碼中直接呼叫的,除此之外在瀏覽器中,使用者還可以直接在瀏覽器位址列中輸入改變路由,因此還需要監聽瀏覽器位址列中路由的變化 ,並具有與通過程式碼呼叫相同的響應行為,在HashHistory中這一功能通過setupListeners監聽hashchange實現:相當於直接呼叫了replace方法。

HTML5History

History interface是瀏覽器歷史記錄棧提供的介面,通過back(),forward(),go()等方法,我們可以讀取瀏覽器歷史記錄棧的資訊,進行各種跳轉操作。 HTML5引入了history.pushState()和history.replaceState()方法,他們分別可以新增和修改歷史記錄條目。這些方法通常與window.onpopstate配合使用。 window.history.pushState(stateObject,title,url) window.history,replaceState(stateObject,title,url) 複製程式碼 stateObject:當瀏覽器跳轉到新的狀態時,將觸發popState事件,該事件將攜帶這個stateObject引數的副本 title:所新增記錄的標題 url:所新增記錄的url(可選的)

pushState和replaceState兩種方法的共同特點:當呼叫他們修改瀏覽器歷史棧後,雖然當前url改變了,但瀏覽器不會立即傳送請求該url,這就為單頁應用前端路由,更新檢視但不重新請求頁面提供了基礎。

export function pushState (url?: string, replace?: boolean) {
    saveScrollPosition()
    // 加了 try...catch 是因為 Safari 有呼叫 pushState 100 次限制
    // 一旦達到就會丟擲 DOM Exception 18 錯誤
    const history = window.history
    try {
        if (replace) {
            // replace 的話 key 還是當前的 key 沒必要生成新的
            history.replaceState({ key: _key }, '', url)
        } else {
            // 重新生成 key
            _key = genKey()
            // 帶入新的 key 值
            history.pushState({ key: _key }, '', url)
        }
    } catch (e) {
        // 達到限制了 則重新指定新的地址
        window.location[replace ? 'replace' : 'assign'](url)
    }
}

// 直接呼叫 pushState 傳入 replace 為 true
export function replaceState (url?: string) {
    pushState(url, true)
}

複製程式碼程式碼結構以及更新檢視的邏輯與hash模式基本類似,只不過將對window.location.hash()直接進行賦值window.location.replace()改為了呼叫history.pushState()和history.replaceState()方法。 在HTML5History中新增對修改瀏覽器位址列URL的監聽popstate是直接在建構函式中執行的:

constructor (router: Router, base: ?string) {

  window.addEventListener('popstate', e => {
    const current = this.current
    this.transitionTo(getLocation(this.base), route => {
      if (expectScroll) {
        handleScroll(router, route, current, true)
      }
    })
  })
}

複製程式碼以上就是’hash’和’history’兩種模式,都是通過瀏覽器介面實現的。

兩種模式比較

一般的需求場景中,hash模式與history模式是差不多的,根據MDN的介紹,呼叫history.pushState()相比於直接修改hash主要有以下優勢:

1、pushState設定的新url可以是與當前url同源的任意url,而hash只可修改#後面的部分,故只可設定與當前同文檔的url 2、pushState設定的新url可以與當前url一模一樣,這樣也會把記錄新增到棧中,而hash設定的新值必須與原來不一樣才會觸發記錄新增到棧中 3、pushState通過stateObject可以新增任意型別的資料記錄中,而hash只可新增短字串 4、pushState可額外設定title屬性供後續使用

history模式的問題

hash模式僅改變hash部分的內容,而hash部分是不會包含在http請求中的(hash帶#): http://oursite.com/#/user/id //如請求,只會傳送http://oursite.com/ 所以hash模式下遇到根據url請求頁面不會有問題 而history模式則將url修改的就和正常請求後端的url一樣(history不帶#) http://oursite.com/user/id 如果這種向後端傳送請求的話,後端沒有配置對應/user/id的get路由處理,會返回404錯誤。 官方推薦的解決辦法是在服務端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。同時這麼做以後,伺服器就不再返回 404 錯誤頁面,因為對於所有路徑都會返回 index.html 檔案。為了避免這種情況,在 Vue 應用裡面覆蓋所有的路由情況,然後在給出一個 404 頁面。或者,如果是用 Node.js 作後臺,可以使用服務端的路由來匹配 URL,當沒有匹配到路由的時候返回 404,從而實現 fallback。