iT邦幫忙

2025 iThome 鐵人賽

DAY 5
0
Modern Web

Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延系列 第 5

【Day 5】- 入門 JavaScript 網頁架設:LocalStorage

  • 分享至 

  • xImage
  •  

摘要
昨天(Day 4)我們完成了表單操作:輸入沒做的事 → 選擇原因 → 顯示鼓勵語錄。
但這個版本有一個缺點:一旦重整網頁,紀錄就消失了!
今天要用 LocalStorage 讓資料留在瀏覽器裡,讓「拖延紀錄」真的能被保存下來。

為什麼要用 LocalStorage?

  • 避免資料消失:瀏覽器重整後資料還在。
  • 幫助回顧:可以慢慢累積紀錄,讓使用者看到「自己常出現的拖延模式」。
  • 無需伺服器:LocalStorage 完全存在瀏覽器端,不用額外後端就能保存。

JavaScript Object Notation (JSON) 是什麼?

在保存資料之前,我們要先知道 JSON。
JSON 是一種用純文字表示資料的格式,常用來在程式之間交換資訊。

JSON 的基本特性:

  • 用 key:value 配對儲存資料
  • key 一定要用雙引號
  • value 可以是字串、數字、布林、陣列或物件

範例:
存一筆拖延紀錄:

{
  "task": "讀 20 頁書",
  "reason": "太累了",
  "quote": "能量回來,再走就好。"
}

這就是一個 JSON 物件。

多筆紀錄就會變成 JSON 陣列:

[
  {
    "task": "讀 20 頁書",
    "reason": "太累了",
    "quote": "能量回來,再走就好。"
  },
  {
    "task": "寫一段程式",
    "reason": "不知從哪開始",
    "quote": "先從最簡單的 1% 開始。"
  }
]

簡單來說:JSON 就像文字版的資料表格,人好讀、程式好用。

Day 4 回顧:最小可行產品(Minimum Viable Product, MVP)範例

在 coding 裡,MVP 幫助新手減輕對新領域的心理負擔,先跑起來再慢慢加功能。
MVP 範例:

  1. 引起拖延人的共鳴畫面
  2. 拖延記錄表單 + 下拉原因 + 顯示語錄(Day 4 完成)
  3. 儲存語錄到 LocalStorage
  4. 語錄回饋畫面
  5. 顯示紀錄(歷史語錄回顧)

今天專注在 第 3 步:把想做的事(task)/沒做的原因(reason)/對應原因的語錄(quote)存進 LocalStorage。

LocalStorage + JSON 實作(單筆版)

先從最簡單的記錄單筆版開始。
核心流程:

  1. 表單送出時,把 task/reason/quote 組成一個物件。
  2. 把物件轉成 JSON 字串,用 localStorage.setItem 保存。
  3. 讀取時用 JSON.parse 還原成 JS 物件。

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?
在你的網頁空白處按右鍵,選擇「檢查」。
https://ithelp.ithome.com.tw/upload/images/20250820/20177913YYxNDWJ5Cb.png

選擇「應用程式」、展開「本機儲存空間」並選擇你的網址,應該會在右側看到一個名為「procrastinationData」的項目,以及其對應的 JSON 值。
https://ithelp.ithome.com.tw/upload/images/20250820/20177913nNMwYoklsh.png

進階:LocalStorage + script.js 記錄多筆資料

核心流程:
如果想要保存多筆紀錄,就要像「記事本」一樣:

  1. 讀 (getItem):先把「記事本」拿出來,看看裡面有什麼。
  2. parse (JSON.parse):記事本裡的內容是「文字」,要先翻譯回程式能用的「清單」。
  3. push (陣列.push):在清單的最後加上一筆新的紀錄。
  4. stringify (JSON.stringify):把清單再轉回「文字版」。
  5. setItem (存回去):把更新後的記事本放回 LocalStorage。

簡單來說:讀 → 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?

  1. 同樣在你的網頁空白處按右鍵,選擇「檢查」。
  2. 選擇「應用程式」、展開「本機儲存空間」並選擇你的網址。
  3. 應該會在右側看到一個名為「procrastination_records」的項目,以及其對應的 JSON 值。
    https://ithelp.ithome.com.tw/upload/images/20250820/20177913sIOuP5Q5fG.png

邊界情況:首筆、空資料、JSON 解析錯誤

  • 首筆資料:safeParseArray(null) 會回傳 [],可直接 push 成第一筆。
  • 空資料:讀不到就回 [],不會報錯。
  • JSON 壞掉:parse 失敗時自動 removeItem(STORAGE_KEY) 重置;使用者可繼續新增。

LocalStorage 的限制與提醒

  • 容量有限:大約 5MB。
  • 只能存字串:物件要轉成 JSON,讀取時要 parse 回來。
  • 同源限制:不同網域、子網域不能共用。
  • 安全性:存在瀏覽器裡,使用者隨時能清掉,不能用來存「敏感資料」。

上一篇
【Day 4】- 入門 JavaScript 網頁架設 :用 MVP 讓 Gemini CLI 幫忙寫第一段程式碼
下一篇
【Day 6】- 入門 JavaScript 網頁架設:歷史回顧清單與一鍵清空
系列文
Modern Web × AI《拖延怪日記》:語錄陪伴擺脫拖延19
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言