微信小程序考研题库源码实战:导入配置、答题逻辑与接口对接

📅 发布时间:2026/9/16 0:50:27
微信小程序考研题库源码实战:导入配置、答题逻辑与接口对接
简介这是一份面向考研复习场景的微信小程序前后端完整源码项目适合想学习小程序开发及教育类应用设计的初学者或初中级开发者。项目涵盖题库管理、在线答题、学习统计、错题集与分享互动等模块从 WXML/WXSS 界面搭建到 wx.request 网络通信、本地缓存与生命周期处理均有体现同时附带基于 Java、Vue 的配套管理后台并包含 SQL 初始化脚本与一键安装、运行、构建的 bat 脚本便于整体理解业务链路并快速启动工程。资源包共 1242 个文件、约 14.99 MB主要包括 png、js、vue、java、json、wxml、wxss 等类型其中 wxml/wxss 负责小程序视图与样式vue/java 支撑管理端与服务端逻辑json 用于配置与数据交换png 等提供界面资源。已有 513 人学习下载通过学习这份源码可以掌握小程序项目从页面开发、交互实现到后台对接的完整流程也能了解题库系统的模块设计与教育内容产品化的思路是练手与二次开发的实用参考。1. 微信小程序考研题库源码.zip拿到手先分清页面和接口从网上下载一个“考研知识题库小程序源码.zip”解压后通常是一整个可以导入微信开发者工具的小程序工程而不是某个独立的插件。这个工程的核心价值不是里面那几百道题而是它把题库类小程序最常见的页面结构做出来了首页有科目分类答题页有单选、判断和多选交卷后能看到正确率和错题列表。数据可能写死在utils/questions.js里也可能预留了云开发或后端接口。不管是做毕业设计、课程设计还是第一次接触微信小程序的前端工程师都能从这个项目里摸到页面栈、组件事件、本地缓存和生命周期之间的关系。拿到压缩包后第一步不是逐行读源码而是先分清这套代码用的是静态数据还是动态接口因为这会直接决定后面导入、编译和真机调试时要做什么配置。2. 导入微信开发者工具前appid、编译模式与顶部导航栏高度三个参数下载源码后最常见的编译失败根因往往不在业务代码而在工程的三个基础配置里。第一个是project.config.json中的 appid第二个是编译模式对应的启动页面第三个是自定义导航栏的高度计算。这三个地方不处理后面的页面逻辑再干净也跑不起来。2.1 project.config.json 里的 appid 不换后续“不校验合法域名”也会失效工程根目录的project.config.json一般长这样{ appid: touristappid, projectname: kaoyan-tiku, compileType: miniprogram, setting: { urlCheck: true, es6: true, postcss: true, minified: true } }appid字段决定这个项目绑定到哪个小程序账号。源码包里常见三种值别人的真实 appid、touristappid、空字符串。其中touristappid是微信开发者工具的游客模式能打开模拟器但不能调用wx.login、wx.cloud和订阅消息这类需要正式身份的接口。所以如果只准备本地预览题库游客模式够用一旦想保存学习记录、上传体验版就必须换成自己的 AppID。改 appid 的路径有两条一是在开发者工具右上角“详情 - 基本信息”里修改工具会同步写回project.config.json二是直接用编辑器改文件再点工具栏的“重新编译”。要注意的是改完 appid 后setting.urlCheck这个参数会和正式账号联动。urlCheck为 true 时wx.request请求的域名必须在小程序后台配置过源码里写的接口地址大概率是作者自己的测试域名直接用会出现url not in domain list。开发阶段可以把urlCheck临时改为 false或者在开发者工具里勾选“不校验合法域名”。这个勾选只对模拟器和真机调试生效真机预览正式版时照样会被拦。另一个跟导入相关的字段是miniprogramRoot。很多 zip 解压后会多一层外层目录开发者工具导入时如果选到外层会报“找不到 app.json”。正确做法是选到包含app.json的那一层如果app.json就在根目录miniprogramRoot可以留空或不写。2.2 自定义导航栏高度用 wx.getMenuButtonBoundingClientRect 算胶囊位置不少源码为了视觉效果把app.json里的窗口配置写成{ window: { navigationStyle: custom } }此时系统不再渲染顶部导航栏页面顶部直接顶到状态栏。微信小程序右上角的胶囊按钮仍然存在它的位置由微信客户端控制页面代码只能读取不能修改。要让自定义导航栏不遮挡按钮需要拿到两个数值状态栏高度和胶囊按钮的布局信息。公共方法通常放在utils/nav.js// utils/nav.js function getNavMetrics() { const menu wx.getMenuButtonBoundingClientRect() const windowInfo wx.getWindowInfo ? wx.getWindowInfo() : wx.getSystemInfoSync() const statusBarHeight windowInfo.statusBarHeight || 20 const navBarHeight (menu.top - statusBarHeight) * 2 menu.height return { statusBarHeight, navBarHeight, capsuleTop: menu.top, capsuleHeight: menu.height } } module.exports { getNavMetrics }计算逻辑是menu.top是胶囊顶部到屏幕顶部的距离减去statusBarHeight得到胶囊与状态栏底部之间的间隔。导航栏总高度由“状态栏高度 一个间隔 胶囊高度 一个间隔”组成所以用(menu.top - statusBarHeight) * 2 menu.height。如果直接把menu.top menu.height当作导航栏高度标题会偏上胶囊下方会空出一大块。拿到数值后用于页面 wxmlview classnav-bar stylepadding-top: {{statusBarHeight}}px; height: {{navBarHeight}}px; view classnav-title考研题库/view /view注意statusBarHeight单位是 pxstyle里不能再乘rpx。另外wx.getMenuButtonBoundingClientRect在模拟器里返回的是模拟数值真机上不同机型差异明显所以必须动态计算不能把写死的 44px 当作通用值。三个必调参数的关系如下参数来源含义statusBarHeightwx.getWindowInfo()状态栏高度menu.topwx.getMenuButtonBoundingClientRect()胶囊顶部到屏幕顶部距离navBarHeight由前两者计算自定义导航栏总高度自定义导航栏还会丢失系统自带的返回箭头。如果题库页面是从首页进入的需要在这个导航栏里自己放一个返回按钮用wx.navigateBack处理否则用户会一直停留在当前页。2.3 编译模式里启动页面写错会看到白屏而不是题库app.json的 pages 数组决定页面路径{ pages: [ pages/exam/index, pages/wrong/index, pages/mine/index ] }开发者工具默认编译 pages 数组的第一项。如果源码作者没有把题库首页放在第一项而是先放了一个登录页打开后见到的就不是刷题界面。遇到这种情况先不要怀疑源码有 bug在工具栏的“普通编译”下拉框里添加编译模式把“启动页面”改成pages/exam/index再重新编译。另一种更隐蔽的情况是下载包外层还有一层带日期格式的文件夹开发者工具记住了旧编译模式但新工程里对应路径已经不存在于是报 app.json 页面路径错误。清掉project.private.config.json或者直接删除编译模式即可。这个文件是个人项目配置里面保存了最近使用的编译条件提交源码时通常可以忽略但会干扰别人打开工程。3. 答题状态管理setData 更新、radio-group 单选框和 user_data_path 持久化答题页面是题库小程序的核心。这里主要说三件事题目数据的组织方式、单选题组件的绑定写法、错题本的文件持久化。3.1 把题目数据拆成静态 questions 和运行态 selected避免直接改原数组推荐把题目放进独立的utils/questions.js导出数组module.exports [ { id: 1, subject: 政治, type: single, question: 划分历史唯物主义和历史唯心主义的标准是, options: [ 对思维与存在关系问题的不同回答, 对社会存在与社会意识关系问题的不同回答, 对世界是否可知问题的不同回答, 对世界是否运动变化问题的不同回答 ], answerIndex: 1, explain: 历史唯物主义认为社会存在决定社会意识。 } ]把正确答案统一记为answerIndex而不是保存答案文本这样判分时只需比较下标。答题页 data 只维护当前题号和用户本次的选择结果Page({ data: { currentIndex: 0, currentQ: null, selectedAnswer: -1, showExplain: false }, onLoad() { const questions require(../../utils/questions.js) this.questions questions this.setData({ currentQ: questions[0] || null }) } })selectedAnswer初始化为 -1表示用户尚未选择。切换下一题时把selectedAnswer重置为 -1、showExplain重置为 false这两个字段必须放进同一个setData里避免中间态导致视图闪烁。常见错误是直接写this.data.currentQ questions[1]这不会触发页面渲染。正确写法始终是this.setData({ currentQ: questions[nextIndex] })。这里有一个性能边界setData如果每次都更新整个题目对象页面会有可感知的卡顿。更好的做法是把题目拆成question和options两个字段切换页面时只 setcurrentQ选择答案时只 setselectedAnswer。对于几百道题的量级直接整题 setData 影响不大一旦题目超过一千道并且带图片建议按章节拆分文件用wx.nextTick做页面渲染后的滚动。3.2 radio-group 单选框选项与解析联动value 用下标禁止拿对象直接赋值单选题组件可以用原生radio-group。wxml 代码如下radio-group classoption-list bindchangeonOptionChange label wx:for{{currentQ.options}} wx:key*this classoption-item radio value{{index}} checked{{selectedAnswer index}} disabled{{showExplain}} / text{{item}}/text /label /radio-group这里有两个细节。第一radio的 value 一定是字符串如果直接value{{item}}选项文本里有中文或标点没有问题但判断题选项 true/false 会被转成字符串true和布尔值true比较时容易出错。第二checked用selectedAnswer index其中 selectedAnswer 是数字index 也是数字但在 radio 事件里e.detail.value是字符串所以判断时要写Number(e.detail.value)。事件处理逻辑onOptionChange(e) { if (this.data.showExplain) return const index Number(e.detail.value) const currentQ this.data.currentQ const isRight index currentQ.answerIndex this.setData({ selectedAnswer: index, showExplain: true, isRight }) if (!isRight) { this.saveWrong(currentQ) } }showExplain在这里兼任锁定开关用户选过一次后不能再改避免连续点击造成错题记录重复写入。解析区域用wx:if{{showExplain}}控制显示不需要重新请求数据。如果需要支持多选题radio-group就不够用要改用checkbox-group事件里返回的是一个数组判分时先排序再比较。3.3 错题本写入 wx.env.user_data_path文件比 Storage 更适合图文混排错题本需要跨页面保存。轻量场景用wx.setStorageSync(wrongList, list)就能完成但对于图文混排的题目题目里的图片路径、解析文本、用户答案和作答时间累加起来很容易达到几个 MB而 Storage 的容量有上限且不能直接分享。更通用的做法是写到用户数据目录wx.env.USER_DATA_PATH。这个目录不需要向用户申请权限数据只在当前设备上可读。写入错题的完整流程const fs wx.getFileSystemManager() const wrongDir wx.env.USER_DATA_PATH /wrong const wrongFile wrongDir /wrong.json saveWrong(question) { fs.mkdir({ dirPath: wrongDir, recursive: true, success: () { fs.readFile({ filePath: wrongFile, encoding: utf8, success: (res) { const list JSON.parse(res.data || []) list.push(question) this.writeWrong(list) }, fail: () this.writeWrong([question]) }) } }) }, writeWrong(list) { fs.writeFile({ filePath: wrongFile, data: JSON.stringify(list), encoding: utf8 }) }第一次读取时readFile 的 fail 回调属于正常路径说明错题文件还不存在此时用[question]作为初始数组。recursive: true是递归创建目录的参数只在新版基础库支持老版本可以先调用fs.access判断目录是否存在不存在时再单独创建。Storage 和文件方案的选择可以这样看持久化方式适合数据量能否导出/分享典型用途wx.setStorageSync几十条记录不能答题进度、阅读设置user_data_path 文件按磁盘容量可以错题全文、图片缓存、导出文件实际项目中可以封装 Promise 来避免回调嵌套并且把写文件、读文件、删除文件三个操作放到同一个 utils 里方便后续扩展到其他页面。4. 题库数据从本地 JSON 接到接口wx.request 域名配置、云开发和分页参数本地 JSON 适合静态题库但题目一旦频繁更新或需要记录答题统计就要接接口。这一章讲常见的三条路本地压缩、云开发、自建后端。4.1 本地题库超过 2MB 怎么办先看 question JSON 是否被重复打包微信小程序主包有大小限制传统要求是打包后不超过 2MB。考研题库把政治、英语、数学全部塞进一个questions.js很容易超。拿到源码 zip 后先看project.config.json的packOptions.ignore是否能忽略不必要的图片目录再检查utils/questions.js里是否有大量重复字段比如每题都重复保存subject: 政治可以拆成按章节分别加载。常见的拆法是把utils/questions/变成一个目录下面按chapter1.js、chapter2.js分文件再在页面里按需 require。需要提醒的是require的模块在编译时都会被算进包体所以按需 require 并不能减少打包体积只是减少内存占用。真正减少包体要配合subpackages分包加载。源码如果提供了一个pages/subject/index作为科目列表可以把每科题目放到分包里主包只保留公共逻辑。不过分包配置会改动app.json的 pages 结构建议先确认功能正常再做。4.2 云开发 callFunction 拿题目适合毕设级别的免运维方案云开发的优点是省去服务器域名和证书直接用云函数访问云数据库。先在项目根目录创建cloudfunctions/getQuestionsconst cloud require(wx-server-sdk) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db cloud.database() exports.main async (event) { const page event.page || 1 const size event.size || 20 const res await db.collection(questions) .skip((page - 1) * size) .limit(size) .get() return { list: res.data, total: res.data.length } }云函数默认单次get最多返回 20 条这也是为什么函数里必须做分页。如果导入的源码没有cloudfunctions目录需要自己在开发者工具里开通云开发环境然后上传云函数。调用端wx.cloud.callFunction({ name: getQuestions, data: { page: 1, size: 20 } }).then(res { console.log(res.result.list) })调用前必须在app.js的onLaunch里先执行wx.cloud.init({ env: 你的环境ID })否则会提示Cloud API isnt enabled。常见的坑是环境 ID 写错或多个协作者用了不同环境导致一份代码里出现多个环境 ID。建议把环境 ID 放到config.jsmodule.exports { cloudEnv: kaoyan-xxx }这样上线前只需要改一个文件。云开发的数据库权限也需要检查如果只允许创建者读写游客用户拉不到题目题库数据通常应该对所有用户可读。4.3 wx.request 自建 API域名白名单配置与 onReachBottom 分页写法如果源码后端是自建的小程序端会用wx.request。这个接口受到微信域名白名单限制需要在小程序管理后台添加“request 合法域名”要求是 HTTPS、不带端口、不带路径。开发时可以先在开发者工具里关闭校验但真机预览正式版时白名单不生效所以最终必须配置。一个带分页的列表页写法Page({ data: { questions: [], page: 1, size: 20, isEnd: false, loading: false }, onLoad(options) { this.subject options.subject || this.fetchPage(1) }, fetchPage(page) { if (this.data.loading || this.data.isEnd) return this.setData({ loading: true }) wx.request({ url: https://api.example.com/kaoyan/questions, data: { page, size: this.data.size, subject: this.subject }, success: (res) { const list res.data.list || [] const total res.data.total || 0 const newQuestions this.data.questions.concat(list) this.setData({ questions: newQuestions, page, loading: false, isEnd: newQuestions.length total }) }, fail: () { this.setData({ loading: false }) wx.showToast({ title: 加载失败, icon: none }) } }) }, onReachBottom() { this.fetchPage(this.data.page 1) } })这里有几个容易出错的地方。this.subject是用 options 参数保存到页面实例上的不能在 fetchPage 里直接使用options.subject因为 fetchPage 调用时并没有 options 变量。loading标志位要放在请求开始之前避免onReachBottom连续触发isEnd的判断是newQuestions.length total如果后端不返回 total也可以改成list.length size表示已经是最后一页。还要注意用concat生成新数组后再 setData而不是this.data.questions.push后直接 setData后者在某些基础库下会触发过量数据对比。三种取题方案的对比方案是否需要域名是否需要后端服务题量限制推荐场景本地 JSON否否受包体限制题目少于 200 的演示项目云开发否否需云环境受数据库单次查询限制课程设计、个人刷题wx.request是是主要看后端在线更新题库多人使用后端接口返回的字段最好统一成{ list, total }这样前端分页逻辑可以复用。如果接口返回的是驼峰字段questionList可以在 success 里做一次映射避免改动每一处调用点。5. 真机调试时验证答题进度vConsole、读取错题文件、CSV 导出下载的源码在模拟器里能编译不代表真机上不出问题。这一章分享三个验证和扩展技巧也是把题库源码改成可用产品时最常用到的收尾动作。5.1 用 vConsole 观察 setData 时序在答题页面里加入console.log(selected, index)和console.log(current, this.data.currentIndex)然后使用开发者工具的“真机调试”打开手机屏幕会出现绿色 vConsole 按钮。点开 Console 面板确认e.detail.value的类型和打印值。如果发现每次点击都触发两次 onOptionChange检查 wxml 中是否把bindchange同时写在了 radio 和 radio-group 上。事件冒泡导致重复执行时只需保留 radio-group 的 bindchange。5.2 从 user_data_path 读回错题文件写完错题后验证文件是否真实存在可以加一个调试函数function readWrongFile() { const fs wx.getFileSystemManager() fs.readFile({ filePath: wx.env.USER_DATA_PATH /wrong/wrong.json, encoding: utf8, success: (res) { console.log(错题文件内容, res.data) }, fail: () { console.warn(错题文件不存在) } }) }fail 回调不需要弹 toast因为首次使用该功能时文件必不存在能区分文件是否存在本身已经是有效验证。这个函数可以放在onShow里但生产环境记得删除否则每次打开页面都会读一遍文件。5.3 把答题记录导出为 CSV 并分享到微信题库小程序最常见的扩展是导出成绩单。不需要引入 excel 库直接用 CSV 兼容 Excelfunction exportCSV(records) { const header 题号,题目,你的答案,正确答案,是否答对\n const rows records.map(r [ r.id, r.question, r.selectedAnswer, r.correctAnswer, r.isRight ? 对 : 错 ].join(,)) const filePath wx.env.USER_DATA_PATH /export.csv const fs wx.getFileSystemManager() fs.writeFile({ filePath, data: header rows.join(\n), encoding: utf8, success: () { wx.shareFileMessage({ filePath, fileName: 考研错题.csv }) } }) }CSV 的坑在于题目文本里如果包含逗号、双引号或换行Excel 会错列。最简单的方式是先把这些字符做转义用半角引号包住整段文本并把文本内部的引号替换成两个引号。wx.shareFileMessage需要基础库 2.11.0 以上用户点击分享后可以把 csv 发到聊天窗口然后用手机上的 Excel 打开。这个扩展不依赖后端和wx.env.USER_DATA_PATH的文件存储方案共用同一套文件系统算是把源码里的错题列表变成可交付文件表单的最短路径。本文还有配套的精品资源点击获取