Nuxt3 - data & api
數據獲取 (api)
Nuxt 提供了三個方便獲得數據的方法:useFetch
、useAsyncData
和 $fetch
。
根據官方推薦,他們建議使用 Nuxt 就用這三個方法就好,他們不太建議再去裝 axios 等套件。
這三種方法的特點如下:
useFetch
:適合於組件初始化時的數據獲取。$fetch
:適合需要根據用戶行為動態發起請求的情況 (如按下送出按鍵時執行 POST)。useAsyncData
結合$fetch
:適合需要對數據進行細緻操作的情況。
關於 useFetch
跟 $fetch
的使用時機
如果在組件初始化時使用 $fetch
來獲取初始資料,會導致數據在 server 與 client 端都被獲取一次。
useFetch
useFetch
其實就是 useAsyncData
和 $fetch
的結合,基本用法如下:
<script setup lang="ts">
const { data: count } = await useFetch('/api/count')
</script>
<template>
<p>Page visits: {{ count }}</p>
</template>
$fetch
上述提到 useFetch
是對 $fetch
的封裝,提供了開發者更方便的接口,$fetch
內置底層的 HTTP 請求工具,由 ofetch 提供。
<script setup lang="ts">
async function addTodo() {
const todo = await $fetch('/api/todos', {
method: 'POST',
body: {
// My todo data
}
})
}
</script>
useAsyncData
useAsyncData
的作用是包裝非同步邏輯,並在其解析(resolved)後返回結果。
本質上 useFetch
就是 useAsyncData
配合 $fetch
的語法糖。
所以基本上 useFetch(url)
幾乎等同於 useAsyncData(url, () => $fetch(url))
。
那為何還要保留 useAsyncData
的用法?
根據 Nuxt 官方說法,當如果要從 CMS 或第三方服務的查詢層獲取資料時,使用 useFetch
可能並不恰當,這時應改用 useAsyncData
配合 $fetch
來達到更靈活的操作並保留 useAsyncData
數據管理、狀態管理和錯誤處理的好處。
什麼是 CMS 或第三方服務的查詢層
CMS(Content Management System)的最佳範例就是那個大家大概都聽過的 WordPress,所謂 CMS 查詢層就比如 WordPress 提供用來查詢文章、頁面、分類等內容的 REST API。
第三方服務的查詢層則比如 Algolia 提供搜索 API,可以用來查詢和檢索數據。
<script setup lang="ts">
const { data: discounts, pending } = 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>
用 Nuxt server 開發 api
這裡示範的是串接 vercel 的 Postgres database。
如何串接請參閱上一篇文章末段。
需要額外安裝 dotenv 來讀取環境變數:
pnpm install dotenv
建立種子資料
因為是我實際開發的例子,中間過程會有許多 code,我會將其拿掉保留比較關鍵的部分,讓程式碼不要太多、太複雜。
- 建立一個 /script/seed.js 檔案 (其實不一定要有 script 資料夾,想放根目錄也行):
import { db } from '@vercel/postgres'
async function clearTables(client) {
try {
// 清空所有資料表
await client.query(`
DROP TABLE IF EXISTS order_details;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS delivery;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS users;
`)
console.log('Tables cleared successfully.')
} catch (error) {
console.error('Error clearing tables:', error)
throw error
}
}
async function createTables(client) {
try {
// 創建用戶表
await client.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
address VARCHAR(255) NOT NULL,
phone VARCHAR(255) NOT NULL,
card_number VARCHAR(255) NOT NULL
);
`)
// Details skipped
console.log('Tables created successfully.')
} catch (error) {
console.error('Error creating tables:', error)
throw error
}
}
// 在 main 函數中連接數據庫,並調用 createTables 函數創建表
async function main() {
const client = await db.connect()
await clearTables(client)
await createTables(client)
await client.end()
}
main().catch((err) => {
console.error('An error occurred while creating tables:', err)
})
- 前往 package.json 增加一段指令:
"seed": "node -r dotenv/config ./script/seed.js"
- 執行
pnpm seed
即可在資料庫中建立起表格。
開發 api
先來看資料夾結構:
---| /server
------| /api
---------| /order.get.ts
---------| /search.post.ts
使用 Nuxt server 開發 api 其實滿方便的,放在 /server/api 中的檔案 Nuxt 會根據檔名自動解析成不同 method 的 RESTful api:
- order.get.ts:會被解析為 /api/order,method 為 GET。
const { data: orderList } = useFetch('/api/order') as any
- search.post.ts:會被解析為 /api/search,method 為 POST。
const onClick = async () => {
try {
const data = await $fetch('/api/search', {
method: 'POST',
body: { orderNumber: orderNumber.value.trim() }
})
console.log(data)
} catch (error) {
console.error(error)
}
}
接著是撰寫 api 文件的範例:
- order.get.ts:
import { db } from '@vercel/postgres'
export default defineEventHandler(async () => {
const client = await db.connect()
try {
// 獲取訂單數據
const { rows: orders } = await client.query(
`SELECT o.*, u.name AS user_name, u.phone, u.address, u.card_number,
d.name AS delivery_method,
array_agg(p.name) AS product_names,
array_agg(p.unit) AS product_units,
array_agg(od.quantity) AS product_quantities,
array_agg(od.price) AS product_prices
FROM orders o
JOIN users u ON o.users_id = u.id
JOIN delivery d ON o.delivery_id = d.id
JOIN order_details od ON o.id = od.order_id
JOIN products p ON od.product_id = p.id
GROUP BY o.id, u.name, u.phone, u.address, u.card_number, d.name
ORDER BY o.created_at DESC`
)
client.release()
return {
success: true,
data: orders
}
} catch (error) {
console.error('Error fetching orders:', error)
return { success: false, message: 'Error fetching orders' }
}
})
- search.post.ts:
import { db } from '@vercel/postgres'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const orderNumber = body.orderNumber
if (!orderNumber) {
return { success: false, message: 'Order number is required' }
}
const client = await db.connect()
try {
const { rows: orderRows } = await client.query(
`SELECT o.id, o.total_price, o.status, o.delivery_time, o.tracking_number, u.name, u.phone, u.address, u.card_number, d.name as delivery_name
FROM orders o
JOIN users u ON o.users_id = u.id
JOIN delivery d ON o.delivery_id = d.id
WHERE o.order_number = $1`,
[orderNumber]
)
// Details skipped
client.release()
return {
success: true,
order: orderRows[0],
products: orderDetails.rows
}
} catch (error) {
console.error('Error searching order:', error)
return { success: false, message: 'Error searching order' }
}
})