有請主角登場:react
Dva:我目前 Github 上 12432 個星,你呢?
React-coat:我目前 74 個。
Dva:那你還敢吐槽我?
React-coat:我星少我怕誰?
Dva:...
複製代碼
Dva:我來自阿里,系出名門,你呢?
React-coat:我的項目。
Dva:那你還敢挑剔我?
React-coat:我野蠻生長我怕誰?
Dva:...
複製代碼
DvaJS 和 React-coat 都是 React+Redux+Redux-router 生態的框架,都是把傳統MVC的調用風格引入MVVM,二者諸多地方頗爲類似。webpack
DvaJS已經廣爲人知,上線已經好幾年了,從文檔、穩定性、測試充分度、輔助工具等方面都天然比 react-coat 強。React-coat 只不過是個人我的項目,以前一直在公司內部使用,今年 1 月升級到 4.0 後感受較穩定了纔開始向外界發佈。git
本文撇開其它因素,僅從設計思路和用戶使用 API 對二者進行深度對比。互聯網是一個神奇的世界,人人都有機會發表自已的觀點,正所謂初生螞蟻不畏象,但願 Dva 不要介意,畢竟二者不是一個量級,沒有吐槽哪有進步嘛。另外若是存在對 DvaJS 理解錯誤的地方,請網友們批評指正。github
>-<,好吧,我認可有點標題黨了。客官別急,請上坐,飲杯茶歇息一下。。。web
雖然 Dva 號稱支持 Typescript,但是看了一下官方給出的:使用 TypeScript 的例子,徹底感受不到誠意,action、model、view 之間的數據類型都是孤立的,沒有相互約束?路由配置裏的文件路徑也是沒法反射,全是字符串,看得我一頭霧水...typescript
舉個 Model 例子,在 Dva 中定義一個 Model:npm
export default {
effects: {
// Call、Put、State類型都需自已手動引入
*fetch(action: {payload: {page: number}}, {call: Call, put: Put}) {
//使用 yield 後,data 將反射不到 usersService.fetch
const data = yield call(usersService.fetch, {page: action.payload.page});
// 這裏將觸發下面 save reducer,但是它們之間沒有創建強關聯
// 如何讓這裏的 playload 類型與下面 save reducer中的 playload 類型自動約束?
// 若是下面 save reducer 更名爲 save2,如何讓這裏的 type 自動感應報錯?
yield put({type: "save", payload: data});
},
},
reducers: {
save(state: State, action: {payload: {list: []}}) {
return {...state, ...action.payload};
},
},
};
複製代碼
反過來看看在 React-coat 中定義一個一樣的 Model:編程
class ModuleHandlers extends BaseModuleHandlers {
// this.state、this.actions、this.dispatch都集成在Model中,直接調用便可
@effect("loading") // 注入loading狀態
public async fetch(payload: {page: number}) {
// 使用 await 更直觀,並且 data 能自動反射類型
const data = await usersService.fetch({page: action.payload.page});
// 使用方法調用,更直觀,並且參數類型和方法名都有自動約束
this.dispatch(this.actions.save(data));
}
@reducer
public save(payload: {list: []}): State {
return {...this.state, ...payload};
}
}
複製代碼
另外,在 react-coat 的 demo 中用了大量的 TS 泛型運算來保證 module、model、action、view、router 之間相互檢查與約束,具體可看一下react-coat-helloworldapi
結論:bash
二者集成框架都差很少,都屬於 Redux 生態圈,最大差異:
Redux-Saga 有不少優勢,好比方便測試、方便 Fork 多任務、 多個 Effects 之間 race 等。但缺點也很明顯:
結論:
你喜不喜歡 Saga,這是我的選擇的問題了,沒有絕對的標準。
umi 和 dva 都喜歡用 Page 爲主線來組織站點結構,並和 Router 綁定,官方文檔中這樣說:
在組件設計方法中,咱們提到過 Container Components,在 dva 中咱們一般將其約束爲 Route Components,由於在 dva 中咱們一般以頁面維度來設計 Container Components。
因此,dva 的工程多爲這種目錄結構:
src
├── components
├── layouts
├── models
│ └── globalModel.js
├── pages
│ ├── photos
│ │ ├── page.js
│ │ └── model.js
│ ├── videos
│ │ ├── page.js
│ │ └── model.js
複製代碼
幾個質疑:
來看看 React-coat
在 React-coat 中沒有 Page 的概念,只有 View,由於一個 View 有可能被路由加載成爲一個所謂的 Page,也可能被一個 modal 彈出成爲一個彈窗,也可能被其它 View 直接嵌套。
假若有一個 PhotosView:
// 以路由方式加載,所謂的 Page
render() {
return (
<Switch>
<Route exact={true} path="/photos/:id" component={DetailsView} />
<Route component={ListView} />
</Switch>
);
}
複製代碼
// 也能夠直接用 props 參數來控制加載
render() {
const {showDetails} = this.props;
return showDetails ? <DetailsView /> : <ListView />; } 複製代碼
在 React-coat 中的組織結構的主線是 Module,它以業務功能的**高內聚,低耦合**的原則劃分:一個 Module = 一個model(維護數據)
和一組view(展示交互)
。典型的目錄結構以下:
src
├── components
├── modules
│ ├── app
│ │ ├── views
│ │ │ ├── View1.tsx
│ │ │ ├── View2.tsx
│ │ │ └── index.ts
│ │ ├── model.ts
│ │ └── index.ts
│ ├── photos
│ │ ├── views
│ │ │ ├── View1.tsx
│ │ │ ├── View2.tsx
│ │ │ └── index.ts
│ │ ├── model.ts
│ │ └── index.ts
複製代碼
結論:
在 Dva 中的路由是集中配置
式的,須要用 app.router()方法來註冊。比較複雜,涉及到 Page、Layout、ContainerComponents、RealouteComponents、loadComponent、loadMode 等概念。複雜一點的應用會有動態路由、權限判斷等,因此 Router.js 寫起來又臭又長,可讀性不好。並且使用一些相對路徑和字符串名稱,沒辦法用引發 TS 的檢查。
後面在 umi+dva 中,路由以 Pages 目錄結構自動生成,對於簡單應用尚可,對於複雜一點的又引起出新問題。好比某個 Page 可能被多個 Page 嵌套,某個 model 被多個 page 共用等。因此,umi 又想出來一些潛規則:
model 分兩類,一是全局 model,二是頁面 model。全局 model 存於 /src/models/ 目錄,全部頁面均可引用;頁面 model 不能被其餘頁面所引用。
規則以下:
src/models/**/*.js 爲 global model
src/pages/**/models/**/*.js 爲 page model
global model 全量載入,page model 在 production 時按需載入,在 development 時全量載入
page model 爲 page js 所在路徑下 models/**/*.js 的文件
page model 會向上查找,好比 page js 爲 pages/a/b.js,他的 page model 爲 pages/a/b/models/**/*.js + pages/a/models/**/*.js,依次類推
約定 model.js 爲單文件 model,解決只有一個 model 時不須要建 models 目錄的問題,有 model.js 則不去找 models/**/*.js
複製代碼
看看在 React-coat 中:
不使用路由集中配置,路由邏輯分散在各個組件中,沒那麼多強制的概念和潛規則。
一句話:一切皆 Component
結論:
React-coat 的路由無限制,更簡單明瞭。
在 Dva 中,由於 Page 是和路由綁定的,因此按需加載只能使用在路由中,須要配置路由:
{
path: '/user',
models: () => [import(/* webpackChunkName: 'userModel' */'./pages/users/model.js')],
component: () => import(/* webpackChunkName: 'userPage' */'./pages/users/page.js'),
}
複製代碼
幾個問題:
在 React-coat 中,View 能夠用路由加載,也能夠直接加載:
// 定義代碼分割
export const moduleGetter = {
app: () => {
return import(/* webpackChunkName: "app" */ "modules/app");
},
photos: () => {
return import(/* webpackChunkName: "photos" */ "modules/photos");
},
}
複製代碼
// 使用路由加載:
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
...
<Route exact={false} path="/photos" component={PhotosView} />
複製代碼
// 直接加載:
const PhotosView = loadView(moduleGetter, ModuleNames.photos, "Main");
...
render() {
const {showDetails} = this.props;
return showDetails ? <DetailsView /> : <ListView />; } 複製代碼
React-coat 這樣作的好處:
結論:
在使用 Dva 時發現一個嚴重的問題,讓我一度懷疑是自已哪裏弄錯了:
1.首先進入一個頁面:localhost:8000/pages,此時查看 Redux-DevTools 以下:
2.而後點擊一個 link 進入 localhost:8000/photos,此時查看 Redux-DevTools 以下:
眼尖的夥伴們看出什麼毛病來沒有?
加載 photos model 時,第一個 action @@INIT 時的 State 快照居然變了,把 photos 強行塞進去了。Redux 奉行的不是不可變數據麼???
結論:
Dva 動態加載 model 時,破壞了 Redux 的基本原則,而 React-coat 不會。
model 分兩類,一是全局 model,二是頁面 model。全局 model 存於 /src/models/ 目錄,全部頁面均可引用;頁面 model 不能被其餘頁面所引用。
global model 全量載入,page model 在 production 時按需載入,在 development 時全量載入。
複製代碼
一個字:繞
React-coat 中 model 跟着業務功能走,一個 module 只能有一個 model:
在 Module 內部,咱們可進一步劃分爲`一個model(維護數據)`和`一組view(展示交互)`
集中在一個名爲model.js的文件中編寫 Model,並將此文件放在本模塊根目錄下
model狀態能夠被全部Module讀取,但只能被自已Module修改,(切合combineReducers理念)
複製代碼
結論:
Dva 中定義 model 使用一個 Object 對象,有五個約定的 key,例如:
{
namespace: 'count',
state: 0,
reducers: {
aaa(payload) {...},
bbb(payload) {...},
},
effects: {
*ccc(action, { call, put }) {...},
*ddd(action, { call, put }) {...},
},
subscriptions: {
setup({ dispatch, history }) {...},
},
}
複製代碼
這樣有幾個問題:
如何保證 reducers 和 effects 之間命名不重複?簡單的一目瞭然還好,若是是複雜的長業務流程,可能涉及到重用和提取,用到 Mixin 和 Extend,這時候怎麼保證?
如何重用和擴展?官方文檔中這樣寫道:
從這個角度看,咱們要新增或者覆蓋一些東西,都會是比較容易的,好比說,使用 Object.assign 來進行對象屬性複製,就能夠把新的內容添加或者覆蓋到原有對象上。注意這裏有兩級,model 結構中的 state,reducers,effects,subscriptions 都是對象結構,須要分別在這一級去作 assign。能夠藉助 dva 社區的 dva-model-extend 庫來作這件事。換個角度,也能夠經過工廠函數來生成 model。
仍是一個字:繞
如今反過來看看 React-coat 怎麼解決這兩個問題:
class ModuleHandlers extends BaseModuleHandlers<State, RootState, ModuleNames> {
@reducer
public aaa(payload): State {...}
@reducer
protected bbb(payload): State {...}
@effect("loading")
protected async ccc(payload) {...}
}
複製代碼
結論:
react-coat 的 model 利用 Class 和裝飾器來實現,更簡單,更適合 TS 類型檢查,也更利於重用與提取。
在 Dva 中,派發 action 裏要手動寫 type 和 payload,缺乏類型驗證和靜態檢查
dispatch({ type: 'moduleA/query', payload:{username:"jimmy"}} })
複製代碼
在 React-coat 中直接利用 TS 的類型反射:
dispatch(moduleA.actions.query({username:"jimmy"}))
複製代碼
結論:
react-coat 的 Action 派發方式更優雅
咱們能夠簡單的認爲:在 Redux 中 store.dispatch(action),能夠觸發一個註冊過的 reducer,看起來彷佛是一種觀察者模式。推廣到以上的 effect 概念,effect 一樣是一個觀察者。一個 action 被 dispatch,可能觸發多個觀察者被執行,它們多是 reducer,也多是 effect。因此 reducer 和 effect 統稱爲:ActionHandler
ActionHandler 機制對於複雜業務流程、跨 model 之間的協做有着強大的做用,舉例說明:
在 React-coat 中,有一些框架級的特別 Action 在適當的時機被觸發,好比:
**module/INIT**:模塊初次載入時觸發
**@@router/LOCATION_CHANGE**: 路由變化時觸發
**@@framework/ERROR**:發生錯誤時觸發
**module/LOADING**:loading狀態變化時觸發
**@@framework/VIEW_INVALID**:UI界面失效時觸發
複製代碼
有了 ActionHandler 機制,它們所有變成了可注入的 hooks,你能夠監聽它們,例如:
// 兼聽自已的INIT Action
@effect()
protected async [ModuleNames.app + "/INIT"]() {
const [projectConfig, curUser] = await Promise.all([settingsService.api.getSettings(), sessionService.api.getCurUser()]);
this.updateState({
projectConfig,
curUser,
startupStep: StartupStep.configLoaded,
});
}
複製代碼
在 Dva 中,要同步處理 effect 必須使用 put.resolve,有點抽象,在 React-coat 中直接 await 更直觀和容易理解。
// 在 Dva 中處理同步 effect
effects: {
* query (){
yield put.resolve({type: 'otherModule/query',payload:1});
yield put({type: 'updateState', payload: 2});
}
}
// 在React-coat中,可以使用 awiat
class ModuleHandlers {
async query (){
await this.dispatch(otherModule.actions.query(1));
this.dispatch(thisModule.actions.updateState(2));
}
}
複製代碼
// 在Dva中須要主動Put調用ModuleB或ModuleC的Action
effects: {
* update (){
...
if(callbackModuleName==="ModuleB"){
yield put({type: 'ModuleB/update',payload:1});
}else if(callbackModuleName==="ModuleC"){
yield put({type: 'ModuleC/update',payload:1});
}
}
}
// 在React-coat中,可以使用ActionHandler觀察者模式:
class ModuleB {
//在ModuleB中兼聽"ModuleA/update" action
async ["ModuleA/update"] (){
....
}
}
class ModuleC {
//在ModuleC中兼聽"ModuleA/update" action
async ["ModuleA/update"] (){
....
}
}
複製代碼
結論
React-coat 中由於引入了 ActionHandler 機制,對於複雜流程和跨 model 協做比 Dva 簡單清晰得多。
好了,先對比這些點,其它想起來再補充吧!百聞不如一試,只有切身用過這兩個框架才能感覺它們之間的差異。因此仍是請君一試吧:
git clone https://github.com/wooline/react-coat-helloworld.git
npm install
npm start
複製代碼
固然,Dva 也有不少優秀的地方,由於它已經廣爲人知,因此就不在此複述了。重申一下,以上觀點僅表明我的,若是文中對 Dva 理解有誤,歡迎批評指正。