简介:面向抖音小程序开发者的“测试小程序”完整源码包,无需独立后端即可部署运行,适合个人开发者及小团队快速上线轻量级应用,并借助流量主功能实现广告变现。压缩包共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?from=normal' }) } } })这里有几个参数值得说。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会遍历每道题的选中项,把该选项的每个维度得分叠加到总表里。比如第一题选了 A,social加 2,energy加 1;第二题选了 B,social加 0,energy加 3。最终social为 2,energy为 4,pickType会返回'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中带参数,这样之前提到的问题——分享后对方打开首页而不是对应结果——就解决了。分享路径里的from=share会被结果页用来识别来源,从而决定是否展示底部重新测试按钮。
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?from=share',但分享者自己点开没问题,朋友点开就不行。原因多半是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生成组合键,再查表。如果查不到组合键,就降级用最高维度的单类型文案,这样老用户遇到新组合也不会看到空白页面。
改完以后一定要验证的路径是:清空缓存、重新打开、答完所有题、看完视频、点分享、再打开分享链接。我自己的习惯是每次改完数据结构,都会强制走一遍这个完整链路,因为这类纯前端项目最怕“模拟器看着正常,真机缓存一冲突就翻车”。从那以后我每次替换题库或者加新结果类型,都强制把广告点击、分享回流、缓存覆盖这三件事当作门禁,全部通过才敢提交新版本。这套流程不复杂,但能帮你挡掉九成以上的线上问题,希望帮到你。
本文还有配套的精品资源,点击获取