history源码解析-管理会话历史记录

栏目: 服务器 · 发布时间: 5年前

内容简介:本篇读后感分为五部分,分别为前言、使用、解析、demo、总结,五部分互不相连可根据需要分开看。

history 是一个JavaScript库,可让你在JavaScript运行的任何地方轻松管理会话历史记录

1.前言

history 是由Facebook维护的, react-router 依赖于 history ,区别于浏览器的 window.historyhistory 是包含 window.history 的,让开发者可以在任何环境都能使用 history 的api(例如 NodeReact Native 等)。

本篇读后感分为五部分,分别为前言、使用、解析、demo、总结,五部分互不相连可根据需要分开看。

前言为介绍、使用为库的使用、解析为源码的解析、demo是抽取源码的核心实现的小demo,总结为吹水,学以致用。

建议跟着源码结合本文阅读,这样更加容易理解!

  1. history
  2. history解析的Github地址

2.使用

history 有三种不同的方法创建history对象,取决于你的代码环境:

  1. createBrowserHistory :支持 HTML5 history api 的现代浏览器(例如: /index );
  2. createHashHistory :传统浏览器(例如: /#/index );
  3. createMemoryHistory :没有Dom的环境(例如: NodeReact Native )。

注意:本片文章只解析 createBrowserHistory ,其实三种构造原理都是差不多的

<!DOCTYPE html>
<html>
  <head>
    <script src="./umd/history.js"></script>
    <script>
      var createHistory = History.createBrowserHistory
      // var createHistory = History.createHashHistory

      var page = 0
      // createHistory创建所需要的history对象
      var h = createHistory()

      // h.block触发在地址栏改变之前,用于告知用户地址栏即将改变
      h.block(function (location, action) {
        return 'Are you sure you want to go to ' + location.path + '?'
      })

      // h.listen监听当前地址栏的改变
      h.listen(function (location) {
        console.log(location, 'lis-1')
      })
    </script>
  </head>
  <body>
    <p>Use the two buttons below to test normal transitions.</p>
    <p>
      <!-- h.push用于跳转 -->
      <button onclick="page++; h.push('/' + page, { page: page })">history.push</button>
      <!-- <button onclick="page++; h.push('/#/' + page)">history.push</button> -->

      <button onclick="h.goBack()">history.goBack</button>
    </p>
  </body>
</html>
复制代码

block 用于地址改变之前的截取, listener 用于监听地址栏的改变, pushreplacego(n) 等用于跳转,用法简单明了

3.解析

贴出来的源码我会删减对理解原理不重要的部分!!!如果想看完整的请下载源码看哈

从history的源码库目录可以看到modules文件夹,包含了几个文件:

  1. createBrowserHistory.js 创建createBrowserHistory的history对象;
  2. createHashHistory.js 创建createHashHistory的history对象;
  3. createMemoryHistory.js 创建createMemoryHistory的history对象;
  4. createTransitionManager.js 过渡管理(例如:处理block函数中的弹框、处理listener的队列);
  5. DOMUtils.js Dom工具类(例如弹框、判断浏览器兼容性);
  6. index.js 入口文件;
  7. LocationUtils.js 处理Location工具;
  8. PathUtils.js 处理Path工具。

入口文件index.js

export { default as createBrowserHistory } from "./createBrowserHistory";
export { default as createHashHistory } from "./createHashHistory";
export { default as createMemoryHistory } from "./createMemoryHistory";
export { createLocation, locationsAreEqual } from "./LocationUtils";
export { parsePath, createPath } from "./PathUtils";
复制代码

把所有需要暴露的方法根据文件名区分开,我们先看 history 的构造函数 createBrowserHistory

3.1 createBrowserHistory

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // 浏览器的history
  const globalHistory = window.history;
  // 初始化location
  const initialLocation = getDOMLocation(window.history.state);
  // 创建地址
  function createHref(location) {
    return basename + createPath(location);
  }

  ...

  const history = {
    //  window.history属性长度
    length: globalHistory.length,

    // history 当前行为(包含PUSH-进入、POP-弹出、REPLACE-替换)
    action: "POP",

    // location对象(与地址有关)
    location: initialLocation,

    // 当前地址(包含pathname)
    createHref,

    // 跳转的方法
    push,
    replace,
    go,
    goBack,
    goForward,

    // 截取
    block,

    // 监听
    listen
  };

  return history;
}

export default createBrowserHistory;
复制代码

无论是从代码还是从用法上我们也可以看出,执行了 createBrowserHistory 后函数会返回 history 对象, history 对象提供了很多属性和方法,最大的疑问应该是 initialLocation 函数,即 history.location 。我们的解析顺序如下:

  1. location;
  2. createHref;
  3. block;
  4. listen;
  5. push;
  6. replace。

3.2 location

location属性存储了与地址栏有关的信息,我们对比下 createBrowserHistory 的返回值 history.locationwindow.location

// history.location
history.location = {
  hash: ""
  pathname: "/history/index.html"
  search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  state: undefined
}

// window.location
window.location = {
  hash: ""
  host: "localhost:63342"
  hostname: "localhost"
  href: "http://localhost:63342/history/index.html?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  origin: "http://localhost:63342"
  pathname: "/history/index.html"
  port: "63342"
  protocol: "http:"
  reload: ƒ reload()
  replace: ƒ ()
  search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
}
复制代码

结论是history.location是window.location的儿砸!我们来研究研究作者是怎么处理的。

const initialLocation = getDOMLocation(window.history.state)
复制代码

initialLocation 函数等于 getDOMLocation 函数的返回值( getDOMLocationhistory 中会经常调用,理解好这个函数比较重要)。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  // 处理basename(相对地址,例如:首页为index,假如设置了basename为/the/base,那么首页为/the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";
  
  const initialLocation = getDOMLocation(window.history.state);

  // 处理state参数和window.location
  function getDOMLocation(historyState) {
    const { key, state } = historyState || {};
    const { pathname, search, hash } = window.location;

    let path = pathname + search + hash;

    // 保证path是不包含basename的
    if (basename) path = stripBasename(path, basename);

    // 创建history.location对象
    return createLocation(path, state, key);
  };

  const history = {
    // location对象(与地址有关)
    location: initialLocation,
    ...
  };

  return history;
}
复制代码

一般大型的项目中都会把一个功能拆分成至少两个函数,一个专门处理参数的函数和一个接收处理参数实现功能的函数:

  1. 处理参数: getDOMLocation 函数主要处理 statewindow.location 这两参数,返回自定义的 history.location 对象,主要构造 history.location 对象是 createLocation 函数;
  2. 构造功能: createLocation 实现具体构造 location 的逻辑。

接下来我们看在 LocationUtils.js 文件中的 createLocation 函数

// LocationUtils.js
import { parsePath } from "./PathUtils";

export function createLocation(path, state, key, currentLocation) {
  let location;
  if (typeof path === "string") {
    // 两个参数 例如: push(path, state)

    // parsePath函数用于拆解地址 例如:parsePath('www.aa.com/aa?b=bb') => {pathname: 'www.aa.com/aa', search: '?b=bb', hash: ''}
    location = parsePath(path);
    location.state = state;
  } else {
    // 一个参数 例如: push(location)
    location = { ...path };

    location.state = state;
  }

  if (key) location.key = key;

  // location = {
  //   hash: ""
  //   pathname: "/history/index.html"
  //   search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  //   state: undefined
  // }
  return location;
}

// PathUtils.js
export function parsePath(path) {
  let pathname = path || "/";
  let search = "";
  let hash = "";

  const hashIndex = pathname.indexOf("#");
  if (hashIndex !== -1) {
    hash = pathname.substr(hashIndex);
    pathname = pathname.substr(0, hashIndex);
  }

  const searchIndex = pathname.indexOf("?");
  if (searchIndex !== -1) {
    search = pathname.substr(searchIndex);
    pathname = pathname.substr(0, searchIndex);
  }

  return {
    pathname,
    search: search === "?" ? "" : search,
    hash: hash === "#" ? "" : hash
  };
}
复制代码

createLocation 根据传递进来的 path 或者 location 值,返回格式化好的 location ,代码简单。

3.3 createHref

createHref 函数的作用是返回当前路径名,例如地址 http://localhost:63342/history/index.html?a=1 ,调用 h.createHref(location) 后返回 /history/index.html?a=1

// createBrowserHistory.js
import {createPath} from "./PathUtils";

function createBrowserHistory(props = {}){
  // 处理basename(相对地址,例如:首页为index,假如设置了basename为/the/base,那么首页为/the/base/index)
  const basename = props.basename ? stripTrailingSlash(addLeadingSlash(props.basename)) : "";

  function createHref(location) {
    return basename + createPath(location);
  }
  
  const history = {
    // 当前地址(包含pathname)
    createHref,
    ...
  };

  return history;
}

// PathUtils.js
function createPath(location) {
  const { pathname, search, hash } = location;

  let path = pathname || "/";
  
  if (search && search !== "?") path += search.charAt(0) === "?" ? search : `?${search}`;

  if (hash && hash !== "#") path += hash.charAt(0) === "#" ? hash : `#${hash}`;

  return path;
}
复制代码

3.4 listen

在这里我们可以想象下大概的 监听 流程:

  1. 绑定我们设置的监听函数;
  2. 监听历史记录条目的改变,触发监听函数。

第二章使用 代码中,创建了 History 对象后使用了 h.listen 函数。

// index.html
h.listen(function (location) {
  console.log(location, 'lis-1')
})
h.listen(function (location) {
  console.log(location, 'lis-2')
})
复制代码

可见 listen 可以绑定多个监听函数,我们先看作者的 createTransitionManager.js 是如何实现绑定多个监听函数的。

createTransitionManager 是过渡管理(例如:处理block函数中的弹框、处理listener的队列)。代码风格跟createBrowserHistory几乎一致,暴露全局函数,调用后返回对象即可使用。

// createTransitionManager.js
function createTransitionManager() {
  let listeners = [];

  // 设置监听函数
  function appendListener(fn) {
    let isActive = true;

    function listener(...args) {
      // good
      if (isActive) fn(...args);
    }

    listeners.push(listener);

    // 解除
    return () => {
      isActive = false;
      listeners = listeners.filter(item => item !== listener);
    };
  }

  // 执行监听函数
  function notifyListeners(...args) {
    listeners.forEach(listener => listener(...args));
  }

  return {
    appendListener,
    notifyListeners
  };
}
复制代码
  1. 设置监听函数 appendListenerfn 就是用户设置的监听函数,把所有的监听函数存储在 listeners 数组中;
  2. 执行监听函数 notifyListeners :执行的时候仅仅需要循环依次执行即可。

这里感觉有值得借鉴的地方:添加队列函数时,增加状态管理(如上面代码的 isActive ),决定是否启用。

有了上面的理解,下面看 listen 源码。

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  function listen(listener) {
    // 添加 监听函数 到 队列
    const unlisten = transitionManager.appendListener(listener);

    // 添加 历史记录条目 的监听
    checkDOMListeners(1);

    // 解除监听
    return () => {
      checkDOMListeners(-1);
      unlisten();
    };
  }

  const history = {
    // 监听
    listen
    ...
  };

  return history;
}


复制代码

history.listen 是当历史记录条目改变时,触发回调监听函数。所以这里有两步:

transitionManager.appendListener(listener)
checkDOMListeners

下面看看如何历史记录条目的改变 checkDOMListeners(1)

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // 是否已经添加
    if (listenerCount === 1 && delta === 1) {
      // 添加绑定,当历史记录条目改变的时候
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      //  解除绑定
      window.removeEventListener('popstate', handlePopState);
    }
  }
  
  // getDOMLocation(event.state) = location = {
  //   hash: ""
  //   pathname: "/history/index.html"
  //   search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  //   state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    const action = "POP";
    setState({ action, location })
  }
}
复制代码

虽然作者写了很多很细的回调函数,可能会导致有些不好理解,但细细看还是有它道理的:

  1. checkDOMListeners :全局只能有一个监听历史记录条目的函数( listenerCount 来控制);
  2. handlePopState :必须把监听函数提取出来,不然不能解绑;
  3. handlePop :监听历史记录条目的核心函数,监听成功后执行 setState

setState({ action, location }) 作用是根据当前地址信息( location )更新history。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function setState(nextState) {
    // 更新history
    Object.assign(history, nextState);
    history.length = globalHistory.length;

    // 执行监听函数listen
    transitionManager.notifyListeners(history.location, history.action);
  }

  const history = {
    // 监听
    listen
    ...
  };

  return history;
}
复制代码

在这里,当更改历史记录条目成功后:

  1. 更新history;
  2. 执行监听函数listen;

这就是 h.listen 的主要流程了,是不是还挺简单的。

3.5 block

history.block 的功能是当历史记录条目改变时,触发提示信息。在这里我们可以想象下大概的 截取 流程:

  1. 绑定我们设置的截取函数;
  2. 监听历史记录条目的改变,触发截取函数。

哈哈这里是不是感觉跟 listen 函数的套路差不多呢?其实 h.listenh.block 的监听历史记录条目改变的代码是公用同一套(当然拉只能绑定一个监听历史记录条目改变的函数),3.1.3为了方便理解我修改了部分代码,下面是完整的源码。

第二章使用 代码中,创建了 History 对象后使用了 h.block 函数(只能绑定一个 block 函数)。

// index.html
h.block(function (location, action) {
  return 'Are you sure you want to go to ' + location.path + '?'
})
复制代码

同样的我们先看看作者的 createTransitionManager.js 是如何实现提示的。

createTransitionManager 是过渡管理(例如:处理block函数中的弹框、处理listener的队列)。代码风格跟createBrowserHistory几乎一致,暴露全局函数,调用后返回对象即可使用。

// createTransitionManager.js
function createTransitionManager() {
  let prompt = null;

  // 设置提示
  function setPrompt(nextPrompt) {
    prompt = nextPrompt;

    // 解除
    return () => {
      if (prompt === nextPrompt) prompt = null;
    };
  }

  /**
   * 实现提示
   * @param location:地址
   * @param action:行为
   * @param getUserConfirmation 设置弹框
   * @param callback 回调函数:block函数的返回值作为参数
   */
  function confirmTransitionTo(location, action, getUserConfirmation, callback) {
    if (prompt != null) {
      const result = typeof prompt === "function" ? prompt(location, action) : prompt;

      if (typeof result === "string") {
        // 方便理解我把源码getUserConfirmation(result, callback)直接替换成callback(window.confirm(result))
        callback(window.confirm(result))
      } else {
        callback(result !== false);
      }
    } else {
      callback(true);
    }
  }

  return {
    setPrompt,
    confirmTransitionTo
    ...
  };
}
复制代码

setPromptconfirmTransitionTo 的用意:

  1. 设置提示setPrompt:把用户设置的提示信息函数存储在prompt变量;
  2. 实现提示confirmTransitionTo:
    1. 得到提示信息:执行prompt变量;
    2. 提示信息后的回调:执行callback把提示信息作为结果返回出去。

下面看 h.block 源码。

// createBrowserHistory.js
import createTransitionManager from "./createTransitionManager";
const transitionManager = createTransitionManager();

function createBrowserHistory(props = {}){
  let isBlocked = false;

  function block(prompt = false) {
    // 设置提示
    const unblock = transitionManager.setPrompt(prompt);

    // 是否设置了block
    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    }

    // 解除block函数
    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      }

      // 消除提示
      return unblock();
    };
  }

  const history = {
    // 截取
    block,
    ...
  };

  return history;
}
复制代码

history.block 的功能是当历史记录条目改变时,触发提示信息。所以这里有两步:

transitionManager.setPrompt(prompt)
checkDOMListeners

这里感觉有值得借鉴的地方:调用 history.block ,它会返回一个解除监听方法,只要调用一下返回函数即可解除监听或者复原(有趣)。

我们看看监听历史记录条目改变函数 checkDOMListeners(1) (注意: transitionManager.confirmTransitionTo )。

// createBrowserHistory.js
function createBrowserHistory(props = {}){
  function block(prompt = false) {
    // 设置提示
    const unblock = transitionManager.setPrompt(prompt);

    // 是否设置了block
    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    }

    // 解除block函数
    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      }

      // 消除提示
      return unblock();
    };
  }

  let listenerCount = 0;

  function checkDOMListeners(delta) {
    listenerCount += delta;
    
    // 是否已经添加
    if (listenerCount === 1 && delta === 1) {
      // 添加绑定,当地址栏改变的时候
      window.addEventListener('popstate', handlePopState);
    } else if (listenerCount === 0) {
      //  解除绑定
      window.removeEventListener('popstate', handlePopState);
    }
  }
  
  // getDOMLocation(event.state) = location = {
  //   hash: ""
  //   pathname: "/history/index.html"
  //   search: "?_ijt=2mt7412gnfvjpfeuv4hjkq2uf8"
  //   state: undefined
  // }
  function handlePopState(event) {
    handlePop(getDOMLocation(event.state));
  }
  
  function handlePop(location) {
    // 不需要刷新页面
    const action = "POP";

    // 实现提示
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (ok) {
          // 确定
          setState({ action, location });
        } else {
          // 取消
          revertPop(location);
        }
      }
    );
  }

  const history = {
    // 截取
    block
    ...
  };

  return history;
}
复制代码

就是在 handlePop 函数触发 transitionManager.confirmTransitionTo 的(3.1.3我对这里做了修改为了方便理解)。

transitionManager.confirmTransitionTo 的回调函数callback有两条分支,用户点击提示框的确定按钮或者取消按钮:

setState({ action, location })
revertPop(location)

到这里已经了解完 h.block 函数、 h.listencreateTransitionManager.js 。接下来我们继续看另一个重要的函数 h.push

3.6 push

function createBrowserHistory(props = {}){
  function push(path, state) {
    const action = "PUSH";
    // 构造location
    const location = createLocation(path, state, createKey(), history.location);

    // 执行block函数,弹出框
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;

        // 获取当前路径名
        const href = createHref(location);
        const { key, state } = location;

        // 添加历史条目
        globalHistory.pushState({ key, state }, null, href);
        
        if (forceRefresh) {
          // 强制刷新
          window.location.href = href;
        } else {
          // 更新history
          setState({ action, location });
        }
      }
    );
  }

  const history = {
    // 跳转
    push,
    ...
  };

  return history;
}
复制代码

这里最重要的是 globalHistory.pushState 函数,它直接添加新的历史条目。

3.7 replace

function createBrowserHistory(props = {}){
  function replace(path, state) {
    const action = "REPLACE";
    // 构造location
    const location = createLocation(path, state, createKey(), history.location);

    // 执行block函数,弹出框
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;
        // 获取当前路径名
        const href = createHref(location);
        const { key, state } = location;

        globalHistory.replaceState({ key, state }, null, href);

        if (forceRefresh) {
          window.location.replace(href);
        } else {
          setState({ action, location });
        }
      }
    );
  }

  const history = {
    // 跳转
    replace,
    ...
  };

  return history;
}
复制代码

其实 pushreplace 的区别就是 history.pushStatehistory.replaceState 的区别。

3.8 go

function createBrowserHistory(props = {}){
   function go(n) {
    globalHistory.go(n);
  }

  function goBack() {
    go(-1);
  }

  function goForward() {
    go(1);
  }

  const history = {
    // 跳转
    go,
    goBack,
    goForward,
    ...
  };

  return history;
}
复制代码

其实就是 history.go 的运用。

4.demo

手把手教你写history,稍后放出哈哈哈~


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Measure What Matters

Measure What Matters

John Doerr / Portfolio / 2018-4-24 / GBP 19.67

In the fall of 1999, John Doerr met with the founders of a start-up he’d just given $11.8 million, the biggest investment of his career. Larry Page and Sergey Brin had amazing technology, entrepreneur......一起来看看 《Measure What Matters》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

SHA 加密
SHA 加密

SHA 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换