基于hash实现前端路由

495 阅读4分钟

前端路由分为两部分,hash 路由和 History 路由,例如我们常用的 vue-roter 就包含这两部分,这里并不探讨框架是如何进行封装的,而是使用原生的 api 来实现这样一个功能。

预计分为两部分,这里先介绍 hash 路由,实现基本的接受响应和前进后退(为了方便,下面代码不做任何兼容处理)

hash 指的就是 url 标识符后面#号部分的内容(包含#),例如:https://xxx#abc这个 url 的 hash 就是#abc

而 hash 路由就是指接收 hash 的变化更新对应的路由视图,它的优点就是兼容性很好,在 ie 下也能正常工作,不足之处就是#这个符号很丑陋。

监听 hash 变化

window 对象上有hashchange事件可以监听到 hash 的变化,我们先拿来用用,看看好不好用。

class Router {
  constructor() {
    this.hash = new Map();
    this.change = () => {
      const href = this.getHash();
      this.move(href);
    };
    window.addEventListener("hashchange", this.change);
  }
  route(href, fn) {
    this.hash.set(href, fn);
  }
  move(href) {
    if (!this.hash.has(href)) {
      return false;
    }
    const fn = this.hash.get(href);
    if (typeof fn == "function") {
      fn.call(this, href);
    }
    return true;
  }
  getHash() {
    return location.hash ? location.hash.slice(1) : "/";
  }
}

使用方法

const color = {
  hash1: "#333",
  hash2: "#666",
  hash3: "#DDD"
};
const route = new Router();
route.route("hash1", 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];
});

这里已经实现了 hash 响应了,不过还是有一个问题就是如果点击 hash 之后刷新浏览器,对应的回调函数并不会执行,所以我们还需要浏览器加载完成后响应对应的回调函数,修订这个问题很简单监听 load 事件即可。

// 省略其他代码
constructor() {
    window.addEventListener('load', this.change);
    window.addEventListener("hashchange", this.change);
  }

后退功能

上面完成了基础功能,下面对这个代码进行改造,首先新增一个后退处理。

实现的思路是新增一个history数组来储存变化的 hash,并且创建一个指针,在后退的时候移动指针变化,同时触发对应的函数。

class Router {
  constructor() {
    this.hash = new Map();
    // 储存hash变化的数组
    this.history = [];
    // 指针
    this.pointer = this.history.length - 1;
    this.change = () => {
      const href = this.getHash();
      if (!this.move(href)) {
        return console.error(`hash路由${href}无对应处理函数`);
      }
      // 在路由发生变化的时候,同时对history添加,和移动指针
      this.history.push(href);
      this.pointer++;
    };
    // 后退
    this.backOff = () => {
      // 数组下表不能负
      if (this.pointer <= 0) {
        this.pointer = 0;
      } else {
        // 移动下标
        this.pointer -= 1;
      }
      // 读取对应的值,移动hash
      const href = this.history[this.pointer];
      console.log(href, this.pointer, this.history);
      this.setHash(href);
      this.move(href);
    };
    window.addEventListener("load", this.change);
    window.addEventListener("hashchange", this.change);
  }
  route(href, fn) {
    this.hash.set(href, fn);
  }
  move(href) {
    if (!this.hash.has(href)) {
      return false;
    }
    const fn = this.hash.get(href);
    if (typeof fn == "function") {
      fn.call(this, href);
    }
    return true;
  }
  getHash() {
    // 过滤掉'#'
    return location.hash ? location.hash.slice(1) : "/";
  }
  setHash(href) {
    if (!href) {
      return;
    }
    if (!/^/.test(href)) {
      href = `#${href}`;
    }
    location.hash = href;
  }
}

上面看似实现了后退功能,不过仔细观察上面的代码,我们会发现这样实现是有问题的

  1. 回调函数被执行两次;
  2. history数组记录的 hash 不对,因为后退是不需要记录的;

下面来修正这两个问题。

class Router {
  constructor() {
    this.hash = new Map();
    // 储存hash变化的数组
    this.history = [];
    // 指针
    this.pointer = this.history.length - 1;
    // 是否后退,默认给false
    this.isBackOff = false;
    this.change = () => {
      const href = this.getHash();
      if (!this.hash.has(href)) {
        return console.error(`hash路由${href}无对应处理函数`);
      }
      if (!this.isBackOff) {
        // 看一下指针长度对不对,如果多余就截取掉
        if (this.pointer < this.history.length - 1) {
          this.history = this.history.slice(0, this.pointer + 1);
        }
        // 在路由发生变化的时候,同时对history添加,和移动指针
        this.history.push(href);
        this.pointer++;
      }
      this.move(href);
      this.isBackOff = false;
    };
    // 后退
    this.backOff = () => {
      this.isBackOff = true;
      // 数组下表不能负
      if (this.pointer <= 0) {
        this.pointer = 0;
      } else {
        // 移动下标
        this.pointer -= 1;
      }
      // 读取对应的值,移动hash
      const href = this.history[this.pointer];
      console.log(href, this.pointer, this.history);
      this.setHash(href);
    };
    window.addEventListener("load", this.change);
    window.addEventListener("hashchange", this.change);
  }
  route(href, fn) {
    this.hash.set(href, fn);
  }
  move(href) {
    const fn = this.hash.get(href);
    if (typeof fn == "function") {
      fn.call(this, href);
    }
  }
  getHash() {
    // 过滤掉'#'
    return location.hash ? location.hash.slice(1) : "/";
  }
  setHash(href) {
    if (!href) {
      return;
    }
    if (!/^/.test(href)) {
      href = `#${href}`;
    }
    location.hash = href;
  }
}

借助变量isBackOff来控制,如果是后退只执行回调函数,当正常操作的时候新增history数据,同时清楚多余数组,前进跟后退类似,下面就来实现它。

前进

思路跟后退的一致,借助变量来进行控制,定义一个前进方法。

class Router {
  constructor() {
    this.hash = new Map();
    // 储存hash变化的数组
    this.history = [];
    // 指针
    this.pointer = this.history.length - 1;
    // 是否后退,默认给false
    this.isBackOff = false;
    // 前进标识
    this.isForward = false;
    this.change = () => {
      const href = this.getHash();
      if (!this.hash.has(href)) {
        return console.error(`hash路由${href}无对应处理函数`);
      }
      if (!this.isBackOff && !this.isForward) {
        // 看一下指针长度对不对,如果多余就截取掉
        if (this.pointer < this.history.length - 1) {
          this.history = this.history.slice(0, this.pointer + 1);
        }
        // 在路由发生变化的时候,同时对history添加,和移动指针
        this.history.push(href);
        this.pointer++;
      }
      this.move(href);
      this.isBackOff = false;
      this.isForward = false;
    };
    // 后退
    this.backOff = () => {
      this.isBackOff = true;
      // 数组下表不能负
      if (this.pointer <= 0) {
        this.pointer = 0;
      } else {
        // 移动下标
        this.pointer -= 1;
      }
      // 读取对应的值,移动hash
      const href = this.history[this.pointer];
      console.log(href, this.pointer, this.history);
      this.setHash(href);
    };
    // 前进
    this.forward = () => {
      this.isForward = true;
      // 数组下表不能负
      if (this.pointer >= this.history.length - 1) {
        this.pointer = this.history.length - 1;
      } else {
        // 移动下标
        this.pointer += 1;
      }
      // 读取对应的值,移动hash
      const href = this.history[this.pointer];
      console.log(`前进${href}`, this.pointer, this.history);
      this.setHash(href);
    };
    window.addEventListener("load", this.change);
    window.addEventListener("hashchange", this.change);
  }
  route(href, fn) {
    this.hash.set(href, fn);
  }
  move(href) {
    const fn = this.hash.get(href);
    if (typeof fn == "function") {
      fn.call(this, href);
    }
  }
  getHash() {
    // 过滤掉'#'
    return location.hash ? location.hash.slice(1) : "/";
  }
  setHash(href) {
    if (!href) {
      return;
    }
    if (!/^/.test(href)) {
      href = `#${href}`;
    }
    location.hash = href;
  }
}

跟后退对比,实质上也只是把后退指针改成前进了,如果想要实现go跳转的功能也跟前进、后退思路一样。

使用方法

<ul>
  <li><a href="#hash1">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>
1
const color = {
  hash1: "#333",
  hash2: "#666",
  hash3: "#DDD"
};
const route = new Router();
route.route("hash1", 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];
});
const backOff = document.querySelector(".b");
const forward = document.querySelector(".f");
backOff.addEventListener("click", route.backOff);
forward.addEventListener("click", route.forward);

最后

这里实现了 hash 路由的响应变化前进后退404 的功能,但是还有很多地方需要完善,比如go跳转。

下一节将介绍 History 路由的实现,如果喜欢请点击一下 star