與DvaJS風雲對話,是DvaJS挑戰者?仍是又一輪子?

有請主角登場: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 基於 JS,支持 Typescript
  • React-coat 基於 Typescript 支持 JS

雖然 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

  • react-coat 將 Typescript 轉換爲生產力,而 dva 只是讓你玩玩 Typescript。
  • react-coat 有着更直觀和天然的 API 調用。

集成框架

二者集成框架都差很少,都屬於 Redux 生態圈,最大差異:

  • Dva 集成 Redux-Saga,使用 yield 處理異步
  • React-Coat 使用原生 async + await

Redux-Saga 有不少優勢,好比方便測試、方便 Fork 多任務、 多個 Effects 之間 race 等。但缺點也很明顯:

  • 概念太多、容易把問題複雜化
  • 使用 yield 時,不能返回 typescript 類型

結論:

你喜不喜歡 Saga,這是我的選擇的問題了,沒有絕對的標準。


Page vs Module

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

複製代碼

幾個質疑:

  • 單頁 SPA,什麼是 Page? 它的邊界在哪裏?它和其它 Component 有什麼區別?目前看起來是個 Page,說不必定有一天它被嵌套在別的 Component 裏,也說不定有一天它被 Modal 彈窗彈出。
  • 某些 Component 可能被多個 Page 引用,那應當放在哪一個 Page 下面呢?
  • 爲何路由要和 Page 強關聯?Page 切換必需要用路由加載嗎?不用路由行不行?
  • model 跟着 Page 走?model 是抽象的數據,它與 UI 多是一對多的關係。

來看看 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 />; } 複製代碼
  • 用哪一種方式來加載,這屬於 PhotosView 的內部事務,對外界來講,你只管加載 PhotosView 自己就行了。
  • 對於 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 中以 UI Page 爲主線來主織業務功能,並將其與路由綁定,比較死板,在簡單應用中還好,對於交互性複雜的項目,Model 和 UI 的重用將變得很麻煩。
  • React-coat 以業務功能的高內聚、低偶合來劃分 Moduel,更自由靈活,也符合編程理念。

路由設計

在 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'),
}
複製代碼

幾個問題:

  • models 和 component 分開配置,如何保證 models 中加載了 component 中所須要的 全部 model?
  • 每一個 model 和 component 都做爲一個 split code,會不會太碎了?
  • 路由和代碼分割綁定在一塊兒,不夠靈活。
  • 集中配置加載邏輯致使配置文件可讀性差。

在 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 這樣作的好處:

  • 代碼分割只作代碼分割,不參和路由的事,由於模塊也不必定是非得用路由的方式來加載。
  • 路由只作路由的事情,不參和代碼分割的事,由於模塊也不必定非得作代碼分割。
  • 一個 Module 總體打包成一個 bundle,包括 model 和 views,不至於太碎片。
  • 載入 View 會自動 載入與該 View 相關的全部 Model,無需手工配置。
  • 將路由邏輯分散在各 View 內部並對外隱藏細節,更符合一切皆組件的理念。

結論:

  • 使用 React-coat 作代碼分割和按需加載更簡單也更靈活。

動態加載 model 時對 Redux 的破壞

在使用 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 定義

  • Dva 中的 Model 跟着 Page 走,而 Page 又跟着路由走。
  • Dva 中的 Model 比較散,能夠隨意定義多個,也能夠隨意 load,因而 umi 又出了某些限制,如:
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理念)
複製代碼

結論:

  • React-coat 中的 model 更簡單和純粹,不與 UI 和路由掛勾。
  • Dva 中路由按需加載 Page 時還須要手工配置加載 Model。
  • React-coat 中按需加載 View 時會自動加載相應的 Model。

Model 結構

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) {...}
}
複製代碼
  • 至關於 reducer、effect、subscriptions 都做爲方法寫在一個 Class 中,自然不會重名。
  • 由於基於 Class,因此重用和擴展就能夠充分利用類的繼承、覆蓋、重載。
  • 由於基於 TS,還能夠利用 public 或 private 權限來減小對外暴露。

結論:

react-coat 的 model 利用 Class 和裝飾器來實現,更簡單,更適合 TS 類型檢查,也更利於重用與提取。


Action 派發

在 Dva 中,派發 action 裏要手動寫 type 和 payload,缺乏類型驗證和靜態檢查

dispatch({ type: 'moduleA/query', payload:{username:"jimmy"}} })
複製代碼

在 React-coat 中直接利用 TS 的類型反射:

dispatch(moduleA.actions.query({username:"jimmy"}))
複製代碼

結論:

react-coat 的 Action 派發方式更優雅


React-coat 獨有的 ActionHandler 機制

咱們能夠簡單的認爲:在 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));
    }
}
複製代碼
  • 若是 ModuleA 進行某項操做成功以後,ModuleB 或 ModuleC 都須要 update 自已的 State,因爲缺乏 action 的觀察者模式,因此只能將 ModuleB 或 ModuleC 的刷新動做寫死在 ModuleA 中:
// 在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 理解有誤,歡迎批評指正。

相關文章
相關標籤/搜索