dva實用的學習筆記


本文改自CSDN博主「黃大琪琪」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。

原文鏈接:https://blog.csdn.net/weixin_38398698/article/details/93387757

什么是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)

      

  • 輸入url渲染對應的組件,該組件通過dispatch去出發action里面的函數,如果是同步的就去進入model的ruducer去修改state,如果是異步比如fetch獲取數據就會被effect攔截通過server交互獲取數據進而修改state,同樣state通過connect將model、狀態數據與組件相連

簡單快速的dva項目

步驟:

  1. npm install dva-cli -g
  2. dva new dva-quickstart
  • 這會創建 dva-quickstart 目錄,包含項目初始化目錄和文件,並提供開發服務器、構建腳本、數據 mock 服務、代理服務器等功能。
  • npm start啟動開發服務器

 

使用 antd

通過 npm 安裝 antd 和 babel-plugin-import 。babel-plugin-import 是用來按需加載 antd 的腳本和樣式的,詳見 repo 。

$ npm install antd babel-plugin-import --save 

編輯 .webpackrc,使 babel-plugin-import 插件生效。

{
+  "extraBabelPlugins": [
+    ["import", { "libraryName": "antd", "libraryDirectory": "es", "style": "css" }]
+  ]
}

注:dva-cli 基於 roadhog 實現 build 和 dev,更多 .webpackrc 的配置詳見 roadhog#配置

 

 

定義路由

我們要寫個應用來先顯示產品列表。首先第一步是創建路由,路由可以想象成是組成應用的不同頁面。

新建 route component routes/Products.js,內容如下:

import React from 'react';

const Products = (props) => (
  <h2>List of Products</h2>
);

export default Products;

添加路由信息到路由表,編輯 router.js :

import Products from './routes/Products';
...
<Route path="/products" exact component={Products} />

然后在瀏覽器里打開 http://localhost:8000/#/products ,你應該能看到前面定義的 <h2> 標簽。

 

編寫 UI Component

隨着應用的發展,你會需要在多個頁面分享 UI 元素 (或在一個頁面使用多次),在 dva 里你可以把這部分抽成 component 。

我們來編寫一個 ProductList component,這樣就能在不同的地方顯示產品列表了。

新建 components/ProductList.js 文件:

import React from 'react';
import PropTypes from 'prop-types';
import { Table, Popconfirm, Button } from 'antd';

const ProductList = ({ onDelete, products }) => {
  const columns = [{
    title: 'Name',
    dataIndex: 'name',
  }, {
    title: 'Actions',
    render: (text, record) => {
      return (
        <Popconfirm title="Delete?" onConfirm={() => onDelete(record.id)}>
          <Button>Delete</Button>
        </Popconfirm>
      );
    },
  }];
  return (
    <Table
      dataSource={products}
      columns={columns}
    />
  );
};

ProductList.propTypes = {
  onDelete: PropTypes.func.isRequired,
  products: PropTypes.array.isRequired,
};

export default ProductList;

定義 Model

完成 UI 后,現在開始處理數據和邏輯。

dva 通過 model 的概念把一個領域的模型管理起來,包含同步更新 state 的 reducers,處理異步邏輯的 effects,訂閱數據源的 subscriptions 。

新建 model models/products.js :

export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
};

這個 model 里:

  • namespace 表示在全局 state 上的 key
  • state 是初始值,在這里是空數組
  • reducers 等同於 redux 里的 reducer,接收 action,同步更新 state

然后別忘記在 index.js 里載入他:

// 3. Model
app.model(require('./models/products').default);

connect 起來

到這里,我們已經單獨完成了 model 和 component,那么他們如何串聯起來呢?

dva 提供了 connect 方法。如果你熟悉 redux,這個 connect 就是 react-redux 的 connect 。

編輯 routes/Products.js,替換為以下內容:

import React from 'react';
import { connect } from 'dva';
import ProductList from '../components/ProductList';

const Products = ({ dispatch, products }) => {
  function handleDelete(id) {
    dispatch({
      type: 'products/delete',
      payload: id,
    });
  }
  return (
    <div>
      <h2>List of Products</h2>
      <ProductList onDelete={handleDelete} products={products} />
    </div>
  );
};

// export default Products;
export default connect(({ products }) => ({
  products,
}))(Products);

我們還需要一些初始數據讓這個應用 run 起來。編輯 index.js

const app = dva({
   initialState: {
     products: [
       { name: 'dva', id: 1 },
       { name: 'antd', id: 2 },
     ],
   },
 });

 

 

 

index.js(入口文件)

app = dva(opts)

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

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轉化為browserHistory

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

同樣可以配置hooks以及注冊其他插件

import createLoading from 'dva-loading';
...
app.use(createLoading(opts));
app.model

在普通的react-redux+redux-saga的項目中,我們首先會建4個文件夾,分別是actions,reducer,saga,組件,還有獲取請求數據的services文件夾,同樣在入口文件那要引入很多中間件、provider、connect等去將這幾個文件夾聯系起來,在這里的model以下就將這些集成在了一起,大大減小了開發工作量

 

 

  • namespace

          model 的命名空間,同時也是他在全局 state 上的屬性,只能用字符串,不支持通過 . 的方式創建多層命名空間。相當於這個model的key,在組件里面,通過connect+這個key將想要引入的model加入

import { connect } from 'dva'
...
export default connect(({app})=>{return {...app}})(Top)
  • state
    為狀態值的初始值,優先級要低於app.dva({})

 

const app = dva({
  initialState: { count: 1 }, }); app.model({ namespace: 'count', state: 0, });

 

  • 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 被稱為副作用,在我們的應用中,最常見的就是異步操作。它來自於函數編程的概念,之所以叫副作用是因為它使得我們的函數變得不純,同樣的輸入不一定獲得同樣的輸出。

dva 為了控制副作用的操作,底層引入了redux-sagas做異步流程控制,由於采用了generator的相關概念,所以將異步轉成同步寫法,從而將effects轉為純函數。至於為什么我們這么糾結於 純函數,如果你想了解更多可以閱讀Mostly adequate guide to FP,或者它的中文譯本JS函數式編程指南。

純函數的好處:將函數抽離出來,與業務不耦合,更有利於單元測試 無副作用(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 方法,用於取消數據訂閱。

 

 

 

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的動態擴展

  • 注意到dva中的每個model,實際上都是普通的JavaScript對象,可以利用object.assign進行覆蓋使用
  • 通過工廠函數來生成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' } });
  • 可以借助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);
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM