昨天終於在 lessons.js 建立了第一份泰語教材,包含你好、謝謝、對不起和再見四句基本問候。
不過,光是把教材放進檔案裡,使用者還是沒辦法直接學習。
目前的首頁雖然已經有五張情境課程卡片,但它們都還只是靜態畫面。就算看到「基本問候」,也還不能真的點進去學習。
所以今天要解決的問題很明確:
讓使用者可以從首頁進入基本問候課程,看到四句泰語,再返回首頁。
這也是「泰語小旅伴」第一次真正加入課程互動功能。
前面 Day 2 已經規劃了五個旅遊情境,包括基本問候、購物、餐廳點餐、咖啡店和交通問路。
但現在如果一次把五個課程都做出來,對完全不熟悉 JavaScript 的我來說,可能會一下子多出太多需要理解和驗收的內容。
所以這次先選擇「基本問候」,利用已經準備好的四句教材,專心處理使用者怎麼看到它們。
和 ChatGPT 討論後,我們先把這次的目標整理成三件事:
暫時不加入發音、測驗和學習紀錄。
這樣做完之後,我至少知道自己該檢查哪些功能,而不是看到 Codex 說完成,就不知道接下來該做什麼。
在 Day 6 製作首頁時,我們主要調整的是 HTML 和 CSS,當時的目標是讓畫面看起來更像手機 App,還沒有要求課程卡片真的能操作。
但今天不一樣了。
使用者按下「基本問候」之後,App 需要顯示另一個畫面,並把前一天建立的教材呈現出來。
我先把這件事理解成:
首頁
↓
點擊「基本問候」
↓
顯示基本問候課程
↓
看到四句教材
↓
點擊「返回首頁」
↓
回到首頁
看起來只有幾個步驟,但它已經不只是把文字放在網頁上,而是要讓畫面根據使用者的操作產生變化。
這時候,Day 4 認識的 JavaScript 終於開始派上用場。
討論清楚之後,就把需求交給 Codex 實作。
這次的要求包括讓「基本問候」可以點擊、顯示四句教材,以及提供返回首頁的操作。
另外,教材內容要使用前面建立的 data/lessons.js,而不是重新把四句泰語寫進 HTML。
根據開發紀錄,這一輪總共修改四個檔案:
index.html
css/style.css
data/lessons.js
js/app.js
這裡補充一下:這四個檔案其實是在同一輪實作中修改的。昨天的 Day 8 先介紹教材資料的建立,今天則接著理解同一輪實作中的課程互動部分。
透過這次修改,我開始看到不同檔案怎麼互相配合。
lessons.js 負責保存教材,app.js 負責處理課程切換與教材呈現,而 HTML 和 CSS 則提供畫面結構與樣式。
原本分開存在專案裡的檔案,現在開始一起完成同一個功能了。
這次有個地方讓我開始理解,為什麼前一天要先把教材獨立出來。
因為教材已經放在 lessons.js,JavaScript 就可以使用這些資料,產生畫面上的教材卡片,不需要在 HTML 裡重新保存一份相同的內容。
Codex 在 app.js 裡建立了一個函式:
// 根據一筆教材資料建立一張句子卡片。
function createPhraseCard(phrase) {
const card = document.createElement("article");
const chinese = document.createElement("h3");
const thai = document.createElement("p");
const pronunciation = document.createElement("p");
card.className = "phrase-card";
chinese.className = "phrase-chinese";
thai.className = "phrase-thai";
pronunciation.className = "phrase-pronunciation";
chinese.textContent = phrase.chinese;
thai.textContent = phrase.thai;
thai.lang = "th";
pronunciation.textContent = phrase.pronunciation;
card.append(chinese, thai, pronunciation);
return card;
}
第一次看到這段程式,我還沒辦法完全理解每一行,但可以先從它的目的開始看。
createPhraseCard() 的工作,是根據一筆教材資料,建立一張顯示句子的卡片。
其中,document.createElement() 可以用來建立 HTML 元素。
例如:
const card = document.createElement("article");
就是建立一個 article 元素,準備用來當作教材卡片。
接著,程式透過 textContent,把教材資料填入對應的位置:
chinese.textContent = phrase.chinese;
thai.textContent = phrase.thai;
pronunciation.textContent = phrase.pronunciation;
這三行對應的就是中文、泰文和羅馬拼音。
例如 phrase.thai 會取得教材資料中的泰文字串,再放進畫面上的泰文元素。
最後,程式把這些元素組合成一張卡片:
card.append(chinese, thai, pronunciation);
對現在的我來說,可以先把整段程式理解成:
拿到一筆教材 → 建立卡片 → 填入中文、泰文和拼音 → 把卡片交回去使用。
這比直接把四句教材都寫死在 HTML 裡,更能讓我理解前一天討論的資料與畫面分離。
建立好單張教材卡片後,接下來還需要把整個課程顯示出來。
Codex 在 app.js 裡寫了另一個函式:
// 顯示基本問候課程,並把 lessons.js 的教材放進畫面。
function showBasicGreetingsLesson() {
const lesson = lessons.basicGreetings;
lessonTitle.textContent = lesson.title;
phraseList.replaceChildren();
lesson.phrases.forEach(function (phrase) {
phraseList.append(createPhraseCard(phrase));
});
homeView.hidden = true;
bottomNav.hidden = true;
lessonView.hidden = false;
}
這段程式雖然比前面多了一些新語法,但拆開來看,還是能慢慢理解。
首先:
const lesson = lessons.basicGreetings;
這行會從 lessons.js 的教材資料中,取得基本問候課程。
接著:
lessonTitle.textContent = lesson.title;
phraseList.replaceChildren();
第一行負責設定課程標題,第二行則是先清空原本的教材列表。
為什麼要先清空?
因為接下來會重新產生教材卡片,先清空列表,就能避免重複加入原本的內容。
而真正把四句教材顯示出來的關鍵,是這段:
lesson.phrases.forEach(function (phrase) {
phraseList.append(createPhraseCard(phrase));
});
forEach() 可以先理解成:針對陣列裡的每一筆資料,依序執行指定的工作。
我們的基本問候課程有四句教材,所以程式就會依序取得四筆資料,每次呼叫 createPhraseCard() 建立一張卡片,再把卡片放進教材列表。
這也是我第一次透過自己的 App,看到 JavaScript 的陣列不只是保存資料,還可以讓程式用相同的方式處理多筆內容。
不用為了「你好」、「謝謝」、「對不起」和「再見」,各自寫一套不同的卡片產生程式。
教材準備好之後,還需要讓使用者看到課程畫面。
在 showBasicGreetingsLesson() 的最後,Codex 寫了:
homeView.hidden = true;
bottomNav.hidden = true;
lessonView.hidden = false;
這裡的 hidden 可以先理解成控制 HTML 元素是否隱藏的屬性。
設定成 true,代表把對應元素隱藏;設定成 false,則代表取消隱藏。
所以這三行的意思就是:
把首頁藏起來、把底部導覽藏起來,再顯示課程畫面。
對現在的我來說,這種做法比想像中容易理解。
原來這次不是重新開啟另一個網頁,而是在同一個頁面裡,控制哪些區域要顯示、哪些區域要隱藏。
不過,要讓它真的可以操作,還需要把這個函式和首頁按鈕連接起來。
根據 Codex 的紀錄,首頁的「基本問候」已經改成真正的 HTML button 元素。
而在 JavaScript 裡,則透過這一行把按鈕和功能連接起來:
basicGreetingsButton.addEventListener("click", showBasicGreetingsLesson);
addEventListener() 可以先理解成:告訴程式,要留意某個元素發生了什麼事件。
這裡的 "click" 就是點擊事件。
當使用者點擊「基本問候」按鈕時,程式就會執行 showBasicGreetingsLesson(),進入剛才介紹的課程畫面。
那返回首頁呢?
Codex 另外寫了一個函式:
// 隱藏課程畫面並回到原本首頁。
function showHome() {
lessonView.hidden = true;
homeView.hidden = false;
bottomNav.hidden = false;
}
它和進入課程的邏輯剛好相反。
把課程畫面藏起來,再把首頁和底部導覽顯示出來。
最後,再把返回按鈕接上這個函式:
backButton.addEventListener("click", showHome);
這樣一來,今天規劃的操作流程就完整了。
使用者可以從首頁進入基本問候課程,看完四句教材後,再返回首頁。
而且這次沒有把點擊處理直接寫成 HTML 裡的 inline onclick,而是把事件處理放在 app.js 中。
這也讓我再次看到,HTML 和 JavaScript 可以分開負責不同的事情。
功能完成後,Codex 回報已經進行測試。
根據當時的開發紀錄,測試結果包括:
此外,Codex 也確認 lessons.js 會在 app.js 之前載入,讓處理課程的程式能取得教材資料。
但這次新增的是互動功能,還是需要實際操作,才能確認整個流程是否符合原本的需求。
因此,Codex 的測試完成後,接下來就是我自己的驗收。
我在自己的瀏覽器打開「泰語小旅伴」,從首頁點擊「基本問候」,確認可以進入課程畫面。
接著檢查四句教材是否正常顯示,包括你好、謝謝、對不起和再見。
最後再點擊返回首頁,確認可以順利回到原本的畫面。
這三個操作都正常。
因此,這次不只有 Codex 的測試回報,也有我自己實際操作的驗收結果。
不過,驗收成功的範圍仍然需要分清楚。
目前確認的是基本問候課程可以進入、教材可以顯示,以及返回首頁的功能正常。
這不代表其他四個情境課程已經可以使用,也不代表發音、測驗或學習進度已經完成。
至少第一個課程,終於真的可以操作了。
還記得 Day 7 第一次使用 Git,就是希望能保存已經確認正常的開發階段。
這次完成基本問候課程後,Codex 也建立了第二個 commit:
f8eb6b5
feat: add basic greetings lesson
這個版本包含基本問候的教材和課程互動功能。
也就是說,目前已經有兩個明確的開發階段:
3ddd34e
第一版靜態首頁
↓
f8eb6b5
加入基本問候課程
這也讓昨天建立的版本保存流程,開始真正用在後續功能開發中。
每完成一個範圍明確的功能,經過測試與驗收之後,再保存一個版本,就比較容易知道每一次修改到底增加了什麼。
今天終於讓「泰語小旅伴」從只有靜態首頁,變成有第一個可以操作的課程。
原本只是顯示在首頁上的「基本問候」,現在已經可以點進去,看到四句泰語,再返回首頁。
透過這次實作,我也開始理解 HTML、CSS、JavaScript 和教材資料是怎麼一起工作的。
HTML 提供畫面結構,CSS 負責外觀,lessons.js 保存教材,而 app.js 則讓使用者的操作和教材資料真正連接起來。
這次也第一次透過自己的專案接觸到 createElement()、forEach()、hidden 和 addEventListener() 這些 JavaScript 用法。
雖然我還沒有完全理解所有語法,但至少已經知道這次的程式要完成什麼,以及該怎麼檢查它有沒有正常運作。
和第一次只有一張歡迎畫面的網頁相比,現在的「泰語小旅伴」終於開始有了真正的學習功能。
不過,還有一個很重要的功能沒做。
現在使用者可以看到泰文和羅馬拼音,但如果完全不會泰語,光看文字還是不容易知道該怎麼發音。
能不能讓使用者按一下按鈕,就直接聽到泰語?
下一篇,就來研究瀏覽器的 Web Speech API,讓「泰語小旅伴」第一次真正開口說泰語。