摘要
昨天(Day 4)我們完成了表單操作:輸入沒做的事 → 選擇原因 → 顯示鼓勵語錄。
但這個版本有一個缺點:一旦重整網頁,紀錄就消失了!
今天要用 LocalStorage 讓資料留在瀏覽器裡,讓「拖延紀錄」真的能被保存下來。
在保存資料之前,我們要先知道 JSON。
JSON 是一種用純文字表示資料的格式,常用來在程式之間交換資訊。
JSON 的基本特性:
範例:
存一筆拖延紀錄:
{
"task": "讀 20 頁書",
"reason": "太累了",
"quote": "能量回來,再走就好。"
}
這就是一個 JSON 物件。
多筆紀錄就會變成 JSON 陣列:
[
{
"task": "讀 20 頁書",
"reason": "太累了",
"quote": "能量回來,再走就好。"
},
{
"task": "寫一段程式",
"reason": "不知從哪開始",
"quote": "先從最簡單的 1% 開始。"
}
]
簡單來說:JSON 就像文字版的資料表格,人好讀、程式好用。
在 coding 裡,MVP 幫助新手減輕對新領域的心理負擔,先跑起來再慢慢加功能。
MVP 範例:
今天專注在 第 3 步:把想做的事(task)/沒做的原因(reason)/對應原因的語錄(quote)存進 LocalStorage。
先從最簡單的記錄單筆版開始。
核心流程:
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Procrastinator MVP</title>
</head>
<body>
<!-- 使用者在此輸入框中輸入他們想做的事情 -->
<h1>I want to...</h1>
<input type="text" id="taskInput" placeholder="Enter your task...">
<!-- 使用者從此下拉選單中選擇拖延的原因 -->
<select id="reasonSelect">
<option value="tired">太累 (Tired)</option>
<option value="mood">沒心情 (Not in the mood)</option>
<option value="distractions">太多干擾 (Too many distractions)</option>
<option value="difficult">太難 (Too difficult)</option>
<option value="dontKnow">不知道怎麼開始 (Don't know how to start)</option>
<option value="other">其他 (Other)</option>
</select>
<!-- 當使用者點擊此按鈕時,將調用 showQuote 函數 -->
<button onclick="showQuote()">Submit</button>
<!-- 鼓勵語錄將顯示在此段落中 -->
<p id="quote"></p>
<script>
// 當點擊「提交」按鈕時,將調用此函數
function showQuote() {
// 從輸入框中獲取任務
const task = document.getElementById('taskInput').value;
// 從下拉選單中獲取原因
const reasonSelect = document.getElementById('reasonSelect');
const reason = reasonSelect.value;
const reasonText = reasonSelect.options[reasonSelect.selectedIndex].text;
// 將原因映射到鼓勵語錄的物件
const quotes = {
tired: "休息不是懶惰,夏天躺在草地上,聽著潺潺流水,看著白雲飄過,絕非浪費時間。 (Rest is not idleness, and to lie sometimes on the grass under trees on a summer's day, listening to the murmur of the water, or watching the clouds float across the sky, is by no means a waste of time.)",
mood: "太陽每天都在提醒我們,我們也可以從黑暗中再次升起,我們也可以照亮自己的光芒。 (The sun is a daily reminder that we too can rise again from the darkness, that we too can shine our own light.)",
distractions: "成功的戰士是普通人,但有著雷射般的專注力。 (The successful warrior is the average man, with laser-like focus.)",
difficult: "河流之所以能切穿岩石,不是因為它的力量,而是因為它的堅持。 (A river cuts through rock, not because of its power, but because of its persistence.)",
dontKnow: "成功的秘訣在於開始。 (The secret of getting ahead is getting started.)",
other: "相信自己能做到,你就已經成功了一半。 (Believe you can and you're halfway there.)"
};
// 獲取與所選原因對應的語錄
const quote = quotes[reason];
// 在頁面上顯示語錄
document.getElementById('quote').innerText = quote;
// 創建包含任務、原因和語錄的資料物件
const data = {
task: task,
reason: reasonText,
quote: quote
};
// 將資料物件轉換為 JSON 字串並將其保存到 localStorage
localStorage.setItem('procrastinationData', JSON.stringify(data));
}
</script>
</body>
</html>
怎麼看有沒有存到 LocalStorage?
在你的網頁空白處按右鍵,選擇「檢查」。
選擇「應用程式」、展開「本機儲存空間」並選擇你的網址,應該會在右側看到一個名為「procrastinationData」的項目,以及其對應的 JSON 值。
核心流程:
如果想要保存多筆紀錄,就要像「記事本」一樣:
簡單來說:讀 → parse → push → stringify → setItem。
index.html:
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Procrastinator MVP</title>
</head>
<body>
<h1>I want to...</h1>
<input type="text" id="taskInput" placeholder="Enter your task..." />
<select id="reasonSelect">
<option value="tired">太累 (Tired)</option>
<option value="mood">沒心情 (Not in the mood)</option>
<option value="distractions">太多干擾 (Too many distractions)</option>
<option value="difficult">太難 (Too difficult)</option>
<option value="dontKnow">不知道怎麼開始 (Don't know how to start)</option>
<option value="other">其他 (Other)</option>
</select>
<button id="submitBtn">Submit</button>
<p id="quote"></p>
<script src="script.js"></script>
</body>
</html>
script.js:
// ===== 常數與兼容處理 =====
const STORAGE_KEY = 'procrastinator_records';
const LEGACY_KEY = 'procrastinationData'; // Day4 單筆版本的舊 key(若有就自動轉移)
// 解析安全:任何異常都回 []
function safeParseArray(raw) {
try {
if (!raw) return [];
const parsed = JSON.parse(raw);
const arr = Array.isArray(parsed) ? parsed : parsed?.records;
return Array.isArray(arr) ? arr : [];
} catch {
// JSON 壞掉就重置,避免每次都報錯
localStorage.removeItem(STORAGE_KEY);
return [];
}
}
// 讀 / 寫 / 新增
function readRecords() {
return safeParseArray(localStorage.getItem(STORAGE_KEY));
}
function writeRecords(list) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
}
function addRecord(record) {
const list = readRecords();
list.push(record);
writeRecords(list);
}
// 一次性遷移:把 Day4 的單筆資料轉成陣列儲存
(function migrateLegacyKey() {
const raw = localStorage.getItem(LEGACY_KEY);
if (!raw) return;
try {
const obj = JSON.parse(raw);
if (obj && typeof obj === 'object') {
const list = readRecords();
list.push({
id: String(Date.now()),
task: obj.task ?? '',
reason: obj.reason ?? '',
reasonCode: 'legacy',
quote: obj.quote ?? '',
createdAt: Date.now()
});
writeRecords(list);
}
} catch { /* 忽略無法解析的舊資料 */ }
localStorage.removeItem(LEGACY_KEY);
})();
// 語錄查表
function getQuoteByReason(code) {
const quotes = {
tired: ['先補點能量也算前進。', '睡一覺,回來再戰。'],
mood: ['先做 3 分鐘,心情會跟上。', '心情像雲,會散開的。'],
distractions: ['關掉一個通知,就是一步。', '把手機放遠一點點就好。'],
difficult: ['把任務切成最小一口。', '先完成最簡單的 1%。'],
dontKnow: ['寫下第一步就夠了。', '先鋪一張桌子,事就開始。'],
other: ['你願意回顧,已在改變。', '行動勝於完美。']
};
const list = quotes[code] || quotes.other;
return list[Math.floor(Math.random() * list.length)];
}
// 取得表單值與驗證
function getFormValues() {
const task = document.getElementById('taskInput').value.trim();
const reasonSelect = document.getElementById('reasonSelect');
const reasonCode = reasonSelect.value;
const reasonText = reasonSelect.options[reasonSelect.selectedIndex].text;
if (!task) throw new Error('EMPTY_TASK');
return { task, reasonCode, reasonText };
}
// 顯示回饋(Day5 先只顯示語錄)
function showFeedback(reasonText, quote) {
document.getElementById('quote').textContent = `原來你因為「${reasonText}」。👉 ${quote}`;
}
// 送出流程
function onSubmit() {
try {
const { task, reasonCode, reasonText } = getFormValues();
const quote = getQuoteByReason(reasonCode);
addRecord({
id: String(Date.now()),
task,
reason: reasonText,
reasonCode,
quote,
createdAt: Date.now()
});
showFeedback(reasonText, quote);
// Day 6 才會渲染歷史清單
} catch (e) {
if (e.message === 'EMPTY_TASK') {
alert('先寫下你想做的事喔!');
} else {
alert('發生未預期的錯誤,請稍後再試。');
console.error(e);
}
}
}
// 綁定事件
document.getElementById('submitBtn')?.addEventListener('click', onSubmit);
怎麼看有沒有存多筆資料到 LocalStorage?