
简介这是一套基于微信原生框架开发的英语教育类小程序完整源码面向前端开发者、教育类小程序学习者及英语教学产品原型设计人员旨在解决英语学习场景中多模块功能集成与用户学习行为闭环构建问题。资源包共2000个文件以1277个JS逻辑文件为核心支撑各学习模块交互373个JSON配置文件管理题库与用户数据334个MD文档提供模块说明与开发注释辅以HTML入口页、XML配置及TypeScript相关工具脚本如tsc.js、typescriptServices.js整体压缩包大小为34.04MB。已有48人下载学习适合希望深入理解教育类小程序架构设计、掌握语音评测与学习进度跟踪等核心功能实现逻辑的开发者。读者可直接运行调试获取完整的单词记忆算法、口语评测接口对接方案、阅读材料分级加载策略及用户学习轨迹持久化方案。 这个项目是我帮一位做在线教育的朋友攒出来的完整交付物就是标题里那个 zip 包一套基于微信原生框架的英语学习小程序。市面上做英语学习的小程序不少但大多用 uni-app 跨端方案真正拿原生 WXML/WXSS/JS 一套一套写下来、还把单词、语法、听力、口语、阅读、写作六个模块都塞进去的还真不算多。写这篇东西是想把整个项目的选型思路、模块设计、数据结构、踩坑记录都摊开来讲。如果你正准备做教育类小程序或者想看看微信原生开发到底能承载多复杂的功能这篇应该能帮你省不少时间。1. 项目概览做这么一套英语学习小程序的核心思路1.1 为什么是微信原生框架而不是 uni-app / Taro先说结论在只有一个目标平台 需要深度调用平台能力 小程序包体有硬限制这3个条件下原生框架依然是最稳的选择。这套英语学习小程序的场景很典型目标平台只有微信不需要 App、H5、支付宝小程序多端复用要高频使用录音口语评测、音频播放听力、长按选词阅读、富文本渲染阅读写作范文这类平台级能力单词库、文章库这类静态资源体积不小必须做分包加载。如果用 uni-app开发效率确实高但一遇到平台差异最终还是得写条件编译去调微信专用 API等于把原生代码又写了一遍。而且 uni-app 的视图层在复杂交互比如听力模块的倍速进度控制、口语模块录音电平动画下状态同步的中间层开销是实打实的。原生框架的 setData 虽然也有性能坑但至少每一步都可以精准控制逻辑链路更短。另外微信开发者工具对原生项目的调试支持是最完整的wxml 面板实时看节点、Network 面板看请求、Storage 面板直接改缓存这些在跨端框架下多多少少会打折。说实话跨端框架最大的优势是多端一致我们根本没有多端需求那这个优势就等于零成本闲置。1.2 项目目录结构与模块划分整个项目结构是这样组织的├── app.js / app.json / app.wxss ├── pages/ │ ├── index/ # 首页今日学习概览、快捷入口 │ ├── login/ # 登录注册 │ ├── profile/ # 个人中心学习统计、进度曲线 │ ├── word/ # 单词记忆 │ ├── grammar/ # 语法练习 │ ├── listening/ # 听力训练 │ ├── speaking/ # 口语评测 │ ├── reading/ # 阅读理解 │ └── writing/ # 写作辅导 ├── components/ # 通用组件进度条、卡片、弹窗 ├── utils/ │ ├── request.js # 封装 wx.request │ ├── storage.js # 本地缓存读写 │ ├── audio.js # 音频播放管理器 │ ├── recorder.js # 录音管理器封装 │ └── scheduler.js # 单词复习调度算法 ├── assets/ # 图片、音频等静态资源 └── subpackages/ # 分包文章库、单词库、题库首页核心逻辑就一句话把用户今天该做的事全部推到他面前。比如今天该复习多少个单词、做了几道语法题、听力练了多久、口语说了几段全部按进度数据聚合展示。这样设计的原因是英语学习类产品的用户流失率极高留存全靠每天打开有一个明确清单——不是让用户自己找内容而是系统告诉他今天学什么。模块划分上六个学习模块全部独立成页面不互相跳转嵌套。页面栈最多就是首页→功能页→详情页三级避免微信页面栈最多10层的限制被调爆。这是原生开发里一个非常容易忽略但特别实用的原则能不进详情页就通过弹层解决比如单词卡片的词义展开、语法练习的答案解析都用半屏弹层做而不是新开页面。2. 六大学习模块逐个拆解2.1 单词记忆用间隔复习表管理背诵节奏单词模块不能做成简单的词卡划过必须有复习调度逻辑。我们参考了类似艾宾浩斯遗忘曲线的简化思路在utils/scheduler.js里维护一张间隔表初次学习的单词当天结束后会在第1天、第2天、第4天、第7天、第15天安排复习。复习时答题答对了间隔继续拉长答错了复习周期重置回第1天。调度逻辑核心代码如下// utils/scheduler.js const INTERVALS [1, 2, 4, 7, 15]; function getNextReview(record, isCorrect) { if (!isCorrect) { return { intervalIndex: 0, nextTime: Date.now() 86400000 }; } const nextIndex Math.min(record.intervalIndex 1, INTERVALS.length - 1); return { intervalIndex: nextIndex, nextTime: Date.now() INTERVALS[nextIndex] * 86400000, }; }这里有一个特别重要的细节复习调度必须是到期当天才出现在待复习列表而不是提前堆给用户。如果一上来就给一堆复习任务用户直接懵了。我们每天只在首页展示今日待复习 N 个词当天复习完列表清空。这个交互设计很大程度上决定了用户愿不愿意每天点进来。单词数据的存储也分了两层。单词本身词意、音标、例句、音频属于静态内容放在分包里随文章库一起加载每个用户的单词状态学习进度、复习次数、熟练度、错题记录放在本地 Storage 里定期同步到服务端。本地数据结构大致是这样{ wordId: word_001, status: learning, intervalIndex: 2, lastReviewTime: 1725000000000, nextReviewTime: 1726000000000, reviewCount: 3, wrongTimes: 1 }单词卡片 UI 做了左右滑动切换动画用 CSS transform 实现避免频繁 setData 导致卡顿。每次滑动结束只更新当前卡片索引其余卡片用wx:for配合wx:key做局部渲染。实测下来一屏 20 个词的词汇列表滚动非常流畅没有出现白屏或跳帧。2.2 语法练习题库设计 错题闭环语法模块的核心不是出题而是错题的闭环跟踪。用户做错的每一道题都会被记录到错题集并在后续练习中按更高概率重新出现。这是一套带权重的抽题逻辑// 抽题权重错题权重 基础权重 错题次数 * 2 function pickQuestion(questions, wrongStats) { const pool questions.map(q ({ question: q, weight: wrongStats[q.id] ? wrongStats[q.id] * 2 1 : 1, })); const totalWeight pool.reduce((sum, item) sum item.weight, 0); let random Math.random() * totalWeight; for (const item of pool) { random - item.weight; if (random 0) return item.question; } return questions[0]; }题库结构本身不复杂一道题的 JSON 长这样{ id: g_001, type: single_choice, grade: 初中, topic: 时态, question: She ____ to school every day., options: [go, goes, went, going], answer: 1, explanation: 主语为第三人称单数一般现在时动词加 s。 }答题结束后的解析环节我们用的是半屏弹层把正确答案 解析 相关知识链接放在一起。这里有个小设计解析里不只要讲对在哪还要直接列出你选的为什么错很多学习类产品只给正确答案不解释错误理由用户很快就失去兴趣了。实现上就是在选项里加一个wrongReason字段选错时直接展示。语法模块的本地进度数据不需要每道题都存只需要存错题 id 错误次数 最近错误时间。这个做法是为了控制 Storage 体积因为题库总量可能上千道每个用户都存全量做题记录本地缓存分分钟爆掉。2.3 听力训练播放器注意别踩这3个坑听力模块是原生开发里最原生的功能因为它要深度依赖微信的InnerAudioContext。项目里封装了一个全局唯一的音频播放管理器utils/audio.js核心作用就是确保任何时候整个小程序只有一个音频实例切页、退页、进新页面都不需要重新创建音频对象。// utils/audio.js let innerAudioContext null; function getAudio() { if (!innerAudioContext) { innerAudioContext wx.createInnerAudioContext(); } return innerAudioContext; } function play(src, options {}) { const audio getAudio(); audio.stop(); audio.src src; audio.play(); }这个模块踩过的坑我整理成清单做听力功能前建议先看iOS 上音频自动播放会被拦截。微信小程序里 iOS 对音频自动播放有限制除非用户在页面里有过点击行为否则audio.play()不生效。解决办法是在页面onLoad时先给用户一个点击开始播放的提示按钮或者用wx.onUserIntercept监听用户交互后触发。音频播放进度和时长更新不能太频繁。audio.onTimeUpdate在播放时高频触发如果每次回调都setData更新进度条整个页面的性能会严重下降尤其是在低端安卓机上。正确做法是只更新本地变量通过 CSS animation 驱动进度条动画仅在播放结束或用户暂停时才同步一次。后台播放要在 app.json 里声明。如果听力内容需要用户锁屏后继续播放必须在app.json的requiredBackgroundModes里加audio。不加的话小程序一退到后台音频就断了。但注意这个字段如果配置了微信审核的时候会重点检查你的类目是否符合要求学习类应用还好但别在非音频类目下乱开。听力练习的题型除了最基础的听完选答案我们还加了听写填空模式播放音频用户根据听到的内容在输入框补全单词。这个功能的实现逻辑是在文本中挖空用户输入后和原文比对。听写的输入框用的是原生input组件需要注意在安卓上输入法弹起时页面会被顶上去需要通过adjust-position属性控制。2.4 口语评测录音上传 服务端评分口语评测是整个项目里技术复杂度最高的模块也是用户留存的一个核心亮点。原生小程序本身没有现成的口语评测能力微信有一个同声传译插件WechatSI可以做语音识别但它只是把语音转写成文字并没有“流利度、完整度、准确度、发音标准度”这类评分维度。所以我们最终选择了客户端录音 服务端调用第三方评测 API的方案// utils/recorder.js const recorderManager wx.getRecorderManager(); function startRecord() { recorderManager.start({ duration: 60000, sampleRate: 16000, numberOfChannels: 1, encodeBitRate: 48000, format: mp3, }); } recorderManager.onStop((res) { wx.uploadFile({ url: https://api.example.com/evaluation, filePath: res.tempFilePath, name: audio, formData: { textId: currentTextId }, success: (resp) { const result JSON.parse(resp.data); // result: { overall: 85, fluency: 82, accuracy: 88, pronunciation: 80 } }, }); });录音参数这里我特意标注一下sampleRate用 16000encodeBitRate用 48000。这组参数是很多评测 API 的推荐配置低于这个采样率会导致音质过差、识别准确率下降高于它文件体积变大、上传时间变长测评成绩不会明显提升。录音格式用 mp3 就对了wx.uploadFile直接传临时文件路径服务端收到再做时长校验和转码。口语评测的交互设计也有讲究。用户说出文本之后我们不只展示一个总分而是展示四个维度分数 每个单词的发音正确率。具体做法是服务端评测完返回一个wordScores数组前端在文本上给单词标色绿色正确、黄色一般、红色偏差较大。这个视觉化的纠错反馈会让用户觉得这个功能专业而不是形式主义。这里有一个必须提醒的坑录音授权一定要在用户点击开始评测时主动触发不能等系统自动弹窗。如果在小程序启动时就直接调wx.getRecorderManager().start()微信会直接拒绝并提示用户未授权。更稳妥的做法是先用wx.authorize({ scope: scope.record })主动请求授权然后再启动录音。另外2023年之后微信加强了对隐私接口的管理小程序后台必须配置《用户隐私保护指引》并在代码里调用wx.requirePrivacyAuthorize来确认用户同意隐私协议否则录音接口会被拦截。这个在后面第4章会详细讲。2.5 阅读理解富文本渲染与生词点查阅读模块要处理的核心问题是怎么把一篇带格式的英语文章流畅地展示出来同时支持长按查词。文章的存储格式是 Markdown 转的 HTML小程序里直接用rich-text组件渲染。但这里有一个经典问题rich-text支持的标签有限像style标签里的内联样式会被过滤所有样式必须写在标签的style属性里。所以我们的服务端在输出文章 HTML 时就已经把所有样式处理成了内联样式前端不用做二次处理。生词点查是阅读模块最亮眼的功能。实现方案是在文章渲染完成后把每个单词用span包裹绑定点击事件查词后弹层显示释义。这里有两个关键细节不能长按整篇文本后弹文字选择因为rich-text不支持文本选择必须每个单词单独绑定bindtap。如果文章很长把所有单词都包span会生成大量 DOM 节点性能会崩。折中方案是只对难度等级 ≥ 4的单词加span基础词直接裸露这样 DOM 节点数量能减少 60% 以上。点击后的查词结果用的是半屏弹层展示不跳转新页面。这样用户在阅读时不会被跳出感打断读完一整篇文章的意愿会高很多。查词接口优先走本地词库本地没有再去查在线词典降低对网络的依赖。阅读计时功能我们也做了。用户进入文章开始计时退出时记录阅读时长按文章预估字数算出阅读速度 words/min在历史记录里展示变化曲线。这个功能虽然小但对学习数据完整性很重要因为进度跟踪不能只记做了和没做还要记效率怎么样。2.6 写作辅导本地编辑器如何设计写作模块表面上是文字输入其实坑非常多。首先小程序里的textarea是原生组件层级永远最高任何弹层、悬浮按钮都会被它盖住。写作文时用户可能想打开写作提示浮层看提纲结果 float 层直接被 textarea 盖在下面点都点不到。解决这个问题的方案是需要用浮层时先把 textarea 隐藏用wx:if控制或移到屏幕外等浮层关闭再恢复。这个方案虽然有点粗暴但实测是所有方案里最稳定的。另外一个思路是放弃textarea改用viewcontenteditable但小程序里没有 contenteditable 属性这条路走不通所以最终还是用 textarea 显隐控制的方案。写作模块还接了一个语法纠错能力用户写完一段话点检查按钮文本会发送到服务端调用语法纠错 API返回错误位置和修改建议。前端拿到结果后通过span把错误片段标红并在下方列出修改建议列表。这个功能的背后是每次提交时后台做文本 diff返回startOffset / endOffset / suggestion这样的结构前端做渲染即可。写作练习的另一种形态是范文仿写展示一篇高分范文用户对照仿写。仿写界面采用左右分栏左侧是原文右侧是写作输入区。这个界面在 6 英寸手机上其实挺挤的所以我们把小屏手机screenWidth 375自动切换成上下布局保证输入的舒适度。这种响应式细节虽然不起眼但用户体感差异特别大。3. 用户注册与学习进度跟踪怎么落地3.1 登录注册方案微信小程序里没有传统的用户名密码很多从 Web 转小程序开发的同学第一反应是做一个手机号 验证码的登录页。实际上在小程序里传统账号密码体系非常反人类我们最终采用的是微信静默登录 绑定手机号的组合用户首次进入小程序前端调wx.login()拿到临时code后端用code调微信的code2Session接口换取openid自动创建用户账号如果业务需要更完整的用户信息再引导用户点击获取手机号按钮通过open-typegetPhoneNumber获取加密手机号服务端解密后绑定到账号。// 登录核心流程 wx.login({ success: (res) { wx.request({ url: https://api.example.com/auth/login, data: { code: res.code }, success: (resp) { const { token, userInfo } resp.data; wx.setStorageSync(token, token); wx.setStorageSync(userInfo, userInfo); }, }); }, });这里要注意一个 2022 年之后的变更微信收紧了用户昵称和头像的获取接口wx.getUserInfo已经拿不到真实昵称头像了新方案是调用wx.getUserProfile也有调整或者让用户通过input typenickname和头像选择按钮主动填写。我们的做法是用户不填昵称也能用注册时系统默认分配一个POYI学员 7位随机数的 ID用户在我的 - 编辑资料里再主动完善。这样可以显著降低注册环节的流失率。3.2 学习进度跟踪的数据模型进度跟踪是这个项目的灵魂六个模块的学习行为全部要落到数据表里最终在首页和个人中心以图表展示。为了避免每个模块都存一份表我设计了一个通用的学习行为记录表字段类型说明user_idstring用户 IDmodule_typestringword/grammar/listening/speaking/reading/writingcontent_idstring模块内具体内容 IDactionstring学习动作如复习单词完成练习完成评测scorenumber得分口语/语法/阅读类模块使用durationnumber学习时长秒completed_attimestamp完成时间这张表在前端本地用 Storage 缓存每次完成一个动作就追加一条记录定期比如每天学习结束时、或进入首页时批量同步到服务端。同步机制做了防重记录本地生成一个 uuid 作为client_record_id服务端按这个 id 去重避免重复提交。首页的学习概览核心就是两条 SQL今日任务完成度统计今天完成的所有学习记录和昨天按算法预生成的任务清单做对比连续学习天数streak按completed_at分组取最近连续有学习的日期数。连续学习天数这个概念虽然简单但对留存的推动力非常大。我见过太多学习类产品上线后只关心新增用户其实不如把streak做明显一点用户断签一次就会有强烈的补上今天的欲望。这也是首页把 streak 放在最上面展示的原因。学习趋势图是用小程序 canvas 绘制的展示最近7天的学习时长柱状图。这里有一个 canvas 的坑小程序 canvas 的坐标单位和逻辑像素不是一回事在高分屏上必须用wx.getSystemInfoSync().pixelRatio做缩放否则画出来的图会模糊。具体做法是 canvas 的width设为 逻辑宽度 × dpr然后通过ctx.scale(dpr, dpr)把坐标系缩放回逻辑像素。4. 开发与上线阶段的典型踩坑记录4.1 性能与组件层面的坑原生小程序开发最容易被新手踩爆的就是 setData 性能问题。setData 是一次全量 diff 的过程不是只更新你传进去的那几个字段。如果页面数据量很大比如单词列表一次性渲染 500 个词卡每一次 setData 都会把整个数据对象序列化一遍低端安卓机上会出现明显的滑动卡顿。我们的优化方案有几个列表渲染时用到wx:for配合wx:key指定唯一 key尽量不改整份数组只更新需要变的那个对象需要更新数组中指定项时使用字段路径写法this.setData({ wordList[3].status: learned })而不是this.setData({ wordList })音频播放的进度、Canvas 动画这类高频回调直接越过 setData 更新通过 WXSWeiXin Script响应式处理或者干脆用 CSS 动画模拟。另一个大坑是页面栈限制。小程序页面栈最多只有10层如果用户从首页 → 单词列表 → 单词详情 → 例句详情 → 外部链接可能到第5层就接近危险值了。我习惯在onUnload里做一个统计凡是页面关闭时栈深度大于8就提醒用户在下一跳改用wx.redirectTo替代navigateTo避免溢出卡死。分包处理也是必须提前规划的。我们的项目把单词库、文章库、题库、音频素材全放在subpackages里主包只保留首页、登录、个人中心和公共组件。这样主包体积可以控制在 2MB 以内避免初始加载时间过长。如果后续还要扩展音频资源可以玩一下分包异步化用wx.loadSubpackage预加载下一个可能要打开的包用户点进去时页面几乎秒开。这个功能在app.json里配置好包之间依赖关系后代码层面几乎无感但体感提升很明显。4.2 审核与隐私合规注意点这一步是最容易翻车的地方也是最多人忽略的。微信小程序从 2023 年开始严格执行用户隐私保护要求凡是调用wx.getRecorderManager麦克风、wx.chooseMedia相册、wx.getLocation位置这类隐私接口必须在小程序管理后台配置对应的《用户隐私保护指引》并且在代码里调用wx.requirePrivacyAuthorize()做二次授权确认。这里有两个很常见的翻车点隐私弹窗时机不对。有些开发者在用户点击开始口语评测按钮时才弹出隐私授权如果用户拒绝再点击第二次就不再弹了录音直接静默失败。正确做法是在登录后首次进入口语模块时提前弹一次隐私授权用户确认后再进入页面。授权状态没有本地记忆。每次冷启动都重复弹隐私弹窗用户会非常反感。正确做法是wx.getPrivacySetting()查一次授权状态存到全局变量里后续进入模块时直接跳过。审核方面还有一个容易忽略的点音频类功能如果没有后台播放的合理使用场景不要轻易配置requiredBackgroundModes: [audio]。配置了这个字段审核时会被要求提供详尽的音频使用说明。英语听力场景还算容易通过但如果你的类目和音频完全不搭可能会被驳回。我们项目里听力模块有锁屏续听需求所以保留了配置但配套在 app.json 里写了清晰的用途描述。最后一个常见问题是顶部导航栏。很多自定义导航栏的实现方式是在app.json里设置navigationStyle: custom但不同机型的状态栏高度不一样如果硬编码statusBarHeight在刘海屏手机上顶部会直接重叠。我的方案是用wx.getWindowInfo().statusBarHeight动态计算并且把导航栏高度定义为statusBarHeight 4444 是常用的导航栏内容高度在 iPhone 和安卓上保持视觉一致。这个小问题看起来不起眼但对用户第一印象的影响非常大。最后再分享一个小技巧。整套项目交付之后我建议你保留一个学习诊断页面把所有进度数据和维度以极简的卡片形式展示让用户一眼看出我本周哪些地方做得好、哪些地方下滑了。表面上看这只是个数据展示实际上它是整个产品最能让用户愿意付费的功能点——用户不会为功能付费但会为清晰了解自己付费。这个逻辑在英语学习这类自驱型产品里尤其成立。本文还有配套的精品资源点击获取