Appearance
useAsyncFaaS
异步调用云函数,并轮询异步云函数的执行状态,直至任务完成或者任务超时
常见的使用场景有:数据导出、批量数据处理(如批量审核)
使用
@ifanrx/faas
需要1.2.0
及以上的版本@ifanrx/dashboard
需要1.2.0
及以上的版本- 在应用内新增
faas_task
数据表,详见 faas-task 数据表结构
实现原理
dashboard 端
在使用 useAsyncFaaS
请求云函数时,会在本地随机生成一个 task_id
作为本次任务的索引,并且在云函数的请求参数中插入 _taskId
和 _faasName
,使用异步形式调用云函数
异步云函数调用完成之后,使用 retry 创建一个轮询器,每隔一段时间使用 task_id
查询 faas_task
,直至 faas_task
的状态变为 success
或者 failure
,或者云函数超时(异步云函数的超时时间为 300s)
faas 端
@ifanrx/faas
的 createFaaS
在接收到异步请求时,会创建 faas_task
,用于标记该异步任务
createFaaS
做了以下工作:
- 记录当前异步任务的 jobId(event.jobId)、task_id、faas_name、调用者
- 使用 AES 加密本次请求使用的参数,并存储到 payload 中。加密密钥用的是 event.signKey
- 使用上述信息创建一个状态为
pending
的faas_task
- 开始执行云函数内容直至云函数执行完成或失败
- 若云函数执行完成,则将
faas_task
的状态更新为success
,并且将云函数执行结果存储到faas_task
的 result 字段中 - 若云函数执行失败,则将
faas_task
的状态更新为failure
,并且将云函数执行错误信息存储到faas_task
的 result 字段中
- 若云函数执行完成,则将
参数
Name | Type | Default value | Description |
---|---|---|---|
faas | Function | undefined | createIO 生成的 faas 函数,如: io.faas.exportData |
options | Object | {} | |
options.onError | undefined | (taskId : undefined | string , error : Error ) => void | undefined | 异步云函数调用失败或执行失败后触发 |
options.onInvoked | undefined | (taskId : string ) => void | undefined | 异步云函数调用后触发,此时未开始轮询任务状态 |
options.onStart | undefined | (taskId : string ) => void | undefined | 开始轮询异步云函数状态时触发 |
options.onSuccess | undefined | (taskId : string , data : object ) => void | undefined | 异步云函数执行成功后触发 |
options.preserve | undefined | boolean | false | 是否在组件注销时保存任务状态,在组件下次挂载时恢复任务轮询 |
返回值
Object
Name | Type |
---|---|
invoke | (any : any ) => Promise <any > |
isInvoking | boolean |
tasks | { reset : () => void ; status : string ; stop : () => void ; taskId : string }[] |
invoke: (any
: any
) => Promise
<any
>
-
isInvoking: boolean
-
tasks: { reset
: () => void
; status
: string
; stop
: () => void
; taskId
: string
}[]
-
源码
示例
在 @ifanrx/dashboard Table 中导出选中行
TIP
本示例未启用 preserve,在组件注销时会清空所有轮询任务,若组件注销时任务未完成,也不会再触发 onSuccess/onError
jsx
export default function ExportableTable() {
const {message, notification} = App.useApp()
const {invoke: exportData, isInvoking} = useAsyncFaaS(io.faas.exportData, {
onInvoked: taskId => {
message.info(`导出中,稍后可从弹窗获取导出结果。任务 id: ${taskId}`)
},
onError: (_, error) => {
notification.error({
message: '导出失败',
description: error.displayMessage || error.message,
duration: null,
})
},
onSuccess: (taskId, {downloadLink}) => {
window.open(downloadLink)
notification.success({
message: `导出成功 (${taskId})`,
description: (
<>
即将开始自动下载导出文件,若下载失败,可
<a href={downloadLink} rel="noreferrer" target="_blank">
点击这里
</a>
重试
</>
),
duration: null,
})
},
})
return (
<Table
rowSelection={{
preserveSelectedRowKeys: true,
}}
toolBarRender={(_, {selectedRows}) => [
<AsyncButton
key="export"
size="middle"
type="primary"
onClick={() =>
exportData({
queryConfig: {},
excelConfig: {
rows: selectedRows,
},
})
}
>
{isInvoking ? '导出中' : '导出'}
</AsyncButton>,
]}
/>
)
}
开启 preserve
jsx
export default function ExportableTable() {
const {message, notification} = App.useApp()
const {invoke: exportData, isInvoking} = useAsyncFaaS(io.faas.exportData, {
onInvoked: taskId => {
message.info(`导出中,稍后可从弹窗获取导出结果。任务 id: ${taskId}`)
},
// 组件注销时暂停任务状态轮询,下次重新挂载该组件时恢复轮询
preserve: true,
onError: (_, error) => {
notification.error({
message: '导出失败',
description: error.displayMessage || error.message,
duration: null,
})
},
onSuccess: (taskId, {downloadLink}) => {
window.open(downloadLink)
notification.success({
message: `导出成功 (${taskId})`,
description: (
<>
即将开始自动下载导出文件,若下载失败,可
<a href={downloadLink} rel="noreferrer" target="_blank">
点击这里
</a>
重试
</>
),
duration: null,
})
},
})
return (
<Table
rowSelection={{
preserveSelectedRowKeys: true,
}}
toolBarRender={(_, {selectedRows}) => [
<AsyncButton
key="export"
size="middle"
type="primary"
onClick={() =>
exportData({
queryConfig: {},
excelConfig: {
rows: selectedRows,
},
})
}
>
{isInvoking ? '导出中' : '导出'}
</AsyncButton>,
]}
/>
)
}
用于导出数据的按钮
TIP
本示例包含了导出数据配置的 jsdoc,已内置到脚手架模板内,可复制到项目中使用
jsx
/**
*
* @param {ExportButtonProps & import('antd').ButtonProps} props
* @return {import('react').ReactElement}
*/
export default function ExportButton({onExport, ...buttonProps}) {
const {message, notification} = App.useApp()
const {invoke, isPending} = useAsyncFaaS(io.faas.exportData, {
onInvoked: taskId => {
message.info(`导出中,稍后可从弹窗获取导出结果。任务 id: ${taskId}`)
},
onError: (_, error) => {
notification.error({
message: '导出失败',
description: error.displayMessage || error.message,
duration: null,
})
},
onSuccess: (taskId, {download_link: downloadLink}) => {
window.open(downloadLink)
notification.success({
message: `导出成功 (${taskId})`,
description: (
<>
即将开始自动下载导出文件,若下载失败,可
<a href={downloadLink} rel="noreferrer" target="_blank">
点击这里
</a>
重试
</>
),
duration: null,
})
},
preserve: true,
})
const exportData = async () => {
const exportConfig = onExport()
await invoke(exportConfig)
}
return (
<Button {...buttonProps} loading={isPending} onClick={exportData}>
{isPending ? '导出中' : '导出'}
</Button>
)
}
/**
* @typedef ExportButtonProps
* @prop {() => ExportDataOptions} onExport
*/
/**
* @typedef ExportDataOptions
* @prop {ExcelConfig} excelConfig
* @prop {QueryConfig} queryConfig 与 io.table.find 参数类似,需指定 table
* @prop {string} [categoryId] 知晓云文件分类 ID
*/
/**
* @typedef QueryConfig
* @prop {string} table 数据表名称,snake_case,如:prize_log、lottery_log
* @prop {import('./io/types/baas').default.Query} query
* @prop {string[] | string} orderBy
* @prop {string[] | string} expand
* @prop {string[] | string} select
*/
/**
* @typedef ExcelConfig
* @prop {object[] | Array[]} [rows] 指定导出的数据行,为空时需配置 queryConfig 用于自动获取源数据。支持 Object[] 以及 Array[]
* @prop {[key: string, label: string, options: ColOptions][]} cols 数据列配置,二维数组,item[0] 为每列要取的的字段,item[1] 为列标题,item[2] 为扩展配置 例:['title', '标题', {}]
* @prop {string} [title] 大标题
* @prop {string} [sheetName] 工作表名称
*/
/**
* defaultValue, renderIndex, template 只能同时存在一个
* @typedef ColOptions
* @prop {any} defaultValue
* @prop {object} mapping 枚举值映射表
* @prop {string} template 支持插值,如 '{{provinceName}}{{cityName}}{{countyName}}{{detailInfo}}'
* @prop {boolean | string} renderIndex 使用数据项序号填充该列,使用 renderIndex 时 field 需配置为空字符串或者 undefined
* @prop {number} indexStartFrom 使用 renderIndex 时,指定序号的起始值,默认从 1 开始
* @prop {{operation: 'add' | 'sub', fields: string[]}} calculation 支持两个字段的运算
* @prop {string} dateFormatter 格式化时间戳
*/
附录
faas_task 数据表结构
Details
json
{
"name": "faas_task",
"description": null,
"row_read_perm": ["user:{created_by}"],
"row_write_perm": [],
"write_perm": [],
"schema": {
"fields": [
{
"name": "id",
"type": "id",
"description": "id"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "job_id",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": "event.jobId"
},
{
"acl": {
"clientVisible": false,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "payload",
"type": "object",
"constraints": {
"rules": [],
"required": false
},
"description": "加密过的请求数据"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "task_id",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": "任务 id"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "faas_name",
"type": "string",
"constraints": {
"rules": [],
"required": false
},
"description": "云函数名称"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "status",
"type": "string",
"default": "running",
"constraints": {
"rules": [
{
"type": "choices",
"value": ["running", "success", "failure"],
"remark": ["云函数运行中", "云函数执行成功", "云函数执行失败"]
}
],
"required": false
},
"description": "云函数执行状态"
},
{
"acl": {
"clientVisible": true,
"clientReadOnly": false,
"creatorVisible": false
},
"name": "result",
"type": "object",
"constraints": {
"rules": [],
"required": false
},
"description": "执行结果/错误信息,都以 object 形式存储"
},
{
"name": "created_by",
"type": "integer",
"description": "created_by"
},
{
"name": "created_at",
"type": "integer",
"description": "created_at"
},
{
"name": "updated_at",
"type": "integer",
"description": "updated_at"
}
]
}
}