Nuxt3 中文课程 《实战全栈开发简书》.

数据获取

Nuxt 提供了组合函数来处理应用程序中的数据获取。

Nuxt 提供了两个组合函数和一个内置库,用于在浏览器或服务器环境中执行数据获取:useFetchuseAsyncData$fetch

简而言之:

  • useFetch 是在组件设置函数中处理数据获取的最简单方法。
  • $fetch 可以根据用户交互进行网络请求。
  • useAsyncData 结合 $fetch,提供了更精细的控制。

useFetchuseAsyncData 共享一组常见的选项和模式,在后面的章节中我们将详细介绍。

在此之前,首先要知道为什么需要这些组合函数。

为什么需要特定的组合函数?

使用像 Nuxt 这样的框架可以在客户端和服务器环境中执行调用和呈现页面时,必须解决一些问题。这就是为什么 Nuxt 提供了组合函数来封装查询,而不是让开发者仅依赖于 $fetch 调用。

网络请求重复

useFetchuseAsyncData 组合函数确保一旦在服务器上进行了 API 调用,数据将以有效的方式在负载中传递到客户端。

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

使用 Nuxt DevToolsPayload 选项卡 中检查此数据。

Suspense

Nuxt 在底层使用 Vue 的 <Suspense> 组件,以防止在视图中的每个异步数据可用之前导航。数据获取组合函数可以帮助您利用此功能,并根据每个调用的需求使用最适合的方法。

useFetch

useFetch 组合函数是执行数据获取的最简单的方法。

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

<template>
  页面访问量:{{ count }}
</template>

这个组合函数是 useAsyncData 组合函数和 $fetch 工具的封装。

Read more in Docs > API > Composables > Use Fetch.
Read and edit a live example in Docs > Examples > Features > Data Fetching.

$fetch

Nuxt 包括了 ofetch 库,并且作为全局别名 $fetch 自动导入到应用程序中。它是 useFetch 在幕后使用的工具。

const users = await $fetch('/api/users').catch((error) => error.data)
请注意,仅使用 $fetch 将不会提供 网络请求重复和导航阻止。建议在提交数据到事件处理程序时使用 $fetch,在客户端逻辑中使用,或与 useAsyncData 结合使用。

ofetch 库是基于 Fetch API 构建的,并为其添加了便利功能:

  • 在浏览器、Node 或 worker 环境中的使用方式相同
  • 自动解析响应
  • 错误处理
  • 自动重试
  • 拦截器
阅读 ofetch 的完整文档
阅读更多关于 $fetch 的内容

useAsyncData

useAsyncData 组合函数负责封装异步逻辑并在解析完成后返回结果。

事实上,useFetch(url) 几乎等同于 useAsyncData(url, () => $fetch(url)) - 它是为最常见的用例提供的开发者体验糖。

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

useAsyncData 的第一个参数是用于缓存第二个参数(查询函数)的响应的唯一键。如果直接传递查询函数,则可以忽略该参数。在这种情况下,它将自动生成。

const { data, error } = await useAsyncData('users', () => myGetFunction('users'))

由于自动生成的键仅考虑调用 useAsyncData 的文件和行,因此建议始终创建自己的键以避免不需要的行为,如果您正在创建自己的自定义组合函数并封装 useAsyncData

const id = ref(1)

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

useAsyncData 组合函数是包装和等待多个 useFetch 完成,并获取每个结果的绝佳方式。

const { data: discounts, pending } = await useAsyncData('cart-discount', async () => {
  const [coupons, offers] = await Promise.all([$fetch('/cart/coupons'), $fetch('/cart/offers')])

  return {
    coupons,
    offers
  }
})
阅读更多关于 useAsyncData 的内容

选项

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

懒加载

默认情况下,数据获取的组合函数会在异步函数解析完成之前使用Vue的Suspense进行页面导航。通过使用lazy选项,可以忽略此功能在客户端导航时的使用。在这种情况下,你需要手动处理加载状态,使用pending值。

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

<template>
  <!-- 你需要处理加载状态 -->
  <div v-if="pending">
    加载中...
  </div>
  <div v-else>
    <div v-for="post in posts">
      <!-- 做一些操作 -->
    </div>
  </div>
</template>

你还可以使用useLazyFetchuseLazyAsyncData作为方便的方法来执行相同的操作。

const { pending, data: posts } = useLazyFetch('/api/posts')
了解更多关于useLazyFetch的信息
了解更多关于useLazyAsyncData的信息

仅在客户端获取数据

默认情况下,数据获取的组合函数会在客户端和服务器环境中执行其异步函数。将server选项设置为false,只在客户端执行调用。在初始加载时,在水合过程完成之前不会获取数据,因此你需要处理一个加载状态,但在随后的客户端导航中,数据将在加载页面之前等待获取。

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

/* 此调用在水合之前执行 */
const { article } = await useFetch('api/article')

/* 此调用仅在客户端执行 */
const { pending, data: posts } = useFetch('/api/comments', {
  lazy: true,
  server: false
})

useFetch组合函数用于在设置方法中调用,或在生命周期钩子函数的函数顶层直接调用,否则你应该使用$fetch方法

减少有效负载大小

pick选项可帮助你通过仅选择你想要从组合函数返回的字段来减少存储在HTML文档中的有效负载大小。

<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都不会阻止初始时获取不需要的数据。但它们将阻止不需要的数据被添加到从服务器传输到客户端的有效负载中。

缓存和重新获取数据

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

  • useFetch使用提供的URL作为键。或者,可以在作为最后一个参数传递的options对象中提供key值。
  • useAsyncData如果第一个参数是字符串,则将其用作键。如果第一个参数是执行查询的处理函数,则会为useAsyncData的实例生成一个基于文件名和行号的唯一键。
要根据键获取缓存的数据,可以使用useNuxtData

刷新和执行

如果要手动获取或刷新数据,请使用组合函数提供的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的别名,使用方式完全相同,但在非立即的情况下更语义化。

要全局刷新或使缓存的数据失效,请参阅clearNuxtDatarefreshNuxtData

监听

如果希望在应用程序中的其他响应式值更改时重新运行获取函数,请使用watch选项。可以将其用于一个或多个可监听的元素。

const id = ref(1)

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

请注意,监视响应式值不会更改获取的URL。例如,这将保持获取用户的相同初始ID,因为URL是在调用函数时构建的。

const id = ref(1)

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

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

计算URL

有时,您可能需要从响应式值计算URL,并在每次更改时刷新数据。不需要费力地解决此问题,您可以将每个参数作为响应式值附加。Nuxt将自动使用响应式值并在每次更改时重新获取。

const id = ref(null)

const { data, pending } = useLazyFetch('/api/user', {
  query: {
    user_id: id
  }
})

在URL构建更复杂的情况下,可以使用回调函数作为计算getter,返回URL字符串。

每当依赖关系更改时,将使用新构建的URL获取数据。将其与非立即结合使用,可以在响应元素更改之前等待获取。

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

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

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

    <div v-if="status === 'idle'">
      输入用户ID
    </div>
    
    <div v-else-if="pending">
      加载中...
    </div>

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

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

不立即执行

useFetch 组合函数在调用时会立即开始获取数据。你可以通过设置 immediate: false 来阻止立即执行,例如,等待用户交互。

为此,你需要使用 status 来处理获取生命周期,并使用 execute 来开始数据获取。

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

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

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

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

为了更精细地控制,status 变量可以有以下取值:

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

当我们在浏览器中调用 $fetch 时,用户的请求头(如 cookie)会直接发送到 API。但在服务器端渲染期间,由于 $fetch 请求在服务器内部进行,它不包含用户浏览器的 Cookie,也不会传递来自获取响应的 Cookie。

将客户端请求头传递到 API

我们可以使用 useRequestHeaders 来访问和代理服务器端的 Cookie 到 API。

下面的示例将请求头添加到同构的 $fetch 调用中,以确保 API 端点能够访问用户最初发送的相同 cookie 请求头。

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

const { data } = await useFetch('/api/me', { headers })
</script>
在代理请求头到外部 API 之前,请谨慎选择需要包含的请求头。并非所有的请求头都可以安全地绕过,可能会引入不希望的行为。以下是一些不应该代理的常见请求头的列表:
  • hostaccept
  • content-lengthcontent-md5content-type
  • x-forwarded-hostx-forwarded-portx-forwarded-proto
  • cf-connecting-ipcf-ray ::
如果你想要将 Cookie 传递/代理到另一个方向,从内部请求返回到客户端,你需要自行处理。
composables/fetch.ts
import { appendResponseHeader, H3Event } from 'h3'

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

const result = await fetchWithCookie(event, '/api/with-cookie')

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

选项 API 支持

Nuxt 3 提供了一种在选项 API 中执行 asyncData 获取数据的方式。你必须将组件定义包装在 defineNuxtComponent 中才能使用此功能。
<script>
export default defineNuxtComponent({
  /* 使用 fetchKey 选项提供一个唯一的键 */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>
在 Nuxt 3 中,使用 <script setup lang="ts"> 是声明 Vue 组件的推荐方式。
Read more in Docs > API > Utils > Define Nuxt Component.

序列化

当从 server 目录获取数据时,响应会使用 JSON.stringify 进行序列化。然而,由于序列化仅限于 JavaScript 原始类型,Nuxt 会尽其所能将 $fetchuseFetch 的返回类型转换为匹配实际值的类型。
了解更多关于 JSON.stringify 的限制。

示例

server/api/foo.ts
export default defineEventHandler(() => {
  return new Date()
})
app.vue
<script setup lang="ts">
// 尽管我们返回了一个 Date 对象,但 `data` 的类型被推断为字符串
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.vue
<script setup lang="ts">
// `data` 的类型被推断为
// {
//   createdAt: {
//     year: number
//     month: number
//     day: number
//   }
// }
const { data } = await useFetch('/api/bar')
</script>

使用替代序列化器

Nuxt 当前不支持将 JSON.stringify 替换为其他序列化器。但是,你可以将负载返回为普通字符串,并利用 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.vue
<script setup lang="ts">
  import superjson from 'superjson'

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