url 组成

//http://127.0.0.1:8001/01-hash.html?a=100&b=20#/aaa/bbb location.protocal //
'http:' 协议 localtion.hostname // '127.0.0.1' 主机名 location.host //
'127.0.0.1:8001' 主机 location.port //8001 端口号 location.pathname
//'01-hash.html' 访问页面 location.serach // '?a=100&b=20' 搜索内容
location.hash // '#/aaa/bbb' 哈希值

hash 特点

概述

  • hash 本身用于页面定位
  • hash 变化会触发网页跳转,即浏览器的前进和后退。
  • hash 可以改变 url ,但是不会触发页面重新加载(hash 的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url 。
  • hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。
  • hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)。

实现前端路由

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hash 模式</title>
  </head>
  <body>
    <div>
      <ul>
        <li><a href="#/page1">page1</a></li>
        <li><a href="#/page2">page2</a></li>
      </ul>
      <!--渲染对应组件的地方-->
      <div id="route-view"></div>
    </div>
    <script type="text/javascript">
      // 第一次加载的时候,不会执行 hashchange 监听事件,默认执行一次
      // DOMContentLoaded 为浏览器 DOM 加载完成时触发
      window.addEventListener("DOMContentLoaded", Load);
      window.addEventListener("hashchange", HashChange);
      // 展示页面组件的节点
      var routeView = null;
      function Load() {
        routeView = document.getElementById("route-view");
        HashChange();
      }
      function HashChange() {
        // 每次触发 hashchange 事件,通过 location.hash 拿到当前浏览器地址的 hash 值
        // 根据不同的路径展示不同的内容
        switch (location.hash) {
          case "#/page1":
            routeView.innerHTML = "page1";
            return;
          case "#/page2":
            routeView.innerHTML = "page2";
            return;
          default:
            routeView.innerHTML = "page1";
            return;
        }
      }
    </script>
  </body>
</html>

封装

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Hash 模式</title>
  </head>
  <body>
    <div>
      <!-- html:菜单中href设置为hash形式,id为app中放置页面内容 -->
      <ul id="menu">
        <li><a href="#index">首页</a></li>
        <li><a href="#news">资讯</a></li>
        <li><a href="#user">个人中心</a></li>
      </ul>
      <div id="app"></div>
    </div>
    <script type="text/javascript">
      class Router {
        constructor() {
          this.routers = []; //存放我们的路由配置
        }
        add(route, callback) {
          this.routers.push({
            path: route,
            render: callback,
          });
        }
        listen(callback) {
          window.onhashchange = this.hashChange(callback);
          this.hashChange(callback)(); //首次进入页面的时候没有触发hashchange,必须要就单独调用一下
        }
        hashChange(callback) {
          let self = this;
          return function () {
            let hash = location.hash;
            console.log(hash);
            for (let i = 0; i < self.routers.length; i++) {
              let route = self.routers[i];
              if (hash === route.path) {
                callback(route.render());
                return;
              }
            }
          };
        }
      }

      let router = new Router();
      router.add("#index", () => {
        return "<h1>这是首页内容</h1>";
      });
      router.add("#news", () => {
        return "<h1>这是新闻内容</h1>";
      });
      router.add("#user", () => {
        return "<h1>这是个人中心内容</h1>";
      });

      console.log(router);
      router.listen((renderHtml) => {
        let app = document.getElementById("app");
        app.innerHTML = renderHtml;
      });
    </script>
  </body>
</html>

histroy

概述

  • History.length:当前窗口访问过的网址数量(包括当前网页)
  • History.state:History 堆栈最上层的状态值
  • History.back():移动到上一个网址,等同于点击浏览器的后退键。对于第一个访问的网址,该方法无效果。
  • History.forward():移动到下一个网址,等同于点击浏览器的前进键。对于最后一个访问的网址,该方法无效果。
  • History.go():接受一个整数作为参数,以当前网址为基准,移动到参数指定的网址,比如 go(1)相当于 forward()go(-1)相当于 back()。如果参数超过实际存在的网址范围,该方法无效果;如果不指定参数,默认参数为 0,相当于刷新当前页面。
  • History.pushState()方法用于在历史中添加一条记录。该方法接受三个参数,依次为:
  • state:一个与添加的记录相关联的状态对象,主要用于 popstate事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填 null
  • title:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。
  • url:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
  • pushState()方法不会触发页面刷新,只是导致 History 对象发生变化,地址栏会有反应。
  • 如果 pushState的 URL 参数设置了一个新的锚点值(即 hash),并不会触发 hashchange事件。反过来,如果 URL 的锚点值变了,则会在 History 对象创建一条浏览记录。
  • 如果 pushState()方法设置了一个跨域网址,则会报错。
  • History.replaceState()方法用来修改 History 对象的当前记录,其他都与 pushState()方法一模一样
  • popstate 事件,每当同一个文档的浏览历史(即 history对象)出现变化时,就会触发 popstate事件。
  • 注意,仅仅调用 pushState()方法或 replaceState()方法 ,并不会触发该事件,只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用 History.back()History.forward()History.go()方法时才会触发。另外,该事件只针对同一个文档,如果浏览历史的切换,导致加载不同的文档,该事件也不会触发。
window.onpopstate = function (event) {
  console.log("location: " + document.location);
  console.log("state: " + JSON.stringify(event.state));
};

// 或者
window.addEventListener("popstate", function (event) {
  console.log("location: " + document.location);
  console.log("state: " + JSON.stringify(event.state));
});

实现前端路由(需要项目启动)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>History 模式</title>
</head>
<body>
<div>
  <ul>
    <li><a href="/page1">page1</a></li>
    <li><a href="/page2">page2</a></li>
  </ul>
  <div id="route-view"></div>
</div>
<script type="text/javascript">
  window.addEventListener("DOMContentLoaded", Load);
  window.addEventListener("popstate", PopChange);
  var routeView = null;
  function Load() {
    routeView = document.getElementById("route-view");
    // 默认执行一次 popstate 的回调函数,匹配一次页面组件
    PopChange();
    // 获取所有带 href 属性的 a 标签节点
    var aList = document.querySelectorAll("a[href]");
    // 遍历 a 标签节点数组,阻止默认事件,添加点击事件回调函数
    aList.forEach((aNode) =>
      aNode.addEventListener("click", function (e) {
        e.preventDefault(); //阻止a标签的默认事件
        var href = aNode.getAttribute("href");
        //  手动修改浏览器的地址栏
        history.pushState(null, "", href);
        // 通过 history.pushState 手动修改地址栏,
        // popstate 是监听不到地址栏的变化,所以此处需要手动执行回调函数 PopChange
        PopChange();
      })
    );
  }
  function PopChange() {
    console.log("location", location);
    switch (location.pathname) {
      case "/page1":
        routeView.innerHTML = "page1";
        return;
      case "/page2":
        routeView.innerHTML = "page2";
        return;
      default:
        routeView.innerHTML = "page1";
        return;
    }
  }
</script>
</body>
</html>

封装

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>history 模式</title>
</head>
<body>
<div>
  <!-- html:菜单中href设置为hash形式,id为app中放置页面内容 -->
  <ul id="menu">
    <li><a href="/index">首页</a></li>
    <li><a href="/news">资讯</a></li>
    <li><a href="/user">个人中心</a></li>
  </ul>
  <div id="app"></div>
</div>
<script type="text/javascript">
  class Router {
    constructor() {
      this.routers = [];
      this.renderCallback = null;
    }
    add(route, callback) {
      this.routers.push({
        path: route,
        render: callback,
      });
    }
    pushState(path, data = {}) {
      window.history.pushState(data, "", path);
      this.renderHtml(path);
    }
    listen(callback) {
      this.renderCallback = callback;
      this.changeA();
      window.onpopstate = () => this.renderHtml(this.getCurrentPath());
      this.renderHtml(this.getCurrentPath());
    }
    changeA() {
      document.addEventListener("click", (e) => {
        if (e.target.nodeName === "A") {
          e.preventDefault();
          let path = e.target.getAttribute("href");
          this.pushState(path);
        }
      });
    }
    getCurrentPath() {
      return location.pathname;
    }
    renderHtml(path) {
      for (let i = 0; i < this.routers.length; i++) {
        let route = this.routers[i];
        if (path === route.path) {
          this.renderCallback(route.render());
          return;
        }
      }
    }
  }

  let router = new Router();
  router.add("/index", () => {
    return "<h1>这是首页内容</h1>";
  });
  router.add("/news", () => {
    return "<h1>这是新闻内容</h1>";
  });
  router.add("/user", () => {
    return "<h1>这是个人中心内容</h1>";
  });
  router.listen((renderHtml) => {
    let app = document.getElementById("app");
    app.innerHTML = renderHtml;
  });
</script>
</body>


## 参考

https://juejin.cn/post/6993840419041706014
https://cloud.tencent.com/developer/article/1904982
https://wangdoc.com/javascript/bom/history