yugasun
Published on

前端路由实现 - history篇

Authors
  • avatar
    Name
    Yuga Sun
    Twitter

在上一篇 前端路由实现-hash 篇 已经介绍了如何通过 hash 的方式来实现前端路由,这一篇将在此基础上增加 history 路由的方式,想要实现此功能,必须先了解 History API

History API

官方 History API 已经讲得很清楚了,我这里主要列举出来,方便参考。

History API 是 HTML5 新增的历史记录 API,它可以实现无刷新的更改地址栏链接,简单讲,就是当页面为 yugasun.com, 执行 Javascript 语句:

window.history.pushState(null, null, '/about')

之后,地址栏地址就会变成 yugasun.com/about, 但浏览器不会刷新页面,甚至不会检测目标页面是否存在。

History 对象属性

| 属性 | 只读 | 描述 | | ------------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------ | | History.length | 是 | 返回一个整数,该整数表示会话历史中元素的数目,包括当前加载的页。例如,在一个新的选项卡加载的一个页面中,这个属性返回 1。 | | History.scrollRestoration | | 允许 Web 应用程序在历史导航上显式地设置默认滚动恢复行为。此属性可以是自动的(auto)或者手动的(manual)。 | | History.state | 是 | 返回一个表示历史堆栈顶部的状态的值。这是一种可以不必等待 popstate 事件而查看状态而的方式。 |

History 对象方法

| 方法 | 描述 | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | History.back() | 返回到上一条 history 记录,等同于: history.go(-1); | | History.forward() | 前进到吓一跳 history 记录,等同于: history.go(1); | | History.go() | 加载 history 中存储的指定标识的记录,以当前记录为基准 0,下一条为 1,上一条为-1,一次类推。 | | History.pushState() | 将指定的记录 push 到 history 记录栈中。三个参数分辨为: state object - 状态对象,title - 火狐浏览器已经忽略此参数,一般传入 null 值,URL - 新的历史记录的地址。 | | History.replaceState() | 替换当前的历史记录, 参数同 pushState 一致 |

实现原理

  • 通常点击页面 a 链接,页面会刷新跳转,所以需要监听页面所有 a 链接点击事件,并阻止默认事件, 然后调用 history.pushState() 方法来实现路由切换

当活动历史记录条目更改时,将触发 popstate 事件, 需要注意的是,调用 history.pushState()history.replaceState() 不会触发 popstate 事件。只有在做出浏览器动作时,才会触发该事件,如用户点击浏览器的回退/前进按钮。

代码实现

1.给上一篇中实现的 SPARouter 类构造函数添加一个参数 mode(默认为 hash), 如果传入值为 history 时, 我们就采用 history 路油方式。

constructor(el, routers, mode) {
    this.mode = mode || 'hash';
    //...
}

2.给 window 的popstate添加事件监听,给所有 a 链接添加 click 事件监听

  initEvent() {
    window.addEventListener('load', () => {
      console.log('load')
      this.routeUpdate();
    });
    if (this.mode === 'history') {
      window.addEventListener('popstate', (e) => {
        console.log('popstate')
        this.routeUpdate();
      });
      // 禁用所有a 链接默认跳转事件
      let self = this;
      document.addEventListener('click', function (e) {
        let target = e.target || e.srcElement;
        if (target.tagName === 'A') {
          e.preventDefault(); // 这里阻止默认事件
          let href = target.getAttribute('href');
          let path = href.split('?')[0];
          window
            .history
            .pushState({
              path: path
            }, null, href);
          self.routeUpdate();
        }
      })
    } else {
      window.addEventListener('hashchange', () => {
        console.log('hashchange')
        this.routeUpdate();
      });
    }
  }

3.新增获取当前 history state 方法:

getHistoryRoute() {
    // 默认第一次加载时,获取不到state值,这里需要兼容处理
    let path = (window.history.state && window.history.state.path) || '';
    let queryStr = window
      .location
      .hash
      .split('?')[1];
    let params = queryStr
      ? queryStr.split('&')
      : [];
    let query = {};
    params.map((item) => {
      let temp = item.split('=');
      query[temp[0]] = temp[1];
    });
    return {path: path, query: query};
}

4.对路由更新方法 routerUpdate 添加 mode 条件判断

routeUpdate() {
    let getLocation = this.mode === 'history'
      ? this.utils.getHistoryRoute
      : this.utils.getHashRoute;
    let currentLocation = getLocation();
    this.currentRoute.query = currentLocation['query']
    this
      .routers
      .map((item) => {
        if (item.path === currentLocation.path) {
          this.currentRoute = item;
          this.refresh();Ï
        }
      });
    if (!this.currentRoute.path) {
      if (this.mode === 'history') {
        window
          .history
          .pushState({
            path: '/index'
          }, null, '/index');
        this.routeUpdate();
      } else {
        location.hash = '/index';
      }
    }
}

做完以上更新后,我们的 SPARouter 类就实现了 history 路由功能了,然后将页面中链接中删除 #,然后实例化 SPARouter 时添加第三个参数为 history, 如下:

var router = new SPARouter(el, routes, 'history')

运行效果图