01為何網站需要資料管理
前兩章的複利計算器與時間價值工具,主要依靠使用者即時輸入資料。但真正的金融知識網站還需要保存並管理大量資料——金融文章、債券商品、考照題目、書籍介紹、網站導覽設定。若每一筆資料都直接寫在 HTML 中,網站內容一多,程式會難以閱讀與維護:檔案過長、格式不一致(利率有時 3.5 有時 0.035)、無法搜尋篩選、同一筆資料散落多頁修改容易漏改、無法建立管理後台。
內容與畫面分離的架構——資料驅動網站將三個部分分開管理:資料內容(JSON/Markdown/CSV)保存資訊;顯示模板(HTML)決定呈現方式;程式邏輯(JavaScript)負責讀取、搜尋、篩選與產生畫面。
同一份 JSON 可被多個頁面重複使用:首頁顯示最新 3 筆、債券專區顯示全部、搜尋頁顯示符合條件的資料,完全不需要複製貼上。
02JSON 的基本結構
JSON(JavaScript Object Notation)是一種文字格式,用來保存及交換結構化資料:文字格式可用任何編輯器開啟;可表示物件與陣列;適合 JavaScript 及 Python 處理;廣泛用於前後台資料交換;可透過 GitHub 進行版本控制。
// JSON 物件:用大括號 { } 表示單筆資料
{
"id": "BOND001",
"name": "甲公司五年期公司債",
"coupon_rate": 0.035
}
// JSON 陣列:用中括號 [ ] 包含多筆物件
[
{ "id": "BOND001", "name": "甲公司五年期公司債" },
{ "id": "BOND002", "name": "乙銀行七年期金融債" }
]
JSON 常見資料型態
| 資料型態 | 範例寫法 | 金融應用場景 | 注意事項 |
|---|---|---|---|
| 文字(string) | "AA" | 名稱、分類、評等、日期 | 必須用雙引號包住 |
| 數字(number) | 100000 / 0.035 | 面額、票面利率、年數 | 不可加千分位逗號 |
| 布林值(boolean) | true / false | 是否上架、是否精選 | 小寫,不加引號 |
| 陣列(array) | ["固定收益","ESG"] | 標籤、分類清單 | 用中括號,逗號分隔 |
| 物件(object) | {"issuer":"甲公司"} | 多層巢狀資料 | 用大括號 |
| 空值(null) | null | 尚無資料的欄位 | 小寫,不加引號 |
JSON 格式規則與常見錯誤
// ✓ 正確格式
{
"face_value": 100000,
"coupon_rate": 0.035,
"active": true
}
// ✗ 錯誤:鍵沒有雙引號、數字有千分位、布林值大寫、末尾多逗號
{
face_value: "100,000", ← 鍵無引號 + 數字寫成文字
coupon_rate: 0.035,
active: True, ← 布林值必須小寫
} ← 最後一個逗號是錯誤!
// ✗ 錯誤:JSON 不允許單引號、不允許註解
{ 'name': '甲公司' } ← 必須用雙引號
{ "name": "甲公司" } // 備註 ← 不允許 // 或 /* */ 註解
JSON 格式非常嚴格。一個多餘的逗號或少一個引號,就會導致整份資料無法讀取,並出現 Unexpected token 錯誤。建議使用 VS Code 的 JSON 語法高亮功能,錯誤會以紅色底線標示。
03設計金融商品資料欄位
資料欄位若一開始缺乏規則,後續容易發生:同一欄位使用不同名稱(coupon_rate vs rate vs 票面利率)、百分比有時用 3.5 有時用 0.035、日期格式不一致、某些資料缺少唯一識別碼——這些問題都會讓搜尋與排序失效。
本章債券資料欄位規格
| 欄位名稱 | 資料型態 | 必填 | 說明與規則 |
|---|---|---|---|
| id | string | 是 | 唯一識別碼,如 BOND001,不可重複、不可修改 |
| name | string | 是 | 債券完整名稱 |
| issuer | string | 是 | 發行機構名稱 |
| bond_type | string | 是 | 公司債/金融債/政府公債/綠色債券 |
| face_value | number | 是 | 面額,單位為元,必須大於 0 |
| coupon_rate | number | 是 | 票面利率,使用小數(3.5% → 0.035) |
| maturity_years | number | 是 | 到期年限,必須大於 0 |
| payment_frequency | number | 否 | 每年付息次數(1=年付、2=半年付、4=季付) |
| credit_rating | string | 是 | 信用評等,如 AAA/AA+/AA/A+/A/BBB |
| issue_date / maturity_date | string | 否 | 格式 YYYY-MM-DD |
| tags | array | 否 | 標籤陣列,如 ["固定收益","ESG"] |
| featured / active | boolean | active 必填 | 是否精選/是否上架(false = 前台不顯示) |
唯一識別碼 id 的重要性——每筆資料都應有固定 id(如 BOND001)。名稱可能因更名而修改,但 id 永不改變,用於精確識別、建立詳細頁連結、避免名稱重複、建立收藏紀錄、與後台系統連接。好的 id 格式:類型前綴 + 序號,避免用名稱當 id。
百分比與日期的統一標準
| 項目 | 建議方式 | 不建議 | 原因 |
|---|---|---|---|
| 票面利率 | "coupon_rate": 0.035(小數) | 3.5 或 "3.5%" | 小數可直接用 Intl.NumberFormat 轉換;文字格式無法排序 |
| 發行日期 | "issue_date": "2026-01-15" | "2026年1月15日" | YYYY-MM-DD 字串排序即等於日期排序;中文格式無法比較大小 |
04JSON、Markdown 與 CSV 的差異與使用分工
| 比較項目 | JSON | Markdown | CSV |
|---|---|---|---|
| ✅ 適合 | 債券商品清單、題庫、文章索引、網站設定與導覽、分類與標籤 | 長篇文章正文、課程教材講義、操作說明文件、書籍介紹頁 | 股票每日價格、基金淨值序列、大量交易紀錄、利率時間序列 |
| ❌ 不適合 | 長篇文章正文、大量數值計算 | 複雜欄位結構、排序搜尋困難 | 多層巢狀結構、文字中包含逗號 |
| 使用時機 | 需要程式搜尋、篩選、排序的結構化資料 | 人工撰寫的長文,轉換為 HTML 呈現 | 表格式、大量、可用試算表開啟的數值資料 |
三種格式的整合使用——實務上可以同時使用三種格式,各司其職:
// JSON:保存文章索引(標題、分類、Markdown 路徑)
[
{ "id": "ART001", "title": "債券投資入門", "category": "固定收益", "file": "articles/bond-intro.md" }
]
// Markdown:保存完整文章正文(articles/bond-intro.md)
# 債券投資入門
債券是發行機構向投資人借款所發行的有價證券。
## 一、債券的基本要素
1. 面額 2. 票面利率 3. 到期日
// CSV:保存每日價格資料(data/bond-prices.csv)
date,bond_id,price,yield
2026-01-02,BOND001,99.5,0.036
2026-01-03,BOND001,99.8,0.035
05母資料與前台索引資料
| 概念 | 說明 | 內容範例 | 保存位置 |
|---|---|---|---|
| 母資料 | 最完整、最具權威性的資料來源 | 書名、ISBN、章節、完整介紹、上下架狀態 | data/bond-details.json |
| 前台索引 | 精簡版,只含前台顯示所需欄位 | id、名稱、封面、簡短摘要、排序 | data/bonds.json(程式產生) |
母資料應只維護一份。若首頁、列表頁與後台各自保存一份,容易發生書名已修改但首頁仍顯示舊名、商品已下架但搜尋頁仍可找到等同步問題。只修改母資料 → 重新產生索引 → 前台自動更新。
06建立債券 JSON 資料
在 data/ 資料夾中建立 bonds.json,包含 6 筆債券,其中 BOND006 設定 active: false(下架),前台將只顯示 5 筆。
[
{
"id": "BOND001",
"name": "甲公司五年期公司債",
"issuer": "甲科技股份有限公司",
"bond_type": "公司債",
"face_value": 100000,
"coupon_rate": 0.035, ← 3.5%,使用小數
"maturity_years": 5,
"payment_frequency": 1, ← 每年付息一次
"credit_rating": "AA",
"issue_date": "2026-01-15", ← YYYY-MM-DD 格式
"maturity_date": "2031-01-15",
"currency": "TWD",
"tags": ["固定收益", "科技業", "中期債券"],
"featured": true, ← 精選商品
"active": true, ← 上架中
"description": "由甲科技發行的五年期普通公司債,每年付息一次。"
},
{
"id": "BOND006",
"name": "戊公司四年期公司債",
"...": "...",
"active": false ← 下架!前台不顯示
}
]
三個關鍵欄位說明
| 欄位 | 值 | 前台行為 | 初學者常見錯誤 |
|---|---|---|---|
| active | true / false(布林值) | false → 程式篩選排除,前台不顯示 | "false"(字串)仍會通過 !== null 判斷,導致顯示異常 |
| featured | true / false(布林值) | true → 卡片顯示「精選」標籤,排序優先 | 與 active 獨立:可以精選但下架,也可上架但不精選 |
| tags | ["ESG","綠色金融"](陣列) | 搜尋時合併納入搜尋文字範圍 | 寫成字串而非陣列,程式展開會失敗 |
07建立債券資料庫 HTML 頁面
新頁面 bond-database.html 包含五個區域:頁首導覽列、篩選卡片(關鍵字/評等/年限/排序)、結果計數列、債券卡片 Grid,以及無結果提示區。
<!DOCTYPE html>
<html lang="zh-Hant">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="可依關鍵字、信用評等與到期年限篩選的教學用債券資料庫。">
<title>金融商品資料庫</title>
<link rel="stylesheet" href="css/bond-database.css">
</head>
<body>
<!-- ① 頁首導覽 -->
<header class="site-header">
<div class="header-container">
<a href="index.html" class="site-title">金融程式設計實作</a>
<nav class="main-nav">
<a href="index.html">複利工具</a>
<a href="time-value.html">時間價值</a>
<a href="bond-database.html" aria-current="page">債券資料庫</a>
</nav>
</div>
</header>
<main class="page-container">
<!-- ② 頁面標題 -->
<section class="page-introduction">
<p class="category-label">金融資料管理</p>
<h1>金融商品資料庫</h1>
<p>本頁使用 JSON 保存債券資料,並透過 JavaScript 載入、搜尋及篩選。</p>
</section>
<!-- ③ 篩選卡片(4欄 Grid) -->
<section class="filter-card">
<div class="filter-grid">
<div class="form-group">
<label for="keywordInput">關鍵字搜尋</label>
<input type="search" id="keywordInput" placeholder="輸入名稱、發行機構或標籤">
</div>
<div class="form-group">
<label for="ratingFilter">信用評等</label>
<select id="ratingFilter">
<option value="all">全部評等</option>
<option value="AAA">AAA</option>
<option value="AA+">AA+</option>
<option value="AA">AA</option>
<option value="A+">A+</option>
<option value="A">A</option>
<option value="BBB">BBB</option>
</select>
</div>
<div class="form-group">
<label for="maturityFilter">到期年限</label>
<select id="maturityFilter">
<option value="all">全部年限</option>
<option value="short">3 年以下</option>
<option value="medium">4 年至 7 年</option>
<option value="long">8 年以上</option>
</select>
</div>
<div class="form-group">
<label for="sortOrder">排序方式</label>
<select id="sortOrder">
<option value="featured">精選優先</option>
<option value="rate-high">票面利率由高至低</option>
<option value="rate-low">票面利率由低至高</option>
<option value="years-short">年限由短至長</option>
<option value="years-long">年限由長至短</option>
</select>
</div>
</div>
<div class="filter-actions">
<button type="button" id="clearFiltersButton" class="secondary-button">清除篩選</button>
</div>
</section>
<!-- ④ 結果計數列 -->
<section class="result-summary">
<p id="resultCount">資料載入中</p>
<p id="statusMessage" class="status-message" aria-live="polite"></p>
</section>
<!-- ⑤ 債券卡片 Grid(由 JS 產生) -->
<section id="bondList" class="bond-grid" aria-live="polite"></section>
<!-- ⑥ 無結果提示 -->
<section id="emptyState" class="empty-state hidden">
<h2>找不到符合條件的資料</h2>
<p>請調整關鍵字、信用評等或到期年限。</p>
</section>
</main>
<footer class="site-footer">本頁資料為教學示例,不代表實際募集、報價或投資建議。</footer>
<script src="js/bond-database.js"></script>
</body>
</html>
08CSS 卡片與篩選介面
* { box-sizing: border-box; }
:root {
--primary-color: #2457a6;
--card-color: #ffffff;
--tag-background: #eef4fb;
--success-background: #eaf7ef;
--success-text: #1f6b3d;
--warning-background: #fff5e6;
--warning-text: #8a5310;
}
/* ── 篩選卡片:4 欄 Grid ── */
.filter-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 18px;
}
/* ── 債券卡片 Grid:桌面 3 欄 ── */
.bond-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 22px;
}
.bond-card { position: relative; padding: 24px; border-radius: 14px;
background: var(--card-color); box-shadow: 0 6px 18px rgba(0,0,0,0.06); }
.bond-card.featured { border-top: 5px solid var(--primary-color); }
/* ── 債券資料列(dt/dd 標籤−值二欄對齊) ── */
.bond-data-row { display: grid; grid-template-columns: 100px 1fr; gap: 10px;
padding: 7px 0; border-bottom: 1px solid #edf0f2; }
.bond-data-row dd { margin: 0; text-align: right; font-weight: 700; }
/* ── 標籤 pill 樣式 ── */
.tag { padding: 4px 10px; border-radius: 999px; background: var(--tag-background); font-size: 13px; }
.risk-label { display: inline-block; margin-top: 10px; padding: 4px 10px;
border-radius: 6px; background: var(--warning-background); color: var(--warning-text); font-size: 13px; }
/* ── RWD 三斷點 ── */
@media (max-width: 980px) {
.filter-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.bond-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 650px) {
.filter-grid, .bond-grid { grid-template-columns: 1fr; }
.bond-data-row { grid-template-columns: 1fr; gap: 2px; }
.bond-data-row dd { text-align: left; }
}
| CSS 設計 | 說明 | 初學者要點 |
|---|---|---|
| .bond-card.featured | 精選卡片頂端加藍色邊框 | 選擇器組合:代表同時有兩個 class 的元素 |
| .bond-grid repeat(3,...) | 債券卡片 3 欄 Grid 排列 | 980px 以下改 2 欄,650px 以下改 1 欄 |
| .bond-data-row (dt/dd) | 標籤−值二欄對齊排版 | dt 左欄說明文字,dd 右欄數值(粗體) |
| .tag(圓角標籤) | 標籤 pill 樣式 | border-radius: 999px 讓任何寬度都顯示為橢圓形 |
| .risk-label(橘色警示) | 根據評等自動顯示風險等級 | 由 JS 的 getRiskLabel() 函數決定文字內容 |
09使用 fetch 讀取 JSON
fetch 可向指定路徑取得資料。讀取檔案需要時間,瀏覽器不會停住整個頁面等待,而會先繼續處理其他工作——這種方式稱為非同步操作。await 表示「等待此步驟完成再繼續」,async 表示「此函數包含非同步操作」。
// 方式一:傳統 .then() 鏈式寫法(較難讀)
fetch("data/bonds.json")
.then(response => response.json())
.then(data => console.log(data));
// 方式二:async/await(本章使用,較易讀)
async function loadBondData() {
try {
const response = await fetch("data/bonds.json");
if (!response.ok) throw new Error(`HTTP 錯誤:${response.status}`);
const data = await response.json();
// 繼續處理資料...
} catch (error) {
console.error("載入失敗:", error);
}
}
await 只能在 async 函數內使用。try...catch 是必要的,因為網路請求或 JSON 解析都可能失敗。
10JavaScript 完整資料處理程式
程式分為十六個功能區塊,每個區塊負責單一職責。
"use strict";
// ── ① 取得所有 HTML 元素 ─────────────────────────
const keywordInput = document.getElementById("keywordInput");
const ratingFilter = document.getElementById("ratingFilter");
const maturityFilter = document.getElementById("maturityFilter");
const sortOrder = document.getElementById("sortOrder");
const clearFiltersButton = document.getElementById("clearFiltersButton");
const resultCount = document.getElementById("resultCount");
const statusMessage = document.getElementById("statusMessage");
const bondList = document.getElementById("bondList");
const emptyState = document.getElementById("emptyState");
// ── ② 全域資料陣列(載入後保存,搜尋時不需重新讀取) ──
let allBonds = [];
// ── ③ 格式化函數 ────────────────────────────────
function formatCurrency(amount) {
return new Intl.NumberFormat("zh-TW", {
style: "currency", currency: "TWD", maximumFractionDigits: 0
}).format(amount);
}
function formatPercent(rate) {
return new Intl.NumberFormat("zh-TW", {
style: "percent", minimumFractionDigits: 2, maximumFractionDigits: 2
}).format(rate);
}
function formatPaymentFrequency(freq) {
if (freq === 1) return "每年一次";
if (freq === 2) return "每半年一次";
if (freq === 4) return "每季一次";
return `每年 ${freq} 次`;
}
// ── ④ 根據信用評等產生風險標示文字 ─────────────
function getRiskLabel(rating) {
const low = ["AAA","AA+","AA","AA-"];
const mid = ["A+","A","A-"];
if (low.includes(rating)) return "相對較低信用風險";
if (mid.includes(rating)) return "中度信用風險";
return "信用風險相對較高";
}
// ── ⑤ 資料驗證:必要欄位是否存在 ──────────────
function validateBond(bond) {
const required = ["id","name","issuer","bond_type",
"face_value","coupon_rate","maturity_years",
"credit_rating","active"];
return required.every(f => bond[f] !== undefined && bond[f] !== null);
}
// ── ⑥ 安全輸出:防止 HTML 注入 ─────────────────
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
// ── ⑦ 產生標籤 HTML ─────────────────────────────
function createTagHtml(tags) {
if (!Array.isArray(tags)) return "";
return tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join("");
}
// ── ⑧ 產生單張債券卡片 HTML ─────────────────────
function createBondCard(bond) {
const featuredClass = bond.featured ? "featured" : "";
const featuredLabel = bond.featured ? '<span class="featured-label">精選</span>' : "";
return `
<article class="bond-card ${featuredClass}">
${featuredLabel}
<p class="bond-type">${escapeHtml(bond.bond_type)}</p>
<h2>${escapeHtml(bond.name)}</h2>
<p class="issuer">發行機構:${escapeHtml(bond.issuer)}</p>
<dl class="bond-data-list">
<div class="bond-data-row"><dt>面額</dt><dd>${formatCurrency(bond.face_value)}</dd></div>
<div class="bond-data-row"><dt>票面利率</dt><dd>${formatPercent(bond.coupon_rate)}</dd></div>
<div class="bond-data-row"><dt>到期年限</dt><dd>${bond.maturity_years} 年</dd></div>
<div class="bond-data-row"><dt>付息方式</dt><dd>${formatPaymentFrequency(bond.payment_frequency)}</dd></div>
<div class="bond-data-row"><dt>信用評等</dt><dd>${escapeHtml(bond.credit_rating)}</dd></div>
<div class="bond-data-row"><dt>到期日</dt><dd>${escapeHtml(bond.maturity_date)}</dd></div>
</dl>
<p class="description">${escapeHtml(bond.description)}</p>
<div class="tag-list">${createTagHtml(bond.tags)}</div>
<span class="risk-label">${getRiskLabel(bond.credit_rating)}</span>
</article>
`;
}
// ── ⑨ 三個篩選函數 ──────────────────────────────
function matchesKeyword(bond, keyword) {
if (keyword === "") return true;
const text = [
bond.name, bond.issuer, bond.bond_type,
bond.credit_rating, bond.description,
...(Array.isArray(bond.tags) ? bond.tags : []) // 標籤展開
].join(" ").toLowerCase();
return text.includes(keyword.toLowerCase());
}
function matchesRating(bond, rating) {
if (rating === "all") return true;
return bond.credit_rating === rating;
}
function matchesMaturity(bond, maturity) {
if (maturity === "all") return true;
if (maturity === "short") return bond.maturity_years <= 3;
if (maturity === "medium") return bond.maturity_years >= 4 && bond.maturity_years <= 7;
if (maturity === "long") return bond.maturity_years >= 8;
return true;
}
// ── ⑩ 排序(用 [...bonds] 建立副本,不改原陣列)──
function sortBonds(bonds, selectedSort) {
const sorted = [...bonds]; // 展開運算子建立副本
if (selectedSort === "featured") sorted.sort((a,b) =>
a.featured === b.featured ? a.name.localeCompare(b.name,"zh-Hant")
: Number(b.featured) - Number(a.featured));
if (selectedSort === "rate-high") sorted.sort((a,b) => b.coupon_rate - a.coupon_rate);
if (selectedSort === "rate-low") sorted.sort((a,b) => a.coupon_rate - b.coupon_rate);
if (selectedSort === "years-short") sorted.sort((a,b) => a.maturity_years - b.maturity_years);
if (selectedSort === "years-long") sorted.sort((a,b) => b.maturity_years - a.maturity_years);
return sorted;
}
// ── ⑪ 渲染:將資料陣列產生 HTML 並插入頁面 ─────
function renderBonds(bonds) {
bondList.innerHTML = "";
if (bonds.length === 0) {
resultCount.textContent = "共找到 0 筆資料";
emptyState.classList.remove("hidden");
return;
}
emptyState.classList.add("hidden");
resultCount.textContent = `共找到 ${bonds.length} 筆資料`;
bondList.innerHTML = bonds.map(createBondCard).join("");
}
// ── ⑫ 套用篩選:整合搜尋+篩選+排序+渲染 ────────
function applyFilters() {
const keyword = keywordInput.value.trim();
const rating = ratingFilter.value;
const maturity = maturityFilter.value;
const sort = sortOrder.value;
const filtered = allBonds
.filter(b => matchesKeyword(b, keyword))
.filter(b => matchesRating(b, rating))
.filter(b => matchesMaturity(b, maturity));
renderBonds(sortBonds(filtered, sort));
}
// ── ⑬ 清除所有篩選條件 ─────────────────────────
function clearFilters() {
keywordInput.value = "";
ratingFilter.value = "all";
maturityFilter.value= "all";
sortOrder.value = "featured";
applyFilters();
}
// ── ⑭ 非同步載入 JSON(主要入口) ───────────────
async function loadBondData() {
statusMessage.textContent = "資料載入中";
try {
const response = await fetch("data/bonds.json");
if (!response.ok) throw new Error(`讀取失敗:${response.status}`);
const data = await response.json();
if (!Array.isArray(data)) throw new Error("JSON 最外層必須是陣列。");
allBonds = data
.filter(validateBond) // 排除缺少必要欄位的資料
.filter(b => b.active === true); // 只保留上架資料
statusMessage.textContent = "";
applyFilters();
} catch (error) {
resultCount.textContent = "資料無法顯示";
statusMessage.textContent = "載入債券資料時發生錯誤,請檢查 JSON 格式與檔案路徑。";
console.error(error);
}
}
// ── ⑮ 事件監聽:四個篩選條件 + 清除按鈕 ────────
keywordInput.addEventListener("input", applyFilters);
ratingFilter.addEventListener("change", applyFilters);
maturityFilter.addEventListener("change", applyFilters);
sortOrder.addEventListener("change", applyFilters);
clearFiltersButton.addEventListener("click", clearFilters);
// ── ⑯ 頁面載入後執行 ────────────────────────────
loadBondData();
11JavaScript 程式重點解析
| 函數 | 職責 | 使用的 JS 方法 |
|---|---|---|
| validateBond() | 驗證必要欄位是否存在 | Array.every() |
| escapeHtml() | 防止 HTML 注入(安全輸出) | String.replaceAll() |
| createTagHtml() | 將標籤陣列轉為 HTML 標籤 | Array.map() + join() |
| createBondCard() | 產生單張債券卡片 HTML | 模板字串(template literal) |
| matchesKeyword() | 關鍵字搜尋(含標籤) | Array.includes() + toLowerCase() |
| sortBonds() | 依條件排序(不改原陣列) | [...bonds] 展開 + Array.sort() |
| renderBonds() | 將資料陣列插入 DOM | innerHTML + map().join() |
| loadBondData() | 非同步載入與驗證 JSON | async/await + fetch + try/catch |
五個關鍵 JavaScript 技術
// 1. filter():保留符合條件的元素
allBonds.filter(b => b.active === true) // 只保留上架資料
// 2. map():將每筆資料轉換為 HTML 字串
bonds.map(createBondCard).join("") // 所有卡片串接
// 3. 展開運算子 [...]:建立副本不改原陣列
const sorted = [...bonds]; // sort() 直接改原陣列,所以先複製
// 4. 搜尋時展開 tags 陣列並合併
const text = [bond.name, bond.issuer,
...(Array.isArray(bond.tags) ? bond.tags : [])
].join(" ").toLowerCase();
// 如果 tags = ["ESG", "綠色金融"],展開後合併為 "ESG 綠色金融"
// 5. escapeHtml():防止資料中的 HTML 標籤被執行
escapeHtml("<script>alert(1)</script>")
// 輸出:<script>alert(1)</script> ← 安全文字
escapeHtml 是金融工具的資安基礎。若資料直接插入 innerHTML,惡意內容(如 script 標籤)可能執行不預期的程式。這在真實網站中屬於 XSS(跨站腳本攻擊)漏洞。
12為何直接開 HTML 可能讀不到 JSON
若直接雙擊開啟 bond-database.html,網址會顯示 file:///C:/...。部分瀏覽器基於安全限制(CORS 政策),不允許 fetch 讀取本機 JSON 檔案,Console 會出現與 CORS 相關的錯誤訊息。
| 方式 | 操作步驟 | 適合場景 |
|---|---|---|
| VS Code Live Server(推薦) | 安裝 Live Server 延伸套件 → 在 HTML 按右鍵 → Open with Live Server → 網址變為 http://127.0.0.1:5500/ | 日常開發,自動重新整理頁面 |
| Python 本機伺服器 | 在專案資料夾開啟 PowerShell → 輸入指令 → 瀏覽器開啟 http://localhost:8000/ | 不需安裝 VS Code 延伸套件 |
# 在專案資料夾的 PowerShell 輸入(Python 3.12): py -3.12 -m http.server 8000 # 若電腦的 Python 指令是 python,可改用: python -m http.server 8000 # 瀏覽器開啟: http://localhost:8000/bond-database.html # 停止伺服器:回到 PowerShell 按 Ctrl + C
13操作實務與測試
開啟頁面後,預期顯示 5 筆資料(JSON 共 6 筆,但 BOND006 的 active 為 false 被排除)。resultCount 應顯示「共找到 5 筆資料」。
| 測試操作 | 輸入/選擇 | 預期結果 |
|---|---|---|
| 關鍵字搜尋 | 輸入「能源」 | 顯示丁能源六年期綠色債券 |
| 標籤搜尋 | 輸入「ESG」 | 顯示丁能源(因 ESG 在 tags 中) |
| 評等篩選 | 選 AA | 只顯示甲公司五年期公司債 |
| 年限篩選 | 選 3 年以下 | 只顯示丙建設三年期公司債 |
| 排序 | 票面利率由高至低 | 順序:4.80%→4.10%→3.50%→3.20%→1.80% |
| 複合篩選 | 關鍵字「金融」+ 評等「AA+」+ 年限「4-7年」 | 只顯示乙銀行七年期金融債 |
錯誤資料測試——將 bonds.json 中的一個逗號暫時刪除,重新載入頁面。預期:資料無法顯示、頁面顯示載入錯誤訊息、F12 Console 顯示 JSON 解析錯誤。測試完成後務必恢復正確格式。
14實務案例
(一)資料欄位不一致的問題
若三筆債券的利率欄位各不相同(coupon_rate: 0.035、rate: 4.2、票面利率: "3.8%"),程式無法統一處理,前台排序錯誤、金融計算可能誤用百分比。解決方式:建立資料字典,全站統一使用 coupon_rate(小數格式)。
(二)票面利率高不代表投資較好
投資人可能只搜尋票面利率最高的債券,結果優先看到 BBB 評等的丙建設債券(4.80%)。票面利率較高,通常反映市場要求較高風險補償。金融資料卡片應同時呈現票面利率、信用評等、到期年限與風險提醒,讓使用者能全面判斷。
(三)下架資料仍出現在前台
常見原因:程式沒有篩選 active(遺漏 .filter(b => b.active === true));active 被錯誤保存為字串 "false" 而非布林值 false(字串 "false" 在 === true 判斷時會排除,但若用 if (bond.active) 判斷,字串 "false" 仍為 truthy!);首頁使用另一份舊的 JSON 索引資料,與母資料未同步;瀏覽器快取了舊版 JSON,需 Ctrl+F5 強制重新整理。
(四)日期格式造成排序錯誤
若資料中同時出現 2026-03-08、03/08/2026 與 2026年3月8日 三種格式,字串排序會產生錯誤結果。統一使用 YYYY-MM-DD 格式,字串排序即等於日期排序,不需額外轉換。
15常見錯誤與除錯
| 錯誤訊息 / 症狀 | 最可能原因 | 修正方式 |
|---|---|---|
| Unexpected token 或 JSON parse error | JSON 格式錯誤(逗號、引號問題) | 用 VS Code JSON 格式化工具,或貼到 jsonlint.com 驗證 |
| Failed to fetch | 直接以 file:// 開啟,或路徑拼錯 | 啟動 Live Server 或 Python 本機伺服器 |
| 資料載入但畫面空白 | 所有 active 為 false,或必要欄位缺失 | F12 Console 查錯,確認 validateBond 通過 |
| 票面利率顯示 350% | JSON 用 3.5 但格式化時又乘以 100 | JSON 統一存 0.035,formatPercent 直接傳小數 |
| 搜尋不到標籤內容 | tags 不是陣列,或展開運算子邏輯錯誤 | 確認 Array.isArray(bond.tags) |
| 排序改變原始資料順序 | sort() 直接修改原陣列 | 先用 [...bonds] 建立副本再排序 |
16資料安全與品質管理
| 概念 | 說明 | 本章對應程式 |
|---|---|---|
| 輸入驗證 | 確認資料符合欄位規則(型態、範圍、必填) | validateBond() 檢查必要欄位是否存在 |
| 輸出編碼 | 避免資料中的特殊符號被瀏覽器當成 HTML 執行 | escapeHtml() 轉換特殊符號 |
兩者功能不同,不能只做其中一項。輸入驗證保護資料品質;輸出編碼保護前台安全。
資料字典範例
| 欄位 | 型態 | 必填 | 驗證規則 |
|---|---|---|---|
| id | string | 是 | 不可重複、不可空白、格式 BOND\d{3} |
| coupon_rate | number | 是 | 小數格式,0 < coupon_rate < 0.5 |
| active | boolean | 是 | 只接受 true 或 false,不接受字串 |
| tags | array | 否 | 必須為陣列,每個元素為字串 |
備份策略
data/ ├─ bonds.json ← 現行版本 └─ backups/ ├─ bonds_2026-06-01.json ├─ bonds_2026-06-10.json └─ bonds_2026-06-20.json # 也可透過 GitHub 版本紀錄還原特定日期的 JSON
17關鍵字
18實作練習
- 新增一筆八年期公司債資料(BOND007),設定 active: true,加入三個標籤
- 測試關鍵字是否可以搜尋到新資料(含標籤搜尋)
- 將 BOND007 設定為精選(featured: true),確認卡片頂端出現藍色邊框
- 將 BOND003 設定為下架(active: false),確認前台從 5 筆變為 4 筆
- 測試「年限由長至短」排序,確認 10 年排在最前面
- 增加債券種類篩選(公司債/金融債/政府公債/綠色債券)
- 增加計價幣別篩選(TWD/USD)
- 增加只顯示精選商品的勾選框(featured 篩選)
- 增加票面利率區間篩選(< 2% / 2%–4% / > 4%)
- 增加資料卡片的「查看詳情」按鈕(連結到 bond-detail.html?id=BOND001)
- 增加目前篩選條件的文字摘要
- 建立 bond-detail.html,透過 URL 參數 ?id=BOND001 讀取並顯示指定債券詳情
- 使用 Python 撰寫欄位驗證腳本,自動檢查所有債券的 id 唯一性與欄位格式
- 建立從 CSV 轉換為 JSON 的 Python 腳本(pandas read_csv → to_json)
- 將 Markdown 文章與 JSON 索引連結,建立文章詳細頁
- 將債券資料部署至 Cloudflare Pages,確認 fetch 在線上環境可正常運作
上面「功能修改」清單中的統計類需求,正是本章挑戰任務(04 分頁)任務一的兩個變體;「常見錯誤與除錯」表格中的症狀,則對應任務二、三——先在這裡讀懂需求與陷阱,再到挑戰任務分頁動手實作與除錯。
本章結語——本章將金融網站從單一計算工具,進一步擴充為資料驅動的金融資訊系統。JSON 適合保存結構化資料(債券、題庫、文章索引);Markdown 適合長篇文章;CSV 適合大量表格式數值資料。
讀者建立了完整的債券 JSON 資料,設計唯一識別碼、票面利率(小數格式)、信用評等、上下架控制、精選標籤等欄位,並使用 fetch + async/await 載入資料,再以 filter、map、sort 實現搜尋、篩選與排序功能。
良好的金融資料管理不只是將資料存入檔案,而是要建立一致的欄位名稱、資料型態標準、驗證規則與備份機制。只要底層資料結構穩定,前台功能才能持續擴充而不失控。
下一章進入 Python 金融運算與資料清理,學習使用 Python + Pandas 讀取 CSV 股票價格,計算每日報酬率、累積報酬率、平均報酬率、波動度及最大單日漲跌幅。
—金融商品資料庫(教材原始範例)
輸入關鍵字、切換信用評等或到期年限、改變排序方式,資料會即時更新。這就是課本第六~十節的 bond-database.html/bond-database.css/bond-database.js 完全對應的成品——資料是用 fetch + async/await 從 JSON 載入的(示範環境用內嵌資料模擬檔案讀取)。等一下的挑戰任務就是在這份程式碼上動手修改。
—觀念測驗
共 15 題選擇題,涵蓋本章所有核心觀念。每題作答後會立即顯示解析。
挑戰任務:修改程式碼,讓工具自己告訴你哪裡錯
下面三個練習都是「真的會執行」的程式碼,包含真實的 fetch 非同步資料載入。修改完成後按下「執行測試」,系統會把你的程式碼放進真實瀏覽器環境中運行(並自動等待資料載入完成),用白話文告訴你哪裡不對。
任務一:功能修改
對應教材「十八、實作練習(二)功能修改」
- 在結果計數列旁邊,新增顯示目前「精選商品」的筆數
- HTML 已經留好 #featuredCount 空欄位,只需要在 JavaScript 中賦值
提示(卡住了再打開)
renderBonds() 函式中,resultCount.textContent = ... 之後、bondList.innerHTML = ... 之前,加入一行把統計結果寫進對應的空欄位。
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己動手做一次,或按「換一題再練習」挑戰另一個類似的任務。
任務二:抓出隱藏的 id 錯誤
對應教材「十五、常見錯誤與除錯」— 資料載入但畫面空白
- 這份程式碼交接自另一位同學,執行後完全沒有債券卡片顯示出來
- 找出問題並修正,讓資料庫恢復正常運作
提示(卡住了再打開)
document.getElementById("...") 抓到的是 null。逐一比對 HTML 裡每個 id="..." 跟 JS 裡 getElementById("...") 的拼字是否一模一樣。
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一個 id 錯誤。
任務三:資料邏輯錯誤
對應教材「十五、常見錯誤與除錯」— 下架資料仍出現 / 票面利率顯示 350%
- 這份程式碼可以正常執行、不會報錯,但顯示的資料不合理
- 找出問題所在並修正
提示(卡住了再打開)
💡 查看範例解答(建議自己先試過再打開)
看完解答後,可以按「還原初始程式碼」重新自己抓一次,或按「換一題再練習」練習抓另一種資料邏輯錯誤。