上一篇提到,我第一次看到寵物資料從資料庫經過後端 API,最後顯示在前端卡片上。
那一刻真的很有成就感。
原本以為資料已經成功出現在畫面上,代表寵物功能最困難的部分應該結束了。
但很快我就發現,事情沒有這麼順利。
有些欄位可以正常顯示,有些欄位卻是空的;表單裡明明已經填了資料,送出後卻沒有按照預期保存。
當時我第一個反應是:
後來我才逐漸理解,資料即使已經送出或回傳,如果前端、後端和資料庫的欄位名稱沒有明確對應,保存或顯示仍會出現異常。
Day 7 要處理的核心是:
前端、後端和資料庫使用的欄位名稱沒有完全一致。
對人來說,這些欄位看起來都在表示同一件事。
但對程式來說,只要名稱不一樣,就是不同的資料。
在 JavaScript 和 Vue 裡,我們通常會使用 camelCase 命名。
例如:
microchipNumber
bloodType
furColor
photoUrl
camelCase 的特色,是第一個單字使用小寫,後面的單字開頭使用大寫。
例如:
microchip + number
↓
microchipNumber
但在 PostgreSQL 資料庫裡,欄位通常會使用 snake_case。
例如:
microchip_number
blood_type
fur_color
avatar_url
snake_case 會使用底線把單字分開。
這兩種命名方式本身都沒有錯。
問題是,當資料要從前端送到後端,再寫入資料庫時,程式必須知道它們之間的對應關係。
例如:
前端:microchipNumber
後端:microchip_number
資料庫:microchip_number
如果沒有做轉換,後端接收到的是:
{
microchipNumber: '123456789'
}
但後端真正要讀取的是:
req.body.microchip_number
這時候取得的結果就會是:
undefined
資料明明存在於 Request 裡,但因為程式讀取了不同的 key,所以看起來就像資料沒有傳過來。
以前學習 JavaScript 物件時,我知道可以透過 key 取得 value。
例如:
const pet = {
name: 'Milk',
microchipNumber: '123456789',
}
取得名字時可以寫:
pet.name
取得晶片號碼時可以寫:
pet.microchipNumber
但如果寫成:
pet.microchip_number
結果會是:
undefined
因為物件裡根本沒有叫做 microchip_number 的 key。
雖然 microchipNumber 和 microchip_number 對人來說意思一樣,但 JavaScript 不會自動幫我們判斷它們是同一個欄位。
程式只會確認:
這個 key 是否完全相同?
這也是我第一次真正理解,欄位名稱不是只有「看起來差不多」就可以。
只差一個底線、大小寫或單複數,程式就會讀不到資料。
在 PawPal 的寵物功能裡,前端和後端之間有幾個需要特別處理的欄位。
例如:
| 前端欄位 | 後端/資料庫欄位 |
|---|---|
microchipNumber |
microchip_number |
bloodType |
blood_type |
furColor |
fur_color |
photoUrl |
avatar_url |
note |
notes |
前面三組比較容易理解,只是 camelCase 和 snake_case 的差別。
但後面兩組不只是命名格式不同,連單字本身也不完全一樣:
photoUrl ↔ avatar_url
note ↔ notes
如果只是把大寫字母換成底線,這兩個欄位仍然無法正確對應。
所以欄位轉換不能只靠猜測,也不能假設所有欄位都能使用同一種自動規則處理。
photoUrl 是前端接收與使用寵物資料時的欄位;現行新增表單選取圖片後,會傳出 avatarFile,再由 API 層以 avatar 作為檔案欄位送出。
以下不是現行新增寵物表單的逐字資料結構,而是為了說明欄位 mapping,使用一個不包含圖片檔案上傳流程的簡化 JSON 範例:
const form = {
name: 'Milk',
microchipNumber: '123456789',
bloodType: 'A',
furColor: '灰色',
photoUrl: 'https://example.com/milk.jpg',
note: '容易緊張',
}
從前端的角度看,資料都存在。
在畫面上輸入時,也不一定會出現問題。
但如果直接把這個物件送給後端:
await axios.post('/api/v1/pets', form)
後端實際讀取的欄位是:
{
name: 'Milk',
microchip_number: '123456789',
blood_type: 'A',
fur_color: '灰色',
avatar_url: 'https://example.com/milk.jpg',
notes: '容易緊張',
}
這兩個物件看起來很像,實際上卻包含不同的 key。
所以「表單裡有資料」只能證明前端狀態中存在這些內容。
它不能直接證明:
這也是我當時一直卡住的原因。
我只看到畫面上已經有資料,就以為後面的流程應該也會知道這些資料代表什麼。
但程式不會自己理解欄位之間的關係。
我們必須明確告訴它怎麼轉換。
最直接的做法,是在表單送出前建立一個新的物件。
下面是簡化後的概念範例:
const payload = {
name: form.name,
microchip_number: form.microchipNumber,
blood_type: form.bloodType,
fur_color: form.furColor,
avatar_url: form.photoUrl,
notes: form.note,
}
await axios.post('/api/v1/pets', payload)
這樣後端就能收到自己期待的欄位名稱。
這個方式在功能比較少時看起來沒有問題。
但當新增、編輯和圖片更新都需要處理寵物資料時,相同的轉換就會散落在很多地方。
例如:
AddPetModal
PetProfileModal
Pet Store
其他寵物元件
每個地方都自己轉一次,久了很容易出現:
當時的我只想讓功能先動起來,不一定會立刻想到維護問題。
但隨著寵物功能越做越多,就會開始感受到重複轉換帶來的混亂。
PawPal 的寵物資料欄位轉換集中在:
src/api/pet.js
前端元件可以繼續使用比較符合 JavaScript 習慣的 camelCase。
API 層則負責把資料轉換成後端需要的格式。
實際的 Request 轉換函式名稱是 mapPetToApi。以下保留與本篇主題相關的簡化示意:
const mapPetToApi = (pet) => ({
name: pet.name,
microchip_number: pet.microchipNumber,
blood_type: pet.bloodType,
fur_color: pet.furColor,
avatar_url: pet.photoUrl,
notes: pet.note,
})
送出資料時:
const payload = mapPetToApi(form)
await axios.post('/api/v1/pets', payload)
上面的 Axios 範例只聚焦欄位轉換;PawPal 實作會從 localStorage 取得 pawpal_token,並在請求附上 Authorization: Bearer <token>。
實際新增時,createPetRequestData 會先使用 mapPetToApi:沒有 avatarFile 時送出 JSON;有圖片檔案時改用 FormData,並以 avatar 作為檔案欄位。
這樣元件不需要知道資料庫使用哪些欄位名稱。
它只需要處理前端畫面真正需要的資料。
資料流會變成:
Vue 表單
使用 camelCase
↓
src/api/pet.js
統一進行欄位轉換
↓
Express API
使用後端欄位格式
↓
PostgreSQL
使用 snake_case
我後來才理解,API 檔案不只是集中放 Axios 請求。
它也可以成為前端和後端之間的翻譯層。
欄位轉換不只發生在「送出資料」的時候。
當後端從資料庫取得資料並回傳給前端時,API 層也需要補上前端習慣使用的 camelCase 欄位。
後端回傳的資料例如:
{
name: 'Milk',
microchip_number: '123456789',
blood_type: 'A',
fur_color: '灰色',
avatar_url: 'https://example.com/milk.jpg',
notes: '容易緊張',
}
但前端元件讀取的是:
pet.microchipNumber
pet.bloodType
pet.furColor
pet.photoUrl
pet.note
如果沒有整理這些欄位,畫面仍然可能會顯示空白。
下面同樣是簡化後的概念範例:
const mapPetFromApi = (pet) => ({
...pet,
microchipNumber: pet.microchip_number,
bloodType: pet.blood_type,
furColor: pet.fur_color,
photoUrl: pet.avatar_url,
note: pet.notes,
})
取得資料後:
const response = await axios.get('/api/v1/pets')
const pets = response.data.pets.map(mapPetFromApi)
這樣前端其他地方就能統一使用 camelCase。
也就是說,API 層需要處理兩個方向:
送出資料
camelCase → 後端欄位格式
取得資料
後端欄位格式
↓
API 層整理
↓
前端使用 camelCase
看到這裡,可以問一個問題:
為什麼不乾脆讓前端、後端和資料庫全部使用相同欄位名稱?
如果專案一開始就能統一,確實可以減少很多轉換。
但實際開發時,不同環境有自己的命名習慣:
snake_case
而且像:
photoUrl ↔ avatar_url
note ↔ notes
這種情況不是單純改大小寫就能解決。
所以真正重要的不是強迫每一層長得一模一樣,而是:
必須清楚決定誰負責轉換,而且轉換規則只能有一個主要來源。
在 PawPal 裡,把這件事集中在 src/api/pet.js,就能避免每個 Vue 元件各自處理。
當時遇到資料沒有正常顯示時,我一開始沒有完整的排查順序。
很容易看到畫面是空的,就開始到處修改程式。
後來我慢慢整理出一個比較清楚的檢查方式。
先確認使用者輸入後,前端物件裡是不是真的有資料。
例如:
console.log(form)
要確認的不是只有 value,還要注意 key:
{
microchipNumber: '123456789'
}
表單有資料,不代表送出去的資料也正確。
在瀏覽器開發者工具的 Network 裡,可以查看 Request Payload。
確認送出的內容是:
microchip_number
還是:
microchipNumber
這一步可以幫助判斷,問題發生在送出 API 之前,還是後端收到資料之後。
接著確認 API 回傳了什麼。
例如後端回傳:
{
microchip_number: '123456789'
}
但前端卻讀取:
pet.microchipNumber
這時就能知道,資料其實已經回來了,只是前端使用了不同的 key。
如果 Request Payload 看起來正常,就繼續檢查後端。
例如後端是讀取:
req.body.microchip_number
還是:
req.body.microchipNumber
前端送出的 key 和後端讀取的 key 必須一致,否則後端就會取得 undefined。
最後再到資料庫確認:
NULL
這樣才能分辨問題到底發生在:
表單
↓
Request
↓
後端
↓
資料庫
↓
Response
↓
前端畫面
這次問題讓我學到一件很重要的事。
有些 Bug 不一定會出現明顯的紅色錯誤訊息。
程式沒有當掉,API 甚至也回傳成功。
但資料就是沒有顯示,或某些欄位一直是空的。
這時候真正需要確認的是:
以前遇到問題時,我常常只看畫面最後有沒有成功。
後來才知道,比起一直猜原因,更有效的方式是沿著資料流一層一層確認。
如果現在重新處理同樣的功能,我會在開發前先整理一份欄位對照表。
例如:
| 畫面用途 | 前端欄位 | API/資料庫欄位 |
|---|---|---|
| 晶片號碼 | microchipNumber |
microchip_number |
| 血型 | bloodType |
blood_type |
| 毛色 | furColor |
fur_color |
| 寵物照片 | photoUrl |
avatar_url |
| 備註 | note |
notes |
接著明確決定:
所有欄位轉換都由 src/api/pet.js 負責
元件只處理:
API 層則處理:
這樣遇到欄位問題時,就不用在很多元件之間來回尋找。
剛開始寫專案時,只要畫面沒有顯示資料,我就會覺得問題很嚴重。
有時候甚至不知道應該從哪一個檔案開始看。
但這次欄位對不上的問題,讓我第一次建立了比較具體的排查方向。
我會開始問:
表單裡有資料嗎?
↓
Request 送出了什麼?
↓
後端讀取哪個欄位?
↓
資料庫保存在哪裡?
↓
Response 回傳什麼?
↓
前端最後讀取哪個 key?
錯誤並沒有因此消失。
之後還是會遇到新的 Bug,也還是會卡住。
但至少我不再只是看到畫面不對,就立刻到處修改程式。
我開始知道,可以沿著資料流一步一步縮小問題範圍。
這次欄位對應問題,讓我整理出幾個重點:
microchipNumber 和 microchip_number 對程式來說是不同欄位。camelCase 和 snake_case 可以同時存在,但必須有清楚的轉換規則。photoUrl 與 avatar_url 這類欄位不能只靠自動改大小寫處理。這次我學到的不只是兩種命名方式。
更重要的是,我開始知道一筆資料經過不同層時,格式會改變,而每一次改變都需要有人負責。
欄位名稱統一後,寵物資料終於可以正確送進後端。
但新增成功,不代表畫面會自動出現新的寵物資料。前端還需要重新取得列表,並把 API 回傳的資料顯示成寵物卡片。
Day 8,我會從「新增」到「顯示」,整理寵物資料如何透過 POST、GET 與前端畫面完成一段完整的資料流程。
下一篇:
Day 8|從「新增」到「顯示」,我的第一個 CRUD 完成了!