Skip to content

useAsyncFaaS

异步调用云函数,并轮询异步云函数的执行状态,直至任务完成或者任务超时

常见的使用场景有:数据导出、批量数据处理(如批量审核)

使用

  1. @ifanrx/faas 需要 1.2.0 及以上的版本
  2. @ifanrx/dashboard 需要 1.2.0 及以上的版本
  3. 在应用内新增 faas_task 数据表,详见 faas-task 数据表结构

实现原理

dashboard 端

在使用 useAsyncFaaS 请求云函数时,会在本地随机生成一个 task_id 作为本次任务的索引,并且在云函数的请求参数中插入 _taskId_faasName,使用异步形式调用云函数

异步云函数调用完成之后,使用 retry 创建一个轮询器,每隔一段时间使用 task_id 查询 faas_task,直至 faas_task 的状态变为 success 或者 failure,或者云函数超时(异步云函数的超时时间为 300s)

faas 端

@ifanrx/faascreateFaaS 在接收到异步请求时,会创建 faas_task,用于标记该异步任务

createFaaS 做了以下工作:

  • 记录当前异步任务的 jobId(event.jobId)、task_id、faas_name、调用者
  • 使用 AES 加密本次请求使用的参数,并存储到 payload 中。加密密钥用的是 event.signKey
  • 使用上述信息创建一个状态为 pendingfaas_task
  • 开始执行云函数内容直至云函数执行完成或失败
    • 若云函数执行完成,则将 faas_task 的状态更新为 success,并且将云函数执行结果存储到 faas_task 的 result 字段中
    • 若云函数执行失败,则将 faas_task 的状态更新为 failure,并且将云函数执行错误信息存储到 faas_task 的 result 字段中

参数

NameTypeDefault valueDescription
faasFunctionundefinedcreateIO 生成的 faas 函数,如: io.faas.exportData
optionsObject{}
options.onErrorundefined | (taskId: undefined | string, error: Error) => voidundefined异步云函数调用失败或执行失败后触发
options.onInvokedundefined | (taskId: string) => voidundefined异步云函数调用后触发,此时未开始轮询任务状态
options.onStartundefined | (taskId: string) => voidundefined开始轮询异步云函数状态时触发
options.onSuccessundefined | (taskId: string, data: object) => voidundefined异步云函数执行成功后触发
options.preserveundefined | booleanfalse是否在组件注销时保存任务状态,在组件下次挂载时恢复任务轮询

返回值

Object

NameType
invoke(any: any) => Promise<any>
isInvokingboolean
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 }[]

-


源码

use-async-faas.js

示例

在 @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"
      }
    ]
  }
}