Appearance
服务请求 - createIO
集成知晓云表操作(读/写数据表)、云函数调用、后端接口调用,并且引入了 @tanstack/vue-query(下文简称 vue-query
),能够满足更多场景的使用。
简介
将表操作、云函数调用、接口调用统一封装到 io 内,这样做的好处:
- 统一管理 表、云函数、API,避免分散在项目各地,管理混乱
- 使用
io.faas.cloudFunctionName
代替BaaS.invoke('cloudFunctionName')
,从而减少魔法值的使用
增加 TypeScript 类型声明,可以更加便捷高效地调用 io
对
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},
],
}
可参考:
针对不同使用场景可以使用不同的解决方案:
能够直接通过 io.useRequest 开启缓存功能,不需要手动定义 queryKey
jsconst cacheTime = 3 * 1000 const {data} = io.useRequest( io.notice.count, {query: io.query.compare('viewed', '=', false)}, { select: value => value || 0, staleTime: cacheTime, gcTime: cacheTime, } )
获取某个产品信息详情,希望再次打开页面时可以使用缓存,可以使用
[{namespace: 'getProductDetail', productId}]
作为 queryKey,为了避免与其他产品详情相关的页面产生冲突,可以在 queryKey 内加上当前页面路径,比如:jsimport {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, } )
在不同的页面获取相同的数据,比如说在 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.useMutation 会将
vue-query
的 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: '当最终文案为空时,使用该默认回退文案',
})
}