分析 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 中的必要性。當然對你來說也許不同,請留言指教指正。