Skip to content

服务请求 - createIO

集成知晓云表操作(读/写数据表)、云函数调用、后端接口调用,并且引入了 @tanstack/vue-query(下文简称 vue-query),能够满足更多场景的使用。

简介

  • 将表操作、云函数调用、接口调用统一封装到 io 内,这样做的好处:

    • 统一管理 表、云函数、API,避免分散在项目各地,管理混乱
    • 使用 io.faas.cloudFunctionName 代替 BaaS.invoke('cloudFunctionName') ,从而减少魔法值的使用
  • 增加 TypeScript 类型声明,可以更加便捷高效地调用 io

    • image
  • vue-query 简易封装

  • 新增 IOError,所有的 io operation 执行错误时都会抛出统一的错误对象,方便错误收集上报以及错误信息提示

主要功能

使用 createIO 定义 io 实例

TIP

table、faas、api 的 key 必须为 camelCase(小写驼峰式)

js
import {createIO} from '@ifanrx/uni-mp'

export default createIO({
  table: {
    userprofile: '_userprofile',
    settings: 'settings',
    likesLog: 'likes_log',
  },
  api: {
    getUserprofile: {
      method: 'POST',
      url: '/xxx',
    },
  },
  faas: {
    helloWorld: 'hello_world',
  },
})

基础 io 功能

基础功能包括文件上传、服务器时间获取、vue-query 的集成等

js
// 构造查询实例以及数据查询
const query = io.query.compare('key', '=', 'something')

io.activity.find({query})

// 服务器时间获取
const serverDate = await io.useServerDate()

// 文件上传
io.uploadFile(file, options)

// 使用 `vue-query`
const {isFetching, data: userprofile} = io.useQuery(() => io.userprofile.find())

点击查看更多基础功能

读写知晓云数据表

主要包括了 get/find/first/update/updateMany/delete/deleteMany/create/createMany/count 等方法,满足各种场景下的数据表读写需求

支持通过 io[tableName][method] 方法来读写数据表,如 io.userprofile.get({id: userId})表示获取 userprofile 表内 id 为 userId 的数据项

点击查看更多数据表读写方法

云函数调用

对知晓云 sdk 的 BaaS.invoke 进行封装,省略了调用 invoke 时对 functionName 参数的传入,让云函数的调用更加便捷,同时支持代码提示

当云函数响应的 code 为 0 时(此时云函数执行成功),返回云函数响应结构中的 data 字段,否则会抛出 IOError

查看源码
js
Object.entries(cloudFunctions).forEach(([functionKey, functionName]) => {
  const invoke = (payload, sync = true) => {
    return uni.BaaS.invoke(functionName, payload, sync)
      .then(response => {
        if (!sync) {
          return response
        }
        const {data, code, error} = response
        if (code !== 0) {
          if (typeof error.message === 'string') {
            throw new IOError({message: error.message})
          }
          const {
            error_msg: message,
            error_code: code,
            display_error_msg: displayMessage,
            jobId,
          } = error.message
          throw new IOError({
            code,
            message,
            displayMessage,
            additionalProps: {jobId},
          })
        }

        return data
      })
      .catch(handleIOError)
  }

  invoke.queryKey = ['faas', functionKey]
  safeSet(io.faas, functionKey, invoke)
})
js
const io = createIO({
  faas: {
    helloWorld: 'hello_world',
  },
})

// 默认使用同步的形式调用云函数
io.faas.helloWorld({message: 'hello world'})

// 使用异步的形式调用云函数
io.faas.helloWorld({message: 'hello world'}, false).then(console.log) // {status: 'ok'}

后端接口调用

此处的封装仅对内部接口生效,后端接口请求支持与云函数一致的调用方式,同样支持代码提示
在接口调用失败或者 response.data.status === 'error' 时抛出 IOError

查看源码
js
Object.entries(apis).forEach(([apiKey, {url, method = 'GET'}]) => {
  const request = ensureLogin((params, options) => {
    return uni.BaaS.request({
      url: formatUrl(url, params),
      method,
      data: params,
      ...options,
    })
      .then(res => {
        if (isUndef(res.data)) {
          return undefined
        }

        const {statusCode} = res

        const {
          data,
          status,
          error_code: code,
          error_msg: message,
          display_error_msg: displayMessage,
        } = res.data

        if (status === 'error') {
          throw new IOError({
            code,
            message,
            displayMessage,
          })
        } else if (statusCode >= 400 && statusCode < 600) {
          throw new IOError({code: statusCode})
        }

        return data || res.data
      })
      .catch(handleIOError)
  })

  request.queryKey = ['api', apiKey]
  safeSet(io.api, apiKey, request)
})
js
const io = createIO({
  api: {
    getUserprofile: {
      method: 'POST',
      url: '/xxxx',
    },
  },
})

io.api.getUserprofile({message: 'hello world'}, {header: {xxx: 'xxx'}})

规范化后端接口响应结构

为了方便对后端接口的响应统一处理,所有接口应保持一致的响应结构

ts
{
  status: string // 取值有 ok | error
  error_code: integer, // 错误状态码
  error_msg: string // 底层的错误信息
  display_error_msg: string // 前端显示的错误信息
  data: {} // 正常数据
}

使用 io 调用 API 时,若接口正常响应且 status 不为 'error',会返回正常数据 data。为了向下兼容,当接口响应结构与上述结构不符,会直接将 response.data 返回

TIP

前端开发同学应在联调时注意接口的响应结构,若与上述结构不同可能会导致使用 io 请求接口时返回内容与预期不符,需及时与后端沟通

内置 Hooks

io.useQuery

  • io.useQuery 对 vue-query 的 useQuery 进行了封装,简化了调用过程
  • 使用 uuid 作为默认 queryKey

TIP

需要注意的是,当需要使用到 useQuery 的缓存功能时需要固定 queryKey,否则缓存将不会生效

同时需要避免使用过于简单的词组作为 queryKey 而发生冲突,如 ['activity']['participationLog']

如果是需要对数据表/云函数/接口响应的数据进行缓存,应优先考虑使用 io.useRequest

为了避免上述问题的出现,需要将 queryKeys 提前预设,在需要使用 useQuery 缓存时使用对应的 queryKey,一般在 @/constants 内进行定义

js
import {useRouter} from '@ifanrx/uni-mp'

export const USE_QUERY_KEYS = {
  getProductDetail: (params = {}) => [
    {namespace: 'getProductDetail', route: useRouter().route, ...params},
  ],
}

可参考:

针对不同使用场景可以使用不同的解决方案:

  1. 能够直接通过 io.useRequest 开启缓存功能,不需要手动定义 queryKey

    js
    const cacheTime = 3 * 1000
    const {data} = io.useRequest(
      io.notice.count,
      {query: io.query.compare('viewed', '=', false)},
      {
        select: value => value || 0,
        staleTime: cacheTime,
        gcTime: cacheTime,
      }
    )
  2. 获取某个产品信息详情,希望再次打开页面时可以使用缓存,可以使用 [{namespace: 'getProductDetail', productId}] 作为 queryKey,为了避免与其他产品详情相关的页面产生冲突,可以在 queryKey 内加上当前页面路径,比如:

    js
    import {useRouter} from '@ifanrx/uni-mp'
    
    // QUERY_KEYS 需要在 @/constants 内定义,这里仅供示例
    const USE_QUERY_KEYS = {
      getProductDetail: (params = {}) => [
        {namespace: 'getProductDetail', route: useRouter().route, ...params},
      ],
    }
    
    const {data} = io.useQuery(
      ({queryKey}) => {
        const [{productId}] = queryKey
        return io.product.get({id: productId})
      },
      {
        queryKey: USE_QUERY_KEYS.getProductDetail({productId: props.productId}),
        staleTime: 60 * 1000,
        gcTime: 60 * 1000,
      }
    )
  3. 在不同的页面获取相同的数据,比如说在 A 页面和 B 页面都需要获取用户的未读消息数量以及关注者/被关注者数量。这种场景下可以对 io.useQuery/io.useQueryAll 二次封装,使用 uuid 将 queryKey 进行固定。需要将封装产物放到公共目录下,给不同的页面/组件引用

js
import {uuid} from '@ifanrx/uni-mp'

// 在 Composition API 外部定义 queryKey
const queryKey = [uuid()]

export default function useUnreadNoticeState() {
  // 默认缓存 3s,超过 3s 后再次调用会刷新缓存
  const cacheTime = 3 * 1000
  return io.useQueryAll(
    [
      () => io.notice.count({query: io.query.compare('viewed', '=', false)}),
      () =>
        io.follow.count({query: io.query.compare('created_by', '=', user.id)}),
      () =>
        io.follow.count({query: io.query.compare('followed_by', '=', user.id)}),
    ],
    {
      staleTime: cacheTime,
      gcTime: cacheTime,
      queryKey,
    }
  )
}

io.useQueryAll / io.useQueryAllSettled

  • 基于 io.useQuery 和 Promise.all/Promise.allSettled 进行二次封装
  • 主要针对并行请求的情况,第一个参数必须为数组,且数组元素必须为返回 Promise 的函数,返回 data 可以直接解构,其他与 io.useQuery 一致
  • io.useQueryAll / io.useQueryAllSettled 的区别与 Promise.all 和 Promise.allSettled 一致
  • 查看详细例子

io.useRequest

  • io.useRequest 是对 io.useQuery 二次封装,主要针对有需要缓存请求的场景,参数与 io.useQuery 有很大不同,应特别留意
  • 为了避免在使用时使用了重复/错误的 queryKey,io.useRequest 会根据 requestFn 的表名、查询方法和参数自动填充 queryKey。原理是在每个 io operation 下面根据其类型和方法名生成一组 queryKey,如 io.userprofile.find.queryKey === ['userprofile', 'find'],并且在发送请求时将请求参数拼接进该 queryKey 内,组成具有唯一性的 queryKey
  • io.useRequest 只适用于 io 内的方法,其他场景请使用的 io.useQuery
  • 查看详细例子

io.useMutation

io.useServerDate

  • io.useServerDate() 返回一个响应式的 Dayjs 对象,记为 serverDate。serverDate 会在收到知晓云接口响应时刷新,在没有请求发出前 serverDate 为本地时间
  • 原理是给 BaaS.request 增加一个响应拦截器,每当接收到知晓云接口响应时,会将 response.header.Date 转为 Dayjs 实例后赋值给 serverDate
  • 需要特别注意的是:serverDate 的状态是全局共享的
  • 通常情况下,在视图层使用到服务器时间,更推荐使用 io.useServerDate,在逻辑层需要使用服务器时间做校验,更推荐使用 io.getServerDate,可根据实际应用场景来选择
  • 查看详细例子

IOError

  • io 内所有返回 Promise 的方法都会在 catch 时包上一层 handleIOError,抛出相同的 Error 对象,方便统一错误处理以及错误上报
js
class IOError extends Error {
  code: number
  displayMessage: string
  info: Record<string, any>
}

TIP

通常情况下,可以配合 message.showIOErrorModal 使用,以便轻松地将错误信息显示给用户。

js
try {
  // 调用 io 方法
} catch (error) {
  message.showIOErrorModal(error, {
    10001: '优先显示自定义报错',
    'request timeout': '网络超时',
    default: '当最终文案为空时,使用该默认回退文案',
  })
}

相关文档