基于History实现前端路由

164 阅读3分钟

History是 HTML5 新增的标准,对比hash它的展示更加优雅,但低版本 ie 还有兼容问题。

首先History表示窗口浏览历史,可以通过pushState方法添加历史记录,以及通过go方法来实现跳转,还有popstate事件可以监听到记录变更。

下面就来分析实现一个History路由的基础是什么

  1. 它需要在变更地址的时候,不会导致文档直接跳转,例如hash路由的出现是因为 url 的hash变更不会导致文档变化;
  2. 需要有事件可以辅助我们监听到这个变化;

这两个要求History都能很好满足,当使用pushState方法添加一条记录的时候,只会导致浏览器的地址栏发生变化,但是不会跳转到这个地址,并且浏览器也不会检查这个地址,利用这个特性我们可以实现比较优雅的路由地址,例如https://xxx.com/aa就是这个路由地址。

为了方便理解下面实现的代码,简短介绍一下pushState

pushState

pushState方法,它有三个参数

  • 第一个是state,表示发生popstate事件的时候传递的对象;
  • title,当前浏览器已经忽略了,传递''就好了;
  • 网址,要更改的地址,注意要符合同源政策;

举个例子,当前网站的根路径是https://xxx.com,我们使用pushState添加一条记录

history.pushState({ path: "/abc" }, "", "/abc");

当前的网址就是https://xxx.com/abc

popstate

pushState事件触发的条件是用户点击前进后退按钮或者调用 History.back()History.forward()History.go()方法时才会触发。

触发事件后有一个event对象,它有一个state属性,就是对应pushState方法的第一个参数。

更多关于history的信息可以查阅MDN

history 路由实现

class Router {
  constructor() {
    this.routes = new Map();
    this.init();
  }
  change(e) {
    // 防止为null
    const { path } = e.state || {};
    this.implement(path);
  }
  init() {
    window.addEventListener("popstate", this.change.bind(this));
    window.addEventListener("load", () => {
      const { pathname } = location;
      history.replaceState({ path: pathname }, "", pathname);
      this.implement(pathname);
    });
  }
  implement(path) {
    if (!this.routes.has(path)) {
      return;
    }
    const fn = this.routes.get(path);
    typeof fn == "function" && fn.call(this, path);
  }
  go(num) {
    history.go(num);
  }
  route(state, fn) {
    this.routes.set(state, fn);
  }
  push(state) {
    history.pushState({ path: state }, "", state);
    this.implement(state);
  }
  replace(state) {
    history.replaceState({ path: state }, "", state);
    this.implement(state);
  }
}

这一块比较简单就不做更多赘述了,唯一一点需要注意的就是页面加载完毕也需要监听一次,这里用了load事件

使用方法

<ul>
  <li><a href="/">hash1</a></li>
  <li><a href="/hash2">hash2</a></li>
  <li><a href="/hash3">hash3</a></li>
</ul>
<div><button class="f">前进</button> <button class="b">后退</button></div>
const color = {
  "/": "yellow",
  "/hash2": "#333",
  "/hash3": "#DDD"
};
const route = new Router();
route.route("/", function(e) {
  document.body.style.background = color[e];
});
route.route("/hash2", function(e) {
  document.body.style.background = color[e];
});
route.route("/hash3", function(e) {
  document.body.style.background = color[e];
});
Array.from(document.links).forEach(fn => {
  fn.addEventListener("click", e => {
    e.preventDefault();
    const href = fn.href;
    const { pathname } = new URL(href);
    route.push(pathname);
  });
});
const backOff = document.querySelector(".b");
const forward = document.querySelector(".f");
backOff.addEventListener("click", () => route.go(-1));
forward.addEventListener("click", () => route.go(1));

这里注意,我直接屏蔽了a链接的默认跳转,这个是防止它直接到其他文档。

最后

关于前端路由的两种实现,这里就抛砖引玉的讲解完成了,剩下的就是在这个原理上更加完善,例如对 404 页面的处理。

如果有帮助可以 star 一下。