Part01 What's the problem
這段代碼意圖是把router傳遞props的路由信息再傳遞給redux。有這麼幾個問題:
- 如果靠組件生命週期轉發 每個路由下面的頂級組件都要調這樣一個action
- 並且,如果路由有參數改變(很多時候頁面狀態的參數會在路由中體現),這段代碼是無法檢測的,還需要在componentWillReceiveProps裏去處理邏輯。
- 還有這個setTimeout解決異步問題,極度不優雅。
Can't cooperate
redux 是狀態管理的庫,router 是(唯一)控制頁面跳轉的庫。兩者都很美好,但是不美好的是兩者無法協同工作。換句話說,當路由變化以後,store 無法感知到。
redux是想把絕大多數應用程序的狀態都保存在單一的store裏,而當前的路由狀態明顯是應用程序狀態很重要的一部分,應當是要保存在store中的。
目前是,如果直接使用react router,就意味着所有路由相關的信息脫離了Redux store的控制,假借組件接受router信息轉發dispatch的方法屬於反模式,違背了redux的設計思想,也給我們應用程序帶來了更多的不確定性。
Part02 What do we need
我們需要一個這樣的路由系統,他技能利用React Router的聲明式特性,又能將路由信息整合進Redux Store中。
react-router-redux
react-router-redux 是 redux 的一箇中間件(中間件:JavaScript 代理模式的另一種實踐 針對 dispatch 實現了方法的代理,在 dispatch action 的時候增加或者修改) ,主要作用是:
加強了React Router庫中history這個實例,以允許將history中接受到的變化反應到state中去。
Part03 How to use
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import { Router, Route, browserHistory } from 'react-router'
import { syncHistoryWithStore, routerReducer } from 'react-router-redux'
import reducers from '<project-path>/reducers'
const store = createStore(
combineReducers({
...reducers,
routing: routerReducer
})
)
const history = syncHistoryWithStore(browserHistory, store)
ReactDOM.render(
<Provider store={store}>
<Router history={history}>
<Route path="/" component={App} />
</Router>
</Provider>,
document.getElementById(‘app')
)
使用簡單直白的api syncHistoryWithStore來完成redux的綁定工作,我們只需要傳入react router中的history(前面提到的)以及redux中的store,就可以獲得一個增強後的history對象。
將這個history對象傳給react router中的Router組件作爲props,就給應用提供了觀察路由變化並改變store的能力。
現在,只要您按下瀏覽器按鈕或在應用程序代碼中導航,導航就會首先通過Redux存儲區傳遞新位置,然後再傳遞到React Router以更新組件樹。如果您計時旅行,它還會將新狀態傳遞給React Router以再次更新組件樹。
如何訪問容器組件中的路由器狀態?
React Router 通過路徑組件的props提供路由信息。這使得從容器組件訪問它們變得容易。當使用react-redux對connect()你的組件進行陳述時,你可以從第二個參數mapStateToProps訪問路由器的道具:
Part04 Code principle
https://github.com/reactjs/react-router-redux
// index.js
/**
* 作爲外部 syncHistoryWithStore
* 綁定store.dispatch方法引起的state中路由狀態變更到影響瀏覽器location變更
* 綁定瀏覽器location變更觸發store.dispatch,更新state中路由狀態
* 返回當前的histroy、綁定方法listen(dispatch方法觸發時執行,以綁定前的路由狀態爲參數)、解綁函數unsubscribe
*/
export syncHistoryWithStore from './sync'
/**
* routerReducer監聽路由變更子reducer,通過redux的combineReducers複合多個reducer後使用
*/
export { LOCATION_CHANGE, routerReducer } from './reducer'
/**
* 構建actionCreater,作爲外部push、replace、go、goBack、goForward方法的接口,通常不直接使用
*/
export {
CALL_HISTORY_METHOD,
push, replace, go, goBack, goForward,
routerActions
} from './actions'
/**
* 構建route中間件,用於分發action,觸發路徑跳轉等事件
*/
export routerMiddleware from './middleware'
// sync.js
import { LOCATION_CHANGE } from './reducer'
// 默認用state.routing存取route變更狀態數據
const defaultSelectLocationState = state => state.routing
/**
* 作爲外部syncHistoryWithStore接口方法
* 綁定store.dispatch方法引起的state中路由狀態變更到影響瀏覽器location變更
* 綁定瀏覽器location變更觸發store.dispatch,更新state中路由狀態
* 返回當前的histroy、綁定方法listen(dispatch方法觸發時執行,以綁定前的路由狀態爲參數)、解綁函數unsubscribe
*/
export default function syncHistoryWithStore(history, store, {
// 約定redux.store.state中哪個屬性用於存取route變更狀態數據
selectLocationState = defaultSelectLocationState,
// store中路由狀態變更是否引起瀏覽器location改變
adjustUrlOnReplay = true
} = {}) {
// Ensure that the reducer is mounted on the store and functioning properly.
// 確保redux.store.state中某個屬性綁定了route變更狀態
if (typeof selectLocationState(store.getState()) === 'undefined') {
throw new Error(
'Expected the routing state to be available either as `state.routing` ' +
'or as the custom expression you can specify as `selectLocationState` ' +
'in the `syncHistoryWithStore()` options. ' +
'Ensure you have added the `routerReducer` to your store\'s ' +
'reducers via `combineReducers` or whatever method you use to isolate ' +
'your reducers.'
)
}
let initialLocation // 初始化route狀態數據
let isTimeTraveling // 瀏覽器頁面location.url改變過程中標識,區別頁面鏈接及react-router-redux變更location兩種情況
let unsubscribeFromStore // 移除store.listeners中,因路由狀態引起瀏覽器location變更函數
let unsubscribeFromHistory // 移除location變更引起路由狀態更新函數
let currentLocation // 記錄上一個當前路由狀態數據
// 獲取路由事件觸發後路由狀態,或者useInitialIfEmpty爲真值獲取初始化route狀態,或者undefined
const getLocationInStore = (useInitialIfEmpty) => {
const locationState = selectLocationState(store.getState())
return locationState.locationBeforeTransitions ||
(useInitialIfEmpty ? initialLocation : undefined)
}
// 初始化route狀態數據
initialLocation = getLocationInStore()
// If the store is replayed, update the URL in the browser to match.
// adjustUrlOnReplay爲真值時,store數據改變事件dispatch發生後,瀏覽器頁面更新location
if (adjustUrlOnReplay) {
// 由store中路由狀態改變情況,更新瀏覽器location
const handleStoreChange = () => {
// 獲取路由事件觸發後路由狀態,或者初始路由狀態
const locationInStore = getLocationInStore(true)
if (currentLocation === locationInStore || initialLocation === locationInStore) {
return
}
// 瀏覽器頁面location.url改變過程中標識,區別頁面鏈接及react-router-redux變更location兩種情況
isTimeTraveling = true
// 記錄上一個當前路由狀態數據
currentLocation = locationInStore
// store數據改變後,瀏覽器頁面更新location
history.transitionTo({
...locationInStore,
action: 'PUSH'
})
isTimeTraveling = false
}
// 綁定事件,完成功能爲,dispatch方法觸發store中路由狀態改變時,更新瀏覽器location
unsubscribeFromStore = store.subscribe(handleStoreChange)
// 初始化設置路由狀態時引起頁面location改變
handleStoreChange()
}
// 頁面鏈接變更瀏覽器location,觸發store.dispatch變更store中路由狀態
const handleLocationChange = (location) => {
// react-router-redux引起瀏覽器location變更過程中,無效;頁面鏈接變更,有效
if (isTimeTraveling) {
return
}
// Remember where we are
currentLocation = location
// Are we being called for the first time?
if (!initialLocation) {
// Remember as a fallback in case state is reset
initialLocation = location
// Respect persisted location, if any
if (getLocationInStore()) {
return
}
}
// Tell the store to update by dispatching an action
store.dispatch({
type: LOCATION_CHANGE,
payload: location
})
}
// hashHistory、boswerHistory監聽瀏覽器location變更,觸發store.dispatch變更store中路由狀態
unsubscribeFromHistory = history.listen(handleLocationChange)
// History 3.x doesn't call listen synchronously, so fire the initial location change ourselves
// 初始化更新store中路由狀態
if (history.getCurrentLocation) {
handleLocationChange(history.getCurrentLocation())
}
// The enhanced history uses store as source of truth
return {
...history,
// store中dispatch方法觸發時,綁定執行函數listener,以綁定前的路由狀態爲參數
listen(listener) {
// Copy of last location.
// 綁定前的路由狀態
let lastPublishedLocation = getLocationInStore(true)
// Keep track of whether we unsubscribed, as Redux store
// only applies changes in subscriptions on next dispatch
let unsubscribed = false // 確保listener在解綁後不執行
const unsubscribeFromStore = store.subscribe(() => {
const currentLocation = getLocationInStore(true)
if (currentLocation === lastPublishedLocation) {
return
}
lastPublishedLocation = currentLocation
if (!unsubscribed) {
listener(lastPublishedLocation)
}
})
// History 2.x listeners expect a synchronous call. Make the first call to the
// listener after subscribing to the store, in case the listener causes a
// location change (e.g. when it redirects)
if (!history.getCurrentLocation) {
listener(lastPublishedLocation)
}
// Let user unsubscribe later
return () => {
unsubscribed = true
unsubscribeFromStore()
}
},
// 解綁函數,包括location到store的handleLocationChange、store到location的handleStoreChange
unsubscribe() {
if (adjustUrlOnReplay) {
unsubscribeFromStore()
}
unsubscribeFromHistory()
}
}
}
// reducer.js
/**
* This action type will be dispatched when your history
* receives a location change.
*/
export const LOCATION_CHANGE = '@@router/LOCATION_CHANGE'
const initialState = {
locationBeforeTransitions: null
}
/**
* 監聽路由變更子reducer,通過redux的combineReducers複合多個reducer後使用,作爲外部routerReducer接口
* 提示redux使用過程中,可通過子組件模塊中注入reducer,再使用combineReducers複合多個reducer
* 最後使用replaceReducer方法更新當前store的reducer,意義是構建reducer拆解到各個子模塊中
* */
export function routerReducer(state = initialState, { type, payload } = {}) {
if (type === LOCATION_CHANGE) {
return { ...state, locationBeforeTransitions: payload }
}
return state
}
// actions.js
export const CALL_HISTORY_METHOD = '@@router/CALL_HISTORY_METHOD'
function updateLocation(method) {
return (...args) => ({
type: CALL_HISTORY_METHOD, // route事件標識,避免和用於定義的action衝突
payload: { method, args } // method系hashHistroy、boswerHistroy對外接口方法名,args爲參數
})
}
/**
* 返回actionCreater,作爲外部push、replace、go、goBack、goForward方法的接口,通常不直接使用
*/
export const push = updateLocation('push')
export const replace = updateLocation('replace')
export const go = updateLocation('go')
export const goBack = updateLocation('goBack')
export const goForward = updateLocation('goForward')
export const routerActions = { push, replace, go, goBack, goForward }
完結
(此文由PPT摘抄完成)PPT鏈接