昨天(Day 9)我們用最簡單的 fetch
打通了 GET / POST,正式踏出「能跟世界溝通」的第一步。
今天把這條資料管線升級:導入 Axios 和 Service 層,實作 完整 CRUD(新增、讀取、更新、刪除),並處理 loading / error
狀態,讓呼叫更一致、可維護。
同時我們也要訓練「工程分工的直覺」:
哪些功能必須交給後端 API?哪些其實在前端就能高效完成?
例如:OrderStats.vue
的「統計表」其實前端拿到 order list
後再計算即可(你的情境計算簡單、無巨量資料),這樣可減少後端壓力與溝通次數。反之,需要持久化、跨裝置共享、權限驗證的功能,就交給後端( server.js
已提供完整 CRUD)。
fetch
,建立共用客戶端。OrderService
完成 CRUD 與 loading/error 管理。GET /api/orders
POST /api/orders
PUT /api/orders/:id
DELETE /api/orders/:id
後端
server.js
已支援上述四個端點,資料持久化到order.json
。
下面把「前端工程師 / 後端工程師」視角可能會有點不一樣,這邊我幫大家補充一下。
有時候可以多站在別人的角度思考一下,這個功能:
是否有完成我的目的-> user
UI是否有流程的體驗->前端
資料邏輯跟api設計是否正確-> 後端
這也是開發一個專案要注意的地方~!!
前端工程師(FE)
作為使用者,我想在進入頁面時看到目前所有訂單。
驗收標準
onMounted
觸發讀取;列表渲染完成。loading
、錯誤顯示 error
。order.json
資料一致。FE 任務
OrderService.list()
(axios.get)loading/error
<OrderList />
呈現後端工程師(BE)
作為 API,我要提供目前的所有訂單資料。
驗收標準
GET /api/orders
回傳 200
與 application/json
order.json
,順序/欄位正確BE 任務
order.json
,回傳陣列500 + { error }
前端工程師(FE)
作為使用者,我想送出一筆新的飲料訂單。
驗收標準
FE 任務
<AddOrder />
收集資料 → OrderService.create(payload)
await loadOrders()
後端工程師(BE)
作為 API,我要把收到的訂單寫入 order.json
。
驗收標準
POST /api/orders
回 201
與新訂單 JSON(含 id/createdAt
)BE 任務
id
、createdAt
前端工程師(FE)
作為使用者,我想修改既有訂單(例如更正名字或飲料)。
驗收標準
FE 任務
<EditOrder />
選單帶入值OrderService.update(id, patch)
→ 成功後重刷後端工程師(BE)
作為 API,我要按 id
找到目標並更新欄位。
驗收標準
PUT /api/orders/:id
回 200
+ 更新後物件404
BE 任務
前端工程師(FE)
作為使用者,我想移除一筆訂單。
驗收標準
FE 任務
<OrderList />
觸發 @remove(id)
OrderService.remove(id)
→ 成功後重刷後端工程師(BE)
作為 API,我要根據 id
刪除訂單並回傳結果。
驗收標準
DELETE /api/orders/:id
回 200 + { message }
404
BE 任務
動作 | Method & Path | 說明 |
---|---|---|
讀取 | GET /api/orders |
取全列表 |
新增 | POST /api/orders |
建立一筆 |
更新 | PUT /api/orders/:id |
覆寫/更新欄位 |
刪除 | DELETE /api/orders/:id |
依 id 刪除 |
什麼時候需要開立api、計算什麼時候需要在後端做?
這個就需要工程師的判斷
比如說金融或是線上的交易,應該都是要給代號或是序號在後端計算,金額從前端送出去會有被串改的風險!!
(很常會遇到新聞說: 網路XX駭客自由改動金額,董前後端的工程師看了就會)
我們可以根據這些情境
來畫畫看前後端不同事件的時序圖
後端根據這些設計我們可以產生出後端的code如下
import express from "express";
import { promises as fs } from "fs";
import path from "path";
import { fileURLToPath } from "url";
import cors from "cors";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const app = express();
const PORT = 3000;
const DATA_FILE = path.join(__dirname, "order.json");
// ✅ 啟用 CORS(允許所有來源)
app.use(cors());
app.use(express.json());
// 輔助函數:讀取訂單資料
async function readOrders() {
try {
const txt = await fs.readFile(DATA_FILE, "utf8");
return JSON.parse(txt);
} catch (error) {
console.error("讀取訂單資料失敗:", error);
return [];
}
}
// 輔助函數:寫入訂單資料
async function writeOrders(orders) {
try {
await fs.writeFile(DATA_FILE, JSON.stringify(orders, null, 2), "utf8");
return true;
} catch (error) {
console.error("寫入訂單資料失敗:", error);
return false;
}
}
// 🔍 GET /api/orders - 取得所有訂單
app.get("/api/orders", async (_req, res) => {
try {
const orders = await readOrders();
res.json(orders);
} catch (error) {
res.status(500).json({ error: "無法取得訂單資料" });
}
});
// ➕ POST /api/orders - 新增一筆訂單
app.post("/api/orders", async (req, res) => {
try {
const orders = await readOrders();
const newOrder = {
id: Date.now().toString(),
name: req.body.name || '',
note: req.body.note || '',
drink: req.body.drink || '',
sweetness: req.body.sweetness || '',
ice: req.body.ice || '',
createdAt: new Date().toISOString(),
};
orders.push(newOrder);
const success = await writeOrders(orders);
if (!success) {
return res.status(500).json({ error: "無法儲存訂單" });
}
res.status(201).json(newOrder);
} catch (error) {
res.status(500).json({ error: "新增訂單失敗" });
}
});
// ✏️ PUT /api/orders/:id - 更新指定訂單
app.put("/api/orders/:id", async (req, res) => {
try {
const orders = await readOrders();
const index = orders.findIndex(o => o.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: "找不到指定的訂單" });
}
// 保留原始的 id 和 createdAt
const updatedOrder = {
...orders[index],
name: req.body.name || orders[index].name,
note: req.body.note || orders[index].note,
drink: req.body.drink || orders[index].drink,
sweetness: req.body.sweetness || orders[index].sweetness,
ice: req.body.ice || orders[index].ice,
updatedAt: new Date().toISOString(),
};
orders[index] = updatedOrder;
const success = await writeOrders(orders);
if (!success) {
return res.status(500).json({ error: "無法更新訂單" });
}
res.json(updatedOrder);
} catch (error) {
res.status(500).json({ error: "更新訂單失敗" });
}
});
// 🗑️ DELETE /api/orders/:id - 刪除指定訂單
app.delete("/api/orders/:id", async (req, res) => {
try {
const orders = await readOrders();
const index = orders.findIndex(o => o.id === req.params.id);
if (index === -1) {
return res.status(404).json({ error: "找不到指定的訂單" });
}
const deletedOrder = orders[index];
orders.splice(index, 1);
const success = await writeOrders(orders);
if (!success) {
return res.status(500).json({ error: "無法刪除訂單" });
}
res.json({ message: "訂單已刪除", order: deletedOrder });
} catch (error) {
res.status(500).json({ error: "刪除訂單失敗" });
}
});
// 錯誤處理中間件
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: "伺服器內部錯誤" });
});
// 404 處理
app.use((req, res) => {
res.status(404).json({ error: "找不到指定的路由" });
});
app.listen(PORT, () => {
console.log(`🚀 飲料點單 API 伺服器啟動成功!`);
console.log(`📍 伺服器位址: http://localhost:${PORT}`);
});
有這份code run起來後
我們可以使用postman看看是否正確
如果後端資訊需要json物件(例如新增訂單) 記得要在postman的raw ->type選json喔
!!
然後可以把這整串json丟進去測試
{
"name": "測試用戶",
"note": "請加珍珠",
"drink": "紅茶",
"sweetness": "正常甜",
"ice": "去冰"
}
今天會講一下,最近寫專案喜歡的寫法分享
會多兩隻檔案:
axios 是我們在前端call api很好用的一套library他原生支援async function的處理
npm i axios
src/services/http.js
) axios初始機器職責:建立並輸出一個共用 HTTP 客戶端(axios.create),集中設定 baseURL / timeout / headers,以及請求/回應攔截器(統一列印、攔截錯誤、加 token…)。
為什麼要獨立?
任何 API 呼叫都經過這個實例,同一處就能改全局行為(例如:加上 Authorization、全域錯誤訊息格式化)。
日後換網域、加超時、改重試策略,只動這一個檔案。
因為這篇是有預設有經驗的js工程師
所以你應該知道: 如果以後要改url的設定或是讀取邏輯會 超。級。麻。煩!!!
所以這隻會當作我default 初始化axios呼叫器的東西
Ps port跟url通常還是會放到env file或是CICD注入參數
// src/services/http.js
import axios from 'axios'
// 建立 axios 實例
export const http = axios.create({
baseURL: 'http://localhost:3000',
timeout: 5000,
headers: {
'Content-Type': 'application/json',
},
})
// 請求攔截器
http.interceptors.request.use(
(config) => {
// 可以在這裡加入 loading 狀態或 token
console.log('發送請求:', config.method?.toUpperCase(), config.url)
return config
},
(error) => {
console.error('請求錯誤:', error)
return Promise.reject(error)
}
)
// 回應攔截器
http.interceptors.response.use(
(response) => {
console.log('收到回應:', response.status, response.config.url)
return response
},
(error) => {
console.error('回應錯誤:', error.response?.status, error.message)
// 統一處理錯誤
if (error.response) {
// 伺服器回應了錯誤狀態碼
const { status, data } = error.response
switch (status) {
case 404:
console.error('資源不存在:', data.error)
break
case 500:
console.error('伺服器錯誤:', data.error)
break
default:
console.error('API 錯誤:', data.error || error.message)
}
} else if (error.request) {
// 請求已發送但沒有收到回應
console.error('網路錯誤: 無法連接到伺服器')
} else {
// 其他錯誤
console.error('未知錯誤:', error.message)
}
return Promise.reject(error)
}
)
src/services/orderService.js
)這邊可以叫做orderService.js也可以叫做api service
主要目的就是管理你api function的地方
職責:把「業務語意」的 API 封裝成乾淨的函式:list / create / update / remove。
重點:元件不直接碰 URL 與 axios,而是呼叫 OrderService.create(payload) 這樣的語意方法。
為什麼要獨立?
抽掉重複樣板:URL、HTTP 方法、錯誤格式化都集中這裡。
強化可測性:單元測試可以直接 mock 這層,而非改動元件。
未來可演進:要加快取、重試、佇列、離線同步,都在 Service 層做。
https.js向是axios魔法的初始設定,有點像是我們使用魔法前需要設定的魔仗跟知識心法
orderService.js就是api的套裝概念,這個就像是我們的咒語跟動作,也就是身體運行的部分
// src/services/orderService.js
import { http } from './http'
export const OrderService = {
/**
* 取得所有訂單
* @returns {Promise<Array>} 訂單陣列
*/
async list() {
const { data } = await http.get('/api/orders')
return data
},
/**
* 新增訂單
* @param {Object} payload - 訂單資料
* @param {string} payload.name - 姓名
* @param {string} payload.note - 備註
* @param {string} payload.drink - 飲料
* @param {string} payload.sweetness - 甜度
* @param {string} payload.ice - 冰量
* @returns {Promise<Object>} 新建的訂單
*/
async create(payload) {
const { data } = await http.post('/api/orders', payload)
return data
},
/**
* 更新訂單
* @param {string} id - 訂單 ID
* @param {Object} patch - 要更新的欄位
* @returns {Promise<Object>} 更新後的訂單
*/
async update(id, patch) {
const { data } = await http.put(`/api/orders/${id}`, patch)
return data
},
/**
* 刪除訂單
* @param {string} id - 訂單 ID
* @returns {Promise<Object>} 刪除結果
*/
async remove(id) {
const { data } = await http.delete(`/api/orders/${id}`)
return data
},
}
JSDoc 是 JavaScript 的文檔標準,用來為函數、類別、變數等加上說明文件。
通常會寫起來方便前後端工程師在做的時候
給自己或是夥伴們看的註記
src/App.vue
<script setup>
import { ref, onMounted } from 'vue'
import { OrderService } from './services/orderService'
import OrderList from './components/OrderList.vue'
import AddOrder from './components/AddOrder.vue'
import EditOrder from './components/EditOrder.vue'
const orders = ref([])
const loading = ref(false)
const error = ref('')
// 讀取
async function loadOrders() {
loading.value = true
error.value = ''
try {
orders.value = await OrderService.list()
} catch (e) {
error.value = e?.message || '載入失敗'
} finally {
loading.value = false
}
}
// 新增
async function handleCreate(order) {
await OrderService.create(order)
await loadOrders()
}
// 更新
async function handleUpdate({ id, patch }) {
await OrderService.update(id, patch)
await loadOrders()
}
// 刪除
async function handleRemove(id) {
await OrderService.remove(id)
await loadOrders()
}
onMounted(loadOrders)
</script>
<template>
<main style="padding:12px">
<h1>Day 9.5 — Axios + Service:飲料訂單 CRUD</h1>
<AddOrder @submit="handleCreate" />
<section style="margin:12px 0">
<button @click="loadOrders">重新載入</button>
<span v-if="loading" style="margin-left:8px">Loading...</span>
<span v-if="error" style="margin-left:8px; color:#c00">{{ error }}</span>
</section>
<OrderList
:orders="orders"
@remove="handleRemove"
/>
<EditOrder
v-if="orders.length"
:orders="orders"
@update="handleUpdate"
/>
</main>
</template>
職責:做為頁面容器 / 單一資料來源(Single Source of Truth):
持有 orders / loading / error
呼叫 OrderService 完成 CRUD
把資料與事件用 props / emits傳遞給子元件(, , )
為什麼把資料放在 App?
這是本頁的最小共享範圍:表單、清單、統計都要用到同一份 orders。
放在 App 能確保資料只修改一次,子元件只負責顯示/觸發事件。
// 原有:靜態資料
const orders = reactive([
{ name: 'alice', note: '', drink: '紅茶', sweetness: '正常甜', ice: '正常冰' },
// ...
])
// 改為:動態資料 + 狀態管理
const orders = reactive([]) // 空陣列,從 API 載入
const loading = ref(false) // 載入狀態
const error = ref('') // 錯誤訊息
有時候api取得失敗或是資料錯誤 , 我們會希望使用顏色呈現,不一樣的css讓user知道 feedback
潛在限制(以及簡短對策)
App.vue 可能變得複雜
useOrders()
)或用 Pinia 管理共享狀態。深層組件 props 傳遞冗長
Service 層需要維護(端點變動要同步改)
Ps 一句話收斂
http.js
管通道、orderService.js
管語意、App.vue
管狀態與流向。
通道統一、語意清晰、資料單源、UI 純粹 —— 這就是可維護的前端。
程式碼大家可以參考~