目錄
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í)資料
- 工具: dva-cli 腳手架
- 環(huán)境:
nodejs 6.11.1&npm 5.3.0 - 學(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 啟動項目
-
npm start啟動項目 - 瀏覽器打開
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';