简介:面向微信小程序开发者和成语游戏爱好者,这份源码完整呈现了一个以成语接龙为核心玩法的轻量级应用。项目实现了无限关卡体系,集成横幅广告、激励视频和插屏广告,并通过分享给微信好友获得游戏能力,形成社交裂变增长闭环,适合希望学习小游戏变现与用户增长策略的开发者参考。压缩包共9928个文件,大小约62.94MB,以js(页面逻辑与交互)、png/svg/jpg(图标与视觉素材)、php(后端数据处理)、css/less/wxss(界面样式)、html(静态页面)、json(接口与配置)等类型为主,整体包含前端代码、后台服务及素材资源,目录结构清晰,便于按模块检索。目前已有1515人学习浏览。通过分析源码,可掌握微信小程序的框架调用、广告组件接入、分享机制设计、关卡难度曲线与奖励激励策略,并了解从1.0版本出发的迭代规划思路。对于希望实现答题类游戏、探索广告变现和社交裂变的开发者,这是一份可直接运行和二次开发的完整参考案例。
1. 成语小秀才微信小程序源码:到底是什么项目,谁适合拿它起步
如果你搜到这个标题,多半是想找一套能直接跑起来的微信小程序源码,改改成语题库、换换皮肤就能上线。成语小秀才就是这么一套答题闯关类小程序:用户进入关卡,看到题干和四个选项,选对进入下一题,答完一组解锁新关卡,进度存在本地。整套源码麻雀虽小、五脏俱全,页面、题库、逻辑、缓存和分享入口都是现成的。适合三类人:想快速上线一个教育类答题小程序的运营者,刚开始学小程序开发、需要一个真实完整案例的初学者,以及要接广告或做私域流量的个人开发者。它的价值不在“成语”本身,而在于“小程序答题类项目”的通用骨架,换一套题库它就是党建答题、安全知识竞赛或校园测验。
2. 这套源码的结构与核心玩法:成语题库、答题判定和关卡进度三个骨架
先别急着打开微信开发者工具。我见过太多人拿到源码第一步就导入、预览,结果界面出来了,却不知道改哪个文件能改变关卡数量、调整答题规则。把这个项目的骨架看清楚,后面所有改动都有的放矢。
2.1 项目目录与模块划分
微信小程序跟传统 Web 项目的直观区别就是“页面即目录”:每个页面是一个文件夹,里面放着同一名字的 .js、.wxml、.wxss 和 .json 四个文件。成语小秀才这种量级的源码,目录通常长这样:
成语小秀才/ ├── app.js # 小程序入口逻辑,全局变量与初始化 ├── app.json # 页面注册、窗口样式、导航栏配置 ├── app.wxss # 全局样式 ├── data/ # 题库与成语数据,通常以 js 模块导出 │ └── idioms.js ├── pages/ │ ├── index/ # 首页:关卡列表、开始答题入口 │ ├── game/ # 答题页:题干、选项、答题判定 │ ├── result/ # 结果页:答对数量、得分、分享按钮 │ └── level/ # 关卡进度页:解锁状态、星星或分数 ├── utils/ │ ├── storage.js # 本地缓存封装,管理做题进度 │ └── share.js # 分享配置与邀请逻辑 └── project.config.json # 开发者工具的项目配置这套目录对新手最大的意义是“改代码知道去哪改”:改首页按钮文案去 pages/index,改答题界面的倒计时去 pages/game,改成语数据去 data/idioms.js。我见过 A 同学想换标题文字,结果把全项目搜了一个遍,最后发现只是在 app.json 的 navigationBarTitleText 里改一行。记住,小程序里“文字位置”不等于“代码位置”,先看 app.json 的页面注册,再进对应 pages 文件夹。
app.json 里的 pages 数组是应用的地图,每个页面都必须在这里注册,否则跳转时直接白屏。答题类小程序一般注册四个页面:index 做入口,game 做核心玩法,result 做结算,level 做进度概览。页面顺序还决定启动时加载哪个页面,第一个写的就是默认首页。改页面路径时注意保持数组里每一项都以“pages/”开头,斜杠别漏。
这里有一个不少源码会忽略的细节:如果项目里用了自定义组件或第三方组件库,需要在 app.json 或页面 json 里提前声明 usingComponents。成语小秀才这版通常不用自定义组件,但你往里加组件库时就要记得补注册,不然控制台会报“Component is not found in path”。遇到这个报错,先检查组件路径和 usingComponents 是否匹配,多半是大小写或目录名写错。
2.2 成语题库的数据结构与设计意图
答题类小程序的题库,结构决定了写代码时是舒服还是难受。成语小秀才这类源码里,成语数据普遍是一个对象数组,每个对象包含题目、正确答案、干扰选项和解释:
// data/idioms.js const idioms = [ { id: 1, word: "画蛇添足", explanation: "比喻做了多余的事,反而坏了事。", options: ["画蛇添足", "画龙点睛", "守株待兔", "亡羊补牢"], answerIndex: 0, level: 1 }, { id: 2, word: "亡羊补牢", explanation: "出了问题及时补救,避免继续损失。", options: ["亡羊补牢", "对牛弹琴", "拔苗助长", "井底之蛙"], answerIndex: 0, level: 1 } ]; module.exports = idioms;字段看着简单,但有两个设计细节值得注意。第一,options 里正确答案和干扰项混在一起,靠 answerIndex 标记正确下标。这样做的好处是渲染四个选项时只需一个数组,wx:for 循环输出到按钮即可,不需要再单独判断“哪个是正解”,判定逻辑变成一次下标等值比较。第二,level 字段给每道题打了关卡标签,关卡解锁时用 level 与当前进度过滤题目,这是闯关类小程序的典型做法。
注意这里的 answerIndex 是数字下标,从 0 开始,不是选项内容。很多人改动时会把 answerIndex 误写成正确答案的文本,导致页面渲染没问题,但判定永远失败。一个简单的自检是:answerIndex 最大值必须等于 options 数组长度减一,也就是 3 以内。用第 4 章给的校验脚本跑一遍,这种错误一分钟内就能显形。
至于为什么用 js 模块而不是 JSON 文件来存题库,常见原因是小程序里 require 一个 js 文件比 wx.request 读远程 JSON 更直接,打包后也能同步管理版本。本地 js 在编译期就打包进去,远程数据在运行期拉取。数据量小、更新不频繁时选本地 js,数据量大、需要运营频繁替换时选远程接口,这个选型后面会有更详细的对比。
2.3 关卡进度与本地缓存的存储方式
答题进度一般不能丢,用户关掉小程序再打开,关卡还应该停留在上次的位置。小程序不提供浏览器的 localStorage,替代方案是 wx.setStorageSync 和 wx.getStorageSync。源码里通常会把读写进度封装到一个模块里,避免散落各处:
// utils/storage.js const LEVEL_KEY = 'idiom_master_level'; const TOTAL_LEVEL = 10; function getCurrentLevel() { const level = wx.getStorageSync(LEVEL_KEY); return level || 1; // 没有记录时默认第 1 关 } function unlockNextLevel() { const current = getCurrentLevel(); if (current < TOTAL_LEVEL) { wx.setStorageSync(LEVEL_KEY, current + 1); return current + 1; } return current; } function resetProgress() { wx.removeStorageSync(LEVEL_KEY); } module.exports = { getCurrentLevel, unlockNextLevel, resetProgress };这里的核心思路是只存“一个数字”,而不是存“整份进度对象”。很多新手会想把答题记录全部存进去,比如每一题选了什么选项、耗时多少秒,结果越存越乱,还容易碰到小程序本地缓存 1MB 的上限。只存关卡数字的好处是读写快、不会报错、多设备之间也容易做校正。解锁逻辑也很直接:答完当前关卡的最后一题,调用 unlockNextLevel 给进度加一。
我建议在封装时挂一个 resetProgress 方法,方便开发时一键清缓存。没有它,你测试关卡解锁时就得一遍遍去开发者工具的 Storage 面板手动删缓存,非常折磨人。这个小函数看起来多余,但在联调阶段能省下不少时间,属于“平时用不到、用一次救命”的代码。
关于进度存储还有一个边界情况要提:用户删除小程序再重新打开,本地缓存会消失,关卡进度回到第 1 关。这是小程序平台的行为,源码里一般不做处理。如果你希望进度跟着用户走,就得把进度上传到云开发数据库,存一个 openid 加 level 的记录。云开发的内容放在后面的章节展开,本地缓存方案对初版产品已经足够。
3. 在微信开发者工具里跑通源码:导入、配置与真机预览
拿到源码的第一步不是看代码,而是先让它在模拟器里跑起来。跑通了,后面改一处看一处,改错了立刻知道错在哪。这章按我习惯的流程走:注册测试号、导入项目、调基础库、解决预览时的真机问题。
3.1 注册测试小程序账号并拿到 AppID
小程序不是随便一个文件夹就能跑。微信开发者工具要求项目绑定一个 AppID,没有的话只能用测试号。个人开发者的操作路径是:微信公众平台注册小程序账号,选择个人主体,不需要付费认证,注册完成后在“开发管理 - 开发设置”里能看到 AppID。
如果你只是本地调试,不想走注册流程,开发者工具支持“测试号”模式,导入项目时点“测试号”即可。但测试号有两个明显限制:不能用云开发,真机预览时一部分接口调用会受限。所以只要你想把源码跑到手机上看真实效果,就必须用自己注册的小程序 AppID。注册环节唯一容易卡住的是邮箱验证和主体信息,准备好身份证号和手机号,几分钟能完成。
有一点要提前说明:注册完成后,小程序是“未发布”状态,这完全不影响本地开发、预览和体验版分发。很多人误以为一定要走完发布审核才能在手机上打开,其实真机预览和体验版是开发阶段的正常通道,只有正式发布给陌生用户时才需要审核。
3.2 导入项目与基础库版本设置
打开微信开发者工具,点“导入项目”,项目目录选源码所在文件夹,AppID 填刚才拿到的,然后点导入。如果是第一次运行,工具可能会提示“当前项目未通过校验”,这是源码包常见现象,点继续进入即可,不影响本地调试。
导入后先别急着点编译。第一步是看 project.config.json 里的 libVersion,也就是基础库版本。有些老源码用的是 2.10.x 甚至更早的基础库,而你工具默认可能已经到 3.6.x。大部分时候新版基础库能向下兼容,但一旦源码用了旧 API 或依赖某个老组件的特殊行为,就可能出现“wx.cloud 未定义”之类的报错。我的习惯是把基础库调到当前稳定版本,再看 console 有没有 deprecation 警告,有就逐个改。
另一个动作容易被忽略:在“详情 - 本地设置”里勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。这是调试阶段的开关,如果源码里用了 wx.request 请求远程接口,不勾选会直接提示域名不合法。注意这是调试专用,上线前必须关掉,否则正式环境会跳过域名校验,存在安全隐患。
基础库版本和调试开关是导入源码后最先要动的地方。很多人跳过了这两步,后面一遇到接口报错就以为是代码问题,查了半天才发现是环境开关没打开。先花两分钟把环境配好,比反复排查省事得多。
3.3 真机预览前必须检查的三处配置
模拟器里跑通只是第一步,真机才是用户体验的真相。预览要靠工具右上角“预览”生成二维码,但扫码前我会先检查三处:
第一,app.json 的页面路径。路径写错一个字,真机上打开对应页面就是白屏。检查方式是点“编译模式”增加启动页面,逐个页面都编译一遍,确保没有路径错误的提示。第二,网络请求域名。如果源码里调了远程接口,真机不认 localhost,必须把接口换成已备案且支持 HTTPS 的域名,并到小程序后台的“服务器域名”里添加白名单。第三,隐私协议弹窗。小程序要求首次打开时弹出隐私授权说明,如果你在源码里看到 wx.getPrivacySetting 或 wx.getUserProfile 之类的调用,务必确认弹窗逻辑在 app.js 里初始化过,否则真机上首次进入会吃掉一次授权点击。
真机预览出现的冷门问题比较玄学,常见的是安卓扫码后白屏,过两秒又恢复,这多半是基础库版本或首屏包加载过慢。我在第 5 章会专门展开这个问题。这里你只要记住一个原则:模拟器正常不等于真机正常。真机上的网络、存储、渲染时序都和模拟器不同,遇到问题先打开 console 看真机日志,再对照模拟器行为,不要自己脑补原因。
4. 核心代码拆解:答题渲染、判定逻辑与题库替换
这一章直接面对源码里最关键的三个逻辑点,把答题页每一段代码弄清楚,你就有能力改成自己的题库和玩法。
4.1 答题页的数据流与渲染逻辑
答题页通常叫 game,核心工作是拿到当前题目、渲染题干、渲染四个选项按钮。数据流非常简单:进入页面时从 storage 读当前关卡,从题库里筛出该关卡题目,把第一题填充到 data 中,页面模板循环渲染选项。
// pages/game/game.js const idioms = require('../../data/idioms.js'); const storage = require('../../utils/storage.js'); Page({ data: { questionIndex: 0, currentQuestion: null, selectedIndex: -1, totalCount: 0 }, onLoad() { const level = storage.getCurrentLevel(); const levelQuestions = idioms.filter(item => item.level === level); this.setData({ questions: levelQuestions, totalCount: levelQuestions.length, questionIndex: 0, currentQuestion: levelQuestions[0] }); }, selectAnswer(e) { const index = e.currentTarget.dataset.index; if (this.data.selectedIndex !== -1) return; // 防连点 this.setData({ selectedIndex: index }); this.checkAnswer(index); }, checkAnswer(selectedIndex) { const current = this.data.currentQuestion; if (selectedIndex === current.answerIndex) { this.nextQuestion(); } else { wx.showToast({ title: '选错了', icon: 'none' }); this.setData({ selectedIndex: -1 }); // 允许重选,不扣分 } }, nextQuestion() { const nextIndex = this.data.questionIndex + 1; if (nextIndex >= this.data.totalCount) { storage.unlockNextLevel(); wx.redirectTo({ url: '/pages/result/result?score=' + this.data.totalCount }); return; } this.setData({ questionIndex: nextIndex, currentQuestion: this.data.questions[nextIndex], selectedIndex: -1 }); } });这段逻辑的关键点有两个。一个是 selectedIndex 同时承担“防连点”和“选项高亮”两个职责,点击后先把它设为当前下标,再次点击的请求就会被开头的判断挡掉。另一个是答错的策略:这里写的是答错重选、不扣分,这是休闲答题源码最常见的设定;如果你想改成答错扣一条命,或者答错直接结束本关,只需调整 checkAnswer 里的分支逻辑,把 toast 换成对应处理。
注意:setData 是异步的,不能指望 setData 之后立刻从 this.data 里读到新值。nextQuestion 里连续调用 setData 看起来没问题,但后面如果要拿 questionIndex 做别的判断,一定要用局部变量,而不是马上读 this.data。这是小程序新手最容易踩的异步坑。
关于 setData 还有一个性能问题:不要把整个数组塞给 setData。有些同学图方便,在答题记录时直接 this.setData({ questions: this.data.questions }),这会把全部题目重新渲染一遍,关卡题量一大就明显卡顿。正确做法是只更新变化字段,比如 currentQuestion、selectedIndex,真机上 30 道题一关时差别很大。
4.2 选项按钮的 WXML 渲染与选中状态
答题页的界面并不复杂,但模板语法里有一个细节值得单独说:循环渲染四个选项时,要用 wx:for 遍历 options 数组,点击事件通过><!-- pages/game/game.wxml --> <view class="question-card"> <text class="question-word">{{currentQuestion.word}}</text> <text class="question-desc">{{currentQuestion.explanation}}</text> </view> <view class="option-list"> <view wx:for="{{currentQuestion.options}}" wx:key="*this" class="option-item {{selectedIndex === index ? 'active' : ''}}" >// tools/validate-idioms.js const idioms = require('../data/idioms.js'); const errors = []; idioms.forEach((item, i) => { if (!Array.isArray(item.options) || item.options.length !== 4) { errors.push(`第${i}条 options 长度不是 4: ${item.word}`); } if (item.answerIndex < 0 || item.answerIndex > 3) { errors.push(`第${i}条 answerIndex 越界: ${item.word}`); } if (item.id === undefined) { errors.push(`第${i}条缺少 id: ${item.word}`); } }); if (errors.length) { errors.forEach(e => console.error(e)); process.exit(1); } else { console.log(`校验通过,共 ${idioms.length} 条`); }
脚本要点是每条 error 都给出“第几条 + 字段 + 成语”三个信息,方便回原数据修正。跑之前确认 node 环境存在,然后命令行执行 node tools/validate-idioms.js 就行。除了校验,它还顺手打印出总条数,方便你核对关卡题量分布。
远程题库的场景需要额外处理加载时序。小程序回到前台不会自动重新拉题库,我通常在 app.js 的 onLaunch 里发起拉取,存到全局变量,答题页 onLoad 时如果发现全局题库为空,先展示 loading 再等请求返回。这与 5.1 的白屏问题同源,都属于“数据还没就绪就先渲染”的异步坑,只是规模更大。题库更新频率超过每周一次的项目,我还习惯在拉取时带上一个 version 字段,版本变化才替换本地缓存,减少无效请求。
5. 成语小秀才源码改版避坑:5 个容易翻车的问题与排查方法
写小程序的人都知道一句话:模拟器里一切都好,真机上一言不合就翻车。这章把我这些年看源码、改源码真正踩过的坑整理出来,按“现象、原因、解决”三步写,每个都给出排查路径,不让你在 console 里瞎翻。
5.1 白屏:页面打开后内容出不来,console 报 undefined 错误
现象:首页能正常打开,进入答题页后一片空白,console 出现 “Cannot read property 'word' of undefined” 或 “Cannot read property 'options' of undefined”。
原因:页面的 onLoad 里异步获取题库或关卡数据,setData 之前 data 中的 currentQuestion 还是 null,页面模板却已经尝试渲染 options。在本地 js 题库版本里,通常是过滤结果为空,当前关卡没有匹配题目;在云开发版本里,则是回调还没返回就开始渲染。
处理:在 data 里给 currentQuestion 一个合理的默认值,比如 { word: '', explanation: '', options: [], answerIndex: -1 },同时在 WXML 的题干区域加 wx:if="{{currentQuestion && currentQuestion.options.length}}",让数据没到时不渲染。排查顺序是:先在 console 看具体是哪一行调用 setData,再打印 levelQuestions 的长度,判断是“数据为空”还是“接口未返回”,再决定是修数据还是修时序。
5.2 页面栈溢出:跳转三四关后页面卡死或按钮失效
现象:用户从第一关玩到第五关,中途点击任何按钮都没有反应,或者页面直接卡在转场动画里出不来。
原因:小程序页面栈有硬性上限,10 层。有些源码的答题页面跳转用了 wx.navigateTo,这是压栈操作,每次跳一关就压一层,栈满之后新页面无法创建,旧页面也回不去,表现就是整个小程序假死。
处理:把关卡流转相关的跳转改为 wx.redirectTo,它是替换当前页,不增加栈深度。改造规则很简单:首页到答题页,保留 navigateTo;答题页到结果页、结果页到下一关、结果页返回首页,全部用 redirectTo 或 switchTab。改完之后,可以用 wx.getCurrentPages().length 在 console 打印当前栈深度验证,超过 5 层就说明还有漏网的 navigateTo。
5.3 选项连点:手速一快就跳题、判定错乱
现象:真机上快速连续点击两个选项,第一题的判定结果异常,有时直接跳过下一题,偶尔还出现显示“答对”但实际得分不对。
原因:bindtap 触发的事件处理里没有做状态锁。第一次点击 setData 后,渲染还没完成,第二次点击又进来,两个点击事件都执行了 checkAnswer,导致进度前移了两次。答题类小程序在触屏上的连点概率远高于模拟器鼠标操作,这个坑几乎每个改版者都会遇到。
处理:在 selectAnswer 开头加一道守卫,参考 4.1 的 selectedIndex 判断:
selectAnswer(e) { if (this.data.selectedIndex !== -1 || this.data.answering) return; const index = e.currentTarget.dataset.index; this.setData({ selectedIndex: index, answering: true }, () => { // setData 渲染完成后再判定 this.checkAnswer(index); }); }把回答状态拆成两个字段更好用:selectedIndex 负责 UI 高亮,answering 负责逻辑锁。在 setData 回调里再做判定,是为了等渲染完成后才进入下一题,避免视觉上出现“还没来得及看结果就跳题”的感觉。测试时,在模拟器里连续快速点击选项,再看题号是否稳定递增。
5.4 云开发报错:题库读不出来,全是权限或环境错误
现象:接云开发的源码,编译时一切正常,真机上拉取题目却报错,常见信息是 “errCode: -501000” 或 “collection not exists”,有时是 “env not found”。
原因:三个坑叠加。第一,app.js 里 wx.cloud.init 的 env 参数还是原作者的环境 ID,你没有改成自己的;第二,云开发环境里没有创建对应的集合,比如 idioms;第三,集合权限设置成“仅创建者可读写”,用户访问不到。
处理:先到云开发控制台新建环境和集合,把环境 ID 填回 app.js;再把集合权限改成“所有用户可读”,写操作保持仅管理员或按实际业务定制。排查时用真机调试模式打开 console,看报错信息里是 env 还是 permission:env 字样优先检查初始化参数,permission 字样优先检查集合权限。这两类错误在本地模拟器里经常被掩盖,因为开发者工具自动带上了开发者身份的权限。
5.5 老机型白屏恢复慢:基础库兼容与首屏体积
现象:某些安卓手机扫码打开小程序,首页出现后点进答题页,白屏持续两秒才渲染,偶尔还闪一下旧页面再刷新。
原因:多半是微信基础库版本停留在旧版本,渲染新版本的 WXML 结构慢;同时如果题库数据全部打进主包,首屏包体积偏大,老机器解析 JS 文件耗时明显。
处理:project.config.json 里把 libVersion 提到稳定版,并引导用户升级微信到新版本;如果题库超过三四百条,建议拆成独立分包,或改为远程加载。验证时用真机调试的网络面板看首页加载耗时,主包 JS 超过 1MB 就该考虑分包。这里要说明一下,分包不是把 data 文件夹直接挪走,而是在 app.json 里声明 subPackages,然后把题库页面和题库数据放进去,访问路径会变成分包根路径加相对路径,跳转逻辑要跟着改。
这五个坑不是全部,但覆盖了答题类小程序八成以上的线上问题。每次改版遇到没有见过的报错,我建议不要直接搜报错文本,先看报错信息里有没有“page”或“function”字样,定位是布局问题还是逻辑问题,再打开数据面板确认关键变量的值,最后才动手改代码,这个顺序能省下很多时间。
6. 进阶:把它改造成你自己的成语闯关小游戏,再加一点运营手段
最后这章聊的是如何让这份源码不只是一个演示项目,而是能持续运营的小产品。
第一个值得加的是“每日一题”模块。这不是简单加个页面,而是在题库表里增加一个 date 字段或单独维护每日题目映射,前端在 onShow 时读取当天日期,根据日期取题。很多团队做运营会发现,固定关卡的新鲜感撑不过一周,每日一题能带来一个稳定的日活来源。实现上不复杂,只要在 index 页面加一个入口,复用答题页的渲染逻辑即可。注意日期要用当前时间的年月日,不要依赖云端下发的绝对时间戳,否则时区会出问题。
第二个模块是接微信广告。答题类小程序的商业化最简单的方式是激励视频,比如“看视频获得一次答错重新选择的机会”。流程是:在小程序后台开通流量主,拿到广告位 ID;在答题失败弹窗里加一个“看视频复活”按钮,调用 wx.createRewardedVideoAd;广告播放完成后在 onClose 回调里恢复答题状态。注意激励视频的调用必须由用户主动点击触发,不能在 onLoad 里自动调用,否则会被判违规,严重时还会把广告位封掉。
第三个模块是数据埋点。小程序自带的 wx.reportAnalytics 可以上报基础事件,但我更建议你在关键路径上埋自定义事件:启动次数、进入答题次数、完成关卡数、答错后是否选择复活、分享按钮点击率。有了这些数据你才能判断改版有没有效果,而不是靠感觉。这个埋点不用写得很复杂,每处一行代码,关键是先埋,后面再慢慢调整上报字段。
我自己的一个习惯是:每改完一个模块,先在开发者工具里跑一遍全流程,再真机复测一次。这个习惯救了我好几次,有一次就是改每日一题时,因为日期格式化写错,凌晨零点定时任务取了空数据导致用户刷不出题。后来我在工具里写了一个 mock 日期的方法,把系统时间固定到次日零点测试,才把问题暴露出来。希望这些思路帮到你,让你的成语闯关小程序少走弯路。
本文还有配套的精品资源,点击获取