學了這麼多功能,最好的練習方式就是做一個完整的小專案。這一篇我們用前面學過的 DOM、事件、表單驗證、localStorage 與 JSON,做一個可以新增、刪除並記住資料的「簡易記帳本」。
一、功能與用到的技術
| 功能 | 用到的技術 | 對應文章 |
|---|---|---|
| 輸入項目與金額,按下新增 | 表單、事件、FormData | (二)、(八) |
| 顯示清單與結餘 | DOM 建立元素、陣列方法 | (二)、(十二) |
| 點擊刪除 | 事件委派 | (二) |
| 重新開啟網頁後資料還在 | localStorage、JSON | (五)、(十二) |
| 資料讀取失敗時不當機 | try...catch | (十三) |
二、步驟一:HTML 結構
<h2>簡易記帳本</h2>
<form id="ledgerForm">
<input name="item" placeholder="項目" required maxlength="20">
<input name="amount" type="number" placeholder="金額" min="1" step="1" required>
<select name="type">
<option value="out">支出</option>
<option value="in">收入</option>
</select>
<button>新增</button>
</form>
<ul id="ledgerList"></ul>
<p>結餘:<span id="ledgerTotal">0</span> 元</p>
欄位的 required、min 等屬性會自動交給瀏覽器做基本驗證(第八篇)。
三、步驟二:讀取與儲存資料
const KEY = 'ledger-demo';
const form = document.getElementById('ledgerForm');
const list = document.getElementById('ledgerList');
const total = document.getElementById('ledgerTotal');
function load() {
try {
return JSON.parse(localStorage.getItem(KEY)) ?? [];
} catch {
return []; // 資料損壞時,從空清單開始
}
}
function save() {
localStorage.setItem(KEY, JSON.stringify(items));
}
let items = load();
localStorage.getItem 找不到資料會回傳 null,JSON.parse(null) 會得到 null,所以用 ?? [] 補上預設的空陣列。
四、步驟三:把資料畫到畫面上
function render() {
list.replaceChildren(); // 先清空清單
let sum = 0;
items.forEach((it, i) => {
const li = document.createElement('li');
const sign = it.type === 'in' ? '+' : '-';
li.textContent = it.item + ' ' + sign + it.amount.toLocaleString() + ' 元 ';
const btn = document.createElement('button');
btn.textContent = '刪除';
btn.dataset.index = i; // 記住這筆是第幾個
li.append(btn);
list.append(li);
sum += it.type === 'in' ? it.amount : -it.amount;
});
total.textContent = sum.toLocaleString();
}
這裡用 textContent 而不是 innerHTML 來放使用者輸入的文字,可以避免有人輸入 HTML 或指令碼造成安全問題。
五、步驟四:新增與刪除
form.addEventListener('submit', (e) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(form));
items.push({
item: data.item.trim(),
amount: Number(data.amount),
type: data.type
});
save();
render();
form.reset();
});
// 事件委派:把監聽器放在清單上,不必替每個按鈕各綁一次
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-index]');
if (!btn) return;
items.splice(Number(btn.dataset.index), 1);
save();
render();
});
render(); // 開啟網頁時,先把已存的資料畫出來
六、完整程式碼(可存成 ledger.html 練習)
<!doctype html>
<meta charset="utf-8">
<title>簡易記帳本</title>
<h2>簡易記帳本</h2>
<form id="ledgerForm">
<input name="item" placeholder="項目" required maxlength="20">
<input name="amount" type="number" placeholder="金額" min="1" step="1" required>
<select name="type"><option value="out">支出</option><option value="in">收入</option></select>
<button>新增</button>
</form>
<ul id="ledgerList"></ul>
<p>結餘:<span id="ledgerTotal">0</span> 元</p>
<script>
const KEY = 'ledger-demo';
const form = document.getElementById('ledgerForm');
const list = document.getElementById('ledgerList');
const total = document.getElementById('ledgerTotal');
function load() {
try { return JSON.parse(localStorage.getItem(KEY)) ?? []; }
catch { return []; }
}
function save() { localStorage.setItem(KEY, JSON.stringify(items)); }
let items = load();
function render() {
list.replaceChildren();
let sum = 0;
items.forEach((it, i) => {
const li = document.createElement('li');
li.textContent = it.item + ' ' + (it.type === 'in' ? '+' : '-') + it.amount.toLocaleString() + ' 元 ';
const btn = document.createElement('button');
btn.textContent = '刪除';
btn.dataset.index = i;
li.append(btn);
list.append(li);
sum += it.type === 'in' ? it.amount : -it.amount;
});
total.textContent = sum.toLocaleString();
}
form.addEventListener('submit', (e) => {
e.preventDefault();
const d = Object.fromEntries(new FormData(form));
items.push({ item: d.item.trim(), amount: Number(d.amount), type: d.type });
save(); render(); form.reset();
});
list.addEventListener('click', (e) => {
const btn = e.target.closest('button[data-index]');
if (!btn) return;
items.splice(Number(btn.dataset.index), 1);
save(); render();
});
render();
</script>
用記事本存成 ledger.html(編碼選 UTF-8),再用瀏覽器開啟就能使用。部分瀏覽器直接用檔案路徑開啟時,可能限制 localStorage,如果資料沒有被記住,請改用簡單的本機伺服器(例如編輯器的 Live Server 外掛)開啟。
七、驗算
新增三筆:「便當 支出 90」「薪水 收入 1000」「飲料 支出 60」,結餘應為 1000 − 90 − 60 = 850 元。刪除「飲料」後應變成 910 元。重新整理網頁,資料仍然存在,表示 localStorage 運作正常。
八、常見問題
- 資料重新整理就不見:檢查
save()是否有在新增與刪除後呼叫,以及瀏覽器是否處於無痕模式。 - 刪錯筆:確認
render()每次都重新設定data-index,刪除後陣列位置會改變。 - 想清空所有資料:執行
localStorage.removeItem('ledger-demo')並重新整理。 - 隱私提醒:localStorage 的資料只存在使用者自己的瀏覽器,換電腦或清除瀏覽資料就會消失,真正重要的帳務不應該只靠它保存。
九、延伸練習
- 加上「日期」欄位,並依日期由新到舊排序(可複習第十二篇)。
- 加上分類(餐飲、交通、娛樂),並統計各分類的支出總額。
- 支出超過一定金額時,把該筆文字用 CSS 類別標成紅色,並用
shake()提示(第十五篇)。 - 加上「匯出資料」按鈕,把
items轉成 JSON 下載。
結語
這個小專案把變數、陣列、函式、DOM、事件、表單、localStorage 與錯誤處理全部串在一起。如果你能不看範例,自己重新做出來,就代表你已經掌握了 JavaScript 的基礎。接下來可以挑一個自己有興趣的小題目,繼續練習與擴充。
沒有留言:
張貼留言