先說些廢話 最近在開發React技術棧的項目產品,對於數據狀態的管理使用了Dva.js,作為一個資深的ow玩家,我看到這個名字第一反應就是————這不是ow里的一個女英雄嗎?仔細閱讀了官方文檔之後,發現開發者還真是因為這個角色獲得靈感,來命名這個數據狀態管理插件,果然開發大佬都是工作和休閑兩不誤~ ...
先說些廢話
最近在開發React技術棧的項目產品,對於數據狀態的管理使用了Dva.js
,作為一個資深的ow玩家,我看到這個名字第一反應就是————這不是ow里的一個女英雄嗎?仔細閱讀了官方文檔之後,發現開發者還真是因為這個角色獲得靈感,來命名這個數據狀態管理插件,果然開發大佬都是工作和休閑兩不誤~
學過React的同學都知道它的技術棧非常多且雜,所以每當你使用React的時候都需要引入很多的模塊,那麼Dva就是把這些用到的模塊集成在一起,比如一些需要引入的依賴react-saga
/react-loger
、必寫的ReactDOM.render
、provider、connect
包裹等都省去不寫,形成一定的架構規範,大大提高我們的開發效率
今天,就來寫一份文檔,幫助後續使用Dva的開發者更好得在實際項目中(PS:需要是以UMI為基礎框架,純Dva來構建項目可以直接看文章結尾的參考文檔列表)上手使用
什麼是Dva
Dva
首先是一個基於redux
和redux-saga
的數據流方案,然後為了簡化開發體驗,Dva
還額外內置了react-router
和fetch
,所以也可以理解為一個輕量級的應用框架。
在我目前的項目中,更多是使用數據狀態管理的功能,他在我司的fish框架中做了內嵌,在主流的React開發框架UMI中也做了內嵌適配,使用起來非常方便快速。
Dva
設計的目的就是簡化元素,降低難度,讓你不用管他怎麼實現的,我們按照預設的這個規則去寫就可以
數據流向
數據的改變發生通常是通過用戶交互行為或者瀏覽器行為(如路由跳轉等)觸發的,當此類行為會改變數據的時候可以通過dispatch
發起一個action
,如果是同步行為會直接通過reducers
改變states
,如果是非同步行為(副作用)會先觸發effects
然後流向reducers
最終改變states
分層開發
無論是Vue還是React開發,實際的大型應用一定有嚴格的分層開發規範,確保後續開發的可維護性,主要的分層結構有以下幾點:
- Page 負責與用戶直接打交道:渲染頁面,接受用戶的操作輸入,側重於展示型交互性邏輯,這裡需要瞭解無狀態組件
- Model 負責處理業務邏輯,為 Page 做數據、狀態的讀寫、變換、暫存等,
Dva
中model
就是做了這一層的操作 - Service 負責與 HTTP 介面對接,進行純粹的數據讀寫
基礎概念
- namespace
model
的命名空間,同時也是他在全局state
上的屬性- 只能用字元串,不支持通過
.
的方式創建多層命名空間,相當於這個model
的key
- 在組件裡面,通過
connect
這個key
將想要引入的model
加入
import { connect } from 'dva'; export default connect(({ namespaceValue }) => ({ ...namespaceValue }))(DvaCompoent);
- state
- 表示
model
的狀態數據 - 操作的時候每次都要當作不可變數據
immutable data
來對待,保證每次都是全新對象,沒有引用關係
- 表示
- reducer
- 必須是純函數,有固定輸入輸出,主要目的是修改自身
state
- 接受兩個參數:之前已經累積運算的結果和當前要被累積的值,返回的是一個新的累積結果,該函數把一個集合歸併成一個單值
- 需要註意的是同樣的輸入必然得到同樣的輸出,它們不應該產生任何副作用
effect
。並且,每一次的計算都應該使用immutable data
- 必須是純函數,有固定輸入輸出,主要目的是修改自身
- effect
- 主要用於非同步請求,介面調用之類的
effect
被稱為副作用,在我們的應用中,最常見的就是非同步操作- 它來自於函數編程的概念,之所以叫副作用是因為它使得我們的函數變得不純,同樣的輸入不一定獲得同樣的輸出
- subscription
subscription
語義是訂閱,用於訂閱一個數據源,然後根據條件dispatch
需要的action
- 數據源可以是當前的時間、伺服器的websocket連接、keyboard輸入、geolocation變化、history路由變化等等
- 內部定義的函數都會被被執行,執行之後作為監聽來處理事務
- dispatch
dispatch
是一個用於觸發action
的函數,action
是改變state
的唯一途徑,但是它只描述了一個行為,而dipatch
可以看作是觸發這個行為的方式,reducer
則是描述如何改變數據的- 在
Dva
中,connect model
的組件通過props
可以訪問到dispatch
,可以調用model
中的reducer
或者effects
import { connect } from 'dva'; const testCom = props => { const { dispatch } = props; const changeValue = (id, val) => { // 調用reducer,一般是同步修改state中的值 dispatch({ type: 'dva/save', payload: { param: val }, }); // 調用effect,一般是發送後臺請求 dispatch({ type: 'dva/queryValue', payload: { id: id }, }); }; return( <div>'hello world'</div> ) } export default connect(({ dva }) => ({ ...dva }))(testCom);
Model中的Effects函數解析
需要註意的是:Effects
裡面的函數都是Generator函數
- yield
- 固定關鍵詞,
Generator
函數自帶的關鍵詞,和*
搭配使用,有點像async
和await
,使用*
則表明它是Generator
函數 - 然後每使用一個
yield
就是告訴程式這裡是非同步,需要等待這個後面的代碼執行完成,同步代碼可不使用該關鍵詞
- 固定關鍵詞,
- payload
- 頁面上通過
dispatch
傳過來的payload
同名參數
- 頁面上通過
- select
Dva
中Effects
函數的固定傳參- 用於拿到
model
中state
的數據,需要註意的是,state
後面跟命名空間namespace
的值
const data = yield select((state) => state.namespaceName.valueName);
- call
Dva
中Effects
函數的固定傳參- 第一個參數是一個非同步函數,
payload
是參數,可以通過call
來執行一個完整的非同步請求,又因為yield
的存在,就實現了非同步轉同步的方案
const { data } = yield call(queryInterface, payload);
- put
Dva
中Effects
函數的固定傳參- 可以使用同
model
中的Reducers
或者Effects
,通過Reducers
來實現數據到頁面的更新,也可以通過put
實現Effects
的嵌套使用
yield put({ type: 'save', payload: { ...payload }, });
開發目錄
由於公司的fish框架以及常見的umi框架都對Dva做了深度繼承,會預設將src/models
下的model
定義自動掛載,只需要在model
文件夾中新建文件即可新增一個model
用來管理組件狀態,對於某個page
文件夾下麵的model
也會預設掛載
├─assets `靜態資源`
├─components `公共組件`
├─config `路由和環境配置`
├─constants `全局靜態常量`
├─locale `國際化`
│ ├─en_US `英文配置`
│ └─zh_CN `中文配置`
├─models `全局數據狀態` *Dva涉及的目錄*
├─pages `頁面目錄,用我參與開發的其中一個目錄來作為示例` *Dva涉及的目錄*
│ ├─NodeConfig `NodeConfig示例目錄`
│ │ ├─components
│ │ │ ├─Select `Select組件頁面文件` *Dva涉及的目錄*
│ │ │ │ └─components
│ │ │ │ ├─AudienceInfo
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─BlackList
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─ControlGroup
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ └─GroupSelect
│ │ │ │ │ ├─index.js
│ │ │ │ │ └─index.less
│ │ │ │ ├─index.js
│ │ │ │ └─index.less
│ │ ├─models
│ │ │ ├─select.js `Select組件數據狀態管理` *Dva涉及的目錄*
│ │ └─services
├─services `全局介面配置`
├─themes `全局樣式主題`
└─utils `js通用工具`
PS: 該樹形圖通過 `windows shell` 自帶的 `tree` 命令生成
如何使用Dva
首先定義一個簡易的model示例
export default {
namespace: 'dva',
state: {
id: '',
value: {},
},
effects: {
// 所有effect前必須要加 *
*queryValue({ payload }, { select, call, put }) {
const params = {
id: payload.id ? payload.id : yield select(state => state.select.id)
}
const { data } = yield call(queryInterface, params); // queryInterface是定義好的後臺請求介面,一般用axios或fetch來完成
yield put({ type: 'save', payload: data });
},
},
reducers: {
save(state, { payload }) {
return {
...state,
...payload,
};
},
},
subscriptions: {
keyboardWatcher({ dispatch }) {
key('⌘+up, ctrl+up', () => { dispatch( {type:'save'}) });
},
},
};
然後把model和組件綁定在一起
React的Connect函數是一種柯里化寫法
import { connect } from 'dva';
const testCom = props => {
const { helloWorld = 'hello world'} = props;
return(
<div>{ helloWorld }</div>
)
}
// 綁定之後就可以在testCom組件中使用命名為dva的model了
export default connect(({ dva }) => ({ ...dva }))(testCom);
柯里化
柯里化是把接受多個參數的函數轉換成接受一個單一參數的函數(PS:Scala語言中也有類似的設計)
// 柯里化
var foo = function(x) {
return function(y) {
return x + y
}
}
foo(3)(4)
// 普通方法
var add = function(x, y) {
return x + y;
}
add(3, 4)
無狀態組件
創建無狀態組件是為了創建純展示組件,這種組件只負責根據傳入的props來展示,不涉及到要改變state狀態的操作,
在實際項目中頁面組件被寫成無狀態的組件,通過簡單組合可以構建成頁面或複雜組件,通過多個簡單組件來合併成一個複雜的大應用
const NoStateComponent = props => {
const { helloWorld = 'hello world'} = props;
return(
<div>{ helloWorld }</div>
)
}
export default NoStateComponent;
無狀態組件的優點
- 由於是無狀態組件,所以無狀態組件就不會在有組件實例化的過程,無實例化過程也就不需要分配多餘的記憶體,從而性能得到一定的提升
- 代碼整潔、可讀性高,對於大型項目的開發維護非常有好處
參考文檔一 ———— Dva官方文檔
參考文檔二 ———— UMI官方文檔
參考文檔三 ———— REACT基礎筆記 MODEL分層
參考文檔四 ———— 前端數據流方案Dva
參考文檔五 ———— 淺析dva (史上最全的dva用法及分析)
參考文檔六 ———— 【dva】model中effects函數的解析
參考文檔七 ———— Generator 函數的詳解
參考文檔八 ———— React connect()() 雙括弧 --柯里化寫法
參考文檔九 ———— 高級函數技巧-函數柯里化
我是 fx67ll.com,如果您發現本文有什麼錯誤,歡迎在評論區討論指正,感謝您的閱讀!
如果您喜歡這篇文章,歡迎訪問我的 本文github倉庫地址,為我點一顆Star,Thanks~