iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Vibe Coding

《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》系列 第 22 篇

【Day 22|航跡】最近瀏覽與快取:資料一定要每次去資料庫查嗎?

  • 分享至 

  • xImage
  •  

船經過海面時,身後會留下一道航跡。但過不了多久,浪一打,痕跡就散了。

船過水無痕,本來很正常;可是假如我們真的想回頭找到剛剛經過的地方,就不能只靠海面替我們記住。

網站也是一樣。看了好幾張專輯之後,想回頭找剛剛那一張,卻怎麼也想不起名字;剛剛搜尋過的關鍵字,也得再打一次。如果網站什麼都不記,離開之後就像沒發生過。

所以今天要做最近瀏覽和最近搜尋。但「記住」這件事,也帶來另一個問題:什麼應該留下?留下多久?留下來的資料,又能相信多久?


一、哪些東西值得網站幫我記住?

Day 19 做了收藏,今天要做的則是最近瀏覽和最近搜尋。三種功能看起來都是「記住使用者的東西」,性質卻有一些不一樣:

收藏 最近瀏覽 最近搜尋
怎麼產生的 使用者主動按愛心 網站自動記錄 網站自動記錄
代表什麼 「我喜歡這個」,一個決定 「我看過這個」,一段足跡 「我找過這個」,一段足跡
存多久 直到使用者取消 只留最近幾筆 只留最近幾筆
不見了會怎樣 使用者可能會生氣 有點可惜,但沒關係 相對可能比較無感
需要跨裝置嗎 需要 通常不需要 通常不需要

收藏是使用者做的決定,網站有責任好好保存,所以 Day 19 把它存進資料庫,還做了跨裝置同步和 RLS。最近瀏覽和最近搜尋,是網站順手記下的足跡,方便使用者回頭找。資料的重要性,決定了要花多少成本保存它。

當然,最近瀏覽或是搜尋紀錄,也可以實作成可以跨裝置同步的。但在今天的文章中,我們會先以不跨裝置同步來實作。


二、要放在哪裡?

瀏覽器和伺服器都能存資料,常見的位置有四種:

位置 關掉頁面後還在嗎 換裝置看得到嗎 要登入嗎 適合
記憶體(React state) 不在 看不到 不用 目前畫面的暫時狀態
sessionStorage 關掉分頁就沒了 看不到 不用 只在這次瀏覽有用的資料
localStorage 還在 看不到 不用 偏好設定、最近瀏覽
資料庫 還在 看得到 通常要 收藏、訂單這類帳號資料

最近瀏覽和最近搜尋,考量到未登入訪客以及比較少情況會跨裝置使用,所以這次選擇存放在 localStorage。也就是說,不需要登入、不需要新增資料表,也不用處理 Day 19 那種登入前後的合併問題。

如果發現自己開發的網站,該紀錄的資料卻消失了,可以詢問 AI 資料儲存的位置,來判斷消失的可能原因。


三、最近瀏覽要記什麼?

最直覺的做法,是把看過的專輯「整份」存起來:名稱、封面、藝人。下次打開,直接顯示。

但專輯資料是有可能會修改的。管理員修正了專輯名稱、換了封面,甚至把重複的專輯刪掉,localStorage 裡那份卻不會跟著變。使用者看到的,就會是一份過期的資料。

所以這次選擇只記 專輯的識別碼:

["armageddon", "new-jeans", "ive-switch"]

就像服務生記得你上次點了哪幾道菜(餐點名稱),而不是把上次的菜留著。這是一筆紀錄,下次要上菜時,還是照廚房現在的做法重新出餐。(餐點做法、原料可能會換,但菜名可能還是一樣)

顯示時,再用目前的專輯資料把內容還原回來。找不到的專輯,代表已經不存在,就略過並從紀錄中移除。

唯一資料來源(Single Source of Truth)

同一份資料,只讓一個地方當正式版本。專輯資料的正式版本在 Supabase 資料庫中;localStorage 只記「看過哪幾張」,不另外保存一份專輯內容。顯示時,以目前取得的專輯資料為準,而不是把 localStorage 裡的東西當成正式資料。

如果兩邊都存,就會有兩份「真相」。若資料不一致,會需要去確認哪一份才是對的,比較麻煩一點。

不過,這樣又出現一個新問題:每次顯示最近瀏覽,都要重新取得專輯資料,不會浪費嗎?


四、儲存和快取,是同一件事嗎?

講到「把資料留下來」,很多人會想到快取(cache)。但留下來的資料,不一定都是快取。

儲存,是因為之後還需要這份資料;快取,是因為不想再花同樣的成本取得一次。

儲存的資料如果不見了,就真的沒了;快取的資料不見了沒關係,再拿一次就好,因為正式版本還在別的地方。

用第一篇的餐廳來比喻:倉庫是儲存,食材丟了就真的沒了;廚房旁的備料台是快取,醬汁倒掉沒關係,再從倉庫拿食材重做一份就好。

所以,存在 localStorage 裡的東西,不一定是快取。要看裡面放的是什麼、為什麼放,例如:

localStorage 裡放的東西 比較像什麼
最近瀏覽的專輯識別碼 使用者的行為紀錄
深色模式的設定 偏好設定
還沒送出的表單內容 暫存
從伺服器拿回來的專輯資料,為了下次少拿一次 快取

localStorage 只是一個存放的位置。同一個位置,可以放紀錄,也可以放快取。


五、快取:拿過一次,下次還要重來嗎?

快取的做法其實不難:資料拿過一次,就先存一份複本在比較近的地方,下次直接用。

https://ithelp.ithome.com.tw/upload/images/20261006/20178017OiG4qgO8IA.png
【圖 1|有快取和沒有快取】

名詞 意思
命中(hit) 快取裡有,直接用
沒命中(miss) 快取裡沒有,只好去資料來源拿
過期(stale) 快取裡有,但資料來源已經更新了,這份是舊的
失效(invalidation) 把過期的快取作廢,下次重新拿
有效期限(TTL) 設定快取能用多久,時間到就自動作廢

快取可以放在很多地方。越靠近使用者,通常取得越快;但只要存了一份複本,就得決定它能用多久,以及來源更新後怎麼讓它作廢:

瀏覽器記憶體 → 瀏覽器快取 → CDN → 伺服器快取 → 資料庫
(通常最快、最近)                          (最遠,但通常是正式/正確版本)

這是簡化的示意,用來說明複本可以存在不同的地方;不代表每次請求都會一層一層經過,資料會不會過期,也不是由距離決定的。

換成餐廳:

網站 餐廳
資料庫 倉庫:食材最齊全、最準,但離得最遠,每跑一趟都要時間
伺服器快取 廚房旁的備料台:常用的醬汁、切好的配料先放在手邊
CDN 各地分店的冷藏櫃:熱門商品先放在離客人近的地方
瀏覽器快取 已經送到桌上的水壺:客人自己倒,不用再叫服務生

剛剛那幾個名詞,在餐廳裡是這樣的:備料台上有,直接用,就是命中;沒有,跑一趟倉庫,就是沒命中;醬汁放太久不新鮮了,就是過期;每份備料貼上保存期限,就是有效期限;菜單改了配方,舊的醬汁要先倒掉,就是失效。

真正的配方,記在主廚的配方本上(資料庫)。備料台上的醬汁,只是照著配方先做好的一份。配方改了,要以配方本為準。

所以快取最麻煩的,通常不是怎麼存,而是什麼時候它已經不能用了。因此資料的更新週期通常會是快取實作方式的重要判斷依據之一。

5.1 CDN 和伺服器快取,差在哪裡?

兩者都在伺服器那一側,但存的東西不一樣:

CDN 伺服器快取
在哪裡 分散在世界各地、離使用者近的伺服器 自己的網站伺服器(例如 Next.js)裡
常見內容 可以重複使用的回應:圖片、程式檔、整頁 HTML 資料或計算結果:例如查到的專輯清單
私人資料 通常用來放大家可以共用的內容;放私人資料風險很高,除非設定得很仔細 一樣要區分清楚,不能因為在伺服器端就假設安全
適合 公開、大家看到都一樣的內容 頁面每次產生,但其中某些資料不用每次查

CDN 像是把做好的成品,先放在離客人近的地方;伺服器快取則是廚房裡的半成品,菜還是現做,只是省掉一些準備工作。

5.2 瀏覽器裡的四種「記住」

瀏覽器這一側也有好幾種,很容易混在一起:

是什麼 誰決定存什麼 什麼時候消失 算快取嗎
瀏覽器記憶體 程式執行時的變數,例如 React state、Next.js 記住剛看過的頁面 程式 重新整理或關掉分頁 用來暫存時算
瀏覽器快取 瀏覽器自動保存下載過的圖片、程式檔 伺服器回應的 Cache-Control 依伺服器的規則決定何時要重新確認;空間不夠時也可能被提早清掉 算
localStorage 程式主動寫入的資料 程式 沒有內建的有效期限,通常會保留到被程式、使用者或瀏覽器清除為止 看放了什麼(第四節)
sessionStorage 跟 localStorage 一樣,但每個分頁各自一份 程式 關掉分頁 看放了什麼

最容易搞混的是瀏覽器快取和 localStorage。瀏覽器快取是瀏覽器自動做的,程式通常不直接操作,而是由伺服器告訴瀏覽器能存多久;localStorage 則是程式主動寫進去的,而且沒有內建的有效期限。

5.3 公開快取和私人快取

快取還要分清楚:這份複本,可以給誰?

例子 可以給誰
公開快取 專輯名稱、封面 所有人都可以共用
私人快取 我的收藏、後台的回報 只能給特定的人

備料台上的醬汁,每位客人的菜都能用;但客人寄放的酒,只能拿給寄酒的那位客人,拿錯人就出事了。

私人資料也可以快取,例如瀏覽器記住自己的收藏。真正不能做的,是把私人資料放進大家共用的快取。

Day 21 後台的回應使用 no-store,代表這份回應不應被儲存下來重複使用。對包含管理資料的頁面來說,這是保守又合理的做法,也避免它被當成可以共用的內容。


六、SSR、ISR 和快取有什麼關係?

Day 20 講了渲染方式,今天講快取,兩者很容易混在一起:

渲染方式決定頁面什麼時候、在哪裡被做出來;快取決定做過的結果,能不能留下來重複使用。

有三句話值得記住:

  • SSR 不代表每次都查資料庫:頁面每次都由伺服器產生,但頁面裡用到的資料,可以先快取一份
  • 快取不代表頁面一定是靜態的:就算頁面每次現做,資料一樣可以從快取拿
  • 從快取的角度,可以把 ISR 理解成:把已經產生的頁面重複使用一段時間,需要時再重新產生

這也回答了 Day 20 留下的一個問題。Day 20 發現,在專案中,因為根 layout 讀了每個人各自的登入狀態,完整頁面不能給所有人共用。但那說的是完整頁面。整頁不能共用快取,不代表其中所有資料都不能快取。 頁面裡的公開資料,仍然可以另外快取。專輯資料是公開的、很少變動,正適合放進公開快取。

一般來說,如果資料不太會變、讀取的次數很多,大家看到的又都一樣 ,就很適合加上快取,可以減少對資料庫的請求 。專輯資料其實就符合這些條件。

不過這次先不實作資料快取。只有 25 張專輯,每次查詢都很快;加了快取,就得開始處理「資料改了多久才看得到」。等資料量或流量真的需要時,再來決定有效期限和失效的方式。


七、實作:最近瀏覽與最近搜尋

7.1 最近瀏覽

規則 做法
什麼時候記錄 打開專輯詳情時。完整詳情頁和側邊欄共用同一個元件,記錄寫在這裡,兩種入口都涵蓋
存什麼 只存專輯識別碼
幾筆 最多 10 筆,新的在前
重複看同一張 移到最前面,不重複
找不到的專輯 略過,並從紀錄中移除
查詢失敗時 顯示暫時無法載入,不刪除紀錄。查詢失敗,和確認資料不存在,不是同一件事
清除 提供清除按鈕

最近瀏覽分成兩個階段:記錄和顯示。

階段一:記錄看過哪一張

https://ithelp.ithome.com.tw/upload/images/20261006/20178017LBzCeaBovw.png
【圖 2|記錄最近瀏覽】

  1. 使用者打開一張專輯,不管是完整詳情頁還是側邊欄
  2. 瀏覽器從 localStorage 讀出目前的紀錄
  3. 把這張專輯的識別碼放到最前面;如果原本就在清單裡,先移除舊的那筆;超過 10 筆就刪掉最舊的
  4. 寫回 localStorage

這一段只動到識別碼,不需要問伺服器,也不需要登入。

階段二:在首頁顯示

localStorage 只記得看過哪幾張,名稱和封面不在裡面。所以首頁要分兩段:先由伺服器載入首頁本身,再由瀏覽器拿識別碼,直接向 Supabase 取得專輯資料。

https://ithelp.ithome.com.tw/upload/images/20261006/20178017N5v76bp8v2.png
【圖 3|在首頁顯示最近瀏覽】

  1. 載入首頁:伺服器照常產生首頁,最近瀏覽那一塊先放固定尺寸的佔位
  2. 讀出紀錄:首頁到了瀏覽器之後,才讀 localStorage,因為伺服器讀不到它
  3. 取得專輯資料:瀏覽器用公開金鑰直接呼叫 Supabase 的 Data API,只查卡片需要的摘要欄位,例如名稱、藝人、封面、發行日期和版本格式,不拿款式和內容物
  4. 顯示:依照紀錄原本的順序,把佔位換成卡片;查不到的專輯略過,並從紀錄中移除

這裡沒有另外寫一支 API。專輯本來就是公開資料(Day 15),瀏覽器用公開金鑰查詢,由 RLS 決定只能讀、不能改;公開金鑰可以出現在瀏覽器裡,secret key 則絕對不行。這也是 Day 19 收藏用過的做法。

未來如果流量大了,我就會考慮替專輯資料做快取,如此一來不用每一次找一張專輯的詳情資料都要去資料庫撈一次,而是可以直接從快取取得。

這也是 Day 20 的延續:一個頁面不需要為了最近瀏覽,整頁改成在瀏覽器渲染。 伺服器讀不到 localStorage,所以只有讀取紀錄的那一塊,交給瀏覽器處理。

伺服器產生的畫面,和瀏覽器第一次顯示的畫面,都先放同樣尺寸的佔位;讀完紀錄後再換成內容。這樣伺服器和瀏覽器的第一個畫面會一致,也能減少載入時的版面跳動。

https://ithelp.ithome.com.tw/upload/images/20261006/20178017eYUNnGFtl8.png
【圖4|點擊幾張專輯後,顯示最近瀏覽(登入狀態)】

https://ithelp.ithome.com.tw/upload/images/20261006/20178017Bdo81d5UYr.png
【圖5|登出後,最近瀏覽仍然存在】

https://ithelp.ithome.com.tw/upload/images/20261006/20178017HgemCdyYPX.png
【圖6|清除後,最近瀏覽紀錄消失】

7.2 最近搜尋

最近搜尋用的是很類似做法,只是規則不同。最需要決定的是:什麼時候才算「搜尋過」?

如果每打一個字就記錄,紀錄會變成 a、ae、aes、aesp、aespa。專案中的搜尋,在 Day 20 已經改成要案下Enter 或是點擊搜尋按鈕才會送出才查詢,所以最近搜尋也可以跟著同一個時機:按 Enter 或搜尋按鈕送出時才記錄。

規則 做法
什麼時候記錄 正式送出搜尋時
存什麼 去掉前後空白的關鍵字;空白不記錄
幾筆 最多 5 筆,新的在前
重複搜尋 移到最前面;比對時不分英文大小寫,aespa 和 AESPA 算同一筆
顯示 點搜尋框時,出現下拉清單;點其中一筆直接套用,並移到最前面
清除 提供清除按鈕

搜尋紀錄存的是使用者自己打的字,共用電腦時,下一個人點開搜尋框就看得到。所以清除按鈕在這裡特別重要。

https://ithelp.ithome.com.tw/upload/images/20261006/20178017Lnd3x2Hc9i.png
【圖7|最近搜尋紀錄(登入狀態)】

https://ithelp.ithome.com.tw/upload/images/20261006/20178017YzFr4UaTNb.png
【圖8|最近搜尋紀錄(登出)】


驗收

測試 預期結果
依序看 A、B、C 顯示 C、B、A
再看 A A 移到最前,不重複
從列表打開側邊欄 也被記錄
看超過 10 張 最舊的被移除
重新整理、關掉瀏覽器再開 紀錄還在
新開的無痕視窗 讀不到一般視窗的紀錄
專輯改名後 最近瀏覽顯示新名稱
專輯被刪除後 自動略過,不會出錯
手動清掉 localStorage 最近瀏覽回到空狀態
按下清除 紀錄消失
搜尋時打字但沒送出 不會被記錄
按 Enter 送出 關鍵字被記錄
輸入 aespa 送出 記成 aespa
空白直接送出 不會被記錄
點最近搜尋的關鍵字 直接套用搜尋

「專輯改名後」這一列很重要,它證明只存識別碼的設計真的有效:使用者看到的是目前取得的專輯資料,不是 localStorage 裡的舊複本。


換個領域:網站都在記住什麼?

產品 功能 存在哪裡
購物網站 最近看過的商品 通常存在瀏覽器;登入後可能同步到帳號(或是強制登入後才能瀏覽)
影音平台 繼續觀看 跟隨著帳號(通常存在資料庫),因為使用者常在手機看一半、回家用電視接著看
地圖 App 最近搜尋的地點 存在帳號或裝置,並提供清除紀錄

同樣是「最近」,影音平台的繼續觀看通常會跨裝置同步,因為它對使用者更重要。這又回到第一節:資料的重要性,決定了要花多少成本保存它。


結語與明日預告

今天做的只是最近瀏覽和最近搜尋,但真正學到的是:不是每一份資料都要永久存進資料庫,也不是每一次畫面出現,都必然要重新向資料來源取得。要不要加入快取,要看資料量、更新頻率和查詢成本;這次資料很少,所以選擇每次都用識別碼取得目前的資料,先不增加快取和失效的機制。

有些資料是使用者的紀錄,有些是暫存,有些是快取。差別不在放在哪裡,而在為什麼要存,以及可以相信它多久 。航跡終究會散。重要的不是全部留下,而是知道哪些值得記住,以及可以相信它多久,根據情況決定儲存資料的方式。

第三幕從一張資料表開始。到今天,網站已經能存資料、驗證輸入、辨認身分、分配權限,也知道哪些資料該記住、哪些該重新拿。第三幕到這邊就先告一個段落了,到目前為止,這個網站都還只在我的電腦上。開發、測試、正式的資料,也都放在同一個資料庫裡。

明天,我們將把它部署到網路上,讓他並不只能在我的電腦中開啟。

我們明天見。


上一篇
【Day 21|瞭望】管理員後台與角色權限:登入只是第一關
系列文
《我與 AI 的奇幻漂流:30 天,把「能跑」變成「能上線」》 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言