React Navigation 的個人分析與融合

szouc發表於2019-03-03

分析 React Navigation:(不是教程)

Learn once, navigate anywhere.

React Native 官方推薦的一種路由模組,其本身主要包含三個部分:

  • The Navigation Prop
  • Router
  • View

The Navigation Prop 主要用於 Action 的分發,這部分在後面討論。我們首先根據 RouterView 分析一下模組的內建導航器(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 PropRouter 所提供的屬性顯示相關內容。React Navigation 內建的 View 分為:

  • CardStack
  • Tabs
  • Drawer

根據上述內建 RouterView 的排列組合,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
中的 dispatchstate 屬性生成的。 dispatch ??? Actions???看來 React Navigation 模組天生和 Redux 相容,事實也確實如此,我們只需要將 Redux 中的 dispatchstate 的路由部分分別賦值給 Navigation Propsdispatchstate,然後使用 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 中的必要性。當然對你來說也許不同,請留言指教指正。

相關文章