1. 程式人生 > 實用技巧 >從瞭解Hash和Html5 History 到簡單實現路由

從瞭解Hash和Html5 History 到簡單實現路由

Hash

hash 屬性是一個可讀可寫的字串,該字串是 URL 的錨部分(從 # 號開始的部分),在頁面中的hash有多種功能意義:

錨點

url: http://www.example.com/index.html#jump
dom: <a name="jump"></a> 或者 <div id="jump" >

瀏覽器讀取到hash之後自動滾動到該對應元素所在位置的可視區域內

不附加在請求上

意味著它不管怎麼變化都不會影響請求URL,即它只針對瀏覽器的.

瀏覽器: http://www.example.com/index.html#jump
伺服器: http://www.example.com/index.html

注意: 有種情況是你會在URL上帶#符號,但是你本意不是作為hash使用的,例如回撥地址或者傳參之類,這時候瀏覽器只會當做hash處理,所以需要先轉碼.

// 未轉碼
瀏覽器: http://www.example.com/index.html?test=#123
伺服器: http://www.example.com/index.html?test=

// 轉碼
瀏覽器: http://www.example.com/index.html?test=%23123
伺服器: http://www.example.com/index.html?test=%23123

改變訪問歷史但不會觸發頁面重新整理

這個大家都知道,儘管它不會跳轉也不會重新整理,但是你能通過點選瀏覽器前進後退發現它也會被新增去訪問歷史記錄裡.(低版本IE不考慮)

缺點

  • 搜尋引擎不友好
  • 難以追蹤使用者行為

思路

當URL的片段識別符號更改時,將觸發hashchange事件 (跟在#符號後面的URL部分,包括#符號),然後根據hash值做些路由跳轉處理的操作.具體引數可以訪問location檢視

http://www.example.com/index.html#jump

最基本的路由實現方法監聽事件根據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

DOM window 物件通過 history 物件提供了對瀏覽器的會話歷史的訪問。它暴露了很多有用的方法和屬性,允許你在使用者瀏覽歷史中向前和向後跳轉

向前和向後跳轉

window.history.back();
window.history.forward();

跳轉到 history 中指定的一個點

你可以用 go() 方法載入到會話歷史中的某一特定頁面, 通過與當前頁面相對位置來標誌 (當前頁面的相對位置標誌為0).

window.history.go();

新增歷史記錄中的條目

不會立即載入頁面的情況下改變了當前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.replaceState(state, title , URL);

popstate 事件

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

獲取當前狀態

頁面載入時,或許會有個非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>

注意,該方法不支援本地執行,只能線上運作或者啟動伺服器檢視效果

html5_demo.html:26 Uncaught DOMException: Failed to execute 'pushState' on 'History': A history state object with URL 'file:///C:/b' cannot be created in a document with origin 'null' and URL 'file:///C:/work/project/router_demo/src/html5_demo.html'.
at HTMLAnchorElement.<anonymous> (file:///C:/work/project/router_demo/src/html5_demo.html:26:15)
(anonymous) @ html5_demo.html:26

簡單封裝路由庫

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)
  }
}

廣州VI設計公司https://www.houdianzi.com

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)
  }
}