两种前端路由实现的方式hash模式和history模式的详解与区别

3,052 阅读11分钟

众所周知,vue和react是大家普遍在使用的前端框架,而框架在构建单页面应用的时候都缺少不了路由, vue对应的有vue-router,react对应的有react-router-dom,而在react-router-dom之前有一个叫做react-router的依赖,那么它们两个有什么区别呢

react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件

也就是说react-router有的组件或者子项,react-router-dom一定有

BrowserRouter和HashRouter 这两个组件,前者使用pushState和popState事件构建路由,基于history模式,后者使用window.location.hash和hashchange事件构建路由,基于hash模式,那么什么是history,什么是hash呢?

先说说hash

HTML中的hash(#号)

1、#的涵义

#代表网页中的一个位置。右面的字符就是代表的位置信息:如

http://localhost:8081/cbuild/index.html#first就代表网页index.html的first位置。浏览器读取这个URL后,会自动将first位置滚动至可视区域。

为网页制定标识符: 一是使用锚点,比如。 二是使用id属性,比如<divid="print" >。

2、HTTP请求不包括#

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

浏览器实际发出的请求是这样的: GET /index.html HTTP/1.1 不包含#first

3、#后的字符

在第一个#后面出现的任何字符,都会被浏览器解读为位置标识符。这意味着,这些字符都不会被发送到服务器端。 比如,下面URL的原意是指定一个颜色值: www.example.com/?color=#fff 但是,浏览器实际发出的请求是: GET /?color= HTTP/1.1 Host: www.example.com 可以看到,"#fff"被省略了。只有将#转码为%23,浏览器才会将其作为实义字符处理。也就是说,上面的网址应该被写成: example.com/?color=%23f…

4、改变#不触发网页重载

单单改变#后的部分,浏览器只会滚动到相应位置,不会重新加载网页。 比如,从 www.example.com/index.html#… 改成 www.example.com/index.html#… 浏览器不会重新向服务器请求index.html。

5、改变#会改变浏览器的访问历史

每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置。 这对于ajax应用程序特别有用,可以用不同的#值,表示不同的访问状态,然后向用户给出可以访问某个状态的链接。 值得注意的是,上述规则对IE6和IE7不成立,它们不会因为#的改变而增加历史记录。

6、window.location.hash读取#值

window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录。

7、onhashchange事件

这是一个HTML 5新增的事件,当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件。 它的使用方法有三种: window.onhashchange = func;

window.addEventListener("hashchange",func, false); 对于不支持onhashchange的浏览器,可以用setInterval监控location.hash的变化。

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

以上是所有内容了