iT邦幫忙

2026 iThome 鐵人賽

DAY 8
0
AI Engineering

知識圖譜 : 技能樹式學習歷程系列 第 8

Day 8 — 隨堂測驗元件 + 課文寫作規格

  • 分享至 

  • xImage
  •  

今天要解的問題

讀完一課,怎麼知道自己真的懂了?

我要的不是完整測驗系統,是 algomaster 那種輕量 check-in:一課結尾一題選擇題,點下去立刻知道對錯,附一段解釋為什麼其他選項錯。

再加上一個更長期的問題:兩百多課的課文,怎麼保持品質一致? 今天一起處理。

測驗元件:純 HTML + 30 行 JS

課文裡的測驗長這樣:

<div class="quiz" data-answer="1">           <!-- 0-based! -->
  <div class="quiz-label">📝 隨堂測驗</div>
  <p class="quiz-q">下列哪一項是「不拒絕 H₀」的正確解讀?</p>
  <button class="quiz-opt">已證明 H₀ 為真</button>
  <button class="quiz-opt">現有證據不足以推翻 H₀</button>
  <button class="quiz-opt">H₀ 與 H₁ 同時成立</button>
  <div class="quiz-exp">✔ 詳解:假設檢定不會「證明」H₀…</div>
</div>

沒有 JSON、沒有題庫檔——題目就寫在課文裡,因為它是課文的一部分。少一個資料層就少一個同步問題。

綁定邏輯:

bindQuizzes() {
  document.querySelectorAll(".quiz").forEach(quiz => {
    const correct = quiz.dataset.answer;
    quiz.querySelectorAll(".quiz-opt").forEach((opt, i) => {
      opt.addEventListener("click", () => {
        if (quiz.classList.contains("answered")) return;   // 只能答一次
        quiz.classList.add("answered");
        const key = (document.getElementById("lesson-body").dataset.lessonkey || "") + ":quiz";
        quiz.querySelectorAll(".quiz-opt").forEach((o, j) => {
          o.disabled = true;
          if (String(j) === correct) o.classList.add("correct");
          else if (j === i) o.classList.add("wrong");
        });
        if (String(i) === correct && window.Gamify) Gamify.onQuizCorrect(key);
      });
    });
  });
}

幾個細節:

  • 答完顯示正解,不只顯示對錯。 答錯時同時標出「你選的」(.wrong)與「正解」(.correct),這才有學習價值。
  • .answered 鎖定:答過就不能改,避免亂點到對為止。
  • String(j) === correctdataset 讀出來是字串,忘記轉型就永遠不相等。這種 bug 的症狀是「所有選項都算錯」。
  • key 帶上課文 key"ch09-1:quiz",Day 9 的計分需要它來確保同一題只給一次 XP。

CSS 用 class 切換,不寫行內樣式:

.quiz-exp { display: none; }
.quiz.answered .quiz-exp { display: block; }
.quiz-opt.correct { border-color: var(--green); background: var(--green-soft); }
.quiz-opt.wrong   { border-color: var(--red);   background: var(--red-soft); }

詳解的顯示完全交給 CSS——JS 只負責加一個 class。狀態在 DOM class 上,樣式在 CSS 裡,這是沒有框架時最好維護的模式。

為什麼「每課恰一題」

這是設計決策,不是偷懶:

  1. 降低寫作成本。兩百多課,如果每課五題,就是一千多題要寫、要校對答案。一題還能維持每題都是好題。
  2. 降低閱讀阻力。一題不會讓人覺得「還要考試」,反而像確認鍵。
  3. 可以被機械驗證。「恰一題」是能寫成斷言的規格:
assert(quizCount === 1, `${key}: expected exactly one quiz, got ${quizCount}`);

第 3 點最重要。「每課恰一題」不是靠自律,是靠 Day 10 的驗證器在 CI 裡強制執行。多寫一題會失敗、忘記寫也會失敗。

(章末的多題總測驗會在第 14 天做,那是另一個層次的東西:計分、亂序、錯題回顧。)

課文寫作規格

兩百多課要一致,靠的不是意志力,是規格。這是我實際遵守的骨架:

開場 1–2 句(這課解決什麼問題,白話)
💡 callout.idea(核心觀念,一課最多 1–2 個)
<h2> 小節 ×2–4:概念 → .formula 公式 → .example 例題
⚠️ callout.warn(常見誤區,強烈建議每課至少一個)
✅ callout.tip(實用技巧/總整理,可選)
📝 .quiz 隨堂測驗(固定收尾,恰一題)

長度基準:中文 400–800 字 + 1–3 個公式塊 + 1–2 個例題。

區塊元件

<!-- Callout 三種語意 -->
<div class="callout idea"><span class="co-label">💡 核心觀念</span> 內文…</div>
<div class="callout warn"><span class="co-label">⚠️ 常見誤區</span> 內文…</div>
<div class="callout tip"><span class="co-label">✅ 學習提示</span> 內文…</div>

<!-- 公式塊 -->
<div class="formula">\[ \bar{x} \pm z_{\alpha/2}\frac{\sigma}{\sqrt{n}} \]
<div class="f-note">σ 已知時的信賴區間</div></div>

<!-- 例題盒 -->
<div class="example"><div class="ex-head">📌 例題:平均通話時間</div><div class="ex-body">
<p>題目 → 解法 → 「👉」收尾點出 takeaway。</p></div></div>

<!-- 名詞表(中英對照) -->
<dl class="terms"><dt>型一誤差 (Type I error)</dt><dd>H₀ 為真卻拒絕它。</dd></dl>

只有六種區塊,全部是 class 沒有客製標籤。這讓課文是乾淨的語意 HTML——第 12 天要建搜尋索引、第 15 天要自動抽名詞表時,都可以直接用選擇器掃出來。規格化的內容會在之後變成可程式化的資產。

風格規則(我實際在遵守的)

  • 白話優先:先給直覺(callout.idea),再給公式。公式不是解釋,是精確化。
  • 誤區導向:每課至少一個 ⚠️。學生真正會卡住的地方是混淆(互斥 vs 獨立、p 值誤讀、「接受 H₀」),點名它比多講一遍定義有效。
  • 中英對照:統計名詞第一次出現附英文原文。
  • 測驗測觀念,不測計算:問「什麼時候該用這個方法」、「這個解讀對不對」,不問「請算出 t 值」。
  • 詳解要解釋錯誤選項,不只說正解為什麼對。

黑歷史:我曾經量產了兩百多課假課文

這段是今天最該寫的部分。

課綱有兩百多課要寫。我當時的想法是:「結構都一樣,寫一支產生器不就好了?」於是有了 advanced.js——輸入課程名與關鍵字,套進共用的講解段落,一次生出全部課文。

一天就「完成」了兩百多課。看起來很豐富,實際上:

  1. 同一門課的 12 課共用同一段講解,只有標題和關鍵字不同。讀第二課就會發現在重複。
  2. 測驗答案是算出來的data-answer = lessonIndex % 3。題目是套版的、正確答案是餘數。它「答得出對錯」,但那個對錯毫無意義。
  3. 課文裡印出了參考資料的檔名——「詳見講義第 3 章」這種字樣被套進模板,等於在網站上暴露我的私人參考素材。

最後我把整個產生器刪掉,兩百多課全部重寫。

教訓

規格可以樣板化,內容不行。

差別在於:

  • 樣板化規格(每課都要有 ⚠️ 誤區、都要恰一題測驗、公式塊都用 .formula)→ 品質下限提高。
  • 樣板化內容(12 課共用一段講解、答案用餘數算)→ 品質變成零,而且是看不出來的零,因為結構完美、驗證全過。

順帶一個具體的判準:如果你的產生器需要「填入關鍵字」,那你產的是內容;如果它只是「檢查骨架是否齊全」,那你做的是規格。前者刪掉,後者保留——後者就是第 10 天的驗證器。


踩到的雷

測驗選項的順序不能隨便改。

data-answer 是位置索引,改順序就要同步改索引。這點很明顯。但有個不明顯的:

我後來發現全站測驗的正解位置分布得不太均勻,想寫個腳本重排選項讓它均勻一點。實際統計 207 課的分布是 0:65 / 1:70 / 2:72——已經很接近均勻了,原本的印象是錯的。

而且真正的殺手在這裡:207 課裡有 141 課的詳解直接指名選項位置,例如「選項二誤把 entropy 當成…」。重排選項會讓這 141 段詳解全部前後矛盾。

結論:不重排。要重排就得連詳解一起改寫,成本遠大於收益。

這件事的教訓是:動手改資料前先量。 我如果直接寫腳本重排,會製造一百多處錯誤,而且結構驗證器一個都抓不到(答案索引還是有效的,只是文字對不上)。

驗證

先寫一段檢查,這會直接併進 Day 10 的驗證器:

function countClass(html, className) {
  const attrs = [...html.matchAll(/class=["']([^"']+)["']/g)];
  return attrs.filter(m => m[1].split(/\s+/).includes(className)).length;
}

for (const [key, html] of Object.entries(L)) {
  const quizCount = countClass(html, "quiz");
  if (quizCount !== 1) console.log(`${key}: 測驗數 ${quizCount}(應為 1)`);

  const m = html.match(/class=["']quiz["'][^>]*data-answer=["'](\d+)["']/);
  if (!m) { console.log(`${key}: 缺 data-answer`); continue; }

  const optCount = countClass(html, "quiz-opt");
  if (Number(m[1]) >= optCount)
    console.log(`${key}: data-answer ${m[1]} 超出 ${optCount} 個選項`);
}

注意 countClass 不能用 html.includes('class="quiz"')——因為 class="quiz-opt" 也含 quiz 字樣。要拆開 class 屬性逐一比對,這種細節不小心就會讓驗證器誤判整站。

瀏覽器實測:點一個錯的選項,確認三件事同時發生——你選的變紅、正解變綠、詳解出現。只測「答對」的路徑是不夠的。

小結與明天預告

今天的重點:

  1. 測驗題寫在課文裡,狀態用 CSS class 切換,JS 只加 class。
  2. 「每課恰一題」是可被機械驗證的規格,靠 CI 不靠自律。
  3. 規格可以樣板化,內容不行——這是我砍掉兩百多課換來的。
  4. 改資料前先量:我差點為了「均勻」製造一百多處矛盾。

明天做遊戲化:XP、等級、連續學習天數、徽章。順便處理一個真的資安問題——localStorage 是使用者可以隨意編輯的,而我把它的內容塞進 innerHTML


上一篇
Day 7 — MathJax 整合與兩個「靜靜壞掉」的 bug
下一篇
Day 9 — 讓人想回來:遊戲化與互動元件
系列文
知識圖譜 : 技能樹式學習歷程9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言