数据获取

Nuxt 为您的应用提供了用于处理数据获取的 composables。

Nuxt 附带两个 composables 和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData$fetch

简而言之:

  • $fetch 是发起网络请求的最简单方式。
  • useFetch 是围绕 $fetch 的封装,在通用渲染中只会获取一次数据。
  • useAsyncDatauseFetch 类似,但提供更细粒度的控制。

useFetchuseAsyncData 都共享一组通用的选项和模式,我们将在最后几节详细说明。

useFetchuseAsyncData 的必要性

Nuxt 是一个可以在服务器和客户端环境中运行同构(或通用)代码的框架。如果在 Vue 组件的 setup 函数中使用 $fetch 函数来执行数据获取,可能会导致数据被重复获取:一次在服务器上(用于渲染 HTML),另一次在客户端(在 HTML 被 hydration 时)。这会导致 hydration 问题、增加交互时间并导致不可预测的行为。

useFetchuseAsyncData 这两个 composables 通过确保如果在服务器上进行了 API 调用,则会将数据转发到客户端的 payload 来解决此问题。

payload 是一个可以通过 useNuxtApp().payload 访问的 JavaScript 对象。它在客户端用于避免在浏览器中hydration 期间执行相同代码时重新获取相同的数据。

使用 Nuxt DevToolsPayload 选项卡 中检查这些数据。
app/app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit () {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // 我的表单数据
    },
  })
}
</script>

<template>
  <div v-if="data == undefined">
    无数据
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- 表单输入标签 -->
    </form>
  </div>
</template>

在上面的示例中,useFetch 会确保请求在服务器上发生并正确转发到浏览器。$fetch 没有这种机制,当请求仅在浏览器端发起时,使用 $fetch 更合适。

Suspense

Nuxt 在内部使用 Vue 的 <Suspense> 组件,以防止在所有异步数据可用于视图之前进行导航。数据获取的 composables 可以帮助您利用此功能,并可按调用情况选择最适合的方案。

您可以添加 <NuxtLoadingIndicator> 以在页面导航之间添加进度条。

关于 await 的说明

本 документации 中的示例通常会对 useFetchuseAsyncData 的调用使用 await,但这并不总是必需的。

await 不会 改变服务器渲染的 HTML。在服务器渲染期间,Nuxt 无论如何都会在序列化页面之前等待请求完成(底层通过 <Suspense>onServerPrefetch 实现),因此完整填充后的结果总会发送到浏览器。

await 真正 改变的是在你自己的 <script setup> 中接下来会发生什么,以及客户端导航的行为:

  • 使用 await,执行会暂停,直到数据准备就绪,因此调用之后的任何代码都可以依赖 data 已经被填充。在客户端导航时,这会阻塞导航,直到数据解析完成:用户会停留在当前页面(可选地显示 <NuxtLoadingIndicator>),然后进入一个已完整填充的页面。这是默认行为。
  • 不使用 await,请求在后台运行的同时,执行会立即继续,因此 data 会先使用其默认值,并在请求解析后填充。在客户端导航时,这意味着导航会立即发生,你需要自行处理加载和错误状态,通常通过返回的 statuserror refs 来完成。

这两种方式没有哪一种在所有场景下都更好;正确选择取决于你希望该路由呈现怎样的体验。

不使用 await 在用户可见效果上与 lazy 选项类似(导航不会被阻塞,加载状态由你自己处理),但两者并不相同:lazy 是一个显式标志,会将请求延迟到组件挂载之后,而简单地不使用 await 则会在 setup 期间就开始请求。当你想要非阻塞行为时,优先使用 lazy(或 useLazyFetch / useLazyAsyncData),因为这能更明确地表达意图。

awaitlazy 是相互独立的,并且在客户端上,await 一个 lazy 函数不会产生你预期的效果。如果你 await 一个 lazy 调用(例如 await useLazyFetch(...)await useFetch(..., { lazy: true })),它仍然会像往常一样阻塞服务器渲染,但在 客户端导航 中,await 会立即解析,而不会等待请求完成。await 之后 data 仍然会保持其默认值,你必须通过 status 来处理加载状态。如果你实际上希望导航等待数据,请不要依赖 await,而是移除 lazy 选项。

$fetch

Nuxt 包含 ofetch 库,并在应用中全局自动以 $fetch 别名导入。

pages/todos.vue
<script setup lang="ts">
async function addTodo () {
  const todo = await $fetch('/api/todos', {
    method: 'POST',
    body: {
      // 我的待办数据
    },
  })
}
</script>
请注意,仅使用 $fetch 无法提供网络调用去重和导航阻止
建议在客户端交互(基于事件)中使用 $fetch,或在获取组件初始数据时与 useAsyncData 结合使用。
阅读关于 $fetch 的更多内容。

将客户端头信息传递给 API

当在服务器上调用 useFetch 时,Nuxt 会使用 useRequestFetch 来代理客户端的头信息和 cookie(不包括不应转发的头,例如 host)。

<script setup lang="ts">
const { data } = await useFetch('/api/echo')
</script>
// /api/echo.ts
export default defineEventHandler(event => parseCookies(event))

另外,下面的示例演示如何使用 useRequestHeaders在服务端请求(源自客户端)中访问并向 API 发送 cookie。通过使用同构的 $fetch 调用,我们确保 API 端点可以访问用户浏览器最初发送的相同 cookie 头。如果您没有使用 useFetch,这才是必要的。

<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])

async function getCurrentUser () {
  return await $fetch('/api/me', { headers })
}
</script>
您也可以使用 useRequestFetch 自动将头代理到调用中。
在将头代理到外部 API 之前请务必谨慎,仅包含您需要的头。并非所有头都可以安全地被绕过,可能会引入不期望的行为。以下是一些通常不应被代理的常见头列表:
  • host, accept
  • content-length, content-md5, content-type
  • x-forwarded-host, x-forwarded-port, x-forwarded-proto
  • cf-connecting-ip, cf-ray

useFetch

useFetch composable 在 setup 函数中使用 $fetch 作为底层实现,以便进行 SSR 安全的网络调用。

app/app.vue
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>

<template>
  <p>页面访问次数: {{ count }}</p>
</template>

该 composable 是对 useAsyncData composable 和 $fetch 工具的封装。

Docs > 4 X > API > Composables > Use Fetch 中查看详情
Read and edit a live example in Docs > 4 X > Examples > Features > Data Fetching.

useAsyncData

useAsyncData 组合式函数负责包装异步逻辑,并在其解析后返回结果。

useFetch(url)useAsyncData(url, () => event.$fetch(url)) 基本等价。
它是针对最常见用例的开发者体验糖衣。(您可以在 useRequestFetch 中了解更多关于 event.fetch 的信息。)

在某些情况下,使用 useFetch 并不合适,例如当 CMS 或第三方提供自己的查询层时。在这种情况下,您可以使用 useAsyncData 来包装调用,同时仍保留该组合式函数提供的好处。

app/pages/users.vue
<script setup lang="ts">
const { data, error } = await useAsyncData('users', () => myGetFunction('users'))

// 也可以这样写:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData 的第一个参数是用于缓存第二个参数(查询函数)响应的唯一键。通过直接传递查询函数也可以忽略该键,此时键会被自动生成。

由于自动生成的键只会考虑调用 useAsyncData 的位置,因此建议始终创建自己的键,以避免不希望出现的行为,例如当您创建自己的自定义组合式函数来包装 useAsyncData 时。

设置键也可以用于在使用 useNuxtData 的组件之间共享相同数据,或用于刷新特定数据
app/pages/users/[id].vue
<script setup lang="ts">
const { id } = useRoute().params

const { data, error } = await useAsyncData(`user:${id}`, () => {
  return myGetFunction('users', { id })
})
</script>

useAsyncData 是包装并等待多个 $fetch 请求完成然后处理结果的绝佳方式。

<script setup lang="ts">
const { data: discounts, status } = await useAsyncData('cart-discount', async (_nuxtApp, { signal }) => {
  const [coupons, offers] = await Promise.all([
    $fetch('/cart/coupons', { signal }),
    $fetch('/cart/offers', { signal }),
  ])

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
useAsyncData 用于获取和缓存数据,而不是触发副作用(例如调用 Pinia 的 actions),因为这可能导致诸如重复执行且带有空值等意外行为。如果您需要触发副作用,请使用 callOnce 工具来执行。
<script setup lang="ts">
const offersStore = useOffersStore()

// 你不能这样做
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
阅读关于 useAsyncData 的更多内容。

返回值

useFetchuseAsyncData 具有相同的返回值,列举如下。

  • data:传入的异步函数返回的结果。
  • refresh/execute:用于刷新由 handler 函数返回的数据的函数。
  • clear:用于将 data 设置为 undefined(或如果提供了 options.default() 则为该值)、将 error 设置为 undefined、将 status 设置为 idle,并将任何当前挂起的请求标记为已取消的函数。
  • error:如果数据获取失败,则为错误对象。
  • status:指示数据请求状态的字符串("idle""pending""success""error")。
dataerrorstatus 是 Vue 的 refs,在 <script setup> 中可通过 .value 访问。

默认情况下,Nuxt 会等待 refresh 完成后才允许再次执行。

如果你没有在服务端获取数据(例如使用 server: false),那么数据不会在 hydration 完成之前被获取。这意味着即使你在客户端 await useFetch,在 <script setup>data 仍将保持为 undefined。

选项

useAsyncDatauseFetch 返回相同的对象类型,并在最后一个参数接受一组通用选项。它们可以帮助您控制 composable 的行为,例如导航阻止、缓存或执行方式。

延迟(Lazy)

默认情况下,数据获取的 composables 会在其异步函数解析完成之前使用 Vue 的 Suspense 等待,从而阻止导航。在客户端导航时,可以通过 lazy 选项忽略此功能。在这种情况下,您需要使用 status 值手动处理加载状态。

app/app.vue
<script setup lang="ts">
const { status, data: posts } = useFetch('/api/posts', {
  lazy: true,
})
</script>

<template>
  <!-- 你需要自己处理加载状态 -->
  <div v-if="status === 'pending'">
    加载中 ...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- 执行某些操作 -->
    </div>
  </div>
</template>

您也可以使用 useLazyFetchuseLazyAsyncData 作为方便的方法来实现相同功能。

<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
阅读关于 useLazyFetch 的更多内容。
阅读关于 useLazyAsyncData 的更多内容。

仅客户端获取

默认情况下,数据获取的 composables 会在客户端和服务器环境中执行它们的异步函数。将 server 选项设置为 false 可仅在客户端执行调用。在初次加载时,数据不会在 hydration 完成之前被获取,因此您必须处理挂起(pending)状态;不过在随后的客户端导航中,数据会在加载页面之前被等待。

lazy 选项结合使用时,这对首次渲染时不需要的数据(例如非 SEO 相关数据)非常有用。

/* 这个调用会在 hydration 之前执行 */
const articles = await useFetch('/api/article')

/* 这个调用只会在客户端执行 */
const { status, data: comments } = useFetch('/api/comments', {
  lazy: true,
  server: false,
})

useFetch composable 应在 setup 方法中被调用或在生命周期钩子的顶层函数中直接调用,否则应使用 $fetch 方法

最小化 payload 大小

pick 选项可以帮助您最小化存储在 HTML 文档中的 payload 大小,只选择您希望从 composables 返回的字段。

<script setup lang="ts">
/* 仅选择模板中使用的字段 */
const { data: mountain } = await useFetch('/api/mountains/everest', {
  pick: ['title', 'description'],
})
</script>

<template>
  <h1>{{ mountain.title }}</h1>
  <p>{{ mountain.description }}</p>
</template>

如果您需要更多控制或对多个对象进行映射,您可以使用 transform 函数来更改查询结果。

const { data: mountains } = await useFetch('/api/mountains', {
  transform: (mountains) => {
    return mountains.map(mountain => ({ title: mountain.title, description: mountain.description }))
  },
})
picktransform 都不会阻止初始不想要的数据被获取。但它们会防止不想要的数据被添加到从服务器转发到客户端的 payload 中。

缓存与重新获取

键(Keys)

useFetchuseAsyncData 使用键来防止重新获取相同的数据。

  • useFetch 会根据 URL、fetch 选项以及源码中调用位置生成一个 key。这意味着,在不同组件中对相同 URL 的两个 useFetch 调用会有不同的 key,并且各自都会发起自己的请求。要在多个组件之间共享相同数据,请在作为最后一个参数传入的 options 对象中提供相同的显式 key
  • useAsyncData 如果第一个参数是字符串,则会将其作为 key。若第一个参数是执行查询的处理函数,则会为您生成一个在源码中 useAsyncData 调用位置唯一的 key。
要通过键获取缓存的数据,您可以使用 useNuxtData

共享状态与选项一致性

当多个组件对相同键使用 useAsyncDatauseFetch 时,它们将共享相同的 dataerrorstatus refs。这确保了组件间的一致性,但要求某些选项保持一致。

以下选项在使用相同键的所有调用中必须保持一致:

  • handler 函数
  • deep 选项
  • transform 函数
  • pick 数组
  • getCachedData 函数
  • default
// ❌ 这会触发开发时警告
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: false })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { deep: true })

以下选项可以安全地不同而不会触发警告:

  • server
  • lazy
  • immediate
  • dedupe
  • watch
// ✅ 这是允许的
const { data: users1 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: true })
const { data: users2 } = useAsyncData('users', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }), { immediate: false })

如果您需要独立的实例,请使用不同的键:

// 这些是完全独立的实例
const { data: users1 } = useAsyncData('users-1', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))
const { data: users2 } = useAsyncData('users-2', (_nuxtApp, { signal }) => $fetch('/api/users', { signal }))

响应式键

您可以使用计算 ref、普通 ref 或 getter 函数作为键,从而实现基于依赖变化自动更新的数据获取:

// 使用计算属性作为键
const userId = ref('123')
const { data: user } = useAsyncData(
  computed(() => `user-${userId.value}`),
  () => fetchUser(userId.value),
)

// 当 userId 改变时,数据会自动重新获取
// 并且如果没有其他组件使用它,旧数据会被清理
userId.value = '456'

refresh 与 execute

如果您想手动获取或刷新数据,请使用 composables 提供的 executerefresh 函数。

<script setup lang="ts">
const { data, error, execute, refresh } = await useFetch('/api/users')
</script>

<template>
  <div>
    <p>{{ data }}</p>
    <button @click="() => refresh()">
      刷新数据
    </button>
  </div>
</template>

execute 函数是 refresh 的别名,功能完全相同,但在 fetch 不是立即执行 的情况下,execute 更具语义性。

要全局重新获取或使缓存数据失效,请参见 clearNuxtDatarefreshNuxtData

清除(Clear)

如果您想清除所提供的数据(出于任何原因),而不需要知道传递给 clearNuxtData 的特定键,可以使用 composables 提供的 clear 函数。

<script setup lang="ts">
const { data, clear } = await useFetch('/api/users')

const route = useRoute()
watch(() => route.path, (path) => {
  if (path === '/') {
    clear()
  }
})
</script>

监听(Watch)

要在应用中其他响应式值发生变化时重新执行您的获取函数,请使用 watch 选项。您可以将其用于一个或多个可监听元素。

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch('/api/users', {
  /* 更改 id 会触发重新获取 */
  watch: [id],
})
</script>

请注意,监听响应式值不会更改被获取的 URL。例如,下面的代码会一直获取初始用户 ID 对应的相同 URL,因为 URL 在函数被调用时就已构建。

<script setup lang="ts">
const id = ref(1)

const { data, error, refresh } = await useFetch(`/api/users/${id.value}`, {
  watch: [id],
})
</script>

如果您需要基于响应式值更改 URL,您可能想使用计算 URL

当提供响应式获取选项时,它们将被自动监视并触发重新获取。在某些情况下,您可能希望通过指定 watch: false 来选择退出此行为。

const id = ref(1)

// 当 id 改变时不会自动重新获取
const { data, execute } = await useFetch('/api/users', {
  query: { id }, // id 默认会被监听
  watch: false, // 禁用对 id 的自动监听
})

// 不会触发重新获取
id.value = 2

计算 URL(Computed URL)

有时您可能需要基于响应式值构建 URL,并在这些值每次变化时刷新数据。您可以将每个参数作为响应式值附加,Nuxt 会自动使用该响应式值并在其变化时重新获取。

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch('/api/user', {
  query: {
    user_id: id,
  },
})
</script>

在更复杂的 URL 构建场景中,您可以使用回调作为计算 getter,返回 URL 字符串。

每当依赖项发生变化时,数据将使用新构建的 URL 进行获取。将此与非立即执行 结合,您可以在响应式元素变化之前等待再去获取。

<script setup lang="ts">
const id = ref(null)

const { data, status } = useLazyFetch(() => `/api/users/${id.value}`, {
  immediate: false,
})
</script>

<template>
  <div>
    <!-- 在获取时禁用输入 -->
    <input
      v-model="id"
      type="number"
      :disabled="status === 'pending'"
    >

    <div v-if="status === 'idle'">
      输入用户 ID
    </div>

    <div v-else-if="status === 'pending'">
      加载中 ...
    </div>

    <div v-else>
      {{ data }}
    </div>
  </div>
</template>

如果您需要在其他响应式值更改时强制刷新,也可以监听其他值

非立即执行(Not immediate)

useFetch composable 会在被调用时立即开始获取数据。您可以通过将 immediate: false 设置为防止这一行为,例如等待用户交互后再执行。

在这种情况下,您需要同时使用 status 来处理获取生命周期,并使用 execute 来启动数据获取。

<script setup lang="ts">
const { data, error, execute, status } = await useLazyFetch('/api/comments', {
  immediate: false,
})
</script>

<template>
  <div v-if="status === 'idle'">
    <button @click="execute">
      获取数据
    </button>
  </div>

  <div v-else-if="status === 'pending'">
    加载评论中...
  </div>

  <div v-else>
    {{ data }}
  </div>
</template>

更细粒度的控制中,status 变量可以是:

  • idle:尚未开始获取
  • pending:获取已开始但尚未完成
  • error:获取失败
  • success:获取成功完成

当我们在浏览器中调用 $fetch 时,用户的头信息如 cookie 会直接发送到 API。

通常,在服务端渲染期间,出于安全考虑,$fetch 不会包含用户浏览器的 cookies,也不会将 fetch 响应中的 cookie 转发回客户端。

但是,当在服务器上使用相对 URL 调用 useFetch 时,Nuxt 会使用 useRequestFetch 来代理头和 cookies(不包括不应转发的头,例如 host)。

如果您想将 cookie 从内部请求传回/代理到客户端,您需要自行处理。

app/composables/fetch.ts
import type { H3Event } from 'h3'

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* 从服务器端点获取响应 */
  const res = await $fetch.raw(url)
  /* 获取响应中的 cookie */
  const cookies = res.headers.getSetCookie()
  /* 将每个 cookie 附加到我们的传入请求 */
  for (const cookie of cookies) {
    event.res.headers.append('set-cookie', cookie)
  }
  /* 返回响应数据 */
  return res._data
}
<script setup lang="ts">
// 该 composable 会自动将 cookie 传递到客户端
const event = useRequestEvent()

const { data: result } = await useAsyncData(() => fetchWithCookie(event!, '/api/with-cookie'))

onMounted(() => console.log(document.cookie))
</script>

Options API 支持

Nuxt 提供了一种在 Options API 中执行 asyncData 获取的方法。为使其工作,您必须将组件定义包装在 defineNuxtComponent 中。

<script>
export default defineNuxtComponent({
  /* 使用 fetchKey 选项提供唯一键 */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello'),
    }
  },
})
</script>
在 Nuxt 中推荐使用 <script setup><script setup lang="ts"> 来声明 Vue 组件。
Docs > 4 X > API > Utils > Define Nuxt Component 中查看详情

将数据从服务器序列化到客户端

当使用 useAsyncDatauseLazyAsyncData 将在服务器上获取的数据传输到客户端(以及任何其他使用Nuxt payload的功能)时,payload 会使用 devalue 进行序列化。这允许我们不仅传输基本的 JSON,还可以序列化并恢复/反序列化更高级的数据类型,例如正则表达式、Date、Map 和 Set、refreactiveshallowRefshallowReactiveNuxtError 等。

也可以为 Nuxt 不支持的类型定义您自己的序列化/反序列化器。您可以在 useNuxtApp 文档中阅读更多内容。

请注意,这不适用于使用 $fetchuseFetch 从服务器路由获取并传递的数据——更多信息请参见下一节。

从 API 路由序列化数据

server 目录获取数据时,响应使用 JSON.stringify 进行序列化。然而,由于序列化仅限于 JavaScript 基本类型,Nuxt 会尽力将 $fetchuseFetch 的返回类型转换为与实际值相匹配的类型。

了解有关 JSON.stringify 限制的更多信息。

示例

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app/app.vue
<script setup lang="ts">
// 虽然我们返回了 Date 对象,但 `data` 类型推断为 string
const { data } = await useFetch('/api/foo')
</script>

自定义序列化函数

要自定义序列化行为,您可以在返回的对象上定义 toJSON 函数。如果您定义了 toJSON 方法,Nuxt 将尊重该函数的返回类型,不会尝试转换类型。

server/api/bar.ts
export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    toJSON () {
      return {
        createdAt: {
          year: this.createdAt.getFullYear(),
          month: this.createdAt.getMonth(),
          day: this.createdAt.getDate(),
        },
      }
    },
  }
  return data
})
app/app.vue
<script setup lang="ts">
// `data` 类型被推断为:
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

使用替代序列化器

Nuxt 当前不支持将序列化器替换为 JSON.stringify 之外的方案。不过,您可以将 payload 作为普通字符串返回,并利用 toJSON 方法来保持类型安全。

下面的示例中,我们使用 superjson 作为序列化器。

server/api/superjson.ts
import superjson from 'superjson'

export default defineEventHandler(() => {
  const data = {
    createdAt: new Date(),

    // 解决类型转换问题
    toJSON () {
      return this
    },
  }

  // 使用 superjson 将输出序列化为字符串
  return superjson.stringify(data) as unknown as typeof data
})
app/app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `data` 的类型被推断为 { createdAt: Date },您可以安全地使用 Date 对象方法
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

Recipes

Use SSE (Server-Sent Events) via POST request

If you consume SSE via GET request, you can use EventSource or VueUse's composable useEventSource.

When consuming SSE via POST request, you need to handle the connection manually. As shown below:

// Initiate a POST request to the SSE endpoint
const response = await $fetch<ReadableStream>('/chats/ask-ai', {
  method: 'POST',
  body: {
    query: 'Hello AI, how are you?',
  },
  responseType: 'stream',
})

// Use TextDecoderStream to create a new ReadableStream and get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// Read the received data chunks
while (true) {
  const { value, done } = await reader.read()

  if (done) { break }

  console.log('Received:', value)
}

Parallel requests

When requests do not depend on each other, you can use Promise.all() to send requests in parallel and improve performance.

const { data } = await useAsyncData((_nuxtApp, { signal }) => {
  return Promise.all([
    $fetch('/api/comments/', { signal }),
    $fetch('/api/author/12', { signal }),
  ])
})

const comments = computed(() => data.value?.[0])
const author = computed(() => data.value?.[1])