dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此dva是基於現有應用架構 (redux + react-router + redux-saga 等)的一層輕量封裝。是由阿里架構師 sorrycc 帶領 team 完成的一套前端框架。javascript
快速搭建基於react的項目(PC端,移動端)。css
第一步:安裝nodehtml
第二步:安裝最新版本dva-cli前端
1 $ npm install dva-cli -g 2 $ dva -v
第三步:dva new
建立新應用java
1 $ dva new myapp
也能夠在建立項目目錄myapp後,用dva init初始化項目
node
1 $ dva init
第四步:運行項目
react
1 $ cd myapp 2 $ npm start
瀏覽器會自動打開一個窗口webpack
1 import dva from 'dva'; 2 import {message} from 'antd'; 3 import './index.css'; 4 5 // 1. Initialize 建立 dva 應用實例 6 const app = dva(); 7 8 // 2. Plugins 裝載插件(可選) 9 app.use({ 10 onError: function (error, action) { 11 message.error(error.message || '失敗', 5); 12 } 13 }); 14 15 // 3. Model 註冊model 16 app.model(require('../models/example').default); 17 18 // 4. Router 配置路由 19 app.router(require('../routes/router').default); 20 21 // 5. Start 啓動應用 22 app.start('#root'); 23 24 export default app._store; // eslint-disable-line 拋出
在opts
能夠配置全部的hooks
nginx
1 const app = dva({ 2 history, 3 initialState, 4 onError, 5 onHmr, 6 });
這裏比較經常使用的是,history的配置,通常默認的是hashHistory
,若是要配置 history 爲 browserHistory
,能夠這樣:git
1 import dva from 'dva'; 2 import createHistory from 'history/createBrowserHistory'; 3 const app = dva({ 4 history: createHistory(), 5 });
initialState
:指定初始數據,優先級高於 model 中的 state,默認是 {}
,可是基本上都在modal裏面設置相應的state。1 app.use({ 2 onError: function (error, action) { 3 message.error(error.message || '失敗', 5); 4 } 5 });
能夠根據本身的須要來選擇註冊相應的插件
1 export default { 2 3 namespace: 'example',// 的命名空間,同時也是他在全局 上的屬性,只能用字符串,咱們發送在發送 到相應的 時,就會須要用到 4 5 state: {},//表示 Model 的狀態數據,一般表現爲一個 javascript 對象(固然它能夠是任何值) 6 7 subscriptions: {//語義是訂閱,用於訂閱一個數據源,而後根據條件 dispatch 須要的 action 8 setup({ dispatch, history }) { // eslint-disable-line 9 }, 10 }, 11 12 effects: {//Effect 被稱爲反作用,最多見的就是異步操做 13 *fetch({ payload }, { call, put }) { // eslint-disable-line 14 yield put({ type: 'save' }); 15 }, 16 }, 17 18 reducers: {//reducers 聚合積累的結果是當前 model 的 state 對象 19 save(state, action) { 20 return { ...state, ...action.payload }; 21 }, 22 }, 23 24 };modelstateactionreducernamespace
1 import React from 'react'; 2 import { routerRedux, Route ,Switch} from 'dva/router'; 3 import { LocaleProvider } from 'antd'; 4 import App from '../components/App/App'; 5 import Flex from '../components/Header/index'; 6 import Login from '../pages/Login/Login'; 7 import Home from '../pages/Home/Home'; 8 import zhCN from 'antd/lib/locale-provider/zh_CN'; 9 const {ConnectedRouter} = routerRedux; 10 11 function RouterConfig({history}) { 12 return ( 13 <ConnectedRouter history={history}> 14 <Switch> 15 <Route path="/login" component={Login} /> 16 <LocaleProvider locale={zhCN}> 17 <App> 18 <Flex> 19 <Switch> 20 <Route path="/" exact component={Home} /> 21 </Switch> 22 </Flex> 23 </App> 24 </LocaleProvider> 25 </Switch> 26 </ConnectedRouter> 27 ); 28 } 29 30 export default RouterConfig;
啓動咱們本身的應用
model
是 dva
中最重要的概念,Model
非 MVC
中的 M
,而是領域模型,用於把數據相關的邏輯聚合到一塊兒,幾乎全部的數據,邏輯都在這邊進行處理分發
1 import Model from 'dva-model'; 2 // import effect from 'dva-model/effect'; 3 import queryString from 'query-string'; 4 import pathToRegexp from 'path-to-regexp'; 5 import {ManagementPage as namespace} from '../../utils/namespace'; 6 import { 7 getPages, 8 } from '../../services/page'; 9 10 export default Model({ 11 namespace, 12 subscriptions: { 13 setup({dispatch, history}) { // eslint-disable-line 14 history.listen(location => { 15 const {pathname, search} = location; 16 const query = queryString.parse(search); 17 const match = pathToRegexp(namespace + '/:action').exec(pathname); 18 if (match) { 19 dispatch({ 20 type:'getPages', 21 payload:{ 22 s:query.s || 10, 23 p:query.p || 1, 24 j_code:parseInt(query.j,10) || 1, 25 } 26 }); 27 } 28 29 }) 30 } 31 }, 32 reducers: { 33 getPagesSuccess(state, action) { 34 const {list, total} = action.result; 35 return {...state, list, loading: false, total}; 36 }, 37 } 38 }, { 39 getPages, 40 })
model
的命名空間,同時也是他在全局 state
上的屬性,只能用字符串,咱們發送在發送 action
到相應的 reducer
時,就會須要用到 namespace
初始值,咱們在 dva()
初始化的時候和在 modal
裏面的 state
對其兩處進行定義,其中 modal
中的優先級低於傳給 dva()
的 opts.initialState
1 // dva()初始化 2 const app = dva({ 3 initialState: { count: 1 }, 4 }); 5 6 // modal()定義事件 7 app.model({ 8 namespace: 'count', 9 state: 0, 10 });
Model中state的優先級比初始化的低,可是基本上項目中的 都是在這裏定義的state
Subscriptions 是一種從 源 獲取數據的方法,它來自於 elm。語義是訂閱,用於訂閱一個數據源,而後根據條件 dispatch 須要的 action。數據源能夠是當前的時間、服務器的 websocket 鏈接、keyboard 輸入、geolocation 變化、history 路由變化等等
1 subscriptions: { //觸發器。setup表示初始化即調用。 2 setup({dispatch, history}) { 3 history.listen(location => {//listen監聽路由變化 調用不一樣的方法 4 if (location.pathname === '/login') { 5 //清除緩存 6 } else { 7 dispatch({ 8 type: 'fetch' 9 }); 10 } 11 }); 12 }, 13 },
用於處理異步操做和業務邏輯,不直接修改 state
,簡單的來講,就是獲取從服務端獲取數據,而且發起一個 action
交給 reducer
的地方。其中它用到了redux-saga裏面有幾個經常使用的函數。
1 effects: { 2 *login(action, saga){ 3 const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 用戶調用異步邏輯 支持Promise 4 if (data && data.token) { 5 yield saga.put(routerRedux.replace('/home'));//put 用於觸發action 什麼是action下面會講到 6 } 7 }, 8 *logout(action, saga){ 9 const state = yield saga.select(state => state);//select 從state裏獲取數據 10 }, 11 12 },
1 reducers: { 2 add1(state) { 3 const newCurrent = state.current + 1; 4 return { ...state, 5 record: newCurrent > state.record ? newCurrent : state.record, 6 current: newCurrent, 7 }; 8 }, 9 minus(state) { 10 return { ...state, current: state.current - 1}; 11 }, 12 }, 13 effects: { 14 *add(action, { call, put }) { 15 yield put({ type: 'add1' }); 16 yield call(delayDeal, 1000); 17 yield put({ type: 'minus' }); 18 }, 19 },
若是effect
與reducers
中的add
方法重合了,這裏會陷入一個死循環,由於當組件發送一個dispatch
的時候,model
會首先去找effect
裏面的方法,當又找到add
的時候,就又會去請求effect
裏面的方法。
這裏的 delayDeal,是我這邊寫的一個延時的函數,咱們在 utils
裏面編寫一個 utils.js
1 /** 2 *超時函數處理 3 * @param timeout :timeout超時的時間參數 4 * @returns {*} :返回樣式值 5 */ 6 export function delayDeal(timeout) { 7 return new Promise((resolve) => { 8 setTimeout(resolve, timeout); 9 }); 10 }
接着咱們在 models/example.js
導入這個 utils.js
1 import { delayDeal} from '../utils/utils';
以key/value
格式定義 reducer
,用於處理同步操做,惟一能夠修改 state
的地方。由 action
觸發。其實一個純函數。
1 reducers: { 2 loginSuccess(state, action){ 3 return {...state, auth: action.result, loading: false}; 4 }, 5 }
Router
表示路由配置信息,項目中的 router.js
RouteComponent
表示 Router
裏匹配路徑的 Component
,一般會綁定 model
的數據
action
的格式以下,它須要有一個 type
,表示這個 action
要觸發什麼操做;payload
則表示這個 action
將要傳遞的數據
1 { 2 type: namespace + '/login', 3 payload: { 4 userName: payload.userName, 5 password: payload.password 6 } 7 }
構建一個Action
建立函數,以下:
1 function goLogin(payload) { 2 let loginInfo = { 3 type: namespace + '/login', 4 payload: { 5 userName: payload.userName, 6 password: payload.password 7 } 8 } 9 return loginInfo 10 } 11 12 //咱們直接dispatch(goLogin()),就發送了一個action。 13 dispatch(goLogin())
type dispatch = (a: Action) => Action
dispatching function 是一個用於觸發 action 的函數,action 是改變 State 的惟一途徑,可是它只描述了一個行爲,而 dipatch 能夠看做是觸發這個行爲的方式,而 Reducer 則是描述如何改變數據的。
在 dva 中,connect Model 的組件經過 props 能夠訪問到 dispatch,能夠調用 Model 中的 Reducer 或者 Effects,常見的形式如:
1 dispatch({ 2 type: namespace + '/login', // 若是在 model 外調用,須要添加 namespace,若是在model內調用 無需添加 namespace 3 payload: {}, // 須要傳遞的信息 4 });
先安裝 antd
和 babel-plugin-import
1 npm install antd babel-plugin-import --save 2 # 或 3 yarn add antd babel-plugin-import
babel-plugin-import
也能夠經過 -D
參數安裝到 devDependencies
中,它用於實現按需加載。而後在 .webpackrc
中添加以下配置:
1 { 2 "extraBabelPlugins": [ 3 ["import", { 4 "libraryName": "antd", 5 "libraryDirectory": "es", 6 "style": true 7 }] 8 ] 9 }
如今就能夠按需引入 antd 的組件了,如 import { Button } from 'antd'
,Button 組件的樣式文件也會自動幫你引入。
1,entry是入口文件配置
單頁類型:
1 entry: './src/entries/index.js',
多頁類型:
1 "entry": "src/entries/*.js"
2,extraBabelPlugins 定義額外的 babel plugin 列表,格式爲數組。
3,env針對特定的環境進行配置。dev 的環境變量是?development
,build 的環境變量是?production
。
1 "extraBabelPlugins": ["transform-runtime"], 2 "env": { 3 development: { 4 extraBabelPlugins: ['dva-hmr'], 5 }, 6 production: { 7 define: { 8 __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' } 9 } 10 }
開發環境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"]
,而生產環境下是?["transform-runtime"]
4,配置 webpack 的?externals?屬性
1 // 配置 @antv/data-set和 rollbar 不打入代碼 2 "externals": { 3 '@antv/data-set': 'DataSet', 4 rollbar: 'rollbar', 5 }
5,配置 webpack-dev-server 的 proxy 屬性。 若是要代理請求到其餘服務器,能夠這樣配:
1 proxy: { 2 "/api": { 3 // "target": "http://127.0.0.1/", 4 // "target": "http://127.0.0.1:9090/", 5 "target": "http://localhost:8080/", 6 "changeOrigin": true, 7 "pathRewrite": { "^/api" : "" } 8 } 9 },
6,disableDynamicImport
禁用 import()
按需加載,所有打包在一個文件裏,經過 babel-plugin-dynamic-import-node-sync 實現。
7,publicPath
配置 webpack 的 output.publicPath 屬性。
8,extraBabelIncludes
定義額外須要作 babel 轉換的文件匹配列表,格式爲數組
9,outputPath
配置 webpack 的 output.path 屬性。
打包輸出的文件
1 config["outputPath"] = path.join(process.cwd(), './build/')
10,根據需求完整配置以下:
文件名稱是:.webpackrc.js,可根據實際狀況添加以下代碼:
1 const path = require('path'); 2 3 const config = { 4 entry: './src/entries/index.js', 5 extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]], 6 env: { 7 development: { 8 extraBabelPlugins: ['dva-hmr'], 9 }, 10 production: { 11 define: { 12 __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' } 13 } 14 }, 15 externals: { 16 '@antv/data-set': 'DataSet', 17 rollbar: 'rollbar', 18 }, 19 lessLoaderOptions: { 20 javascriptEnabled: true, 21 }, 22 proxy: { 23 "/api": { 24 // "target": "http://127.0.0.1/", 25 // "target": "http://127.0.0.1:9090/", 26 "target": "http://localhost:8080/", 27 "changeOrigin": true, 28 } 29 }, 30 es5ImcompatibleVersions:true, 31 disableDynamicImport: true, 32 publicPath: '/', 33 hash: false, 34 extraBabelIncludes:[ 35 "node_modules" 36 ] 37 }; 38 if (module.exports.env !== 'development') { 39 config["outputPath"] = path.join(process.cwd(), './build/') 40 } 41 export default config
.webpackrc
的配置請參考 roadhog 配置。先安裝 antd-mobile 和 babel-plugin-import
1 npm install antd-mobile babel-plugin-import --save # 或 2 yarn add antd-mobile babel-plugin-import
babel-plugin-import
也能夠經過 -D
參數安裝到 devDependencies
中,它用於實現按需加載。而後在 .webpackrc
中添加以下配置:
1 { 2 "plugins": [ 3 ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 會加載 less 文件 4 ] 5 }
如今就能夠按需引入antd-mobile 的組件了,如 import { DatePicker} from '
antd-mobile'
,DatePicker 組件的樣式文件也會自動幫你引入。
url
訪問相關的 Route-Component
,在組件中咱們經過 dispatch
發送 action
到 model
裏面的 effect
或者直接 Reducer
action
發送給Effect
,基本上是取服務器上面請求數據的,服務器返回數據以後,effect
會發送相應的 action
給 reducer
,由惟一能改變 state
的 reducer
改變 state
,而後經過connect
從新渲染組件。action
發送給reducer
,那直接由 reducer
改變 state
,而後經過 connect
從新渲染組件。以下圖所示:數據的改變發生一般是經過用戶交互行爲或者瀏覽器行爲(如路由跳轉等)觸發的,當此類行爲會改變數據的時候能夠經過 dispatch
發起一個 action,若是是同步行爲會直接經過 Reducers
改變 State
,若是是異步行爲(反作用)會先觸發 Effects
而後流向 Reducers
最終改變 State
重置models裏的數據:
1 dispatch({type:namespace+'/set',payload:{mdata:[]}});
set是內置的方法
1,使用match後的路由跳轉問題,版本routerV4
match是一個匹配路徑參數的對象,它有一個屬性params,裏面的內容就是路徑參數,除經常使用的params屬性外,它還有url、path、isExact屬性。
問題描述:不能跳轉新頁面或匹配跳轉後,刷新時url所傳的值會被重置掉
不能跳轉的狀況
1 const {ConnectedRouter} = routerRedux; 2 3 function RouterConfig({history}) { 4 const tests =({match}) =>( 5 <div> 6 <Route exact path={`${match.url}/:tab`} component={Test}/> 7 <Route exact path={match.url} component={Test}/> 8 </div> 9 10 ); 11 return ( 12 <ConnectedRouter history={history}> 13 <Switch> 14 <Route path="/login" component={Login}/> 15 <LocaleProvider locale={zhCN}> 16 <App> 17 <Flex> 18 <Switch> 19 <Route path="/test" component={tests }/> 20 <Route exact path="/test/bindTest" component={BindTest}/> 21 22 </Switch> 23 </Flex> 24 </App> 25 </LocaleProvider> 26 </Switch> 27 </ConnectedRouter> 28 ); 29 }
路由如上寫法,使用下面方式不能跳轉,可是地址欄路徑變了
1 import { routerRedux} from 'dva/router'; 2 ... 3 4 this.props.dispatch(routerRedux.push({ 5 pathname: '/test/bindTest', 6 search:queryString.stringify({ 7 // ...query, 8 Code: code, 9 Name: name 10 }) 11 })); 12 13 ...
能跳轉,可是刷新所傳的參數被重置
1 const {ConnectedRouter} = routerRedux; 2 3 function RouterConfig({history}) { 4 const tests =({match}) =>( 5 <div> 6 <Route exact path={`${match.url}/bindTest`} component={BindTest}/> 7 <Route exact path={`${match.url}/:tab`} component={Test}/> 8 <Route exact path={match.url} component={Test}/> 9 </div> 10 11 ); 12 return ( 13 <ConnectedRouter history={history}> 14 <Switch> 15 <Route path="/login" component={Login}/> 16 <LocaleProvider locale={zhCN}> 17 <App> 18 <Flex> 19 <Switch> 20 <Route path="/test" component={tests }/> 21 </Switch> 22 </Flex> 23 </App> 24 </LocaleProvider> 25 </Switch> 26 </ConnectedRouter> 27 ); 28 }
路由如上寫法,使用下面方式能夠跳轉,可是刷新時所傳的參數會被test裏所傳的參數重置
1 ... 2 3 this.props.dispatch(routerRedux.push({ 4 pathname: '/test/bindTest', 5 search:queryString.stringify({ 6 // ...query, 7 Code: code, 8 Name: name 9 }) 10 })); 11 12 ...
解決辦法以下:地址多加一級,跳出之前的界面
路由配置
1 const {ConnectedRouter} = routerRedux; 2 3 function RouterConfig({history}) { 4 const tests =({match}) =>( 5 <div> 6 <Route exact path={`${match.url}/bind/test`} component={BindTest}/> 7 <Route exact path={`${match.url}/:tab`} component={Test}/> 8 <Route exact path={match.url} component={Test}/> 9 </div> 10 11 ); 12 return ( 13 <ConnectedRouter history={history}> 14 <Switch> 15 <Route path="/test" component={tests }/> 16 </Switch> 17 </ConnectedRouter> 18 ); 19 }
調用
1 ... 2 3 this.props.dispatch(routerRedux.push({ 4 pathname: '/test/bind/test1', 5 search:queryString.stringify({ 6 // ...query, 7 Code: code, 8 Name: name 9 }) 10 })); 11 12 ...
箭頭函數的this定義:箭頭函數的this是在定義函數時綁定的,不是在執行過程當中綁定的。簡單的說,函數在定義時,this就繼承了定義函數的對象。