透過 100 多個技巧的集合學習 Nuxt!

資料獲取

Nuxt 提供了 composable 函式來處理應用程式內部的資料獲取。

Nuxt 內建了兩個 composable 函式和一個程式庫,用於在瀏覽器或伺服器環境中執行資料獲取:useFetchuseAsyncData$fetch

簡而言之

  • $fetch 是發出網路請求最簡單的方式。
  • useFetch$fetch 的包裝器,僅在通用渲染中獲取資料一次。
  • useAsyncDatauseFetch 類似,但提供更精細的控制。

useFetchuseAsyncData 都共用一組常見的選項和模式,我們將在最後的章節中詳細說明。

需要 useFetchuseAsyncData 的原因

Nuxt 是一個框架,可以在伺服器和用戶端環境中執行同構(或通用)程式碼。如果在 Vue 元件的 setup 函式中使用$fetch 函式來執行資料獲取,這可能會導致資料被獲取兩次,一次在伺服器端(用於渲染 HTML),另一次在用戶端(當 HTML 被 hydration 時)。這可能會導致 hydration 問題、增加互動時間並導致不可預測的行為。

useFetchuseAsyncData composable 函式透過確保 API 呼叫在伺服器端發出時,資料會轉發到用戶端 payload 中來解決此問題。

payload 是一個 JavaScript 物件,可以透過 useNuxtApp().payload 存取。它在用戶端用於避免在瀏覽器中執行程式碼時重新獲取相同的資料在 hydration 期間

使用 Nuxt DevToolsPayload 標籤頁中檢查此資料。
app.vue
<script setup lang="ts">
const { data } = await useFetch('/api/data')

async function handleFormSubmit() {
  const res = await $fetch('/api/submit', {
    method: 'POST',
    body: {
      // My form data
    }
  })
}
</script>

<template>
  <div v-if="data == null">
    No data
  </div>
  <div v-else>
    <form @submit="handleFormSubmit">
      <!-- form input tags -->
    </form>
  </div>
</template>

在上面的範例中,useFetch 將確保請求在伺服器端發生並正確轉發到瀏覽器。$fetch 沒有這種機制,更適合在請求僅從瀏覽器發出時使用。

Suspense

Nuxt 在底層使用 Vue 的 <Suspense> 元件,以防止在每個非同步資料可用於視圖之前進行導航。資料獲取 composable 函式可以幫助您利用此功能,並在每次呼叫時使用最適合的方式。

您可以新增 <NuxtLoadingIndicator> 以在頁面導航之間新增進度條。

$fetch

Nuxt 包含了 ofetch 程式庫,並在您的應用程式中作為 $fetch 別名全域自動匯入。

pages/todos.vue
<script setup lang="ts">
async function 
addTodo
() {
const
todo
= await
$fetch
('/api/todos', {
method
: 'POST',
body
: {
// My todo data } }) } </script>
請注意,僅使用 $fetch 將不會提供網路呼叫去重複和防止導航功能。
建議將 $fetch 用於用戶端互動(基於事件)或與useAsyncData 結合使用,以獲取初始元件資料。
閱讀更多關於 $fetch 的資訊。

將用戶端標頭傳遞到 API

在伺服器端呼叫 useFetch 時,Nuxt 將使用 useRequestFetch 來代理用戶端標頭和 cookies(除了不打算轉發的標頭,例如 host)。

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

或者,下面的範例示範了如何使用 useRequestHeaders 從伺服器端請求(源自用戶端)存取 cookies 並將其傳送到 API。使用同構 $fetch 呼叫,我們確保 API 端點可以存取使用者瀏覽器最初發送的相同 cookie 標頭。如果您未使用 useFetch,則這僅是必要的。

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

async function getCurrentUser() {
  return await $fetch('/api/me', { headers })
}
</script>
您也可以使用 useRequestFetch 自動將標頭代理到呼叫。
在將標頭代理到外部 API 之前,請務必非常小心,並且僅包含您需要的標頭。並非所有標頭都可以安全繞過,並且可能會引入不必要的行為。以下是不應代理的常見標頭列表
  • hostaccept
  • content-lengthcontent-md5content-type
  • x-forwarded-hostx-forwarded-portx-forwarded-proto
  • cf-connecting-ipcf-ray

useFetch

useFetch composable 函式在底層使用 $fetch,以便在 setup 函式中進行 SSR 安全的網路呼叫。

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

此 composable 函式是 useAsyncData composable 函式和 $fetch 工具的包裝器。

觀看 Alexander Lichter 的影片,以避免錯誤使用 useFetch
文件 > API > Composable 函式 > Use Fetch中閱讀更多資訊。
文件 > 範例 > 功能 > 資料獲取中閱讀和編輯即時範例。

useAsyncData

useAsyncData composable 函式負責包裝非同步邏輯,並在解析後傳回結果。

useFetch(url) 幾乎等同於 useAsyncData(url, () => event.$fetch(url))
它是最常見用例的開發人員體驗糖。(您可以在useRequestFetch中找到更多關於 event.fetch 的資訊。)
觀看 Alexander Lichter 的影片,以更深入地了解 useFetchuseAsyncData 之間的差異。

在某些情況下,使用 useFetch composable 函式是不適當的,例如當 CMS 或協力廠商提供他們自己的查詢層時。在這種情況下,您可以使用 useAsyncData 來包裝您的呼叫,並仍然保留 composable 函式提供的優勢。

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

// This is also possible:
const { data, error } = await useAsyncData(() => myGetFunction('users'))
</script>
useAsyncData 的第一個參數是一個唯一鍵,用於快取第二個參數(查詢函式)的回應。可以直接傳遞查詢函式來忽略此鍵,鍵將自動產生。

由於自動產生的鍵僅考慮到調用 useAsyncData 的檔案和行,因此建議始終建立自己的鍵以避免不必要的行為,例如當您建立自己的自訂 composable 函式包裝 useAsyncData 時。

設定鍵對於在使用 useNuxtData 的元件之間共享相同資料或重新整理特定資料非常有用。
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 composable 函式是包裝和等待多個 $fetch 請求完成,然後處理結果的好方法。

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

  return { coupons, offers }
})
// discounts.value.coupons
// discounts.value.offers
</script>
useAsyncData 用於獲取和快取資料,而不是觸發副作用,例如呼叫 Pinia actions,因為這可能會導致意外行為,例如使用 nullish 值重複執行。如果您需要觸發副作用,請使用 callOnce 工具來執行此操作。
<script setup lang="ts">
const offersStore = useOffersStore()

// you can't do this
await useAsyncData(() => offersStore.getOffer(route.params.slug))
</script>
閱讀更多關於 useAsyncData 的資訊。

傳回值

useFetchuseAsyncData 具有下面列出的相同傳回值。

  • data:傳入的非同步函式的結果。
  • refresh/execute:可用於重新整理 handler 函式傳回的資料的函式。
  • clear:可用於將 data 設定為 undefined、將 error 設定為 null、將 status 設定為 idle 以及將任何目前擱置的請求標記為已取消的函式。
  • error:資料獲取失敗時的錯誤物件。
  • status:指示資料請求狀態的字串("idle""pending""success""error")。
dataerrorstatus 是 Vue refs,可以在 <script setup> 中使用 .value 存取

預設情況下,Nuxt 會等到 refresh 完成後才能再次執行。

如果您未在伺服器端獲取資料(例如,使用 server: false),則資料將不會在 hydration 完成之前獲取。這表示即使您在用戶端等待 useFetchdata<script setup> 中仍將保持為 null。

選項

useAsyncDatauseFetch 傳回相同的物件類型,並接受一組常見的選項作為它們的最後一個參數。它們可以幫助您控制 composable 函式的行為,例如導航阻止、快取或執行。

Lazy

預設情況下,資料獲取 composable 函式將在使用 Vue 的 Suspense 導航到新頁面之前,等待其非同步函式的解析。可以使用 lazy 選項在用戶端導航中忽略此功能。在這種情況下,您將必須使用 status 值手動處理載入狀態。

app.vue
<script setup lang="ts">
const { 
status
,
data
:
posts
} =
useFetch
('/api/posts', {
lazy
: true
}) </script> <template> <!-- you will need to handle a loading state --> <
div
v-if="
status
=== 'pending'">
Loading ... </
div
>
<
div
v-else>
<
div
v-for="
post
in
posts
">
<!-- do something --> </
div
>
</
div
>
</template>

您可以選擇使用 useLazyFetchuseLazyAsyncData 作為執行相同操作的便捷方法。

<script setup lang="ts">
const { 
status
,
data
:
posts
} =
useLazyFetch
('/api/posts')
</script>
閱讀更多關於 useLazyFetch 的資訊。
閱讀更多關於 useLazyAsyncData 的資訊。

僅用戶端獲取

預設情況下,資料獲取 composable 函式將在用戶端和伺服器環境中執行其非同步函式。將 server 選項設定為 false 以僅在用戶端執行呼叫。在初始載入時,資料將在 hydration 完成之前不會獲取,因此您必須處理擱置狀態,儘管在後續用戶端導航中,資料將在載入頁面之前等待。

lazy 選項結合使用時,這對於首次渲染不需要的資料(例如,非 SEO 敏感資料)非常有用。

/* This call is performed before hydration */
const 
articles
= await
useFetch
('/api/article')
/* This call will only be performed on the client */ const {
status
,
data
:
comments
} =
useFetch
('/api/comments', {
lazy
: true,
server
: false
})

useFetch composable 函式旨在 setup 方法中調用,或直接在生命週期掛鉤中的函式頂層調用,否則您應該使用 $fetch 方法

最小化 payload 大小

pick 選項可幫助您僅選擇要從 composable 函式傳回的欄位,從而最小化儲存在 HTML 文件中的 payload 大小。

<script setup lang="ts">
/* only pick the fields used in your template */
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 作為鍵。或者,可以在作為最後一個參數傳遞的 options 物件中提供 key 值。
  • useAsyncData 如果第一個參數是字串,則將其用作鍵。如果第一個參數是執行查詢的處理常式函式,則將為您產生一個對於 useAsyncData 實例的檔案名稱和行號唯一的鍵。
要按鍵獲取快取的資料,您可以使用 useNuxtData

Refresh 和 execute

如果您想手動獲取或重新整理資料,請使用 composable 函式提供的 executerefresh 函式。

<script setup lang="ts">
const { 
data
,
error
,
execute
,
refresh
} = await
useFetch
('/api/users')
</script> <template> <
div
>
<
p
>{{
data
}}</
p
>
<
button
@
click
="() =>
refresh
()">Refresh data</
button
>
</
div
>
</template>

execute 函式是 refresh 的別名,其工作方式完全相同,但對於獲取不是立即的情況更語義化。

要全域重新獲取或使快取的資料失效,請參閱 clearNuxtDatarefreshNuxtData

Clear

如果您想清除提供的資料,無論出於何種原因,而無需知道要傳遞給 clearNuxtData 的特定鍵,則可以使用 composable 函式提供的 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', {
/* Changing the id will trigger a refetch */
watch
: [
id
]
}) </script>

請注意,監看響應式值不會變更獲取的 URL。例如,這將持續獲取使用者的相同初始 ID,因為 URL 是在調用函式時建構的。

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

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

如果您需要根據響應式值變更 URL,您可能需要改用計算 URL

計算 URL

有時您可能需要從響應式值計算 URL,並在這些值每次變更時重新整理資料。您可以將每個參數作為響應式值附加,而不是以自己的方式處理。Nuxt 將自動使用響應式值,並在每次變更時重新獲取。

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

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

在更複雜的 URL 建構情況下,您可以使用回呼作為傳回 URL 字串的計算 getter

每次依賴項變更時,都將使用新建構的 URL 獲取資料。將其與not-immediate結合使用,您可以等到響應式元素變更後再獲取。

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

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

const pending = computed(() => status.value === 'pending');
</script>

<template>
  <div>
    <!-- disable the input while fetching -->
    <input v-model="id" type="number" :disabled="pending"/>

    <div v-if="status === 'idle'">
      Type an user ID
    </div>

    <div v-else-if="pending">
      Loading ...
    </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">Get data</button>
  </div>

  <div v-else-if="status === 'pending'">
    Loading comments...
  </div>

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

為了更精細的控制,status 變數可以是

  • idle 當獲取尚未開始時
  • pending 當獲取已開始但尚未完成時
  • error 當獲取失敗時
  • success 當獲取成功完成時

傳遞標頭和 Cookies

當我們在瀏覽器中呼叫 $fetch 時,使用者標頭(如 cookie)將直接傳送到 API。

通常,在伺服器端渲染期間,出於安全考慮,$fetch 不會包含使用者的瀏覽器 cookies,也不會傳遞 fetch 回應中的 cookies。

但是,當在伺服器端使用相對 URL 呼叫 useFetch 時,Nuxt 將使用 useRequestFetch 來代理標頭和 cookies(除了不打算轉發的標頭,例如 host)。

在 SSR 回應中從伺服器端 API 呼叫傳遞 Cookies

如果您想在另一個方向(從內部請求傳回到用戶端)傳遞/代理 cookies,則需要自行處理。

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

export const fetchWithCookie = async (event: H3Event, url: string) => {
  /* Get the response from the server endpoint */
  const res = await $fetch.raw(url)
  /* Get the cookies from the response */
  const cookies = res.headers.getSetCookie()
  /* Attach each cookie to our incoming Request */
  for (const cookie of cookies) {
    appendResponseHeader(event, 'set-cookie', cookie)
  }
  /* Return the data of the response */
  return res._data
}
<script setup lang="ts">
// This composable will automatically pass cookies to the client
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({
  /* Use the fetchKey option to provide a unique key */
  fetchKey: 'hello',
  async asyncData () {
    return {
      hello: await $fetch('/api/hello')
    }
  }
})
</script>
在 Nuxt 3 中,建議使用 <script setup><script setup lang="ts"> 來宣告 Vue 元件。
請在 文件 > API > 工具函式 > 定義 Nuxt 元件 中閱讀更多資訊。

將資料從伺服器序列化至客戶端

當使用 useAsyncDatauseLazyAsyncData 將伺服器上抓取的資料傳輸到客戶端時 (以及任何其他使用 Nuxt payload 的東西),payload 會使用 devalue 進行序列化。這讓我們不僅可以傳輸基本的 JSON,還可以序列化和還原/反序列化更進階的資料類型,例如正規表示式、日期 (Dates)、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.vue
<script setup lang="ts">
// Type of `data` is inferred as string even though we returned a Date object
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">
// Type of `data` is inferred as
// {
//   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(),

    // Workaround the type conversion
    toJSON() {
      return this
    }
  }

  // Serialize the output to string, using superjson
  return superjson.stringify(data) as unknown as typeof data
})
app.vue
<script setup lang="ts">
import superjson from 'superjson'

// `date` is inferred as { createdAt: Date } and you can safely use the Date object methods
const { data } = await useFetch('/api/superjson', {
  transform: (value) => {
    return superjson.parse(value as unknown as string)
  },
})
</script>

指南

透過 POST 請求使用 SSE (伺服器發送事件)

如果您透過 GET 請求使用 SSE,您可以使用 EventSource 或 VueUse composable useEventSource

當透過 POST 請求使用 SSE 時,您需要手動處理連線。以下是如何操作:

// Make 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',
})

// Create a new ReadableStream from the response with TextDecoderStream to get the data as text
const reader = response.pipeThrough(new TextDecoderStream()).getReader()

// Read the chunk of data as we get it
while (true) {
  const { value, done } = await reader.read()

  if (done)
    break

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