Quasar 没有内置 HTTP 客户端,也不要求你使用某个特定的库。原生 Fetch API 在 Quasar 应用运行的所有环境中都可用,无论使用哪种构建模式。你可以直接使用它,也可以选择任何第三方库来获得额外功能,如拦截器、缓存或响应式状态管理。
选择一个库
- ofetch - 轻量的 fetch 封装,支持自动 JSON 解析和拦截器
- axios - 广泛使用的客户端,支持拦截器,拥有庞大的生态系统(插件、OpenAPI 代码生成等)
- VueUse useFetch - 响应式的 fetch composable
- Pinia Colada 和 TanStack Query - 在上述任何客户端之上提供缓存和异步状态管理
共享 API 客户端
大多数应用只和一个后端通信。你可以只配置一次客户端的基础 URL 和通用设置,然后在整个应用中复用。Boot 文件是放置这些配置的好地方。
下面的示例使用 ofetch,但对于任何客户端(如 axios.create() 或你自己的 fetch 封装)模式都是一样的。首先安装包:
pnpm add ofetch然后创建 boot 文件并在 quasar.config 文件的 boot 数组中注册它:
import { defineBoot } from '#q-app'
import { ofetch } from 'ofetch'
// 如果你在上面添加了用户级别的状态,则不适合 SSR。请参见下方 SSR 注意事项部分。
export const apiFetch = ofetch.create({
baseURL: 'https://api.example.com'
// 你可以在这里添加拦截器(onRequest, onResponse, ...)和其他选项
})
export default defineBoot(({ app }) => {
// 如果需要,可以在这里使用 app 实例
})在组件中使用:
<script setup>
import { ref } from 'vue'
import { Notify } from 'quasar'
import { apiFetch } from '@/boot/api'
const data = ref(null)
async function loadData() {
try {
data.value = await apiFetch('/backend/data')
} catch (error) {
console.error(error)
Notify.create({
type: 'negative',
message: '加载失败'
})
}
}
</script>ofetch 直接返回解析后的响应体,并在错误响应时抛出异常,因此不需要额外的 response.data 步骤。
SSR 注意事项
在 SPA 中,每个访问者在自己的浏览器中运行应用的独立副本,因此上面在模块级别创建的 apiFetch 实例只属于该访问者。SSR 则不同——一个 Node 进程为所有访问者渲染页面,模块级别的代码只在服务器启动时执行一次,由此产生的 apiFetch 实例会在所有传入请求之间共享。
只要共享实例只包含所有人都相同的数据(如基础 URL),就不会有问题。但当用户级别的数据进入模块作用域时,问题就来了:
let token = null
export const apiFetch = ofetch.create({
baseURL: 'https://api.example.com',
onRequest({ options }) {
if (token) {
options.headers.set('Authorization', `Bearer ${token}`)
}
}
})
// 用户登录后调用
export function setToken(newToken) {
token = newToken
}在浏览器中,这段代码运行正常。但在服务器上,那个 token 变量被所有访问者共享。如果用户 A 在服务器为用户 B 渲染页面时登录了,那么用户 B 的请求现在就会携带用户 A 的 token。
你可以通过每次调用都手动传递 token 来避免存储状态,但这很繁琐且容易出错。健壮的方案是在 boot 函数内部创建客户端。在服务器端,boot 文件会在每个传入请求时运行一次,因此这样的客户端只属于单个请求,可以安全地保存用户级别的状态:
import { defineBoot } from '#q-app'
import { ofetch, type $Fetch } from 'ofetch'
import { inject, type InjectionKey } from 'vue'
const injectionKey = Symbol('apiFetch') as InjectionKey<$Fetch>
// 用于在其他地方访问客户端的辅助函数
export function useApi() {
const apiFetch = inject(injectionKey)
if (!apiFetch) {
throw new Error('API client not provided')
}
return apiFetch
}
export default defineBoot(({ app, ssrContext }) => {
const apiFetch = ofetch.create({
baseURL: 'https://api.example.com',
// 将访问者的 cookie 转发给 API
headers: ssrContext ? { cookie: ssrContext.req.headers.cookie ?? '' } : {}
})
app.provide(injectionKey, apiFetch)
})然后在组件中:
<script setup>
import { useApi } from '@/boot/api'
const apiFetch = useApi()
async function loadData() {
const data = await apiFetch('/private/data')
// ...
}
</script>或者在另一个 boot 文件中:
import { defineBoot } from '#q-app'
import { useApi } from '@/boot/api'
export default defineBoot(async ({ app }) => {
// `inject()` 需要活跃的 Vue 上下文,但 boot 文件在上下文外运行,所以用 `runWithContext`
const apiFetch = app.runWithContext(() => useApi())
const data = await apiFetch('/private/data')
// ...
})代价是:按请求创建的实例无法在模块级别导出,所以你需要通过 useApi() 辅助函数来访问它,而不是直接导入。
至于 token 本身:如果身份验证需要在页面刷新后保持有效,建议使用服务器管理的 cookie(设置 HttpOnly、Secure 和 SameSite),而不是将 token 存储在页面 JavaScript 可以读取的地方。
相关功能
Prefetch 功能 可以在路由组件显示之前预先获取数据。
如果想在请求进行时显示反馈,可以使用 QAjaxBar 或 LoadingBar 插件。