git clone https://github.com/ReactTraining/history.git
“version”: “4.10.1”
/modules/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';
createLocation
import resolvePathname from 'resolve-pathname';
import valueEqual from 'value-equal';
// import { parsePath } from './PathUtils.js';
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
};
}
export function createLocation(path, state, key, currentLocation) {
let location;
if (typeof path === 'string') {
// Two-arg form: push(path, state)
location = parsePath(path);
location.state = state;
} else {
// One-arg form: push(location)
location = { ...path };
if (location.pathname === undefined) location.pathname = '';
if (location.search) {
if (location.search.charAt(0) !== '?')
location.search = '?' + location.search;
} else {
location.search = '';
}
if (location.hash) {
if (location.hash.charAt(0) !== '#') location.hash = '#' + location.hash;
} else {
location.hash = '';
}
if (state !== undefined && location.state === undefined)
location.state = state;
}
try {
location.pathname = decodeURI(location.pathname);
} catch (e) {
if (e instanceof URIError) {
throw new URIError(
'Pathname "' +
location.pathname +
'" could not be decoded. ' +
'This is likely caused by an invalid percent-encoding.'
);
} else {
throw e;
}
}
if (key) location.key = key;
if (currentLocation) {
// Resolve incomplete/relative pathname relative to current location.
if (!location.pathname) {
location.pathname = currentLocation.pathname;
} else if (location.pathname.charAt(0) !== '/') {
location.pathname = resolvePathname(
location.pathname,
currentLocation.pathname
);
}
} else {
// When there is no prior location and pathname is empty, set it to /
if (!location.pathname) {
location.pathname = '/';
}
}
return location;
}
export function locationsAreEqual(a, b) {
return (
a.pathname === b.pathname &&
a.search === b.search &&
a.hash === b.hash &&
a.key === b.key &&
valueEqual(a.state, b.state)
);
}
createBrowserHistory
import { createLocation } from './LocationUtils.js';
import {
addLeadingSlash,
stripTrailingSlash,
hasBasename,
stripBasename,
createPath
} from './PathUtils.js';
import createTransitionManager from './createTransitionManager.js';
import {
canUseDOM,
getConfirmation,
supportsPopStateOnHashChange,
isExtraneousPopstateEvent
} from './DOMUtils.js';
import invariant from './invariant.js';
import warning from './warning.js';
const PopStateEvent = 'popstate';
const HashChangeEvent = 'hashchange';
function getHistoryState() {
try {
return window.history.state || {};
} catch (e) {
// IE 11 sometimes throws when accessing window.history.state
// See https://github.com/ReactTraining/history/pull/289
return {};
}
}
function supportsHistory() {
const ua = window.navigator.userAgent;
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
)
return false;
return window.history && 'pushState' in window.history;
}
/**
* Creates a history object that uses the HTML5 history API including
* pushState, replaceState, and the popstate event.
*/
function createBrowserHistory(props = {}) {
invariant(canUseDOM, 'Browser history needs a DOM');
const globalHistory = window.history;
const canUseHistory = supportsHistory();
const needsHashChangeListener = !supportsPopStateOnHashChange();
const {
forceRefresh = false,
getUserConfirmation = getConfirmation,
keyLength = 6
} = props;
const basename = props.basename
? stripTrailingSlash(addLeadingSlash(props.basename))
: '';
function getDOMLocation(historyState) {
const { key, state } = historyState || {};
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);
return createLocation(path, state, key);
}
function createKey() {
return Math.random()
.toString(36)
.substr(2, keyLength);
}
const transitionManager = createTransitionManager();
function setState(nextState) {
Object.assign(history, nextState);
history.length = globalHistory.length;
transitionManager.notifyListeners(history.location, history.action);
}
function handlePopState(event) {
// Ignore extraneous popstate events in WebKit.
if (isExtraneousPopstateEvent(event)) return;
handlePop(getDOMLocation(event.state));
}
function handleHashChange() {
handlePop(getDOMLocation(getHistoryState()));
}
let forceNextPop = false;
function 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);
}
}
);
}
}
function 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
function createHref(location) {
return basename + createPath(location);
}
function 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';
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.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);
nextKeys.push(location.key);
allKeys = nextKeys;
setState({ action, location });
}
} else {
warning(
state === undefined,
'Browser history cannot push state in browsers that do not support HTML5 history'
);
window.location.href = href;
}
}
);
}
function 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);
}
}
);
}
function go(n) {
globalHistory.go(n);
}
function goBack() {
go(-1);
}
function goForward() {
go(1);
}
let listenerCount = 0;
function checkDOMListeners(delta) {
listenerCount += delta;
if (listenerCount === 1 && delta === 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;
function block(prompt = false) {
const unblock = transitionManager.setPrompt(prompt);
if (!isBlocked) {
checkDOMListeners(1);
isBlocked = true;
}
return () => {
if (isBlocked) {
isBlocked = false;
checkDOMListeners(-1);
}
return unblock();
};
}
function listen(listener) {
const unlisten = transitionManager.appendListener(listener);
checkDOMListeners(1);
return () => {
checkDOMListeners(-1);
unlisten();
};
}
const history = {
length: globalHistory.length,
action: 'POP',
location: initialLocation,
createHref,
push,
replace,
go,
goBack,
goForward,
block,
listen
};
return history;
}
export default createBrowserHistory;
createTransitionManager 监听器
当BrowserHistory发出setState时,会执行对应的listener
import warning from './warning.js';
function createTransitionManager() {
let prompt = null;
function setPrompt(nextPrompt) {
warning(prompt == null, 'A history supports only one prompt at a time');
prompt = nextPrompt;
return () => {
if (prompt === nextPrompt) prompt = null;
};
}
function confirmTransitionTo(
location,
action,
getUserConfirmation,
callback
) {
// TODO: If another transition starts while we're still confirming
// the previous one, we may end up in a weird state. Figure out the
// best way to handle this.
if (prompt != null) {
const result =
typeof prompt === 'function' ? prompt(location, action) : prompt;
if (typeof result === 'string') {
if (typeof getUserConfirmation === 'function') {
getUserConfirmation(result, callback);
} else {
warning(
false,
'A history needs a getUserConfirmation function in order to use a prompt message'
);
callback(true);
}
} else {
// Return false from a transition hook to cancel the transition.
callback(result !== false);
}
} else {
callback(true);
}
}
let listeners = [];
function appendListener(fn) {
let isActive = true;
function listener(...args) {
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 {
setPrompt,
confirmTransitionTo,
appendListener,
notifyListeners
};
}
export default createTransitionManager;