react-router分析 - 一、history

react-router基于history库,它是一个管理js应用session会话历史的js库。它将不同环境(浏览器,node等)的变量统一成了一个简易的API来管理历史堆栈、导航、确认跳转、以及sessions间的持续状态。区别于浏览器的window.history,history是包含window.history的

来看看官方解释

The history library is a lightweight layer over browsers' built-in History and Location APIs. The goal is not to provide a full implementation of these APIs, but rather to make it easy for users to opt-in to different methods of navigation.

history库基于浏览器内置History和Location API,实现的加强版本

We provide 3 different methods for creating a history object, depending on the needs of your environment:

提供了三种API

createBrowserHistory is for use in modern web browsers that support the HTML5 history API (see cross-browser compatibility)

createBrowserHistory用于支持HTML5历史记录API的现代Web浏览器,不兼容老浏览器,需要服务器配置;用于操纵浏览器地址栏的变更;createBrowserHistory使用HTML5中的pushState和replaceState来防止在浏览时从服务器重新加载整个页面

createHashHistory is for use in situations where you want to store the location in the hash of the current URL to avoid sending it to the server when the page reloads

createHashHistory用于要在当前URL的哈希中存储位置以避免在页面重新加载时将其发送到服务器的情况

兼容老浏览器 IE8+ 都可以用;生产环境不推荐使用,如果服务器不配置,可用;用于操纵 hash 路径的变更

createMemoryHistory is used as a reference implementation and may also be used in non-DOM environments, like React Native or tests

Depending on the method you want to use to keep track of history, you'll import (or require, if you're using CommonJS) only one of these methods.

Memory history 不会在地址栏被操作或读取;服务器渲染用到,一般用于react-native;其实就是管理内存中的虚拟历史堆栈

应用

import { createBrowserHistory } from 'history';

const history = createBrowserHistory();

// 获取当前location
const location = history.location;

// 监听当前的地址变换
const unlisten = history.listen((location, action) => {
  // location is an object like window.location
  console.log(action, location.pathname, location.state);
});

// 将新入口放入历史URL堆栈
history.push('/home', { some: 'state' });

//  停止监听
unlisten();

history基于原生对象提供的api

  • push(location) 浏览器会添加新记录
  • replace(location) 当前页面不会保存到会话历史中(session History),这样,用户点击回退按钮时,将不会再跳转到该页面。
  • go(n)
  • goBack()
  • goForward()

Each history object has the following properties:

history.length - The number of entries in the history stack

history.location - The current location (see below)

history.action - The current navigation action (see below)

history = {
    length: globalHistory.length,//返回当前session的history个数
    action: "POP",//默认为pop
    location: initialLocation,//Object
    createHref,//接下来一系列方法
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };

history和location

history 知道如何去监听浏览器地址栏的变化, 并解析这个 URL 转化为 location 对象, 然后 router 使用它匹配到路由,最后正确地渲染对应的组件

location docs from mdn

源码解析

createBrowserHistory

import warning from "warning";
import invariant from "invariant";
import { createLocation } from "./LocationUtils";
import {
  addLeadingSlash,
  stripTrailingSlash,
  hasBasename,
  stripBasename,
  createPath
} from "./PathUtils";
import createTransitionManager from "./createTransitionManager";
import {
  canUseDOM,
  getConfirmation,
  supportsHistory,
  supportsPopStateOnHashChange,
  isExtraneousPopstateEvent
} from "./DOMUtils";

const PopStateEvent = "popstate";
const HashChangeEvent = "hashchange";

const getHistoryState = () => {
  try {
    //一般控制台打印出的都是null
    //经过overstackflow,发现webkit内核浏览器没有实现
    //https://stackoverflow.com/questions/8439145/reading-window-history-state-object-in-webkit
    //state必须由pushState或者replaceState产生,不然就是null
    return window.history.state || {};
  } catch (e) {
    // IE 11 sometimes throws when accessing window.history.state
    // See https://github.com/ReactTraining/history/pull/289
    return {};
  }
};

/**
 * Creates a history object that uses the HTML5 history API including
 * pushState, replaceState, and the popstate event.
 */
const createBrowserHistory = (props = {}) => {
  invariant(canUseDOM, "Browser history needs a DOM");

  const globalHistory = window.history;//这边拿到全局的history对象
  const canUseHistory = supportsHistory();
  const needsHashChangeListener = !supportsPopStateOnHashChange();

  const {
    forceRefresh = false,
    getUserConfirmation = getConfirmation,
    keyLength = 6
  } = props;
  //这边会传入一个基地址,一般传入的props为空,所以也就没有基地址
  const basename = props.basename
    ? stripTrailingSlash(addLeadingSlash(props.basename))
    : "";
  //这个函数时获取封装之后的location
  const getDOMLocation = historyState => {
    const { key, state } = historyState || {};
    //可以在控制台打印出window.location看一下
    const { pathname, search, hash } = window.location;
    //将域名后的部分拼接起来
    let path = pathname + search + hash;

    warning(
      !basename || hasBasename(path, basename),
      "You are attempting to use a basename on a page whose URL path does not begin " +
        'with the basename. Expected path "' +
        path +
        '" to begin with "' +
        basename +
        '".'
    );

    if (basename) path = stripBasename(path, basename);
    //看一下createLoaction,在下方
    return createLocation(path, state, key);
  };

  const createKey = () =>
    Math.random()
      .toString(36)
      .substr(2, keyLength);

  const transitionManager = createTransitionManager();
  //这个地方更新了history,length,并添加上了监听器
  const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
  };

  const handlePopState = event => {
    // Ignore extraneous popstate events in WebKit.
    if (isExtraneousPopstateEvent(event)) return;

    handlePop(getDOMLocation(event.state));
  };

  const handleHashChange = () => {
    handlePop(getDOMLocation(getHistoryState()));
  };

  let forceNextPop = false;

  const handlePop = location => {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      const action = "POP";

      transitionManager.confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        ok => {
          if (ok) {
            setState({ action, location });
          } else {
            revertPop(location);
          }
        }
      );
    }
  };

  const revertPop = fromLocation => {
    const toLocation = history.location;

    // TODO: We could probably make this more reliable by
    // keeping a list of keys we've seen in sessionStorage.
    // Instead, we just default to 0 for keys we don't know.

    let toIndex = allKeys.indexOf(toLocation.key);

    if (toIndex === -1) toIndex = 0;

    let fromIndex = allKeys.indexOf(fromLocation.key);

    if (fromIndex === -1) fromIndex = 0;

    const delta = toIndex - fromIndex;

    if (delta) {
      forceNextPop = true;
      go(delta);
    }
  };

  const initialLocation = getDOMLocation(getHistoryState());
  let allKeys = [initialLocation.key];

  // Public interface
  //创建一个路由pathname
  const createHref = location => basename + createPath(location);
  //实现push方法,是类似于栈结构push进去一个新的路由
  const push = (path, state) => {
    warning(
      !(
        typeof path === "object" &&
        path.state !== undefined &&
        state !== undefined
      ),
      "You should avoid providing a 2nd state argument to push when the 1st " +
        "argument is a location-like object that already has state; it is ignored"
    );
    //这边将动作更换
    const action = "PUSH";
    //创建location对象,这个函数的解析在下面
    const location = createLocation(path, state, createKey(), history.location);
    //这边是更新路由前的确认操作,transition部分解析也在下面
    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;

        const href = createHref(location);
        const { key, state } = location;
        //可以使用就将路由推入
        if (canUseHistory) {
          //这个地方只是地址栏进行更新,但是浏览器不会加载页面
          globalHistory.pushState({ key, state }, null, href);
          //强制刷新选项
          if (forceRefresh) {
            window.location.href = href;
          } else {
            const prevIndex = allKeys.indexOf(history.location.key);
            const nextKeys = allKeys.slice(
              0,
              prevIndex === -1 ? 0 : prevIndex + 1
            );

            nextKeys.push(location.key);
            allKeys = nextKeys;
            //setState更新history对象
            setState({ action, location });
          }
        } else {
          warning(
            state === undefined,
            "Browser history cannot push state in browsers that do not support HTML5 history"
          );
          //不能用就直接刷新
          window.location.href = href;
        }
      }
    );
  };

  //replace操作,这是直接替换路由
  const replace = (path, state) => {
    warning(
      !(
        typeof path === "object" &&
        path.state !== undefined &&
        state !== undefined
      ),
      "You should avoid providing a 2nd state argument to replace when the 1st " +
        "argument is a location-like object that already has state; it is ignored"
    );

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

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

        const href = createHref(location);
        const { key, state } = location;

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

          if (forceRefresh) {
            window.location.replace(href);
          } else {
            const prevIndex = allKeys.indexOf(history.location.key);

            if (prevIndex !== -1) allKeys[prevIndex] = location.key;

            setState({ action, location });
          }
        } else {
          warning(
            state === undefined,
            "Browser history cannot replace state in browsers that do not support HTML5 history"
          );

          window.location.replace(href);
        }
      }
    );
  };

  const go = n => {
    globalHistory.go(n);
  };

  const goBack = () => go(-1);

  const goForward = () => go(1);

  let listenerCount = 0;
  //这边是监听window.histoty对象上的几个事件
  const checkDOMListeners = delta => {
    listenerCount += delta;

    if (listenerCount === 1) {
      window.addEventListener(PopStateEvent, handlePopState);

      if (needsHashChangeListener)
        window.addEventListener(HashChangeEvent, handleHashChange);
    } else if (listenerCount === 0) {
      window.removeEventListener(PopStateEvent, handlePopState);

      if (needsHashChangeListener)
        window.removeEventListener(HashChangeEvent, handleHashChange);
    }
  };

  let isBlocked = false;

  const block = (prompt = false) => {
    const unblock = transitionManager.setPrompt(prompt);

    if (!isBlocked) {
      checkDOMListeners(1);
      isBlocked = true;
    }

    return () => {
      if (isBlocked) {
        isBlocked = false;
        checkDOMListeners(-1);
      }

      return unblock();
    };
  };

  const listen = listener => {
    const unlisten = transitionManager.appendListener(listener);
    checkDOMListeners(1);

    return () => {
      checkDOMListeners(-1);
      unlisten();
    };
  };

  //这边是最终导出的history对象
  const history = {
    length: globalHistory.length,//返回当前session的history个数
    action: "POP",
    location: initialLocation,//Object
    createHref,//接下来一系列方法
    push,
    replace,
    go,
    goBack,
    goForward,
    block,
    listen
  };

  return history;
};

export default createBrowserHistory;

createHashHistory

const HashChangeEvent = "hashchange";

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
  }
};

const getHashPath = () => {
  //这边给出了不用window.location.hash的原因是firefox会预解码
  // We can't use window.location.hash here because it's not
  // consistent across browsers - Firefox will pre-decode it!
  const href = window.location.href;
  const hashIndex = href.indexOf("#");//找到#号出现的位置,并去掉
  return hashIndex === -1 ? "" : href.substring(hashIndex + 1);
};

const pushHashPath = path => (window.location.hash = path);

const replaceHashPath = path => {
  const hashIndex = window.location.href.indexOf("#");

  window.location.replace(
    window.location.href.slice(0, hashIndex >= 0 ? hashIndex : 0) + "#" + path
  );
};

const createHashHistory = (props = {}) => {
  invariant(canUseDOM, "Hash history needs a DOM");

  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];

  const getDOMLocation = () => {
    //创建一个hash路由
    let path = decodePath(getHashPath());

    warning(
      !basename || hasBasename(path, basename),
      "You are attempting to use a basename on a page whose URL path does not begin " +
        'with the basename. Expected path "' +
        path +
        '" to begin with "' +
        basename +
        '".'
    );

    if (basename) path = stripBasename(path, basename);
    //这个函数之前看到过的
    return createLocation(path);
  };

  const transitionManager = createTransitionManager();

  const setState = nextState => {
    Object.assign(history, nextState);

    history.length = globalHistory.length;

    transitionManager.notifyListeners(history.location, history.action);
  };

  let forceNextPop = false;
  let ignorePath = null;

  const handleHashChange = () => {
    const path = getHashPath();
    const encodedPath = encodePath(path);

    if (path !== encodedPath) {
      // Ensure we always have a properly-encoded hash.
      replaceHashPath(encodedPath);
    } else {
      const location = getDOMLocation();
      const prevLocation = history.location;

      if (!forceNextPop && locationsAreEqual(prevLocation, location)) return; // A hashchange doesn't always == location change.
      //hash变化不会总是等于地址变化

      if (ignorePath === createPath(location)) return; // Ignore this change; we already setState in push/replace.
      //如果我们在push/replace中setState就忽视

      ignorePath = null;

      handlePop(location);
    }
  };

  const handlePop = location => {
    if (forceNextPop) {
      forceNextPop = false;
      setState();
    } else {
      const action = "POP";

      transitionManager.confirmTransitionTo(
        location,
        action,
        getUserConfirmation,
        ok => {
          if (ok) {
            setState({ action, location });
          } else {
            revertPop(location);
          }
        }
      );
    }
  };

  const revertPop = fromLocation => {
    const toLocation = history.location;

    // TODO: We could probably make this more reliable by
    // keeping a list of paths we've seen in sessionStorage.
    // Instead, we just default to 0 for paths we don't know.
    //注释说可以用sessiongStorage使得路径列表更可靠

    let toIndex = allPaths.lastIndexOf(createPath(toLocation));

    if (toIndex === -1) toIndex = 0;

    let fromIndex = allPaths.lastIndexOf(createPath(fromLocation));

    if (fromIndex === -1) fromIndex = 0;

    const delta = toIndex - fromIndex;

    if (delta) {
      forceNextPop = true;
      go(delta);
    }
  };

  // Ensure the hash is encoded properly before doing anything else.
  const path = getHashPath();
  const encodedPath = encodePath(path);

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

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

  // Public interface
  //hash路由
  const createHref = location =>
    "#" + encodePath(basename + createPath(location));

  const push = (path, state) => {
    warning(
      state === undefined,
      "Hash history cannot push state; it is ignored"
    );

    const action = "PUSH";
    const location = createLocation(
      path,
      undefined,
      undefined,
      history.location
    );

    transitionManager.confirmTransitionTo(
      location,
      action,
      getUserConfirmation,
      ok => {
        if (!ok) return;
        //获取当前路径并比较有没有发生变化
        const path = createPath(location);
        const encodedPath = encodePath(basename + path);
        const hashChanged = getHashPath() !== encodedPath;

        if (hashChanged) {
          // We cannot tell if a hashchange was caused by a PUSH, so we'd
          // rather setState here and ignore the hashchange. The caveat here
          // is that other hash histories in the page will consider it a POP.
          ignorePath = path;
          pushHashPath(encodedPath);

          const prevIndex = allPaths.lastIndexOf(createPath(history.location));
          const nextPaths = allPaths.slice(
            0,
            prevIndex === -1 ? 0 : prevIndex + 1
          );

          nextPaths.push(path);
          allPaths = nextPaths;

          setState({ action, location });
        } else {
          warning(
            false,
            "Hash history cannot PUSH the same path; a new entry will not be added to the history stack"
          );

          setState();
        }
      }
    );
  };

  const replace = (path, state) => {
    warning(
      state === undefined,
      "Hash history cannot replace state; it is ignored"
    );

    const action = "REPLACE";
    const location = createLocation(
      path,
      undefined,
      undefined,
      history.location
    );

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

        const path = createPath(location);
        const encodedPath = encodePath(basename + path);
        const hashChanged = getHashPath() !== encodedPath;

        if (hashChanged) {
          // We cannot tell if a hashchange was caused by a REPLACE, so we'd
          // rather setState here and ignore the hashchange. The caveat here
          // is that other hash histories in the page will consider it a POP.
          ignorePath = path;
          replaceHashPath(encodedPath);
        }

        const prevIndex = allPaths.indexOf(createPath(history.location));

        if (prevIndex !== -1) allPaths[prevIndex] = path;

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

  const go = n => {
    warning(
      canGoWithoutReload,
      "Hash history go(n) causes a full page reload in this browser"
    );

    globalHistory.go(n);
  };

  const goBack = () => go(-1);

  const goForward = () => go(1);

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

  return history;
};

export default createHashHistory;

使用

import { Router } from 'react-router';
import createBrowserHistory from 'history/createBrowerHistory';//or createHashHistory
const history = createBrowserHistory();
  <Router history={history}>
    <App />
  </Router>

import React from 'react'
import createBrowserHistory from 'history/lib/createBrowserHistory'
import { Router, Route, IndexRoute } from 'react-router'
import App from '../components/App'
import Home from '../components/Home'
import About from '../components/About'
import Features from '../components/Features'

React.render(
  <Router history={createBrowserHistory()}>
    <Route path='/' component={App}>
      <IndexRoute component={Home} />
      <Route path='about' component={About} />
      <Route path='features' component={Features} />
    </Route>
  </Router>,
  document.getElementById('app')
)