history
- React Routerのroutingに関するUtil Library
- 公式サイト
- React Router内のhisotryはこのLibraryのこと
listen
const createBrowserHistory = (options = {}) => {
...,
const listen = (listener) =>
startListener(listener, false)
}
listenerをstartListenerに渡しているだけのようです。
startListenerを見に行きます。
const startListener = (listener, before) => {
if (++listenerCount === 1)
stopListener = BrowserProtocol.startListener(
history.transitionTo
)
const unlisten = before
? history.listenBefore(listener)
: history.listen(listener)
return () => {
unlisten()
if (--listenerCount === 0)
stopListener()
}
}
初回は++listenerCount === 1
なのでstopListenerが作成されます。
まずは、BrowserProtocolを見てみます。
export const startListener = (listener) => {
const handlePopState = (event) => {
if (event.state !== undefined) // Ignore extraneous popstate events in WebKit
listener(_createLocation(event.state))
}
addEventListener(window, PopStateEvent, handlePopState)
return () =>
removeEventListener(window, PopStateEvent, handlePopState)
}
htmlのpopstate
イベントにlistenerを登録しています。
まずは、listenerとして渡された、history.transitionToを見てみます。
その前に、historyとは何なのかを確認します。
const Protocol = useRefresh ? RefreshProtocol : BrowserProtocol
const {
getUserConfirmation,
getCurrentLocation,
pushLocation,
replaceLocation,
go
} = Protocol
const history = createHistory({
getUserConfirmation, // User may override in options
...options,
getCurrentLocation,
pushLocation,
replaceLocation,
go
})
ProtocolはBrowserProtocolと考えて進めます。
BrowserProtocolで定義されているmethodを取得後に、createHistoryしています。
const createHistory = (options = {}) => {
const {
getCurrentLocation,
getUserConfirmation,
pushLocation,
replaceLocation,
go,
keyLength
} = options
...,
return {
getCurrentLocation,
listenBefore,
listen,
transitionTo,
push,
replace,
go,
goBack,
goForward,
createKey,
createPath,
createHref,
createLocation
}
}
定義したmethodをまとめたObjectのようです。
transitionToがあるのでこれを確認します。
const transitionTo = (nextLocation) => {
if (
(currentLocation && locationsAreEqual(currentLocation, nextLocation)) ||
(pendingLocation && locationsAreEqual(pendingLocation, nextLocation))
)
return // Nothing to do
...,
confirmTransitionTo(nextLocation, (ok) => {
if (pendingLocation !== nextLocation)
return // Transition was interrupted during confirmation
pendingLocation = null
if (ok) {
if (nextLocation.action === PUSH) {
const prevPath = createPath(currentLocation)
const nextPath = createPath(nextLocation)
if (nextPath === prevPath && deepEqual(currentLocation.state, nextLocation.state))
nextLocation.action = REPLACE
if (nextLocation.action === POP) {
updateLocation(nextLocation)
} else if (nextLocation.action === PUSH) {
if (pushLocation(nextLocation) !== false)
updateLocation(nextLocation)
} else if (nextLocation.action === REPLACE) {
if (replaceLocation(nextLocation) !== false)
updateLocation(nextLocation)
}
} else if (currentLocation && nextLocation.action === POP) {
const prevIndex = allKeys.indexOf(currentLocation.key)
const nextIndex = allKeys.indexOf(nextLocation.key)
if (prevIndex !== -1 && nextIndex !== -1)
go(prevIndex - nextIndex) // Restore the URL
}
})
}
Locationのactionによって処理を変更しています。
処理内のconfirmTransitionToを見てみます。
const confirmTransitionTo = (location, callback) => {
loopAsync(
beforeListeners.length,
(index, next, done) => {
runTransitionHook(beforeListeners[index], location, (result) =>
result != null ? done(result) : next()
)
},
(message) => {
if (getUserConfirmation && typeof message === 'string') {
getUserConfirmation(message, (ok) => callback(ok !== false))
} else {
callback(message !== false)
}
}
)
}
laodAsyncに3つの引数が渡されています。
loadAsyncが何をしているかを確認します。
export const loopAsync = (turns, work, callback) => {
let currentTurn = 0, isDone = false
let isSync = false, hasNext = false, doneArgs
const done = (...args) => {
...,
}
const next = () => {
...,
}
next()
}
ここまででlistenerが分かりました。
listenerの引数である_createLocation
を見に行きます。
const _createLocation = (historyState) => {
const key = historyState && historyState.key
return createLocation({
pathname: window.location.pathname,
search: window.location.search,
hash: window.location.hash,
state: (key ? readState(key) : undefined)
}, undefined, key)
}
event.state.keyを取得して、createLocationしています。
stateのreadStateを見てみます。
export const readState = (key) => {
let json
try {
json = window.sessionStorage.getItem(createKey(key))
} catch (error) {
if (error.name === SecurityError) {
// Blocking cookies in Chrome/Firefox/Safari throws SecurityError on any
// attempt to access window.sessionStorage.
warning(
false,
'[history] Unable to read state; sessionStorage is not available due to security settings'
)
return undefined
}
}
if (json) {
try {
return JSON.parse(json)
} catch (error) {
// Ignore invalid JSON.
}
}
return undefined
}
まずは、createKeyを見ていきます。
const KeyPrefix = '@@History/'
const createKey = (key) =>
KeyPrefix + key
このkeyをwindow.sessionStorage.getItemに渡してjsonを取得しています。
おそらく、browserが持つsessionの内、履歴についてのものでしょうか。
export const createLocation = (input = '/', action = POP, key = null) => {
const object = typeof input === 'string' ? parsePath(input) : input
const pathname = object.pathname || '/'
const search = object.search || ''
const hash = object.hash || ''
const state = object.state
return {
pathname,
search,
hash,
state,
action,
key
}
}
input={...}
のObjectを渡しています。
actionにはundefinedが渡されていますが、undefinedの場合はdefault値が適用されるので、action=POPとなります。
結果は、LocationのObjectを返しています。
inputを引数に取るparsePathを見に行きます。
export const parsePath = (path) => {
let pathname = extractPath(path)
let search = ''
let hash = ''
...,
const hashIndex = pathname.indexOf('#')
if (hashIndex !== -1) {
hash = pathname.substring(hashIndex)
pathname = pathname.substring(0, hashIndex)
}
const searchIndex = pathname.indexOf('?')
if (searchIndex !== -1) {
search = pathname.substring(searchIndex)
pathname = pathname.substring(0, searchIndex)
}
if (pathname === '')
pathname = '/'
return {
pathname,
search,
hash
}
}
pathnameを返す、extractPathを見てみます。
const extractPath = (string) => {
const match = string.match(/^https?:\/\/[^\/]*/)
return match == null ? string : string.substring(match[0].length)
}
inputのstringに対して正規表現で抜き出しています。
https://hogehoge/bar
とhttp://hogehoge/foo
のようなものはmatchしますが、それ以外はstringそのままを返します。
parsePathに戻って、次にhashstringとquerystringが含まれていればそこまでの文字列をpathnameとしています。
https://hogehoge/#bar -> { pathname: https://hogehoge/, hash: #bar }
となり、https://hogehoge/foo?item=bar -> { pathname: https://hogehoge/foo, search: ?item=bar }
となります。
以上からまとめると、createBrowserHistory.listen(listener)が実行されると次のように処理が走ることがわかりました。
window.popstate(browserのurlが変更される)のイベントが発生->現在のstateからlocationを作成->loadAsync内の処理でok=trueならば、actionはPOPなのでupdateLocationが実行される
これでReact Routerを読み進めることができます。