Dva双向数据流的理解
(Dva双向数据流的理解&)
Dva双向数据流的理解&
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State
首先通过路由监听来调用dispatch来触发reducers或effect中对应的方法,最终还是要在reducer中通过return {...state,...loding.payload} ,通过return返回来改变state,state改变以后会自动触发UI中Dom的操作。
调用effect中的query方法中的put获取后台数据
拿到数据以后,调用put方法调用reducers中对应的方法。
通过return来改变state,从而更新dom。
(关于Route Components的理解&)
关于Route Components的理解&
在 dva 中我们通常将其约束为 Route Components,因为在 dva 中我们通常以页面维度来设计 Container Components。
所以在 dva 中,通常需要 connect Model的组件都是 Route Components,组织在/routes/目录下,而/components/目录下则是纯组件(Presentational Components)。
Model中的namespace是全局唯一的key,一个key对应一个state数据仓库。
在Route Components中,通过this.props来获取connect的model中的state,一般是通过namespace获取model中对应的state数据。
要想要Route Components与Model进行绑定,要通过connect的方式进行连接。
{users}就是state,通过全局唯一的namespace来获取Model中的state的一种方式。
有两种方式绑定components与Model,一种是全局加载的方式,第一次加载的时候直接引入所有的Model与components。
第二种方式是RouterConfig懒加载处理,当用到components时才回去加载所对应的Model
当绑定好Router Components与model以后,就可以通过this.props获取对应model中的state了。
这边使用的是函数式编程,所以直接在User的参数中获取this.props的属性就可以啦。
This.props中默认有location和dispatch两个属性,以及最重要的从Model中继承而来的state了,这里是{user}
(组件设计方法&)
组件设计方法&
组件设计的原则就是尽量让每个component专注于做自己的事情。
我们开发的都是单页应用,对于单页应用而言,通过路由Route来进行对应Component的跳转。
Router Component一般作为数据容器存在的,它会包含其他的子组件。
另一种组件即为展示组件,它不会订阅Model的数据,所需要的数据也是通过props传递到组件内部的。
案例分析
当前这个案例组件的拆分
首先是路由组件,要绑定对应namespace获取state,上面已经展示过了如何绑定components与Model的。
路由组件下包含了三个子组件,通过react的prop,把路由组件绑定的state传递到子组件中。
从users 的namespace中析构出model中state的数据,分模块传递进对应的子组件中。
每个子组件需要什么state的数据,就给他们封装起来一并传过去。
在组件中,通过this.props获取父组件封装并传递过来的属性。
在react中方法做为一种属性,和属性数据同样的方式传递到子组件中进行调用。
在标签中就可以调用父组件传递过来的方法执行响应的事件了。
完整Demo展示
git clone aaa@qq.com:dvajs/dva.git
git clone aaa@qq.com:dvajs/dva-example-user-dashboard.git
git clone aaa@qq.com:devisions/dvajs-user-dashboard.git
项目结构
入口文件
import dva from 'dva';
import { browserHistory } from 'dva/router';
import createLoading from 'dva-loading';
// 1. Initialize
const app = dva({
history:browserHistory
});
// 2. Plugins
app.use(createLoading());
// 3. Model
//app.model(require('./models/users/users'));
// 4. Router
app.router(require('./router'));
// 5. Start
app.start('#root');
全局路由入口文件 router.js
import React from 'react';
import PropTypes from 'prop-types';
import { Router } from 'dva/router';
const registerModel = (app, model) => {
if (!(app._models.filter(m => m.namespace === model.namespace).length === 1)) {
app.model(model);
}
};
const RouterConfig = ({ history, app }) => {
/**
* 添加路由,path是请求路基,name是名称说明
* */
const routes = [
{
path: '/',
name: 'welcome',
getComponent(nextState, cb) {
require.ensure([], (require) => {
cb(null, require('./routes/indexPage/IndexPage'));
});
},
},
{
path: '/users',
name: 'users',
getComponent(nextState, cb) {
require.ensure([], (require) => {
registerModel(app, require('./models/users'));
cb(null, require('./routes/users/Users'));
});
},
},
];
return <Router history={history} routes={routes} />;
};
RouterConfig.propTypes = {
history: PropTypes.object,
app: PropTypes.object,
};
export default RouterConfig;
Model数据仓库
import { create, remove, update, query } from '../services/users';
import { parse } from 'qs';
export default {
namespace: 'users',
state: {
list: [],
total: null,
field: '', // 搜索框字段
keyword: '', // 搜索框输入的关键字
loading: false, // 控制加载状态
current: 1, // 当前分页信息
currentItem: {}, // 当前操作的用户对象
modalVisible: false, // 弹出窗的显示状态
modalType: 'create', // 弹出窗的类型(添加用户,编辑用户)
},
effects: {
*query({ payload }, { call, put }) {
console.log(payload);
yield put({ type: 'showLoading' });
yield put({ type: 'updateQueryKey', payload });
const { data } = yield call(query, parse(payload));
if (data) {
yield put({
type: 'querySuccess',
payload: {
list: data.list,
total: data.total,
current: data.current,
},
});
}
},
*create({ payload }, { call, put }) {
console.log(payload);
yield put({ type: 'hideModal' });
yield put({ type: 'showLoading' });
const { data } = yield call(create, payload);
console.log(data);
if (data) {
yield put({
type: 'createSuccess',
payload: {
list: data.list,
total: data.total,
current: data.current,
field: '',
keyword: '',
},
});
}
},
*'delete'({ payload }, { call, put }) {
yield put({ type: 'showLoading' });
const { data } = yield call(remove, { id: payload });
if (data) {
yield put({
type: 'deleteSuccess',
payload,
});
}
},
*update({ payload }, { select, call, put }) {
yield put({ type: 'hideModal' });
yield put({ type: 'showLoading' });
const id = yield select(({ users }) => users.currentItem.id);
const newUser = { ...payload, id };
const { data } = yield call(update, newUser);
if (data ) {
yield put({
type: 'updateSuccess',
payload: newUser,
});
}
},
},
subscriptions: {
setup({ dispatch, history }) {
history.listen(location => {
if (location.pathname === '/users') {
console.log(location);
dispatch({
type: 'query',
payload: location.query
});
}
});
},
},
reducers: {
showLoading(state) {// 控制加载状态的 reducer
return { ...state, loading: true };
},
showModal(state, action) {// 控制 Modal 显示状态的 reducer
return { ...state, ...action.payload, modalVisible: true };
},
hideModal(state) {
return { ...state, modalVisible: false };
},
querySuccess(state, action){
return {...state, ...action.payload, loading: false};
},
createSuccess(state, action) {
return { ...state, ...action.payload, loading: false };
},
deleteSuccess(state, action) {
const id = action.payload;
const newList = state.list.filter(user => user.id !== id);
return { ...state, list: newList, loading: false };
},
updateSuccess(state, action) {
const updateUser = action.payload;
const newList = state.list.map(user => {
if (user.id === updateUser.id) {
return { ...user, ...updateUser };
}
return user;
});
return { ...state, list: newList, loading: false,currentItem: {} };
},
updateQueryKey(state, action) {
return { ...state, ...action.payload };
},
}
}
路由组件
import UserList from '../../components/users/UserList';
import UserSearch from '../../components/users/UserSearch';
import UserModal from '../../components/users/UserModal';
import PropTypes from 'prop-types'
import styles from './Users.less';
import { connect } from 'dva';
import React, { useEffect } from 'react'
function Users({ location, dispatch, getUsers,users }) {
console.log(users);
const {
loading, list, total, current,field, keyword,
currentItem, modalVisible, modalType
} = users;
const userSearchProps = {
field,
keyword,
onAdd() {
dispatch({
type: 'users/showModal',
payload: {
modalType: 'create',
currentItem : {}
},
});
},
onSearch(fieldsValue) {
dispatch({
type: 'users/query',
payload: fieldsValue,
});
},
};
const userListProps={
dataSource: list,
total,
loading,
current,
onPageChange(page) {
dispatch(routerRedux.push({
pathname: '/users',
query: { field, keyword, page },
}));
},
onDeleteItem(id) {
dispatch({
type: 'users/delete',
payload: id,
});
},
onEditItem(item) {
console.log(item);
dispatch({
type: 'users/showModal',
payload: {
modalType: 'update',
currentItem: item,
},
});
},
};
const userModalProps = {
item: modalType === 'create' ? {} : currentItem,
type: modalType,
visible: modalVisible,
onOk(data) {
dispatch({
type: `users/${modalType}`,
payload: data,
});
},
onCancel() {
dispatch({
type: 'users/hideModal',
});
},
};
//useEffect(() => { getUsers },[])
return (
<div className={styles.normal}>
{/* 用户筛选搜索框 */}
<UserSearch {...userSearchProps} />
{/* 用户信息展示列表 */}
<UserList {...userListProps} />
{/* 添加用户 & 修改用户弹出的浮层 */}
<UserModal {...userModalProps} />
</div>
);
}
Users.propTypes = {
users: PropTypes.object,
};
const mapStateToProps = ({users})=>{
return {users};
}
//const mapDispatchToProps = (dispatch)=>{
// return {
// getUsers : ()=>{
// dispatch({
// type : "users/querySuccess",
// payload : {}
// });
// return {
// type : "users/querySuccess",
// payload : {}
// }
// }
// }
//}
export default connect(mapStateToProps)(Users);
Services数据接口
import request from '../utils/request';
import qs from 'qs';
import {url} from '../utils/commonConstant';
export async function query(params) {
console.log(params);
if(JSON.stringify(params) == "{}"){
return request(`${url}/users/query`);
}
return request(`${url}/users/query?${qs.stringify(params)}`);
}
export async function create(params) {
console.log(params);
return request(`${url}/users/create`, {
method: 'post',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify(params),
});
}
export async function remove(params) {
console.log(params);
return request(`${url}/users/delete/${params.id}`, {
method: 'delete',
body: qs.stringify(params),
});
}
export async function update(params) {
return request(`${url}/users/update`, {
method: 'put',
headers: {
'Content-Type': 'application/json; charset=utf-8'
},
body: JSON.stringify(params),
});
}
纯组件实现页面展示功能
import React, { PropTypes } from 'react';
import { Form, Input, Button, Select } from 'antd';
import styles from './UserSearch.less';
const UserSearch = ({
field, keyword,
onSearch,
onAdd,
form: {
getFieldDecorator,
validateFields,
getFieldsValue,
},
}) => {
function handleSubmit(e) {
e.preventDefault();
validateFields((errors) => {
if (!!errors) {
return;
}
onSearch(getFieldsValue());
});
}
return (
<div className={styles.normal}>
<div className={styles.search}>
<Form inline onSubmit={handleSubmit}>
<Form.Item>
{getFieldDecorator('field', {
initialValue: field || 'name',
})(
<Select>
<Select.Option value="name">姓名 </Select.Option>
<Select.Option value="address">住址 </Select.Option>
</Select>
)}
</Form.Item>
<Form.Item
hasFeedback
>
{getFieldDecorator('keyword', {
initialValue: keyword || '',
})(
<Input type="text" />
)}
</Form.Item>
<Button style={{ marginRight: '10px' }} type="primary" htmlType="submit">搜索</Button>
</Form>
</div>
<div className={styles.create}>
<Button type="ghost" onClick={onAdd}>Add</Button>
</div>
</div>
);
};
UserSearch.propTypes = {
form: PropTypes.object.isRequired,
onSearch: PropTypes.func,
onAdd: PropTypes.func,
field: PropTypes.string,
keyword: PropTypes.string,
};
export default Form.create()(UserSearch);
// ./src/components/Users/UserList.jsx
import React, { Component, PropTypes } from 'react';
// 采用antd的UI组件
import { Table, message, Popconfirm, Pagination } from 'antd';
// 采用 stateless 的写法
const UserList = ({
total,
current,
loading,
dataSource,
onPageChange,
onDeleteItem,
onEditItem,
}) => {
const columns = [{
title: '姓名',
dataIndex: 'name',
key: 'name',
render: (text) => <a href="#">{text}</a>,
}, {
title: '年龄',
dataIndex: 'age',
key: 'age',
}, {
title: '性别',
dataIndex: 'sex',
key: 'sex',
}, {
title: '住址',
dataIndex: 'address',
key: 'address',
}, {
title: '操作',
key: 'operation',
render: (text, record) => (
<p>
<a onClick={()=>{onEditItem(record)}}>编辑</a>
<Popconfirm title="确定要删除吗?" onConfirm={() => onDeleteItem(record.id)}>
<a>删除</a>
</Popconfirm>
</p>
),
}];
// 定义分页对象
const pagination = {
total,
current,
pageSize: 10,
onChange: ()=>{},
};
return (
<div>
<Table
columns={columns}
dataSource={dataSource}
loading={loading}
rowKey={record => record.id}
pagination={false}
/>
<Pagination
className="ant-table-pagination"
total={total}
current={current}
pageSize={10}
onChange={onPageChange}
/>
</div>
);
}
export default UserList;
import React, { PropTypes } from 'react';
import { Form, Input, Modal } from 'antd';
const FormItem = Form.Item;
const formItemLayout = {
labelCol: {
span: 6,
},
wrapperCol: {
span: 14,
}
};
const UserModal = ({
visible,
item = item || {},
onOk,
onCancel,
form: {
getFieldDecorator,
validateFields,
getFieldsValue,
},
}) => {
function handleOk() {
validateFields((errors) => {
if (errors) {
return;
}
const data = { ...getFieldsValue(), id: item.id };
onOk(data);
});
}
function checkNumber(rule, value, callback) {
if (!value) {
callback(new Error('年龄必须填写!'));
}
if (!/^[\d]{1,2}$/.test(value)) {
callback(new Error('请输入合法年龄!'));
} else {
callback();
}
}
const modalOpts = {
visible,
onOk: handleOk,
onCancel,
};
return (
<Modal {...modalOpts} title={item.id ? '修改用户' : '添加用户'}>
<Form horizontal>
<FormItem
label="姓名:"
hasFeedback
{...formItemLayout}
>
{getFieldDecorator('name', {
initialValue: item.name,
rules: [
{ required: true, message: '姓名必须填写!' },
],
})(
<Input type="text" />
)}
</FormItem>
<FormItem
label="年龄:"
hasFeedback
{...formItemLayout}
>
{getFieldDecorator('age', {
initialValue: item.age,
rules: [
{ validator: checkNumber },
],
})(
<Input type="text" />
)}
</FormItem>
<FormItem
label="性别:"
hasFeedback
{...formItemLayout}
>
{getFieldDecorator('sex', {
initialValue: item.sex,
rules: [
{ required: true, message: '性别必须填写!' },
],
})(
<Input type="text" />
)}
</FormItem>
<FormItem
label="住址:"
hasFeedback
{...formItemLayout}
>
{getFieldDecorator('address', {
initialValue: item.address,
rules: [
{ required: true, message: '住址必须填写!' },
],
})(
<Input type="address" />
)}
</FormItem>
</Form>
</Modal>
);
};
UserModal.propTypes = {
visible: PropTypes.any,
form: PropTypes.object,
item: PropTypes.object,
onOk: PropTypes.func,
onCancel: PropTypes.func,
};
export default Form.create({
mapPropsToFields(props) {
if (props.type==='create') {
return {
name: {},
age: {},
sex: {},
address: {}
}
}
return {
name: {...props.item.name},
age: {...props.item.age},
sex: {...props.item.sex},
address: {...props.item.address}
}
}
})(UserModal);