淺析dva

什麼是dva

  • dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此也能夠理解爲一個輕量級的應用框架。
  • 學過React的童鞋都知道它的技術棧真的不少,因此每當你使用React的時候都須要引入不少的模塊,那麼dva就是把這些用到的模塊集成在一塊兒,造成必定的架構規範。把react經常須要咱們必須寫的須要用到的引用、代碼都集成在了一塊兒,好比一些依賴、必寫的一些ReactDOM.render、引入saga、redux控制檯工具、provider包裹等都省去不寫,大大提升咱們的開發效率
  • 增長了一個 Subscriptions, 用於收集其餘來源的 action, eg: 鍵盤操做、滾動條、websocket、路由等

  • 在react-redux上開發的dva+在redux-saga基礎上開發的dva-core+在webpack基礎上開發的roadhog進行打包啓動服務
  • 數據流向(基於redux,因此同react-redux)
    image
    • 輸入url渲染對應的組件,該組件經過dispatch去出發action裏面的函數,若是是同步的就去進入model的ruducer去修改state,若是是異步好比fetch獲取數據就會被effect攔截經過server交互獲取數據進而修改state,一樣state經過connect將model、狀態數據與組件相連

簡單快速的dva項目

步驟:css

  1. npm install dva-cli -g
  2. dva new dva-quickstart
  • 目錄結構
    在這裏插入圖片描述

src

在這裏插入圖片描述

index.js(入口文件)

在這裏插入圖片描述

app = dva(opts)

建立應用,返回 dva 實例。(注:dva 支持多實例)。node

const app = dva({
  history, // 指定給路由用的 history,默認是 hashHistory
  initialState,  // 指定初始數據,優先級高於 model 中的 state
  onError, // effect 執行錯誤或 subscription 經過 done 主動拋錯時觸發,可用於管理全局出錯狀態。
  onAction, // 在 action 被 dispatch 時觸發
  onStateChange, // state 改變時觸發,可用於同步 state 到 localStorage,服務器端等
  onReducer, // 封裝 reducer 執行。好比藉助 redux-undo 實現 redo/undo
  onEffect, // 封裝 effect
  onHmr, // 熱替換相關
  extraReducers, // 指定額外的 reducer,好比 redux-form 須要指定額外的 form reducer
  extraEnhancers, // 指定額外的 StoreEnhancer ,好比結合 redux-persist 的使用
});
複製代碼

這裏能夠對如下的hook進行option配置 這裏能夠將hashhistory轉化爲browserHistoryreact

import createHistory from 'history/createBrowserHistory';
const app = dva({
  history: createHistory(),
});
複製代碼
app.use(hooks)

一樣能夠配置hooks以及註冊其餘插件webpack

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
複製代碼
app.model

在普通的react-redux+redux-saga的項目中,咱們首先會建4個文件夾,分別是actions,reducer,saga,組件,還有獲取請求數據的services文件夾,一樣在入口文件那要引入不少中間件、provider、connect等去將這幾個文件夾聯繫起來,在這裏的model如下就將這些集成在了一塊兒,大大減少了開發工做量。 web

在這裏插入圖片描述

  • namespace model 的命名空間,同時也是他在全局 state 上的屬性,只能用字符串,不支持經過 . 的方式建立多層命名空間。至關於這個model的key 在組件裏面,經過connect+這個key將想要引入的model加入數據庫

    import { connect } from 'dva'
    複製代碼

在這裏插入圖片描述

  • state 爲狀態值的初始值,優先級要低於app.dva({})
const app = dva({
  initialState: { count: 1 },
});
app.model({
  namespace: 'count',
  state: 0,
});
複製代碼

此時爲1express

  • reducer Action 處理器,處理同步動做,用來算出最新的 State,同redux中的reducer dva對redux作了一層封裝,它會把modal裏面的 reducers函數, 進行一次key的遍歷,每一個key爲一個reducer,固然它加上命名空間,action type對應的reducer、effect
    在這裏插入圖片描述
  • effect Action 處理器,處理異步動做,基於 Redux-saga 實現。Effect 指的是反作用。根據函數式編程,計算之外的操做都屬於 Effect,典型的就是 I/O 操做、數據庫讀寫。以 key/value 格式定義 effect。用於處理異步操做和業務邏輯,不直接修改 state。由 action 觸發,能夠觸發 action,能夠和服務器交互,能夠獲取全局 state 的數據等等 經過generate yield以及saga裏面的經常使用call、put、takeEvery、takeLatest、take
  • call 進行觸發異步操做
  • put 至關於dispatch 觸發reducer改變state
['setQuery']: [function*() {}, { type: 'takeEvery'}],
複製代碼
- takeEvery監聽action的每次變化執行(默認)
- takeLatest監聽action最近一次的變化
- take監聽一次action留着,後面執行動做
複製代碼
  • 爲何要把同步和異步的分開呢 須要注意的是 Reducer 必須是純函數,因此一樣的輸入必然獲得一樣的輸出,它們不該該產生任何反作用。而且,每一次的計算都應該使用immutable data,這種特性簡單理解就是每次操做都是返回一個全新的數據(獨立,純淨),因此熱重載和時間旅行這些功能纔可以使用。

Effect 被稱爲反作用,在咱們的應用中,最多見的就是異步操做。它來自於函數編程的概念,之因此叫反作用是由於它使得咱們的函數變得不純,一樣的輸入不必定得到一樣的輸出。npm

dva 爲了控制反作用的操做,底層引入了redux-sagas作異步流程控制,因爲採用了generator的相關概念,因此將異步轉成同步寫法,從而將effects轉爲純函數。至於爲何咱們這麼糾結於 純函數,若是你想了解更多能夠閱讀Mostly adequate guide to FP,或者它的中文譯本JS函數式編程指南。編程

純函數的好處:將函數抽離出來,與業務不耦合json

更有利於單元測試
	無反作用(side-effect),不會修改做用域外的值,使代碼好調試
	執行順序不會對系統形成影響
	剝離出業務邏輯,好複用
複製代碼
  • action跑哪去了? action在組件的dispath中觸發,dva對redux作了一層封裝,它會把modal裏面的 reducers函數, 進行一次key的遍歷,每一個key爲一個reducer,固然它加上命名空間,action type對應的reducer、effect
const { dispatch } = this.props;
        dispatch({ 
            type: 'app/updateState' ,
            payload: {
                opacityTop: 'none',//控制top的透明度
                hiddenDivDisplay: 'none',//控制隱藏頭部的display
                footerDisplay: 'none'//控制footer的display
            }
        });
複製代碼
  • subscriptions 以 key/value 格式定義 subscription。subscription 是訂閱,用於訂閱一個數據源,而後根據須要 dispatch 相應的 action。在 app.start() 時被執行,數據源能夠是當前的時間、服務器的 websocket 鏈接、keyboard 輸入、geolocation 變化、history 路由變化等等。

    格式爲 ({ dispatch, history }, done) => unlistenFunction。
      注意:若是要使用 app.unmodel(),subscription 必須返回 unlisten 方法,用於取消數據訂閱。
    複製代碼

在這裏插入圖片描述
在這裏插入圖片描述

app.router

在這裏插入圖片描述
直接將路由引入或在多頁應用中只將組件引入

app.router(require('./router'));
app.router(() => <App />);
複製代碼
app.start

啓動應用。selector 可選,若是沒有 selector 參數,會返回一個返回 JSX 元素的函數。 selector爲根元素

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

mock---.roadhogrc.mock.js

roadhog server 支持 mock 功能,相似 dora-plugin-proxy,在 .roadhogrc.mock.js 中進行配置,支持基於 require 動態分析的實時刷新,支持 ES6 語法,以及友好的出錯提示。在配置文件進行一下(node語法)配置,就能夠經過簡單的fetch請求獲取到數據。

.roadhogrc.mock.js
export default {
  // 支持值爲 Object 和 Array
  'GET /api/users': { users: [1,2] },
 
  // GET POST 可省略
  '/api/users/1': { id: 1 },
 
  // 支持自定義函數,API 參考 express@4
  'POST /api/users/create': (req, res) => { res.end('OK'); },
 
  // Forward 到另外一個服務器
  'GET /assets/*': 'https://assets.online/',
 
  // Forward 到另外一個服務器,並指定子路徑
  // 請求 /someDir/0.0.50/index.css 會被代理到 https://g.alicdn.com/tb-page/taobao-home, 實際返回 https://g.alicdn.com/tb-page/taobao-home/0.0.50/index.css
  'GET /someDir/(.*)': 'https://g.alicdn.com/tb-page/taobao-home',
};
複製代碼

若爲多接口應用,則在mock文件夾下利用mockjs進行數據模擬,再在配置文件裏,進行文件遍歷引入

mock->user.js
const qs = require('qs');
const mockjs = require('mockjs');  //導入mock.js的模塊

const Random = mockjs.Random;  //導入mock.js的隨機數

// 數據持久化   保存在global的全局變量中
let tableListData = {};

if (!global.tableListData) {
  const data = mockjs.mock({
    'data|100': [{
      'id|+1': 1,
      'name': () => {
        return Random.cname();
      },
      'mobile': /1(3[0-9]|4[57]|5[0-35-9]|7[01678]|8[0-9])\d{8}/,
    }],
    page: {
      total: 100,
      current: 1,
    },
  });
  tableListData = data;
  global.tableListData = tableListData;
} else {
  tableListData = global.tableListData;
}

module.exports = {
  //post請求  /api/users/ 是攔截的地址   方法內部接受 request response對象
  'GET /users' (req, res) {
    setTimeout(() => {
      res.json({      //將請求json格式返回
        success: true,
        data,
        page: '123',
      });
    }, 200);
  },

.roadhogrc.mock.js

const mock = {}
require('fs').readdirSync(require('path').join(__dirname + '/mock')).forEach(function(file) {
    Object.assign(mock, require('./mock/' + file))
})
module.exports = mock
複製代碼

.webpackrc

格式爲 JSON,容許註釋,布爾類型的配置項默認值均爲 false,支持經過 webpack.config.js 以編碼的方式進行配置,但不推薦,由於 roadhog 自己的 major 或 minor 升級可能會引發兼容問題。

在這裏插入圖片描述

  • entry:設置入口文件
  • disableCSSModules:設置是否css模塊化
  • publicPath:
  • outputPublic:
  • extraBabelPlugins 配置額外的 babel plugin。babel plugin 只能添加,不容許覆蓋和刪除。好比,同時使用 antd, dva 時,一般須要這麼配:
"extraBabelPlugins": [
      "transform-runtime",
      "dva-hmr",
      ["import", { "libraryName": "antd", "libraryDirectory": "lib", "style": "css" }]
    ]
複製代碼
  • proxy 配置代理,詳見 webpack-dev-server#proxy。若是要代理請求到其餘服務器,能夠這樣配:
"proxy": {
  "/api": {
    "target": "http://jsonplaceholder.typicode.com/",
    "changeOrigin": true,
    "pathRewrite": { "^/api" : "" }
  }
}
複製代碼
  • multipage 配置是否多頁應用。多頁應用會自動提取公共部分爲 common.js 和 common.css 。

  • define 配置 webpack 的 DefinePlugin 插件,define 的值會自動作 JSON.stringify 處理。

  • env 針對特定的環境進行配置。server 的環境變量是 development,build 的環境變量是 production。防止生產環境冗餘。

"extraBabelPlugins": ["transform-runtime"],
"env": {
  "development": {
    "extraBabelPlugins": ["dva-hmr"]
  }
}
複製代碼
  • theme 配置主題,其實是配 less 的 modifyVars。支持 Object 和文件路徑兩種方式的配置。結合antd設置全局樣式。
"theme": {
  "@primary-color": "#1DA57A"
}
/
"theme": "./node_modules/abc/theme-config.js"
複製代碼

段位升級

dva/dynamic(懶加載)

在router.js中使用,動態加載model和component app: dva 實例,加載 models 時須要 models: 返回 Promise 數組的函數,Promise 返回 dva model component:返回 Promise 的函數,Promise 返回 React Component

在這裏插入圖片描述
在這裏插入圖片描述

css 模塊化

在roadhog中引入他們本身封裝的af-webpack,這裏面用css-loader以及加上.webpackrc的配置對css進行模塊化,將css結果js的一層封裝,給classname後面加上隨機的hash,使得classname不會衝突,若要全局的就加上:global便可

在這裏插入圖片描述

用model共享全局信息

若是當前應用中加載了不止一個model,在其中一個的effect裏面作select操做,是能夠獲取另一箇中的state的:

*foo(action, { select }) {
  const { a, b } = yield select();
}
複製代碼

model的動態擴展

  1. 注意到dva中的每一個model,實際上都是普通的JavaScript對象,能夠利用object.assign進行覆蓋使用
  2. 經過工廠函數來生成model
function createModel(options) {
  const { namespace, param } = options;
  return {
    namespace: `demo${namespace}`,
    states: {},
    reducers: {},
    effects: {
      *foo() {
        // 這裏能夠根據param來肯定下面這個call的參數
        yield call()
      }
    }
  };
}

const modelA = createModel({ namespace: 'A', param: { type: 'A' } });
const modelB = createModel({ namespace: 'A', param: { type: 'B' } });
複製代碼
  1. 能夠藉助dva社區的dva-model-extend庫來作這件事

多任務調度

  • 任務的並行執行
const [result1, result2]  = yield all([
  call(service1, param1),
  call(service2, param2)
])
複製代碼
  • 任務的競爭
const { data, timeout } = yield race({
  data: call(service, 'some data'),
  timeout: call(delay, 1000)
});

if (data)
  put({type: 'DATA_RECEIVED', data});
else
  put({type: 'TIMEOUT_ERROR'});
複製代碼

跨model的通訊

若是這裏是要在組件裏面作某些事情,怎麼辦? 將resolve傳給model

new Promise((resolve, reject) => {
  dispatch({ type: 'reusable/addLog', payload: { data: 9527, resolve, reject } });
})
.then((data) => {
  console.log(`after a long time, ${data} returns`);
});
複製代碼

在model進行跨model通訊

try {
  const result = yield call(service1);
  yield put({ type: 'service1Success', payload: result });
  resolve(result);
}
catch (error) {
  yield put({ type: 'service1Fail', error });
  reject(ex);
}
複製代碼

源碼淺析

roadhog

roadhog主要是依賴於他們本身封裝的af-webpack

獲取webpackrc的配置以及校驗

在getUserConfig的文件夾下,直接經過json內容去獲取配置

在這裏插入圖片描述
在config下進行每一項的校驗
在這裏插入圖片描述
在index,js中調用watch.js的方法去監聽咱們的配置文件,而監聽文件夾用的是chokidar這個包
在這裏插入圖片描述

css模塊化

在這裏插入圖片描述
在這裏插入圖片描述
經過咱們的配置文件的配置,以及對環境對判斷,動態給classname的後面加上hash值

dva

在這裏插入圖片描述
經過對其package.json的研究能夠看出,這個下面只是對fetch、redux、router進行封裝

dva/dynamic

在這裏插入圖片描述

  • 首先經過傳入的model以及component利用promise.all進行加載,先判斷是否有model,沒有model就直接將component傳出去,有model的話,就在此動態加載model(registerModel)
    在這裏插入圖片描述
    這裏利用app.model進行註冊,固然咱們也能夠利用這個方法去擴展卸載的方法app.unmodel
  • 那組件傳到哪了呢?
    在這裏插入圖片描述
    在這裏看到其實咱們的dynamic裏面還能夠傳一個參數就是默認加載的組件,爲LoadingComponent,利用該默認組件的生命週期的加載,去控制咱們傳入的component的設置,賦值到AsyncComponent,
  • 那爲何有this.state.AsyncComponent = AsyncComponent;這種寫法 防止渲染速度太快,致使默認組件尚未掛在上,直接渲染async組件
  • 當async爲null的時候,就只渲染默認組件,從這能夠看出component爲必填選項
index.js

在這裏插入圖片描述
從這就能夠看出在這裏將provider、render等在此編寫
在這裏插入圖片描述
而這些都是在app.start中完成的,而且router必須在start前註冊
在這裏插入圖片描述
一樣,在dva({})初始化的時候,將中間件註冊並返回了一個app的對象
在這裏插入圖片描述
利用react-router-redux的routerReducer進行action的路由跳轉,並將routing能夠返回給組件使用

dva-core

在這裏插入圖片描述
經過package.json就知道這裏是對redux-saga進行封裝

model

在這裏插入圖片描述

  • 首先在checkModel中進行5個api的校驗
  • 在prefixNamespace中對每一個model加上key的前綴,以即可以將其看成action的type去dispatch
    在這裏插入圖片描述
  • 在createStore中將中間件以及saga進行註冊
    在這裏插入圖片描述
  • 在getSaga getReducer中將reducer、effect功能實現
    在這裏插入圖片描述
  • 在這裏也是返回了一個app的對象實現start具體功能以及use
    在這裏插入圖片描述
    c從卸載的函數裏面就能夠看到他是把model也都存在一個store裏面,經過dispatch去觸發刪除model
  • subscription
    在這裏插入圖片描述
    Object.prototype.hasOwnProperty.call(subs, key) 仍是使用原型方法判斷 key 是否是 subs 的自有屬性 若是是自由屬性,那麼拿到屬性對應的值(是一個 function) 調用該 function,傳入 dispatch 和 history 屬性。history 就是通過 redux-router 強化過的 history,而 dispatch,也就是 prefixedDispatch(app._store.dispatch, model)
  • prefixedDispatch: 就是給dispatch方法加上namespace的前綴
    在這裏插入圖片描述
相關文章
相關標籤/搜索