iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
JavaScript

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

Day 19|換一張頭像,原來不是只把照片傳上去

  • 分享至 

  • xImage
  •  

今天的故事

在 PawPal 裡,會員和寵物都有自己的頭像。

我記得一開始建立資料時,可以先放一張照片,但後來我開始想到一個很實際的問題:

如果之後想換照片怎麼辦?

尤其是寵物。

寵物會慢慢長大,外表也會跟著改變。

如果網站上的照片一直停在最早建立資料時的那一張,好像就少了一點什麼。

所以後來我想把這部分補得更完整,讓會員和寵物都可以重新選擇照片,再換成新的頭像。

而我一開始對這個功能的想像其實非常簡單:

選一張新照片
↓
上傳
↓
把舊照片換掉

當時的我真的只想到這樣。

結果真正開始做之後,我才發現:

換一張頭像,原來不是只把照片傳上去。


第一個問題不是上傳,而是裁切

真正開始做之後,我第一個遇到的問題就是:

照片選完之後,還需要裁切。

因為每個人選擇的照片比例都不一樣。

可能是直式、橫式,也可能人物或寵物根本不在照片正中央。

但 PawPal 的頭像最後是圓形的。

如果直接把原始照片塞進圓形頭像裡,很可能會變成:

頭被切掉
寵物跑到旁邊
真正想留下來的地方不在畫面裡

這時我才開始想:

裁切要自己做嗎?

還是找現成的套件?

最後 PawPal 使用的是:

vue-advanced-cropper

並做了一個共用的:

AvatarCropModal

讓會員頭像、寵物新增照片,以及修改既有寵物照片,都可以使用同一套裁切流程。

實際操作也從原本想像的「選照片、上傳」,變成:

選照片
↓
打開裁切視窗
↓
拖曳圖片
↓
調整縮放
↓
確認裁切
↓
才進入後面的上傳流程

套件裝上去,也不代表操作就完成了

裁切功能做出來之後,使用者可以拖曳照片,也可以調整縮放,把真正想留下來的部分放進圓形範圍裡。

但我印象中,這段拖曳和縮放的操作前後也調整了好幾次。

雖然 Git 沒有留下每一次修改的完整過程,但 Repository 最後有記錄到其中一個實際問題:

縮放滑桿的回饋曾經和圖片實際的縮放狀態不同步。

這也是我第一次比較明顯地感覺到:

套件把核心功能做出來,不代表整個使用體驗就自然完成了。

畫面能動是一回事。

操作起來是不是合理,又是另外一回事。


明明都是同一張圖片,為什麼還要轉這麼多次?

做到「確認裁切」時,我原本以為:

使用者都已經把照片裁好了,接下來直接上傳不就好了嗎?

結果程式裡看到的卻不是:

裁切
↓
上傳

而是:

裁切
↓
Canvas
↓
Blob
↓
File
↓
FormData
↓
上傳

我當時其實完全不懂:

明明看起來都是同一張圖片,為什麼還要轉這麼多次?

在 PawPal 的實作裡,使用者按下確認之後,裁切元件會先取得裁切結果中的 Canvas,再透過:

canvas.toBlob()

把結果轉成 Blob,接著再建立成 File,最後交給既有的圖片上傳流程。

現在回頭看,我會先用很簡單的方式理解:

Canvas
→ 裁切後的影像結果

Blob
→ 圖片的二進位資料

File
→ 帶有檔名、type 等資訊的檔案物件

FormData
→ 把檔案與其他資料一起送給後端

但當時的我其實不是一開始就理解這些差別。

比較真實的情況是:

我先照著流程,把功能做成功。

後來再回頭看、又碰過幾次圖片與檔案處理之後,才慢慢知道它們各自在做什麼。


PawPal 最後把裁切結果統一成 WebP

在這個裁切流程裡,PawPal 最後會把裁切結果輸出成:

512 × 512 的 WebP 圖片

檔名也會重新產生。

例如原本是:

mimi.jpg

裁切後可能會變成:

mimi-cropped.webp

PawPal 當時的實作,是把 Canvas 轉成 Blob,再把 Blob 包成 File,接著交給既有的圖片上傳流程。

不過這裡有一個我後來才知道的細節:

Blob 並不是一定要先變成 File,才能放進 FormData。

這只是 PawPal 當時採用的實作方式。

所以這次對我來說,比較重要的不是背下「一定要 Canvas → Blob → File」。

而是開始知道:

畫面上看到的一張圖片,在程式不同階段裡,可能會是不同形式的資料。


看到新的頭像,不代表已經真的存好了

這個功能還讓我第一次比較明顯地注意到另一件事情:

畫面上的預覽,和後端真的更新成功,是兩回事。

例如會員選擇照片並完成裁切後,畫面可以先顯示新的頭像。

但這個時候還沒有真的更新後端。

只有最後按下「儲存修改」,並且圖片成功上傳、資料更新成功之後,新的 avatar_url 才算真的保存下來。

所以一張照片在這個流程裡,其實可能會經過:

使用者選擇的原始照片
↓
裁切中的圖片
↓
裁切完成的預覽
↓
真正準備上傳的 File
↓
後端更新成功的 avatar_url

以前的我可能會把它們全部想成:

不就是那張照片嗎?

但真的做過一次之後,才慢慢發現:

對使用者來說它們看起來是同一張圖,對程式來說卻可能代表完全不同的狀態。


還遇到了 HEIC/HEIF 的預覽限制

做圖片功能時,我也碰到了 HEIC、HEIF 這類格式。

PawPal 的圖片上傳流程接受:

JPEG
PNG
WebP
HEIC
HEIF

不過「允許上傳」和「瀏覽器能不能直接預覽」又是兩件不同的事情。

裁切的前提是:

瀏覽器必須先把圖片顯示出來,使用者才有辦法拖曳和調整裁切範圍。

所以如果 HEIC/HEIF 在目前的瀏覽器裡無法預覽,裁切視窗會提示使用者改用:

JPG
PNG
WebP

這裡並不是 PawPal 自動把 HEIC/HEIF 轉成 WebP。

而是瀏覽器如果連原始圖片都無法顯示,就沒有辦法進行後面的裁切操作。

這也是我這次才開始注意到:

檔案格式「後端接受」和「前端可以處理」,不一定代表同一件事。


當時其實沒有全部搞懂

如果現在再看到 Canvas、Blob、File、FormData,我已經可以慢慢理解它們大概各自在做什麼。

但如果說我第一次做這個功能時,就已經全部搞懂,那其實不是。

當時比較接近:

先知道這一步要怎麼做,先把整個流程接起來。

等功能真的跑起來之後,我才慢慢回頭理解:

為什麼裁切完不是直接上傳?

為什麼畫面上的預覽不等於真正的檔案?

為什麼看起來都是同一張照片,在程式裡卻有不同的狀態?

也是從這次之後,我才開始覺得:

圖片上傳其實不只是:

<input type="file" />

選完一張照片就結束。

中間也可能有自己完整的資料處理流程。


做完之後,我最有感的反而是畫面

這次做頭像功能,過程中碰到了很多我一開始完全沒有想到的東西:

裁切
Canvas
Blob
File
FormData
WebP
圖片格式

但功能真的完成之後,我自己最有感的反而不是這些技術名稱。

而是:

頭像看起來真的比較完整了。

使用者可以自己決定照片最後要留下哪個位置。

寵物照片也不再只是直接被原始圖片比例限制。

不管最後顯示成比較大的頭像,還是 Header 裡的小頭像,都可以維持比較一致的裁切結果。

回到最一開始讓我想做這個功能的問題:

寵物長大之後,網站上的照片是不是還只能停在以前?

現在至少可以重新選擇一張照片,再自己調整成真正想留下來的樣子。

對我來說,這才是這個功能最後最有感的地方。


本篇重點與心得

這次做會員和寵物頭像,我最大的收穫不是記住多少圖片 API。

而是第一次真的碰到:

一張圖片從畫面走到後端以前,中間可能經過多少處理。

一開始我只想到:

選照片
↓
上傳

真正做下去之後才發現,中間還多了裁切、圖片資料轉換、預覽,以及最後的上傳與資料更新。

原本我只是想:

換一張頭像而已。

最後才發現,真正的圖片上傳不是把照片丟出去就結束了。

而對當時的我來說,先把功能做成功,再慢慢理解每一層為什麼存在,也成了這次實作最重要的學習。


下一篇預告

這次原本只是想換一張照片,最後卻一路碰到裁切、圖片處理和上傳流程。

但有些功能的麻煩,反而不是「新增東西」。

而是:

把原本已經存在的東西拿掉。

如果畫面上的一個篩選條件不需要了,是不是把那個按鈕刪掉就好了?

下一篇:

Day 20|移除一個篩選條件,為什麼不能只刪掉畫面?


上一篇
Day 18|一顆收藏按鈕,背後其實不只有前端
下一篇
Day 20|移除一個篩選條件,為什麼不能只刪掉畫面?
系列文
從看不懂到做出來,用 PawPal 走過前端新手村20
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言