原来这就是hash模式和history模式的区别(vue-router mode)

7,604 阅读3分钟

前端路由原理

前端路由的核心,就在于改变视图的同时不会向后端发出请求;而是加载路由对应的组件。vue-router就是将组件映射到路由, 然后渲染出来的。并实现了三种模式:Hash模式、History模式以及Abstract模式。默认Hash模式,今天主要介绍Hash模式和History模式。

Hash模式

  • 原理
    基于浏览器的hashchange事件,地址变化时,通过window.location.hash 获取地址上的hash值;并通过构造Router类,配置routes对象设置hash值与对应的组件内容。
  • 优点
  1. hash值会出现在URL中, 但是不会被包含在Http请求中, 因此hash值改变不会重新加载页面
  2. hash改变会触发hashchange事件, 能控制浏览器的前进后退
  3. 兼容性好
  • 缺点
  1. 地址栏中携带#,不美观
  2. 只可修改 # 后面的部分,因此只能设置与当前 URL 同文档的 URL
  3. hash有体积限制,故只可添加短字符串
  4. 设置的新值必须与原来不一样才会触发hashchange事件,并将记录添加到栈中
  5. 每次URL的改变不属于一次http请求,所以不利于SEO优化
  • 代码实现
<!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>hash</title>
  </head>
  <body>
    <div class="main">
      <a href="#/a">a页面</a>
      <a href="#/b">b页面</a>
      <a href="#/c">c页面</a>
      <div id="content"></div>
    </div>

    <script>
      // Router 构造函数
      class Router {
        constructor(routers) {
          this.routers = {};
          routers.forEach((item) => {
            // 每个hash值 匹配对应component
            this.route(item.path, () => {
              document.getElementById("content").innerHTML = item.compontent;
            });
          });
          this.init();
        }
        route(path, cb) {
          this.routers[path] = cb;
        }
        init() {
          window.addEventListener("load", this.updateView.bind(this));
          // hash模式 路由修改时 浏览器会触发hashchange事件 调用更新视图函数
          window.addEventListener("hashchange", this.updateView.bind(this));
        }
        updateView(e) {
          // console.log("hash window.location", window.location);
          // 获取页面hash值 通过hash值更新对应的组件内容
          const hashTag = window.location.hash.slice(1) || "/";
          this.routers[hashTag] && this.routers[hashTag]();
        }
      }
      const routers = [
        {
          path: "/a",
          compontent: `<div>我是a页面</div>`,
        },
        {
          path: "/b",
          compontent: `<div>我是b页面</div>`,
        },
        {
          path: "/c",
          compontent: `<div>我是c页面</div>`,
        },
      ];
      new Router(routers);
    </script>
  </body>
</html>

History模式

  • 原理
    基于HTML5新增的pushState()和replaceState()两个api,以及浏览器的popstate事件,地址变化时,通过window.location.pathname找到对应的组件。并通过构造Router类,配置routes对象设置pathname值与对应的组件内容。

  • 优点

  1. 没有#,更加美观
  2. pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
  3. pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中
  4. pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
  5. pushState() 可额外设置 title 属性供后续使用
  6. 浏览器的进后退能触发浏览器的popstate事件,获取window.location.pathname来控制页面的变化
  • 缺点
  1. URL的改变属于http请求,借助history.pushState实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL 必须和实际向后端发起请求的 URL 一致。如果用户输入的URL回车或者浏览器刷新或者分享出去某个页面路径,用户点击后,URL与后端配置的页面请求URL不一致,则匹配不到任何静态资源,就会返回404页面。所以需要后台配置支持,覆盖所有情况的候选资源,如果 URL 匹配不到任何静态资源,则应该返回app 依赖的页面或者应用首页。
  2. 兼容性差,特定浏览器支持
  • 代码实现
<!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>history</title>
  </head>
  <body>
    <div class="main">
      <a href="javascript:;" path="/a">a页面</a>
      <a href="javascript:;" path="/b">b页面</a>
      <a href="javascript:;" path="/c">c页面</a>
      <div id="content"></div>
    </div>

    <script>
      class Router {
        constructor(routers) {
          this.routers = {};
          routers.forEach((item) => {
            this.route(item.path, () => {
              document.getElementById("content").innerHTML = item.compontent;
            });
          });
          this.bindClick();
          this.init();
        }
        route(path, cb) {
          this.routers[path] = cb;
        }
        bindClick() {
          // history模式需要手动添加路由 通过 history的pushState事件
          const links = document.getElementsByTagName("a");
          // [].forEach.call() => Array.prototype.forEach()
          [].forEach.call(links, (link) => {
            link.addEventListener("click", () => {
              const path = link.getAttribute("path");
              this.pushRoute(path);
            });
          });
        }
        pushRoute(path) {
          window.history.pushState({}, null, path);
          this.updateView();
        }
        init() {
          window.addEventListener("load", this.updateView.bind(this));
          // history模式 路由修改 浏览器会触发popstate事件
          window.addEventListener("popstate", this.updateView.bind(this));
        }
        updateView(e) {
          // console.log("history window.location", window.location);
          // console.log("history window.history", window.history);
          const currentUrl = window.location.pathname || "/";
          this.routers[currentUrl] && this.routers[currentUrl]();
        }
      }
      const routers = [
        {
          path: "/a",
          compontent: `<div>我是a页面</div>`,
        },
        {
          path: "/b",
          compontent: `<div>我是b页面</div>`,
        },
        {
          path: "/c",
          compontent: `<div>我是c页面</div>`,
        },
      ];
      new Router(routers);
    </script>
  </body>
</html>

Abstract模式

支持所有javascript运行模式。vue-router 自身会对环境做校验,如果发现没有浏览器的 API,路由会自动强制进入 abstract 模式。在移动端原生环境中也是使用 abstract 模式。

总结

hash 模式和 history 模式都属于浏览器自身的特性,Vue-Router 只是利用了这两个特性(通过调用浏览器提供的接口)来实现前端路由。