iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0
JavaScript

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

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

  • 分享至 

  • xImage
  •  

今天的故事

上一篇,我完成了修改寵物資料的功能。

使用者不只可以新增與查看寵物,也可以打開寵物基本資料,修改原本儲存的內容。

做到這裡之後,CRUD 還剩下最後一個基本操作:

Delete。

一開始,我覺得刪除功能應該是四個操作裡最簡單的一個。

不需要處理一大堆表單欄位,也不用考慮哪些資料被修改。

只要取得寵物 ID,呼叫 DELETE API,再把畫面上的卡片移除,應該就完成了。

但實際測試時,我卻遇到一個讓我很錯愕的情況:

寵物卡片明明已經消失了,重新整理頁面後,為什麼又跑回來?

那一刻我才發現,畫面上看起來刪除成功,不代表資料真的被刪除了。


我原本以為,呼叫 DELETE API 就完成了

處理新增功能時,需要準備表單資料。

處理修改功能時,需要帶入原始內容、整理欄位,再把更新後的資料送出去。

和前面兩個功能相比,刪除看起來單純很多。

在我的想像中,流程大概是:

點擊刪除
↓
取得寵物 ID
↓
呼叫 DELETE API
↓
從畫面移除寵物卡片
↓
完成

因為刪除不需要傳送一整張表單,我原本覺得這應該很快就可以完成。

但真正實作後才知道,即使程式碼看起來不多,仍然有很多事情需要確認。

例如:

  • 目前刪除的是哪一隻寵物?
  • 是否需要再次向使用者確認?
  • DELETE 請求是否真的成功?
  • 後端刪除的是登入者自己的寵物嗎?
  • 資料庫裡的資料是否真的消失?
  • 前端要直接移除卡片,還是重新取得資料?
  • 如果後端失敗,畫面卻先把卡片移除,應該怎麼處理?

刪除不是只有「讓畫面上的東西消失」,而是前端、API、後端和資料庫都必須保持一致。


為什麼刪除前需要再次確認?

刪除和新增、修改有一個很大的差別。

新增或修改內容後,通常還可以再調整。

但刪除資料後,使用者通常無法直接把內容恢復回來。

所以在真正送出 DELETE 請求前,畫面需要讓使用者再次確認。

PawPal 的流程是先從首頁的寵物卡片開啟 PetProfileModal,再按下「刪除資料」。這個彈窗會把選到的寵物透過 delete 事件交給 DashboardView,由 Dashboard 開啟共用的 DeleteConfirmModal

確認視窗的標題是「確認刪除寵物資料?」;它會顯示「刪除後將無法復原」,並帶入寵物名稱。按鈕文字分別是「確定刪除」和「取消」。按下取消、右上角關閉按鈕或視窗外的遮罩,都只會關閉確認視窗,不會送出請求。

流程如下:

點擊刪除資料
↓
顯示確認視窗
↓
使用者取消
→ 關閉視窗,不執行刪除

使用者確認
→ 送出 DELETE 請求

當時的我原本只想趕快把功能做完,沒有立刻意識到確認步驟的重要性。

後來才理解,確認視窗不是多餘的畫面。

它是在避免使用者因為誤觸,就永久失去資料。


寵物 ID 仍然是刪除功能的關鍵

和修改功能一樣,刪除資料時,後端也必須知道目標是哪一隻寵物。

在 PawPal 裡,Dashboard 會把目前選取寵物的 id 和登入狀態保存的 Token 交給 store:

const result = await petStore.deletePet(
  petToDelete.value.id,
  authStore.token,
)

store 接著呼叫 src/api/pet.jsdeletePet,以 Axios 送出:

DELETE /api/v1/pets/:id
Authorization: Bearer <token>

寵物 ID 放在路徑的 :id,這支 DELETE 請求沒有 Request body。

但只有 ID 還不夠。

後端 route 先經過 authenticateToken,從 Bearer Token 驗證出登入者,並把 JWT 中的使用者 ID 放到 req.userId。資料庫刪除時再同時使用 iduser_id

DELETE FROM pets
WHERE id = $1 AND user_id = $2

因此,前端傳送 ID 只是告訴後端「想操作哪一筆資料」。真正決定能不能刪除,仍然需要後端驗證登入身份與資料擁有權。


卡片消失的那一刻,我以為成功了

第一次測試刪除功能時,我點擊寵物卡片上的刪除按鈕。

確認刪除後,那張寵物卡片從畫面上消失。

看到卡片消失時,我的第一個反應是:

成功了,刪除功能完成了。

因為從畫面上看,一切都符合預期。

原本存在的卡片不見了,也沒有出現明顯的錯誤。

對當時的我來說,這已經很像一個完成的功能。

但我後來重新整理頁面時,卻發現剛剛刪除的寵物又出現了。

當下我真的很錯愕。

我第一個懷疑的是:

剛才是不是根本沒有真的刪除成功?

這個情況讓我第一次很直接地看到,前端畫面和資料庫狀態會出現不一致。


為什麼重新整理後資料又回來了?

當卡片從畫面消失時,至少代表當下畫面使用的寵物清單已經發生變化。

重新整理 Dashboard 後,onMounted 會再次呼叫 petStore.fetchPets(),透過 GET /api/v1/pets 重新取得登入會員的寵物資料。

如果重新取得資料後,剛才消失的寵物再次出現,就代表畫面上的刪除結果,和後端實際回傳的資料仍然不一致。

當時我看到的情況是:

卡片從畫面消失
↓
畫面看起來刪除成功
↓
重新整理頁面
↓
前端重新取得寵物列表
↓
剛才消失的卡片再次出現

這是我當時的真實測試經驗。現有的 Issue、PR、Commit 與程式碼歷史沒有保留足以重建當時根因的紀錄,所以我不把問題歸到 API 路徑、ID、Token、SQL 或其他單一環節。

另外,Issue #248 曾記錄過一段尚未串接完成的狀態:PetProfileModal 已有刪除按鈕和 delete 事件,但 Dashboard、store 與 Axios 的 DELETE 呼叫尚未接上。

這筆紀錄不能直接證明就是我當時遇到問題的原因,但它能說明,畫面上的刪除互動和真正刪除後端資料,是兩段都必須完成的流程。


畫面消失,不等於資料真的刪除了

這次經驗讓我理解到,前端的變化只代表目前畫面使用的資料狀態改變。

它不一定能證明後端與資料庫也已經完成相同操作。

刪除功能至少需要確認三件事:

1. DELETE Request 是否成功

前端是否有把正確的寵物 ID 放進路徑並帶上 Bearer Token。

PawPal 成功刪除時回傳 204 No Content,沒有 Response body。API 模組把這個成功結果整理成前端可使用的 success: true 與預設成功訊息。

寵物 ID 格式錯誤時,後端回傳 400;缺少 Token、Token 無效或 JWT 的使用者 ID 無效時回傳 401;找不到這筆寵物,或這筆資料不屬於登入會員時回傳 404;未預期的伺服器錯誤則回傳 500

2. 資料庫是否真的刪除資料

資料是否已經從 pets 資料表消失。

重新呼叫 GET API 時,是否還會取得這筆資料。

3. 前端狀態是否同步

刪除成功後,寵物清單是否正確更新。

刪除失敗時,卡片是否仍然保留,並讓使用者知道發生錯誤。

只有三個部分都一致,刪除功能才算真正完成。


DELETE API 應該在什麼時候更新畫面?

在實作刪除功能時,前端更新畫面大致有兩種做法。

第一種是先等待 DELETE API 成功,再移除畫面上的資料。

送出 DELETE
↓
等待後端成功回應
↓
從前端清單移除寵物

第二種則是先讓畫面移除,再等待後端結果。

先從畫面移除
↓
送出 DELETE
↓
失敗時恢復資料

第二種做法能讓畫面感覺更快,但也需要處理失敗後如何恢復。

PawPal 採用第一種做法。petStore.deletePet 會先等待 Axios 的結果,只有 result.success 為真時,才用 filterpetStore.pets 移除相同 ID 的資料:

const result = await deletePetApi(petId, token)

if (result.success) {
  pets.value = pets.value.filter((pet) => pet.id !== petId)
}

它不會在刪除後重新呼叫 GET,也不是先移除卡片的樂觀更新。API 失敗時,store 不會修改寵物清單,Dashboard 則顯示錯誤提示。

無論使用哪一種方式,重點都是:

前端不能只改畫面,卻不確認後端是否真的成功。


刪除成功後,寵物清單還要更新

後端成功刪除資料後,前端仍然要更新寵物清單。

PawPal 直接把已刪除的寵物從目前清單中移除;若原本選取的正是這隻寵物,store 也會改選清單中的第一隻,或在清單已空時設為 null

因此刪除功能真正完成時,是:

使用者確認刪除
↓
DELETE API 成功
↓
資料庫移除資料
↓
前端更新寵物清單
↓
卡片從畫面消失
↓
重新整理後資料也不會回來

這段流程也讓我知道,畫面上的結果要和真正資料來源一起檢查,才不會只停在第一眼看起來成功。


錯誤提示也是刪除流程的一部分

刪除資料是不可逆或不容易恢復的操作,因此成功與失敗的提示非常重要。

PawPal 的 Dashboard 會用 isDeletingPet 管理刪除中的狀態。送出請求後,確認視窗的「確定刪除」會顯示「刪除中...」,確認、取消與關閉操作都會停用;Dashboard 和確認視窗本身也會阻止重複觸發。

這層防護是 PR #331 補上的。它要求第一次點擊確認刪除後只送出一次 DELETE,請求結束後才恢復按鈕狀態。

成功時,Dashboard 顯示「寵物資料刪除成功」的 success Toast;失敗時顯示 API 回傳的訊息,或「寵物資料刪除失敗,請稍後再試」的 error Toast。Toast 元件固定在畫面上方,預設顯示 2.5 秒。

這讓我理解到:

除了把資料刪掉,使用者是否清楚知道操作結果,也是刪除功能是否完整的一部分。


關聯資料讓刪除不只是刪一列

PawPal 的寵物資料放在 pets 資料表。除了使用者與寵物之間的 user_id 外,專案的 medical_recordsgrowth_recordscalendar_events 都以 pet_id 參照寵物,且 schema 設定了 ON DELETE CASCADE

因此,刪除一隻寵物時,這三類和該寵物相連的資料會由資料庫連帶刪除,不需要後端逐列清理。

行事曆還有另一層處理。controller 會先依寵物 ID 和登入者的 user ID 收集已同步的 google_event_id,再刪除寵物;本地資料庫連帶刪除行事曆資料後,才逐筆嘗試刪除 Google Calendar 上的對應事件。外部同步失敗會在同步流程中處理,並不會把已完成的本地刪除還原。

寵物的照片網址存放在 avatar_url 欄位。現有刪除流程沒有呼叫 Cloudinary 的資源刪除方法,因此文章不把圖片檔已同步刪除寫成既有行為。


從重新出現的卡片,學到怎麼驗證刪除

這次踩坑之後,我不再只看卡片是否從畫面上消失。

我會繼續確認:

  • DELETE API 的 status code。
  • 後端 Response。
  • 資料庫是否還有那筆寵物。
  • 前端清單是否正確更新。
  • 重新整理後資料是否仍然消失。
  • GET API 是否還會回傳該筆資料。
  • 刪除失敗時,畫面是否維持原本狀態。

以前測試刪除時,我只做了第一眼看得到的確認:

卡片不見了。

但現在我知道,重新整理頁面也是很重要的測試。

因為重新整理後,前端會再次從後端取得資料。

如果資料又出現,就代表畫面和真正的資料來源之間仍然不一致。


刪除自己的寵物時,感受和新增完全不同

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

修改資料時,我則感覺這個功能變得更完整。

但測試刪除時,感受其實很不一樣。

即使我知道那只是測試資料,按下確認刪除時,還是會多停一下。

因為刪除不像修改。

它不是把內容換成另一個版本,而是讓整筆資料從系統中消失。

這也讓我更能理解,為什麼刪除功能需要確認、提示和更完整的驗證。

技術上是一支 DELETE API,對使用者來說,這是一個需要慎重處理的動作。


如果現在重新做一次

如果現在重新製作刪除功能,我會先確認:

  • DELETE API 的實際路徑。
  • 寵物 ID 從哪裡取得。
  • 後端是否同時驗證登入者與資料擁有權。
  • 使用者確認刪除的流程。
  • 按鈕處理中是否會停用。
  • API 成功後才移除畫面,還是使用樂觀更新。
  • 刪除失敗時是否保留或恢復原本卡片。
  • 資料庫是否真的刪除資料。
  • 是否存在外鍵或關聯資料限制。
  • 前端清單如何同步。
  • 成功和失敗時如何提示。
  • 重新整理後資料是否仍然消失。

以前我會把刪除功能的完成標準設成:

卡片從畫面消失。

現在我會把標準改成:

API、資料庫、前端狀態和重新整理後的結果都一致。


這次我學到的事

完成刪除功能後,我學到:

  • DELETE 看起來簡單,仍然需要處理完整資料流程。
  • 寵物 ID 是指定刪除目標的重要依據。
  • 後端需要驗證資料是否屬於目前登入者。
  • 畫面上的卡片消失,不代表資料庫已經刪除。
  • API 成功後,前端清單仍然需要同步。
  • 刪除失敗時,不應讓畫面誤導使用者。
  • 確認視窗可以降低誤刪風險。
  • 重新整理頁面是驗證刪除是否真正完成的重要方法。
  • 關聯資料與外鍵會影響刪除流程。
  • 功能完成的標準,是前端、後端與資料庫狀態一致。

以前我以為刪除功能只是呼叫一支 API。

真正踩過坑後才理解,讓資料消失只是表面。

更重要的是確認它真的從資料來源中被移除,而且畫面不會因為狀態不同步而欺騙使用者。


下一篇預告

完成新增、顯示、修改與刪除後,我第一次把寵物資料的基本操作完整串起來。

但當功能越來越多,我也開始遇到另一個問題:

使用者登入之後,網站到底要怎麼記得「我已經登入了」?

原本我以為登入成功,會員功能應該就差不多完成了。

但真正開始處理登入狀態後,我才發現事情沒有這麼簡單。

下一篇:

Day 11|登入功能上線!我以為會員功能完成了……


上一篇
Day 9|第一次修改資料,才知道 PUT 和 PATCH 差很多
下一篇
Day 11|註冊成功後要去哪裡?從註冊頁開始理解會員流程
系列文
從看不懂到做出來,用 PawPal 走過前端新手村14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言