dva介紹
dva 首先是一個基於 redux 和 redux-saga 的數據流方案,然后為了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,所以也可以理解為一個輕量級的應用框架。
數據流向
數據的改變發生通常是通過用戶交互行為或者瀏覽器行為(如路由跳轉等)觸發的,當此類行為會改變數據的時候可以通過 dispatch 發起一個 action,如果是同步行為會直接通過 Reducers 改變 State ,如果是異步行為(副作用)會先觸發 Effects 然后流向 Reducers 最終改變 State,所以在 dva 中,數據流向非常清晰簡明,並且思路基本跟開源社區保持一致(也是來自於開源社區)
如圖:

疑問
剛開始學react代碼的時候總會想,這種全局數據流有什么用,直接ajax請求響應,操作頁面元素,一步走到頭,清晰明了,這些數據流並沒有什么卵用
react數據流
- 1.react頁面渲染都是通過修改state進行頁面渲染的,而state只能控制當前頁面
- 2.使用react代碼開發,要拋棄之前ajax的理念(通過數據以及操作dom進行頁面調整),使用state、props數據流進行頁面開發,如果直接操作dom,不更新state在沒玩明白數據流的情況下可能會出問題,需要時間去積累經驗
- 3.這時候當前組件想要操作別的組件的state尤為困難,這時候就需要全局數據流方案redux
什么是redux
Redux 是 JavaScript狀態容器,提供可預測化的狀態管理,可以讓你構建一致化的應用,運行於不同的環境(客戶端、服務器、原生應用),並且易於測試
為什么用Redux

因為對於react來說,同級組件之間的通信尤為麻煩,或者是非常麻煩了,所以我們把所有需要多個組件使用的state拿出來,整合到頂部容器,進行分發
redux實現了什么
在react的數據交互理念中,只能進行父子組件的通信,無法想和誰通信就和誰通信,redux做到了將數據傳遞添加到了全局,任何地方都可以進行接收使用。
將需要修改的state都存入到store里,發起一個action用來描述發生了什么,用reducers描述action如何改變state樹。創建store的時候需要傳入reducer,真正能改變store中數據的是store.dispatch API

dva配置項
const app = dva({
history, // 指定給路由用的 history,默認是 hashHistory
initialState, // 指定初始數據,優先級高於 model 中的 state
onError, // effect 執行錯誤或 subscription 通過 done 主動拋錯時觸發,可用於管理全局出錯狀態。
onAction, // 在 action 被 dispatch 時觸發
onStateChange, // state 改變時觸發,可用於同步 state 到 localStorage,服務器端等
onReducer, // 封裝 reducer 執行。比如借助 redux-undo 實現 redo/undo
onEffect, // 封裝 effect
onHmr, // 熱替換相關
extraReducers, // 指定額外的 reducer,比如 redux-form 需要指定額外的 form reducer
extraEnhancers, // 指定額外的 StoreEnhancer ,比如結合 redux-persist 的使用
});
Models
如果對於Redux的action,reducer寫法很熟悉的同學一定意識到,以往(即使是Redux的官方例子中)會在src目錄下,新建reducers以及actions目錄用於整體存放reducer和action,針對不同的功能模塊還需要分別在這兩個目錄下設置子目錄或子文件區分。
但dva的model完美的讓某一個模塊的狀態池(對應某一個模塊),action和reducer同時在一個js文件中維護,這樣整個應用的目錄結構會清晰和簡潔很多,但也因此在dva項目中發起的任何一個action,type中必須包含對應model的命名空間(namespace)
import * as addbanner from '../services/addbanner';
export default {
namespace: 'addbanner', // 命名空間
state: {},
subscriptions: { // 監聽
setup({ history, dispatch }, onError) {
}
},
effects: {
//call:執行異步函數
//put:發出一個 Action,更新store,類似於 dispatch
*bannerlist(action, { call, put }) {
const testRes = yield call(addbanner.bannerlist, action.params);
yield put({
type: 'test',
payload: {
bannerlistRes: testRes
},
});
return testRes;
},
reducers: { // 接收數據並改變state,然后進行返回,命名要和effects里的函數type一致
test(state, { payload }) {
return {
...state,
...payload,
};
}
},
};
State
State 表示 Model 的狀態數據,通常表現為一個 javascript 對象(當然它可以是任何值);操作的時候每次都要當作不可變數據(immutable data)來對待,保證每次都是全新對象,沒有引用關系,這樣才能保證 State 的獨立性,便於測試和追蹤變化。
Action
Action 是一個普通 javascript 對象,它是改變 State 的唯一途徑。無論是從 UI 事件、網絡回調,還是 WebSocket 等數據源所獲得的數據,最終都會通過 dispatch 函數調用一個 action,從而改變對應的數據。action 必須帶有 type 屬性指明具體的行為,其它字段可以自定義,如果要發起一個 action 需要使用 dispatch 函數;需要注意的是 dispatch 是在組件 connect Models以后,通過 props 傳入的。
dispatch 函數
dispatching function 是一個用於觸發 action 的函數,action 是改變 State 的唯一途徑,但是它只描述了一個行為,而 dipatch 可以看作是觸發這個行為的方式,而 Reducer 則是描述如何改變數據的。
在 dva 中,connect Model 的組件通過 props 可以訪問到 dispatch,可以調用 Model 中的 Reducer 或者 Effects,常見的形式如
dispatch({
type: 'user/add', // 格式:namespace(models的命名空間)/function(effects的方法名)
payload: {}, // 需要傳遞的信息
});
Reducer
Reducer(也稱為 reducing function)函數接受兩個參數:之前已經累積運算的結果和當前要被累積的值,返回的是一個新的累積結果。該函數把一個集合歸並成一個單值。
Effect
Effect 被稱為副作用,在我們的應用中,最常見的就是異步操作。它來自於函數編程的概念,之所以叫副作用是因為它使得我們的函數變得不純,同樣的輸入不一定獲得同樣的輸出。
Subscription
Subscription 語義是訂閱,用於訂閱一個數據源,然后根據條件 dispatch 需要的 action。數據源可以是當前的時間、服務器的 websocket 連接、keyboard 輸入、geolocation 變化、history 路由變化等等。
// 比如:當用戶進入 /users 頁面時,觸發action users/fetch 加載用戶數據。
subscriptions: {
setup({ dispatch, history }) {
history.listen(({ pathname }) => {
debugger
if (pathname === '/primary') {
dispatch({
type: 'primary/getmerProClassList',
});
}
})
}
}
| dva | redux |
|---|---|
| 引用dva | 要引入多個庫,項目結構復雜 |
| 實現一個異步交互修改的文件很少 | 實現一個異步交互需要修改的文件太多,容易出錯 |
| 使用方式清晰、便捷 | 使用方式復雜、繁瑣 |
Router
這里的路由通常指的是前端路由,由於我們的應用現在通常是單頁應用,所以需要前端代碼來控制路由邏輯,通過瀏覽器提供的 History API 可以監聽瀏覽器url的變化,從而控制路由相關操作。
import { Router, Route } from 'dva/router'; // 引用路由
import dynamic from 'dva/dynamic'; // 路由按需加載
const RouterWrapper = ({ history, app }) => {
const Primary = dynamic({
app,
models: () => [
import('./models/primary'), // 使用對應的models
],
component: () => import('./components/Primary')
});
return (
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
);
});
dva的使用
安裝
npm install dva
- 項目index.js
import 'babel-polyfill';
import dva from 'dva';
import createLoading from 'dva-loading';
import createHistory from 'history/createBrowserHistory';
// import createHistory from 'history/createHashHistory';
import './assets/common.less';
if (module && module.hot) {
module.hot.accept()
}
import { default as miniProgramRemain } from './models/miniProgramRemain';
// =======================
// 1. Initialize
// =======================
const app = dva({
history: createHistory(),
onError(e, dispatch) {
},
});
app.model(miniProgramRemain);
// =======================
// 2. Plugins
// =======================
app.use(createLoading());
// =======================
// 3. Model
// =======================
// Moved to router.js
// =======================
// 4. Router
// =======================
app.router(require('./Router'));
// =======================
// 5. Start
// =======================
app.start('#app');
- 1.創建頁面組件,文件以及組件命名大駝峰,開發相應的邏輯以及UI界面
- 2.添加路由,按項目規則命名(eg:列表頁面:/list,添加列表頁面:/list/listAdd)
- 3.定義model
- 4.定義service以及api地址
- 5.連接model和組件
1.創建頁面組件Primary.js
里面開發對應的頁面邏輯以及UI
import React, { Component } from 'react'
export default class Primary extends Component {
render() {
return (
<div>
Primary
</div>
)
}
}
2.添加路由
注意:需要在福祿管家將對應菜單添加到自己的商戶應用下,否則系統會進行控制,也是無法打開
const Primary = dynamic({
app,
models: () => [
import('./models/primary'),
],
component: () => import('./components/Primary')
});
// 使用
<Route exact path="/primary" render={(props) => WraperRouter(props, Primary)} />
3.創建model文件primary.js
import * as primary from '../services/primary';
export default {
namespace: 'primary',
state: {},
effects: {
*saveOneProClass({ payload, callback }, { call, put }) { // 保存
const testRes = yield call(primary.saveOneProClass, payload);
yield put({
type: 'success',
payload: {
saveOneProClassResult: testRes
}
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
*deleteProClass({ payload, callback }, { call, put }) { // 刪除
const testRes = yield call(primary.deleteProClass, payload);
yield put({
type: 'success',
payload: {
deleteProClassResult: testRes
}
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
*getmerProClassList({ payload, callback }, { call, put }) { // 查詢
const testRes = yield call(primary.getmerProClassList, payload);
yield put({
type: 'test',
payload: {
getmerProClassListRes: testRes
},
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
},
reducers: {
success (state, {payload}) {
return {
...state,
...payload,
};
}
}
}
4.創建service文件primary.js
請求的地址都從service.js進行傳遞
// 引入axios組件
import axios from '../utils/axios';
// 引用項目api地址
import Api from '../configs/api';
// get請求
export function getmerProClassList(params) {
return axios.get(configs.host.test + Api.getmerProClassList, { 'params': params });
}
// post請求
export function saveOneProClass(params) {
return axios.post(configs.host.test + Api.saveOneProClass,params);
}
export function deleteProClass(params) {
return axios.get(configs.host.test + Api.deleteProClass, { 'params': params });
}
// put請求
// export function modifyMembershipInfo(params) {
// return axios.put(configs.host.test + // Api.modifyMembershipInfo,params);
// }
// delete請求
// export function guessYouLikeDelete(params) {
// return axios.delete(configs.host.test + Api.guessYouLike, { // 'params': params })
// }
axios.js
// axios.js用於處理數據請求響應(添加請求header,返回數據異常捕獲等)
import axios from 'axios';
import { message } from 'antd';
import { handleErrCallBack } from 'fl-pro';
// axios.defaults.baseURL = ''; API 域。默認值:當前域
axios.defaults.withCredentials = true; // 允許跨域且攜帶 Cookie(或自定義頭)。默認值:false
axios.defaults.timeout = 30000; // 設置請求超時時間(ms)不超過半分鍾
axios.defaults.headers.common['Authorization'] = ''; // 攜帶的自定義頭
axios.defaults.headers.post['Content-Type'] = 'application/json'; // 設置請求提內容類型,其他可選值:application/x-www-form-urlencoded
axios.interceptors.request.use(config => {
// console.log('【request】', config);
config.headers["Authorization"] = `Bearer ${localStorage.getItem('access_token')}`;
config.headers["MerchantId"] = localStorage.getItem('MerchantId');
return config;
}, error => {
// console.log('【request error】', error);
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
return response.data;
},handleErrCallBack
);
export default axios;
5.api.js添加
(查看后端服務地址進行配置)
getmerProClassList: '/api/product/GetMerProClassList',// 獲取商戶商品分類 //商品一級分類
saveOneProClass: '/api/product/SaveOneProClass', //添加編輯一級分類保存接口
deleteProClass: '/api/product/DeleteProClass', //刪除分類
6.連接model和組件
// 方式一:直接在引用的地方連接
const Primary = dynamic({
app,
models: () => [
import('./models/primary'),
],
component: () => import('./components/Primary')
});
// 方式二:單頁應用入口文件index.js添加引用
import { default as primary } from './models/primary';
app.model(primary);
// 區別: 在入口文件注冊的model文件,可以在項目任何一個路由頁面進行使用,而在單個路由注冊的model文件只能在該頁面使用,別的頁面無法使用你的model
- 流程圖

列表增刪查改

開發流程
1.頁面組件的定義
- 頁面的基礎搭建
2.state定義
- 定義查詢的postData
- 定義搜索的配置項searchConfig(使用SearchForm組件庫)
- 定義selectedRowKeys(用於批量刪除)
- 定義彈窗的開發關閉showModal
3.事件的定義
- getData:異步請求查詢函數
- search:查詢獲取表單數據更新postData的函數並調用查詢函數
- batchDeleteInfo:批量刪除獲取點擊的選中行並調用刪除函數
- deleteInfo:異步請求刪除函數
4.render的定義
- 獲取state
- 設置table的列配置、分頁配置、多選
- html加載面包屑、查詢組件、新增按鈕、批量刪除按鈕、表格、彈窗子組件
5.頁面導出並連接全局數據流
流程如圖:

交互步驟
查詢交互:點擊查詢獲取查詢數據->調用search查詢組裝數據->調用getData公用函數->獲取返回數據->修改state,渲染頁面
新增編輯交互:點擊父組件操作功能,調用彈窗->表單數據完成,並按后端要求組裝好數據,提交新增、編輯->獲取返回數據->調用查詢,關閉彈窗
刪除交互:點擊刪除->確認刪除交互->獲取返回數據->調用查詢
核心代碼查看
- 查詢
// 查詢公用函數
getData = () => {
const { postData } = this.state;
this.props.dispatch({ type: 'primary/getmerProClassList', payload: { ...postData } }).then((res) => {
const { code, data } = res;
if (code === '0') {
return this.setState({
tableData: data.list,
total: data.total
});
}
message.error(res.message);
});
}
// 處理表單values並調用,查詢
search = (err, value) => {
//查詢列表
const { postData } = this.state;
postData.pageIndex = 1;
this.setState({
postData: { ...postData, ...value }
}, () => {
this.getData();
})
}
this.state = {
postData: {
pageIndex: 1,
pageSize: 10,
},
};
- 新增、編輯
// 子組件彈窗,新增編輯一級分類代碼
sureAdd = () => {
const { uploadFile } = this.state;
this.props.form.validateFields((err, values) => {
if (!err) {
const { iconPath, iconPathEdit, detailInfo } = this.state;
let nowImgUrl = '';
if (detailInfo.id) {
values.id = detailInfo.id;
// 如果用戶上傳了或者刪除了
if (iconPathEdit) {
nowImgUrl = iconPath ? iconPath.response.data : '';
}
else {
nowImgUrl = detailInfo.iconPath;
}
} else {
nowImgUrl = iconPath ? (iconPath.flag ? iconPath.fileList[0].url :
iconPath.response.data) : '';
}
if (!nowImgUrl) {
return message.error('請選擇一級分類圖!');
}
values.iconPath = nowImgUrl;
this.props.dispatch({
type: 'primary/saveOneProClass',
payload: values,
callback: ({ code, message: info }) => {
if (code === '0') {
this.props.getData();
this.handleClose();
} else {
message.error(info);
}
}
});
}
});
}
// 圖片上傳的回調
updateImg = (filed, value, fieldEdit) => {
this.setState({
[filed]: value,
[fieldEdit]: true
});
}
// 封裝的上傳圖片組件
<UploadImg
label="一級分類圖"
field="iconPath"
updateImg={this.updateImg}
rowInfo={detailInfo}
imgSize={10000}
formItemLayout={{
labelCol: { span: 8 },
wrapperCol: { span: 14 },
}}
/>
- 刪除
// 批量刪除
batchDeleteInfo = () => {
let { selectedRowKeys } = this.state;
let ids = selectedRowKeys.join(',');
this.deleteInfo(ids);
}
// 刪除公用方法
deleteInfo = (ids) => {
confirm({
title: '刪除確認',
content: '確定要刪除所選數據嗎?',
okText: '確認',
centered: true,
cancelText: '取消',
onOk: () => {
this.props.dispatch({
type: 'primary/deleteProClass', payload: { ids },
callback: ({ code, data, message: info }) => {
if (code === '0') {
message.success(info);
this.setState({
selectedRowKeys: []
})
this.getData();
} else if (code === '-1') {
message.error(info);
}
}
});
},
onCancel() {
},
});
}
- 父子組件傳值
{showModal && <ShowModal hideFildModel={this.showModal} getData={this.getData} rowInfo={rowInfo} />}
新增、編輯、刪除成功后都需進行查詢
如何使用dva全局數據
觀察代碼發現我們使用返回值要么用的是.then,要么用的callback回調函數,如何使用dva數據流處理頁面數據或者做某些事
例如我們在子組件新增、編輯成功后,可以在父組件接受,子組件的成功行為做操作
// 去除子組件的callback獲取返回值的方法
this.props.dispatch({
type: 'primary/saveOneProClass',
payload: param,
// callback: ({ code, message: info }) => {
// if (code === '0') {
// this.props.getData();
// this.handleClose();
// } else {
// message.error(info);
// }
// }
});
// 父組件通過result接收數據進行比較,做修改
componentWillReceiveProps(nextProps) {
const { saveOneProClassResult } = nextProps.primary;
if (saveOneProClassResult !== this.props.primary.saveOneProClassResult) {
const { code, data } = saveOneProClassResult;
if (code === '0') {
this.getData();
this.showModal('showModal', false);
} else {
message.error(saveOneProClassResult.message);
}
}
}
