兩種前端路由實現的方式hash模式和history模式的詳解與區別

衆所周知,vue和react是你們廣泛在使用的前端框架,而框架在構建單頁面應用的時候都缺乏不了路由, vue對應的有vue-router,react對應的有react-router-dom,而在react-router-dom以前有一個叫作react-router的依賴,那麼它們兩個有什麼區別呢html

react-router-dom: 基於react-router,加入了在瀏覽器運行環境下的一些功能,例如:Link組件前端

也就是說react-router有的組件或者子項,react-router-dom必定有vue

BrowserRouter和HashRouter 這兩個組件,前者使用pushState和popState事件構建路由,基於history模式,後者使用window.location.hash和hashchange事件構建路由,基於hash模式,那麼什麼是history,什麼是hash呢?html5

先說說hashreact

HTML中的hash(#號)

一、#的涵義ajax

#表明網頁中的一個位置。右面的字符就是表明的位置信息:如vue-router

http://localhost:8081/cbuild/index.html#first就表明網頁index.html的first位置。瀏覽器讀取這個URL後,會自動將first位置滾動至可視區域。瀏覽器

爲網頁制定標識符: 一是使用錨點,好比。 二是使用id屬性,好比<divid="print" >。安全

二、HTTP請求不包括#bash

好比:http://localhost:8081/cbuild/index.html#first

瀏覽器實際發出的請求是這樣的: GET /index.html HTTP/1.1 不包含#first

三、#後的字符

在第一個#後面出現的任何字符,都會被瀏覽器解讀爲位置標識符。這意味着,這些字符都不會被髮送到服務器端。 好比,下面URL的原意是指定一個顏色值: www.example.com/?color=#fff 可是,瀏覽器實際發出的請求是: GET /?color= HTTP/1.1 Host: www.example.com 能夠看到,"#fff"被省略了。只有將#轉碼爲%23,瀏覽器纔會將其做爲實義字符處理。也就是說,上面的網址應該被寫成: example.com/?color=%23f…

四、改變#不觸發網頁重載

單單改變#後的部分,瀏覽器只會滾動到相應位置,不會從新加載網頁。 好比,從 www.example.com/index.html#… 改爲 www.example.com/index.html#… 瀏覽器不會從新向服務器請求index.html。

五、改變#會改變瀏覽器的訪問歷史

每一次改變#後的部分,都會在瀏覽器的訪問歷史中增長一個記錄,使用"後退"按鈕,就能夠回到上一個位置。 這對於ajax應用程序特別有用,能夠用不一樣的#值,表示不一樣的訪問狀態,而後向用戶給出能夠訪問某個狀態的連接。 值得注意的是,上述規則對IE6和IE7不成立,它們不會由於#的改變而增長曆史記錄。

六、window.location.hash讀取#值

window.location.hash這個屬性可讀可寫。讀取時,能夠用來判斷網頁狀態是否改變;寫入時,則會在不重載網頁的前提下,創造一條訪問歷史記錄。

七、onhashchange事件

這是一個HTML 5新增的事件,當#值發生變化時,就會觸發這個事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持該事件。 它的使用方法有三種: window.onhashchange = func;

window.addEventListener("hashchange",func, false); 對於不支持onhashchange的瀏覽器,能夠用setInterval監控location.hash的變化。

八、Google抓取#的機制

默認狀況下,Google的網絡蜘蛛忽視URL的#部分。 可是,Google還規定,若是你但願Ajax生成的內容被瀏覽引擎讀取,那麼URL中可使用"#!",Google會自動將其後面的內容轉成查詢字符串_escaped_fragment_的值。 好比,Google發現新版twitter的URL以下: twitter.com/#!/username 就會自動抓取另外一個URL: twitter.com/?escaped_fragment=/username 經過這種機制,Google就能夠索引動態的Ajax內容。

思路

當URL的片斷標識符更改時,將觸發hashchange事件 (跟在#符號後面的URL部分,包括#符號),而後根據hash值作些路由跳轉處理的操做.具體參數能夠訪問location查看

最基本的路由實現方法監聽事件根據location.hash判斷界面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>
        <a href="#/a">a</a>
      </li>
      <li>
        <a href="#/b">b</a>
      </li>
      <li>
        <a href="#/c">c</a>
      </li>
    </ul>
    <div id="view"></div>

    <script>
      var view = null;
      // 頁面加載完不會觸發 hashchange,這裏主動觸發一次 hashchange 事件,該事件快於onLoad,因此須要在這裏操做
      window.addEventListener('DOMContentLoaded', function () {
        view = document.querySelector('#view');
        viewChange();
      });
      // 監聽路由變化
      window.addEventListener('hashchange', viewChange);

      // 渲染視圖
      function viewChange() {
        switch (location.hash) {
          case '#/b':
            view.innerHTML = 'b';
            break;
          case '#/c':
            view.innerHTML = 'c';
            break;
          default:
            view.innerHTML = 'a';
            break;
        }
      }
</script>
  </body>
</html>
複製代碼

History

首先咱們在瀏覽器裏來看一下history

  • History.length (只讀)

    返回一個整數,該整數表示會話歷史中元素的數目,包括當前加載的頁。例如,在一個新的選項卡加載的一個頁面中,這個屬性返回1。

  • History.state (只讀)

    返回一個表示歷史堆棧頂部的狀態的值。這是一種能夠沒必要等待popstate 事件而查看狀態而的方式。

  • History.scrollRestoration

    容許Web應用程序在歷史導航上顯式地設置默認滾動恢復行爲。此屬性能夠是自動的(auto)或者手動的(manual)。

history的方法

  • History.back()

    前往上一頁, 用戶可點擊瀏覽器左上角的返回按鈕模擬此方法. 等價於 history.go(-1).

    注意:當瀏覽器會話歷史記錄處於第一頁時調用此方法沒有效果,並且也不會報錯。

  • History.forward()

    在瀏覽器歷史記錄裏前往下一頁,用戶可點擊瀏覽器左上角的前進按鈕模擬此方法. 等價於 history.go(1).

    注意:當瀏覽器歷史棧處於最頂端時( 當前頁面處於最後一頁時 )調用此方法沒有效果也不報錯。。

  • History.go()

    經過當前頁面的相對位置從瀏覽器歷史記錄( 會話記錄 )加載頁面。好比:參數爲-1的時候爲上一頁,參數爲1的時候爲下一頁. 當整數參數超出界限時,例如: 若是當前頁爲第一頁,前面已經沒有頁面了,我傳參的值爲-1,那麼這個方法沒有任何效果也不會報錯。調用沒有參數的 go() 方法或者不是整數的參數時也沒有效果。( 這點與支持字符串做爲url參數的IE有點不一樣)。傳0會刷新當前頁面。

添加歷史記錄中的條目

不會當即加載頁面的狀況下改變了當前URL地址,往歷史記錄添加一條條目,除非刷新頁面等操做

history.pushState(state, title , URL);
複製代碼

三個參數

  • 狀態對象

    state是一個JavaScript對象,popstate事件的state屬性包含該歷史記錄條目狀態對象的副本。

    狀態對象能夠是能被序列化的任何東西。緣由在於Firefox將狀態對象保存在用戶的磁盤上,以便在用戶重啓瀏覽器時使用,咱們規定了狀態對象在序列化表示後有640k的大小限制。若是你給 pushState() 方法傳了一個序列化後大於640k的狀態對象,該方法會拋出異常。若是你須要更大的空間,建議使用 sessionStorage 以及 localStorage.

  • 標題

    Firefox 目前忽略這個參數,但將來可能會用到。在此處傳一個空字符串應該能夠安全的防範將來這個方法的更改。或者,你能夠爲跳轉的state傳遞一個短標題。

  • URL

    新的歷史URL記錄。新URL沒必要須爲絕對路徑。若是新URL是相對路徑,那麼它將被做爲相對於當前URL處理。新URL必須與當前URL同源,不然 pushState() 會拋出一個異常。該參數是可選的,缺省爲當前URL。

    注意: pushState() 絕對不會觸發 hashchange 事件,即便新的URL與舊的URL僅哈希不一樣也是如此。

    更改歷史記錄中的當前條目

    不會當即加載頁面的狀況下改變了當前URL地址,並改變歷史記錄的當前條目,除非刷新頁面等操做

    history.pushState(state, title , URL);

    三個參數

    1. 狀態對象

    state是一個JavaScript對象,popstate事件的state屬性包含該歷史記錄條目狀態對象的副本。

    狀態對象能夠是能被序列化的任何東西。緣由在於Firefox將狀態對象保存在用戶的磁盤上,以便在用戶重啓瀏覽器時使用,咱們規定了狀態對象在序列化表示後有640k的大小限制。若是你給 pushState() 方法傳了一個序列化後大於640k的狀態對象,該方法會拋出異常。若是你須要更大的空間,建議使用 sessionStorage 以及 localStorage.

    1. 標題

    Firefox 目前忽略這個參數,但將來可能會用到。在此處傳一個空字符串應該能夠安全的防範將來這個方法的更改。或者,你能夠爲跳轉的state傳遞一個短標題。

    1. URL

    新的歷史URL記錄。新URL沒必要須爲絕對路徑。若是新URL是相對路徑,那麼它將被做爲相對於當前URL處理。新URL必須與當前URL同源,不然 pushState() 會拋出一個異常。該參數是可選的,缺省爲當前URL。

    注意: pushState() 絕對不會觸發 hashchange 事件,即便新的URL與舊的URL僅哈希不一樣也是如此。

更改歷史記錄中的當前條目

不會當即加載頁面的狀況下改變了當前URL地址,並改變歷史記錄的當前條目,除非刷新頁面等操做

`history.replaceState(state, title , URL);`
複製代碼

popstate 事件

每當活動的歷史記錄項發生變化時, popstate 事件都會被傳遞給window對象。若是當前活動的歷史記錄項是被 pushState 建立的,或者是由 replaceState 改變的,那麼 popstate 事件的狀態屬性 state 會包含一個當前歷史記錄狀態對象的拷貝。

window.onpopstate = function(event) {
  alert("location: " + document.location + ", state: " + JSON.stringify(event.state));
};
//綁定事件處理函數. 
history.pushState({page: 1}, "title 1", "?page=1");    //添加並激活一個歷史記錄條目 http://example.com/example.html?page=1,條目索引爲1
history.pushState({page: 2}, "title 2", "?page=2");    //添加並激活一個歷史記錄條目 http://example.com/example.html?page=2,條目索引爲2
history.replaceState({page: 3}, "title 3", "?page=3"); //修改當前激活的歷史記錄條目 http://ex..?page=2 變爲 http://ex..?page=3,條目索引爲3
history.back(); // 彈出 "location: http://example.com/example.html?page=1, state: {"page":1}"
history.back(); // 彈出 "location: http://example.com/example.html, state: null history.go(2); // 彈出 "location: http://example.com/example.html?page=3, state: {"page":3}

複製代碼

既然 history.pushState 和 history.replaceState 都不會觸發頁面的更新,咱們就須要手動給 window 對象添加 pushState 和 replaceState 事件,這個很重要!

const listenWrapper = function (type) {
     const _func = history[type];
     return function () {
       console.log(this);
       const func = _func.apply(this, arguments);
       const e = new Event(type);
       e.arguments = arguments;
       window.dispatchEvent(e);
       return func;
     };
   };
   history.pushState = listenWrapper('pushState');
   history.replaceState = listenWrapper('replaceState');
   window.addEventListener('pushState', function (e) {
     console.log(e)
   }); 
複製代碼
解釋一下:
1. 在pushState執行的時候建立自定義事件

 2. 在pushSatate外部寫自定義事件的監聽事件

 3. 在pushState執行的時候執行自定義事件
複製代碼
獲取當前狀態

頁面加載時,或許會有個非null的狀態對象。這是有可能發生的,舉個例子,假如頁面(經過pushState() 或 replaceState() 方法)設置了狀態對象然後用戶重啓了瀏覽器。那麼當頁面從新加載時,頁面會接收一個onload事件,但沒有 popstate 事件。然而,假如你讀取了history.state屬性,你將會獲得如同popstate 被觸發時能獲得的狀態對象。

你能夠讀取當前歷史記錄項的狀態對象state,而沒必要等待popstate 事件

思路

監聽點擊事件禁止默認跳轉操做,手動利用history實現一套跳轉邏輯,根據location.pathname渲染界面.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <meta http-equiv="X-UA-Compatible" content="ie=edge"/>
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li>
        <a href="/a">a</a>
      </li>
      <li>
        <a href="/b">b</a>
      </li>
      <li>
        <a href="/c">c</a>
      </li>
    </ul>
    <div id="view"></div>

    <script>
      var view = null;
      // 頁面加載完不會觸發 hashchange,這裏主動觸發一次 hashchange 事件,該事件快於onLoad,因此須要在這裏操做
      window.addEventListener('DOMContentLoaded', function () {
        view = document.querySelector('#view');
        document
          .querySelectorAll('a[href]')
          .forEach(e => e.addEventListener('click', function (_e) {
            _e.preventDefault();
            history.pushState(null, '', e.getAttribute('href'));
            viewChange();
          }));

        viewChange();
      });
      // 監聽路由變化
      window.addEventListener('popstate', viewChange);

      // 渲染視圖
      function viewChange() {
        switch (location.pathname) {
          case '/b':
            view.innerHTML = 'b';
            break;
          case '/c':
            view.innerHTML = 'c';
            break;
          default:
            view.innerHTML = 'a';
            break;
        }
      }
</script>
  </body>
</html>
複製代碼

簡單封裝路由庫

API

基本的路由方法:

router.push(url, onComplete)

router.replace(url, onComplete)

router.go(n)

router.back()

router.stop()
複製代碼
<!DOCTYPE html>
<html>
  <head>
    <title>router</title>
  </head>

  <body>
    <ul>
      <li onclick="router.push('/a', ()=>console.log('push a'))">push a</li>
      <li onclick="router.push('/b', ()=>console.log('push b'))">push b</li>
      <li onclick="router.replace('/c', ()=>console.log('replace c'))">replace c</li>
      <li onclick="router.go(1)">go</li>
      <li onclick="router.back(-1)">back</li>
      <li onclick="router.stop()">stop</li>
    </ul>
    <div id="view"></div>
  </body>
</html>
複製代碼

初始化

import Router from '../router'

window.router = new Router('view', {
  routes: [
    {
      path: '/a',
      component: '<p>a</p>'
    },
    {
      path: '/b',
      component: '<p>b</p>'
    },
    {
      path: '/c',
      component: '<p>c</p>'
    },
    { path: '*', redirect: '/index' }
  ]
}, 'hash')// 或者'html5'
複製代碼

router類

import HashHstory from "./HashHistory";
import Html5History from "./Html5History";

export default class Router {
  constructor(wrapper, options, mode = 'hash') {
    this._wrapper = document.querySelector(`#${wrapper}`)
    if (!this._wrapper) {
      throw new Error(`你須要提供一個容器元素插入`)
    }
    // 是否支持HTML5 History 模式
    this._supportsReplaceState = window.history && typeof window.history.replaceState === 'function'
    // 匹配路徑
    this._cache = {}
    // 默認路由
    this._defaultRouter = options.routes[0].path
    this.route(options.routes)
    // 啓用模式
    this._history = (mode !== 'hash' && this._supportsReplaceState) ? new Html5History(this, options) : new HashHstory(this, options)
  }

  // 添加路由
  route(routes) {
    routes.forEach(item => this._cache[item.path] = item.component)
  }

  // 原生瀏覽器前進
  go(n = 1) {
    window.history.go(n)
  }

  // 原生瀏覽器後退
  back(n = -1) {
    window.history.go(n)
  }

  // 增長
  push(url, onComplete) {
    this._history.push(url, onComplete)
  }

  // 替換
  replace(url, onComplete) {
    this._history.replace(url, onComplete)
  }

  // 移除事件
  stop() {
    this._history.stop()
  }
}
複製代碼

Hash Class

export default class HashHistory {
 constructor(router, options) {
   this.router = router
   this.onComplete = null
   // 監聽事件
   window.addEventListener('load', this.onChange)
   window.addEventListener('hashchange', this.onChange)
 }

 onChange = () => {
   // 匹配失敗重定向
   if (!location.hash || !this.router._cache[location.hash.slice(1)]) {
     window.location.hash = this.router._defaultRouter
   } else {
     // 渲染視圖
     this.router._wrapper.innerHTML = this.router._cache[location.hash.slice(1)]
     this.onComplete && this.onComplete() && (this.onComplete = null)
   }
 }

 push(url, onComplete) {
   window.location.hash = `${url}`
   onComplete && (this.onComplete = onComplete)
 }

 replace(url, onComplete) {
   // 優雅降級
   if (this.router._supportsReplaceState) {
     window.location.hash = `${url}`
     window.history.replaceState(null, null, `${window.location.origin}#${url}`)
   } else {
     // 須要先看看當前URL是否已經有hash值
     const href = location.href
     const index = href.indexOf('#')
     url = index > 0
       ? `${href.slice(0, index)}#${url}`
       : `${href}#${url}`
     // 域名不變的狀況下不會刷新頁面
     window.location.replace(url)
   }

   onComplete && (this.onComplete = onComplete)
 }

 // 移除事件
 stop() {
   window.removeEventListener('load', this.onChange)
   window.removeEventListener('hashchange', this.onChange)
 }
}
複製代碼

HTML5 Class

export default class Html5Hstory {
  constructor(router, options) {
    this.addEvent()
    this.router = router
    this.onComplete = null
    // 監聽事件
    window.addEventListener('popstate', this.onChange)
    window.addEventListener('load', this.onChange)
    window.addEventListener('replaceState', this.onChange);
    window.addEventListener('pushState', this.onChange);
  }

  // pushState/replaceState不會觸發popstate事件,因此咱們須要自定義
  addEvent() {
    const listenWrapper = function (type) {
      const _func = history[type];
      return function () {
        const func = _func.apply(this, arguments);
        const e = new Event(type);
        e.arguments = arguments;
        window.dispatchEvent(e);
        return func;
      };
    };
    history.pushState = listenWrapper('pushState');
    history.replaceState = listenWrapper('replaceState');
  }

  onChange() {
    // 匹配失敗重定向
    if (location.pathname === '/' || !this.router._cache[location.pathname]) {
      window.history.pushState(null, '', `${window.location.origin}${this.router._defaultRouter}`);
    } else {
      // 渲染視圖
      this.router._wrapper.innerHTML = this.router._cache[location.pathname]
      this.onComplete && this.onComplete() && (this.onComplete = null)
    }
  }

  push(url, onComplete) {
    window.history.pushState(null, '', `${window.location.origin}${url}`);
    onComplete && (this.onComplete = onComplete)
  }

  replace(url, onComplete) {
    window.history.replaceState(null, null, `${window.location.origin}${url}`)
    onComplete && (this.onComplete = onComplete)
  }

  // 移除事件
  stop() {
    window.removeEventListener('load', this.onChange)
    window.removeEventListener('popstate', this.onChange)
    window.removeEventListener('replaceState', this.onChange)
    window.removeEventListener('pushState', this.onChange)
  }
}
複製代碼

以上是全部內容了

相關文章
相關標籤/搜索