history 源码分析- createHashHistory

·  阅读 2328

首先讲讲 hashHistory 是什么。

众所周知,在单页应用中,我们常用的两种路由策略是 hashHistory 和 browserHistory。其中 browserHistory 是古老的路由方式,从网页诞生一直沿用到现在,比如掘金个人文章页面的地址 https://juejin.cn/user/1574156384094397。而 hashHistory 是单页应用(SPA)出来之后,被广为使用的一种路由方式,它使用 # 后面的路径作为页面标识,如 https://i.ai.mi.com/h5/ai-xiaoai-3-years-fe/#/home。 前者让我们更难判断项目业务路径(xxx/static/yy-prj-name/home),而后者可以根据 # 后面的路径轻松判断。

history 项目信息:

  • 项目地址:github.com/ReactTraini…
  • ReactTraining/history(react-router 同样出自 ReactTraining)
  • history 是 react-router 的核心依赖
  • rollup 打包

项目结构

项目核心代码在 modules 目录下,其中关键文件是 createBrowserHistory.jscreateHashHistory.jscreateMemoryHistory.js

createHashHistory

createHashHistory.js 文件导出一个 createHashHistory 函数。

createHashHistory 用法如下:

// 下面给出的是默认值
createHashHistory({
  basename: '', // The base URL of the app (see below)
  hashType: 'slash', // The hash type to use (see below)
  getUserConfirmation: (message, callback) => callback(window.confirm(message))
});
复制代码
  • basename 是路由改变时默认会带上的一串字符串
  • hashType 可以忽略,用默认的
  • getUserConfirmation 这个用来在使用 history.block 的时候处理页面离开的函数,history.block(blockInfo) blockInfo 可以是一个函数或者字符串,函数执行的结果就是上面的 message,如果直接是字符串,则这个字符串就是传递过来的message;

createHashHistory(props) 返回下面一个对象,这就是我们平常在使用 react-router 的时候会用到的 props.history

  const history = {
    length: globalHistory.length,
    action: 'POP',
    location: initialLocation,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen,
  };
复制代码

我们对导出的各项逐个分析。

history.push

这个比较常用,push 代码简化之后就是下面这样。

参数:

path 对象或者字符串,字符串就是我们想要push的地址,如果是字符串则会被解析为一个对象(locationObj)。如果是对象则push之后的地址就是 /user/game?name=lxfriday#hahaha,可以容纳下面三个属性,。

history.push({
  pathname: '/user/game',
  search: 'name=lxfriday',
  hash: 'hahaha'
})
复制代码

比如 /user/game?name=lxfriday#hahaha,如果没有设置 basename,则push之后的地址就是 /user/game?name=lxfriday#hahaha,如果设置了 basename 为 /base,则push之后的地址为 /base/user/game?name=lxfriday#hahaha

  function push(path, state) {
    const action = 'PUSH';
    const location = createLocation(
      path,
      undefined,
      undefined,
      // 地址改变之前的 location 信息
      history.location
    );
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      (ok) => {
        if (!ok) return;

        const path = createPath(location);
        // encodedPath 编码之后的新 hash
        // encodePath 加一个首 /
        const encodedPath = encodePath(basename + path);
        const hashChanged = getHashPath() !== encodedPath;
        if (hashChanged) {
          ignorePath = path;
          // window.location.hash = encodedPath;
          pushHashPath(encodedPath);
          // history.location 是当前的
          const prevIndex = allPaths.lastIndexOf(createPath(history.location));
          // nextPaths 是最新的路由栈
          const nextPaths = allPaths.slice(0, prevIndex + 1);
          nextPaths.push(path);
          allPaths = nextPaths;
          setState({ action, location });
        } else {
          setState();
        }
      }
    );
  }
复制代码

源码中 createLocation 表示创建一个 locationObjpath 是字符串或者对 象都可以,返回一个 path 对应的 locationObj

比如 /user/game?name=lxfriday#haha 返回的是:

{
  pathname: '/user/game',
  search: '?name=lxfriday',
  hash: '#haha'
}
复制代码

关于 search? 和 hash 的 #,内部会自动添加。

transitionManager 是一个路由跳转处理器,在没有设置 history.block 的时候我们可以理解为直接调用 confirmTransitionTo 的最后一个回调函数,并且 oktrue

createPath 可以把 locationObj 再拼回一个字符串地址:

encodePath 这里是给 basename + path 拼接的字符串添加一个首 /

接下来比较 push 前后 hash 是否变化。getHashPath 是获取当前浏览器地址栏中的 hash 字符串,也就是当前的地址。

所以 getHashPath() !== encodedPath 比较的是浏览器当前的地址和即将要变更的地址,如果改变了,则会调用 pushHashPath(encodedPath)encodedPath 也就是最新的地址应用到 浏览器地址栏。具体怎么应用呢:

function pushHashPath(path) {
  window.location.hash = path;
}
复制代码

没错就是这么简单,直接更改了 location.hash

接下来要修改 history 内部自己维护的一个路由栈了:

const prevIndex = allPaths.lastIndexOf(createPath(history.location));
// nextPaths 是最新的路由栈
const nextPaths = allPaths.slice(0, prevIndex + 1);
nextPaths.push(path);
allPaths = nextPaths;
复制代码

history.location 怎么理解?记住 history 里面的动力在 setState 调用之前都是旧的,也就是比前面使用 createLocation 创建的 location 旧一个节拍。

所以这里 prevIndex 是拿到push操作前的地址在路由栈中最后一次出现的位置。然后复制一个新数组,把最新的地址放到最后,重新赋值给路由栈,这样就达到了更改路由栈的目的,很简单吧。

接下来是 setState({ action, location });, actionPUSHlocation 上面说了,是push操作后应该要达到的地址,setState 要做什么呢,很简单。

function setState(nextState) {
  Object.assign(history, nextState);
  // history 是在最底部声明的那个对象,与 window.history 很类似
  history.length = globalHistory.length;
  transitionManager.notifyListeners(history.location, history.action);
}
复制代码

actionlocation 属性复制到 history 中,同时触发在 transitionManager 中添加的监听器(也就是路由变更的监听器)。


写了这么多,我发现用文字来描述源码是一件非常难说清楚的事情,因为代码经过多次封装之后,一行代码可能需要查还几个地方才能弄清楚,文字描述难免漏掉一些细节。如果你想更深入了解,可以加入我的交流群,在群内直接提问。

文章源码看起来会比较繁复难以理解,如果你对源码分析感兴趣,可以前往 bilibili 观看我录制的视频,另外下载一份源码到本地自己亲自调试看源码效果会更好哦。

如果觉得看文章看视频依然没有看懂,也可以关注我的公众号,加入交流群,有问必答,欢迎交流讨论~~~

分类:
前端
标签:
分类:
前端
标签: