平常開發和檢查 PawPal 時,大家比較常直接用電腦版操作,手機版相對比較少完整地走過一次流程。
所以有一次,我刻意從手機版重新操作醫院頁面,想看看換到不同裝置之後,實際用起來會不會有什麼不順的地方。
結果真的讓我發現了一個問題。
在手機版裡,我往下滑到醫院列表,點了一間醫院。
功能有反應。
醫院已經被選取,地圖上的狀態也跟著改變。
但我的畫面還停在下面的醫院列表。
如果想確認剛剛點的醫院到底有沒有成功選取,我還得自己再往上滑,回到地圖的位置才能看到結果。
那時候我的第一個想法就是:
這是一個 Bug。
它不是會跳出紅色錯誤訊息的 Bug,也不是 API 壞掉或資料沒有更新。
程式其實有成功執行。
但實際操作起來,就是不順。
如果只從程式的角度來看,這個功能其實沒有壞掉。
大概可以把當時的流程想成:
點擊醫院卡片
↓
醫院成功被選取
↓
地圖狀態跟著更新
↓
功能完成
以前的我看到這裡,可能就會覺得:
「有成功啊,那應該沒問題。」
但真正從手機版走一次流程後,看到的卻是:
使用者往下滑到醫院列表
↓
點擊其中一間醫院
↓
醫院成功被選取
↓
地圖狀態更新
↓
畫面卻還停在列表
↓
使用者看不到剛剛發生的變化
↓
還要自己滑回地圖
問題就出在最後這幾步。
系統知道操作成功了,但使用者不一定知道。
PawPal 當時開的 Issue,就是要處理這個問題:手機版使用者點擊醫院卡片後,醫院雖然已經成功選取,但畫面仍停留在列表,還得手動滑回地圖才能看到結果。
以前檢查功能時,我很容易先看這些事情:
按鈕有沒有反應?
資料有沒有更新?
API 有沒有成功?
Console 有沒有 Error?
這些當然都很重要。
但這次的情況不太一樣。
因為:
事件有成功觸發
醫院選取狀態有更新
地圖也有對應變化
如果只看程式執行的結果,好像什麼都沒有錯。
可是站在使用者的角度,操作完之後卻看不到結果。
這讓我開始注意到:
程式有成功執行,不代表整個操作流程就已經完整。
如果我點了一個按鈕之後,系統其實已經完成操作,但畫面沒有讓我看到結果,我還是很可能會懷疑:
「我剛剛到底有沒有點成功?」
對使用者來說,這樣的流程就還有可以改善的地方。
確認問題之後,要處理的方向其實很直覺。
使用者既然是在下面的醫院列表選擇醫院,那選取成功之後,就應該讓他直接看到上方地圖的結果。
所以最後的流程變成:
點擊醫院卡片
↓
維持原本的醫院選取流程
↓
確認目前是不是堆疊版面
↓
把捲動安排在這一輪 DOM 更新之後
↓
取得地圖區塊
↓
把畫面平滑捲回地圖
在 PawPal 的實作裡,原本選取醫院的邏輯沒有被拿掉,而是在列表點擊的流程外,再補上捲動回地圖的處理。
這次主要碰到了三個東西:
template ref
nextTick()
scrollIntoView()
實際程式的捲動條件使用 (max-width: 1279px),所以最後不只手機,在部分平板或其他使用堆疊版面的寬度下也會套用這個處理。
下面是簡化概念:
const stackedHospitalMapQuery = '(max-width: 1279px)'
const selectHospitalFromList = (hospitalId) => {
selectHospital(hospitalId)
const isStackedLayout = window.matchMedia(
stackedHospitalMapQuery,
).matches
if (!isStackedLayout) return
nextTick(() => {
mapSectionRef.value?.scrollIntoView({
behavior: 'smooth',
block: 'start',
})
})
}
以上為簡化概念,保留這次實作的主要流程與條件,不是 PawPal Repository 的完整原始程式碼。
template ref:先找到我要回去的位置要讓畫面自動回到地圖,第一件事就是要知道:
地圖區塊到底在哪裡?
在 Vue 裡,可以透過 template ref 取得畫面上的特定元素。
概念上可以想成:
<section ref="mapSectionRef">
地圖
</section>
有了這個 template ref,我就能取得地圖區塊,後面才有辦法把畫面捲到這個位置。
這次我不是要修改地圖本身,而是要先回答一個很單純的問題:
「使用者選完醫院之後,我要把畫面帶去哪裡?」
nextTick():為什麼把捲動安排在畫面更新之後?這也是我在這次修改裡碰到的一個觀念。
Vue 的狀態改變後,相關的 DOM 更新會經過自己的更新流程。
而這次 PawPal 的實作,在醫院選取完成、又符合手機/平板等堆疊版面的情況下,會透過 nextTick() 把後面的捲動安排在這一輪 DOM 更新之後。
流程比較像:
選取醫院
↓
狀態改變
↓
確認是堆疊版面
↓
透過 nextTick() 安排後續動作
↓
捲回地圖
所以這裡可以先把 nextTick() 理解成:
「這次實作選擇等目前這一輪 DOM 更新處理後,再執行捲動。」
這不代表所有類似功能都一定要使用 nextTick(),而是在這次 PawPal 的實作裡,它被用來安排選取之後的捲動時機。
以前看到 nextTick(),我大概只知道「好像跟畫面更新有關」。
真的碰到這次的互動問題後,我才比較知道它可以在什麼情況下派上用場。
scrollIntoView():不是做動畫,而是讓結果被看見最後真正負責把畫面移回地圖的是:
scrollIntoView()
這次使用的概念是:
mapSectionRef.value?.scrollIntoView({
behavior: 'smooth',
block: 'start',
})
behavior: 'smooth' 讓畫面平滑移動,而不是突然跳到另一個位置。
block: 'start' 則是讓目標區塊盡量出現在畫面上方。
一開始看到這段程式碼,很容易覺得:
「這不就是一個捲動畫面的效果嗎?」
但對我來說,這次真正重要的不是動畫。
而是:
讓使用者直接看到自己剛才操作造成的結果。
如果只是為了畫面好看,那它可能只是一個視覺效果。
但放在這個情境裡,它其實是在補完整使用者的操作流程。
這次修改也補了一個 regression test(回歸測試)作為保護。
這個測試會檢查 template ref、版面判斷、nextTick()、scrollIntoView() 和列表事件綁定等關鍵結構是否還存在。
它不是完整模擬瀏覽器操作的 UI 測試,但可以降低之後修改程式時,不小心把這段重要連接移除或改壞的風險。
這次修改最讓我有感的,其實不是學會 scrollIntoView()。
而是我開始重新想一個問題:
功能做出來之後,到底怎樣才算「正常使用」?
以前我比較容易從開發者的角度判斷:
功能有沒有做出來?
按鈕有沒有反應?
資料有沒有成功?
API 有沒有錯?
如果這些都正常,我可能就會覺得功能完成了。
可是修完這個手機版地圖問題後,我開始覺得還少了一件事情:
使用者到底能不能自然地完成這個操作?
如果使用者點完一個東西之後,還要自己去找:
「剛才發生什麼事?」
那即使程式邏輯沒有錯,整個使用體驗還是會受到影響。
也是修完這個問題之後,我才真的開始覺得,使用者體驗的好壞,本來就是「功能能不能正常使用」的一部分。
不是只有漂亮或不漂亮。
而是:
使用者能不能理解現在發生了什麼。
如果現在重新做一次類似的功能,我會比以前多做一件事情。
不只是把功能做完,而是真的從不同裝置把流程走一次。
例如:
功能完成
↓
用電腦版操作一次
↓
用手機版操作一次
↓
每完成一個操作都問自己:
「使用者現在看到的是什麼?」
↓
確認操作結果有沒有清楚呈現
以前我可能比較容易停在:
「程式有沒有正常跑?」
現在我會再多問一句:
「如果我完全不知道這段程式怎麼寫,只是一個第一次使用網站的人,我會知道剛剛操作成功了嗎?」
這個角度,是我做完這次修改之後才開始比較注意的事情。
這次手機版醫院頁面的修改,讓我記住幾件事情:
template ref、nextTick()、scrollIntoView() 這次不是單純拿來做畫面效果,而是用來補完整互動流程。以前我可能會覺得:
「能動就好了。」
但這次之後,我開始覺得真正要問的應該是:
它不只會動,使用者真的用得順嗎?
這次的問題,是功能已經有反應,但使用者沒有馬上看到結果。
而在另一個功能裡,我遇到的問題剛好不太一樣:使用者送出了資料,但畫面上的其他資料也必須跟著一起更新。
下一篇,我會從第一次做醫院評論開始,整理一則留言送出去之後,評論、平均星等和畫面上的資料到底怎麼一起變化。
下一篇:
Day 17|第一次做醫院評論,留言送出後星等怎麼跟著變?