XXHolic / blog

I wonder how~, I wonder why~

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Read history

XXHolic opened this issue · comments

目录

引子

history 经常看到,想看看里面封装的方式。

history 版本 4.10.1。

简介

history 库是基于浏览器内置 HistoryLocation API 轻量封装。它的目的不是提供这些 API 的完整实现,而是为了让使用者更加容易的使用不同的导航方法。

该库提供了 3 种不同方法创建一个 history 对象,根据环境的需要有下面的选择:

  • createBrowserHistory 方法适用于那些支持 HTML5 history API 的现代浏览器。
  • createHashHistory 方法适用于当页面重新加载时,想把当前位置存储在当前 URL 的 hash 上,以此避免向服务器发送请求的情景。
  • createMemoryHistory 方法适用于参考实现,也可以用于非 DOM 环境,例如 React Native 或 tests 。

更多说明见 history Doc

实现

createBrowserHistory

createBrowserHistory

主要逻辑代码
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';

function createBrowserHistory(props = {}) {
  const globalHistory = window.history;
  const canUseHistory = supportsHistory();
  const needsHashChangeListener = !supportsPopStateOnHashChange();

  const {
    forceRefresh = false,
    getUserConfirmation = getConfirmation,// 浏览器自带确认弹窗
    keyLength = 6 // 创建随机 key 后截取的位置
  } = props;
  const basename = props.basename
    ? stripTrailingSlash(addLeadingSlash(props.basename)) // 格式兼容处理
    : '';

  // 转变管理器,添加事件监听、执行,以及 url 转变时都会触发里面对应的方法。
  const transitionManager = createTransitionManager();

  // getDOMLocation 方法将 window.history.state 和 window.location 对象中属性统一放在一个对象上
  function getDOMLocation(historyState) {
    const { key, state } = historyState || {};
    const { pathname, search, hash } = window.location;
    let path = pathname + search + hash;
    if (basename) path = stripBasename(path, basename);
    return createLocation(path, state, key);
  }

  // getHistoryState 方法获取 window.history.state, 没有(IE 10/11)就返回 {}
  const initialLocation = getDOMLocation(getHistoryState());
  let allKeys = [initialLocation.key];

  function setState(nextState) {
    Object.assign(history, nextState);
    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }

  // Public interface
  function createHref(location) {
    return basename + createPath(location);
  }

  function push(path, state) {
    const action = 'PUSH';
    const location = createLocation(path, state, createKey(), history.location);
    transitionManager.confirmTransitionTo(location,
      action,
      getUserConfirmation,
      ok => {
        /* ... */
      })
  }

  function replace(path, state) {
    const action = 'REPLACE';
    const location = createLocation(path, state, createKey(), history.location);
    transitionManager.confirmTransitionTo(location,
      action,
      getUserConfirmation,
      ok => {
        /* ... */
      })
  }

  function go(n) { /* ... */ }
  function goBack() { /* ... */ }
  function goForward() { /* ... */ }
  let listenerCount = 0; // 用于 checkDOMListeners 方法判断
  function checkDOMListeners(delta) { /* ... */ } // popstate、hashchange 事件监听
  let isBlocked = false; // 用于 block 方法判断
  function block(prompt = false) { /* ... */ } // 页面离开前的提示信息或操作
  function listen(listener) { /* ... */ }

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

  return history;

代码中主要处理有:

  • createLocation 方法对传入的参数信息进行统一组装处理,产生一个新的 location 对象。
  • 有一个手动维护的数组,存放历史记录。
  • history.pushStatehistory.replaceState 分别包装了一层判断和兼容处理,提供了改变前的提示功能。处理方法放在工具 createTransitionManager 中。
  • 事件监听使用了发布订阅模式处理,放在工具 createTransitionManager 中。
  • 关键的改变都会触发 setState 方法,同步数据到 history 对象,并执行监听事件。

createHashHistory

createHashHistory 方法封装的方式跟 createBrowserHistory 差不多,主要区别是处理 URL 部分的不同。

主要逻辑代码
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';

const HashPathCoders = {
  hashbang: {
    encodePath: path =>
      path.charAt(0) === '!' ? path : '!/' + stripLeadingSlash(path),
    decodePath: path => (path.charAt(0) === '!' ? path.substr(1) : path)
  },
  noslash: {
    encodePath: stripLeadingSlash,
    decodePath: addLeadingSlash
  },
  slash: {
    encodePath: addLeadingSlash,
    decodePath: addLeadingSlash
  }
};

function createHashHistory(props = {}) {

  const globalHistory = window.history;
  const canGoWithoutReload = supportsGoWithoutReloadUsingHash();

  const { getUserConfirmation = getConfirmation, hashType = 'slash' } = props;
  const basename = props.basename
    ? stripTrailingSlash(addLeadingSlash(props.basename))
    : '';

  const { encodePath, decodePath } = HashPathCoders[hashType];

  function getDOMLocation() {
    let path = decodePath(getHashPath());
    if (basename) path = stripBasename(path, basename);

    return createLocation(path);
  }

  const transitionManager = createTransitionManager();

  function setState(nextState) {
    Object.assign(history, nextState);
    history.length = globalHistory.length;
    transitionManager.notifyListeners(history.location, history.action);
  }

  let forceNextPop = false;
  let ignorePath = null;

  // 在执行其他操作之前,确保哈希已正确编码。
  const path = getHashPath();
  const encodedPath = encodePath(path);

  if (path !== encodedPath) replaceHashPath(encodedPath);

  const initialLocation = getDOMLocation();
  let allPaths = [createPath(initialLocation)];

  // Public interface
  function createHref(location) {
    const baseTag = document.querySelector('base');
    let href = '';
    if (baseTag && baseTag.getAttribute('href')) {
      href = stripHash(window.location.href);
    }
    return href + '#' + encodePath(basename + createPath(location));
  }

  function push(path, state) {
    const action = 'PUSH';
    const location = createLocation(
      path,
      undefined,
      undefined,
      history.location
    );
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        /* ... */
      })
  }

  function replace(path, state) {
    const action = 'REPLACE';
    const location = createLocation(
      path,
      undefined,
      undefined,
      history.location
    );
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        /* ... */
      })
  }

  function go(n) { /* ... */ }
  function goBack() { /* ... */ }
  function goForward() { /* ... */ }
  let listenerCount = 0;
  function checkDOMListeners(delta) { /* ... */ }
  let isBlocked = false;
  function block(prompt = false) { /* ... */ }
  function listen(listener) { /* ... */ }

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

  return history;
}

createBrowserHistory 不同的地方有:

  • hashType 处理方式有分类。
  • 哈希值额外的编码和解码处理。
  • 改变使用的是 window.location 对象的方法。

createMemoryHistory

createMemoryHistory 方法没有借助浏览器 historylocation API,是一个导航模拟实现。

import { createPath } from './PathUtils.js';
import { createLocation } from './LocationUtils.js';
import createTransitionManager from './createTransitionManager.js';

function clamp(n, lowerBound, upperBound) {
  return Math.min(Math.max(n, lowerBound), upperBound);
}

function createMemoryHistory(props = {}) {
  const {
    getUserConfirmation,
    initialEntries = ['/'],
    initialIndex = 0,
    keyLength = 6
  } = props;

  // 事件统一处理,状态转变过程处理
  const transitionManager = createTransitionManager();

  // 同步数据状态信息
  function setState(nextState) {
    Object.assign(history, nextState);
    history.length = history.entries.length;
    // 监听事件执行
    transitionManager.notifyListeners(history.location, history.action);
  }

  function createKey() {
    return Math.random()
      .toString(36)
      .substr(2, keyLength);
  }

  const index = clamp(initialIndex, 0, initialEntries.length - 1);
  // 初始化,每一个路径都对应一个路径对象
  const entries = initialEntries.map(entry =>
    typeof entry === 'string'
      ? createLocation(entry, undefined, createKey())
      : createLocation(entry, undefined, entry.key || createKey())
  );

  // Public interface

  const createHref = createPath;

  function push(path, state) {

    const action = 'PUSH';
    const location = createLocation(path, state, createKey(), history.location);

    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;

        const prevIndex = history.index;
        const nextIndex = prevIndex + 1;

        const nextEntries = history.entries.slice(0);
        if (nextEntries.length > nextIndex) {
          nextEntries.splice(
            nextIndex,
            nextEntries.length - nextIndex,
            location
          );
        } else {
          nextEntries.push(location);
        }

        setState({
          action,
          location,
          index: nextIndex,
          entries: nextEntries
        });
      }
    );
  }

  function replace(path, state) {

    const action = 'REPLACE';
    const location = createLocation(path, state, createKey(), history.location);

    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;

        history.entries[history.index] = location;

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

  function go(n) {
    const nextIndex = clamp(history.index + n, 0, history.entries.length - 1);

    const action = 'POP';
    const location = history.entries[nextIndex];

    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (ok) {
          setState({
            action,
            location,
            index: nextIndex
          });
        } else {
          // Mimic the behavior of DOM histories by
          // causing a render after a cancelled POP.
          setState();
        }
      }
    );
  }

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

  function goForward() {
    go(1);
  }

  function canGo(n) {
    const nextIndex = history.index + n;
    return nextIndex >= 0 && nextIndex < history.entries.length;
  }

  function block(prompt = false) {
    return transitionManager.setPrompt(prompt);
  }

  function listen(listener) {
    return transitionManager.appendListener(listener);
  }

  const history = {
    length: entries.length,
    action: 'POP',
    location: entries[index],
    index,
    entries,
    createHref,
    push,
    replace,
    go,
    goBack,
    goForward,
    canGo,
    block,
    listen
  };

  return history;
}

这个方法中比较特别的是每一个路径都会显式的生成一个对应对象,另外两个方法都是利用原生 API 的功能实现。

跟前面对比,感觉这个就是 history 整体封装思路的表达:对相关的属性进行聚合,对事件监听进行统一处理,操作方法对外提供包装后一致的 API,状态变化内部进行记录。

先看 createMemoryHistory 方法的实现逻辑,再看另外两个方法的逻辑会更顺畅。

参考资料

🗑️

下面是一些无关紧要的东西。

看源码感觉总是会消耗我大量元气~~~:sweat::sweat::sweat:

最近看自己的笔记时,感觉少了点什么,回想自己看过印象比较深刻文章中,大概有下面几种特征:

  • 开头有张很大的图,而且不是随便的图,看起来有逼格的图。
  • 开头是桥段情景式,文中和文末都有情节关联。
  • 行文中间适时的放一些表情包或有意思的图。
  • 纯文字式的调侃。
  • 行文由浅入深,循序渐进,一气呵成。

想了想后,自己要么风格不合适,要么还没达到那种层次,不过还是想要加点什么东西。

最后决定以后在结尾的最后面,随便加些其它不同类型的东西:

  • 一方面算是“事后一根烟”的调节
  • 另一方面希望以后看到最后时有种“哎呦,还有这个~~”的感觉

42-poster

这个出自《怪化猫》,我很喜欢稀奇古怪的故事题材,浮世绘的风格显得更加诡异。

《怪化猫》讲的是一个卖药郎各地旅行斩妖的故事。里面有个很有意思的设定:

斩妖时想要拔出退魔之剑,需要找出三种要素:形、真、理

  • 形:妖物的形,是由人的因缘形成
  • 真:事情的真相
  • 理:心中的隐情

拔退魔之剑的动画 GIF 太大了预览加载不出来:sweat:!有兴趣的自己下载看吧!源图片在这里