纯前端无后端测试小程序源码:答题引擎与广告变现实现解析
简介面向抖音小程序开发者的“测试小程序”完整源码包无需独立后端即可部署运行适合个人开发者及小团队快速上线轻量级应用并借助流量主功能实现广告变现。压缩包共236个文件总大小仅1.31MB包含27个js逻辑文件、15个json配置、11个ttss样式与10个ttml页面结构文件以及大量gif和png界面素材目录结构清晰便于直接改造复用。压缩包内附搭建教程docx可帮助开发者从环境配置、代码分析到提审上线的全流程落地。已有1920人学习下载对正在学习抖音小程序开发或希望低成本试水流量变现的读者而言是一份轻量实用的参考源码。1. 别小看“纯前端”一份测试小程序源码为什么能直接上线很多人看到“无需后台”这几个字第一反应是这项目不能碰怕是个半成品。但测试类小程序的商业模型恰恰是“轻后端、重创意”题目写死在本地、答案在本地计算、结果在本地渲染整个闭环完全可以不碰服务器。这份源码内置了答题页、结果页、广告位和分享逻辑导入开发者工具就能跑核心代码量不大却把一款短视频平台互动小程序最需要的部分都补齐了。它适合想快速验证测试玩法的个人开发也适合外包团队用来给客户搭趣味测评原型更适合准备接入流量主广告、拿小游戏式激励视频做变现的开发者。我第一次拆这种项目时也怀疑过没有数据库用户测过的历史怎么办真实跑起来才发现测试类场景根本不需要永久留存用户下次打开继续测就行。关键是把题目数据结构、结果计算规则、广告触发时机这三样东西理清楚整个项目就立住了。2. 架构设计无后端、无数据库的答题引擎是怎么搭起来的整体思路可以用一句话概括把“后端能做的事”全部前置到小程序端。题库编译成 JavaScript 模块答案存在本地缓存结果计算用纯函数执行广告组件挂在原生页面节点上。没有数据库表没有接口请求没有鉴权逻辑所以也就没有服务器宕机和接口超时的概念。2.1 本地缓存是唯一存储层这个项目的存储层级只有一层就是setStorageSync。进入首页时读取缓存如果缓存里已经有上次作答的维度分就直接展示上次结果没有则走全新测试流程。为什么不用云数据库抛开成本不谈测试类场景对数据一致性要求极低用户点“重测”后旧数据直接可以被覆盖。本地缓存天然满足这个需求加载还快几十题的量级对缓存空间来说可以忽略。工程结构大概是这样的. ├── app.js ├── app.json ├── pages │ ├── index │ ├── quiz │ └── result ├── data │ └── questions.js ├── utils │ ├── api.js │ └── scorer.js └── components └── ad-boxdata/questions.js是题库纯数据utils/scorer.js是结果计算器utils/api.js做平台 API 的统一切换components/ad-box是广告组件的封装。页面只有三个首页、答题页、结果页。每个页面只负责一件事首页发题答题页收集答案结果页做展示和转化。2.2 页面结构与路由参数设计三个页面之间的跳转没有用 URL 带答案序列而是走“缓存中转”。答题页每选一道题就把答案字母追加到本地数组答完最后一题后直接navigateTo到结果页结果页从缓存读取答案数组。这样的好处是路径短、参数少也不会因为题目数量变多导致页面跳转 URL 超长。首页到答题页需要传一个setId参数用来区分不同题库。题库结构里可以同时挂多套问卷比如“社恐程度测试”和“压力指数测试”。如果没有传setId默认加载第一套。结果页需要接收一个from参数标记当前是正常完成还是分享回流方便统计后续广告展示策略。2.3 广告组件的接入策略变现设计上激励视频是主力Banner 只做兜底。结果页顶部放 Banner底部放一个醒目的按钮“查看详细解读”点击后触发激励视频广告。用户完整看完视频才将维度分数映射成最终文案并展开详情。正常的广告 SDK 都会提供onClose回调回调里有一个isEnded字段只有它为true时才放行结果防止用户中途关闭广告白嫖内容。这套策略也是短视频平台小程序的通用做法先给用户一个模糊的分数区间完整解读放广告后面。源码里把广告 ID 统一放在一个配置对象中方便上线前批量替换成正式广告位 ID。3. 核心模块拆解从题目渲染到结果生成的完整链路很多人拿到源码会先找“算法”但这类项目真正的核心是数据结构和状态流。面试造火箭、工作拧螺丝这份源码里拧螺丝的功夫都花在三个地方题目怎么组织、答案怎么存、结果怎么算。3.1 题库数据模型一份可扩展的题库不应该只存“题目选项正确答案”。测试类小程序没有绝对正确只有不同维度的得分。所以每个选项都要带一个权重分表// data/questions.js module.exports [ { id: q1, title: 周末朋友突然约你出门你第一反应是, options: [ { key: A, text: 秒答应马上开始挑衣服, score: { social: 2, energy: 1 } }, { key: B, text: 犹豫半天最后说改天吧, score: { social: 0, energy: 2 } } ] }, { id: q2, title: 一个人待着的时候你通常会, options: [ { key: A, text: 刷视频、聊天、声音外放, score: { social: 1, energy: 0 } }, { key: B, text: 看书、发呆、享受安静, score: { social: 0, energy: 3 } } ] } ]score对象里的键名就是维度名social代表社交倾向energy代表精力恢复方式。分数值不是简单的 0/1而是 0 到 3 的权重因为有些选项对某个维度的影响更强。写题库的时候不需要管结果页长什么样只要保证每道题的维度键一致后续计算器就能拼出完整画像。3.2 答题事件与进度推进答题页维护两个核心字段current和answers。current表示当前题目索引answers是按题目顺序存放的答案 key 数组。点击选项时触发onChoose把选中的 key 塞进数组的对应位置然后判断是否到最后一题。// pages/quiz/quiz.js const questions require(../../data/questions) Page({ data: { current: 0, answers: [], total: questions.length, question: questions[0] }, onChoose(e) { const key e.currentTarget.dataset.key const answers this.data.answers answers[this.data.current] key this.setData({ answers }) if (this.data.current this.data.total - 1) { const next this.data.current 1 this.setData({ current: next, question: questions[next] }) } else { api.setStorageSync(test_answers, answers) api.navigateTo({ url: /pages/result/result?fromnormal }) } } })这里有几个参数值得说。dataset.key是从 WXML 的>// utils/scorer.js function calc(answers, questions) { const dims {} answers.forEach((key, i) { const option questions[i].options.find(o o.key key) if (!option) return Object.keys(option.score).forEach(dim { dims[dim] (dims[dim] || 0) option.score[dim] }) }) return dims } function pickType(dims) { const sorted Object.keys(dims).sort((a, b) dims[b] - dims[a]) return sorted[0] } module.exports { calc, pickType }calc会遍历每道题的选中项把该选项的每个维度得分叠加到总表里。比如第一题选了 Asocial加 2energy加 1第二题选了 Bsocial加 0energy加 3。最终social为 2energy为 4pickType会返回energy。结果页拿到类型名后去本地映射表里查找对应的标题、描述、行动建议再配上一张本地图片。这种计算方式的好处是无论题库有 5 题还是 50 题计算器都不用改。想新增一个维度比如“理性/感性”只需要在选项的score里加ratio键然后在结果配置表里补充这类结果文案即可。3.4 激励视频广告与分享回流广告模块没有直接调用平台方法而是经过utils/api.js做了一层适配。原因是同一套源码经常要换平台投放不同短视频平台的 API 前缀可能不同统一封装后换一行配置就行。// utils/api.js const platform typeof tt ! undefined ? tt : (typeof wx ! undefined ? wx : (typeof my ! undefined ? my : {})) function createRewardedVideoAd(adUnitId) { return platform.createRewardedVideoAd ? platform.createRewardedVideoAd({ adUnitId }) : null } module.exports { setStorageSync: platform.setStorageSync.bind(platform), getStorageSync: platform.getStorageSync.bind(platform), navigateTo: platform.navigateTo.bind(platform), createRewardedVideoAd }结果页的广告触发逻辑利用了onClose的isEnded判断// pages/result/result.js const ad api.createRewardedVideoAd(adunit-xxxx) ad.onClose(res { if (res res.isEnded) { this.revealResult() } })adunit-xxxx是广告位 ID上线前必须替换成自己申请到的真实 ID。revealResult会通过setData更新页面数据把隐藏的详细解读展开。分享回流是在onShareAppMessage中带参数这样之前提到的问题——分享后对方打开首页而不是对应结果——就解决了。分享路径里的fromshare会被结果页用来识别来源从而决定是否展示底部重新测试按钮。4. 避坑与排查从导入工程到广告上线的完整验证这个项目虽然简单但我见过不少人卡在“开发者工具里能跑真机上一堆问题”的阶段。这里把最容易翻车的几个点拆开讲每个都是现象、原因、解决三步走。4.1 导入工程时最容易被忽略的三个配置导入流程本身不复杂打开开发者工具选择“导入项目”选中源码目录填上自己的 AppID 或测试号。但接下来三件事一定不能漏。第一件是“AppID 使用测试号”。用测试号时广告组件只能以模拟数据展示真机上点击不会回调真实广告这是正常的不是代码坏了。第二件是“不校验合法域名”这个开关。本纯前端项目没有网络请求理论上不用开但如果你后续在result页里加了远程配置拉取就必须勾上否则真机会直接拦截请求。第三件是本地缓存前缀。如果你之前做过同一个小程序旧的缓存键可能冲突我一般会在app.js里加一个const STORAGE_KEY test_v1_answers这样的版本号后缀每次结构升级就换一个键避免用户读到脏数据。4.2 广告显示正常但点击后没有回调现象是Banner 和按钮都显示了但视频广告就是弹不出来或者用户看完视频却不解锁结果。原因绝大多数不是代码逻辑而是广告位 ID 是测试阶段的占位符。测试号对应的广告位是虚拟的它不产生真实视频流所以onClose永远不会触发。解决方法是把adunit-xxxx替换成你小程序后台申请的正式广告位 ID而且必须在小程序的主体信息通过流量主审核之后才能拿到。在开发阶段想要调试完整流程可以临时写一个跳过逻辑如果isEnded为假也放行但打上日志标记等上线前再删掉。4.3 分享结果给朋友朋友打开却回到首页这个问题最隐蔽。代码里明明写了onShareAppMessage返回path: /pages/result/result?fromshare但分享者自己点开没问题朋友点开就不行。原因多半是path写成了绝对路径pages/result/result少了开头的/或者result页面在渲染前就navigateTo跳转到了首页。测试分享行为时必须用开发者工具的“分享”调试按钮而不是直接预览页面。另外提醒一句如果结果页从缓存读答案分享者已经重测覆盖了旧答案朋友拿到的分享链接可能解析不到数据所以分享路径里至少要带上setId答案数据继续从缓存里读取并建议在结果页做一个“答案已失效重新测一套”的兜底按钮。5. 二次开发把单结果测试改成双维度类型测评这套源码最值得改的地方就是结果计算。原本pickType返回的是最高维度的单一类型比如“社交型”或“安静型”真正常见的性格测试往往是双维度组合比如“社交型活力型”“安静型思考型”。改造思路是让pickType返回前两个维度的组合排序而不是只取第一名。第一步修改scorer.js把pickType改成function pickDimensions(dims) { return Object.keys(dims) .sort((a, b) dims[b] - dims[a]) .slice(0, 2) .join(_) }第二步在结果配置表里新增组合键。比如原本只有social和energy两套文字现在要添加social_energy、energy_social等组合。注意顺序不同可能文案不同所以在dims排序后先比较大小再决定谁在前保证组合键稳定。第三步改结果页的revealResult。原来是拿到单个类型名直接取文案现在要先用pickDimensions生成组合键再查表。如果查不到组合键就降级用最高维度的单类型文案这样老用户遇到新组合也不会看到空白页面。改完以后一定要验证的路径是清空缓存、重新打开、答完所有题、看完视频、点分享、再打开分享链接。我自己的习惯是每次改完数据结构都会强制走一遍这个完整链路因为这类纯前端项目最怕“模拟器看着正常真机缓存一冲突就翻车”。从那以后我每次替换题库或者加新结果类型都强制把广告点击、分享回流、缓存覆盖这三件事当作门禁全部通过才敢提交新版本。这套流程不复杂但能帮你挡掉九成以上的线上问题希望帮到你。本文还有配套的精品资源点击获取