最近把一个儿童安全教育平台的项目从零到一做了完整交付技术栈选的是 uni-app。这个平台面向 3 到 12 岁儿童主要提供交通、消防、防溺水、居家安全、防拐骗等主题的动画课程和互动答题家长端可以查看学习进度和答题报告管理端负责内容审核和上架。整个过程中踩了不少坑也沉淀了不少可以直接复用的方案这里把业务拆解、技术选型、核心功能实现和多端适配的经验都整理出来给正在做教育类跨端应用的朋友做个参考。这个项目最考验人的地方不在“能不能跑起来”而在“儿童用户怎么用”和“家长怎么信任”这两个问题上。uni-app 能在很大程度上降低多端适配的工程量但它本身只是一套框架业务细节、交互约束、数据一致性、隐私合规这些才是实际交付时要花精力的硬骨头。下面按整个项目的推进顺序来聊。1. 项目定位与核心功能拆解1.1 儿童安全教育的业务场景和用户角色儿童安全教育和通用教育类应用不太一样它的使用场景很鲜明家长下载后帮孩子完成账号注册和绑定然后孩子在家长监督下观看安全动画、做互动答题有的家庭没有安装 App 的条件家长希望孩子在微信小程序里直接学部分学校或社区还会组织集体学习需要管理员批量创建班级账号、查看学习完成率。围绕这些场景我最终把用户角色定为三类不是简单的前后端两种。第一类是儿童用户他们打开应用要能快速找到自己感兴趣的主题。儿童不认识太多字页面要有大图标、语音引导、顺畅的视觉动线操作要避免误触不能一碰就跳出应用答题要有即时的正反馈比如音效、奖章、动画效果。第二类是家长用户他们关心的是孩子学了什么、学得怎么样、有没有薄弱环节所以家长端要提供课程记录、学习日报、答题对错分析和再学习建议。第三类是内容运营人员他们负责把制作好的安全课程配置上线、打标签、审核文本和音视频内容。业务还要求支持“家庭账号”和“班级账号”两种体系。家庭账号是一个家长绑定一个或多个儿童班级账号重点是统计维度和布置学习任务。这两种体系不能硬做两个系统而是要在同一套数据结构上通过角色和归属关系区分。1.2 功能模块划分与优先级取舍功能拆解时我按“儿童学习闭环”来组织看课、练习、反馈、激励、报告。围绕闭环分出了下面几个模块内容中心视频课程、图文知识卡、安全绘本朗读音频的展示与播放。互动学习安全主题答题、情景模拟判断、安全口令跟读。学习档案学习时长、答题成绩、薄弱知识点、成长记录。激励系统积分、勋章、打卡日历让儿童产生持续学习的动力。家长监管绑定儿童账号、查看报告、设置学习时长、接收学习提醒。运营管理课程内容上传、审核发布、标签管理、数据统计。第一版没有做社区、PK排行榜这类偏社交的功能也没有做复杂的自适应学习路径。原因是在儿童安全教育这个垂直场景里用户留存的关键不是“玩得多花”而是内容质量和家长信任。先把“学得放心、看得懂进度”做到极致比堆功能更有价值。这个取舍在实际推进中很重要团队少做了大量无用功。1.3 为什么选 uni-app 而不选原生或打包 WebApp技术选型不是炫技而是看交付物落在哪里。客户的要求很明确Android、iOS、微信小程序都要有H5 也要能应急而且项目周期只有几个月。如果三条端各自开发原生至少需要两套移动端团队如果只做 WebApp 套壳视频播放、本地缓存、离线打卡这些能力做起来又很别扭体验也跟不上。uni-app 的优势正好匹配这类诉求Vue 语法写一套代码编译到多端社区组件生态比较成熟video、canvas、地图等常见能力都有封装插件市场里有大量针对小程序和 App 的现成方案遇到问题基本能搜到处理办法。我当时也对比过 React Native 和 Flutter。RN 的生态确实不错但对小程序的复用能力弱还是要额外写一套逻辑Flutter 的自绘引擎在复杂 UI 下流畅度很高但混合开发里还要处理原生插件和 WebView 通信的成本。对一个内容型、表单型、管理后台为主的项目uni-app 的开发效率和端覆盖能力综合下来最优。方案多端覆盖开发效率团队上手成本内容型应用适配度原生三端高低高较高但成本高React Native移动端较好中中一般需再处理小程序Flutter移动端较好中中一般Web 和插件有额外成本uni-app Vue3高高低高插件生态丰富个人体会是跨端框架选的不是“最强技术”而是“最合适的技术”。如果项目里涉及大量高性能原生交互那还是要原生但教育内容平台这种 App、小程序、H5 多端并行的项目uni-app 就是很务实的选择。2. 技术栈搭建与工程架构实施2.1 基础工程、页面路由与目录规划工程搭建上我使用了 uni-app 的 Vue3 版本配合 Vite 构建。相比 Vue2 版本Vue3 的组合式 API 在组织复杂业务逻辑时明显更清晰一个“学习进度”功能相关的响应式数据、计算属性和方法可以放在同一个业务块里而不是分散在 data、watch、methods 中。页面路由采用默认的 pages.json 配置。儿童端主包只放核心页面首页、课程列表、播放页、答题页、我的。这里特别强调了分包加载。课程详情、答题结果详情、家长指引、隐私政策、活动页等内容都放进了分包避免主包体积过大。微信小程序对主包有 2MB 限制一开始没注意内容图片稍微多几组就报警了后来把静态资源和低频页面按主题分包才彻底解决。目录结构示例src/ ├── pages/ # 主包页面 │ ├── index/ # 首页 │ ├── course/ # 课程分类 │ ├── play/ # 视频播放 │ ├── quiz/ # 答题页 │ └── mine/ # 个人中心与档案 ├── subpackages/ │ ├── parent/ # 家长端页面 │ ├── report/ # 学习报告 │ └── activity/ # 活动与主题 ├── components/ # 全局组件 ├── composables/ # 组合式函数useUser、useProgress等 ├── api/ # 接口封装 ├── store/ # Pinia 状态管理 ├── utils/ # 工具函数 ├── static/ # 静态资源 └── pages.json # 路由与原生配置2.2 状态管理与数据请求封装细节状态管理用了 Pinia。这个项目里至少有三种全局状态登录态与角色信息、当前儿童档案、学习进度缓存。我用 useUserStore 负责登录态和账号信息useKidStore 负责当前选中的儿童。切换儿童时相关课程进度和答题记录要跟着变如果这些状态散落各页面容易出现“A 儿童的学习记录串到 B 儿童”的严重 Bug。统一放 store 里加切换前重载逻辑这个坑基本就堵住了。数据请求封装没有直接用第三方库而是基于 uni.request 做了轻量封装重点处理了这些事情请求头带 token 和版本号统一拦截 401 做静默登录网络错误和业务错误分开提示重复请求自动忽略接口耗时统计上报。请求层有个小细节很多教育类项目会忽略“儿童端接口”的幂等性比如答题提交按钮被多次点击可能发出多次提交请求。我在封装里增加了“同 key 请求几秒内不重复发送”的逻辑这个在后面处理积分重复发放时非常有效。// 请求封装核心逻辑示例 const pendingRequests new Map(); function request(options) { const requestKey options.url JSON.stringify(options.data || {}); if (options.duplicateKey pendingRequests.has(requestKey)) { return Promise.reject({ code: DUPLICATE_REQUEST, message: 重复请求已忽略 }); } options.duplicateKey pendingRequests.set(requestKey, true); setTimeout(() pendingRequests.delete(requestKey), options.duplicateInterval || 3000); return new Promise((resolve, reject) { uni.request({ url: baseUrl options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, token: getToken() }, success: (res) { if (res.statusCode 401) { handleLoginExpire(); reject(res); } else if (res.data.code 0) { resolve(res.data.data); } else { uni.showToast({ title: res.data.message || 请求失败, icon: none }); reject(res); } }, fail: (err) { uni.hideLoading(); uni.showToast({ title: 网络开小差了请稍后重试, icon: none }); reject(err); }, complete: () { options.duplicateKey pendingRequests.delete(requestKey); } }); }); }2.3 本地存储与云端数据同步策略儿童学习有个特点可能在没有网络的环境下使用比如在地铁、车里、家庭网络不稳定时。所以平台必须做离线兼容。我采用了“本地为主、云端兜底”的双层策略。视频课程播放结束后学习进度先写入本地 Storage再异步同步到服务端。答题结果也一样答题完成后先把结果保存在本地队列里网络恢复后统一上传。本地存储用 uni.setStorageSync缓存字段按“儿童ID 数据类型”做键名避免多个孩子共用设备时数据混乱。云端同步最麻烦的是“冲突”。比如孩子在地铁上答了十道题但没同步到家后家长又用另一台设备看了学习报告此时服务端数据还是旧的。我的做法是给每条学习记录加一个本地自增序号和记录时间同步接口按“客户端时间与序号”做增量合并相同内容的记录以服务端已存在的最新数据为准而不是简单覆盖。这种方法不算精巧但在儿童学习场景里够用且稳定。3. 儿童端与家长端核心功能的实现过程3.1 视频学习与自动续播功能的落地视频课程是这个项目的核心内容载体。实现视频功能时第一步就是选定播放器方案。小程序端直接用 video 组件App 端要额外注意兼容问题最后统一封装成一个 CoursePlayer 组件把端差异收敛到组件内部。课程列表页展示封面和进度信息点击进入播放页后先读取本地存的学习进度从上次观看位置续播。这个功能很基础但实际实现有细节儿童可能在一节课里反复观看所以“上次位置”要区分是“自然看完退出”还是“中途退出”。如果是自然看完进度标记为已完成下次从头播如果是中途退出记录退出时的时间点下次弹窗提示是否继续播放。这个交互明显减少了几岁孩子不知道怎么操作从而重新看一整节的困扰。视频进度上报不能太频繁。一开始我每 3 秒上报一次播放位置结果服务端压力大、数据库写入也频繁后来改成每 15 秒上报一次退出时再精确上报并主动标记学完状态。template view classplayer-wrap video :idcourseVideo videoId :srcvideoUrl :posterposterUrl :autoplaytrue :controlstrue :enable-progress-gesturetrue timeupdateonTimeUpdate endedonVideoEnded erroronVideoError / /view /template script setup const props defineProps({ videoId: String, videoUrl: String, posterUrl: String, lastPlayTime: Number }); const playTime ref(props.lastPlayTime || 0); const emit defineEmits([progress, ended]); function onTimeUpdate(e) { playTime.value e.detail.currentTime; // 节流上报每15秒记录一次 if (Math.floor(playTime.value) % 15 0) { emit(progress, { videoId: props.videoId, playTime: playTime.value, status: playing }); } } function onVideoEnded() { emit(progress, { videoId: props.videoId, playTime: 0, status: completed }); emit(ended); } /script有一个坑必须提醒App 端 video 在部分安卓机型上存在“视频盖住弹窗”的问题。也就是说视频播放过程中弹出积分提示层、答题弹窗都会被系统原生播放控件压在底下。解决办法是视频播放中需要交互时先暂停视频再显示弹窗如果一定要浮层用 plus.nativeObj.View 或原生 subNVue 方案实现。这个适配问题在真机测试阶段才发现改起来比较麻烦建议在需求阶段就约定“播放中一律暂停再弹窗”的产品规则。3.2 互动答题、积分与勋章的设计实现答题模块由题库随机抽题、答题判分、积分累计、勋章触发几个部分组成。题库按安全主题分类每道题包含题目、选项、正确答案、答案解析、配套安全知识点 ID。后台可以按主题设置每日答题数量比如每天 5 题。抽题策略我做了分层优先出孩子最近答错过的题目再补充新的题目保证复习与学习平衡。如果全部随机孩子会长时间碰不到薄弱题这个设计是为了让“学习报告里的薄弱项”能真正发挥作用。答题完成后要发积分这一步最怕重复发放。前端做了按钮防重复点击、请求层做了重复请求忽略、服务端还有唯一请求流水号校验。三层防护配合下来基本堵死了重复领取的问题。勋章触发条件用规则引擎实现规则表里配置“看完全部交通主题课 通过主题测试获得交通安全小卫士勋章”。前端在获得勋章时播放音效和动画给儿童即时反馈。累计积分用来兑换自定义头像装饰提高参与积极性。答题页的交互强烈建议用“先判断、后展示解析”的方式。儿童提交答案后立刻显示对错然后展示安全知识点解析而不是直接跳到下一题。这个解析环节既是教学也是纠错绝不能因为追求流畅而省略。3.3 家长绑定、学习报告与海报分享的实现家长端最核心的流程是“绑定儿童”。没有绑定关系就无法查看数据。最初我打算做邀请码输入但家长反馈输入邀请码对体验不友好。后来改成二维码家长在家长端首页点击“绑定儿童”生成一个带绑定参数的小程序码或 App 内二维码儿童端扫码时确认绑定关系。这里有一个隐私问题不能回避儿童绑定关系涉及敏感信息不能只凭一个二维码就完成绑定。我在绑定流程中增加了双重校验家长在绑定前需要先完成手机号验证绑定儿童时还需要输入儿童账号的“监护口令”监护口令由创建儿童账号时设置。这样即使别人拿到绑定码没有监护口令也无法绑定。学习报告模块需要把儿童的学习数据汇总成可视化图表。刚开始用 Canvas 手动画图表但工作量很大且不同机型渲染效果不一致。后来直接用一个覆盖图表库组件把数据渲染成柱状图和折线图。分享海报则必须用 Canvas 绘制因为小程序分享卡片不支持自定义长图。绘制海报时有个经验文字内容较多时不能把文字直接画在背景图上因为后端生成分享图会影响速度我选择把标题、当前学习主题、积分、二维码画在前景层背景是一张固定格式的模板图这样既清晰又便于复用。3.4 儿童防误触与防沉迷约束怎么做儿童端最容易被忽视的是防误触和防沉迷设计。第一个问题是返回键误触孩子手指在屏幕边缘滑动很容易退出页面。我从 App 端拦截了返回事件在课程播放页和答题页禁用“右滑返回”只在页面内提供明显的“退出”按钮同时加上二次确认弹窗“小朋友确定要退出吗”。第二个问题是学习时长控制。平台接入的底层能力是统计当天的学习时长超过家长设置的上限后儿童端会弹出一个不能立即关闭的休息提醒页必须由家长输入监护口令才能继续学习。这个功能在实现上没有太复杂关键是“儿童端不能自行跳过”否则防沉迷形同虚设。第三个问题是点击区域的尺寸。儿童的手指精细动作发育还不完善太小或太近的按钮极其容易误触。我把所有可点击图标的最小尺寸统一做到 44px 以上关键按钮间距留足 16px避免两个按钮贴在一起。这个尺寸标准在 UI 验收时要强制检查不能因为视觉美观而牺牲可用性。4. 多端适配遇到的问题汇总与排查思路4.1 视频播放器在 App 和小程序端的差异视频模块是多端适配里最容易出问题的环节没有之一。小程序端的 video 组件行为相对稳定但在 App 端遇到几个典型问题安卓部分机型上 video 默认会全屏播放不遵循页面布局部分 ROM 上 video 层级太高盖住自定义导航栏iOS 上视频播放时自动全屏的开关在不同版本行为不一致。第一版在安卓上出现了视频全屏、页面导航栏被遮挡的问题。排查后确认是 App 端 video 组件本身是原生控件不能像普通 view 那样用 z-index 控制。解决办法是用 cover-view 覆盖到 video 上或者在需要弹窗时先暂停视频。另外uni-app 的 video 在 App 端有多个渲染模式可选我最后选择了“weex”渲染模式在多个测试机上验证通过后再上线。4.2 点击区域、安全区与屏幕适配的坑全面屏时代底部安全区是一个不能忽略的问题。答题页的“提交答案”按钮如果放在底部很容易被 Home 指示条遮挡。我使用 uni-app 的环境变量 env(safe-area-inset-bottom) 和 constant(safe-area-inset-bottom) 来适配同时在 App 端通过 plus.navigator.getSafeAreaInsets 获取安全区高度。刘海屏顶部的适配同样不能省。自定义导航栏如果按常规 44px 高度计算在刘海屏上会被摄像头区域遮挡。我在自定义导航栏组件里加了一个 props让页面根据机型动态传入顶部安全距离统一调整标题栏高度和状态栏 padding。这些适配问题很难在开发机的模拟器上发现一定要准备几台不同的真机做回归测试。我的经验是准备至少三台安卓机加一台旧款 iPhone覆盖不同屏幕比例和系统版本比只测最新款旗舰机可靠得多。4.3 性能优化首屏加载与分包处理首屏加载速度直接影响儿童用户的耐心和家长对产品的信任。首页包含推荐课程、轮播图、主题分类图片较多如果不做处理首屏白屏时间会很长。优化方向有三个图片全部走 CDN 并配置 WebP 格式轮播图在页面 onLoad 才开始加载非首屏组件用 v-if 控制延迟渲染课程视频封面在点击播放前不加载视频资源只显示 poster 图。这些改动让首屏加载时间从 3 秒左右降到了 1 秒内。小程序分包处理也提过但这里要再强调一下不要在开发后期再分包从第一个页面开始就按“主包 业务分包”的结构规划。我见过不少项目上线前才发现主包超限最后被迫把所有页面乱塞进分包导致页面路径混乱、维护成本飙升。4.4 数据同步冲突与重复发放的 Bug答题积分重复发放是教育类应用最容易翻车的问题。当时测试环境里模拟弱网一个“提交答题”请求发出后延迟返回用户又点了一次提交结果服务端收到两条请求积分发了两次。前端按钮禁用、请求拦截都做了但弱网下请求超时重试后服务端还是可能重复处理。最终解决是在服务端加“幂等键”每次答卷生成一个唯一流水号服务端按流水号判断是否已处理过。这个流水号由前端生成提交时带上服务端已处理过相同流水号就直接返回成功不再累加积分。经验就是从项目第一天起涉及积分、奖励、订单这类资源操作必须设计幂等键否则后续极难补救。另外儿童切换账号时的数据同步问题也值得注意。如果两个儿童共用一个设备切换后本地缓存必须强制刷新。这个问题我一开始只做了“重置 store 状态”没有清理本地视频续播记录导致第二个儿童打开视频续播到了第一个儿童的播放位置。后来在切换账号逻辑中统一调用了 clearLocalProgress 方法把所有本地学习状态、缓存键全部重置再按新账号重新拉取。4.5 儿童内容审核与隐私保护的产品设计儿童安全教育平台的内容不但要“教育性强”更要“安全”。内容安全是底线不能儿戏。平台上线前我做了一条硬性规定所有课程文案、图片、音视频必须走人工审核流程同时接入自动机审能力先机审再人审。任何包含危险动作示范、不当引导、未经授权素材的内容一律驳回。隐私保护层面产品设计上遵循最小化采集原则。儿童端不采集真实姓名、家庭住址、精确位置等非必要信息只用唯一 ID 标识学习记录。家长端绑定手机号时明确告知收集用途并提供后台一键删除账号和数据的入口。数据传输全部采用 HTTPS敏感接口增加权限校验。这一点建议所有做儿童类应用的同学都认真对待这不是功能亮点而是必须满足的准入条件。我还有一个体会内容审核记录要留痕。每一条课程从提交到审核通过、发布、下架都要有状态流转和操作记录方便出问题时快速定位。这个后台功能投入不大但每次内容侧需要回溯时都能省大量时间。5. 项目交付后的经验沉淀做完这个项目之后我对“跨端教育应用”的整体认识发生了不少改变。医疗、教育这类项目稳定大于新颖、可用大于功能堆叠。在儿童应用里任何一点体验上的不稳都会被家长放大因为家长是在替孩子做选择信任一旦受损就很难修复。有一点想单独提醒uni-app 的多端能力不是银弹。它的跨端能力能覆盖大部分业务但每个端都有自己绕不开的原生差异和平台限制。开发前一定要把目标端列清楚把各端能力边界的技术调研做在前面不要等到开发中期才发现某个视频功能、某个地图功能在某端会失效。如果后续要扩展这个平台我会优先考虑加入自适应难度的学习路径和安全主题月度推送同时把报告系统做得更细比如按知识点维度输出儿童安全能力雷达图。技术侧则要进一步优化离线能力让儿童在没有网络的环境下也能完整学习并缓存学习记录。最后分享一个实操中的小技巧在 uni-app 项目里用一个统一的环境配置模块管理多端差异比如视频渲染方式、安全区高度、支付开关、分享渠道。这样遇到端差异时只改配置和分支而不用在页面里到处散落条件编译代码。有了这个基础跨端项目的后期维护才会真正轻松起来。