資料存在哪裡:本機、資料庫與雲端
你的打卡紀錄現在住在哪裡?這堂課說明 localStorage 的優點與限制,替「小習慣」加上匯出、匯入備份,並幫你判斷什麼時候需要資料庫、登入和雲端,以及隨之而來的隱私責任。
用了幾天「小習慣」,你的手機裡已經有一些真實的打卡紀錄了。這堂課想先問一個問題:這些紀錄存在哪裡?
答案是:存在你那支手機、那個瀏覽器、那個網址專屬的 localStorage 裡。這句話裡的每一個「那」,都是一個限制。了解它們,你才知道什麼時候 localStorage 夠用,什麼時候該升級。
localStorage 是什麼
上一堂課我們把它比喻成「瀏覽器裡的小儲物櫃」。更精確地說:
- 每個網站有自己的一格。 這裡的「網站」是用來源(origin)來分的,也就是「協定+網域+連接埠」,例如
https://tiny-habits.pages.dev。不同來源之間看不到彼此的資料。 - 只能存文字。 所以我們用
JSON.stringify把物件轉成文字存進去,讀出來再用JSON.parse轉回物件。 - 容量不大。 各家瀏覽器的上限不同,通常是每個網站幾 MB 左右。
- 不會自己過期。 不像 cookie 有期限,只要沒有被清掉,就一直在。
幾 MB 夠「小習慣」用嗎?算算看:一筆打卡日期像 "2026-11-07", 大約 13 個字元。5 個習慣每天都打卡,一年大約是 5 × 365 × 13,差不多 24 KB。就算用上十年,也遠遠用不完。
如果你的 App 要存照片、大量資料,可以改用瀏覽器的另一種儲存空間 IndexedDB,容量大很多,但寫法比較複雜,需要時再請 Claude 幫你改。
資料什麼時候會不見
這是最需要讓使用者知道的部分。localStorage 的資料在這些情況下會消失:
| 情況 | 為什麼 |
|---|---|
| 換了一支手機,或改用電腦打開 | 資料只在原本那台裝置的瀏覽器裡 |
| 在同一支手機換了瀏覽器(例如從 Chrome 換到 Safari) | 每個瀏覽器各有自己的儲物櫃 |
| 使用者清除了瀏覽器的網站資料 | 儲物櫃被整個清空 |
| 在無痕模式(私密瀏覽)下使用 | 關掉視窗就清除 |
| App 換了網址(例如從 Netlify 搬到 Cloudflare Pages) | 不同來源,等於不同儲物櫃 |
| 很久沒有打開 | 某些情況下(例如 iPhone 的 Safari 對長時間沒造訪的網站),瀏覽器可能清除網站資料 |
最後一項要特別說明:加到主畫面的 PWA,通常比一般網頁不容易被清掉資料,這也是我們第五堂要做 PWA 的原因之一。但「不容易」不等於「不會」,所以備份功能不是可有可無。
三個地方各有取捨。「小習慣」第一版用前兩層:平常存本機,重要時刻匯出備份。
替「小習慣」加上匯出與匯入
備份檔的做法很簡單:把整個 state 轉成 JSON 文字,存成一個檔案讓使用者下載。要還原時,選擇那個檔案,讀進來取代目前的資料。
JSON(JavaScript Object Notation)是一種用文字表示資料的格式,人看得懂,程式也讀得懂。你可以用記事本打開備份檔,看看自己的打卡紀錄。
請替小習慣加上備份功能,放在頁面最下方:
- 「匯出備份」按鈕:把目前的 state 存成 JSON 檔下載,檔名包含今天日期,例如 小習慣備份-2026-11-07.json
- 「匯入備份」按鈕:讓使用者選一個 JSON 檔
- 匯入前要檢查格式:一定要有 habits 陣列,每個習慣要有 id、name 和 done 陣列;格式不對就顯示清楚的錯誤訊息,不要動到現有資料
- 格式正確時,先跳出確認,說明備份裡有幾個習慣、會取代目前的資料,使用者同意才匯入
請用一個按鈕觸發隱藏的檔案選擇欄位,這樣在 iPhone 上也比較穩定。
做完後,你的 index.html 會在 main 的最後、script 標籤之前多出這一段:
<footer class="backup">
<button id="export-btn" type="button">匯出備份</button>
<button id="import-btn" type="button">匯入備份</button>
<input id="import-file" type="file" accept=".json,application/json" hidden>
</footer>
app.js 的最後加上這段(參考版本):
// ---------- 備份:匯出與匯入 ----------
function exportBackup() {
const json = JSON.stringify(state, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `小習慣備份-${dayKey()}.json`;
a.click();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function isValidBackup(data) {
return (
data &&
Array.isArray(data.habits) &&
data.habits.every((h) => typeof h.id === 'string' && typeof h.name === 'string' && Array.isArray(h.done))
);
}
async function importBackup(file) {
try {
const data = JSON.parse(await file.text());
if (!isValidBackup(data)) throw new Error('這不是小習慣的備份格式');
const ok = confirm(`備份裡有 ${data.habits.length} 個習慣,要用它取代目前的資料嗎?`);
if (!ok) return;
state = data;
save();
render();
} catch (err) {
alert('匯入失敗:' + err.message);
}
}
const importFile = document.querySelector('#import-file');
document.querySelector('#export-btn').addEventListener('click', exportBackup);
document.querySelector('#import-btn').addEventListener('click', () => importFile.click());
importFile.addEventListener('change', () => {
const file = importFile.files[0];
if (file) importBackup(file);
importFile.value = ''; // 清空,下次選同一個檔案也會觸發
});
可以在 style.css 補上一點間距,讓兩顆按鈕排在一起:
.backup { display: flex; gap: 8px; justify-content: center; margin: 32px 0; }
.backup button { background: #eeecff; color: #5b4ef0; }
幾個設計上的用心:
- 匯入前先驗證,再確認。 使用者可能不小心選錯檔案。如果直接覆蓋,原本的資料就沒了。「先檢查、再問、最後才動手」是處理會覆蓋資料的操作時的好習慣。
JSON.stringify(state, null, 2)的第三個參數2代表「縮排兩格」,讓備份檔用記事本打開時也整齊好讀。- 檔名帶日期,使用者一眼就知道哪個是最新的備份。
你要檢查的事:
- 匯出一次,用記事本打開備份檔,找到你的打卡日期。
- 刪掉一個習慣,再匯入剛剛的備份,它回來了嗎?
- 隨便選一張圖片或文字檔匯入,應該看到錯誤訊息,原本的資料完好無缺。
- 真正的跨裝置測試:在電腦匯出,把檔案傳到手機(用 LINE 傳給自己、存到雲端硬碟都可以),在手機上匯入。
換手機的完整流程。整個過程中資料都沒有經過你的伺服器,因為你根本沒有伺服器。
git add .
git commit -m "加入匯出與匯入備份"
什麼時候需要資料庫和登入
匯出匯入能解決「換手機」,但解決不了這些需求:
- 手機和電腦即時同步:在手機打卡,電腦上馬上看到。
- 多人共用一份資料:全家共用的冰箱庫存、朋友之間的分帳。
- 資料絕對不能丟:例如記帳、病歷紀錄,不能依賴使用者記得備份。
- 伺服器要主動做事:例如每天晚上 9 點寄提醒 Email。
遇到這些需求,資料就要存到後端:一台在網路上、24 小時開著的電腦,上面跑著資料庫(專門存放、查詢資料的軟體)。所有裝置都去同一個地方讀寫,資料自然就同步了。
而一旦資料存在同一個地方,就需要登入:伺服器要知道「你是誰」,才能只給你看你自己的資料。
自己架伺服器很辛苦,所以現在有很多「後端即服務」,幫你把資料庫、登入、檔案儲存都準備好:
| 服務 | 特色 |
|---|---|
| Supabase | 底層是 PostgreSQL 資料庫,用表格的方式思考資料;內建登入;用「資料列層級安全」(Row Level Security)規則限制每個人只能讀寫自己的資料 |
| Firebase | Google 的服務,Firestore 資料庫用「文件」的方式存資料;內建登入;用「安全性規則」限制存取 |
兩者都有免費方案可以入門,詳細的額度與價格請以官網為準。不管選哪個,觀念都一樣:前端 App 負責畫面,後端負責保管資料和確認身分。
隱私:資料在誰手上
「小習慣」目前的設計,在隱私上其實非常好:你身為開發者,手上沒有任何使用者的資料。 朋友的打卡紀錄只存在他自己的手機裡,你看不到、也不可能外洩。
可以把這件事大方地寫在 App 裡,例如在頁尾加一行:「你的資料只存在這支手機上,我們看不到。」對使用者來說,這是一個很溫暖的承諾。
當你把資料搬上雲端,角色就變了:你成為保管別人資料的人。這時候要想清楚:
- 只收需要的資料。 習慣 App 需要使用者的生日和電話嗎?不需要就不要收。
- 說清楚你收了什麼、拿來做什麼。 也就是隱私權政策。
- 讓使用者能刪除自己的資料。
- 留意法規。 在台灣,蒐集和處理個人資料受《個人資料保護法》規範。正式對外提供服務前,值得花點時間了解。
這也是為什麼我們建議:能存在本機,就先存在本機。 等真的需要同步和共用時,再升級到後端。
我的 App 是(描述你的 App)。使用者會在(手機/電腦/多台裝置)上使用,資料會不會給別人看:(會/不會)。
請幫我判斷:只用 localStorage 加上匯出匯入夠不夠?如果需要後端,請比較 Supabase 和 Firebase 哪個比較適合我,並列出我需要設定的安全規則和要注意的隱私事項。先不要寫程式。
- 和 Claude 一起替「小習慣」加上匯出、匯入備份,完成上面四項檢查,然後 commit。
- 打開開發者工具的 Application 分頁,找到 Local Storage 裡的
tiny-habits-v1,對照你匯出的 JSON 檔,確認內容一樣。 - 在 App 的頁尾加上一句說明,告訴使用者資料存在哪裡、記得定期備份。用你自己的話寫,語氣溫暖一點。
- 想一想你自己的 App 點子:它需要後端嗎?用上面的提示詞問 Claude,寫下答案。這會是專案計畫的重要決定。
讀完了嗎?
標記完成後,單元與專案的進度會自動更新(只存在這台裝置的瀏覽器)。
留言與討論
卡關了、有心得,或發現內容需要更新?都歡迎留言。留言使用 GitHub 帳號登入。