iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
JavaScript

從看不懂到做出來,用 PawPal 走過前端新手村系列 第 9

Day 9|第一次修改資料,才知道 PUT 和 PATCH 差很多

  • 分享至 

  • xImage
  •  

今天的故事

上一篇,我完成了新增與顯示寵物資料的流程。

使用者填完表單後,資料可以送到後端、寫進資料庫,最後顯示成 Dashboard 上的寵物卡片。

第一次看到自己的寵物出現在畫面上時,我真的很有成就感。

但資料建立完成後,很快又會遇到下一個很實際的需求:

如果寵物資料填錯了,要怎麼修改?

一開始,我覺得修改功能應該和新增差不多。

反正都是一張表單,只是新增是空白表單,修改則是先把原本的資料放進去。

使用者改完之後,再把整張表單送給後端就好了。

真正做過之後,我才發現,修改資料沒有想像中那麼單純。

我不只需要處理表單原始資料、寵物 ID、API 和資料庫更新,還第一次遇到 PUT 和 PATCH 的差別。


我原本以為,修改就是再送一次表單

製作新增功能時,表單一開始是空白的。

使用者填入寵物名字、生日、體重和其他資料後,再按下送出。

但修改資料時,表單不能是空白的。

使用者按下寵物卡片上的「修改資料」後,應該先看到原本已經儲存的內容。

流程大概是:

點擊寵物卡片
↓
開啟寵物基本資料彈窗
↓
點擊修改資料
↓
帶入原本的寵物資料
↓
使用者修改欄位後儲存
↓
前端送出 PATCH 請求
↓
後端更新資料庫
↓
前端顯示最新資料

當時的我只覺得:

把原本的資料放進表單,再送一次不就好了嗎?

但開始實作後,我才知道中間有很多地方需要處理。

例如:

  • 要怎麼知道目前修改的是哪一隻寵物?
  • 原本的資料要怎麼帶入表單?
  • 沒有修改的欄位還要不要送?
  • 修改一個欄位和修改整筆資料有什麼差別?
  • API 成功後,畫面要怎麼同步?
  • 為什麼有時候修改成功,畫面卻還是舊資料?

這些問題,都是我在真正開始製作修改功能後才遇到的。


編輯表單和新增表單看起來很像,但狀態不同

新增與修改功能會處理不少相同的寵物資料。

新增表單包含寵物名字、種類、品種、性別、生日、體重、晶片號碼、結紮狀態、血型、毛色、備註和照片。

PawPal 的 PetProfileModal 編輯狀態會回填名字、品種、性別、生日、體重、晶片號碼、結紮狀態、血型、毛色、備註和照片;種類不是這個編輯彈窗提供修改的欄位。

但兩個功能的起始狀態並不一樣。

新增時,表單資料大多從空值開始。

修改時,Dashboard 先把點擊的寵物交給 PetProfileModal,彈窗再從 props.pet 建立編輯狀態,讓使用者可以在原本內容上繼續修改。

以下是 PetProfileModal 的「簡化概念」:

function createEditForm() {
  const pet = props.pet ?? {}

  return {
    name: pet.name ?? '',
    breed: pet.breed ?? '',
    gender: normalizeGender(pet.gender),
    birthday: formatBirthdayForDateInput(pet.birthday),
    weight: pet.weight ?? '',
    microchipNumber: pet.microchipNumber ?? pet.microchip_number ?? '',
    neutered: pet.neutered ? '已結紮' : '未結紮',
    // 其餘可編輯欄位依同樣方式帶入
  }
}

日期會整理成日期輸入框需要的 YYYY-MM-DD;體重保留為可輸入的值;結紮狀態則在送出前轉回布林值。

以前我看到「表單預填」時,只覺得是把資料顯示在輸入框裡。

但真正實作後才理解,這些欄位同時也是之後要送出的資料狀態。

如果一開始帶入的格式不正確,使用者即使只改了一個欄位,送出時也會讓其他欄位出問題。


第一次遇到 PUT 和 PATCH

處理更新 API 時,我第一次比較認真接觸 PUT 和 PATCH。

在一開始的理解裡,我只知道它們都和「修改資料」有關。

看起來好像只是兩種不同的寫法。

後來才慢慢知道,它們在概念上有不同的使用情境。

PUT 通常用來更新或取代一整筆資料。

PATCH 通常用來更新其中一部分資料。

例如,先用「簡化概念」表示一筆寵物資料:

{
  "name": "小花",
  "weight": 8,
  "furColor": "棕色"
}

如果使用者只修改體重,PATCH 的概念可以只送出:

{
  "weight": 9
}

而 PUT 的概念則接近把更新後的整筆資料重新送出:

{
  "name": "小花",
  "weight": 9,
  "furColor": "棕色"
}

PawPal 的寵物更新路由實際使用 PATCH /api/v1/pets/:id,沒有為這個功能設定 PUT 路由。

前端的 src/api/pet.js 會先整理 payload,再以 Axios 送出請求:

const request = updatePetRequestData(data, token)

const response = await axios.patch(
  `${API_BASE_URL}/api/v1/pets/${id}`,
  request.data,
  { headers: request.headers },
)

Request header 會帶入 Authorization: Bearer <token>。JSON payload 使用 snake_case,可提供 namespeciesbreedgenderbirthdayweightmicrochip_numberneuteredblood_typefur_colornotesavatar_url 中的一個或多個欄位;選擇新照片時,payload 會改為帶有 avatar 檔案的 FormData

後端不會把整筆資料一律覆蓋,而是從 Request 中取出有提供的欄位,組成 UPDATE pets SET ...。因此,PawPal 的 HTTP method 是 PATCH,資料庫也只更新 payload 內的欄位。

更新成功時,後端回傳 200 OK{ pet: ... };這個 pet 包含更新後的資料庫欄位。Request 資料驗證失敗時回傳 400,未登入或 Token 無效時回傳 401,找不到目標或不屬於登入會員時回傳 404,晶片號碼重複時回傳 409,未預期錯誤則回傳 500。前端 API 層會把後端的錯誤訊息整理成畫面可使用的繁體中文提示。

這讓我理解到,HTTP method 和表單實際送出的欄位範圍要分開看。


我原本把修改過和沒修改過的欄位一起送出

當時製作編輯功能時,我的想法比較接近:

不管使用者修改了哪一個欄位,就把整張表單全部送出去。

因為表單裡已經有原本資料,即使使用者只修改體重,其他欄位也會繼續保留原本的值。

這種做法對當時的我來說比較容易理解。

我不需要另外判斷每個欄位是否改變,只要把目前表單的狀態送給後端即可。

PawPal 的編輯彈窗也是從目前表單狀態建立更新資料;送出前的 mapPetToApi 會把前端欄位轉成後端欄位,並略過空字串、nullundefined,同時保留 false0

以下只列出部分欄位,作為「簡化概念」:

{
  microchip_number: microchipNumber,
  neutered,
  blood_type: bloodType,
  fur_color: furColor,
  notes: note,
}

這樣一來,microchipNumberbloodTypefurColornote 不會直接混進後端使用的 snake_case payload。

我也開始思考:

  • 沒有修改的欄位真的需要重新送出嗎?
  • 空字串在送出前如何處理?
  • 如果照片沒有重新選擇,原本照片要怎麼保留?
  • 日期和布林值在表單中會不會改變格式?
  • 前端欄位名稱和後端欄位名稱是否仍然一致?

照片沒有重新選擇時,selectedPhotoFile 維持空值,更新請求不會帶 avatar 檔案或新的 avatar_url;資料庫的原本照片欄位因此保留。選擇新照片後,前端改用 FormData,把裁切後檔案放進 avatar 欄位,後端完成擁有權檢查後才上傳並寫入新的 avatar_url

修改功能和新增功能雖然都在送資料,但更新舊資料時,還需要更小心保留原本的內容。


寵物 ID 是修改功能的重要關鍵

新增資料時,還沒有一筆已存在的寵物可以指定。

但修改功能不一樣。

後端必須知道使用者目前要修改的是哪一筆資料。

所以修改請求除了表單內容,還需要知道目標寵物的 ID。

PawPal 的彈窗送出時會帶入目前寵物的 id

emit('update', {
  id: props.pet?.id,
  data: buildUpdatePayload(),
})

Dashboard 接到事件後,呼叫 petStore.updatePet(id, data, authStore.token);API 路徑中的 :id 就是這個寵物 ID。

後端先用 Bearer Token 取得登入者的 userId

資料庫更新條件可以用「簡化概念」表示為:

UPDATE pets
SET ...
WHERE id = $1 AND user_id = $2
RETURNING ...

其中:

  • id 用來指定要修改哪一隻寵物。
  • user_id 確保只能更新登入會員自己的寵物。

這也是我第一次更明確地理解,資料庫裡的 ID 不只是流水號。

對前端來說,它是找到特定資料的重要依據。

找不到 ID、寵物不存在,或寵物不屬於登入者時,後端都回傳 404;它不以不同訊息暴露其他會員的寵物資料。


API 成功後,畫面還要同步

完成更新 API 後,我原本以為,只要後端回傳成功,修改功能就完成了。

但我很快又遇到和新增功能相似的問題:

API 已經成功了,為什麼卡片還是顯示舊資料?

資料庫更新成功,只代表後端資料已經改變。

如果前端沒有同步更新寵物清單,畫面就可能繼續保留修改前的狀態。

因此更新完成後,前端需要用後端回傳的新資料替換原本的寵物資料,再讓畫面重新渲染。

PawPal 的 petStore.updatePet 會直接更新同一個 ID 的項目,不會在更新成功後重新呼叫 GET:

pets.value = pets.value.map((pet) => {
  if (pet.id !== petId) return pet

  return normalizePet({ ...pet, ...updatedPet })
})

Dashboard 的 dashboardPets 是從 petStore.pets 計算出來的資料,接著交給 PetCard 顯示。因此 store 更新後,卡片會跟著重新渲染,不需要手動重新整理頁面。

這個問題也讓我更理解,新增、修改和刪除雖然是不同操作,但都有一個共同點:

資料庫改變後,前端狀態也要跟著更新。


有時候,我必須重新整理才看得到修改結果

在測試修改功能時,我曾遇過一個讓我很困惑的情況。

畫面顯示修改成功後,寵物卡片沒有立刻變化。

我需要手動重新整理頁面,才會看到新的內容。

當時我第一個反應是:

修改到底有沒有成功?

因為對使用者來說,如果按下儲存後畫面沒有變化,就很難知道操作是否真的完成。

這件事讓我發現,API 是否成功和使用者是否感受到成功,是兩件不同的事。

即使資料庫已經更新,如果畫面仍然顯示舊資料,使用者還是會認為功能壞掉了。

PawPal 的更新流程最後改為直接替換 petStore.pets 裡同一個 ID 的資料。當時的 PR 測試紀錄也確認,儲存後彈窗會顯示更新後的資料,Dashboard 卡片不必再依賴手動重新整理。


提示訊息也會影響使用者的判斷

除了卡片資料是否更新,我也開始注意操作後的提示。

使用者按下儲存後,需要知道:

  • 是否正在處理。
  • 修改是否成功。
  • 修改是否失敗。
  • 失敗的原因是什麼。

PawPal 在送出更新時會先把 isPetSaving 設為 true,儲存按鈕會顯示「儲存中...」並停用,避免重複送出。

更新成功後,Dashboard 會呼叫 toastStore.showToast(result.message || '寵物資料更新成功')ToastNotification 掛在 App 最上層,以固定在畫面上方的成功提示顯示,預設停留 2.5 秒。

更新失敗時,錯誤訊息不會由這段更新流程送進 Toast,而是放進 petUpdateError,顯示在寵物基本資料彈窗內。

這讓我知道,除了更新資料之外,操作結果是否有明確提示,同樣會影響使用者是否知道功能成功。


修改自己的寵物,成就感又更進一步

第一次把自己的寵物新增到首頁時,我已經覺得很有成就感。

但完成修改功能後,那種感覺又更進一步。

因為畫面上的資料不再只是固定顯示。

我可以打開編輯表單,修改寵物內容,再看到卡片跟著改變。

這讓整個功能變得更像一個真的可以使用的產品。

使用者不需要因為填錯資料就刪掉重來,也不需要永遠接受第一次輸入的內容。

資料可以被調整、修正和持續使用。

對現在的我來說,這已經是一個很基本的功能。

但對當時的我來說,它代表我開始理解:

前端畫面不是只負責顯示資料,也要讓使用者可以管理資料。


如果現在重新做一次

如果現在重新製作修改功能,我會先確認幾件事:

  • 專案的更新 API 使用哪一種 HTTP method。
  • API 需要整筆資料還是部分欄位。
  • 表單預填資料的格式是否正確。
  • 寵物 ID 從哪裡取得。
  • 沒有修改的欄位是否能正確保留。
  • 空字串、null 和未提供欄位要如何處理。
  • 照片沒有重新選擇時是否保留原圖。
  • 更新成功後,前端狀態如何同步。
  • 使用者是否能立即看到修改結果。
  • 成功或失敗時是否有清楚提示。

以前我比較容易把注意力放在:

API 有沒有成功送出?

現在則會繼續問:

使用者看到的結果,是否真的和資料庫一致?

這也是修改功能帶給我最重要的學習。


這次我學到的事

完成修改資料功能後,我學到:

  • 新增表單和編輯表單的起始狀態不同。
  • 編輯表單需要先帶入原本資料。
  • 修改特定資料時需要正確的 ID。
  • PUT 和 PATCH 在概念上有不同的更新方式。
  • PawPal 的寵物更新使用 PATCH,並由後端更新提供的欄位。
  • 沒有修改的欄位也會影響送出的結果。
  • API 更新成功不代表前端畫面會自動同步。
  • 資料庫狀態和畫面狀態需要保持一致。
  • 使用者是否看得到修改結果,也是功能是否完成的一部分。

以前我覺得修改功能只是把資料再送一次。

真正實作後才發現,更新資料其實同時牽涉到原始資料、表單狀態、API 格式、資料庫與前端畫面。

也因為這次經驗,我開始知道:

功能完成的標準,不只是後端回傳成功,而是使用者能立即看到正確的結果。


下一篇預告

完成新增、顯示與修改之後,寵物資料還差最後一個基本操作:

刪除。

原本我以為刪除功能只要呼叫 DELETE API 就完成了。

但實際測試時,我卻遇到資料看起來消失,重新整理後又跑回來的情況。

下一篇:

Day 10|刪除功能看似簡單,但我踩了一個大坑。


上一篇
Day 8|從「新增」到「顯示」,我的第一個 CRUD 完成了!
下一篇
Day 10|刪除功能看似簡單,但我踩了一個大坑
系列文
從看不懂到做出來,用 PawPal 走過前端新手村12
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言