dva框架使用詳解及Demo教程

光強發表於2018-09-03

在前段時間,我們也學習講解過Redux框架的基本使用,但是有很多同學在交流群裡給我的反饋資訊說,redux框架理解上有難度,看了之後還是一臉懵逼不知道如何下手,很多同學就轉向選擇使用dva框架。其實dva框架就是一個redux框架與redux-saga等框架的一個集大成者,把幾個常用的資料處理框架進行了再次封裝,在使用方式上給使用者帶來了便利,下面我們就來簡單的介紹下dva框架的基本API和基本使用

Demo執行效果圖

這裡和講解Redux框架一樣,作者任然是提供了兩個經典的Demo示例,CounterApp 和 TodoList 來幫助初學者更好的理解和使用

http://ovyjkveav.bkt.clouddn.com/17-12-22/25369015.jpg

http://ovyjkveav.bkt.clouddn.com/17-12-22/35513550.jpg

Demo地址

  • CounterApp

github.com/guangqiang-…

  • TodoList

github.com/guangqiang-…

dva的由來

D.Va擁有一部強大的機甲,它具有兩臺全自動的近距離聚變機炮、可以使機甲飛躍敵人或障礙物的推進器、 還有可以抵禦來自正面的遠端攻擊的防禦矩陣。—— 來自 守望先鋒 。

dva 官方地址

github.com/dvajs/dva/b…

dva核心API

  • app = dva(opts)

建立應用,返回 dva 例項(注:dva 支援多例項)

opts 包含如下配置:

  1. history:指定給路由用的 history,預設是 hashHistory
  2. initialState:指定初始資料,優先順序高於 model 中的 state,預設是 {}

如果配置history 為 browserHistory,則建立dva物件可以寫成如下寫法

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
})
複製程式碼

另外,出於易用性的考慮,opts 裡也可以配所有的 hooks ,下面包含全部的可配屬性:

const app = dva({
  history,
  initialState,
  onError,
  onAction,
  onStateChange,
  onReducer,
  onEffect,
  onHmr,
  extraReducers,
  extraEnhancers,
})
複製程式碼
  • app.use(hooks)

配置 hooks 或者註冊外掛。(外掛最終返回的是 hooks )

比如註冊 dva-loading 外掛的例子:

import createLoading from 'dva-loading'
...
app.use(createLoading(opts))
複製程式碼

hooks包含如下配置項:

1、 onError((err, dispatch) => {})

effect 執行錯誤或 subscription 通過 done 主動拋錯時觸發,可用於管理全域性出錯狀態

注意:subscription 並沒有加 try...catch,所以有錯誤時需通過第二個引數 done 主動拋錯

例子:

app.model({
  subscriptions: {
    setup({ dispatch }, done) {
      done(e)
    },
  },
})
複製程式碼

如果我們使用antd元件,那麼最簡單的全域性錯誤處理通常會這麼做:

import { message } from 'antd'
const app = dva({
  onError(e) {
    message.error(e.message, 3)
  },
})
複製程式碼

2、 onAction(fn | fn[])

在action被dispatch時觸發,用於註冊 redux 中介軟體。支援函式或函式陣列格式

例如我們要通過 redux-logger 列印日誌:

import createLogger from 'redux-logger';
const app = dva({
  onAction: createLogger(opts),
})
複製程式碼

3、 onStateChange(fn)

state 改變時觸發,可用於同步 state 到 localStorage,伺服器端等

4、 onReducer(fn)

封裝 reducer 執行,比如藉助 redux-undo 實現 redo/undo :

import undoable from 'redux-undo';
const app = dva({
  onReducer: reducer => {
    return (state, action) => {
      const undoOpts = {};
      const newState = undoable(reducer, undoOpts)(state, action);
      // 由於 dva 同步了 routing 資料,所以需要把這部分還原
      return { ...newState, routing: newState.present.routing };
    },
  },
})
複製程式碼

5、 onEffect(fn)

封裝 effect 執行。比如 dva-loading 基於此實現了自動處理 loading 狀態

6、 onHmr(fn)

熱替換相關,目前用於 babel-plugin-dva-hmr

7、 extraReducers

指定額外的 reducer,比如 redux-form 需要指定額外的 form reducer:

import { reducer as formReducer } from 'redux-form'
const app = dva({
  extraReducers: {
    form: formReducer,
  },
})
複製程式碼
  • app.model(model)

註冊model,這個操作時dva中核心操作,下面單獨做詳解

  • app.unmodel(namespace)

取消 model 註冊,清理 reducers, effects 和 subscriptions。subscription 如果沒有返回 unlisten 函式,使用 app.unmodel 會給予警告⚠️

  • app.router(({ history, app }) => RouterConfig)

註冊路由表,這一操作步驟在dva中也很重要

// 註冊路由
app.router(require('./router'))
複製程式碼
// 路由檔案
import { Router, Route } from 'dva/router';
import IndexPage from './routes/IndexPage'
import TodoList from './routes/TodoList'

function RouterConfig({ history }) {
  return (
    <Router history={history}>
        <Route path="/" component={IndexPage} />
        <Route path='/todoList' components={TodoList}/>
    </Router>
  )
}
export default RouterConfig
複製程式碼

當然,如果我們想解決元件動態載入問題,我們的路由檔案也可以按照下面的寫法來寫

import { Router, Switch, Route } from 'dva/router'
import dynamic from 'dva/dynamic'

function RouterConfig({ history, app }) {
  const IndexPage = dynamic({
    app,
    component: () => import('./routes/IndexPage'),
  })

  const Users = dynamic({
    app,
    models: () => [import('./models/users')],
    component: () => import('./routes/Users'),
  })

  return (
    <Router history={history}>
      <Switch>
        <Route exact path="/" component={IndexPage} />
        <Route exact path="/users" component={Users} />
      </Switch>
    </Router>
  )
}

export default RouterConfig
複製程式碼

其中dynamic(opts) 中opt包含三個配置項:

  • opts

    • app: dva 例項,載入 models 時需要
    • models: 返回 Promise 陣列的函式,Promise 返回 dva model
    • component:返回 Promise 的函式,Promise 返回 React Component
  • app.start(selector?)

啟動應用,selector 可選,如果沒有 selector 引數,會返回一個返回 JSX 元素的函式

app.start('#root')
複製程式碼

那麼什麼時候不加 selector?常見場景有測試、node端、react-native 和 i18n 國際化支援

比如通過 react-intl 支援國際化的例子:

import { IntlProvider } from 'react-intl'
...
const App = app.start()
ReactDOM.render(<IntlProvider><App /></IntlProvider>, htmlElement)
複製程式碼

dva框架中的核心層:Model

下面是簡單常規的 model 檔案的寫法

/** Created by guangqiang on 2017/12/17. */

import queryString from 'query-string'
import * as todoService from '../services/todo'

export default {
  namespace: 'todo',
  state: {
    list: []
  },
  reducers: {
    save(state, { payload: { list } }) {
      return { ...state, list }
    }
  },
  effects: {
    *addTodo({ payload: value }, { call, put, select }) {
      // 模擬網路請求
      const data = yield call(todoService.query, value)
      console.log(data)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      const tempObj = {}
      tempObj.title = value
      tempObj.id = list.length
      tempObj.finished = false
      list.push(tempObj)
      yield put({ type: 'save', payload: { list }})
    },
    *toggle({ payload: index }, { call, put, select }) {
      // 模擬網路請求
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.finished = !obj.finished
      yield put({ type: 'save', payload: { list } })
    },
    *delete({ payload: index }, { call, put, select }) {
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      list.splice(index, 1)
      yield put({ type: 'save', payload: { list } })
    },
    *modify({ payload: { value, index } }, { call, put, select }) {
      const data = yield call(todoService.query, value)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.title = value
      yield put({ type: 'save', payload: { list } })
    }
  },
  subscriptions: {
    setup({ dispatch, history }) {
      // 監聽路由的變化,請求頁面資料
      return history.listen(({ pathname, search }) => {
        const query = queryString.parse(search)
        let list = []
        if (pathname === 'todoList') {
          dispatch({ type: 'save', payload: {list} })
        }
      })
    }
  }
}
複製程式碼

model物件中包含5個重要的屬性:

  • namespace

model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,不支援通過.的方式建立多層名稱空間

  • state

reducer的初始值,優先順序低於傳給dva()的 opts.initialState

例如:

const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
})
複製程式碼

此時,在 app.start() 後 state.count 為 1

  • reducers

以 key/value 格式定義reducer,用於處理同步操作,唯一可以修改 state 的地方,由 action 觸發

格式為 (state, action) => newState[(state, action) => newState, enhancer]

namespace: 'todo',
  state: {
    list: []
  },
  // reducers 寫法
  reducers: {
    save(state, { payload: { list } }) {
      return { ...state, list }
    }
  }
複製程式碼
  • effects

以 key/value 格式定義 effect。用於處理非同步操作和業務邏輯,不直接修改 state。由action 觸發,可以觸發action,可以和伺服器互動,可以獲取全域性 state 的資料等等

注意: dva框架中的effects 模組的設計思想來源於 redux-saga 框架,如果同學們對 redux-saga 框架不熟悉,可以檢視作者對 redux-saga的講解:www.jianshu.com/p/7cac18e8d…

格式為 *(action, effects) => void[*(action, effects) => void, { type }]

type 型別有有如下四種:

1、takeEvery

2、takeLatest

3、throttle

4、watcher

// effects 寫法
effects: {
    *addTodo({ payload: value }, { call, put, select }) {
      // 模擬網路請求
      const data = yield call(todoService.query, value)
      console.log(data)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      const tempObj = {}
      tempObj.title = value
      tempObj.id = list.length
      tempObj.finished = false
      list.push(tempObj)
      yield put({ type: 'save', payload: { list }})
    },
    *toggle({ payload: index }, { call, put, select }) {
      // 模擬網路請求
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.finished = !obj.finished
      yield put({ type: 'save', payload: { list } })
    },
    *delete({ payload: index }, { call, put, select }) {
      const data = yield call(todoService.query, index)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      list.splice(index, 1)
      yield put({ type: 'save', payload: { list } })
    },
    *modify({ payload: { value, index } }, { call, put, select }) {
      const data = yield call(todoService.query, value)
      let tempList = yield select(state => state.todo.list)
      let list = []
      list = list.concat(tempList)
      let obj = list[index]
      obj.title = value
      yield put({ type: 'save', payload: { list } })
    }
  }
複製程式碼
  • subscriptions

以 key/value 格式定義 subscription,subscription 是訂閱,用於訂閱一個資料來源,然後根據需要 dispatch 相應的 action

在 app.start() 時被執行,資料來源可以是當前的時間、伺服器的 websocket 連線、keyboard 輸入、geolocation 變化、history 路由變化等等

格式為 ({ dispatch, history }, done) => unlistenFunction

注意:如果要使用 app.unmodel(),subscription 必須返回 unlisten 方法,用於取消資料訂閱

// subscriptions 寫法
subscriptions: {
    setup({ dispatch, history }) {
      // 監聽路由的變化,請求頁面資料
      return history.listen(({ pathname, search }) => {
        const query = queryString.parse(search)
        let list = []
        if (pathname === 'todoList') {
          dispatch({ type: 'save', payload: {list} })
        }
      })
    }
  }
複製程式碼

使用dva框架和直接使用redux寫法的區別

  • 使用 redux

actions.js 檔案

export const REQUEST_TODO = 'REQUEST_TODO';
export const RESPONSE_TODO = 'RESPONSE_TODO';

const request = count => ({type: REQUEST_TODO, payload: {loading: true, count}});

const response = count => ({type: RESPONSE_TODO, payload: {loading: false, count}});

export const fetch = count => {
  return (dispatch) => {
    dispatch(request(count));

    return new Promise(resolve => {
      setTimeout(() => {
        resolve(count + 1);
      }, 1000)
    }).then(data => {
      dispatch(response(data))
    })
  }
}
複製程式碼

reducer.js 檔案

import { REQUEST_TODO, RESPONSE_TODO } from './actions';

export default (state = {
  loading: false,
  count: 0
}, action) => {
  switch (action.type) {
    case REQUEST_TODO:
      return {...state, ...action.payload};
    case RESPONSE_TODO:
      return {...state, ...action.payload};
    default:
      return state;
  }
}
複製程式碼

app.js 檔案

import React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';

import * as actions from './actions';

const App = ({fetch, count, loading}) => {
  return (
    <div>
      {loading ? <div>loading...</div> : <div>{count}</div>}
      <button onClick={() => fetch(count)}>add</button>
    </div>
  )
}

function mapStateToProps(state) {
  return state;
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(actions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(App)
複製程式碼

index.js 檔案

import { render } from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux'
import thunkMiddleware from 'redux-thunk';

import reducer from './app/reducer';
import App from './app/app';

const store = createStore(reducer, applyMiddleware(thunkMiddleware));

render(
  <Provider store={store}>
    <App/>
  </Provider>
  ,
  document.getElementById('app')
)
複製程式碼
  • 使用dva

model.js 檔案

export default {
  namespace: 'demo',
  state: {
    loading: false,
    count: 0
  },
  reducers: {
    request(state, payload) {
      return {...state, ...payload};
    },
    response(state, payload) {
      return {...state, ...payload};
    }
  },
  effects: {
    *'fetch'(action, {put, call}) {
      yield put({type: 'request', loading: true});

      let count = yield call((count) => {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(count + 1);
          }, 1000);
        });
      }, action.count);

      yield put({
        type: 'response',
        loading: false,
        count
      });
    }
  }
}
複製程式碼

app.js 檔案

import React from 'react'
import { connect } from 'dva';

const App = ({fetch, count, loading}) => {
  return (
    <div>
      {loading ? <div>loading...</div> : <div>{count}</div>}
      <button onClick={() => fetch(count)}>add</button>
    </div>
  )
}

function mapStateToProps(state) {
  return state.demo;
}

function mapDispatchToProps(dispatch) {
  return {
    fetch(count){
      dispatch({type: 'demo/fetch', count});
    }
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(App)
複製程式碼

index.js 檔案

import dva from 'dva';
import model from './model';
import App from './app';

const app = dva();

app.use({});

app.model(model);

app.router(() => <App />);

app.start();
複製程式碼

我們通過上面兩種不同方式來實現一個非同步的計數器的程式碼結構發現:

  1. 使用 redux 需要拆分出action模組和reducer模組

  2. dva將actionreducer封裝到model中,非同步流程採用Generator處理

總結

本篇文章主要講解了dva框架中開發常用API和一些使用技巧,如果想檢視更多更全面的API,請參照dva官方文件:github.com/dvajs/dva

如果同學們看完教程還是不知道如何使用dva框架,建議執行作者提供的Demo示例結合學習

作者建議:同學們可以將作者之前講解的redux框架和dva框架對比來學習理解,這樣更清楚他們之間的區別和聯絡。

福利時間

  • 作者React Native開源專案OneM地址(按照企業開發標準搭建框架完成開發的):github.com/guangqiang-…:歡迎小夥伴們 star
  • 作者簡書主頁:包含60多篇RN開發相關的技術文章www.jianshu.com/u/023338566… 歡迎小夥伴們:多多關注多多點贊
  • 作者React Native QQ技術交流群:620792950 歡迎小夥伴進群交流學習
  • 友情提示:在開發中有遇到RN相關的技術問題,歡迎小夥伴加入交流群(620792950),在群裡提問、互相交流學習。交流群也定期更新最新的RN學習資料給大家,謝謝大家支援!

相關文章