你須要知道的單頁面路由實現原理

前言

最近開發的埋點項目,須要記錄用戶行爲軌跡即用戶頁面訪問順序。須要在頁面跳轉的時候,記錄用戶訪問的信息(好比 url ,請求頭部等),非單頁面應用能夠給 window 對象加上一個 beforeunload 事件,在頁面離開時觸發採集開關,可是如今不少業務是單頁面應用,用戶切換地址的時候,是無刷新的局部更新,沒有辦法觸發 beforeunload。因此單頁面應用的路由插件必定運用了 window 自帶的,無刷新修改用戶瀏覽記錄的方法,pushState 和 replaceState。javascript

pushState 和 replaceState 瞭解一下

history 提供了兩個方法,可以無刷新的修改用戶的瀏覽記錄,pushSate,和 replaceState,區別的 pushState 在用戶訪問頁面後面添加一個訪問記錄, replaceState 則是直接替換了當前訪問記錄html

history 對象的詳細信息已經有不少很好很詳細的介紹文獻,這裏再也不作總結,咱們引用阮老師的教程介紹,history對象 -- JavaScript 標準參考教程(alpha)前端

history.pushState

history.pushState方法接受三個參數,依次爲:vue

state:一個與指定網址相關的狀態對象,popstate事件觸發時,該對象會傳入回調函數。若是不須要這個對象,此處能夠填null。java

title:新頁面的標題,可是全部瀏覽器目前都忽略這個值,所以這裏能夠填null。git

url:新的網址,必須與當前頁面處在同一個域。瀏覽器的地址欄將顯示這個網址。 假定當前網址是example.com/1.html,咱們使用pushState方法在瀏覽記錄(history對象)中添加一個新記錄。github

var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
複製代碼

添加上面這個新記錄後,瀏覽器地址欄馬上顯示 example.com/2.html,但並不會跳轉到 2.html,甚至也不會檢查2.html 是否存在,它只是成爲瀏覽歷史中的最新記錄。這時,你在地址欄輸入一個新的地址(好比訪問 google.com ),而後點擊了倒退按鈕,頁面的 URL 將顯示 2.html;你再點擊一次倒退按鈕,URL 將顯示 1.html。vue-router

總之,pushState 方法不會觸發頁面刷新,只是致使 history 對象發生變化,地址欄會有反應。跨域

若是 pushState 的 url參數,設置了一個新的錨點值(即hash),並不會觸發 hashchange 事件。若是設置了一個跨域網址,則會報錯。瀏覽器

// 報錯
history.pushState(null, null, 'https://twitter.com/hello');

上面代碼中,pushState想要插入一個跨域的網址,致使報錯。這樣設計的目的是,防止惡意代碼讓用戶覺得他們是在另外一個網站上。
複製代碼

history.replaceState

history.replaceState 方法的參數與 pushState 方法如出一轍,區別是它修改瀏覽歷史中當前紀錄,假定當前網頁是 example.com/example.html。

history.pushState({page: 1}, 'title 1', '?page=1');
history.pushState({page: 2}, 'title 2', '?page=2');
history.replaceState({page: 3}, 'title 3', '?page=3');

history.back()
// url顯示爲http://example.com/example.html?page=1

history.back()
// url顯示爲http://example.com/example.html

history.go(2)
// url顯示爲http://example.com/example.html?page=3
複製代碼

單頁面應用用戶訪問軌跡埋點

開發過單頁面應用的同窗,必定比較清楚,單頁面應用的路由切換是無感知的,不會從新進行 http 請求去獲取頁面,而是經過改變頁面渲染視圖來實現。因此他的實現原理必定也是經過原生的 pushState 或則 replaceState 來實現的。因此在頁面跳轉的時候必定會調用 pushState 或則 replaceState ,要記錄用戶的跳轉信息,咱們只要攔截 pushState 和 replaceState,在執行默行爲先執行咱們的方法就可以採集到用戶的跳轉信息了

// 改寫思路:拷貝 window 默認的 replaceState 函數,重寫 history.replaceState 在方法裏插入咱們的採集行爲,在重寫的 replaceState 方法最後調用,window 默認的 replaceState 方法

collect = {}

collect.onPushStateCallback : function(){}  // 自定義的採集方法

(function(history){
    var replaceState = history.replaceState;   // 存儲原生 replaceState
    history.replaceState = function(state, param) {     // 改寫 replaceState
       var url = arguments[2];
       if (typeof collect.onPushStateCallback == "function") {
             collect.onPushStateCallback({state: state, param: param, url: url});   //自定義的採集行爲方法
       }
       return replaceState.apply(history, arguments);    // 調用原生的 replaceState
    };
 })(window.history);
複製代碼

vue-router 的路由實現

既然知道了這個原理,咱們來看下 vue-router 的實現,咱們打開 vue-router 項目地址,把項目克隆下來,或則直接在 github 上預覽,在 Vue 開發的項目裏,咱們經過 router.push('home') 來實現頁面的跳轉,因此咱們檢索下,push 方法的實現

查找push方法的實現

咱們檢索到了 20 個 js 文件,😂,通常到這個時候,咱們會放棄源碼閱讀,那麼咱們今天的文章就到這結束,謝謝你們!

開個玩笑,源碼閱讀不能這麼粗糙,咱們找到 src 目錄,點開 index.js 文件,看到 history對象的定義和 mode 參數有關

if (!inBrowser) {
  mode = 'abstract'
}
this.mode = mode

switch (mode) {
  case 'history':
    this.history = new HTML5History(this, options.base)
    break
  case 'hash':
    this.history = new HashHistory(this, options.base, this.fallback)
    break
  case 'abstract':
    this.history = new AbstractHistory(this, options.base)
    break
  default:
    if (process.env.NODE_ENV !== 'production') {
      assert(false, `invalid mode: ${mode}`)
    }
}
複製代碼

看到 history 對象的實例與配置的 mode 有關,vue-router 經過3中方式實現了路由切換。與咱們今天講的內容相匹配的是 HTML5History 的實現方案,其餘的將再也不文章中作擴展,若果你感興趣想要了解,能夠看文章後面的擴展閱讀

咱們來看 vue-router 中的 HTML5History 源碼:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    pushState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  const { current: fromRoute } = this
  this.transitionTo(location, route => {
    replaceState(cleanPath(this.base + route.fullPath))
    handleScroll(this.router, route, fromRoute, false)
    onComplete && onComplete(route)
  }, onAbort)
}

// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
  saveScrollPosition()
  // try...catch the pushState call to get around Safari
  // DOM Exception 18 where it limits to 100 pushState calls
  const history = window.history
  try {
    if (replace) {
      history.replaceState({ key: _key }, '', url)
    } else {
      _key = genKey()
      history.pushState({ key: _key }, '', url)
    }
  } catch (e) {
    window.location[replace ? 'replace' : 'assign'](url)
  }
}

export function replaceState (url?: string) {
  pushState(url, true)
}
複製代碼

在使用 Vue 開發的過程當中,咱們必定用到過 push 和 replace 來改變路由,和視圖。

router 實例調用的 push 實際是 history 的方法,經過 mode 來肯定匹配 history 的實現方案,從代碼中咱們看到,push 調用了 src/util/push-state.js 中被改寫過的 pushState 的方法,改寫過的方法會根據傳入的參數 replace?: boolean 來進行判斷調用 pushState 仍是 replaceState ,同時作了錯誤捕獲,若是,history 無刷新修改訪問路徑失敗,則調用 window.location.replace(url) ,有刷新的切換用戶訪問地址 ,同理 pushState 也是這樣。這裏的 transitionTo 方法主要的做用是作視圖的跟新及路由跳轉監測,若是 url 沒有變化(訪問地址切換失敗的狀況),在 transitionTo 方法內部還會調用一個 ensureURL 方法,來修改 url。 transitionTo 方法中應用的父方法比較多,這裏不作長篇贅述,具體代碼分析能夠關注後我之後的文章

模擬單頁面路由

經過上面的學習,咱們知道了,單頁面應用路由的實現原理,咱們也嘗試去實現一個。在作管理系統的時候,咱們一般會在頁面的左側放置一個固定的導航 sidebar,頁面的右側放與之匹配的內容 main 。點擊導航時,咱們只但願內容進行更新,若是刷新了整個頁面,到時導航和通用的頭部底部也進行重繪重排的話,十分浪費資源,體驗也會很差。這個時候,咱們就能用到咱們今天學習到的內容,經過使用 HTML5 的 pushState 方法和 replaceState 方法來實現,

思路:首先綁定 click 事件。當用戶點擊一個連接時,經過 preventDefault 函數防止默認的行爲(頁面跳轉),同時讀取連接的地址(若是有 jQuery,能夠寫成(this).attr('href')),把這個地址經過pushState塞入瀏覽器歷史記錄中,再利用 AJAX 技術拉取(若是有 jQuery,可使用.get方法)這個地址中真正的內容,同時替換當前網頁的內容。

爲了處理用戶前進、後退,咱們監聽 popstate 事件。當用戶點擊前進或後退按鈕時,瀏覽器地址自動被轉換成相應的地址,同時popstate事件發生。在事件處理函數中,咱們根據當前的地址抓取相應的內容,而後利用 AJAX 拉取這個地址的真正內容,呈現,便可。

最後,整個過程是不會改變頁面標題的,能夠經過直接對 document.title 賦值來更改頁面標題。

擴展

好了,咱們今天經過多個方面來說了 pushState 方法和 replaceState 的應用,你應該對這個兩個方法能有一個比較深入的印象,若是想要了解更多,你能夠參考如下連接

history對象 -- JavaScript 標準參考教程(alpha)

從vue-router看前端路由的兩種實現

相關文章
相關標籤/搜索