1. 程式人生 > 實用技巧 >Dvajs搭建配置React專案與使用

Dvajs搭建配置React專案與使用

一,介紹與需求分析

1.1,介紹

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

1.2,需求

快速搭建基於react的專案(PC端,移動端)。

二,DvaJS構建專案

2.1,初始化專案

第一步:安裝node

第二步:安裝最新版本dva-cli

 $ npm install dva-cli -g

 $ dva -v

第三步:dva new 建立新應用

$ dva new myapp

也可以在建立專案目錄myapp後,用dva init初始化專案

$ dva init

第四步:執行專案

 $ cd myapp
$ npm start

瀏覽器會自動開啟一個視窗

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

import dva from 'dva'; import {message} from 'antd'; import './index.css'; // 1. Initialize 建立 dva 應用例項 const app = dva(); // 2. Plugins 裝載外掛(可選) app.use({   onError: function (error, action) { message.error(error.message || '失敗', 5); } }); // 3. Model 註冊model app.model(require('../models/example').default); // 4. Router 配置路由 app.router(require('../routes/router').default); // 5. Start 啟動應用 app.start('#root'); export default app._store; // eslint-disable-line 丟擲

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

在opts可以配置所有的hooks

 const app = dva({

      history,

     initialState,

     onError,

    onHmr,

 });

這裡比較常用的是,history的配置,一般預設的是hashHistory,如果要配置 history 為 browserHistory,可以這樣:

 import dva from 'dva';

 import createHistory from 'history/createBrowserHistory';

 const app = dva({

  history: createHistory(),

 });

initialState:指定初始資料,優先順序高於 model 中的 state,預設是 {},但是基本上都在modal裡面設定相應的state。

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

 app.use({

  onError: function (error, action) {

  message.error(error.message || '失敗', 5);

 }

 });

可以根據自己的需要來選擇註冊相應的外掛

3,app.model(ModelObject):這裡是資料邏輯處理,資料流動的地方。

export default {

  namespace: 'example',//model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,我們傳送在傳送 action 到相應的 reducer 時,就會需要用到 namespace

 state: {},//表示 Model 的狀態資料,通常表現為一個 javascript 物件(當然它可以是任何值)

 subscriptions: {//語義是訂閱,用於訂閱一個數據源,然後根據條件 dispatch 需要的 action

    setup({ dispatch, history }) {  // eslint-disable-line

   },

  },

  effects: {//Effect 被稱為副作用,最常見的就是非同步操作

    *fetch({ payload }, { call, put }) {  // eslint-disable-line

     yield put({ type: 'save' });

    },

  },

  reducers: {//reducers 聚合積累的結果是當前 model 的 state 物件

   save(state, action) {

     return { ...state, ...action.payload };

    },

  },
};

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

import React from 'react';

import { routerRedux, Route ,Switch} from 'dva/router';

import { LocaleProvider } from 'antd';

import App from '../components/App/App';

import Flex from '../components/Header/index';

import Login from '../pages/Login/Login';

import Home from '../pages/Home/Home';

import zhCN from 'antd/lib/locale-provider/zh_CN';

const {ConnectedRouter} = routerRedux;

 function RouterConfig({history}) {

  return (

    <ConnectedRouter history={history}>

      <Switch>

        <Route path="/login"  component={Login} />

        <LocaleProvider locale={zhCN}>

        <App>

          <Flex>

            <Switch>

            <Route path="/"  exact component={Home} />

            </Switch>

          </Flex>

        </App>

        </LocaleProvider>

     </Switch>

    </ConnectedRouter>

  );

 }

 export default RouterConfig;

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

啟動我們自己的應用

3.2,DvaJS的十個概念

1,Model

model 是 dva 中最重要的概念,Model 非 MVC 中的 M,而是領域模型,用於把資料相關的邏輯聚合到一起,幾乎所有的資料,邏輯都在這邊進行處理分發

複製程式碼

import Model from 'dva-model';

// import effect from 'dva-model/effect';

 import queryString from 'query-string';

 import pathToRegexp from 'path-to-regexp';

import {ManagementPage as namespace} from '../../utils/namespace';

import {

 getPages,

} from '../../services/page';

 export default Model({

  namespace,

  subscriptions: {

    setup({dispatch, history}) {  // eslint-disable-line

     history.listen(location => {

        const {pathname, search} = location;

        const query = queryString.parse(search);

        const match = pathToRegexp(namespace + '/:action').exec(pathname);

       if (match) {

           dispatch({

              type:'getPages',

            payload:{

                s:query.s || 10,

               p:query.p || 1,

               j_code:parseInt(query.j,10) || 1,

              }

            });

        }

      })

   }

  },

 reducers: {

   getPagesSuccess(state, action) {

    const {list, total} = action.result;

     return {...state, list, loading: false, total};

   },

}

}, {

 getPages,

 })

2,namespace

model 的名稱空間,同時也是他在全域性 state 上的屬性,只能用字串,我們傳送在傳送 action 到相應的 reducer 時,就會需要用到 namespace

3,State(狀態)

初始值,我們在 dva() 初始化的時候和在 modal 裡面的 state 對其兩處進行定義,其中 modal 中的優先順序低於傳給 dva() 的 opts.initialState

 // dva()初始化

 const app = dva({

  initialState: { count: 1 },

 });

 // modal()定義事件

 app.model({

  namespace: 'count',

  state: 0,

 });

Model中state的優先順序比初始化的低,但是基本上專案中的 state 都是在這裡定義的

4,Subscription

Subscriptions 是一種從 源 獲取資料的方法,它來自於 elm。語義是訂閱,用於訂閱一個數據源,然後根據條件 dispatch 需要的 action。資料來源可以是當前的時間、伺服器的 websocket 連線、keyboard 輸入、geolocation 變化、history 路由變化等等

subscriptions: { //觸發器。setup表示初始化即呼叫。

    setup({dispatch, history}) {

     history.listen(location => {//listen監聽路由變化 呼叫不同的方法

      if (location.pathname === '/login') {

          //清除快取

     } else {

          dispatch({

          type: 'fetch'

         });

       }

      });

    },

  },

5,Effects

用於處理非同步操作和業務邏輯,不直接修改 state,簡單的來說,就是獲取從服務端獲取資料,並且發起一個 action 交給 reducer 的地方。其中它用到了redux-saga裡面有幾個常用的函式。

put 用來發起一條action

call 以非同步的方式呼叫函式

select 從state中獲取相關的資料

take 獲取傳送的資料

 effects: {

    *login(action, saga){

     const data = yield saga.call(effect(login, 'loginSuccess', authCache), action, saga);//call 使用者呼叫非同步邏輯 支援Promise

      if (data && data.token) {

        yield saga.put(routerRedux.replace('/home'));//put 用於觸發action 什麼是action下面會講到

      }

    },

    *logout(action, saga){

      const state = yield saga.select(state => state);//select 從state裡獲取資料

    },

  },
reducers: {

    add1(state) {

     const newCurrent = state.current + 1;

     return { ...state,

        record: newCurrent > state.record ? newCurrent : state.record,

        current: newCurrent,

      };

    },

    minus(state) {

      return { ...state, current: state.current - 1};

    },

  },

  effects: {

    *add(action, { call, put }) {

      yield put({ type: 'add1' });

      yield call(delayDeal, 1000);

      yield put({ type: 'minus' });

    },

  },

如果effect與reducers中的add方法重合了,這裡會陷入一個死迴圈,因為當元件傳送一個dispatch的時候,model會首先去找effect裡面的方法,當又找到add的時候,就又會去請求effect裡面的方法。

這裡的 delayDeal,是我這邊寫的一個延時的函式,我們在 utils 裡面編寫一個 utils.js

 /**

  *超時函式處理

  * @param timeout  :timeout超時的時間引數

  * @returns {*} :返回樣式值

  */

 export function delayDeal(timeout) {

  return new Promise((resolve) => {

    setTimeout(resolve, timeout);

  });

 }

接著我們在 models/example.js 匯入這個 utils.js

 import { delayDeal} from '../utils/utils';

6,Reducer

以key/value 格式定義 reducer,用於處理同步操作,唯一可以修改 state 的地方。由 action 觸發。其實一個純函式。

  reducers: {

    loginSuccess(state, action){

      return {...state, auth: action.result, loading: false};

    },

  }

7,Router

Router 表示路由配置資訊,專案中的 router.js

8,RouteComponent

RouteComponent 表示 Router 裡匹配路徑的 Component,通常會繫結 model 的資料

9,Action:表示操作事件,可以是同步,也可以是非同步

action 的格式如下,它需要有一個 type ,表示這個 action 要觸發什麼操作;payload則表示這個 action 將要傳遞的資料

 {
      type: namespace + '/login',

      payload: {

          userName: payload.userName,

          password: payload.password

        }
  } 

構建一個Action 建立函式,如下:

 function goLogin(payload) {

 let loginInfo = {

            type: namespace + '/login',

            payload: {

              userName: payload.userName,

              password: payload.password

            }

          }

  return loginInfo

 }

 //我們直接dispatch(goLogin()),就傳送了一個action。

 dispatch(goLogin())

10,dispatch

type dispatch = (a: Action) => Action

dispatching function 是一個用於觸發 action 的函式,action 是改變 State 的唯一途徑,但是它只描述了一個行為,而 dipatch 可以看作是觸發這個行為的方式,而 Reducer 則是描述如何改變資料的。

在 dva 中,connect Model 的元件通過 props 可以訪問到 dispatch,可以呼叫 Model 中的 Reducer 或者 Effects,常見的形式如:

 dispatch({

    type: namespace + '/login', // 如果在 model 外呼叫,需要新增 namespace,如果在model內呼叫 無需新增 namespace

  payload: {}, // 需要傳遞的資訊

 });

reducers 處理資料

effects 接收資料

subscriptions 監聽資料

3.3,使用antd

先安裝 antd 和 babel-plugin-import

 npm install antd babel-plugin-import --save

 yarn add antd babel-plugin-import

babel-plugin-import 也可以通過 -D 引數安裝到 devDependencies 中,它用於實現按需載入。然後在 .webpackrc 中新增如下配置:

 {
  "extraBabelPlugins": [

    ["import", {

      "libraryName": "antd",

      "libraryDirectory": "es",

      "style": true

    }]
  ]
 }

現在就可以按需引入 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。

 "extraBabelPlugins": ["transform-runtime"],

 "env": {

  development: {

      extraBabelPlugins: ['dva-hmr'],

    },

    production: {

      define: {

        __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }

    }

 }

開發環境下的 extraBabelPlugins 是?["transform-runtime", "dva-hmr"],而生產環境下是?["transform-runtime"]

4,配置 webpack 的?externals?屬性

// 配置 @antv/data-set和 rollbar 不打入程式碼

 "externals": {

    '@antv/data-set': 'DataSet',

    rollbar: 'rollbar',

 }

5,配置 webpack-dev-server 的 proxy 屬性。 如果要代理請求到其他伺服器,可以這樣配:

 proxy: {

    "/api": {

      // "target": "http://127.0.0.1/",

      // "target": "http://127.0.0.1:9090/",

      "target": "http://localhost:8080/",

      "changeOrigin": true,

      "pathRewrite": { "^/api" : "" }

    }

  },

6,disableDynamicImport

禁用 import() 按需載入,全部打包在一個檔案裡,通過 babel-plugin-dynamic-import-node-sync 實現。

7,publicPath

配置 webpack 的 output.publicPath 屬性。

8,extraBabelIncludes

定義額外需要做 babel 轉換的檔案匹配列表,格式為陣列

9,outputPath

配置 webpack 的 output.path 屬性。

打包輸出的檔案

config["outputPath"] = path.join(process.cwd(), './build/')

10,根據需求完整配置如下:

檔名稱是:.webpackrc.js,可根據實際情況新增如下程式碼:

 const path = require('path');

 const config = {

  entry: './src/entries/index.js',

  extraBabelPlugins: [['import', { libraryName: 'antd', libraryDirectory: 'es', style: true }]],

  env: {

    development: {

      extraBabelPlugins: ['dva-hmr'],

    },

    production: {

      define: {

        __CDN__: process.env.CDN ? '//cdn.dva.com/' : '/' }

    }

  },

  externals: {

    '@antv/data-set': 'DataSet',

    rollbar: 'rollbar',

  },

  lessLoaderOptions: {

    javascriptEnabled: true,

  },

  proxy: {

    "/api": {

      // "target": "http://127.0.0.1/",

      // "target": "http://127.0.0.1:9090/",

      "target": "http://localhost:8080/",

      "changeOrigin": true,

    }

  },

  es5ImcompatibleVersions:true,

  disableDynamicImport: true,

  publicPath: '/',

  hash: false,

  extraBabelIncludes:[

    "node_modules"

  ]

 };

 if (module.exports.env !== 'development') {

  config["outputPath"] = path.join(process.cwd(), './build/')

 }

 export default config

更多 .webpackrc 的配置請參考 roadhog 配置。

3.5,使用antd-mobile

先安裝 antd-mobile 和 babel-plugin-import

 npm install antd-mobile babel-plugin-import --save # 或

 yarn add antd-mobile babel-plugin-import

babel-plugin-import 也可以通過 -D 引數安裝到 devDependencies 中,它用於實現按需載入。然後在 .webpackrc 中新增如下配置:

{

  "plugins": [

    ["import", { libraryName: "antd-mobile", style: "css" }] // `style: true` 會載入 less 檔案

  ]

 }

現在就可以按需引入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裡的資料:

 dispatch({type:namespace+'/set',payload:{mdata:[]}});

set是內建的方法

Dva官方文件 nginx代理部署Vue與React專案

五,問題記錄

5.1,路由相關的問題

1,使用match後的路由跳轉問題,版本routerV4

match是一個匹配路徑引數的物件,它有一個屬性params,裡面的內容就是路徑引數,除常用的params屬性外,它還有url、path、isExact屬性。

問題描述:不能跳轉新頁面或匹配跳轉後,重新整理時url所傳的值會被重置掉

不能跳轉的情況

const {ConnectedRouter} = routerRedux;

 function RouterConfig({history}) {

 const tests =({match}) =>(

    <div>

      <Route exact path={`${match.url}/:tab`} component={Test}/>

      <Route exact path={match.url} component={Test}/>

    </div>

  );

  return (

    <ConnectedRouter history={history}>

      <Switch>

        <Route path="/login" component={Login}/>

        <LocaleProvider locale={zhCN}>

          <App>

            <Flex>

              <Switch>

                <Route path="/test" component={tests }/>

                <Route exact path="/test/bindTest" component={BindTest}/>

              </Switch>

            </Flex>

          </App>

        </LocaleProvider>

      </Switch>

    </ConnectedRouter>

  );

 }

路由如上寫法,使用下面方式不能跳轉,但是位址列路徑變了

import { routerRedux} from 'dva/router';

 ...

 this.props.dispatch(routerRedux.push({

      pathname: '/test/bindTest',

      search:queryString.stringify({

        // ...query,

        Code: code,

        Name: name

      })

    }));

 ...

能跳轉,但是重新整理所傳的引數被重置

const {ConnectedRouter} = routerRedux;

 function RouterConfig({history}) {

 const tests =({match}) =>(

    <div>

      <Route exact path={`${match.url}/bindTest`} component={BindTest}/>

      <Route exact path={`${match.url}/:tab`} component={Test}/>

      <Route exact path={match.url} component={Test}/>

    </div>
  );

  return (

    <ConnectedRouter history={history}>

      <Switch>

        <Route path="/login" component={Login}/>

        <LocaleProvider locale={zhCN}>

          <App>

            <Flex>

              <Switch>

                <Route path="/test" component={tests }/>

              </Switch>

            </Flex>

          </App>

        </LocaleProvider>

      </Switch>

    </ConnectedRouter>

  );

 }

路由如上寫法,使用下面方式可以跳轉,但是重新整理時所傳的引數會被test裡所傳的引數重置

 ...

 this.props.dispatch(routerRedux.push({

        pathname: '/test/bindTest',

        search:queryString.stringify({

          // ...query,

          Code: code,

          Name: name

        })

 }));

 ...

解決辦法如下:地址多加一級,跳出以前的介面

路由配置

 const {ConnectedRouter} = routerRedux;

 function RouterConfig({history}) {

 const tests =({match}) =>(

    <div>

      <Route exact path={`${match.url}/bind/test`} component={BindTest}/>

      <Route exact path={`${match.url}/:tab`} component={Test}/>

      <Route exact path={match.url} component={Test}/>

    </div>

  );

  return (

    <ConnectedRouter history={history}>

              <Switch>

                <Route path="/test" component={tests }/>

              </Switch>

    </ConnectedRouter>

  );

 }

呼叫

 ...

 this.props.dispatch(routerRedux.push({

      pathname: '/test/bind/test1',

      search:queryString.stringify({

        // ...query,

        Code: code,

        Name: name

      })

    }));

 ...

5.2,箭頭函式this指向問題

箭頭函式的this定義:箭頭函式的this是在定義函式時繫結的,不是在執行過程中繫結的。簡單的說,函式在定義時,this就繼承了定義函式的物件。