前端技術 | dva,美貌與智慧並存

飛久發表於2019-05-08

經過前面四篇的鋪墊,終於輪到我們的主角dva了,就是下面這個美女:

前端技術 | dva,美貌與智慧並存

先擦一擦哈喇子,我們來介紹一下,dva出自於暴雪出品的一款遊戲《守望先鋒》,援引官方的角色介紹:

D.Va擁有一部強大的機甲,它具有兩臺全自動的近距離聚變機炮、可以使機甲飛躍敵人或障礙物的推進器、 還有可以抵禦來自正面的遠端攻擊的防禦矩陣。

然後呢,螞蟻金服的一位架構師sorrycc很迷這位美女,正巧剛開發了一款前端框架沒有名字,作為一個向女神獻禮的專案,dva框架就此誕生。

實際上,dva只是基於現有開源框架的一層輕量封裝,並沒有引入任何新概念:

  • React:管理View
  • react-router:管理路由
  • Redux:管理Model
  • redux-saga:管理非同步呼叫(副作用)

再來看一下框架圖,是不是都是熟悉的配方,熟悉的味道?

前端技術 | dva,美貌與智慧並存

當然,也不是完全沒有新東西,其中有一個Subscription好像之前沒有見過,這是一種資料來源訂閱機制,資料來源可以是鍵盤輸入事件、路由變化、伺服器的 websocket 連線等等。你可以在資料發生變化時收到通知,並派發必要的action。

實際上,dva是一個整合者,它的目標是解決“Code is everywhere”問題。當我們同時使用上面這些框架時,一般會呈現下面這種型別的檔案結構:

+ src
  + sagas
    - user.js
  + reducers
    - user.js
  + actions
    - user.js
複製程式碼

然後,當我們需要實現一個功能時,就需要在這幾個檔案之間來回切換。。。

另一方面,dva還試圖隱藏一些經常重複書寫的routine程式碼,讓開發者能夠更加專注於業務邏輯。比如我們寫一個應用的入口檔案,需要做下面這麼多事情:

  • 配置middleware
  • 建立store
  • 新增<Provider>繫結
  • 建立watcher saga和root saga
  • 啟動saga
  • 。。。

實際上,可能95%以上的專案中這些程式碼都是一模一樣的,我們不需要每次都花費時間來重新寫一遍這些程式碼。

下面開始正式介紹dva 1.0相關的內容,dva 2.0做了一些優化升級,後面專門有一節介紹具體變化。

8個概念

其實基本都是前面幾篇文章裡介紹過的概念:

  • state:也就是全域性唯一的Store
  • action:即Redux中的action
  • model:這是dva抽象出來的一個概念,為了把下面這些東西放到一個統一的檔案裡
    • reducer:即Redux中的reducer
    • effect:即redux-saga中的worker saga
    • subscription:前面介紹過,用於訂閱資料來源
  • router:即react-router中的<Router>
    • route:即react-router中的<Route/>

最終寫出來的model.js會類似下面這個樣子,可以發現所有相關程式碼都放到一起了,不需要在多個檔案之間來回切換了(這裡的namespace就是以前Redux中的reducer的名字):

export default {
  namespace: 'transactions',
  state: {
    txs: []
  },
  subscriptions: {
    setup({ dispatch, history }) {
      history.listen(location => {
        if (location.pathname === '/transactions/list') {
          dispatch({type: 'fetch'});
        }
      });
    },
  },
  effects: {
    *fetch({ payload }, {call, select, put}) {
      const { result } = yield call(apis.fetchTxs)
      yield put({type:'addTx', payload: result})
    },
  },
  reducers: {
    addTx(state, { payload }) {
      return { ...state, txs: payload };
    },
  },
}
複製程式碼

7個API

dva只有7個API,所以上手基本上沒有什麼難度:

  • app = dva(opts):建立dva物件
  • app.use(hooks):使用外掛(後面介紹)
  • app.model(model):註冊model
  • app.unmodel(namespace):取消model註冊
  • app.replaceModel(model):替換model
  • app.router(({ history, app }) => RouterConfig):配置路由
  • app.start(selector?):啟動應用(引數是根元件id)

下面這個連結展示了5步建立單頁應用的例子:github.com/sorrycc/blo…

4大模組

這個主要是從程式碼結構上來劃分的,一般分為下面4類模組:

  • service:執行非同步任務的API
  • model:包含effect, reducer, subscription
  • component:無狀態元件
    • 傳送action觸發狀態更新
    • action = 'namespace/effect'或者'namespace/reducer'
  • route:各種container(這個名字取得不太好)
    • 通過connect()連線到model

另外,由於dva 1.0使用的是react-router v3,所以最外層還有一個router.js用於配置靜態路由。所以一般的目錄結構如下所示:

+ src/
  + services/
    - users.js
  + models/
    - users.js
  + components/
    + users/
      - users.js
      - users.css
  + routes/
    - users.js
  - router.js
複製程式碼

dva 2.0中採用了react-router v4,就不需要router.js了。另外,現在官方推薦搭配使用umi(烏米,sorrycc最新的開源專案),可以自動幫你註冊model、根據目錄結構生成路由配置,目錄結構會變成下面這個樣子:

+ src/
  + models/
    - global.js
  + pages/
    + users/
      + index.js
      + services
        - users.js
      + models/
        - users.js
  + components/
    + users/
      - users.js
      - users.css
複製程式碼

可以發現,把route以及相關聯的model都放到pages目錄中了,可以達到減少耦合,一刪全刪的功能。

外掛(Plugin)

前面提到過一個API可以註冊“外掛”,所謂外掛就是一些生命週期“鉤子(hooks)”,可以在事件發生時做一些額外處理。包括下面這些型別的鉤子:

  • onError
  • onStateChange
  • onAction
  • onHmr:熱替換(Hot Module Replacement)
  • onReducer:reducer被呼叫
  • onEffect:effect被呼叫
  • extraReducers:額外的reducer被呼叫(比如搭配合redux-form)
  • extraEnhancers:額外的store enhancer被呼叫(比如搭配redux-persist)

一個比較典型的例子是頁面載入資料時轉圈圈,載入完畢後隱藏,這是一個很多地方都需要用到的場景。官方提供了一個dva-loading外掛,可以自動在非同步請求資料時把loading設定為true,獲得資料後把loading設定為false。這樣你就可以在元件中繫結loading實現轉圈圈的自動控制了:

// index.js
import createLoading from 'dva-loading';
app.use(createLoading());

// components
function mapStateToProps(state) {
  const { list, total, page } = state.users;
  return {
    loading: state.loading.models.users,
    list,
    total,
    page,
  };
}
複製程式碼

和redux+saga的對應關係

雖然dva只是一層輕量級封裝,但是做了一些特殊的命名約定,剛開始寫程式碼的時候會有點迷糊,搞不清楚跟之前直接使用redux+saga的時候的對應關係,這裡也幫大家梳理一下。

直接使用redux+saga的流程如下所示:

  • component傳送請求action
  • saga呼叫service,然後put一個結果action
  • reducer從action中獲取type和payload,修改state(可以使用combinedReducer,會依次遍歷)
  • component在mapStateToProps()中通過state.<reducer名字>獲取更新

使用dva時的流程如下所示:(觸發effect為例)

  • component傳送請求action,type約定為namespace/effect
  • effect呼叫service,然後put一個結果action,type約定為namespace/reducer(同一個model中可以省略namespace)
  • reducer從action中獲取type和payload,修改state(每個reducer只處理一種型別的action,type約定為reducer的名字
  • component在mapStateToProps()中通過state.<namespace>獲取更新

dva2.0的變動

目前dva已經進化到2.0版本,除了採用了react-router v4以外,還有一些細節上的變動:

  • dispatch一個effect 型別的action時返回一個Promise,方便檢視層回撥

  • 新增 dynamic 介面,配合 react-router@4 處理元件的按需載入

  • take 自動補全 namespace 字首

  • effect 前後會額外觸發 /@@start/@@end 的 action,可利用此約定實現 put 的同步執行

  • 同名 reducer 和 effect 不會 fallthrough(即兩者都執行),而是僅執行 effect

具體細節可以參見:github.com/sorrycc/blo…

今天就說到這裡,老規矩,上一張思維導圖結束本篇文章:

前端技術 | dva,美貌與智慧並存

參考:

dvajs.com/guide

github.com/dvajs/dva/i…

www.github.com/sorrycc/blo…

github.com/sorrycc/blo…

github.com/sorrycc/blo…

juejin.im/post/5b93c0…

github.com/sorrycc/blo…

segmentfault.com/a/119000001…

相關文章