git clone https://github.com/ReactTraining/history.git
“version”: “4.10.1”

/modules/index.js

  1. export { default as createBrowserHistory } from './createBrowserHistory';
  2. export { default as createHashHistory } from './createHashHistory';
  3. export { default as createMemoryHistory } from './createMemoryHistory';
  4. export { createLocation, locationsAreEqual } from './LocationUtils';
  5. export { parsePath, createPath } from './PathUtils';

createLocation

  1. import resolvePathname from 'resolve-pathname';
  2. import valueEqual from 'value-equal';
  3. // import { parsePath } from './PathUtils.js';
  4. function parsePath(path) {
  5. let pathname = path || '/';
  6. let search = '';
  7. let hash = '';
  8. const hashIndex = pathname.indexOf('#');
  9. if (hashIndex !== -1) {
  10. hash = pathname.substr(hashIndex);
  11. pathname = pathname.substr(0, hashIndex);
  12. }
  13. const searchIndex = pathname.indexOf('?');
  14. if (searchIndex !== -1) {
  15. search = pathname.substr(searchIndex);
  16. pathname = pathname.substr(0, searchIndex);
  17. }
  18. return {
  19. pathname,
  20. search: search === '?' ? '' : search,
  21. hash: hash === '#' ? '' : hash
  22. };
  23. }
  24. export function createLocation(path, state, key, currentLocation) {
  25. let location;
  26. if (typeof path === 'string') {
  27. // Two-arg form: push(path, state)
  28. location = parsePath(path);
  29. location.state = state;
  30. } else {
  31. // One-arg form: push(location)
  32. location = { ...path };
  33. if (location.pathname === undefined) location.pathname = '';
  34. if (location.search) {
  35. if (location.search.charAt(0) !== '?')
  36. location.search = '?' + location.search;
  37. } else {
  38. location.search = '';
  39. }
  40. if (location.hash) {
  41. if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
  42. } else {
  43. location.hash = '';
  44. }
  45. if (state !== undefined && location.state === undefined)
  46. location.state = state;
  47. }
  48. try {
  49. location.pathname = decodeURI(location.pathname);
  50. } catch (e) {
  51. if (e instanceof URIError) {
  52. throw new URIError(
  53. 'Pathname "' +
  54. location.pathname +
  55. '" could not be decoded. ' +
  56. 'This is likely caused by an invalid percent-encoding.'
  57. );
  58. } else {
  59. throw e;
  60. }
  61. }
  62. if (key) location.key = key;
  63. if (currentLocation) {
  64. // Resolve incomplete/relative pathname relative to current location.
  65. if (!location.pathname) {
  66. location.pathname = currentLocation.pathname;
  67. } else if (location.pathname.charAt(0) !== '/') {
  68. location.pathname = resolvePathname(
  69. location.pathname,
  70. currentLocation.pathname
  71. );
  72. }
  73. } else {
  74. // When there is no prior location and pathname is empty, set it to /
  75. if (!location.pathname) {
  76. location.pathname = '/';
  77. }
  78. }
  79. return location;
  80. }
  81. export function locationsAreEqual(a, b) {
  82. return (
  83. a.pathname === b.pathname &&
  84. a.search === b.search &&
  85. a.hash === b.hash &&
  86. a.key === b.key &&
  87. valueEqual(a.state, b.state)
  88. );
  89. }

createBrowserHistory

  1. import { createLocation } from './LocationUtils.js';
  2. import {
  3. addLeadingSlash,
  4. stripTrailingSlash,
  5. hasBasename,
  6. stripBasename,
  7. createPath
  8. } from './PathUtils.js';
  9. import createTransitionManager from './createTransitionManager.js';
  10. import {
  11. canUseDOM,
  12. getConfirmation,
  13. supportsPopStateOnHashChange,
  14. isExtraneousPopstateEvent
  15. } from './DOMUtils.js';
  16. import invariant from './invariant.js';
  17. import warning from './warning.js';
  18. const PopStateEvent = 'popstate';
  19. const HashChangeEvent = 'hashchange';
  20. function getHistoryState() {
  21. try {
  22. return window.history.state || {};
  23. } catch (e) {
  24. // IE 11 sometimes throws when accessing window.history.state
  25. // See https://github.com/ReactTraining/history/pull/289
  26. return {};
  27. }
  28. }
  29. function supportsHistory() {
  30. const ua = window.navigator.userAgent;
  31. if (
  32. (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
  33. ua.indexOf('Mobile Safari') !== -1 &&
  34. ua.indexOf('Chrome') === -1 &&
  35. ua.indexOf('Windows Phone') === -1
  36. )
  37. return false;
  38. return window.history && 'pushState' in window.history;
  39. }
  40. /**
  41. * Creates a history object that uses the HTML5 history API including
  42. * pushState, replaceState, and the popstate event.
  43. */
  44. function createBrowserHistory(props = {}) {
  45. invariant(canUseDOM, 'Browser history needs a DOM');
  46. const globalHistory = window.history;
  47. const canUseHistory = supportsHistory();
  48. const needsHashChangeListener = !supportsPopStateOnHashChange();
  49. const {
  50. forceRefresh = false,
  51. getUserConfirmation = getConfirmation,
  52. keyLength = 6
  53. } = props;
  54. const basename = props.basename
  55. ? stripTrailingSlash(addLeadingSlash(props.basename))
  56. : '';
  57. function getDOMLocation(historyState) {
  58. const { key, state } = historyState || {};
  59. const { pathname, search, hash } = window.location;
  60. let path = pathname + search + hash;
  61. warning(
  62. !basename || hasBasename(path, basename),
  63. 'You are attempting to use a basename on a page whose URL path does not begin ' +
  64. 'with the basename. Expected path "' +
  65. path +
  66. '" to begin with "' +
  67. basename +
  68. '".'
  69. );
  70. if (basename) path = stripBasename(path, basename);
  71. return createLocation(path, state, key);
  72. }
  73. function createKey() {
  74. return Math.random()
  75. .toString(36)
  76. .substr(2, keyLength);
  77. }
  78. const transitionManager = createTransitionManager();
  79. function setState(nextState) {
  80. Object.assign(history, nextState);
  81. history.length = globalHistory.length;
  82. transitionManager.notifyListeners(history.location, history.action);
  83. }
  84. function handlePopState(event) {
  85. // Ignore extraneous popstate events in WebKit.
  86. if (isExtraneousPopstateEvent(event)) return;
  87. handlePop(getDOMLocation(event.state));
  88. }
  89. function handleHashChange() {
  90. handlePop(getDOMLocation(getHistoryState()));
  91. }
  92. let forceNextPop = false;
  93. function handlePop(location) {
  94. if (forceNextPop) {
  95. forceNextPop = false;
  96. setState();
  97. } else {
  98. const action = 'POP';
  99. transitionManager.confirmTransitionTo(
  100. location,
  101. action,
  102. getUserConfirmation,
  103. ok => {
  104. if (ok) {
  105. setState({ action, location });
  106. } else {
  107. revertPop(location);
  108. }
  109. }
  110. );
  111. }
  112. }
  113. function revertPop(fromLocation) {
  114. const toLocation = history.location;
  115. // TODO: We could probably make this more reliable by
  116. // keeping a list of keys we've seen in sessionStorage.
  117. // Instead, we just default to 0 for keys we don't know.
  118. let toIndex = allKeys.indexOf(toLocation.key);
  119. if (toIndex === -1) toIndex = 0;
  120. let fromIndex = allKeys.indexOf(fromLocation.key);
  121. if (fromIndex === -1) fromIndex = 0;
  122. const delta = toIndex - fromIndex;
  123. if (delta) {
  124. forceNextPop = true;
  125. go(delta);
  126. }
  127. }
  128. const initialLocation = getDOMLocation(getHistoryState());
  129. let allKeys = [initialLocation.key];
  130. // Public interface
  131. function createHref(location) {
  132. return basename + createPath(location);
  133. }
  134. function push(path, state) {
  135. warning(
  136. !(
  137. typeof path === 'object' &&
  138. path.state !== undefined &&
  139. state !== undefined
  140. ),
  141. 'You should avoid providing a 2nd state argument to push when the 1st ' +
  142. 'argument is a location-like object that already has state; it is ignored'
  143. );
  144. const action = 'PUSH';
  145. const location = createLocation(path, state, createKey(), history.location);
  146. transitionManager.confirmTransitionTo(
  147. location,
  148. action,
  149. getUserConfirmation,
  150. ok => {
  151. if (!ok) return;
  152. const href = createHref(location);
  153. const { key, state } = location;
  154. if (canUseHistory) {
  155. globalHistory.pushState({ key, state }, null, href);
  156. if (forceRefresh) {
  157. window.location.href = href;
  158. } else {
  159. const prevIndex = allKeys.indexOf(history.location.key);
  160. const nextKeys = allKeys.slice(0, prevIndex + 1);
  161. nextKeys.push(location.key);
  162. allKeys = nextKeys;
  163. setState({ action, location });
  164. }
  165. } else {
  166. warning(
  167. state === undefined,
  168. 'Browser history cannot push state in browsers that do not support HTML5 history'
  169. );
  170. window.location.href = href;
  171. }
  172. }
  173. );
  174. }
  175. function replace(path, state) {
  176. warning(
  177. !(
  178. typeof path === 'object' &&
  179. path.state !== undefined &&
  180. state !== undefined
  181. ),
  182. 'You should avoid providing a 2nd state argument to replace when the 1st ' +
  183. 'argument is a location-like object that already has state; it is ignored'
  184. );
  185. const action = 'REPLACE';
  186. const location = createLocation(path, state, createKey(), history.location);
  187. transitionManager.confirmTransitionTo(
  188. location,
  189. action,
  190. getUserConfirmation,
  191. ok => {
  192. if (!ok) return;
  193. const href = createHref(location);
  194. const { key, state } = location;
  195. if (canUseHistory) {
  196. globalHistory.replaceState({ key, state }, null, href);
  197. if (forceRefresh) {
  198. window.location.replace(href);
  199. } else {
  200. const prevIndex = allKeys.indexOf(history.location.key);
  201. if (prevIndex !== -1) allKeys[prevIndex] = location.key;
  202. setState({ action, location });
  203. }
  204. } else {
  205. warning(
  206. state === undefined,
  207. 'Browser history cannot replace state in browsers that do not support HTML5 history'
  208. );
  209. window.location.replace(href);
  210. }
  211. }
  212. );
  213. }
  214. function go(n) {
  215. globalHistory.go(n);
  216. }
  217. function goBack() {
  218. go(-1);
  219. }
  220. function goForward() {
  221. go(1);
  222. }
  223. let listenerCount = 0;
  224. function checkDOMListeners(delta) {
  225. listenerCount += delta;
  226. if (listenerCount === 1 && delta === 1) {
  227. window.addEventListener(PopStateEvent, handlePopState);
  228. if (needsHashChangeListener)
  229. window.addEventListener(HashChangeEvent, handleHashChange);
  230. } else if (listenerCount === 0) {
  231. window.removeEventListener(PopStateEvent, handlePopState);
  232. if (needsHashChangeListener)
  233. window.removeEventListener(HashChangeEvent, handleHashChange);
  234. }
  235. }
  236. let isBlocked = false;
  237. function block(prompt = false) {
  238. const unblock = transitionManager.setPrompt(prompt);
  239. if (!isBlocked) {
  240. checkDOMListeners(1);
  241. isBlocked = true;
  242. }
  243. return () => {
  244. if (isBlocked) {
  245. isBlocked = false;
  246. checkDOMListeners(-1);
  247. }
  248. return unblock();
  249. };
  250. }
  251. function listen(listener) {
  252. const unlisten = transitionManager.appendListener(listener);
  253. checkDOMListeners(1);
  254. return () => {
  255. checkDOMListeners(-1);
  256. unlisten();
  257. };
  258. }
  259. const history = {
  260. length: globalHistory.length,
  261. action: 'POP',
  262. location: initialLocation,
  263. createHref,
  264. push,
  265. replace,
  266. go,
  267. goBack,
  268. goForward,
  269. block,
  270. listen
  271. };
  272. return history;
  273. }
  274. export default createBrowserHistory;

createTransitionManager 监听器

当BrowserHistory发出setState时,会执行对应的listener

  1. import warning from './warning.js';
  2. function createTransitionManager() {
  3. let prompt = null;
  4. function setPrompt(nextPrompt) {
  5. warning(prompt == null, 'A history supports only one prompt at a time');
  6. prompt = nextPrompt;
  7. return () => {
  8. if (prompt === nextPrompt) prompt = null;
  9. };
  10. }
  11. function confirmTransitionTo(
  12. location,
  13. action,
  14. getUserConfirmation,
  15. callback
  16. ) {
  17. // TODO: If another transition starts while we're still confirming
  18. // the previous one, we may end up in a weird state. Figure out the
  19. // best way to handle this.
  20. if (prompt != null) {
  21. const result =
  22. typeof prompt === 'function' ? prompt(location, action) : prompt;
  23. if (typeof result === 'string') {
  24. if (typeof getUserConfirmation === 'function') {
  25. getUserConfirmation(result, callback);
  26. } else {
  27. warning(
  28. false,
  29. 'A history needs a getUserConfirmation function in order to use a prompt message'
  30. );
  31. callback(true);
  32. }
  33. } else {
  34. // Return false from a transition hook to cancel the transition.
  35. callback(result !== false);
  36. }
  37. } else {
  38. callback(true);
  39. }
  40. }
  41. let listeners = [];
  42. function appendListener(fn) {
  43. let isActive = true;
  44. function listener(...args) {
  45. if (isActive) fn(...args);
  46. }
  47. listeners.push(listener);
  48. return () => {
  49. isActive = false;
  50. listeners = listeners.filter(item => item !== listener);
  51. };
  52. }
  53. function notifyListeners(...args) {
  54. listeners.forEach(listener => listener(...args));
  55. }
  56. return {
  57. setPrompt,
  58. confirmTransitionTo,
  59. appendListener,
  60. notifyListeners
  61. };
  62. }
  63. export default createTransitionManager;