dva搭建簡易react項目實踐總結(jié)

目錄

1. 前言

2. 工具 & 環(huán)境 & 學(xué)習(xí)資料

3. 安裝腳手架 & 創(chuàng)建react項目

4. 設(shè)計

4.1 抽離model
4.2 設(shè)計組件與路由
4.3 添加Reducers
4.4 添加Effects
4.5 分離service服務(wù)

1.前言

根據(jù)師父給的方向,因此學(xué)習(xí)了dva-cli這一個腳手架工具,進(jìn)行開發(fā)React項目。
在網(wǎng)上搜索學(xué)習(xí)教程的過程中,毫無疑問的瀏覽過一篇《12 步 30 分鐘,完成用戶管理的 CURD 應(yīng)用 (react+dva+antd)》這一教程文章。但是在我實際的學(xué)習(xí)中,也許是由于個人軟件環(huán)境(一個說法是node版本)的問題,在使用以下dva命令行時頻頻報錯,無法跟著教程逐步搭建項目。

// 生成名為users的路由組件
dva g route users
// 生成名為users的model數(shù)據(jù)模型
dva g model users

因此找到了同個作者的相對較老版本的教程,感覺在dva的介紹上更加清晰詳實,最新版實在有點過于急于求成了。
親手跟著這篇教程完成一次實踐后,基本可以了解整個react項目的數(shù)據(jù)流通過程。

2.工具 & 環(huán)境 & 學(xué)習(xí)資料

  1. 工具: dva-cli 腳手架
  2. 環(huán)境:nodejs 6.11.1 & npm 5.3.0
  3. 學(xué)習(xí)教程:dva-cli搭建react項目user-dashboard實踐教程

3.安裝腳手架 & 創(chuàng)建react項目

dva結(jié)構(gòu)介紹
dva 官方中文文檔
使用 dva 所需的所有知識點

// 安裝dva-cli腳手架
npm install -g dva-cli

// 使用dva創(chuàng)建react項目框架
dva new [newProjectName] 

react項目的推薦目錄結(jié)構(gòu)(如果使用dva腳手架創(chuàng)建,則自動生成如下)

|── /mock/             # 數(shù)據(jù)mock的接口文件  
|── /src/              # 項目源碼目錄(我們開發(fā)的主要工作區(qū)域)   
|   |── /components/   # 項目組件(用于路由組件內(nèi)引用的可復(fù)用組件)   
|   |── /routes/       # 路由組件(頁面維度) 
|   |  |── route1.js  
|   |  |── route2.js   # 根據(jù)router.js中的映射,在不同的url下,掛載不同的路由組件
|   |  └── route3.js    
|   |── /models/       # 數(shù)據(jù)模型(可以理解為store,用于存儲數(shù)據(jù)與方法)  
|   |  |── model1.js  
|   |  |── model2.js   # 選擇分離為多個model模型,是根據(jù)業(yè)務(wù)實體進(jìn)行劃分
|   |  └── model3.js  
|   |── /services/     # 數(shù)據(jù)接口(處理前臺頁面的ajax請求,轉(zhuǎn)發(fā)到后臺)   
|   |── /utils/        # 工具函數(shù)(工具庫,存儲通用函數(shù)與配置參數(shù))     
|   |── router.js       # 路由配置(定義路由與對應(yīng)的路由組件)  
|   |── index.js       # 入口文件  
|   |── index.less      
|   └── index.html     
|── package.json       # 項目信息  
└── proxy.config.js    # 數(shù)據(jù)mock配置  

4.設(shè)計

4.1、抽離Model

個人理解: 此處的Model包含了一個業(yè)務(wù)實體的狀態(tài),以及方法。model與java的class其實很像,包含了自有變量(state),以及自有方法(effects),不容許外界改變自己的私有變量,但可以在其他地方通過調(diào)用Model內(nèi)部的方法(effects),來修改model的變量值(在effect中調(diào)用reducer)。

抽離Model,根據(jù)設(shè)計頁面需求,設(shè)計相應(yīng)的Model
教程中的需求是一個用戶數(shù)據(jù)的表單展示,包含了增刪改查等功能
提出users模型

// models/users.js
// version1: 從數(shù)據(jù)維度抽取,更適用于無狀態(tài)的數(shù)據(jù)
// version2: 從業(yè)務(wù)狀態(tài)抽取,將數(shù)據(jù)與組件的業(yè)務(wù)狀態(tài)統(tǒng)一抽離成一個model
// 新增部分為在數(shù)據(jù)維度基礎(chǔ)上,改為從業(yè)務(wù)狀態(tài)抽取而添加的代碼
export default {
  namespace: 'users',
  state: {
    list: [],
    total: null,
+   loading: false, // 控制加載狀態(tài)
+   current: null, // 當(dāng)前分頁信息
+   currentItem: {}, // 當(dāng)前操作的用戶對象
+   modalVisible: false, // 彈出窗的顯示狀態(tài)
+   modalType: 'create', // 彈出窗的類型(添加用戶,編輯用戶)
  },

    // 異步操作
    effects: {
        *query(){},
        *create(){},
        *'delete'(){},   // 因為delete是關(guān)鍵字,特殊處理
        *update(){},
    },

    // 替換狀態(tài)樹
    reducers: {
+       showLoading(){}, // 控制加載狀態(tài)的 reducer
+       showModel(){}, // 控制 Model 顯示狀態(tài)的 reducer
+       hideModel(){},
        querySuccess(){},
        createSuccess(){},
        deleteSuccess(){},
        updateSuccess(){},
    }
}

4.2、設(shè)計組件

先設(shè)置容器組件的訪問路徑,再創(chuàng)建組件文件。

4.2.1 兩種組件概念:容器組件與展示組件
  • 容器組件:具有監(jiān)聽數(shù)據(jù)行為的組件,職責(zé)是綁定相關(guān)聯(lián)的 model 數(shù)據(jù),包含子組件;傳入的數(shù)據(jù)來源于model
import React, { Component, PropTypes } from 'react';

// dva 的 connect 方法可以將組件和數(shù)據(jù)關(guān)聯(lián)在一起
import { connect } from 'dva';

// 組件本身
const MyComponent = (props)=>{};

// propTypes屬性,用于限制props的傳入數(shù)據(jù)類型
MyComponent.propTypes = {};

// 聲明模型傳遞函數(shù),用于建立組件和數(shù)據(jù)的映射關(guān)系
// 實際表示 將ModelA這一個數(shù)據(jù)模型,綁定到當(dāng)前的組件中,則在當(dāng)前組件中,隨時可以取到ModelA的最新值
// 可以綁定多個Model
function mapStateToProps({ModelA}) {
  return {ModelA};
}

// 關(guān)聯(lián) model
// 正式調(diào)用模型傳遞函數(shù),完成模型綁定
export default connect(mapStateToProps)(MyComponent);

  • 展示組件:展示通過 props 傳遞到組件內(nèi)部數(shù)據(jù);傳入的數(shù)據(jù)來源于容器組件向展示組件的props
import React, { Component, PropTypes } from 'react';

// 組件本身
// 所需要的數(shù)據(jù)通過 Container Component 通過 props 傳遞下來
const MyComponent = (props)=>{}
MyComponent.propTypes = {};

// 并不會監(jiān)聽數(shù)據(jù)
export default MyComponent;
4.2.2 設(shè)置路由
// .src/router.js
import React, { PropTypes } from 'react';
import { Router, Route } from 'dva/router';
import Users from './routes/Users';

export default function({ history }) {
  return (
    <Router history={history}>
      <Route path="/users" component={Users} />
    </Router>
  );
};

容器組件雛形

// .src/routes/Users.jsx
import React, { PropTypes } from 'react';

function Users() {
  return (
    <div>User Router Component</div>
  );
}

export default Users;
4.2.3 啟動項目
  1. npm start啟動項目
  2. 瀏覽器打開localhost:8000/#/users 查看新增路由與路由中的組件
4.2.4 設(shè)計容器組件

自頂向下的設(shè)計方法:先設(shè)計容器組件,再逐步細(xì)化內(nèi)部的展示容器

組件的定義方式:

// 方法一: es6 的寫法,當(dāng)組件設(shè)計react生命周期時,可采用這種寫法
// 具有生命周期的組件,可以在接收到傳入數(shù)據(jù)變化時,自定義執(zhí)行方法,有自己的行為模式
// 比如在組件生成后調(diào)用xx請求(componentDidMount)、可以自己決定要不要更新渲染(shouldComponentUpdate)等
class App extends React.Component({});

// 方法二: stateless 的寫法,定義無狀態(tài)組件
// 無狀態(tài)組件,僅僅根據(jù)傳入的數(shù)據(jù)更新,修改自己的渲染內(nèi)容
const App = (props) => ({});

容器組件:

// ./src/routes/Users.jsx
import React, { Component, PropTypes } from 'react';

// 引入展示組件 (暫時都沒實現(xiàn))
import UserList from '../components/Users/UserList';
import UserSearch from '../components/Users/UserSearch';
import UserModal from '../components/Users/UserModal';

// 引入css樣式表
import styles from './style.less'

function Users() {

  // 向userListProps中傳入靜態(tài)數(shù)據(jù)
  const userSearchProps = {};
  const userListProps = {
    total: 3,
    current: 1,
    loading: false,
    dataSource: [
      {
        name: '張三',
        age: 23,
        address: '成都',
      },
      {
        name: '李四',
        age: 24,
        address: '杭州',
      },
      {
        name: '王五',
        age: 25,
        address: '上海',
      },
    ],
  };
  const userModalProps = {};

  return (
    <div className={styles.normal}>
      {/* 用戶篩選搜索框 */}
      <UserSearch {...userSearchProps} />
      {/* 用戶信息展示列表 */}
      <UserList {...userListProps} />
      {/* 添加用戶 & 修改用戶彈出的浮層 */}
      <UserModal {...userModalProps} />
    </div>
  );
}

// 很關(guān)鍵的對外輸出export;使外部可通過import引用使用此組件
export default Users;

展示組件UserList

// ./src/components/Users/UserList.jsx
import React, { Component, PropTypes } from 'react';

// 采用antd的UI組件
import { Table, message, Popconfirm } from 'antd';

// 采用 stateless 的寫法
const UserList = ({
    total,
    current,
    loading,
    dataSource,
}) => {
  const columns = [{
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
    render: (text) => <a href="#">{text}</a>,
  }, {
    title: '年齡',
    dataIndex: 'age',
    key: 'age',
  }, {
    title: '住址',
    dataIndex: 'address',
    key: 'address',
  }, {
    title: '操作',
    key: 'operation',
    render: (text, record) => (
      <p>
        <a onClick={()=>{}}>編輯</a>
         
        <Popconfirm title="確定要刪除嗎?" onConfirm={()=>{}}>
          <a>刪除</a>
        </Popconfirm>
      </p>
    ),
  }];

  // 定義分頁對象
  const pagination = {
    total,
    current,
    pageSize: 10,
    onChange: ()=>{},
  };


  // 此處的Table標(biāo)簽使用了antd組件,傳入的參數(shù)格式是由antd組件庫本身決定的
  // 此外還需要在index.js中引入antd  import 'antd/dist/antd.css'
  return (
    <div>
      <Table
        columns={columns}
        dataSource={dataSource}
        loading={loading}
        rowKey={record => record.id}
        pagination={pagination}
      />
    </div>
  );
}

export default UserList;

4.3 添加Reducer

在整個應(yīng)用中,只有model中的reducer函數(shù)可以直接修改自己所在model的state參數(shù),其余都是非法操作;
并且必須使用return {...state}的形式進(jìn)行修改

4.3.1 第一步:實現(xiàn)reducer函數(shù)
// models/users.js
// 使用靜態(tài)數(shù)據(jù)返回,把userList中的靜態(tài)數(shù)據(jù)移到此處
// querySuccess這個action的作用在于,修改了model的數(shù)據(jù)
export default {
  namespace: 'users',
  state: {},
  subscriptions: {},
  effects: {},
  reducers: {
    querySuccess(state){
        const mock = {
          total: 3,
          current: 1,
          loading: false,
          list: [
            {
              id: 1,
              name: '張三',
              age: 23,
              address: '成都',
            },
            {
              id: 2,
              name: '李四',
              age: 24,
              address: '杭州',
            },
            {
              id: 3,
              name: '王五',
              age: 25,
              address: '上海',
            },
          ]
        };
        // return 的內(nèi)容是一個對象,涵蓋原state中的所有屬性,以實現(xiàn)“更新替換”的效果
        return {...state, ...mock, loading: false};
      }
  }
}

4.3.2 第二步:關(guān)聯(lián)Model中的數(shù)據(jù)源
// routes/Users.jsx

import React, { PropTypes } from 'react';

// 最后用到了connect函數(shù),需要在頭部預(yù)先引入connect
import { connect } from 'dva';

function Users({ location, dispatch, users }) {

  const {
    loading, list, total, current,
    currentItem, modalVisible, modalType
    } = users;

  const userSearchProps={};

  // 使用傳入的數(shù)據(jù)源,進(jìn)行數(shù)據(jù)渲染
  const userListProps={
    dataSource: list,
    total,
    loading,
    current,
  };
  const userModalProps={};

  return (
    <div className={styles.normal}>
      {/* 用戶篩選搜索框 */}
      <UserSearch {...userSearchProps} />
      {/* 用戶信息展示列表 */}
      <UserList {...userListProps} />
      {/* 添加用戶 & 修改用戶彈出的浮層 */}
      <UserModal {...userModalProps} />
    </div>
  );
}

// 聲明組件的props類型
Users.propTypes = {
  users: PropTypes.object,
};

// 指定訂閱數(shù)據(jù),并且關(guān)聯(lián)到users中
function mapStateToProps({ users }) {
  return {users};
}

// 建立數(shù)據(jù)關(guān)聯(lián)關(guān)系
export default connect(mapStateToProps)(Users);
4.3.3 第三步:通過發(fā)起Action,在組件中獲取Model中的數(shù)據(jù)
// models/users.js
// 在組件生成后發(fā)出action,示例:
componentDidMount() {
  this.props.dispatch({
    type: 'model/action',     // type對應(yīng)action的名字
  });
}

// 在本次實踐中,在訪問/users/路由時,就是我們獲取用戶數(shù)據(jù)的時機(jī)
// 因此把dispatch移至subscription中
// subcription,訂閱(或是監(jiān)聽)一個數(shù)據(jù)源,然后根據(jù)條件dispatch對應(yīng)的action
// 數(shù)據(jù)源可以是當(dāng)前的時間、服務(wù)器的 websocket 連接、keyboard 輸入、geolocation 變化、history 路由變化等等  
// 此處訂閱的數(shù)據(jù)源就是路由信息,當(dāng)路由為/users,則派發(fā)'querySuccess'這個effects方法
subscriptions: {
    setup({ dispatch, history }) {
      history.listen(location => {
        if (location.pathname === '/users') {
          dispatch({
            type: 'querySuccess',
            payload: {}
          });
        }
      });
    },
  },


###### 4.3.4 第四步: 在index.js中添加models
// model必須在此完成注冊,才能全局有效
// index.js
app.model(require('./models/users.js'));

4.4 添加Effects

Effects的作用在于處理異步函數(shù),控制數(shù)據(jù)流程。
因為在真實場景中,數(shù)據(jù)都來自服務(wù)器,需要在發(fā)起異步請求獲得返回值后再設(shè)置數(shù)據(jù),更新state。
因此我們往往在Effects中調(diào)用reducer
個人理解: 以java類做類比,effects相當(dāng)于public函數(shù),可以被外部調(diào)用,而reducers相當(dāng)于private函數(shù);當(dāng)effects被調(diào)用時,間接調(diào)用到了reducer函數(shù),修改model中的state。當(dāng)然effects的核心在于異步調(diào)用,處理異步請求(如ajax請求)。

export default {
  namespace: 'users',
  state: {},
  subscriptions: {},
  effects: {
    // 添加effects函數(shù)
    // call與put是dva的函數(shù)
    // call調(diào)用執(zhí)行一個函數(shù)
    // put則是dispatch執(zhí)行一個action
    // select用于訪問其他model
    *query({ payload }, { select, call, put }) {
        yield put({ type: 'showLoading' });
        const { data } = yield call(query);
        if (data) {
          yield put({
            type: 'querySuccess',
            payload: {
              list: data.data,
              total: data.page.total,
              current: data.page.current
            }
          });
        }
      },
    },
  reducers: {}
}



// 添加請求處理   包含了一個ajax請求
// models/users.js
import request from '../utils/request';
import qs from 'qs';
async function query(params) {
  return request(`/api/users?${qs.stringify(params)}`);
}

4.5 把請求處理分離到service中

用意在于分離(可復(fù)用的)ajax請求

// services/users.js
import request from '../utils/request';
import qs from 'qs';
export async function query(params) {
  return request(`/api/users?${qs.stringify(params)}`);
}

// 在models中引用
// models/users.js
import {query} from '../services/users';
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容