dva之React Naitve中的戰鬥攻略

weixin_34391445發表於2017-06-22

前言

從最早接觸react native也快接近一年了,不多不少的也做了有3個專案了,但是技術好像沒有什麼提升誒(???),其中很有感觸的是在開發一個收入的專案的時候,做下來發現檔案太多了,不好管理,根據問題檢視程式碼很是膈應。不過還好的是最近接觸到了一個叫dva的前端框架(聽說支付寶前端團隊開發的框架),dva是出自於守望先鋒遊戲的一個角色 => D.Va擁有一部強大的機甲,裝備了各種高科技武器。同樣dva框架呢是對redux+saga這種方式管理資料流的整合封裝,目的很簡單,讓使用者更簡單的,更方便的管理資料流。

dva的作用

從程式碼結構管理層面

以前的專案就是使用原生的redux管理的,當然還有處理非同步操作的saga,所以針對一個業務點,程式碼會分佈在很多檔案中。

1530185-1424bb4e1fb622d0.png
image.png

下圖,則是通過dva來管理的react native專案,action,reducer,saga都放在model模組,相對簡潔很多。

1530185-c7d2b85447ce44c2.png
image.png

其中model的編寫是dva的核心。

從程式碼編寫繁瑣程度

redux store 的建立,actionCreater的建立,中介軟體的配置,路由的初始化,Provider 的 store 的繫結,saga 的初始化,還要處理 reducer, component。
基於上面的這些問題,封裝了 dva 。dva 是基於 redux 最佳實踐 實現的 framework。

dva接入的課前輔導

. Redux 文件
. Redux-sage 文件

簡單畫下我對Redux,Redux-sage 整個流程的理解。

  • Redux


    1530185-ef80a5f53a3630da.png
    image.png
  • Redux-Saga


    1530185-07ec121024f6b40f.png
    image.png

react native+dva+react-navigation 的一個demo

專案結構如下圖

1530185-49baa6fa34407efa.png
image.png

接下來我們就從零搭建,一定要動手去敲哦!!!?

通過dva初始化根頁面

react native 的預設初始化方式, 第二個引數是Component型別

AppRegistry.registerComponent('XXXApp', () => XXXAppComponent)

but,right now

index.ios.js

import {
  AppRegistry
} from 'react-native'
import app from './src'

AppRegistry.registerComponent('ReduxTest', app)

這個app是個什麼東東呢,先賣個關子?

app.js

import React from 'react'
import dva, { connect } from 'dva/mobile'
import { registerModels } from './models'
import Router from './routes'

// 1. Initialize
const app = dva()

// 2. Model
registerModels(app)

// 3. Router
app.router(() => <Router />)

// 4. Start
export default () => {
  return app.start()
}

其中步驟2中的註冊model,可以先不用care,重點放在後兩個步驟,Router是個什麼東西呢,你可以簡單的理解為RootComponent,我們一般開發react native的RootComponent即為TabNavigator,StackNavigator,該demo以StackNavigator為根頁面,所以呢,我們就簡單的將其匯出為Router,然後註冊到dva中,app.start() 將會啟動應用,並返回一個Component。這也很好的解釋了AppRegistry中註冊app.start()返回的Component。

Router ???

router.js

import {
  StackNavigator,
  addNavigationHelpers
} from 'react-navigation'
import React, { Component } from 'react'
import { BackHandler, Animated, Easing } from 'react-native'
import { connect } from 'dva'
import Login from  '../pages/Login'
import Profile from  '../pages/Profile'

const AppNavigator = StackNavigator(
  {
    Login: {screen: Login},
    Profile: {screen: Profile}
  },
  {
    navigationOptions: {
      gesturesEnabled: true,
    },
  }
)
@connect(({ router }) => ({ router }))
export default class Router extends Component {
  render() {
    const { dispatch, router } = this.props
    const navigation = addNavigationHelpers({ dispatch, state: router })
    return <AppNavigator navigation={navigation} />
  }
}

export function routerReducer(state, action = {}) {
  return AppNavigator.router.getStateForAction(action, state)
}

Router主要是簡單定義了下StackNavigator中的存放的Component,預設第一個為RootComponent 即Login。需要解釋一下的是@connect(({ router }) => ({ router }))這是es7的語法,有興趣可以google下,這裡我只把router資料給傳進來,addNavigationHelpers({ dispatch, state: router })是將會在執行navigation.goBack(),navigation.navigate()的同時執行對應的dispatch,更新router資料。
export function routerReducer這個外部介面, 提供外部獲取路由資訊。

Login.js

import React, { Component } from 'react';
import {
  AppRegistry,
  StyleSheet,
  TouchableOpacity,
  Text,
  View
} from 'react-native'
import { connect } from 'dva'
import {
  NavigationActions
} from 'react-navigation'

@connect(
  appNS => ({ ...appNS }),
  {
    increase: () => (({ type: 'appNS/add' })),
    login: () => (({ type: 'appNS/login' }))
  }
)
export default class Login extends Component {

  static navigationOptions = {
    title: '登入頁',
  }

  goLogin() {
    this.props.login()
  }

  render() {
    return (
      <View style={styles.container}>
        <TouchableOpacity style={styles.loginButton} onPress={() => this.goLogin()}>
          <Text style={styles.loginLabel}>登入</Text>
        </TouchableOpacity>
      </View>
    );
  }
}

like this

1530185-5d4b16cdd35463bf.png
image.png

其中點選登入按鈕會觸發dispatch({ type: 'appNS/login' })。接下來我們就來看看重中之中針對這個頁面的model編寫。

models/app.js

import { NavigationActions } from '../tools'
import { createAction } from '../tools'
import { get, post } from '../tools/fetch'

export default {
  namespace: 'appNS',
  state: {
    isLogin: false,
    userName: '路人甲',
    loginFailedReason: 'no reason',
    count: 0
  },
  reducers: {
    add(state, { payload }) {
      return {
        ...state,
        count: (state.count + 1)
      }
    },
    loginSuccessed(state, { payload }) {
      return {
        ...state,
        isLogin: true,
        userName: payload.userName
      }
    },
    loginFailed(state, { payload }) {
      return {
        ...state,
        isLogin: false,
        loginFailedReason: payload.loginFailedReason
      }
    }
  },
  effects: {
    *login(payload, { put, call }) {
      // yield put({ type: 'loginSuccessed', {'name': 'yellow'} })
      // yield put(createAction('loginSuccessed')({'name': 'yellow'}))
      try {
        const res = yield call(() => get('https://httpbin.org/get'))
        if (res.url) {
          yield put(createAction('loginSuccessed')({'userName': 'yellow'}))
          yield put(NavigationActions.navigate({ routeName: 'Profile'}))
        } else {
          yield put(createAction('loginFailed')({'loginFailedReason': '賬號密碼錯誤'}))
        }
      } catch (e) {
        console.log(e);
      }
    }
  }
}

首先,簡單說明下model 就是一個大的json物件,其中有幾個重要的key。namespace,當你connect一個Component就是通過這個值來連線的,以及跨model呼叫action,ex:put({type:'namespace/xxaction'})state放置一些初始化或是需要維護的資料。reducers裡就放一些action對應的純函式,修改state資料來源。effects存放一些網路請求,I/O操作的有副作用的方法,其中會呼叫reducer的方法,從而改變資料來源。

幫助大家理一下流程
page/this.props.login() ——》effects/*login ——》success? reducer/loginSuccessedAction——》state/isLogin: true

effects中有兩個比較常用的輔助函式put,call,put函式呼叫一個action,call用於呼叫非同步邏輯,支援 promise

如何在effects中進行頁面的跳轉呢?

以前我遇到這個問題也很頭疼,就用了一個很暴力的方法,用global全域性物件來儲存Navigator,然後來進行操作。但是react-navigation這個第三方元件,既支援UI層面的頁面切換,也支援對redux的接入(路由資訊的獲取和修改,修改也會影響到UI)

models/router.js

import { createAction, NavigationActions } from '../tools'
import { routerReducer } from '../routes'

const watcher = { type: 'watcher' }

const actions = [
  NavigationActions.BACK,
  NavigationActions.INIT,
  NavigationActions.NAVIGATE,
  NavigationActions.RESET,
  NavigationActions.SET_PARAMS,
  NavigationActions.URI,
]

export default {
  namespace: 'router',
  state: {
    ...routerReducer(),
  },
  reducers: {
    apply(state, { payload: action }) {
      return routerReducer(state, action)
    },
  },
  effects: {
    watch: [
      function*({ take, call, put }) {
        while (true) {
          const payload = yield take(actions)
          yield put(createAction('apply')(payload))
          if (payload.type === 'Navigation/NAVIGATE') {
            console.log('11111',payload);
          }
        }
      }, watcher]
  },
}

其實就做了兩件事,通過之前的Router元件中提供的獲取路由資訊初始化到state中,effects中監聽NavigationActions,然後呼叫apply來更新路由資訊,最後又因為我們將路由資訊連結到Router元件,所以就會有UI頁面的切換。

1530185-e5091801aa1b0003.gif
22.gif

完整demo

二維碼地址


1530185-1801c3f8e05c2a43.png
image.png

總結來說,dva雖然遮蔽了redux和saga的一些細節,但你要真正運用到專案中,還是需要惡補下這方面的知識,前端框架變化莫測,如何擁有一個自學的方法是很關鍵的,以及學習的及時反饋,對於新人來說一劑強力的助推器。

最後上一張我家貓咪生的小寶寶 嘻嘻

1530185-48f6229ff946ecb1.png
image.png

相關文章