微信小程序实战:3Q工具箱如何用原生框架构建13个玩法的离线聚会工具
项目背景最近在研究一个叫3Q工具箱·真心话大冒险的微信小程序项目技术栈是微信原生框架 TypeScript纯单机、零网络、零后端。这个项目有几个工程上的亮点值得拆一下13 个玩法派对模式如何用一个轻量架构承载、个人主体如何在合规约束下做产品完整性、以及如何用工程化手段兜底合规要求。本文从源码角度聊聊它的几个关键设计。架构总览miniprogram/ ├── app.ts # 入口导航度量只在 onLaunch 算一次 ├── pages/ # 主包 9 页 │ ├── home/ # 首页玩法分组派对 CTA │ ├── party-config/ # 派对配置 │ ├── party/ # 派对进行中 │ ├── party-result/ # 派对结算 │ ├── players/ # 玩家名单 │ ├── records/ # 惩罚记录 │ ├── bank/ # 题库 │ ├── settings/ # 设置 │ └── about/ # 关于 ├── packageDice/ # 分包骰子专区 ├── packageLuck/ # 分包运气类 ├── packageCard/ # 分包卡牌文字类 └── utils/ ├── games-meta.ts # 玩法注册表单一数据源 ├── party.ts # 派对调度纯函数 ├── store.ts # 手写极简 store ├── storage.ts # 本机存储版本迁移 └── types.ts # 类型定义主包 9 页 3 个分包共 13 个玩法页。preloadRule 在进入对应分组时预加载分包。关键设计一一份元数据驱动多处这是整个项目最值得学的地方。utils/games-meta.ts里定义了一份 GAME_META 数组每个玩法一条记录包含 key、name、subtitle、group、color、badge、path、title、minPlayers、party、needsAi 等字段。这一份数据同时驱动首页分组渲染groupedGames(aiOpponent)函数返回按组分类的卡片数据首页 wxml 直接 wx:for 渲染派对模式玩法池partyGames()函数过滤 partytrue 的玩法人机开关文案切换resolveAi(game, aiOpponent)函数根据开关返回带 aiSubtitle/aiBadge 的形态页面标题 SEOtitle 字段对应各页 index.json 的 navigationBarTitleText玩法落地时只需要填上 path 字段首页和派对池自动出现不用改首页逻辑。这种单一数据源的设计在玩法数量增长时优势明显——加一个玩法只改一处不会出现首页有入口但派对池没有的漂移。exportfunctionresolveAiTextendsGameMeta(game:T,aiOpponent:boolean):T{if(!game.needsAi||!aiOpponent)returngamereturn{...game,subtitle:game.aiSubtitle||game.subtitle,badge:game.aiBadge||game.badge,minPlayers:game.aiMinPlayersundefined?game.minPlayers:game.aiMinPlayers,}}更妙的是首页分组时把奇数个玩法的最后一张标记为 wide由 CSS 跨两列占满不论将来每组有几个玩法都不会出现空缺。关键设计二派对调度器是纯函数utils/party.ts里的调度逻辑是纯函数输入已用玩法、池子、轮次→ 输出下一个玩法。核心约束是同一玩法在连续 3 轮内不重复用滑动窗口实现维护最近 3 轮的玩法 key 列表从池子里过滤掉这 3 个池子不足 3 个时降级为不与上一轮重复都不满足时才允许重复这个逻辑不依赖小程序运行时可以单独写 Jest 单测。项目里有 13 个测试文件覆盖各玩法的纯函数。派对模式不改 13 个玩法页——这是低耦合的关键。玩法页完全无感知派对模式的存在。派对模式通过 roundStartedAt 时间戳筛本轮新增的惩罚记录玩法页该怎么写怎么写派对结算时按时间戳聚合即可。关键设计三人机开关约束与 precheck个人主体做小程序有一个隐形陷阱人机对战的文案一致性。需求文档里明确写了三条约束电脑是否参与只由settings.aiOpponent决定玩法内部不得有第二个开关玩法注册表里用 needsAi 标记并同时给出开关打开时的副标题/角标/最少人数开关关闭时该玩法必须有一个可玩的真人形态这三条不是靠人肉 review而是靠scripts/precheck.mjs在发布前自动校验。precheck 会扫描所有 needsAitrue 的玩法检查是否有 aiSubtitle、aiBadge、aiMinPlayers并验证首页文案逻辑调用了 resolveAi。这种把合规判断写成检查脚本的做法非常值得借鉴。人肉 review 会漏CI 不会。关键设计四手写极简 store项目没有引 MobX自己写了一个极简的订阅/退订模式。对于这种状态不复杂的应用引入状态管理库反而是负担。store 的核心是setState 时遍历订阅者回调组件 onLoad 时订阅、onUnload 时退订。没有 selector、没有衍生状态、没有中间件——够用就行。这个判断是合理的。很多人一上来就 MobX/Redux其实小程序原生的 setData 一个极简 store 能解决 90% 的问题。关键设计五隐私合规的工程化打开about/index.wxml明确写着不收集、不上传、不共享。这不是营销话术而是工程约束不调用 wx.login不获取 openid不获取头像昵称不写 open-data 组件不申请位置、相机、相册读取、手机号唯一权限是 scope.writePhotosAlbum保存战绩图拒绝可降级为长按保存所有数据走 wx.setStorageSync纯本机更关键的是这套约束在工程上有兜底precheck.mjs 会扫描源码如果发现调用了 wx.login 或获取了敏感权限直接拦截发布。关键设计六导航度量只在 onLaunch 算一次小程序的 getMenuButtonBoundingClientRect 和 statusBarHeight 是很多开发者反复在每页 onLoad 里算的东西其实没必要。这个项目在 app.ts 的 onLaunch 里算一次存 globalData各页直接读 globalData 即可。// app.tsApp({onLaunch(){constsyswx.getWindowInfo()constmenuwx.getMenuButtonBoundingClientRect()this.globalData.navTopmenu.topthis.globalData.navHeightmenu.heightthis.globalData.statusBarHeightsys.statusBarHeight},globalData:{}})小细节但能避免每页重复计算和潜在的机型适配 bug。关键设计七玩法分主持人式与交接式这个设计解决了一台手机多人玩时的核心痛点私密信息怎么不被下家看到。主持人式公开玩法顶部统一用轮到 XX提示条出结果后必须主持人点做到了/没做到才推进惩罚默认记给当前轮到的人。交接式含私密信息如大话骰、数字炸弹换人时插入全屏交接遮罩必须本人亲手点开才继续。私密内容用不透明遮罩遮罩期间不渲染秘密节点。数字炸弹的秘密不属于某个人用半透明遮罩只承担明确换人的作用。这种按信息私密性分流交互的细节是产品想清楚了的标志。一些工程取舍的讨论不是没有可挑剔的地方个人主体不能开通微信支付所以变现路径受限。这个天花板是结构性的。项目代码层预留了广告插口ad-slot 组件AD.enabled 为 false 时整节点不渲染达标后改配置开关即可上线。## 关于合规的几点工程化处理这个项目在合规上做了几件值得学的事类目选工具不选游戏。游戏类目需《网络文化经营许可证》 版号个人主体拿不到且平台明确一旦选择游戏类目不可再变为其他类目。产品叙事落在聚会决策与随机工具。零酒精词汇。参考项目里饮酒语义是主线本项目整体改造为惩罚动作体系做鬼脸、学动物叫、发朋友圈酒精只作为用户自建内容存在。进阶内容分级开关。默认全年龄题库进阶档需手动开启且有二次确认不含性暗示、身体接触、露骨、酒精内容。UGC 不传播。自定义题库仅存本地、不上传、不共享、不可跨用户传播从机制上规避 UGC 传播风险。诱导分享红线。任何功能不以分享为解锁条件不做分享得次数/积分。隐私承诺工程化。不收集、不上传、不共享用户数据所有数据仅存本机并在发布前用 precheck 脚本扫描敏感 API 调用从机制上兜底合规要求。## 总结3Q工具箱·真心话大冒险在工程上做对了几件事单一数据源games-meta驱动多处玩法扩展只改一处派对调度纯函数化低耦合不改玩法页precheck 脚本兜底合规约束不靠人肉 review手写极简 store不引状态管理库隐私承诺工程化precheck 扫描敏感 API 调用主持人式/交接式分流解决私密信息传递问题对于做微信原生小程序的开发者这个项目的架构和工程化思路值得参考。特别是把合规判断写成检查脚本和一份元数据驱动多处这两个设计在很多场景下都能用上。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →