DvaJS構建配置React項目與使用

DvaJS構建配置React項目與使用

一,介紹與需求分析

 1.1,介紹

dva 首先是一個基於 redux 和 redux-saga 的數據流方案,而後爲了簡化開發體驗,dva 還額外內置了 react-router 和 fetch,因此dva是基於現有應用架構 (redux + react-router + redux-saga 等)的一層輕量封裝。是由阿里架構師 sorrycc 帶領 team 完成的一套前端框架。javascript

 1.2,需求

快速搭建基於react的項目(PC端,移動端)。css

二,DvaJS構建項目

2.1,初始化項目

第一步:安裝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

 

2.2,項目架構介紹

|-mock             //存放用於 mock 數據的文件 
|-node_modules             //項目包
|-public             //通常用於存放靜態文件,打包時會被直接複製到輸出目錄(./dist)
|-src               //項目源代碼
  |  |-asserts         //用於存放靜態資源,打包時會通過 webpack 處理
  |  |-caches         //緩存
  |  |-components     //組件 存放 React 組件,通常是該項目公用的無狀態組件
  |  |-entries        //入口
  |  |-models         //數據模型 存放模型文件
  |  |-pages          //頁面視圖
  |  |-routes         //路由 存放須要 connect model 的路由組件
  |  |-services       //服務 存放服務文件,通常是網絡請求等
  |  |-test           //測試
  |  |-utils          //輔助工具 工具類庫
|-package.json      //包管理代碼
|-webpackrc.js   //開發配置
|-tsconfig.json     /// ts配置
|-webpack.config.js //webpack配置 
|-.gitignore //Git忽略文件
在dva項目目錄中主要分3層,models,services,components,其中models是最重要概念,這裏放的是各類數據,與數據交互的應該都是在這裏。services是請求後臺接口的方法。components是組件了。

三,DvaJS的使用

3.1,DvaJS的五個Api

 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 拋出

1,app = dva(Opts):建立應用,返回 dva 實例。(注:dva 支持多實例)​

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。

2,app.use(Hooks):配置 hooks 或者註冊插件。

1 app.use({
2   onError: function (error, action) {
3     message.error(error.message || '失敗', 5);
4   }
5 });

能夠根據本身的須要來選擇註冊相應的插件

3,app.model(ModelObject):這裏是數據邏輯處理,數據流動的地方。

 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

4,app.router(Function):註冊路由表,咱們作路由跳轉的地方

 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;

5,app.start([HTMLElement], opts)

啓動咱們本身的應用

3.2,DvaJS的十個概念

1,Model

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 })

2,namespace

model 的命名空間,同時也是他在全局 state 上的屬性,只能用字符串,咱們發送在發送 action 到相應的 reducer 時,就會須要用到 namespace

3,State(狀態)

初始值,咱們在 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

4,Subscription

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   },

5,Effects

用於處理異步操做和業務邏輯,不直接修改 state,簡單的來講,就是獲取從服務端獲取數據,而且發起一個 action 交給 reducer 的地方。其中它用到了redux-saga裏面有幾個經常使用的函數。

  1. put  用來發起一條action
  2. call 以異步的方式調用函數
  3. select 從state中獲取相關的數據
  4. take 獲取發送的數據
 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   },

若是effectreducers中的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';

6,Reducer

key/value 格式定義 reducer,用於處理同步操做,惟一能夠修改 state 的地方。由 action 觸發。其實一個純函數。

1  reducers: {
2     loginSuccess(state, action){
3       return {...state, auth: action.result, loading: false};
4     },
5   }

7,Router

Router 表示路由配置信息,項目中的 router.js

8,RouteComponent

RouteComponent 表示 Router 裏匹配路徑的 Component,一般會綁定 model 的數據

9,Action:表示操做事件,能夠是同步,也能夠是異步

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())

10,dispatch 

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 });
  1. reducers 處理數據
  2. effects   接收數據
  3. subscriptions 監聽數據

3.3,使用antd

先安裝 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 組件的樣式文件也會自動幫你引入。

3.4,配置.webpackrc

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 配置

3.5,使用antd-mobile

先安裝 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 組件的樣式文件也會自動幫你引入。

四,總體架構

  1. 咱們根據 url 訪問相關的 Route-Component,在組件中咱們經過 dispatch 發送 actionmodel 裏面的 effect 或者直接 Reducer
  2. 當咱們將action發送給Effect,基本上是取服務器上面請求數據的,服務器返回數據以後,effect 會發送相應的 actionreducer,由惟一能改變 statereducer 改變 state ,而後經過connect從新渲染組件。
  3. 當咱們將action發送給reducer,那直接由 reducer 改變 state,而後經過 connect 從新渲染組件。以下圖所示:

數據流向

數據的改變發生一般是經過用戶交互行爲或者瀏覽器行爲(如路由跳轉等)觸發的,當此類行爲會改變數據的時候能夠經過 dispatch 發起一個 action,若是是同步行爲會直接經過 Reducers 改變 State ,若是是異步行爲(反作用)會先觸發 Effects 而後流向 Reducers 最終改變 State

重置models裏的數據:

1 dispatch({type:namespace+'/set',payload:{mdata:[]}});
   set是內置的方法

Dva官方文檔              nginx代理部署Vue與React項目

五,問題記錄

5.1,路由相關的問題

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 ...

 5.2,箭頭函數this指向問題

箭頭函數的this定義:箭頭函數的this是在定義函數時綁定的,不是在執行過程當中綁定的。簡單的說,函數在定義時,this就繼承了定義函數的對象。

相關文章
相關標籤/搜索