本文改自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項目
步驟:
- npm install dva-cli -g
- 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 上的 keystate
是初始值,在這里是空數組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); }