一文完全搞懂 DvaJS 原理

Dva 是什麼

dva 首先是一個基於 reduxredux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-routerfetch,因此也能夠理解爲一個輕量級的應用框架。html

Dva 解決的問題

通過一段時間的自學或培訓,你們應該都能理解 redux 的概念,並承認這種數據流的控制可讓應用更可控,以及讓邏輯更清晰。但隨之而來一般會有這樣的疑問:概念太多,而且 reducer, saga, action 都是分離的(分文件)。前端

  • 文件切換問題。redux 的項目一般要分 reducer, action, saga, component 等等,他們的分目錄存放形成的文件切換成本較大。
  • 不便於組織業務模型 (或者叫 domain model) 。好比咱們寫了一個 userlist 以後,要寫一個 productlist,須要複製不少文件。
  • saga 建立麻煩,每監聽一個 action 都須要走 fork -> watcher -> worker 的流程
  • entry 建立麻煩。能夠看下這個 redux entry 的例子,除了 redux store 的建立,中間件的配置,路由的初始化,Provider 的 store 的綁定,saga 的初始化,還要處理 reducer, component, saga 的 HMR 。這就是真實的項目應用 redux 的例子,看起來比較複雜。

Dva 的優點

  • 易學易用,僅有 6 個 api,對 redux 用戶尤爲友好,配合 umi 使用後更是下降爲 0 API
  • elm 概念,經過 reducers, effects 和 subscriptions 組織 model
  • 插件機制,好比 dva-loading 能夠自動處理 loading 狀態,不用一遍遍地寫 showLoading 和 hideLoading
  • 支持 HMR,基於 babel-plugin-dva-hmr 實現 components、routes 和 models 的 HMR

Dva 的劣勢

  • 對於絕大多數不是特別複雜的場景來講,目前能夠被 Hooks 取代

Dva 的適用場景

  • 業務場景:組件間通訊多,業務複雜,須要引入狀態管理的項目
  • 技術場景:使用 React Class Component 寫的項目

Dva 核心概念

  • 基於 Redux 理念的數據流向。 用戶的交互或瀏覽器行爲經過dispatch發起一個action,若是是同步行爲會直接經過Reducers改變State,若是是異步行爲(能夠稱爲反作用)會先觸發Effects而後流向Reducers最終改變State。

  • 基於 Redux 的基本概念。包括:react

    • State 數據,一般爲一個 JavaScript 對象,操做的時候每次都要看成不可變數據(immutable data)來對待,保證每次都是全新對象,沒有引用關係,這樣才能保證 State 的獨立性,便於測試和追蹤變化。
    • Action 行爲,一個普通 JavaScript 對象,它是改變 State 的惟一途徑。
    • dispatch,一個用於觸發 action 改變 State 的函數。
    • Reducer 描述如何改變數據的純函數,接受兩個參數:已有結果和 action 傳入的數據,經過運算獲得新的 state。
    • Effects(Side Effects) 反作用,常見的表現爲異步操做。dva 爲了控制反作用的操做,底層引入了 redux-sagas 作異步流程控制,因爲採用了generator的相關概念,因此將異步轉成同步寫法,從而將effects轉爲純函數。
    • Connect 一個函數,綁定 State 到 View
  • 其餘概念git

    • Subscription,訂閱,從源頭獲取數據,而後根據條件 dispatch 須要的 action,概念來源於 elm。數據源能夠是當前的時間、服務器的 websocket 鏈接、keyboard 輸入、geolocation 變化、history 路由變化等等。
    • Router,前端路由,dva 實例提供了 router 方法來控制路由,使用的是 react-router
    • Route Components,跟數據邏輯無關的組件。一般須要 connect Model的組件都是 Route Components,組織在/routes/目錄下,而/components/目錄下則是純組件(Presentational Components,詳見組件設計方法

Dva 應用最簡結構

不帶 Model

import dva from 'dva';

const App = () => <div>Hello dva</div>;



// 建立應用

const app = dva();

// 註冊視圖

app.router(() => <App />);

// 啓動應用

app.start('#root');
複製代碼

帶 Model

// 建立應用

const app = dva();



app.use(createLoading()) // 使用插件



// 註冊 Model

app.model({

  namespace: 'count',

  state: 0,

  reducers: {

    add(state) { return state + 1 },

  },

  effects: {

    *addAfter1Second(action, { call, put }) {

      yield call(delay, 1000);

      yield put({ type: 'add' });

    },

  },

});



// 註冊視圖

app.router(() => <ConnectedApp />);



// 啓動應用

app.start('#root');
複製代碼

Dva 的底層原理和部分關鍵實現

背景介紹

  1. 整個dva項目使用 lerna 管理的,在每一個package的package.json中找到模塊對應的入口文件,而後查看對應源碼。
  2. dva 是個函數,返回一了個 app 的對象。
  3. 目前 dva 的源碼核心部分包含兩部分,dva 和 dva-core。前者用高階組件 React-redux 實現了 view 層,後者是用 redux-saga 解決了 model 層。

dva

dva 作了三件比較重要的事情:github

  1. 代理 router 和 start 方法,實例化 app 對象
  2. 調用 dva-core 的 start 方法,同時渲染視圖
  3. 使用 react-redux 完成了 react 到 redux 的鏈接。
// dva/src/index.js

export default function (opts = {}) {



  // 1. 使用 connect-react-router 和 history 初始化 router 和 history

  // 經過添加 redux 的中間件 react-redux-router,強化了 history 對象的功能 

 const history = opts.history || createHashHistory();

  const createOpts = {

    initialReducer: {

      router: connectRouter(history),

    },

    setupMiddlewares(middlewares) {

      return [routerMiddleware(history), ...middlewares];

    },

    setupApp(app) {

      app._history = patchHistory(history);

    },

  };



  // 2. 調用 dva-core 裏的 create 方法 ,函數內實例化一個 app 對象。

 const app = create(opts, createOpts);

  const oldAppStart = app.start;



  // 3. 用自定義的 router 和 start 方法代理

 app.router = router;

  app.start = start;

  return app;



  // 3.1 綁定用戶傳遞的 router 到 app._router

 function router(router) {

    invariant(

      isFunction(router),

      `[app.router] router should be function, but got ${typeof router}`,

    );

    app._router = router;

  }



  // 3.2 調用 dva-core 的 start 方法,並渲染視圖

 function start(container) {

    // 對 container 作一系列檢查,並根據 container 找到對應的DOM節點

    

    if (!app._store) {

      oldAppStart.call(app);

    }

    const store = app._store;



    // 爲HMR暴露_getProvider接口

 // ref: https://github.com/dvajs/dva/issues/469

 app._getProvider = getProvider.bind(null, store, app);



    // 渲染視圖

 if (container) {

      render(container, store, app, app._router);

      app._plugin.apply('onHmr')(render.bind(null, container, store, app));

    } else {

      return getProvider(store, this, this._router);

    }

  }

}



function getProvider(store, app, router) {

  const DvaRoot = extraProps => (

    <Provider store={store}>{router({ app, history: app._history, ...extraProps })}</Provider>

  );

  return DvaRoot;

}



function render(container, store, app, router) {

  const ReactDOM = require('react-dom'); // eslint-disable-line

 ReactDOM.render(React.createElement(getProvider(store, app, router)), container);

}
複製代碼

咱們同時能夠發現 app 是經過create(opts, createOpts)進行初始化的,其中opts是暴露給使用者的配置,createOpts是暴露給開發者的配置,真實的create方法在dva-core中實現web

dva-core

dva-core 則完成了核心功能:編程

  1. 經過 create 方法完成 app 實例的構造,並暴露use、model和start三個接口json

  2. 經過 start 方法完成redux

    1. store 的初始化
    2. models 和 effects 的封裝,收集並運行 sagas
    3. 運行全部的model.subscriptions
    4. 暴露app.model、app.unmodel、app.replaceModel三個接口

dva-core createapi

做用: 完成 app 實例的構造,並暴露use、model和start三個接口

// dva-core/src/index.js

const dvaModel = {

  namespace: '@@dva',

  state: 0,

  reducers: {

    UPDATE(state) {

      return state + 1;

    },

  },

};



export function create(hooksAndOpts = {}, createOpts = {}) {

  const { initialReducer, setupApp = noop } = createOpts; // 在dva/index.js中構造了createOpts對象

  const plugin = new Plugin(); // dva-core中的插件機制,每一個實例化的dva對象都包含一個plugin對象

  plugin.use(filterHooks(hooksAndOpts)); // 將dva(opts)構造參數opts上與hooks相關的屬性轉換成一個插件



  const app = {

    _models: [prefixNamespace({ ...dvaModel })],

    _store: null,

    _plugin: plugin,

    use: plugin.use.bind(plugin), // 暴露的use方法,方便編寫自定義插件

    model, // 暴露的model方法,用於註冊model

    start, // 本來的start方法,在應用渲染到DOM節點時經過oldStart調用

  };

  return app;

}
複製代碼

dva-core start

做用:

  1. 封裝models 和 effects ,收集並運行 sagas
  2. 完成store 的初始化
  3. 運行全部的model.subscriptions
  4. 暴露app.model、app.unmodel、app.replaceModel三個接口
function start() {



  const sagaMiddleware = createSagaMiddleware();

  const promiseMiddleware = createPromiseMiddleware(app);

  app._getSaga = getSaga.bind(null);



  const sagas = [];

  const reducers = { ...initialReducer };

  for (const m of app._models) {

    // 把每一個 model 合併爲一個reducer,key 是 namespace 的值,value 是 reducer 函數

    reducers[m.namespace] = getReducer(m.reducers, m.state, plugin._handleActions);

    if (m.effects) {

      // 收集每一個 effects 到 sagas 數組

      sagas.push(app._getSaga(m.effects, m, onError, plugin.get('onEffect'), hooksAndOpts));

    }

  }



  // 初始化 Store

  app._store = createStore({

    reducers: createReducer(),

    initialState: hooksAndOpts.initialState || {},

    plugin,

    createOpts,

    sagaMiddleware,

    promiseMiddleware,

  });



  const store = app._store;



  // Extend store

  store.runSaga = sagaMiddleware.run;

  store.asyncReducers = {};



  // Execute listeners when state is changed

  const listeners = plugin.get('onStateChange');

  for (const listener of listeners) {

    store.subscribe(() => {

      listener(store.getState());

    });

  }



  // Run sagas, 調用 Redux-Saga 的 createSagaMiddleware 建立 saga中間件,調用中間件的 run 方法全部收集起來的異步方法

  // run方法監聽每個反作用action,當action發生的時候,執行對應的 saga

  sagas.forEach(sagaMiddleware.run);



  // Setup app

  setupApp(app);



  // 運行 subscriptions

  const unlisteners = {};

  for (const model of this._models) {

    if (model.subscriptions) {

      unlisteners[model.namespace] = runSubscription(model.subscriptions, model, app, onError);

    }

  }



  // 暴露三個 Model 相關的接口,Setup app.model and app.unmodel

  app.model = injectModel.bind(app, createReducer, onError, unlisteners);

  app.unmodel = unmodel.bind(app, createReducer, reducers, unlisteners);

  app.replaceModel = replaceModel.bind(app, createReducer, reducers, unlisteners, onError);



  /**

   * Create global reducer for redux.

   *

   * @returns {Object}

   */

  function createReducer() {

    return reducerEnhancer(

      combineReducers({

        ...reducers,

        ...extraReducers,

        ...(app._store ? app._store.asyncReducers : {}),

      }),

    );

  }

}

}
複製代碼

路由

在前面的dva.start方法中咱們看到了createOpts,並瞭解到在dva-core的start中的不一樣時機調用了對應方法。

import * as routerRedux from 'connected-react-router';

const { connectRouter, routerMiddleware } = routerRedux;



const createOpts = {

  initialReducer: {

    router: connectRouter(history),

  },

  setupMiddlewares(middlewares) {

    return [routerMiddleware(history), ...middlewares];

  },

  setupApp(app) {

    app._history = patchHistory(history);

  },

};
複製代碼

其中initialReducer和setupMiddlewares在初始化store時調用,而後才調用setupApp

能夠看見針對router相關的reducer和中間件配置,其中connectRouter和routerMiddleware均使用了connected-react-router這個庫,其主要思路是:把路由跳轉也當作了一種特殊的action。

Dva 與 React、React-Redux、Redux-Saga 之間的差別

原生 React

按照 React 官方指導意見, 若是多個 Component 之間要發生交互, 那麼狀態(即: 數據)就維護在這些 Component 的最小公約父節點上, 也便是

以及 自己不維持任何 state, 徹底由父節點 傳入 props 以決定其展示, 是一個純函數的存在形式, 即: Pure Component

React-Redux

與上圖相比, 幾個明顯的改進點:

  1. 狀態及頁面邏輯從 裏面抽取出來, 成爲獨立的 store, 頁面邏輯就是 reducer
  2. 及都是 Pure Component, 經過 connect 方法能夠很方便地給它倆加一層 wrapper 從而創建起與 store 的聯繫: 能夠經過 dispatch 向 store 注入 action, 促使 store 的狀態進行變化, 同時又訂閱了 store 的狀態變化, 一旦狀態變化, 被 connect 的組件也隨之刷新
  3. 使用 dispatch 往 store 發送 action 的這個過程是能夠被攔截的, 天然而然地就能夠在這裏增長各類 Middleware, 實現各類自定義功能, eg: logging

這樣一來, 各個部分各司其職, 耦合度更低, 複用度更高, 擴展性更好。

Redux-Saga

由於咱們可使用 Middleware 攔截 action, 這樣一來異步的網絡操做也就很方便了, 作成一個 Middleware 就好了, 這裏使用 redux-saga 這個類庫, 舉個栗子:

  1. 點擊建立 Todo 的按鈕, 發起一個 type == addTodo 的 action
  2. saga 攔截這個 action, 發起 http 請求, 若是請求成功, 則繼續向 reducer 發一個 type == addTodoSucc 的 action, 提示建立成功, 反之則發送 type == addTodoFail 的 action 便可

Dva

有了前面三步的鋪墊, Dva 的出現也就水到渠成了, 正如 Dva 官網所言, Dva 是基於 React + Redux + Saga 的最佳實踐, 對於提高編碼體驗有三點貢獻:

  1. 把 store 及 saga 統一爲一個 model 的概念, 寫在一個 js 文件裏面
  2. 增長了一個 Subscriptions, 用於收集其餘來源的 action, 好比鍵盤操做等
  3. model 寫法很簡約, 相似於 DSL(領域特定語言),能夠提高編程的沉浸感,進而提高效率

約定大於配置

app.model({

  namespace: 'count',

  state: {

    record: 0,

    current: 0,

  },

  reducers: {

    add(state) {

      const newCurrent = state.current + 1;

      return { ...state,

        record: newCurrent > state.record ? newCurrent : state.record,

        current: newCurrent,

      };

    },

    minus(state) {

      return { ...state, current: state.current - 1};

    },

  },

  effects: {

    *add(action, { call, put }) {

      yield call(delay, 1000);

      yield put({ type: 'minus' });

    },

  },

  subscriptions: {

    keyboardWatcher({ dispatch }) {

      key('⌘+up, ctrl+up', () => { dispatch({type:'add'}) });

    },

  },

});
複製代碼

Dva 背後值得學習的思想

Dva 的 api 參考了 choo,概念來自於 elm。

  1. Choo 的理念:編程應該是有趣且輕鬆的,API 要看上去簡單易用。

We believe programming should be fun and light, not stern and stressful. It's cool to be cute; using serious words without explaining them doesn't make for better results - if anything it scares people off. We don't want to be scary, we want to be nice and fun, and then casually be the best choice around. Real casually.

We believe frameworks should be disposable, and components recyclable. We don't want a web where walled gardens jealously compete with one another. By making the DOM the lowest common denominator, switching from one framework to another becomes frictionless. Choo is modest in its design; we don't believe it will be top of the class forever, so we've made it as easy to toss out as it is to pick up.

We don't believe that bigger is better. Big APIs, large complexities, long files - we see them as omens of impending userland complexity. We want everyone on a team, no matter the size, to fully understand how an application is laid out. And once an application is built, we want it to be small, performant and easy to reason about. All of which makes for easy to debug code, better results and super smiley faces.

  1. 來自 Elm 的概念:
  • Subscription,訂閱,從源頭獲取數據,數據源能夠是當前的時間、服務器的 websocket 鏈接、keyboard 輸入、geolocation 變化、history 路由變化等等。

附錄

Why dva and what's dva

支付寶前端應用架構的發展和選擇

React + Redux 最佳實踐

Dva 概念

Dva 入門課

Dva 源碼解析

Dva 源碼實現

Dva 源碼分析

相關文章
相關標籤/搜索