
本文介绍如何通过原生 javascript fetch api 与 php 后端协同,构建一个无需页面刷新、逐题呈现 400+ 道 true/false 问题的单页问卷系统,并将用户答案(1 或 0)按序持久化至文本文件。
本文介绍如何通过原生 javascript fetch api 与 php 后端协同,构建一个无需页面刷新、逐题呈现 400+ 道 true/false 问题的单页问卷系统,并将用户答案(1 或 0)按序持久化至文本文件。
在传统表单提交中,每次点击“True”或“False”都会触发整页刷新,导致体验割裂、状态丢失且无法平滑过渡——尤其面对 400+ 题量时,性能与可用性均受挑战。理想的解决方案是采用 AJAX(推荐现代 fetch)实现前后端异步通信,使前端仅更新问题区域与按钮状态,后端专注处理逻辑与数据落盘。
以下是一个完整、可直接运行的单页应用(SPA)实现,包含清晰的职责划分:
✅ 核心设计要点
-
无刷新交互:所有问题切换均由
fetch()完成,不依赖form.submit(); -
状态驱动流程:前端通过
data-id跟踪当前题号,后端依据action=start|question分阶段响应; -
结构化日志:答案文件(
data.cpx)以q:2,a:0格式记录,含题号与答案,便于后续解析; - 边界安全处理:自动识别最后一题并禁用按钮,防止越界提交;
- 轻量可靠:不依赖 jQuery,纯原生 JS + 基础 PHP,降低维护成本。
? 后端 PHP 处理逻辑(嵌入同一文件,如 CPI_Test.php)
<?php if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action'])) {
ob_clean();
$questionsFile = 'CPXQ.dat';
$answersFile = 'data.cpx';
// 读取全部题目(自动去除换行符)
$lines = file($questionsFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if (!$lines) die('Failed to load questions.');
switch ($_POST['action']) {
case 'start':
header('Content-Type: text/plain');
echo $lines[0] ?? 'No questions available.';
exit;
break;
case 'question':
$id = (int)$_POST['id'];
$answer = (int)$_POST['answer']; // 1 for True, 0 for False
// 保存当前答案(含题号)
$logEntry = sprintf("q:%d,a:%d", $id + 1, $answer) . PHP_EOL;
file_put_contents($answersFile, $logEntry, FILE_APPEND | LOCK_EX);
// 计算下一题索引
$nextId = $id + 1;
if ($nextId < count($lines)) {
// 返回下一题数据(JSON)
header('Content-Type: application/json');
echo json_encode([
'id' => $nextId,
'question' => $lines[$nextId]
]);
} else {
// 最终题已答完
header('Content-Type: application/json');
echo json_encode([
'id' => 0,
'question' => 'End of questionnaire. Thank you!'
]);
}
exit;
break;
}
}
?>
? 前端 HTML + JavaScript(同文件内)
<meta charset="UTF-8"><title>CPI 测验系统</title><style>
body { font-family: "Segoe UI", sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; }
fieldset { background: #f9f9f9; padding: 1.5rem; border-radius: 6px; margin-bottom: 1.5rem; }
button { padding: 0.75rem 2rem; font-size: 1.1rem; margin: 0.5rem; cursor: pointer; }
button:disabled { opacity: 0.6; cursor: not-allowed; }
</style><h1>CPI TEST</h1>
<script>
const fieldset = document.querySelector('fieldset');
const form = document.forms.questions;
const fd = new FormData();
// 加载首题
fd.set('action', 'start');
fetch(location.href, { method: 'POST', body: fd })
.then(r => r.text())
.then(text => fieldset.innerHTML = text.trim());
// 按钮点击处理(事件委托)
form.addEventListener('click', e => {
if (e.target.tagName !== 'BUTTON') return;
const value = e.target.dataset.value;
const id = e.target.dataset.id;
fd.set('action', 'question');
fd.set('id', id);
fd.set('answer', value);
fetch(location.href, { method: 'POST', body: fd })
.then(r => r.json())
.then(data => {
fieldset.innerHTML = data.question;
// 更新所有按钮的 data-id;到达终点则禁用
if (data.id === 0) {
form.querySelectorAll('button').forEach(btn => btn.disabled = true);
} else {
form.querySelectorAll('button').forEach(btn => btn.dataset.id = data.id);
}
})
.catch(err => console.error('Submission failed:', err));
});
</script>⚠️ 注意事项与进阶建议
-
并发安全:当前方案使用共享文本文件,不适用于多用户并发场景。生产环境强烈建议改用数据库(如 MySQL),为每位用户分配唯一
session_id或user_id,并将答案存为(user_id, question_id, answer, timestamp)结构化记录。 -
防回退机制:如需禁止返回上一题,可在 JS 中移除历史记录(
history.replaceState()),或服务端校验id严格递增(拒绝id ≤ 已存最大题号的请求)。 -
错误防护:实际部署前应增加 PHP 错误日志、
file_put_contents权限检查、$lines数组长度校验等健壮性措施。 -
用户体验优化:可添加加载状态(如按钮置灰+文字变为“Submitting…”)、答题计时、进度条(
<progress value="5" max="400"></progress>)等增强交互反馈。
该方案以最小技术栈达成核心目标:流畅、可靠、可扩展。掌握其设计思想后,即可轻松适配更复杂的问卷逻辑(如跳题规则、多选、评分计算等)。











