为什么捐赠
API 浏览器
升级指南
创建新项目
quasar.config 配置文件
从 Webpack 项目转换
浏览器兼容性
TypeScript 支持
目录结构
命令列表
CSS 预处理器
使用 VueRouter 进行页面路由
懒加载 - 代码分割
资源处理
Boot 文件
预取特性
API 代理
配置 Vite
处理 import.meta.env
使用 Pinia 管理状态
代码检查与格式化
测试与审计
开发移动应用
Ajax 请求
开放开发服务器到公网
联系站长
Quasar CLI with Vite - @quasar/app-vite
数据请求

Quasar 没有内置 HTTP 客户端,也不要求你使用某个特定的库。原生 Fetch API 在 Quasar 应用运行的所有环境中都可用,无论使用哪种构建模式。你可以直接使用它,也可以选择任何第三方库来获得额外功能,如拦截器、缓存或响应式状态管理。

选择一个库

  • ofetch - 轻量的 fetch 封装,支持自动 JSON 解析和拦截器
  • axios - 广泛使用的客户端,支持拦截器,拥有庞大的生态系统(插件、OpenAPI 代码生成等)
  • VueUse useFetch - 响应式的 fetch composable
  • Pinia ColadaTanStack Query - 在上述任何客户端之上提供缓存和异步状态管理

共享 API 客户端

大多数应用只和一个后端通信。你可以只配置一次客户端的基础 URL 和通用设置,然后在整个应用中复用。Boot 文件是放置这些配置的好地方。

下面的示例使用 ofetch,但对于任何客户端(如 axios.create() 或你自己的 fetch 封装)模式都是一样的。首先安装包:


pnpm add ofetch

然后创建 boot 文件并在 quasar.config 文件的 boot 数组中注册它:

/src/boot/api.js

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(设置 HttpOnlySecureSameSite),而不是将 token 存储在页面 JavaScript 可以读取的地方。

相关功能

Prefetch 功能 可以在路由组件显示之前预先获取数据。

如果想在请求进行时显示反馈,可以使用 QAjaxBarLoadingBar 插件