news 2026/10/17 18:30:59

uniapp考试答题小程序源码解析:跨端开发与实战避坑

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
uniapp考试答题小程序源码解析:跨端开发与实战避坑

简介:这是一款基于uniapp框架开发的多端考试答题小程序,核心定位为学习参考型项目,适合正在学习Vue.js跨平台开发、需要了解答题交互逻辑的初中级前端开发者。程序内置单选、多选、判断三类常见题型,所有题目与答案均预设在静态数据中,未接入后端服务,因此能清晰展示前端如何组织题目数据、渲染选项并处理切换动作。资源包为ZIP格式,大小约1.47MB,目前已有2372人学习。代码涉及页面跳转、数据绑定、组件化开发以及当前题索引维护等关键知识点,配合HBuilderX可直接运行预览;同时,项目没有实现交卷与评分,但这正便于读者将注意力集中在界面交互上,后续可进一步扩展用户登录、后台题库管理、分数计算、答案校验、防作弊与排行榜等真实考试系统所需功能,为从Demo走向完整在线教育应用提供一条可行的演进路径。

1. 考试答题小程序源码:uniapp 跨端方案的底线与上限

期末周前后,总有一批人被“考试答题类小程序”的课程设计或接单需求找上门。市面上的开源项目不少,但真正能跑通的没几个:要么接口全是 Mock,要么数据库脚本一跑就报错,要么交卷逻辑直接把答案写死在前端。这套基于 uniapp 的考试答题小程序源码包,我拆完之后的第一感受是:它把“登录 → 抽题 → 答题 → 倒计时 → 交卷 → 判分 → 错题回顾”整条链路完整打通了,而且编译到微信小程序、App、H5 三端都能跑。适合手上有点 Vue 基础、想直接拿一套代码二次开发的人,也适合正在做课程设计、需要一个能演示完整业务闭环的起点。但要说清楚,它解决的是“有完整可复现的骨架”,不是“开箱即用的成品”。

2. 选型逻辑:为什么 uniapp 适合答题类场景,以及边界在哪

2.1 跨端编译的收益:一套代码覆盖小程序、App 与 H5

答题类小程序的核心业务并不复杂:题目展示、选项交互、计时、交卷、结果统计。这类页面结构高度标准化,没有太多原生依赖,正好是 uniapp 这种跨端框架的舒适区。常见做法是,项目里用 Vue 语法写页面,页面文件由 view、text、button 这类跨端组件构成,编译时由框架自动映射到不同平台的原生组件上去。

收益最明显的是答题卡和题目列表这两块。题目列表用 v-for 渲染,选项用 radio-group 或 checkbox-group,这在微信小程序和 App 端的行为基本一致。代码里不需要写两套,开发和后续改版的时间成本差不多能省一半。比如下面这段就是一个标准的单题渲染结构:

<view class="question-card" v-for="(item, index) in questionList" :key="item.id"> <view class="question-title">{{ index + 1 }}. {{ item.stem }}</view> <radio-group v-model="answerMap[item.id]" @change="onAnswerChange(item.id, $event)"> <label v-for="opt in item.options" :key="opt.key" class="option-item"> <radio :value="opt.key" :checked="answerMap[item.id] === opt.key" /> <text>{{ opt.key }}. {{ opt.text }}</text> </label> </radio-group> </view>

这里 v-model 绑定的 answerMap 是一个对象,key 是题目 ID,value 是用户选的选项值。需要注意,radio-group 的 change 事件在 H5 端返回的 event.detail.value 是字符串,而在部分 App 端可能是数组,所以 onAnswerChange 里建议统一做一次类型规整,再写入 answerMap。这是跨端开发里最常见的隐性差异,后面避坑章节会展开讲。

2.2 条件编译:答题卡、canvas 签名与语音题的差异化处理

uniapp 的跨端不是无条件的。涉及到平台独有 API 或交互差异时,就得用条件编译注释来区分代码块。在答题场景里,最典型的两个差异点是手写签名和语音题。微信小程序里录音需要 wx.getRecorderManager,App 端用的是 plus.audio 或者 uni.getRecorderManager 的兼容封装;canvas 绘制签名在小程序和 H5 上的接口行为也不同。

// #ifdef MP-WEIXIN const recorder = uni.getRecorderManager(); recorder.onStop((res) => { uploadAudio(res.tempFilePath); }); // #endif // #ifdef APP-PLUS // App 端走 HTML5+ 的录音 API,返回路径格式与小程序不同 plus.audio.getRecorder().record({ filename: '_doc/answer_audio/' }, () => { // 录音结束回调 }); // #endif

条件编译的写法本身不难,真正需要把握的是使用边界。我的做法是:业务逻辑抽成公共函数,平台差异尽量收敛到工具类里,页面里少直接写条件编译块。否则项目跑久了会出现满屏是 #ifdef 的情况,编译查错时非常痛苦。这套资源里在音频上传和答题卡横屏适配两块都用了条件编译,结构还算干净,可以当成一个模仿样本。

2.3 项目目录结构:答题业务该怎么分层才不翻车

拆完这套源码,它的目录组织方式比较接近实际开发规范,不是把页面全堆在 pages 下的那种课程设计风格。使用者可以沿用它,也可以按自己的团队习惯调整,但建议把下面几个核心目录保留下来:

目录/文件作用说明
/pages页面层登录、考试列表、答题页、成绩页、错题本
/components通用组件倒计时条、答题卡抽屉、题目卡片
/utils工具函数时间格式化、答案映射、缓存读写
/api接口层统一封装的 uni.request 方法
/store全局状态用户信息、考试进度、答案缓存

这种分层方式的价值在于,交卷逻辑、倒计时这类核心模块跟页面解耦了。比如考试进度恢复,不是写在答题页的 onLoad 里,而是抽成 store 里的一个 action,答题页只负责调用。这样出问题的时候定位快,也方便做单元测试或后续迁移。很多从没维护过跨端项目的人,最容易栽在“所有逻辑都写在页面里”这一条上,等要加防切屏、加断点续答的时候才发现页面已经没法改了。

3. 核心答题链路拆解:从抽题到判分的完整实现

3.1 组卷策略:服务端 SQL 随机抽题还是本地洗牌

考试类项目的组卷方式基本分成两种:题库在服务端时,用 SQL 的随机排序加限制条数来抽题;题库打包在本地时,通过 Fisher-Yates 洗牌算法实现随机并截取前 N 题。这套资源默认接的是服务端题库,SQL 部分按“分类定比例抽题”的思路实现,比单纯随机更适合模拟真实考试场景。

-- 按题型分类,每类随机抽指定数量的题 SELECT * FROM ( SELECT *, ROW_NUMBER() OVER ( PARTITION BY question_type ORDER BY RAND() ) AS rn FROM question_bank WHERE exam_id = #{examId} ) t WHERE t.rn <= #{questionCount}

这个 SQL 的关键在 PARTITION BY question_type:先按题型分区,再在分区内随机排序,最后取每个分区的前 N 条。这样能保证单选题、多选题、判断题各自的比例符合组卷规则,而不是全卷都随机成一种题型。不过要注意,这个写法依赖 MySQL 8.0+ 的窗口函数,如果你用的是 5.7 或更低版本,需要改写用变量实现,或者直接把三类题的抽题 SQL 拆开执行再合并。

3.2 倒计时实现:避免 setInterval 累加产生的漂移

倒计时的常见错误写法是用 setInterval 每秒减 1,这样做在 App 端或小程序切后台时会出大问题:定时器被系统挂起,回到前台时累计偏差可能达到几十秒。正确思路是记录一个服务端下发的考试截止时间戳,每次触发更新时用当前时间减去截止时间计算差值。这样即使定时器跳过了几次回调,界面也能在下一次渲染时自动纠正。

// examStore.js export function startCountdown(deadlineTimestamp) { // deadlineTimestamp 由服务端下发,避免依赖本地时钟校准 clearInterval(this.timer); this.timer = setInterval(() => { const remain = Math.max(0, deadlineTimestamp - Date.now()); this.remainSeconds = Math.floor(remain / 1000); if (this.remainSeconds <= 0) { clearInterval(this.timer); this.submitExam(); // 时间到,自动交卷 } }, 250); // 250ms 刷新一次,渲染层按秒取整,避免秒数跳变 }

这里刷新的间隔设成 250ms 而不是 1000ms,是为了避免网络或主线程卡顿导致界面上的秒数“跳表”。渲染层显示的是 this.remainSeconds 的整数值,而计算基于时间戳差值,所以即使定时器回调被推迟了 200ms,显示也不会错。另一个值得抄的细节是,进入页面时先校验一次 Math.max(0, ...) ,如果剩余时间已经是 0,直接置为 0 并触发交卷,防止卡在“已结束但界面还在倒计时”的中间态。

3.3 答题进度持久化:让意外退出有后悔药

考试小程序最怕的就是答到一半被系统杀掉、或者用户不小心左滑返回,结果进度全丢。单纯把答案存在 Vuex 里是不够的,页面销毁或者小程序进程回收时内存数据直接没了。

实在的做法是:每答完一题,往本地缓存里写一份答题快照,缓存的 key 带上考试 ID 和用户 ID,避免不同用户或不同考试互相覆盖。恢复进度的逻辑在答题页的 onLoad 里读缓存,有值就提示用户“检测到上次未完成的答题记录,是否继续”。

// 每题作答后立即持久化 function saveAnswerSnapshot(examId, questionId, answer) { const key = `exam_snapshot_${examId}_${userId}`; const snapshot = uni.getStorageSync(key) || { answers: {}, updatedAt: 0 }; snapshot.answers[questionId] = answer; snapshot.updatedAt = Date.now(); uni.setStorageSync(key, snapshot); // 只保留最近一份快照,避免垃圾数据堆积 }

这个方案的取舍点是性能。写入是同步的,每题都写一次对大部分低端机会造成轻微卡顿,所以我一般会加一个 3 秒或 5 题的节流:先更新内存,再用 setTimeout 延迟批量写盘。这套资源里用的是“每题同步写”,逻辑更简单,稳定优先,实际体验也可以接受。真要接高并发考试场景再改节流策略。另外,交卷成功后务必调用 uni.removeStorageSync 清掉这个 key,否则下次可能把已交卷的答案又恢复出来。

4. 服务端对接与数据模型:从 Mock 到真实接口的切换路径

4.1 接口结构设计与统一请求封装

这套资源前后端分离,后端接口默认返回统一结构{ code, message, data },前端所有请求都通过一个封装好的 request 方法发出。这个设计的价值很明显:判分逻辑全部在服务端做,前端不接触正确答案,避免有人通过 debugger 改内存直接把自己改成满分。

// utils/request.js 核心封装 const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + uni.getStorageSync('token') }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data); // 只把业务数据抛出 } else if (res.data.code === 401) { // token 过期,跳登录页重新鉴权 uni.reLaunch({ url: '/pages/login/index' }); } else { reject(new Error(res.data.message)); } }, fail: (err) => reject(err) }); }); };

参数说明:BASE_URL 是环境变量,建议分 dev、prod 两套配置,真机预览时不要用 localhost,要填局域网 IP 或线上域名;token 在 header 里统一带,后端通过它识别用户身份。401 的处理避开了“考试中途登录态过期但用户没知觉”的情况。注意 uni.request 在部分平台并不支持同步返回,所以这里用 Promise 包一层,调用方统一走 async/await 风格。

4.2 登录鉴权与考试防作弊的边界

考试类项目的登录不能只做微信的 wx.login,因为答题往往要在 App 和 H5 上都跑。通用的做法是提供一个账号密码登录接口,后端签一个 token 出来,有效期设为考试时长加一些余量。但这里有个隐蔽的坑:如果 token 有效期设太长,用户考试结束后还能通过这个 token 拉取答案接口;设太短,考试中途过期,交卷请求会 401。

// 后端生成 token 时,建议设置两个时间维度 String token = Jwts.builder() .setSubject(userId) .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000)) // 整体有效期 24 小时 .claim("examId", examId) // 关联当前考试 .signWith(secretKey) .compact();

这里有个细节值得强调:token 里带上 examId,服务端交卷接口校验 token 中的 examId 与请求参数里的 examId 是否一致。这样可以防止用户 A 拿自己的 token 去冒用用户 B 的考试场次。答题过程中客户端哪怕把身份证号之类的信息改掉,服务端以 token 解析结果为准,这也是“前端不可信”的典型实践。

4.3 手写签名上传与文件处理:uni.uploadFile 实战

考试场景经常要采集考生签名,表示“本人确认作答”。签名一般通过 canvas 生成一张图片,然后传给后端。这套资源里做的是把 canvas 导出成临时文件路径,再调用 uni.uploadFile 走 multipart 上传。

uni.canvasToTempFilePath({ canvasId: 'signCanvas', success: (res) => { const tmpPath = res.tempFilePath; uni.uploadFile({ url: BASE_URL + '/api/exam/signature', filePath: tmpPath, name: 'file', formData: { examId: examId, userId: userId }, success: (uploadRes) => { const result = JSON.parse(uploadRes.data); // 后端返回 code 200 表示签名上传成功 } }); } });

一个需要留意的地方是 uni.canvasToTempFilePath 在小程序端必须在 canvas 绘制完成后调用,而且不能早于 canvas 的 onReady。常见翻车是 onLoad 里立刻导出,结果 canvas 还没渲染出来,导出的图片是空白的。正确做法是等 canvas 的绘制回调触发了再导出。另外,如果签名 canvas 用的是 CSS 像素做高清适配,导出时要设置 destWidth 和 destHeight 为 canvas 实际尺寸的两倍,否则在部分机型上签名会模糊。

5. 避坑:考试小程序最常见的六个翻车现场

5.1 倒计时到了不交卷

现象:倒计时显示已归零,但交卷请求没有发出,用户还停在答题页。原因:自动交卷逻辑放在了定时器回调里,切后台时定时器被系统冻结,回到前台才恢复执行。解决:交卷动作的触发不要只依赖定时器,进入页面时先检查一次当前时间与截止时间的差值,在 onShow 里也补一次检查,双保险。

5.2 多选答案丢失或格式错乱

现象:用户提交的多选题答案,后台收到的只有一个选项,甚至数据结构不对。原因:checkbox 的 change 事件返回值在部分平台是数组,在部分平台是字符串,直接用数组去请求接口会被 JSON.stringify 成带分号的串,服务端解析失败。解决:统一在提交前做一次标准化处理,把数组转成以逗号拼接的字符串,比如const value = Array.isArray(v) ? v.join(',') : v;,服务端按逗号拆分即可。

5.3 canvas 签名在部分机型白屏

现象:同一套签名代码,在 iPhone 上正常,在部分安卓机型上画不出轨迹。原因:canvas 的接口差异。新版 canvas 2d 接口在小程序端的节点查询方式与旧版不同,老代码里用了 wx.createCanvasContext,在安卓 WebView 渲染时表现不稳定。解决:签名模块整体改用条件编译,小程序端统一走新版 canvas 2d 接口,App 端走 plus.canvas,两套实现互相隔离。

5.4 交卷后进度缓存没清除

现象:用户重新进入同一场考试,页面提示“是否继续上次答题”,点击继续后却显示已交卷的空白答案。原因:交卷接口成功后没有清除本地快照缓存。解决:在交卷成功的 then 分支里,强制性 uni.removeStorageSync 掉对应的 snapshot key,并且这个清除动作要在跳转结果页之前完成。

5.5 题库数据大导致首包膨胀

现象:编译后包体积突破小程序 2MB 主包限制,上传代码直接报错。原因:题库、答案解析、音频素材全放在了主包的静态资源目录里。解决:把题库相关页面和静态资源放进分包,或者将题库数据改为首屏只加载一页,滑动加载后续内容,避免一次性全量下发。小程序分包加载的 limit 是每个分包不能超过 2MB,总包不能超过 20MB,实测数据稍微大点就要拆。

5.6 真机预览请求本地接口失败

现象:开发者工具模拟器里接口都能通,换成手机真机预览就请求超时,或者报 ERR_CONNECTION_REFUSED。原因:真机上 localhost 指向的是手机自身,不是开发电脑。解决:BASE_URL 用电脑的局域网 IP,例如 http://192.168.x.x:8080,同时后端服务要监听在 0.0.0.0 而不是默认的 127.0.0.1,并且关闭防火墙或放行对应端口。这个坑在跨端项目里几乎必踩,配置好一次后面能省很多时间。

6. 进阶用法:考试模式下的代码保护与弱网容错优化

6.1 编译产物代码混淆与防篡改

uniapp 编译到小程序端后,JS 代码默认是可读的明文,懂点技术的人用开发者工具打开就能看到答题逻辑,甚至可能定位到判分接口的调用位置。虽然判分逻辑在服务端,但前端仍然暴露了接口路径和参数结构,给了别人刷接口的入口。常见的做法是给编译后的 JS 做一层混淆,插件市场里有不少 uniapp 专用的混淆方案,代价是热更新和小程序审核时的代码还原难度增加。我一般会对包含接口路径和 token 处理的文件单独开混淆,页面模板代码保持原样,这样排查问题时不至于整包都看不了。

# 以常见的第三方混淆插件为例,构建命令大致如下 npm run build:mp-weixin -- --mode production # 产物在 dist/build/mp-weixin 下,配合混淆插件生成 npm run obfuscator:mp

注意一点:混淆不是万能的,小程序端的 JS 无论如何都跑在用户的设备上,拿到产物的人只是多花时间而已。真正要紧的防线是把判分、查答案、统计这些都搬到服务端,接口加频率限制和签名校验。前端混淆只是提高门槛,不是绝对安全。

6.2 弱网环境下的交卷补偿机制

实测中很常见的场景是:用户答题答到最后,突然 Wi-Fi 断了或者信号变差,点击交卷后 uni.request 直接走 fail 回调,页面一直转圈,用户以为交卷失败,又点了一次交卷。服务端这时可能收到了重复交卷请求,如果没做幂等处理,成绩单就会重复计算。

给这套资源补充一个队列机制是值得的。做法是:交卷请求发出后,如果失败,把请求体完整存入本地缓存,同时启动一个重试定时器,每 30 秒重试一次,直到成功或用户手动取消。服务端交卷接口用 examId + userId 作为唯一索引做幂等,同一个考试同一用户只允许生成一份成绩记录,后到的重复请求直接返回已有结果。

const pendingSubmit = uni.getStorageSync('pending_exam_submit'); if (pendingSubmit && !pendingSubmit.done) { retrySubmit(pendingSubmit); } else if (pendingSubmit && pendingSubmit.done) { uni.removeStorageSync('pending_exam_submit'); }

这个机制很朴素,但成功率提升是实打实的。有一次我给某跨平台系统调试线上环境,用户反馈“交卷困难”,加了这套队列之后,一个月内再没出现过成绩丢失的情况。从那以后我每次做考试类项目,都会强制走一遍“失败重试 + 服务端幂等”这个组合。如果这套 uniapp 源码骨架你拿去做实际部署,这两处优化是最建议先改的,比折腾 UI 样式回报高得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 18:30:03

基于PaddleHub的实时皮影戏交互系统:姿态估计与物理模拟实战

1. 项目缘起与整体设计思路皮影戏这门老手艺&#xff0c;我小时候在庙会上见过几回&#xff0c;一块白布、几根竹签、一盏油灯&#xff0c;艺人手指翻飞之间&#xff0c;孙悟空就能跟白骨精打得天昏地暗。后来一直想&#xff0c;能不能用代码把这套东西复刻出来——不是做那种冷…

作者头像 李华
网站建设 2026/10/17 18:27:19

AI辅助开发登录界面:从需求拆解到前后端联调全流程

前一阵折腾了几个小项目&#xff0c;发现用AI辅助开发这件事&#xff0c;真正难的不是写代码&#xff0c;而是怎么把一个模糊的需求描述成机器能理解的任务。这个系列写到第三篇&#xff0c;正好拿“登录界面”这个看似简单、实际坑不少的场景来聊聊。如果你正准备用AI帮忙搭一…

作者头像 李华
网站建设 2026/10/17 18:21:23

2020A题炉温曲线:热传导模型、参数反演与传送带速度优化

简介&#xff1a;这份资源是2020年全国大学生数学建模竞赛A题「炉温曲线」的讲评资料&#xff0c;面向备战数模竞赛的高校学生及指导教师&#xff0c;帮助读者系统理解该赛题的建模思路与求解方法。压缩包内仅含1个PDF文件&#xff0c;约26.94MB&#xff0c;内容围绕赛题展开完…

作者头像 李华
网站建设 2026/10/17 18:16:40

汇编优化实战:表驱动选择、条件伪指令与移位指令全解析

第一次写带功能菜单的程序时&#xff0c;我老老实实把每个按键值都判断一遍&#xff1a;CMP AL, 0、JE&#xff0c;然后CMP AL, 1、JE……也就五六个分支&#xff0c;写完连我自己都看不下去。旁边带我的前辈扫了一眼代码&#xff0c;只说了一句&#xff1a;“别让 CPU 一条一条…

作者头像 李华