分析 React Navigation:(不是教程)
Learn once, navigate anywhere.
React Native 官方推荐的一种路由模块,其本身主要包含三个部分:
The Navigation Prop
Router
View
The Navigation Prop
主要用于 Action 的分发,这部分在后面讨论。我们首先根据 Router
和 View
分析一下模块的内置导航器(Navigator)。
Router
Router
可以认为是 React Navigation 模块的 reducer , 具体的路由操作和响应是由她来完成的。开发人员通过对 Router
的订制来实现路由的特殊操作,如官网给出 阻止修改中模块的路由 实例。这里需要指出的是 Router
是组件的静态属性,当使用高价组件时,注意使用 hoist-non-react-statics 将静态属性和方法复制到高阶组件上,当然也可以使用 React Navigation 给出的 WithNavigation
方法。React Navigation 模块内置的 Router
分为:
StackRouter
TabRouter
View
View
则是 React Navigation 模块的展示组件,她通过 The Navigation Prop
和 Router
所提供的属性显示相关内容。React Navigation 内置的 View
分为:
CardStack
Tabs
Drawer
根据上述内置 Router
和 View
的排列组合,React Navigation
模块对外给出了三种导航器(Navigator)
-
StackNavigator
StackRouter
CardStack
-
TabNavigator
TabRouter
CardStack
Tabs
-
DrawerNavigator
StackRouter
Drawer
Navigation Props
有了 reducer,有了 展示组件,那么肯定也有触发状态改变的 Action 和 发送 Action 的方法。React Navigation 给出了五种 Actions:
Navigate
Reset
Back
Set Params
Init
与此对应的方法分别是:
navigate
setParams
goBack
但是上述方法都是辅助函数,是由 Navigation Props
中的 dispatch
和 state
属性生成的。 dispatch
??? Actions???看来 React Navigation 模块天生和 Redux 兼容,事实也确实如此,我们只需要将 Redux 中的 dispatch
和 state
的路由部分分别赋值给 Navigation Props
的 dispatch
和 state
,然后使用 React Navigation 给出的 addNavigationHelpers
就可以很方便的生成上述发送 Action 的方法,最后在 Redux 中定义路由的 reducer 就完成了路由状态和 Redux 结合。给出官方的实例:
const AppNavigator = StackNavigator(AppRouteConfigs)
// 此 reducer 与部分模块冲突,需要在以后修改
const navReducer = (state = initialState, action) => {
const nextState = AppNavigator.router.getStateForAction(action, state)
return nextState || state
}
// 根展示组件
class App extends React.Component {
render() {
return (
<AppNavigator navigation={addNavigationHelpers({
dispatch: this.props.dispatch,
state: this.props.nav,
})} />
)
}
}
const mapStateToProps = (state) => ({
nav: state.nav
})
// 控制组件
const AppWithNavigationState = connect(mapStateToProps)(App);
融合 React Navigation:
个人项目能不造轮子就尽量不造了(也没那水平)。主要使用的模块有:
- react native
- redux、react-redux、redux-immutable
- redux-saga
- redux-form
- immutable.js
- reselect
immutable
首先改造路由的 reducer 以适用 immutable:
const navReducer = (state = initialState, action) => {
const nextState = fromJS(AppStackNavigator.router.getStateForAction(action, state.toJS()))
return nextState || state
}
redux-form
随后在使用 redux-form 时,每次发送 back
路由 Action 时,都出现问题。查看发现每次销毁表单后,redux-form 又自动注册了表单,看来是谁又触发了 redux-form,最终发现是由于和路由 reducer 冲突,因为 Action 没有加限制,每次都会执行路由 reducer ,将其改为:
const initialNavState = AppStackNavigator.router.getStateForAction(
NavigationActions.init()
)
const navReducer = (state = fromJS(initialNavState), action) => {
if (
action.type === NavigationActions.NAVIGATE ||
action.type === NavigationActions.BACK ||
action.type === NavigationActions.RESET ||
action.type === NavigationActions.INIT ||
action.type === NavigationActions.SET_PARAMS ||
action.type === NavigationActions.URI
) {
console.log(action)
return fromJS(AppStackNavigator.router.getStateForAction(action, state.toJS()))
} else {
return state
}
}
export default navReducer
redux-saga
redux-saga 中使用 NavigationActions 结合以前的状态机思想,实现了将副作用状态包含路由状态都封装在 saga 中:
// 登录状态机
const machineState = {
currentState: 'login_screen',
states: {
login_screen: {
login: 'loading'
},
loading: {
success: 'main_screen',
failure: 'error'
},
main_screen: {
logout: 'login_screen',
failure: 'error'
},
error: {
login_retry: 'login_screen',
logout_retry: 'main_screen'
}
}
}
// 状态对应的 effects
function * clearError() {
yield delay(2000)
yield put({ type: REQUEST_ERROR, payload: '' })
}
function * mainScreenEffects() {
yield put({ type: SET_AUTH, payload: true })
yield put(NavigationActions.back())
yield put({ type: SET_LOADING, payload: { scope: 'login', loading: false } })
}
function * errorEffects(error) {
yield put({ type: REQUEST_ERROR, payload: error.message })
yield put({ type: SET_LOADING, payload: { scope: 'login', loading: false } })
yield fork(clearError)
}
function * loginEffects() {
yield put({ type: SET_AUTH, payload: false })
yield put(NavigationActions.reset({
index: 1,
actions: [
NavigationActions.navigate({ routeName: 'Main' }),
NavigationActions.navigate({ routeName: 'Login' })
]
})) // Redirect to the login page
}
const effects = {
loading: () =>
put({
type: SET_LOADING,
payload: { scope: 'login', loading: true }
}),
main_screen: () => mainScreenEffects(),
error: error => errorEffects(error),
login_screen: () => loginEffects()
}
// 有限状态自动机
const Machine = (state, effects) => {
let machineState = state
function transition(state, operation) {
const currentState = state.currentState
const nextState = state.states[currentState][operation]
? state.states[currentState][operation]
: currentState
return { ...state, currentState: nextState }
}
function operation(name) {
machineState = transition(machineState, name)
}
function getCurrentState() {
return machineState.currentState
}
const getEffect = name => (...arg) => {
operation(name)
return effects[machineState.currentState](...arg)
}
return { operation, getCurrentState, getEffect }
}
// 生成副作用对应的状态effects
const machine = Machine(machineState, effects)
const loginEffect = machine.getEffect('login')
const failureEffect = machine.getEffect('failure')
const successEffect = machine.getEffect('success')
const logoutEffect = machine.getEffect('logout')
//登录和登出流程
export function * loginFlow(): any {
while (true) {
const action: { type: string, payload: Immut } = yield take(LOGIN_REQUEST)
const username: string = action.payload.get('username')
const password: string = action.payload.get('password')
yield loginEffect()
try {
let isAuth: ?boolean = yield call(Api.login, { username, password })
if (isAuth) {
yield successEffect()
}
} catch (error) {
yield failureEffect(error)
machine.operation('login_retry')
}
}
}
export function * logoutFlow(): any {
while (true) {
yield take(LOGOUT_REQUEST)
try {
let isLogout: ?boolean = yield call(Api.logout)
if (isLogout) {
yield logoutEffect()
}
} catch (error) {
yield failureEffect(error)
machine.operation('logout_retry')
}
}
}
直到 redux-saga 中路由 Action 的使用,才让我感到路由结合进 redux 中的必要性。当然对你来说也许不同,请留言指教指正。