编程 Pinia Colada 实战:query key、enabled 与缓存失效的坑

2026-11-12 21:31:30

Pinia Colada 实战:query key、enabled 与缓存失效的坑

Pinia Colada 是 Pinia 之上的 Vue 数据获取层,提供 useQuery / useMutation / useQueryCache / defineQueryOptions / defineQuery,约 2KB,除 Pinia 外零依赖,支持 SSR。仓库:posva/pinia-colada,文档:pinia-colada.esm.dev。

安装与查询状态

安装命令:

npm i @pinia/colada

必须在 Pinia 之后注册:

import { createPinia } from 'pinia'
import { PiniaColada } from '@pinia/colada'

app.use(createPinia())
app.use(PiniaColada, { /* global options */ })

基本查询:

const { state, asyncStatus, refresh, refetch } = useQuery({
  key: () => ['contacts', route.params.id],
  query: () => getContactById(route.params.id),
})

state 包含 data / error / status(pending | success | error);asyncStatus 是 'idle' | 'loading'。status 是数据状态,asyncStatus 是请求调用状态,分开才能区分“首次加载”和“后台刷新”。refresh() 复用进行中的请求并受 staleTime 约束;refetch() 无视缓存强制请求。

外部参数放进 key 函数

外部参数(路由参数、搜索词)不能作为 query 函数入参;要在 query 函数里读外部响应式值,并把这些值放进 key 函数,否则缓存会串。key 必须是函数才能响应式更新。

inject 在 query 函数里失效

在 query 函数里调用 inject()(或依赖 inject 的 composable)第一次可能成功,但查询被 invalidate 后重新拉取时是在组件 setup() 之外执行,会失败。正确做法是把 useApi() 提到 composable 外层再传进去:

export function useProjectList() {
  const api = useApi()
  return useQuery(projectListQuery(api))
}

缓存读取同理:

queryCache.getQueryData(projectListQuery(api).key)

store 里用 query 会让它永生

官方提醒:大多数情况下不要在 store 里消费 query,因为 store 永不销毁,query 会变成“永生”的,持续 watch key 并刷新。需要 store 里用数据,改用 useQueryCache() 读缓存。

用 enabled 暂停查询

跨页面共享、依赖路由参数的查询会一直触发,需要 enabled:

export const useCurrentDeck = defineQuery(() => {
  const route = useRoute()
  const result = useQuery({
    key: () => ['decks', Number(route.params.deckId)],
    query: () => fetch(`/api/decks?deckId=${route.params.deckId}`).then(res => res.json()),
    enabled: () => 'deckId' in route.params,
  })
  return { ...result }
})

变更与失效:await invalidateQueries 再关弹窗

const queryCache = useQueryCache()
const { mutate: updateContact, isLoading } = useMutation({
  mutation: patchContact,
  async onSettled(updatedContact) {
    await queryCache.invalidateQueries({ key: ['contacts', updatedContact.id], exact: true })
  },
})

被 invalidate 且正在被使用的查询会自动重新拉取。

soubiran.dev 记录了一个弹窗场景的坑:在 onSuccess 里关闭弹窗,再在 onSettled 里 invalidate,用户会先看到弹窗关闭、列表还没刷新,误以为提交没成功。把关闭逻辑移进 onSettled 并 await invalidateQueries,弹窗就在数据回来之后才关:

onSettled: async (_, error) => {
  await queryCache.invalidateQueries({ key: ['comments'] })
  if (!error) { comment.value.text = ''; open.value = false }
}

分页:placeholderData 保留上一页

把页码作为 key 的一部分,key 必须是函数:

key: () => ['contacts', Number(route.query.page)]

翻页时新 key 会新建缓存条目,state.data 变 undefined;用 placeholderData 保留上一页数据,此时 status 仍是 success 但 asyncStatus 是 loading,且 placeholder 不参与 SSR 序列化。无限滚动用 useInfiniteQuery() 把多页合并进一个缓存条目。

清缓存:先 cancel 再 remove

Query Cache 有两类动作:精确动作 cancel(entry) / remove(entry) / invalidate(entry) / fetch(entry) / refresh(entry) / setEntryState(entry, state);便捷动作 invalidateQueries(filters) / cancelQueries(filters) / setQueriesData(filters, updater) / getQueryData(key) / setQueryData(key, data)。清缓存不能只 remove,因为 remove 不取消在途请求,请求返回还会写回状态,正确两步:

queryCache.cancelQueries()
queryCache.getEntries().forEach(entry => queryCache.remove(entry))

登出重置单个查询:get → cancel → remove。

setQueryData 会强置 status=success

setQueryData 会强制把条目 status 设为 'success',即使从未加载过。批量打补丁时先按 status: 'success' 过滤:

queryCache.setQueriesData({ key: ['contacts','list'], status: 'success' }, (list = []) => [...list, newContact])

getEntries 过滤支持 key 前缀、exact、active、stale、status、predicate。

乐观更新与 defineQueryOptions

乐观更新在 onMutate 里 setQueryData 写入预期值并 cancelQueries 取消在途请求(防止旧响应覆盖),返回快照;onError 里先比较缓存里仍是自己写的值再回滚,避免覆盖别人的新改动;onSettled 里 invalidate 拿服务端真值。不适合乐观更新的:金融交易等不可逆操作、服务端会大幅改写数据、失败率高、有副作用(发邮件通知)的操作。

defineQueryOptions 把 key 与 options 收敛到 ./queries 目录,获得 key 的完整类型:

queryCache.getQueryData(productDetailsQuery('24').key)

defineQuery 返回带额外状态的小 store。

Vue Router 集成

Vue Router 实验集成见 Vue Router 数据加载器文档。vue-router/experimental/pinia-colada 的 defineColadaLoader:query(to, { signal }) 里按需请求,key: to => ['users', to.params.id],staleTime 控制新鲜期;导航时默认 refresh 而非 refetch;route tracking 只跟踪 query 函数里真正用到的 to 字段;Nuxt 用 @pinia/colada-nuxt,SSR 后水合不重复请求。

复制全文 生成海报 Vue Pinia 前端 缓存 TypeScript

推荐文章

程序员茄子在线接单