从了解Hash和Html5-History-到简单实现路由

34次阅读

共计 8473 个字符,预计需要花费 22 分钟才能阅读完成。

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>

具体代码可以查看 hash_demo.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
注意, 该方法不支持本地运行, 只能线上运作或者启动服务器查看效果

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

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

完整代码可以拷贝 router_demo

正文完
 0