移动端学习产品的难点不在于“把页面做出来”,而在于同一套学习状态要跨越 App、小程序和 H5,同时经受弱网、系统切后台、播放器生命周期和高频进度事件的考验。织码在线教育系统以 UniApp + Vue 3 + uview-plus 为基础,把平台差异收敛到适配层,把课程学习过程抽象为可恢复、可重试、可观测的状态链路。

一、架构边界:跨端框架解决交付,业务层解决一致性
建议将移动端拆成四个边界:页面与组件层、状态与缓存层、平台适配层、统一请求层。课程页不直接调用原生能力,播放器不直接修改全局业务状态,所有跨端差异通过组件或工具函数暴露稳定接口。
| 边界 | 主要职责 | 关键约束 |
|---|---|---|
| 页面组件 | 组合课程卡片、播放器、进度条 | 不写平台判断 |
| 状态缓存 | 用户、课程、播放进度 | 细粒度更新,避免全页刷新 |
| 平台适配 | 视频、全屏、后台音频、网络 | 条件编译集中管理 |
| 请求服务 | 鉴权、超时、重试、错误码 | 写操作具备幂等语义 |
二、请求层:移动网络下的失败必须可解释
统一请求封装至少要处理四类结果:HTTP 成功且业务成功、登录态失效、业务错误、网络失败。对于 GET 请求可以有限重试;对于进度保存、考试提交等写请求,应携带幂等键,不能简单重放。
const RETRYABLE = ['ETIMEDOUT', 'NETWORK_ERROR']
export async function request(options) {
const requestId = options.method === 'POST' ? uuid() : undefined
const res = await uni.request({
url: BASE_URL + options.url,
method: options.method || 'GET',
data: options.data,
timeout: 8000,
header: { token: userStore.token || '', 'X-Request-Id': requestId }
})
if (res.statusCode === 401 || res.data.code === 401) {
await userStore.refreshOrLogout()
}
if (res.statusCode !== 200 || res.data.code !== 200) throw normalizeError(res)
return res.data
}
接口错误需要统一映射为可展示文案,同时保留 traceId 上报日志。这样用户看到“请重试”时,研发仍能在服务端定位具体请求。
三、平台能力适配:条件编译不等于复制三份页面

#ifdef APP-PLUS、#ifdef MP-WEIXIN 和 #ifdef H5 适合包住平台属性或原生调用,不适合在每个业务页面复制完整模板。可以定义播放器适配接口:
export const playerAdapter = {
play: () => videoContext?.play(),
pause: () => videoContext?.pause(),
seek: (seconds) => videoContext?.seek(seconds),
fullscreen: () => videoContext?.requestFullScreen()
}
App 侧可接入后台音频和原生权限,小程序侧重点是网络变化与组件限制,H5 侧需要处理自动播放策略、浏览器缓存和全屏 API。统一接口让课程业务只关心“播放、暂停、跳转、错误”这些领域事件。
四、播放状态机:恢复、上报和退出要有明确时序

播放器可以抽象为 INIT -> LOADING -> PLAYING -> PAUSED/ENDED,任意阶段都可能进入 ERROR。进入课程页先获取播放地址和历史进度,只有两者准备完成后才初始化 initialTime,否则容易出现先从 0 秒播放、随后突然跳转的闪动。
@Transactional
public void saveProgress(Long userId, ProgressCommand cmd) {
checkChapterBelongsToCourse(cmd.getChapterId(), cmd.getCourseId());
Progress old = progressMapper.find(userId, cmd.getChapterId());
if (old == null || cmd.getCurrentTime() >= old.getCurrentTime()) {
progressMapper.upsert(userId, cmd.getChapterId(),
Math.min(cmd.getCurrentTime(), cmd.getDuration()), cmd.getRequestId());
}
}
客户端按时间节流上报,暂停、退出和结束时立即 flush;服务端以章节为粒度做最大值更新,并校验 currentTime <= duration。这可以避免弱网重试导致进度回退,也能降低频繁写库的压力。
五、性能与可观测性:从“感觉流畅”变成可量化

性能优化要拆成网络、渲染、媒体和状态四层:接口分页与缓存课程元数据;列表触底加载、图片懒加载和分段渲染;视频先显示封面,进入可视区后再创建播放器;Pinia 按字段更新,避免进度变化触发整页重绘。
可观测指标建议包括:首屏渲染时间、接口 P95、视频起播耗时、播放错误率、进度上报成功率、页面崩溃率。指标按端、系统版本、机型和网络类型切分,才能识别“某一类设备在弱网下起播失败”的真实问题。
六、安全与发布:移动端只是可信边界之外
课程视频地址不应硬编码在前端。服务端鉴权后返回短时效签名 URL,接口仍需校验用户是否拥有课程权限;客户端 token 放在安全存储中,日志禁止打印 token 和完整播放地址。发布前还应检查隐私权限说明、异常上报脱敏和不同端的资源包大小。
七、总结
一套跨端代码的价值,最终取决于是否建立了清晰的工程边界。UniApp 负责多端交付,适配层负责平台差异,请求层负责失败语义,播放器状态机负责断点续播,进度接口负责幂等与一致性,指标体系负责持续改进。
如需私有化部署报价、远程产品演示,可访问官网 https://www.weavecodes.com/ ,私信作者领取企业落地案例。
断点续播,进度接口负责幂等与一致性,指标体系负责持续改进。

347

被折叠的 条评论
为什么被折叠?



