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 使用它匹配到路由,最后正確地渲染對應的組件
源碼解析
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')
)