Redux and Router

13次阅读

共计 7738 个字符,预计需要花费 20 分钟才能阅读完成。

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 链接

正文完
 0