在 React 開發中,隨著應用的復雜度增加,如何高效地管理應用狀態成為了一個非常重要的問題。為了解決這一問題,很多開發者選擇了 Redux,然而 Redux 的學習曲線較陡,且需要配置較多的樣板代碼。為此,Ant Design 團隊基于 Redux 開發了 DVA(Data-View-Action),一個輕量級的、基于 Redux 和 redux-saga 的狀態管理框架,旨在簡化開發流程,提高開發效率。
DVA 的設計靈感來源于?Model-View-Action(MVA)?模式,與 React 的組件化思想類似。它把數據流、視圖渲染、業務邏輯封裝成一個個模型,每個模型包含了數據、操作和副作用。DVA 的核心概念包括以下幾個部分:
DVA 的核心概念包括以下幾個部分:
- Model(模型) :DVA 中的 Model 是一個用于管理業務數據的對象,包含了該模塊的狀態(
state
)、同步操作(reducers
)、異步操作(effects
)等。每個 Model 都是獨立的,負責某一業務模塊的狀態和邏輯。 - View(視圖) :通常是 React 組件,負責渲染用戶界面,展示從 Model 獲取的數據。
- Action(動作) :Action 通過
dispatch
觸發,向 Model 發送指令以更新狀態。 - Effect(副作用) :處理副作用邏輯,通常用來做異步操作,如 API 請求、定時任務等,DVA 內置了
redux-saga
來處理副作用。
DVA 是建立在 Redux 的基礎上的,因此它的工作原理與 Redux 類似,具有以下幾個關鍵步驟:
- 初始化:DVA 會初始化一個 Redux store,使用
dispatch
派發 action 來更新應用的狀態。 - Model 管理:每個 Model 包含
state
(應用的狀態)、reducers
(同步操作)和effects
(異步操作)。當dispatch
被觸發時,DVA 會調用相應的reducer
或effect
。 - 異步處理(redux-saga) :DVA 使用
redux-saga
來處理異步操作。在effects
中,開發者可以通過yield
來觸發異步操作(例如,發起 API 請求),并通過put
來觸發 action,從而更新 state。 - 視圖更新:在 React 組件中,通過
connect
或useSelector
獲取 state,并將 state 渲染到視圖中。任何 state 的更新都會導致視圖的重新渲染。
為什么要使用Dva?
大家應該都能理解 redux 的概念,并認可這種數據流的控制可以讓應用更可控,以及讓邏輯更清晰。但隨之而來通常會有這樣的疑問:概念太多,并且 reducer, saga, action 都是分離的(分文件)。
這帶來的問題是:
- 編輯成本高,需要在 reducer, saga, action 之間來回切換
- 不便于組織業務模型 (或者叫 domain model) 。比如我們寫了一個 userlist 之后,要寫一個 productlist,需要復制很多文件。
還有一些其他的:
- saga 書寫太復雜,每監聽一個 action 都需要走 fork -> watcher -> worker 的流程
- entry 書寫麻煩
- ...
而 dva 正是用于解決這些問題。
什么時候需要使用 dva?
在 react hooks 上線之后。在 Umi 項目中,我們建議輕量的使用 dva。僅僅在以下幾種場景下推薦使用 dva:
- 父子組件之間的數據互通
- 多頁面之間的數據傳遞(即,公共數據)
- 非 react 組件的場景
Dva使用方式
要開始使用 DVA,我們首先需要安裝它:
npm install dva
當你使用Umi時會內置dva只需要安裝插件和配置即可,以全局state為例
// global.ts
import { cloneDeep } from 'lodash'
...interface userInfo {avatar: string; // 頭像avatarStatus: string | number; // 頭像審核狀態。1 正常,2 審核中avatarPending: string; // 正在審核中的頭像gender: number; // 姓名 未知countryCode: ''; // 國家地區代碼countryName: ''; // 地區名字uid: number;// 用戶iduuid: string; // 用戶uuid
}
const defaultUserInfo: userInfo = {avatar: '', // 頭像avatarStatus: '', // 頭像狀態。1 正常,2 審核中avatarPending: '', // 正在審核中的頭像gender: -1, // 姓名 未知countryCode: '', // 國家地區代碼countryName: '', // 地區名字uid: 0, // 用戶iduuid: '', // 用戶uuid
}const GlobalStore = {namespace: 'global',state:{userInfo: cloneDeep(defaultUserInfo),},effects:{// 獲取用戶信息* getUserInfo ({ callback, errorCallback }, { call, put }) {const res: userInfo = yield call(getUserInfo)if (!res.code) {yield put({type: 'setData',payload: {accessToken: '',userInfo: defaultUserInfo,isLogin: false,},})if (typeof errorCallback === 'function') {errorCallback()}window.location.href = '/'return}...if (typeof callback === 'function') {callback(res.data)}},},reducers:{setData(state,{payload}){return {...state,...payload,}}}
}
在組件中使用
import { connect, FormattedMessage, useIntl } from 'umi'
...const NavBar = (props)=>{const {global,dispatch} = propsconst { userInfo } = globalconst getUserProfile = (uuid?: string) => {if (!uuid) {return}dispatch({type: 'userInfo/getUserInfo',payload: {visible: true,uuid,},})}...
}...export default connect((state) =>({global:state.global
}))(NavBar )