昨天解决了一个问题,关于"怎么在解决Modal中嵌套Form表单时,表单项的校验和表单项值获取的问题",详见上一篇文章。
继续在昨天的基础上做开发任务,出现了另外一个问题,在API数据流过程中,怎么数据回传的问题。我的需求是在list页面中会有弹出框Modal,然后在Modal中会嵌入一个组件(比如创建和修改组件),当创建和修改组价表单提交时,经过数据交货,API调用,最后会返回response结果,然后根据response结果来控制Modal的显示/隐藏。因为创建成功,Modal应该消失,创建失败则消失。问题就出现在不知道怎么将response中的数据进行回传。
简单的说,从表单提交到数据返回,应该分为这么几个步骤:
- UI提交表单数据,通过dispatch方法发布action,同时会将参数传递给"namespace/interface"中
- 在module中会定义effects,根据1中的接口名找到对应的接口,然后发起service请求,就是调用services文件中的API接口。
- service中的接口去调用mock中定义服务(mock是antd.pro脚手架中umi提供的一种前后端分离,仅前端实现数据交互的过程)。通过异步的方式generator和yield来调用接口,最后response结果和reducer进行绑定,我们知道reducer是用于修改state状态值的。name数据就更新到state中了。
- 这个时候需要将response中的值返回给上层,即1中的dispatch,这样的话在提交表单的js中就能将操作结果(成功/失败)返回到上一层,即list页面。
- list页面获取到4返回response值,从而控制Modal的显示/隐藏。
下面就是怎么实现的问题了,由于初次接触antd.pro和umi这一套,所以碰到的问题不管大小都会作一下记录。
页面布局
这里先讲一下页面布局,知道布局,可能对后面的实现能更好的理解。
lsit.ts UI页面
由于代码比较多,就不全部粘贴出来了,在说到具体的某一块的时候会再详细粘贴。
使用的是antd.pro提供的ProTable, 是一个几乎是table能涉及的功能,都涵盖全了。
然后在list页面引入了两个Modal,一个是创建,一个是修改,然后通过标志来控制显示/隐藏,拿创建来说吧,代码是这样的
<CreateForm onCancel={() => {
console.log("关闭")
handleModalVisible(false)
}} modalVisible={createModalVisible}
onSubmit={async (value) => {
console.log("value=",value)
if (value === 0) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
/>
在里面模态框中帮我们定义了一些Header,content,footer比较简单。其中难点就是onCancle和onSubmit两个方法。
- 取消的话是直接将模态框关闭,
- 提交的话需要根据response中的code值去控制显示隐藏,成功隐藏,失败不隐藏。
CreateForm.ts UI代码如下:
return (
<Modal
destroyOnClose
title={intl.formatMessage({ id: 'home.user-center.newItem' })}
visible={modalVisible}
onCancel={onCancel}
footer={[
<Button key='submit' type="primary" htmlType="submit" onClick={onFinish}>
{intl.formatMessage({ id: 'home.user-center.operation.save' })}
</Button>,
<Button key='cancel' htmlType="button" onClick={onCancel}>
{intl.formatMessage({ id: 'home.user-center.operation.cancel' })}
</Button>
]}
>
<Form {...layout} form={form} name="control-hooks">
<Form.Item name="username"
label={intl.formatMessage({ id: 'login.username' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'login.message.username' }) }]}>
<Input />
</Form.Item>
<Form.Item name="apikey"
label={intl.formatMessage({ id: 'home.user-center.apikey' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'home.user-center.message.apikey' }) }]}>
<Input />
</Form.Item>
<Form.Item name="name"
label={intl.formatMessage({ id: 'home.user-center.name' })}
rules={[{ required: true, message: intl.formatMessage({ id: 'home.user-center.message.name' }) }]}>
<Input />
</Form.Item>
</Form>
</Modal>
);
可以看出是一个简单的Modal,然后Modal中嵌套的是一个简单的表单。这就是简单的页面构建,了解了页面的结构,就具体说一下一步步是怎么实现的。
在将时下之前,先看一下项目的目录情况:
这是一个使用antd.pro脚手架搭建的一个标准的项目目录。
- config 路由国际化配置,代理配置等
- mock 模拟请求及数据
- .umi umi框架提供的一套服务
- assets 静态资源
- components 公共组件
- e2e 测试
- layouts 项目的布局,比如登录页,主页面等
- locales 国际化
- pages 模块视图
- services 服务接口
只是一些主要的文件,其他的就不列出。进入正菜部分。
1、Modal显示
CreateForm.ts文件以下简称create.ts
const [createModalVisible, handleModalVisible] = useState<boolean>(false);
点击按钮通过handleModalVisible设置显示
create.ts
<Button type="primary" onClick={() => handleModalVisible(true)}>
<PlusOutlined /> {intl.formatMessage({ id: 'home.user-center.operation.create' })}
</Button>
2.提交表单
由于在提交表单的时候,需要回传就需要通过callback函数回传,一般是都过props来获取onCancel, onSubmit。
create.ts
const { modalVisible, onCancel, onSubmit } = props;
点击提交按钮就会通过dispatch发送一个action
create.ts
// 表单提交
const onFinish = async () => {
//用异步的方式去处理Modal中的表单校验和取值问题
const values = await form.validateFields();
const { dispatch } = props;
// 发布事件
dispatch({
type: 'apisetting/addApiconfig',
payload: { ...values },
});
};
3.接收到action,调用service接口
根据dispatch中的type找到对应的函数,就会去对应的modules目录下的create.ts文件中的effects对应的函数
modules/create.ts
effects: {
*addApiconfig({ payload, callback }, { call, put }) {
console.log("payload", payload);
const response = yield call(addApi, payload);
yield put({
type: 'add',
payload: response,
});
// 请求成功
if (response.errorCode === 0) {
message.success(response.errorMessage);
} else {
message.error("Operation Faliure!");
}
},
}
使用的是generator和yield实现异步请求。调用services/create.ts中的接口,并把参数传递过去。
4.在service中调用服务端API
servcies/create.ts
import request from 'umi-request';
export async function addApi(params:object) {
return request('/api/apisetting/add',{
method: "POST",
data: params
});
}
5.在mock中配置API接口和数据
mock/create.ts
// eslint-disable-next-line import/no-extraneous-dependencies
import { Request, Response } from 'express';
import { parse } from 'url';
import { TableListItem, TableListParams } from '@/pages/AccountSettings/data';
// mock tableListDataSource
const getApiList = (current: number, pageSize: number) => {
const tableListDataSource: TableListItem[] = [];
tableListDataSource.push({
key:'1',
username:'chenqk',
apikey:'12iuhi1b2gi859791h5iuh18y',
name:'QCC1'
})
tableListDataSource.push({
key:'2',
username:'zhangsan',
apikey:'678yugfjebbifew545',
name:'QCC2'
})
tableListDataSource.push({
key:'3',
username:'lisi',
apikey:'090firhgirh',
name:'QCC3'
})
tableListDataSource.reverse();
return tableListDataSource;
};
let tableListDataSource = getApiList(1, 100);
function list(req: Request, res: Response, u: string) {
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const { current = 1, pageSize = 10 } = req.query;
const params = (parse(realUrl, true).query as unknown) as TableListParams;
let dataSource = [...tableListDataSource].slice(
((current as number) - 1) * (pageSize as number),
(current as number) * (pageSize as number),
);
const sorter = JSON.parse(params.sorter as any);
if (sorter) {
dataSource = dataSource.sort((prev, next) => {
let sortNumber = 0;
Object.keys(sorter).forEach((key) => {
if (sorter[key] === 'descend') {
if (prev[key] - next[key] > 0) {
sortNumber += -1;
} else {
sortNumber += 1;
}
return;
}
if (prev[key] - next[key] > 0) {
sortNumber += 1;
} else {
sortNumber += -1;
}
});
return sortNumber;
});
}
if (params.filter) {
const filter = JSON.parse(params.filter as any) as {
[key: string]: string[];
};
if (Object.keys(filter).length > 0) {
dataSource = dataSource.filter((item) => {
return Object.keys(filter).some((key) => {
if (!filter[key]) {
return true;
}
if (filter[key].includes(`${item[key]}`)) {
return true;
}
return false;
});
});
}
}
if (params.name) {
dataSource = dataSource.filter((data) => data.name.includes(params.name || ''));
}
const result = {
data: dataSource,
total: tableListDataSource.length,
success: true,
pageSize,
current: parseInt(`${params.currentPage}`, 10) || 1,
};
console.log("res.json(result)",res.json(result));
return res.json(result);
}
function postRule(req: Request, res: Response, u: string, b: Request) {
// alert("req="+req+',res='+res+',u='+u+',b='+b)
let realUrl = u;
if (!realUrl || Object.prototype.toString.call(realUrl) !== '[object String]') {
realUrl = req.url;
}
const body = (b && b.body) || req.body;
const { method, name, desc, key } = body;
switch (method) {
/* eslint no-case-declarations:0 */
case 'delete':
tableListDataSource = tableListDataSource.filter((item) => key.indexOf(item.key) === -1);
break;
case 'post':
(() => {
const i = Math.ceil(Math.random() * 10000);
const newRule = {
key: i,
username: 'nick' + i,
apikey: 'app8' + i,
name: 'app6' + i
};
tableListDataSource.unshift(newRule);
// res = {'erroCode':0}
return res.json(newRule);
})();
return;
case 'update':
(() => {
let newRule = {};
tableListDataSource = tableListDataSource.map((item) => {
if (item.key === key) {
newRule = { ...item, desc, name };
return { ...item, desc, name };
}
return item;
});
return res.json(newRule);
})();
return;
default:
break;
}
const result = {
list: tableListDataSource,
pagination: {
total: tableListDataSource.length,
},
};
res.json({errorCode:0,errorMessage:'Operation Successfully!'});
}
export default {
'GET /api/apisetting/list': list,
// 'POST /api/rule': postRule,
'POST /api/apisetting/add ': postRule
};
由于后期需要根服务端对接接口,这里只是为了测试用的,测试整个项目的数据流向是否正确,所以我在最后简单的返回了一个errorCode和errorMessage,以便测试。
由于这是umi提供的,在保存后会自动编译,找不到对应的源码,好像是log都打不出来。
6.数据回溯
在1-5步中我们实现了从UI->module->service->mock的数据流向,那现在服务器收到了我们的请求,并且也response了,我们怎么将数据就行回传呢?
我们知道请求是在module的时候作为入口的,那么response应该是返回到module中。
*addApiconfig({ payload, callback }, { call, put }) {
console.log("payload", payload);
const response = yield call(addApi, payload);
yield put({
type: 'add',
payload: response,
});
// 请求成功
if (response.errorCode === 0) {
message.success(response.errorMessage);
} else {
message.error("Operation Faliure!");
}
},
}
yield回来的数据,我们都存放到response中了,并且将请求回来的数据通过put的方式绑定到reducer的add中,从而去更新数据。
reducers: {
add(state, { payload }) {
return {
...state,
errorCode: payload.errorCode,
list: payload.list
};
},
}
7.表单create.ts的dispatch回调函数
这个是我碰到的核心问题,问题倒是不难,就是初次使用的时候,没有好好的看API,导致遗漏。在提交表单的时候使用dispatch的方式去发布action,它还提供了一个callback回调函数,我想碰到回调函数就应该知道怎么处理了。
// 表单提交
const onFinish = async () => {
//用异步的方式去处理Modal中的表单校验和取值问题
const values = await form.validateFields();
const { dispatch } = props;
// 发布事件
dispatch({
type: 'apisetting/addApiconfig',
payload: { ...values },
callback: response => {
console.log("reponse=", response)
}
});
};
在dispatch中加上一个callback函数,即可在调用函数的时候通过callback将response结果回传过来。
8.在module中使用callback函数回传response
*addApiconfig({ payload, callback }, { call, put }) {
console.log("payload", payload);
const response = yield call(addApi, payload);
yield put({
type: 'add',
payload: response,
});
// 请求成功
if (response.errorCode === 0) {
message.success(response.errorMessage);
} else {
message.error("Operation Faliure!");
}
callback(response)
},
就是最后那行代码callback(response),可以将我们的请求结果返回到表单处。
9.通过callback函数将子组件的值传个父组件
list页面是父组件,create.ts表单为子组件。我们知道父组件往子组件传值通过props,子组件往父组件传值通过callback函数去传值。那只要调用一下onSubmit方法即可。
// 表单提交
const onFinish = async () => {
//用异步的方式去处理Modal中的表单校验和取值问题
const values = await form.validateFields();
const { dispatch } = props;
// 发布事件
dispatch({
type: 'apisetting/addApiconfig',
payload: { ...values },
callback: response => {
console.log("reponse=", response)
onSubmit(response.errorCode)
}
});
};
10.在list页面接受回传值
<CreateForm onCancel={() => {
console.log("关闭")
handleModalVisible(false)
}} modalVisible={createModalVisible}
onSubmit={async (value) => {
console.log("value=",value)
if (value === 0) {
handleModalVisible(false);
if (actionRef.current) {
actionRef.current.reload();
}
}
}}
/>
测试一下结果是不是能正常的返回。
- payload是往接口传递的参数
- response是接口返回的参数
- value值是最终返回到list页面的值
这样,从UI请求到请求返回,这一套数据流向终于弄明白了。