在前段時間,咱們也學習講解過Redux框架的基本使用,可是有不少同窗在交流羣裏給個人反饋信息說,redux框架理解上有難度,看了以後仍是一臉懵逼不知道如何下手,不少同窗就轉向選擇使用dva框架。其實dva框架就是一個redux框架與redux-saga等框架的一個集大成者,把幾個經常使用的數據處理框架進行了再次封裝,在使用方式上給使用者帶來了便利,下面咱們就來簡單的介紹下dva框架的基本API和基本使用html
這裏和講解Redux框架同樣,做者任然是提供了兩個經典的Demo示例,CounterApp 和 TodoList 來幫助初學者更好的理解和使用node
D.Va擁有一部強大的機甲,它具備兩臺全自動的近距離聚變機炮、可使機甲飛躍敵人或障礙物的推動器、 還有能夠抵禦來自正面的遠程攻擊的防護矩陣。—— 來自 守望先鋒 。github
建立應用,返回 dva 實例(注:dva 支持多實例)redux
opts
包含以下配置:react-native
history
:指定給路由用的 history,默認是 hashHistoryinitialState
:指定初始數據,優先級高於 model 中的 state,默認是 {}若是配置history
爲 browserHistory,則建立dva對象能夠寫成以下寫法數組
import createHistory from 'history/createBrowserHistory';
const app = dva({
history: createHistory(),
})
複製代碼
另外,出於易用性的考慮,opts 裏也能夠配全部的 hooks ,下面包含所有的可配屬性:bash
const app = dva({
history,
initialState,
onError,
onAction,
onStateChange,
onReducer,
onEffect,
onHmr,
extraReducers,
extraEnhancers,
})
複製代碼
配置 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,
},
})
複製代碼
註冊model,這個操做時dva中核心操做,下面單獨作詳解
取消 model 註冊,清理 reducers, effects 和 subscriptions。subscription 若是沒有返回 unlisten 函數,使用 app.unmodel 會給予警告⚠️
註冊路由表,這一操做步驟在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.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)
複製代碼
下面是簡單常規的 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個重要的屬性:
model 的命名空間,同時也是他在全局 state 上的屬性,只能用字符串,不支持經過.
的方式建立多層命名空間
reducer的初始值,優先級低於傳給dva()的 opts.initialState
例如:
const app = dva({
initialState: { count: 1 },
});
app.model({
namespace: 'count',
state: 0,
})
複製代碼
此時,在 app.start()
後 state.count 爲 1
以 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 }
}
}
複製代碼
以 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 類型有有以下四種:
一、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 } })
}
}
複製代碼
以 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} })
}
})
}
}
複製代碼
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')
)
複製代碼
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();
複製代碼
咱們經過上面兩種不一樣方式來實現一個異步的計數器的代碼結構發現:
使用 redux 須要拆分出action
模塊和reducer
模塊
dva將action
和reducer
封裝到model
中,異步流程採用Generator處理
本篇文章主要講解了dva框架中開發經常使用API和一些使用技巧,若是想查看更多更全面的API,請參照dva官方文檔:github.com/dvajs/dva
若是同窗們看完教程仍是不知道如何使用dva框架,建議運行做者提供的Demo示例結合學習
做者建議:同窗們能夠將做者以前講解的redux框架和dva框架對比來學習理解,這樣更清楚他們之間的區別和聯繫。