学习前提
理解数组、对象状态、事件监听和条件渲染。
本课完成后你能做什么
完成选择、判题、解释、下一题和重做流程,确保每题只计分一次。
先理解机制
把应用看成几个明确状态:待回答、已回答、全部完成。index指向当前题,score累计正确数,answered阻止同题重复计分。数据中存题干、选项、答案索引和解释,界面根据数据渲染。
用fieldset与legend把单选项组成一题,required提供未选时的原生提示。提交成功后锁定本题选项、显示解释,再提供下一步。只禁用按钮还不够,处理器也检查answered,确保逻辑本身维护约束。
重绘时明确重置临时状态:answered、反馈、下一步可见性和按钮disabled。完成最后一题后隐藏表单并显示总分。重做重置index与score,不能只把题目换回第一题,否则会继承旧分数。
键盘体验属于流程的一部分:判题后焦点移到下一步,新题出现后移到第一选项,结束后到重做按钮。示例答案在前端公开,适合自测,不适合需要防作弊的正式考试。
可运行示例
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Web quiz</title><style>body{max-width:650px;margin:auto;padding:24px;font:18px/1.6 system-ui}label{display:block;padding:8px}button{font:inherit;margin:8px 4px}fieldset{min-width:0}</style>
<main><h1>Web foundations quiz</h1><p id="progress"></p><form id="quiz"><fieldset><legend id="question"></legend><div id="choices"></div></fieldset><button id="submit">Check answer</button></form><p id="feedback" role="status"></p><button id="next" hidden>Next question</button><button id="restart">Restart</button></main>
<script>
const questions=[{text:'Which element performs an action?',options:['button','a','span'],answer:0,why:'A native button supports keyboard activation.'},{text:'Which property allows flex items to wrap?',options:['position','flex-wrap','opacity'],answer:1,why:'flex-wrap permits additional flex lines.'},{text:'Which method inserts plain text?',options:['innerHTML','eval','textContent'],answer:2,why:'textContent treats input as literal text.'}];
let index=0,score=0,answered=false;
const form=document.querySelector('#quiz'),choices=document.querySelector('#choices'),feedback=document.querySelector('#feedback'),next=document.querySelector('#next'),submit=document.querySelector('#submit');
function render(){
answered=false;feedback.textContent='';next.hidden=true;form.hidden=false;submit.disabled=false;
const q=questions[index];document.querySelector('#progress').textContent=`Question ${index+1} of ${questions.length}`;document.querySelector('#question').textContent=q.text;choices.replaceChildren();
q.options.forEach((text,i)=>{const label=document.createElement('label'),input=document.createElement('input');input.type='radio';input.name='answer';input.value=String(i);input.required=true;label.append(input,document.createTextNode(text));choices.append(label);});
}
form.addEventListener('submit',event=>{
event.preventDefault();if(answered)return;
const selected=form.querySelector('input:checked');if(!selected)return;
answered=true;const q=questions[index],correct=Number(selected.value)===q.answer;if(correct)score++;
feedback.textContent=(correct?'Correct. ':'Try reviewing this idea. ')+q.why;
form.querySelectorAll('input').forEach(x=>x.disabled=true);submit.disabled=true;next.hidden=false;next.textContent=index===questions.length-1?'See result':'Next question';next.focus();
});
next.addEventListener('click',()=>{index++;if(index===questions.length){form.hidden=true;next.hidden=true;feedback.textContent=`Completed: ${score} / ${questions.length}`;document.querySelector('#restart').focus();}else{render();choices.querySelector('input').focus();}});
document.querySelector('#restart').addEventListener('click',()=>{index=0;score=0;render();choices.querySelector('input').focus();});render();
</script></html>
推演与验证
不选答案提交应被拦住;连续触发提交不得重复加分;分别跑全对、全错和混合答案;最终分数应在0至3之间。答到第二题时重做,第一题应恢复未答且总分归零。全程只用键盘再走一遍。
常见错误与边界
题目数量为0或答案索引越界需要导入校验,本例使用已知常量数据。不要随机打乱选项后仍用旧答案索引;若增加乱序,需把正确答案绑定稳定id。
动手练习
增加错题回顾,记录题目id、所选项和正确项;回顾页应解释原因,而不是只显示红叉。
参考解法与理由
在首次提交时追加记录,结束时从记录派生错题列表。重新开始清空记录。用textContent生成回顾文字,并保证同题重复事件不产生两份记录。