Web-Dev-For-Beginners 打字游戏教程:基于 addEventListener 的浏览器事件驱动编程完整实践
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
本文以 Web-Dev-For-Beginners 课程中的「使用事件建立游戏」章节为主体,完整讲解事件驱动编程(Event-Driven Programming)的核心机制,并带你从零实现一个会实时响应每一次按键的打字速度游戏:理解事件监听者如何通过addEventListener注册与触发,掌握click、input等常见浏览器事件的处理方式,最终得到一个可用index.html、script.js、style.css三个文件搭建、可独立运行并可继续扩展的实战项目。
一、为什么浏览器程序必须"事件驱动"
当你为浏览器构建应用时,你提供的是一个图形用户界面(GUI),用户在按钮、文本框等对象上点击或输入。这里有一个程序式编程无法回避的问题:开发者无法预知用户何时会对这些对象产生互动——用户可能立刻点击开始按钮,也可能五分钟后才动手,甚至永远不会点击。
事件驱动编程正是解决这一问题的范式。拆开这个名词,主轴是事件(Event),即"将要发生的事"。它有效解决了上述不确定性:我们知道当用户产生互动时,哪些代码必须做出回应,唯一不知道的是用户何时互动。
回顾程序式编程:函数依照顺序一行一行地被运行。事件驱动编程同样由函数组成,差别在于如何去呼叫这些函数——不是按书写顺序执行,而是"等待某个事件发生后再执行"。
事件监听者的注册方式
要处理点击按钮、输入文字这类事件,需要注册事件监听者(Event Listeners)。事件监听者是一种函数,负责在事件触发时提供相应回应:更新用户界面、呼叫伺服器,或做任何你希望它做的事。最通用的注册 API 是addEventListener,它接收两个参数:事件名称与要执行的函数。
值得注意,建立事件监听者有许多方式:
- 使用匿名函数(arrow function)或有名字的命名函数;
- 使用快捷方式,如直接设定
element.onclick = handler这类事件属性; - 使用
addEventListener,事件名以参数传入,因此对任何事件都适用,是弹性最高的方案。
本课程聚焦addEventListener加匿名函数这一最常见、弹性最高的组合。
常见事件
浏览器提供了数十种可监听的事件——基本上用户在网页上做的任何行为都会触发事件,你只需要处理其中一小部分。本课程游戏用到的核心事件是click与input,其他常见事件包括:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
click | 使用者點擊物件(通常是按鈕或連結) | 開始遊戲、提交表單 |
contextmenu | 使用者點擊滑鼠右鍵 | 自訂右鍵選單 |
select | 使用者標記特定文字 | 文字編輯、複製操作 |
input | 使用者在輸入框中輸入文字 | 即時驗證、即時搜尋 |
二、游戏设计目标与流程
游戏本身测试玩家的打字技巧——一项常被程序员忽视却极其重要的技能。大致流程如下:
- 玩家点击「开始」按钮,游戏随机产生一行引文(本游戏使用夏洛克·福尔摩斯的经典语录);
- 玩家尽快地将文字输入文字框:
- 当单词输入完毕时,立即高亮标记下一个单词;
- 当玩家打错字时,文字框转为红色;
- 当玩家完成整段引文输入时,显示祝贺语与花费的时间。
文件结构
整个游戏只需三个文件:index.html(页面结构)、script.js(游戏逻辑)、style.css(视觉样式)。这是驱动绝大多数网页的"经典三件套"。
建立项目目录并进入:
# Linux 或 macOS mkdir typing-game && cd typing-game # Windows md typing-game && cd typing-game用 Visual Studio Code 打开当前目录:
code .然后在编辑器中新增三个文件:index.html、script.js、style.css。
三、建立使用者介面(HTML)
回顾需求,HTML 页面上需要四个"食材":呈现待输入引文的位置、呈现状态讯息(如祝贺文)的位置、玩家输入用的文字框、以及一个开始按钮。每个物件都需要唯一的id,让 JavaScript 能控制它们;同时在 HTML 中引入 CSS 与 JavaScript 文件。
在index.html中加入以下代码:
<!-- inside index.html --> <html> <head> <title>Typing game</title> <link rel="stylesheet" href="style.css"> </head> <body> <h1>Typing game!</h1> <p>Practice your typing skills with a quote from Sherlock Holmes. Click **start** to begin!</p> <p id="quote"></p> <!-- This will display our quote --> <p id="message"></p> <!-- This will display any status messages --> <div> <input type="text" aria-label="current word" id="typed-value" /> <!-- The textbox for typing --> <button type="button" id="start">Start</button> <!-- To start the game --> </div> <script src="script.js"></script> </body> </html>结构要点:#quote与#message是两个占位段落,内容由 JavaScript 动态填充;#typed-value输入框带有aria-label="current word"无障碍标签,供屏幕阅读器识别其用途;#start按钮是唯一的交互触发点;<script>放在</body>前,确保脚本执行时 DOM 元素已经存在。
执行应用程序
最好的开发模式是定期确认程式结果。Visual Studio Code 有一个常用扩充套件 Live Server,它会在你储存网页档案时同时架设本地服务器并自动刷新浏览器:
- 安装 Live Server 扩充套件后重啟 Visual Studio Code;
- 按下
Ctrl-Shift-P(macOS 为Cmd-Shift-P)開啟指令視窗; - 輸入Live Server: Open with Live Server,它会架設并发布你的网页;
- 開啟瀏覽器前往
https://localhost:5500,即可看到刚建立的页面。
四、加入 CSS:两种状态样式
游戏需要两件事在视觉上一目了然:玩家需要输入哪个单词、以及是否输错了。用两组 class 即可完成:
在style.css中加入:
/* 在 style.css 中 */ .highlight { background-color: yellow; } .error { background-color: lightcoral; border: red; }.highlight:黄色背景标记"当前要输入的单词",引导玩家视线;.error:珊瑚红背景加红色边框,打错字时立即反馈,且不打断输入流。
处理 CSS 时可以按自己的喜好规划版面:更换字型、改变标题颜色、调整物件大小,都不会影响后续 JavaScript 逻辑,因为脚本只依赖id与这两个 class 名。仓库中的 范例解答样式 在同样两个 class 的基础上补充了全局重置、聚焦光晕与响应式布局,可以作为美化参考。
五、JavaScript:游戏逻辑的实现
5.1 建立常數
同样以"食材清单"的方式,程式需要:
- 储存所有引文的阵列(陣列);
- 储存单一引文所有单词的空阵列;
- 追踪玩家当前正在输入单词的索引变数;
- 记录玩家点击开始时点的变数;
- 以及使用者介面三个元素的连结:文字框(
typed-value)、引文显示(quote)、讯息栏(message)。
// 在檔案 script.js 中 // 所有的引文內容 const quotes = [ 'When you have eliminated the impossible, whatever remains, however improbable, must be the truth.', 'There is nothing more deceptive than an obvious fact.', 'I ought to know by this time that when a fact appears to be opposed to a long train of deductions it invariably proves to be capable of bearing some other interpretation.', 'I never make exceptions. An exception disproves the rule.', 'What one man can invent another can discover.', 'Nothing clears up a case so much as stating it to another person.', 'Education never ends, Watson. It is a series of lessons, with the greatest for the last.', ]; // 儲存單字列表及目前要輸入的單字索引 let words = []; let wordIndex = 0; // 開始時間 let startTime = Date.now(); // 網頁物件連結 const quoteElement = document.getElementById('quote'); const messageElement = document.getElementById('message'); const typedValueElement = document.getElementById('typed-value');关键取舍:引文清单在游戏过程中不会变化,用const;words、wordIndex、startTime会随每局游戏重置,用let。至于 DOM 元素,虽然随时可以用document.getElementById取得,但把它们存为常量可以避免字符串拼写错误、提高可读性,也让后续引用一目了然。✅ 建议顺手往quotes里多加几句自己喜爱的引文。
5.2 事件监听者一:开始逻辑
玩家点击start按钮时,我们需要:挑选一段引文、设定使用者介面、追踪当前单词与时间。由于不知道玩家何时点击,所以必须用事件监听者。把以下代码追加到script.js末端:
// 在 script.js 末端 document.getElementById('start').addEventListener('click', () => { // 取得一行引文 const quoteIndex = Math.floor(Math.random() * quotes.length); const quote = quotes[quoteIndex]; // 將引文分成許多單字,存在矩陣中 words = quote.split(' '); // 重制單字索引來做追蹤 wordIndex = 0; // 更新使用者介面 // 建立 span 元素的矩陣,設定 class 用。 const spanWords = words.map(function(word) { return `<span>${word} </span>`}); // 轉換成字串並以 innerHTML 顯示引文 quoteElement.innerHTML = spanWords.join(''); // 標記第一個單字 quoteElement.childNodes[0].className = 'highlight'; // 清除訊息欄之前的訊息 messageElement.innerText = ''; // 設定文字框 // 清除文字框 typedValueElement.value = ''; // 設定 focus typedValueElement.focus(); // 設定事件驅動程式 // 開始計時器 startTime = new Date().getTime(); });逐段拆解:
单字追踪设定
Math.floor(Math.random() * quotes.length):Math.random()产生[0, 1)的随机小数,乘以引文数量再向下取整,得到合法的随机下标,实现"随机挑一行引文";quote.split(' ')把引文按空格切成单词阵列,存入words,后续用它逐词比对;wordIndex = 0:玩家总是从第一个单词开始输入。
使用者介面设定
words.map(...)生成spanWords:每个单词被<span>...</span>包裹,这是后面能"逐个单词"切换 class 的前提;spanWords.join('')拼成一个字符串后写入quoteElement.innerHTML,一次 DOM 更新即呈现整段引文;quoteElement.childNodes[0].className = 'highlight'高亮第一个单词;messageElement.innerText = ''清除上一局的祝贺/状态讯息。
文字框与计时
- 清空
typedValueElement.value,再调用typedValueElement.focus(),让玩家点击开始后无需再点一次输入框就能直接打字; startTime = new Date().getTime()记下当前毫秒时间戳,作为计算用时的基准。
5.3 事件监听者二:打字逻辑
玩家每次按键,input事件都会被触发。对应的监听者需要检查玩家是否输对了单词、监控整体游戏状况。追加到script.js最末端:
// script.js 最末端 typedValueElement.addEventListener('input', () => { // 取得目前的單字 const currentWord = words[wordIndex]; // 取得目前輸入的數值 const typedValue = typedValueElement.value; if (typedValue === currentWord && wordIndex === words.length - 1) { // 句子最末端 // 顯示成功 const elapsedTime = new Date().getTime() - startTime; const message = `CONGRATULATIONS! You finished in ${elapsedTime / 1000} seconds.`; messageElement.innerText = message; } else if (typedValue.endsWith(' ') && typedValue.trim() === currentWord) { // 單字最末端 // 清除輸入的數值,準備給新的單字使用 typedValueElement.value = ''; // 移動到下一個單字 wordIndex++; // 重設所有引文子元素的 class 名稱 for (const wordElement of quoteElement.childNodes) { wordElement.className = ''; } // 標記新單字 quoteElement.childNodes[wordIndex].className = 'highlight'; } else if (currentWord.startsWith(typedValue)) { // 單字目前輸入正確 // 標記下一個單字 typedValueElement.className = ''; } else { // 單字輸入錯誤 typedValueElement.className = 'error'; } });这段代码是一个"从最具体到最一般"的级联判断(waterfall),四个分支各管一种状态:
分支一:整段引文完成。条件是typedValue === currentWord且wordIndex === words.length - 1——注意必须同时满足两个条件,否则中间任何一个单词打对都会被误判为整句完成。完成后用当前时间减去startTime得到毫秒级用时,除以 1000 转为秒,拼进祝贺讯息写入messageElement。
分支二:当前单词完成。以空格为界:typedValue.endsWith(' ')且trim()后与currentWord相等,说明玩家刚打完一个单词并按了空格。此时清空输入框、wordIndex++推进到下一词,先把quoteElement所有子元素的className重置为''(恢复默认呈现),再把新词的 class 设为highlight。
分支三:输入正确但未完成。currentWord.startsWith(typedValue)为真表示已输入部分是目标单词的前缀,此时清除className,让输入框回到默认样式(从"错误红"恢复)。
分支四:输入错误。以上皆不成立,说明已经打错,把className设为error,触发珊瑚红背景——反馈即时且不打断输入。
六、测试你的应用程序
点击start按钮、开始输入单词,效果应与上文开头的演示动画一致。测试清单:随机引文是否正常显示、当前单词高亮是否随输入推进、打错字时输入框是否变红、打完单词后是否自动清空并前进、整句完成后是否显示耗时祝贺文。
遇到错误是常态——所有的开发者都会面对错误,查看程式讯息并侦错本身就是练习的一部分。常用排查手段:按 F12 打开浏览器 DevTools 的 Console 查看 JavaScript 报错、确认三个文件名与引入路径完全一致(区分大小写)、确认 Live Server 正在运行并正确刷新。
源码级印证:仓库 solution 版本的工程化改进
仓库提供了一个经过重构的解答实现 solution/index.js,对照学习可以看出课程代码向生产级写法演进的典型方向:
- 命名函数取代匿名函数:开始逻辑与打字逻辑被抽成
startGame与handleTyping两个命名函数,注册时只需startButton.addEventListener("click", startGame)(见 index.js 第90-91行),可读性更好,也便于后续"移除/重新注册监听者"这类挑战; querySelector取代getElementById:DOM 引用统一为document.querySelector("#quote")等写法(见 第20-23行);classList取代直接赋值className:高亮切换收敛为一个highlightWord工具函数,用el.classList.toggle("highlight", i === index)一行完成"重置所有 + 高亮当前"(见 第44-48行),错误状态则用classList.add("error")/classList.remove("error"),避免了直接覆盖className可能误伤其它 class 的问题;- 完成即禁用输入框:游戏结束时执行
typedValueElement.disabled = true(见 第69-73行),直接实现了课程挑战中"完成引文时关闭文字框"这一条; - 集中管理文案:祝贺、错误、开始提示收敛为
messages物件(见 第26-30行),成功讯息用seconds.toFixed(2)保留两位小数,比课程版的整数秒更精细。
配套的 solution/index.html 还演示了更完整的无障碍做法:用<label for="typed-value" class="hidden-label">配合视觉隐藏样式,替代单纯的aria-label;solution/index.css 则把.highlight/.error两个核心 class 保留不变,在其外围补充了聚焦态、阴影与响应式断点——验证了"逻辑只依赖 id 与 class 名"这一设计的好处:样式可以任意重做而不触碰脚本。
七、挑战:继续扩展游戏
课程列出的进阶挑战(solution 版本已替你实现了其中一部分,可对照源码学习实现方式):
- 在完成游戏时关闭
input事件监听者(removeEventListener),重新开始时再开启——这需要把监听函数存为具名引用才能移除; - 当玩家完成引文时关闭文字框(设定
disabled); - 以**对话窗格(dialog/modal)**的方式显示祝贺讯息;
- 利用localStorage储存最高分:用
setItem/getItem持久化最佳成绩,刷新页面后依然可读。
此外,英文原版课程还布置了一个 AI Agent 挑战:为游戏实现"难度系统"——追踪 WPM 与正确率、按表现自动切换 Easy/Medium/Hard 三档引文池、在 UI 上显示当前难度与统计、连击 3 次后升档,并配以颜色/动画反馈。
八、复习与自學
- 浏览浏览器为开发者提供的所有事件类型文档,逐一思考每种事件适用的实际场景——你只会用到其中一小撮,但要能说出"为什么用
input而不用change"这类取舍; - 完成课程附带的课前/课后测验,巩固事件驱动的概念模型;
- 本课的正式作业是 建立一款新的鍵盤遊戲:基于键盘事件开发一款小游戏,评分标准以"呈现完整的遊戲內容(優良)/ 遊戲內容單調(普通)/ 遊戲出現問題(待改進)"三档衡量。
小结:本教程完整走通了"GUI 不确定性 → 事件监听者 →click启动游戏 →input实时判定 → 状态样式反馈 → 计时结算"的事件驱动闭环。addEventListener加匿名函数、split/startsWith做逐词校验、className切换做视觉反馈、时间戳差值做计时,这四个模式几乎可以直接迁移到你日后写的任何交互式网页。相关文档可回溯至 繁體中文課程原文、英文課程原文 与 章節總覽。
【免费下载链接】Web-Dev-For-Beginners24 Lessons, 12 Weeks, Get Started as a Web Developer项目地址: https://gitcode.com/GitHub_Trending/we/Web-Dev-For-Beginners
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考