路由的history模式实现原理

1. 不从pushStatepopstate说起

路由的history模式是基于pushStatepopstate实现的。

api说明请传送 pushStatepopstate

Note that just calling history.pushState() or history.replaceState() won’t trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript).

2. 原生js实现路由思路说明

  • 调用pushState:只会修改当前history栈的内容,不刷新的话并不会导航到新页面,
location.href // "https://juejin.cn/editor/drafts/69724xxxxxx02504968"
history.state // null
history.pushState({id: 1}, 'id1', '/a')
location.href // "https://juejin.cn/a"
history.state // {id: 1}
复制代码
  • 那怎么可以让pushState更新页面呢?重新封装一下这个方法,并同时在要构造的方法内部提供更新页面的机制。
  function push(url) {
    try {
      // 这里用try是因为pushState可以的url可以传入绝对路径,但在跨域的时候会报错
      history.pushState({}, '', url)
    } catch (e) {
      location.assign(url)
    }
    updatePage()
  }
  function updatePage(location) {      
    // 根据当前location去操作dom
  }
复制代码

这里用观察者模式,改写下更新逻辑:

  // 借鉴于history库
  const MyRouter = function() {
    // 维护一组监听列表
    const listeners = [] 
    // 添加监听函数
    function listen(listener) {
      listeners.push(listener)      
      return function() {
        listeners.filter(s => s !== listen)
      }
    }
    // 执行所有监听事件
    function call(...args) {
      listeners.forEach(listener => listener(...args))
    }
    return {
        listen,
        call
    }    
  }
  
 const { listen, call } = myRouter()
复制代码

这样我们可以改写一下push方法,将updatePage改成call,这样我们可以注入更新逻辑然后在push时调用。

那什么时候注入监听器呢?

我们用原生写的话直接在页面加载的时候去注入就可以了:

  listen(function(location) {
     // 根据当前路径 渲染对应的内容 
     updatePage(location)
  })
复制代码

react-router-dombrowser-router是在constructor的时候注入的更新逻辑。首先browser-router定义了locationstate, 然后监听更新逻辑很简单:

 listen((location) => {
   this.setState({ location })
 })
 
 // 这样 push的时候,会调用call去遍历所有listener执行,
 // 就会触发this.setState去更新,从而重新渲染 <BrowserRouter />
复制代码
  • popstate用来做什么的?

可以触发popstate事件的都是已经改变了地址栏的操作,换句话说,此时的地址栏内的地址是上一个地址了,这个时候我们可以根据当前地址去更新至对应的页面了。

 // 这个功能是可以直接封装到myRouter的逻辑内的
 window.addEventListener("popstate", function (event) {
   call(location);
 });
复制代码
  1. 完整代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      a {
        margin-right: 20px;
      }
    </style>
  </head>
  <div class="link-wrapper">
    <a href="javascript:;" data-href="/home">home</a>
    <a href="javascript:;" data-href="/abc">abc</a>
    <a href="javascript:;" data-href="/def">def</a>
    <a href="javascript:;" data-href="/ghi">ghi</a>
  </div>
  <div class="content"></div>
  <body>
    <script>
      const history = window.history;
      const createSomeElement = (textContent) => {
        const div = document.createElement("div");
        div.textContent = textContent;
        return div;
      };
      // 路径与路由组件的银映射
      const pages = {
        "/home": createSomeElement("home"),
        "/abc": createSomeElement("abc"),
        "/def": createSomeElement("def"),
        "/ghi": createSomeElement("ghi")
      };
        
      const IfRedirectPath = () => {
        const pagesId = Object.keys(pages);
        if (path == "/" || !pagesId.includes(path)) {
          path = "/home";
          history.pushState({}, "", "/home");          
        }
        return path;
      }
    
      const renderPage = (path) => {
        // redirect的逻辑
        path = IfRedirectPath(path);
        const cont = document.querySelector(".content");
        const firstChild = cont.firstChild;
        if (firstChild) {
          cont.removeChild(firstChild);
        }
        cont.appendChild(pages[path]);
      };

      const myRouter = function () {
        const handlers = [];
        const listen = (listener) => {
          handlers.push(listener);
          return function unlisten() {
            handlers.filter((h) => h !== listener);
          };
        };
        const call = (location) => {
          handlers.forEach((cb) => cb(location));
        };

        window.addEventListener("popstate", function () {
          call({ url: window.location.pathname });
        });

        return {
          listen,
          call
        };
      };

      const { listen, call } = myRouter();

      const push = (url, state = {}, title) => {
        try {
          history.pushState(state, title, url);
        } catch (e) {
          window.assign(url);
        }
        call({ state, title, url });
      };

      window.addEventListener("beforeunload", function () {
        unListen && unListen();
      });

      document
        .querySelector(".link-wrapper")
        .addEventListener("click", function (e) {
          if (e.target.tagName !== "A") return;
          push(e.target.dataset.href);
        });

      var unListen = listen(function (loc) {
        renderPage(loc.url);
      });

      window.addEventListener("load", function () {
        const path = window.location.pathname;
        push(path);
      });
    </script>
  </body>
</html>

复制代码

完整代码

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享