第七课之dva以及前后端交互
dva介绍
dva 首先是一个基于 redux 和 redux-saga 的数据流方案,然后为了简化开发体验,dva 还额外内置了 react-router 和 fetch,所以也可以理解为一个轻量级的应用框架。
数据流向
数据的改变发生通常是通过用户交互行为或者浏览器行为(如路由跳转等)触发的,当此类行为会改变数据的时候可以通过 dispatch 发起一个 action,如果是同步行为会直接通过 Reducers 改变 State ,如果是异步行为(副作用)会先触发 Effects 然后流向 Reducers 最终改变 State,所以在 dva 中,数据流向非常清晰简明,并且思路基本跟开源社区保持一致(也是来自于开源社区)
如图:
疑问
刚开始学react代码的时候总会想,这种全局数据流有什么用,直接ajax请求响应,操作页面元素,一步走到头,清晰明了,这些数据流并没有什么卵用
react数据流
- 1.react页面渲染都是通过修改state进行页面渲染的,而state只能控制当前页面
- 2.使用react代码开发,要抛弃之前ajax的理念(通过数据以及操作dom进行页面调整),使用state、props数据流进行页面开发,如果直接操作dom,不更新state在没玩明白数据流的情况下可能会出问题,需要时间去积累经验
- 3.这时候当前组件想要操作别的组件的state尤为困难,这时候就需要全局数据流方案redux
什么是redux
Redux 是 JavaScript状态容器,提供可预测化的状态管理,可以让你构建一致化的应用,运行于不同的环境(客户端、服务器、原生应用),并且易于测试
为什么用Redux
因为对于react来说,同级组件之间的通信尤为麻烦,或者是非常麻烦了,所以我们把所有需要多个组件使用的state拿出来,整合到顶部容器,进行分发
redux实现了什么
在react的数据交互理念中,只能进行父子组件的通信,无法想和谁通信就和谁通信,redux做到了将数据传递添加到了全局,任何地方都可以进行接收使用。
将需要修改的state都存入到store里,发起一个action用来描述发生了什么,用reducers描述action如何改变state树。创建store的时候需要传入reducer,真正能改变store中数据的是store.dispatch API
dva配置项
const app = dva({
history, // 指定给路由用的 history,默认是 hashHistory
initialState, // 指定初始数据,优先级高于 model 中的 state
onError, // effect 执行错误或 subscription 通过 done 主动抛错时触发,可用于管理全局出错状态。
onAction, // 在 action 被 dispatch 时触发
onStateChange, // state 改变时触发,可用于同步 state 到 localStorage,服务器端等
onReducer, // 封装 reducer 执行。比如借助 redux-undo 实现 redo/undo
onEffect, // 封装 effect
onHmr, // 热替换相关
extraReducers, // 指定额外的 reducer,比如 redux-form 需要指定额外的 form reducer
extraEnhancers, // 指定额外的 StoreEnhancer ,比如结合 redux-persist 的使用
});
Models
如果对于Redux的action,reducer写法很熟悉的同学一定意识到,以往(即使是Redux的官方例子中)会在src目录下,新建reducers以及actions目录用于整体存放reducer和action,针对不同的功能模块还需要分别在这两个目录下设置子目录或子文件区分。
但dva的model完美的让某一个模块的状态池(对应某一个模块),action和reducer同时在一个js文件中维护,这样整个应用的目录结构会清晰和简洁很多,但也因此在dva项目中发起的任何一个action,type中必须包含对应model的命名空间(namespace)
import * as addbanner from '../services/addbanner';
export default {
namespace: 'addbanner', // 命名空间
state: {},
subscriptions: { // 监听
setup({ history, dispatch }, onError) {
}
},
effects: {
//call:执行异步函数
//put:发出一个 Action,更新store,类似于 dispatch
*bannerlist(action, { call, put }) {
const testRes = yield call(addbanner.bannerlist, action.params);
yield put({
type: 'test',
payload: {
bannerlistRes: testRes
},
});
return testRes;
},
reducers: { // 接收数据并改变state,然后进行返回,命名要和effects里的函数type一致
test(state, { payload }) {
return {
...state,
...payload,
};
}
},
};
State
State 表示 Model 的状态数据,通常表现为一个 javascript 对象(当然它可以是任何值);操作的时候每次都要当作不可变数据(immutable data)来对待,保证每次都是全新对象,没有引用关系,这样才能保证 State 的独立性,便于测试和追踪变化。
Action
Action 是一个普通 javascript 对象,它是改变 State 的唯一途径。无论是从 UI 事件、网络回调,还是 WebSocket 等数据源所获得的数据,最终都会通过 dispatch 函数调用一个 action,从而改变对应的数据。action 必须带有 type 属性指明具体的行为,其它字段可以自定义,如果要发起一个 action 需要使用 dispatch 函数;需要注意的是 dispatch 是在组件 connect Models以后,通过 props 传入的。
dispatch 函数
dispatching function 是一个用于触发 action 的函数,action 是改变 State 的唯一途径,但是它只描述了一个行为,而 dipatch 可以看作是触发这个行为的方式,而 Reducer 则是描述如何改变数据的。
在 dva 中,connect Model 的组件通过 props 可以访问到 dispatch,可以调用 Model 中的 Reducer 或者 Effects,常见的形式如
dispatch({
type: 'user/add', // 格式:namespace(models的命名空间)/function(effects的方法名)
payload: {}, // 需要传递的信息
});
Reducer
Reducer(也称为 reducing function)函数接受两个参数:之前已经累积运算的结果和当前要被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。
Effect
Effect 被称为副作用,在我们的应用中,最常见的就是异步操作。它来自于函数编程的概念,之所以叫副作用是因为它使得我们的函数变得不纯,同样的输入不一定获得同样的输出。
Subscription
Subscription 语义是订阅,用于订阅一个数据源,然后根据条件 dispatch 需要的 action。数据源可以是当前的时间、服务器的 websocket 连接、keyboard 输入、geolocation 变化、history 路由变化等等。
// 比如:当用户进入 /users 页面时,触发action users/fetch 加载用户数据。
subscriptions: {
setup({ dispatch, history }) {
history.listen(({ pathname }) => {
debugger
if (pathname === '/primary') {
dispatch({
type: 'primary/getmerProClassList',
});
}
})
}
}
dva | redux |
---|---|
引用dva | 要引入多个库,项目结构复杂 |
实现一个异步交互修改的文件很少 | 实现一个异步交互需要修改的文件太多,容易出错 |
使用方式清晰、便捷 | 使用方式复杂、繁琐 |
Router
这里的路由通常指的是前端路由,由于我们的应用现在通常是单页应用,所以需要前端代码来控制路由逻辑,通过浏览器提供的 History API 可以监听浏览器url的变化,从而控制路由相关操作。
import { Router, Route } from 'dva/router'; // 引用路由
import dynamic from 'dva/dynamic'; // 路由按需加载
const RouterWrapper = ({ history, app }) => {
const Primary = dynamic({
app,
models: () => [
import('./models/primary'), // 使用对应的models
],
component: () => import('./components/Primary')
});
return (
app.router(({history}) =>
<Router history={history}>
<Route path="/" component={HomePage} />
</Router>
);
);
});
dva的使用
安装
npm install dva
- 项目index.js
import 'babel-polyfill';
import dva from 'dva';
import createLoading from 'dva-loading';
import createHistory from 'history/createBrowserHistory';
// import createHistory from 'history/createHashHistory';
import './assets/common.less';
if (module && module.hot) {
module.hot.accept()
}
import { default as miniProgramRemain } from './models/miniProgramRemain';
// =======================
// 1. Initialize
// =======================
const app = dva({
history: createHistory(),
onError(e, dispatch) {
},
});
app.model(miniProgramRemain);
// =======================
// 2. Plugins
// =======================
app.use(createLoading());
// =======================
// 3. Model
// =======================
// Moved to router.js
// =======================
// 4. Router
// =======================
app.router(require('./Router'));
// =======================
// 5. Start
// =======================
app.start('#app');
- 1.创建页面组件,文件以及组件命名大驼峰,开发相应的逻辑以及UI界面
- 2.添加路由,按项目规则命名(eg:列表页面:/list,添加列表页面:/list/listAdd)
- 3.定义model
- 4.定义service以及api地址
- 5.连接model和组件
1.创建页面组件Primary.js
里面开发对应的页面逻辑以及UI
import React, { Component } from 'react'
export default class Primary extends Component {
render() {
return (
<div>
Primary
</div>
)
}
}
2.添加路由
注意:需要在福禄管家将对应菜单添加到自己的商户应用下,否则系统会进行控制,也是无法打开
const Primary = dynamic({
app,
models: () => [
import('./models/primary'),
],
component: () => import('./components/Primary')
});
// 使用
<Route exact path="/primary" render={(props) => WraperRouter(props, Primary)} />
3.创建model文件primary.js
import * as primary from '../services/primary';
export default {
namespace: 'primary',
state: {},
effects: {
*saveOneProClass({ payload, callback }, { call, put }) { // 保存
const testRes = yield call(primary.saveOneProClass, payload);
yield put({
type: 'success',
payload: {
saveOneProClassResult: testRes
}
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
*deleteProClass({ payload, callback }, { call, put }) { // 删除
const testRes = yield call(primary.deleteProClass, payload);
yield put({
type: 'success',
payload: {
deleteProClassResult: testRes
}
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
*getmerProClassList({ payload, callback }, { call, put }) { // 查询
const testRes = yield call(primary.getmerProClassList, payload);
yield put({
type: 'test',
payload: {
getmerProClassListRes: testRes
},
});
if (callback instanceof Function) {
callback(testRes);
}
return testRes;
},
},
reducers: {
success (state, {payload}) {
return {
...state,
...payload,
};
}
}
}
4.创建service文件primary.js
请求的地址都从service.js进行传递
// 引入axios组件
import axios from '../utils/axios';
// 引用项目api地址
import Api from '../configs/api';
// get请求
export function getmerProClassList(params) {
return axios.get(configs.host.test + Api.getmerProClassList, { 'params': params });
}
// post请求
export function saveOneProClass(params) {
return axios.post(configs.host.test + Api.saveOneProClass,params);
}
export function deleteProClass(params) {
return axios.get(configs.host.test + Api.deleteProClass, { 'params': params });
}
// put请求
// export function modifyMembershipInfo(params) {
// return axios.put(configs.host.test + // Api.modifyMembershipInfo,params);
// }
// delete请求
// export function guessYouLikeDelete(params) {
// return axios.delete(configs.host.test + Api.guessYouLike, { // 'params': params })
// }
axios.js
// axios.js用于处理数据请求响应(添加请求header,返回数据异常捕获等)
import axios from 'axios';
import { message } from 'antd';
import { handleErrCallBack } from 'fl-pro';
// axios.defaults.baseURL = ''; API 域。默认值:当前域
axios.defaults.withCredentials = true; // 允许跨域且携带 Cookie(或自定义头)。默认值:false
axios.defaults.timeout = 30000; // 设置请求超时时间(ms)不超过半分钟
axios.defaults.headers.common['Authorization'] = ''; // 携带的自定义头
axios.defaults.headers.post['Content-Type'] = 'application/json'; // 设置请求提内容类型,其他可选值:application/x-www-form-urlencoded
axios.interceptors.request.use(config => {
// console.log('【request】', config);
config.headers["Authorization"] = `Bearer ${localStorage.getItem('access_token')}`;
config.headers["MerchantId"] = localStorage.getItem('MerchantId');
return config;
}, error => {
// console.log('【request error】', error);
return Promise.reject(error);
});
axios.interceptors.response.use(response => {
return response.data;
},handleErrCallBack
);
export default axios;
5.api.js添加
(查看后端服务地址进行配置)
getmerProClassList: '/api/product/GetMerProClassList',// 获取商户商品分类 //商品一级分类
saveOneProClass: '/api/product/SaveOneProClass', //添加编辑一级分类保存接口
deleteProClass: '/api/product/DeleteProClass', //删除分类
6.连接model和组件
// 方式一:直接在引用的地方连接
const Primary = dynamic({
app,
models: () => [
import('./models/primary'),
],
component: () => import('./components/Primary')
});
// 方式二:单页应用入口文件index.js添加引用
import { default as primary } from './models/primary';
app.model(primary);
// 区别: 在入口文件注册的model文件,可以在项目任何一个路由页面进行使用,而在单个路由注册的model文件只能在该页面使用,别的页面无法使用你的model
- 流程图
列表增删查改
开发流程
1.页面组件的定义
- 页面的基础搭建
2.state定义
- 定义查询的postData
- 定义搜索的配置项searchConfig(使用SearchForm组件库)
- 定义selectedRowKeys(用于批量删除)
- 定义弹窗的开发关闭showModal
3.事件的定义
- getData:异步请求查询函数
- search:查询获取表单数据更新postData的函数并调用查询函数
- batchDeleteInfo:批量删除获取点击的选中行并调用删除函数
- deleteInfo:异步请求删除函数
4.render的定义
- 获取state
- 设置table的列配置、分页配置、多选
- html加载面包屑、查询组件、新增按钮、批量删除按钮、表格、弹窗子组件
5.页面导出并连接全局数据流
流程如图:
交互步骤
查询交互:点击查询获取查询数据->调用search查询组装数据->调用getData公用函数->获取返回数据->修改state,渲染页面
新增编辑交互:点击父组件操作功能,调用弹窗->表单数据完成,并按后端要求组装好数据,提交新增、编辑->获取返回数据->调用查询,关闭弹窗
删除交互:点击删除->确认删除交互->获取返回数据->调用查询
核心代码查看
- 查询
// 查询公用函数
getData = () => {
const { postData } = this.state;
this.props.dispatch({ type: 'primary/getmerProClassList', payload: { ...postData } }).then((res) => {
const { code, data } = res;
if (code === '0') {
return this.setState({
tableData: data.list,
total: data.total
});
}
message.error(res.message);
});
}
// 处理表单values并调用,查询
search = (err, value) => {
//查询列表
const { postData } = this.state;
postData.pageIndex = 1;
this.setState({
postData: { ...postData, ...value }
}, () => {
this.getData();
})
}
this.state = {
postData: {
pageIndex: 1,
pageSize: 10,
},
};
- 新增、编辑
// 子组件弹窗,新增编辑一级分类代码
sureAdd = () => {
const { uploadFile } = this.state;
this.props.form.validateFields((err, values) => {
if (!err) {
const { iconPath, iconPathEdit, detailInfo } = this.state;
let nowImgUrl = '';
if (detailInfo.id) {
values.id = detailInfo.id;
// 如果用户上传了或者删除了
if (iconPathEdit) {
nowImgUrl = iconPath ? iconPath.response.data : '';
}
else {
nowImgUrl = detailInfo.iconPath;
}
} else {
nowImgUrl = iconPath ? (iconPath.flag ? iconPath.fileList[0].url :
iconPath.response.data) : '';
}
if (!nowImgUrl) {
return message.error('请选择一级分类图!');
}
values.iconPath = nowImgUrl;
this.props.dispatch({
type: 'primary/saveOneProClass',
payload: values,
callback: ({ code, message: info }) => {
if (code === '0') {
this.props.getData();
this.handleClose();
} else {
message.error(info);
}
}
});
}
});
}
// 图片上传的回调
updateImg = (filed, value, fieldEdit) => {
this.setState({
[filed]: value,
[fieldEdit]: true
});
}
// 封装的上传图片组件
<UploadImg
label="一级分类图"
field="iconPath"
updateImg={this.updateImg}
rowInfo={detailInfo}
imgSize={10000}
formItemLayout={{
labelCol: { span: 8 },
wrapperCol: { span: 14 },
}}
/>
- 删除
// 批量删除
batchDeleteInfo = () => {
let { selectedRowKeys } = this.state;
let ids = selectedRowKeys.join(',');
this.deleteInfo(ids);
}
// 删除公用方法
deleteInfo = (ids) => {
confirm({
title: '删除确认',
content: '确定要删除所选数据吗?',
okText: '确认',
centered: true,
cancelText: '取消',
onOk: () => {
this.props.dispatch({
type: 'primary/deleteProClass', payload: { ids },
callback: ({ code, data, message: info }) => {
if (code === '0') {
message.success(info);
this.setState({
selectedRowKeys: []
})
this.getData();
} else if (code === '-1') {
message.error(info);
}
}
});
},
onCancel() {
},
});
}
- 父子组件传值
{showModal && <ShowModal hideFildModel={this.showModal} getData={this.getData} rowInfo={rowInfo} />}
新增、编辑、删除成功后都需进行查询
如何使用dva全局数据
观察代码发现我们使用返回值要么用的是.then,要么用的callback回调函数,如何使用dva数据流处理页面数据或者做某些事
例如我们在子组件新增、编辑成功后,可以在父组件接受,子组件的成功行为做操作
// 去除子组件的callback获取返回值的方法
this.props.dispatch({
type: 'primary/saveOneProClass',
payload: param,
// callback: ({ code, message: info }) => {
// if (code === '0') {
// this.props.getData();
// this.handleClose();
// } else {
// message.error(info);
// }
// }
});
// 父组件通过result接收数据进行比较,做修改
componentWillReceiveProps(nextProps) {
const { saveOneProClassResult } = nextProps.primary;
if (saveOneProClassResult !== this.props.primary.saveOneProClassResult) {
const { code, data } = saveOneProClassResult;
if (code === '0') {
this.getData();
this.showModal('showModal', false);
} else {
message.error(saveOneProClassResult.message);
}
}
}