dva框架使用詳解及Demo教程

dva框架的使用詳解及Demo教程

在前段時間,咱們也學習講解過Redux框架的基本使用,可是有不少同窗在交流羣裏給個人反饋信息說,redux框架理解上有難度,看了以後仍是一臉懵逼不知道如何下手,不少同窗就轉向選擇使用dva框架。其實dva框架就是一個redux框架與redux-saga等框架的一個集大成者,把幾個經常使用的數據處理框架進行了再次封裝,在使用方式上給使用者帶來了便利,下面咱們就來簡單的介紹下dva框架的基本API和基本使用html

Demo運行效果圖

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

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

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

Demo地址

  • CounterApp

https://github.com/guangqiang-liu/react-dva-counterreact

  • TodoList

https://github.com/guangqiang-liu/react-dva-todoListgit

dva的由來

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

dva 官方地址

https://github.com/dvajs/dva/blob/master/README_zh-CN.mdgithub

dva核心API

  • app = dva(opts)

建立應用,返回 dva 實例(注:dva 支持多實例)web

opts 包含以下配置:redux

  1. history:指定給路由用的 history,默認是 hashHistory
  2. initialState:指定初始數據,優先級高於 model 中的 state,默認是 {}

若是配置history 爲 browserHistory,則建立dva對象能夠寫成以下寫法react-native

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包含以下配置項:

一、 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)
  },
})

二、 onAction(fn | fn[])

在action被dispatch時觸發,用於註冊 redux 中間件。支持函數或函數數組格式

例如咱們要經過 redux-logger 打印日誌:

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

三、 onStateChange(fn)

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

四、 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 };
    },
  },
})

五、 onEffect(fn)

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

六、 onHmr(fn)

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

七、 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的講解:https://www.jianshu.com/p/7cac18e8d870

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

type 類型有有以下四種:

一、takeEvery

二、takeLatest

三、throttle

四、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將action reducer 封裝到model 中,異步流程採用Generator處理

總結

本篇文章主要講解了dva框架中開發經常使用API和一些使用技巧,若是想查看更多更全面的API,請參照dva官方文檔:https://github.com/dvajs/dva

若是同窗們看完教程仍是不知道如何使用dva框架,建議運行做者提供的Demo示例結合學習

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

福利時間

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