cj-query-adapter-vue
v1.1.5
Published
A Vue adapter for query management, providing hooks for caching and synchronizing asynchronous and remote data
Downloads
99
Readme
cj-query-adapter-vue 使用文档
目录
简介
cj-query-adapter-vue 是一个基于 cj-vue-query 的 Vue 适配器,提供统一的查询/分页/变更(Mutation)能力,并在类型层面为 Vue 2 与 Vue 3 进行适配。它支持:
- 数据查询与缓存管理
- 分页查询与无限滚动
- 统一的请求数据映射(params/body/url)与响应转换
- 查询状态管理、自动重试、错误处理
- 响应式数据绑定
- 便捷的 Query 操作(invalidate/refetch/remove/cancel/reset/getData/setData)
安装
npm install cj-query-adapter-vue基础概念
核心组件
CreateQueryFactory: 创建查询工厂函数QueryAdapter: 查询适配器接口,提供各种查询方法useQuery: 普通查询 HookuseInfiniteQuery: 分页查询 HookuseMutation: 变更操作 HookuseAction: 查询操作工具
类型参数
P(Payload): 请求参数类型D(Data): 原始响应数据类型T(Transformed): 转换后的响应数据类型
基本用法
创建查询
import { CreateQueryFactory } from 'cj-query-adapter-vue'
// 约定的 Service 入参/出参类型见下文 API
const service = async (config) => {
const response = await fetch(config.url, {
method: config.method,
body: config.body ? JSON.stringify(config.body) : undefined,
headers: { 'Content-Type': 'application/json' },
})
return response.json()
}
// 创建查询工厂
const defineQuery = CreateQueryFactory(service)
// 定义一个用户详情查询(演示 :id 占位符自动替换)
const getUserById = defineQuery<{ id: number }, User>({
url: '/api/user/:id',
method: 'GET',
})
// 定义一个用户列表查询
const getUserList = defineQuery<{ pageSize?: number; pageNo?: number }, PaginatedResponse<User>>({
url: '/api/users',
method: 'GET',
})
// 定义一个创建用户的变更
const createUser = defineQuery<Omit<User, 'id'>, User>({
url: '/api/users',
method: 'POST',
})使用查询
<script setup>
import { getUserById } from './api'
// 使用查询钩子(传入函数以支持响应式依赖;返回 null 可禁用请求)
const { data, isLoading, isError, error } = getUserById.useQuery(() => ({ id: 1 }))
</script>
<template>
<div>
<div v-if="isLoading">加载中...</div>
<div v-else-if="isError">加载失败: {{ error?.message }}</div>
<div v-else-if="data">
<h2>{{ data.name }}</h2>
<p>{{ data.email }}</p>
</div>
</div>
</template>使用分页查询(无限加载)
<script setup>
import { getUserList } from './api'
const {
data,
isLoading,
isError,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
flatData,
} = getUserList.useInfiniteQuery(() => ({ pageSize: 10 }))
</script>
<template>
<div>
<div v-if="isLoading">加载中...</div>
<div v-else-if="isError">加载失败: {{ error?.message }}</div>
<div v-else>
<ul>
<li v-for="user in flatData" :key="user.id">
{{ user.name }} - {{ user.email }}
</li>
</ul>
<button v-if="hasNextPage" @click="fetchNextPage" :disabled="isFetchingNextPage">
{{ isFetchingNextPage ? '加载中...' : '加载更多' }}
</button>
<div v-else>没有更多数据了</div>
</div>
</div>
</template>API 详解
CreateQueryFactory
创建一个查询工厂函数,用于定义查询与变更。
function CreateQueryFactory(
service: Service,
queryClient?: QueryClient,
baseOptions?: BaseAdapterOptions<any, any>, // 基础配置,不包含 isCache
): <P = unknown, D = unknown, T = D>(
serviceOptions: ServiceOptions<P, D, T>,
defaultOptions?: AdapterOptions<T, D>, // 实例默认配置,包含 isCache
serviceConfig?: FnDataType<P, T>,
) => QueryAdapter<P, D, T>service: 实际执行网络请求的服务函数queryClient?: 可选的查询客户端实例baseOptions?: 适配器级别的基础配置(如全局的 staleTime、retry 等,注意此层级不包含isCache)defaultOptions?: 当前查询/变更实例的默认配置serviceConfig?: 额外的服务配置
Service / BaseServiceConfig
export interface BaseServiceConfig<P> extends OtherOptions {
url: string
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
body: P | undefined
params: P | undefined
}
export type Service = <P, D>(options: BaseServiceConfig<P>) => Promise<D>ServiceOptions
定义查询服务的配置选项,包含 URL 构建、参数处理等配置。
interface BaseUrlOptions {
/**
* 请求的 url,同时也是 query key 的一部分。
* 常用: '/api/user/list',带参数: '/api/user/:id'
* 请求 url 计算优先级:selectUrl > buildDataToUrl > options.url
*/
url: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
/** POST 等请求的 body 是否进行 FormData 转换 */
formData?: boolean
/** 是否使用数据对 url 中的占位符进行替换,默认 true */
buildDataToUrl?: boolean
/** 是否剔除被替换进 url 的字段,默认 true */
cullBuildData?: boolean
}
export interface ServiceOptions<P = any, D = any, T = D> extends BaseUrlOptions, OtherOptions {
/**
* 自定义请求 url 的生成,不影响 query key。
* 一般无需拼接查询字符串,库会处理 `params`。
*/
selectUrl?: (data: P, options: BaseUrlOptions) => string
/**
* 定义 body 的生成:
* - true:使用默认逻辑(非 GET 且有数据则作为 body)
* - null:不传 body
* - function:自定义生成体
*/
selectBody?: true | null | ((data: P, options: BaseUrlOptions) => object)
/**
* 定义 params 的生成:
* - true:直接映射(GET 默认)
* - null:不传 params
* - function:基于 data 生成对象
*/
selectParams?: true | null | ((data: P, options: BaseUrlOptions) => object)
/**
* 转换响应数据
*/
transformResponse?: (response: D, requestData?: P) => T
}QueryAdapter
查询适配器接口,包含所有可用的查询方法。
interface QueryAdapter<P = any, D = any, T = D> {
(data: P): Promise<T>
/** 请求路径 + 方法的拼接字符串。例如:GET:/api/user/:id */
_key: string
/** 同 _key,兼容旧命名 */
queryKey: string
/** 为 QueryClient 提供模糊匹配功能的基础 key */
baseQueryKey: [string]
/**
* 生成用于与 QueryClient 精确匹配的 queryKey。
* - requestId 仅在 isCache=-1(不共享缓存)时作为 key 的一部分。
*/
spliceQueryKey: (
queryData?: MakeOptional<P> | null,
requestId?: RequestIdType,
isToValue?: boolean,
) => [string, undefined | RequestIdType, (MakeOptional<P> | null)?]
/**
* 生成 useQuery/useInfiniteQuery 所需的 options(内部使用)。
*/
_createQueryOptions: <C extends QueryConfig<T> | QueryInfiniteConfig<T>>(
queryData?: MakeOptional<P>,
queryConfig?: C,
) => QueryOptionResult<C, T>
/** 普通查询 */
useQuery: ((
queryData?: MakeOptional<P>,
queryConfig?: QueryConfig<T> & { initialData?: undefined },
) => UseQueryReturnType<T, Error>) & ((
queryData?: MakeOptional<P>,
queryConfig?: QueryConfig<T> & { initialData: NonUndefinedGuard<T> | (() => NonUndefinedGuard<T>) },
) => UseQueryDefinedReturnType<T, Error>)
/** 分页查询(无限加载) */
useInfiniteQuery: (
queryData?: MakeOptional<P>,
queryConfig?: QueryInfiniteConfig<T>,
) => UseInfiniteQueryReturnType<InfiniteData<T>, Error> & { flatData: Ref<PaginatingRecordPage<T>> }
/** 变更(提交数据) */
useMutation: <V = P>(mutationConfig?: MutationOptions<T, P, V>) => UseMutationReturnType<T, Error, V, any>
/** 操作查询(invalidate/refetch/remove/cancel/reset/getData/setData) */
useAction: (baseQueryData?: MakeOptional<P>) => {
invalidate: (queryData?: MakeOptional<P>, filters?: any, options?: any) => Promise<void>
refetch: (queryData?: MakeOptional<P>, filters?: any, options?: any) => Promise<void>
remove: (queryData?: MakeOptional<P>, filters?: any) => void
cancel: (queryData?: MakeOptional<P>, filters?: any, options?: any) => Promise<void>
reset: (queryData?: MakeOptional<P>, filters?: any, options?: any) => Promise<void>
getData: (queryData?: MakeOptional<P>) => D | undefined
setData: (data: D, queryData?: MakeOptional<P>) => void
}
}配置选项
QueryConfig
普通查询的配置选项:
interface QueryConfig<D> extends Omit<QueryOptions<D>, 'queryKey' | 'queryFn' | 'enabled'> {
/** 额外:控制缓存策略(默认 0)
* - -1: 禁用缓存。数据仅在组件生命周期内有效,不共享,组件卸载即清除;需要 `requireId` 以区分多个组件实例。
* - 0: 启用缓存,数据贯穿整个应用程序。
* - 1: 启用缓存,数据贯穿整个应用程序,且永不过期(内部设置 staleTime=Infinity)。
* - 2: 启用缓存,当没有活跃查询时清除缓存(内部设置 staleTime=Infinity)。
*/
isCache?: -1 | 0 | 1 | 2
/** 唯一标识符,仅当 isCache 为 -1 时有效 */
requireId?: number | string
/** 可选:覆盖 staleTime(当 isCache=1/2 且未显式传入 staleTime 时,默认 Infinity) */
staleTime?: number
}QueryInfiniteConfig
分页查询的配置选项:
interface QueryInfiniteConfig<D> extends Omit<InfiniteQueryOptions<PaginatingRecord<D>>, 'queryKey' | 'queryFn' | 'enabled'> {
isCache?: -1 | 0 | 1 | 2
/** 页面参数字段名,默认 'pageNo' */
pageParamKey?: string
/** 唯一标识符,仅当 isCache 为 -1 时有效 */
requireId?: number | string
/** 自定义扁平化函数,默认会把每页的 `records` 扁平到一个数组(若无 records 则当作单项) */
flatDataSelect?: (data?: InfiniteData<D>) => PaginatingRecordPage<D>
}MutationOptions
变更操作的配置选项:
type MutationOptions<T, P, V = P> = Omit<UnwrapRef<UseMutationOptions<T, Error, P>>, 'mutationFn' | 'mutationKey'> & {
/** 将 mutate 的 variables 转换为实际请求的 data */
transformVariables?: (variables: V) => P
/** 是否展示全局 loading(与 `showLoading` 配合使用)*/
showGlobalLoading?: boolean
/** 展示全局 loading 的方法,返回关闭函数 */
showLoading?: () => () => void
/** 是否显示成功通知 */
showSuccessNotification?: boolean
/** 是否显示错误通知 */
showErrorNotification?: boolean
/** 成功通知的回调函数 */
onSuccessNotification?: (data?: T, config?: any) => void
/** 错误通知的回调函数 */
onErrorNotification?: (error?: Error, config?: any) => void
}高级用法
自定义 URL 生成
你可以使用 selectUrl 函数来自定义 URL 生成逻辑:
const customUrlQuery = defineQuery<{ id: number, category: string }, any>({
url: '/api/items/:id',
method: 'GET',
selectUrl: (data, options) => {
// 自定义 URL 生成逻辑
return `/api/${data?.category}/items/${data?.id}`
}
})自定义参数处理
使用 selectParams 和 selectBody 来自定义参数和请求体的处理:
const customParamsQuery = defineQuery<{ search: string, page: number }, any>({
url: '/api/search',
method: 'GET',
selectParams: (data) => ({
q: data?.search,
page: data?.page,
timestamp: Date.now() // 添加时间戳参数
})
})
const customBodyQuery = defineQuery<{ name: string, details: any }, any>({
url: '/api/create',
method: 'POST',
selectBody: (data) => ({
name: data?.name,
details: JSON.stringify(data?.details) // 将对象序列化为字符串
})
})响应数据转换
使用 transformResponse 在数据返回前进行转换:
const userQueryWithTransform = defineQuery<{ id: number }, User, { name: string; emailDomain: string; originalData: User}>({
url: '/api/user/:id',
method: 'GET',
transformResponse: (response, requestData) => {
const [_, domain = ''] = response.email.split('@')
return { name: response.name, emailDomain: domain, originalData: response }
},
})变量转换
在 Mutation 中使用 transformVariables 转换请求参数:
const userMutation = createUser.useMutation<{ fullName: string; emailAddress: string }>({
transformVariables: (variables) => ({
name: variables.fullName,
email: variables.emailAddress,
}),
})
await userMutation.mutateAsync({ fullName: '测试用户', emailAddress: '[email protected]' })表单数据提交
对于文件上传等需要使用 FormData 的场景:
const uploadFile = defineQuery<{ file: File, description: string }, any>({
url: '/api/upload',
method: 'POST',
formData: true // 自动将 body 转换为 FormData
})
// 使用
const { mutate } = uploadFile.useMutation()
mutate({ file: selectedFile, description: '文件描述' })响应式禁用请求
通过返回 null 可以禁用请求:
<script setup>
import { ref } from 'vue'
const enabled = ref(true)
const id = ref(1)
// 当 enabled 为 false 或 id 为 null 时,请求将被禁用
const { data, isLoading } = getUserById.useQuery(() =>
enabled.value ? { id: id.value } : null
)
</script>自动化通知与加载状态
useMutation 支持自动化处理加载状态和通知提醒。你可以通过全局配置或局部配置来开启这些功能。
在 CreateQueryFactory 配置全局方法
import { CreateQueryFactory } from 'cj-query-adapter-vue'
// 1. 创建查询工厂, 配置提示,lodaing的事件; 优先级最低
const defineQuery = CreateQueryFactory(service, queryClient, {
mutations: {
onSuccessNotification: (data) => {
Toast.success('创建成功')
},
onErrorNotification: (error) => {
Toast.fail(error.message || '创建失败')
},
showLoading: () => {
const toast = Toast.loading('加载中...')
return () => toast.clear()
}
}
})
// 2. 什么一个defineQuery service, 优先将中等
// 可以在这个定义这个service提示,lodaing是否展示
const addUser = defineQuery<UserData>({
url: '/api/user',
method: 'POST'
}, {
mutations: {
showGlobalLoading: true,
showSuccessNotification: true,
showErrorNotification: true,
}
})
// 3. 业务层使用service对应的hook; 优先级最高
const addUserMutation = addUser.useMutation(user, {
showGlobalLoading: true,
showSuccessNotification: true,
showErrorNotification: true,
})
// 4. 执行mutate或者mutateAsync的突变
addUserMutation.mutate(user)
开启加载状态
const createUser = defineQuery<UserData, User>({
url: '/api/user',
method: 'POST'
}, {
mutations: {
showGlobalLoading: true,
showLoading: () => {
const toast = Toast.loading('加载中...')
return () => toast.clear()
}
}
})开启自动化通知
const createUser = defineQuery<UserData, User>({
url: '/api/user',
method: 'POST'
}, {
mutations: {
showSuccessNotification: true,
onSuccessNotification: (data) => {
Toast.success('创建成功')
},
showErrorNotification: true,
onErrorNotification: (error) => {
Toast.fail(error.message || '创建失败')
}
}
})Mutation 守卫 (Guards)
useMutation 支持 guards 选项,允许在执行 mutation 之前运行一系列验证或检查函数。
- 顺序执行:Guards 按数组顺序依次执行。
- 阻塞执行:如果任何一个 Guard 抛出错误或返回被拒绝的 Promise,Mutation 将终止执行,并触发
onError。 - 错误处理:Guard 抛出的错误会被包装成
GuardError,包含guardName和originalError。
import { Guard, GuardError } from 'cj-query-adapter-vue'
const createUser = defineQuery<UserData, User>({
url: '/api/user',
method: 'POST'
})
// 定义 Guard
const validateUser: Guard<UserData> = async (user) => {
if (!user.name) {
throw new Error('用户名不能为空')
}
}
const checkPermission: Guard<UserData> = async () => {
// 模拟权限检查
const hasPermission = true
if (!hasPermission) {
throw new Error('无权操作')
}
}
// 使用 Guard
const createUserMutation = createUser.useMutation({
guards: [validateUser, checkPermission],
onError: (error) => {
if (error instanceof GuardError) {
console.log('Guard Error:', error.guardName, error.originalError)
} else {
console.log('Mutation Error:', error)
}
}
})
// 触发 Mutation
createUserMutation.mutate({ name: '', email: '[email protected]' })缓存策略
该库提供了灵活的缓存策略,通过 isCache 选项控制:
isCache: 0(默认) - 启用缓存,数据贯穿整个应用程序isCache: 1- 启用缓存,数据永不过期isCache: 2- 启用缓存,当没有活跃查询时清除缓存isCache: -1- 禁用缓存,数据生命周期等同于组件生命周期
// 永不过期的缓存
const neverExpireQuery = defineQuery<{ id: number }, User>({
url: '/api/user/:id',
method: 'GET',
isCache: 1
})
// 组件卸载时清除的缓存
const componentScopedQuery = defineQuery<{ id: number }, User>({
url: '/api/user/:id',
method: 'GET',
isCache: -1
})微服务名称空间
可以通过声明合并为请求增加自定义字段(如下示例的 nameSpace):
// global.d.ts
import 'cj-query-adapter-vue'
declare module 'cj-query-adapter-vue' {
interface OtherOptions {
nameSpace?: string
}
}const defineQuery = CreateQueryFactory(async (config) => {
const host = 'https://api.example.com'
const url = pathJoin(host, config.nameSpace, config.url)
const response = await fetch(url, {
method: config.method,
body: config.body ? JSON.stringify(config.body) : undefined,
headers: { 'Content-Type': 'application/json' },
})
return response.json()
})
const getUserFromUserService = defineQuery<{ id: number }, User>({
nameSpace: 'user',
url: '/api/users/:id',
method: 'GET',
})
// 请求示例:/user/api/users/1
const user = await getUserFromUserService({ id: 1 })查询操作
使用 useAction 可以对查询执行各种操作:
// 获取 action 对象
const action = getUserList.useAction()
// 使缓存失效
await action.invalidate()
// 重新获取数据
await action.refetch()
// 从缓存中移除
action.remove()
// 取消请求
await action.cancel()
// 重置查询
await action.reset()
// 获取缓存数据
const cacheData = action.getData()
// 设置缓存数据
action.setData({ records: [], total: 0 })
// 使用特定查询参数操作
await action.invalidate({ pageSize: 10, pageNo: 1 })
const specificData = action.getData({ pageSize: 10, pageNo: 1 })带默认查询参数的操作
可以为 action 指定默认的查询参数:
// 使用默认查询参数
const action = getUserList.useAction({ pageSize: 10 })
// 操作将使用默认参数,除非显式提供
await action.refetch() // 使用 { pageSize: 10 }
await action.refetch({ pageSize: 20 }) // 使用 { pageSize: 20 }配置合并策略
cj-query-adapter-vue 采用多层级配置合并机制(CreateQueryFactory 层 -> defineQuery 层 -> useMutation 调用层)。
合并规则
- 普通选项: 采用深度合并,右侧优先级更高。
- 生命周期钩子 (
onMutate):- 多个钩子会按顺序依次执行。
onMutate返回的context会被深度合并,⚠️不要改context.__loadingClose这个属性;并传递给后续的onSuccess/onError/onSettled。
- 普通生命周期钩子 (
onSuccess,onError,onSettled):- 多个钩子会按顺序异步执行。
- 不合并返回值。
- 通知回调 (
onSuccessNotification,onErrorNotification):- 采用覆盖原则,只有最后一层定义的配置会生效(右侧优先级更高)。
- 即使某一层只开启了开关(如
showSuccessNotification: true),它也会触发在更基础层级(如CreateQueryFactory)定义的通知回调。
useQueryState
useQueryState 用于将外部异步数据源(如接口查询结果)与本地可编辑状态进行安全同步。其行为语义严格对齐 React Hook Form 的 values / reset 机制:
- 当状态未被用户修改(
isDirty === false)时,会持续同步最新的外部数据。 - 一旦用户修改状态(调用
setState或修改modelValue),外部数据将不再覆盖当前状态。 - 调用
reset()会将状态重置为最近一次被信任的外部值。 - 调用
reset(nextValue)会显式更新基准值,并同步到状态。 - 提供
modelValue属性,方便与 Vue 的v-model指令配合使用。
<script setup>
import { ref } from 'vue'
import { getUserById } from './api'
import { useQueryState } from 'cj-query-adapter-vue'
const id = ref(1)
const userQuery = getUserById.useQuery(() => ({ id: id.value }))
// 将 query 的数据同步到本地状态
const { state, modelValue, reset, isDirty } = useQueryState(
() => userQuery.data.value?.name,
{ defaultValue: '张三' }
)
</script>
<template>
<form @submit.prevent>
<!-- 方式 1:使用 v-model 绑定 modelValue -->
<input v-model="modelValue" />
<button type="button" @click="() => reset()">重置</button>
<p>是否已修改: {{ isDirty }}</p>
</form>
</template>测试
包中包含全面的测试用例,涵盖了各种使用场景:
use-query.test.ts- 普通查询的基本功能测试use-infinite-query.test.ts- 分页查询功能测试use-mutation.test.ts- 变更操作测试mutation-notification.test.ts- Mutation 通知与加载状态增强功能测试base-options.test.ts- 基础配置合并逻辑测试utils-logic.test.ts- 核心工具函数逻辑测试use-action.test.ts- 查询操作功能测试defineQuery.test.ts- 查询定义功能测试namespace.test.ts- 名称空间功能测试query-state.test.ts- 状态同步(useQueryState)功能测试
你可以参考这些测试用例来理解各种功能的使用方式。
