在 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 最後會把裁切結果輸出成:
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 這類格式。
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|移除一個篩選條件,為什麼不能只刪掉畫面?