Skip to main content

Nuxt3 資料獲取與 api

數據獲取 (api)

Nuxt 提供了三個方便獲得數據的方法:useFetchuseAsyncData$fetch
根據官方推薦,他們建議使用 Nuxt 就用這三個方法就好,他們不太建議再去裝 axios 等套件。
這三種方法的特點如下:

  1. useFetch:適合於組件初始化時的數據獲取。
  2. $fetch:適合需要根據用戶行為動態發起請求的情況 (如按下送出按鍵時執行 POST)。
  3. useAsyncData 結合 $fetch:適合需要對數據進行細緻操作的情況。
warning

關於 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 數據管理、狀態管理和錯誤處理的好處。

info

什麼是 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

warning

這裡示範的是串接 vercel 的 Postgres database。
如何串接請參閱上一篇文章末段。

需要額外安裝 dotenv 來讀取環境變數:

pnpm install dotenv

建立種子資料

warning

因為是我實際開發的例子,中間過程會有許多 code,我會將其拿掉保留比較關鍵的部分,讓程式碼不要太多、太複雜。

  1. 建立一個 /script/seed.js 檔案 (其實不一定要有 script 資料夾,想放根目錄也行):
seed.js
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)
})
  1. 前往 package.json 增加一段指令:
"seed": "node -r dotenv/config ./script/seed.js"
  1. 執行 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' }
}
})

參考資料

  1. Data fetching
  2. Nuxt.js 3.x Server 目錄-建立 API