資料擷取
Nuxt 內建兩個 composable 和一個程式庫,用於在瀏覽器或伺服器環境中執行資料擷取:useFetch
、useAsyncData
和 $fetch
。
簡而言之
$fetch
是發出網路請求最簡單的方式。useFetch
是$fetch
的包裝函式,只在通用渲染中擷取資料一次。useAsyncData
類似於useFetch
,但提供更精細的控制。
useFetch
和 useAsyncData
都共用一組選項和模式,我們將在最後幾節中詳細說明。
需要 useFetch
和 useAsyncData
的原因
Nuxt 是一個可以在伺服器和客戶端環境中執行同構(或通用)程式碼的框架。如果在 Vue 元件的 setup 函式中使用 $fetch
函式 執行資料獲取,可能會導致資料被獲取兩次:一次在伺服器端(用於渲染 HTML),另一次在客戶端(當 HTML 被水合時)。這可能會導致水合問題、增加互動時間並導致不可預測的行為。
useFetch
和 useAsyncData
這兩個 composables 通過確保如果在伺服器端發出 API 呼叫,資料會轉發到 payload 中的客戶端來解決這個問題。
payload 是一個 JavaScript 物件,可通過 useNuxtApp().payload
存取。它在客戶端使用,以避免在瀏覽器 在水合期間 執行程式碼時重新獲取相同的資料。
<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>
元件,以防止在每個非同步資料對視圖可用之前進行導航。資料獲取 composables 可以幫助您利用此功能,並在每次呼叫的基礎上使用最適合的功能。
<NuxtLoadingIndicator>
在頁面導航之間添加進度條。$fetch
Nuxt 包含 ofetch 函式庫,並且在您的應用程式中自動匯入為全域的 $fetch
別名。
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
將客戶端標頭傳遞到 API
在伺服器端渲染期間,由於 $fetch
請求在伺服器「內部」發生,因此它不會包含使用者的瀏覽器 Cookie。
我們可以使用 useRequestHeaders
從伺服器端存取 Cookie 並將其代理到 API。
下面的範例將請求標頭添加到同構的 $fetch
呼叫中,以確保 API 端點可以存取使用者最初傳送的相同 cookie
標頭。
<script setup lang="ts">
const headers = useRequestHeaders(['cookie'])
async function getCurrentUser() {
return await $fetch('/api/me', { headers: headers.value })
}
</script>
host
、accept
content-length
、content-md5
、content-type
x-forwarded-host
、x-forwarded-port
、x-forwarded-proto
cf-connecting-ip
、cf-ray
useRequestFetch
將標頭自動代理到呼叫中。useFetch
useFetch
composable 在底層使用 $fetch
,以便在 setup 函式中進行 SSR 安全的網路呼叫。
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
此 composable 是 useAsyncData
composable 和 $fetch
工具的包裝器。
useAsyncData
useAsyncData
composable 負責包裝非同步邏輯,並在解析後傳回結果。
useFetch(url)
幾乎等同於 useAsyncData(url, () => event.$fetch(url))
。它是最常見用例的開發人員體驗糖。(您可以在
useRequestFetch
中找到更多關於 event.fetch
的資訊。)在某些情況下,使用 useFetch
composable 並不適合,例如當 CMS 或第三方提供他們自己的查詢層時。在這種情況下,您可以使用 useAsyncData
來包裝您的呼叫,並且仍然保持 composable 提供的優點。
<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
的元件之間共用相同資料或重新整理特定資料非常有用。<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>
傳回值
useFetch
和 useAsyncData
具有下面列出的相同傳回值。
data
:傳入的非同步函式的結果。refresh
/execute
:一個函式,可以用於重新整理handler
函式傳回的資料。clear
:一個函式,可以用於將data
設定為undefined
,將error
設定為null
,將status
設定為idle
,並將任何目前處於擱置狀態的請求標記為已取消。error
:如果資料獲取失敗,則會傳回錯誤物件。status
:一個字串,指示資料請求的狀態 ("idle"
、"pending"
、"success"
、"error"
)。
data
、error
和 status
是 Vue refs,可以在 <script setup>
中使用 .value
存取。預設情況下,Nuxt 會等待 refresh
完成後才能再次執行。
server: false
),則在水合完成之前不會獲取資料。這表示即使您在客戶端等待 useFetch
,data
在 <script setup>
中仍將保持為 null。選項
useAsyncData
和 useFetch
傳回相同的物件類型,並接受一組共同的選項作為最後一個引數。它們可以幫助您控制 composables 的行為,例如導航封鎖、快取或執行。
Lazy
預設情況下,資料獲取 composables 會在使用 Vue 的 Suspense 導航到新頁面之前,等待其非同步函式的解析。使用 lazy
選項可以在客戶端導航時忽略此功能。在這種情況下,您必須使用 status
值手動處理載入狀態。
<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>
或者,您可以使用 useLazyFetch
和 useLazyAsyncData
作為執行相同操作的便利方法。
<script setup lang="ts">
const { status, data: posts } = useLazyFetch('/api/posts')
</script>
僅限客戶端獲取
預設情況下,資料獲取 composables 會在客戶端和伺服器環境中執行其非同步函式。將 server
選項設定為 false
以僅在客戶端執行呼叫。在初始載入時,資料將在水合完成之前不會被獲取,因此您必須處理擱置狀態,但是在後續的客戶端導航中,資料將在載入頁面之前等待。
與 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
選項可幫助您通過僅選擇您希望從 composables 傳回的欄位,來最小化儲存在 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 }))
}
})
pick
和 transform
都不會阻止最初獲取不需要的資料。但是它們會阻止將不需要的資料添加到從伺服器傳輸到客戶端的 payload 中。快取和重新獲取
鍵
useFetch
和 useAsyncData
使用鍵來防止重新獲取相同的資料。
useFetch
使用提供的 URL 作為鍵。或者,可以在作為最後一個引數傳遞的options
物件中提供key
值。useAsyncData
如果第一個引數是字串,則將其用作鍵。如果第一個引數是執行查詢的處理常式函式,則會為您產生一個對於useAsyncData
的執行個體的檔案名稱和行號唯一的鍵。
useNuxtData
重新整理和執行
如果您想要手動獲取或重新整理資料,請使用 composables 提供的 execute
或 refresh
函式。
<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
的別名,其運作方式完全相同,但在非立即獲取的情況下更具語義性(非立即)。
clearNuxtData
和 refreshNuxtData
。清除
如果您想在無需知道傳遞給 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
選項。您可以將其用於一個或多個可監看的元素。
<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 獲取資料。將此與非立即結合使用,您可以等待響應式元素變更後再獲取。
<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>
如果您需要在其他響應式數值變更時強制重新整理,您也可以監看其他數值。
非立即
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
,當獲取成功完成時
傳遞標頭和 Cookie
當我們在瀏覽器中呼叫 $fetch
時,像是 cookie
這類的用戶標頭會直接傳送至 API。
通常,在伺服器端渲染期間,由於 $fetch
請求是在伺服器「內部」進行的,它不會包含用戶的瀏覽器 cookies,也不會傳遞 fetch 回應中的 cookies。
但是,當在伺服器上呼叫 useFetch
時,Nuxt 將使用 useRequestFetch
來代理標頭和 cookies(除了不應該轉發的標頭,例如 host
)。
在 SSR 回應中從伺服器端 API 呼叫傳遞 Cookie
如果您想在另一個方向(從內部請求傳遞回客戶端)傳遞/代理 cookies,則需要自行處理。
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>
<script setup>
或 <script setup lang="ts">
來宣告 Vue 元件。將資料從伺服器序列化至客戶端
當使用 useAsyncData
和 useLazyAsyncData
將在伺服器上獲取的資料傳輸到客戶端(以及任何使用Nuxt payload的其他內容)時,payload 會使用 devalue
進行序列化。這使我們不僅可以傳輸基本的 JSON,還可以序列化和恢復/反序列化更進階的資料類型,例如正規表示式、日期、Map 和 Set、ref
、reactive
、shallowRef
、shallowReactive
和 NuxtError
等。
也可以為 Nuxt 不支援的類型定義您自己的序列化器/反序列化器。您可以在useNuxtApp
文件中閱讀更多資訊。
$fetch
或 useFetch
獲取時從伺服器路由傳遞的資料 - 請參閱下一節了解更多資訊。從 API 路由序列化資料
當從 server
目錄獲取資料時,會使用 JSON.stringify
序列化回應。但是,由於序列化僅限於 JavaScript 原始類型,Nuxt 會盡力轉換 $fetch
和 useFetch
的傳回類型,以匹配實際值。
範例
export default defineEventHandler(() => {
return new Date()
})
<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 將尊重該函式的傳回類型,並且不會嘗試轉換類型。
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
})
<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 作為我們的序列化器。
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
})
<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(伺服器發送事件)
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)
}