微信小程序视频学习功能实现(进度跟踪,防抖节流提交)
2026/7/24 8:04:16 网站建设 项目流程

1、重点功能概括

  • 多视频垂直轮播、视频懒初始化缓存(原生性能优化)

  • 页面刷新自动恢复历史播放进度、精准进度缓存

  • 四态视频学习展示:未学习/正在学习/已学习/待解锁

  • 顺序解锁机制:仅完成上一视频才可学习下一视频

  • 防抖+节流双策略持久化学习进度(规避频繁请求)

  • 视频播放完毕自动切下一集、全集学完自动结课

  • 页面隐藏/卸载自动暂停视频,防止后台异常播放

2、核心数据定义

data: { x: 130, y: 480, // 文件类型:1=视频(核心学习类型) fileType: 1, detail: {}, // 视频资源地址数组 fileUrls: [], fileUrl: null, currentLength: 0, // 学习状态视图:2-未观看 3-观看完成 studyStatusView: 2, // 当前轮播下标 currentIndex: 0, time: 10*1000, previewImageList: [], previewImageIndex: 0, countDownHidden: true, repeatView: false, // 复盘学习倒计时时长 time2: 45*60*1000, // 视频进度缓存对象 {下标: 已学时长} videoProgress: {}, // 视频上下文实例缓存 videoContexts: {}, // 视频初始化标记缓存 initVideoCache: {}, },

3、WXML 视频核心结构

<movable-area class="area"> <movable-view wx:if="{{examStatus==='5'}}" class="area-box" direction="all" x="{{x}}" y="{{y}}"> <view>复盘学习</view> <view wx:if="{{repeatView}}">已完成</view> <van-count-down id="control-count-down2" format="mm:ss" time="{{ time2 }}" auto-start="{{false}}" bind:finish="countdownFinish2"/> </movable-view> <view wx:if="{{fileUrl}}" class="top-box"> <view class="course-banner"> <swiper indicator-dots="true" indicator-color="rgba(255,255,255,0.5)" indicator-active-color="#fff" vertical="true" circular="true" current="{{currentIndex}}" bindchange="swiperChange" > <swiper-item wx:for="{{fileUrls}}" wx:key="item"> <view class="img-item">4、核心 JS 视频学习完整源码

4.1 视频懒初始化工具方法(原生性能优化)

// * 懒初始化单个视频上下文,已初始化直接返回不重复创建 lazyInitVideoContext(index) { const { initVideoCache, videoContexts } = this.data; // 已经初始化过直接跳过 if (initVideoCache[index]) { return videoContexts[index]; } // 创建当前视频上下文 const ctx = wx.createVideoContext(`myVideo_${index}`); // 存入缓存 this.setData({ [`videoContexts.${index}`]: ctx, [`initVideoCache.${index}`]: true }) return ctx; },

4.2 轮播切换视频逻辑

//轮播图 切换 swiperChange(event) { if (event.detail.source !== "touch") return; const current = event.detail.current; this.setData({ currentIndex: current }); if(this.data.fileType === 1) { const { videoContexts } = this.data; // 1. 遍历所有已初始化视频,全部暂停 Object.values(videoContexts).forEach(ctx => { ctx.pause(); }) // 2. 懒初始化当前滑动到的视频 this.lazyInitVideoContext(current); } },

4.3课程详情获取 & 视频进度恢复核心逻辑

getDeatil(){ const that = this $http({ url: "/education-service/trainingTask/courseDetail", data: {id: this.data.id} }).then(res=>{ this.setData(res.data) if(that.data.examStatus==='5'){ setTimeout(()=>{ countDown2 = that.selectComponent('#control-count-down2'); if(res.data.fileType!==2){ countDown2.start() } },2000) } if(res.data.fileType!==1){ let fileUrls = res.data.fileUrl.split(',') this.setData({ fileUrls: fileUrls, totalCurrentLength: fileUrls.length, fileUrlNames: fileUrls.map(ele => { return decodeURIComponent(ele.split("/").pop().slice(13)) }) }) return } if(res.data.fileType===1){ const videoList = res.data.videoList || [] const progressObj = res.data.currentLength || {} const videoProgress = {} Object.keys(progressObj).forEach(key => { videoProgress[key] = progressObj[key] }) // 从 currentLength 恢复进度 if(res.data.studyStatus === 2 && res.data.currentLength && Object.keys(progressObj).length === 0){ let remaining = res.data.currentLength for(let i = 0; i < videoList.length; i++){ const videoDuration = videoList[i].videoTime if(remaining >= videoDuration){ videoProgress[i] = videoDuration remaining -= videoDuration } else if(remaining > 0){ videoProgress[i] = remaining remaining = 0 } else { videoProgress[i] = 0 } } } const fileUrls = videoList.map(item => item.videoUrl) const fileUrlNames = videoList.map(item => item.title) // 计算总时长 const totalVideoDuration = videoList.reduce((sum, item) => sum + item.videoTime, 0) // 计算当前正在播放的视频索引 let currentVideoIndex = 0 for(let i = 0; i < videoList.length; i++){ const watched = progressObj[i] || 0 if(watched > 0 && watched < videoList[i].videoTime){ currentVideoIndex = i } } // 如果所有视频都已完成,停在最后一个 if(currentVideoIndex === 0 && videoList.length > 0){ const lastWatched = progressObj[videoList.length - 1] || 0 if(lastWatched >= videoList[videoList.length - 1].videoTime){ currentVideoIndex = videoList.length - 1 } } this.setData({ videoList: videoList, fileUrls: fileUrls, fileUrlNames: fileUrlNames, totalCurrentLength: videoList.length, totalVideoDuration: totalVideoDuration, videoProgress: videoProgress, currentVideoIndex: currentVideoIndex, currentIndex: currentVideoIndex }) // 恢复播放进度 setTimeout(()=>{ if(res.data.studyStatus!==2 || !res.data.currentLength) return const result = that.calculateVideoProgress(res.data.currentLength, videoList) if(result){ that.setData({ currentIndex: result.videoIndex, currentVideoIndex: result.videoIndex, currentVideoProgress: result.progress }) setTimeout(()=>{ const targetCtx = that.lazyInitVideoContext(result.videoIndex) if(targetCtx){ targetCtx.seek(result.progress) } }, 500) } },1000) }else { this.setData({ currentIndex: res.data.currentLength }) } }) }, /education-service/trainingTask/courseDetail 接口返回数据示意 {code: 20000, msg: "操作成功",…} code: 20000 data: {courseId: 687, studyHours: 100, introduction: "测试", coursewareName: "测试十个课件7-15",…} courseId: 687 coursewareName: "测试十个课件7-15" currentLength: 0 fileType: 1 fileUrl: "/Video_26-07-10_15-19-01.mp4,https://zyztest-1310595178.cos.ap-g/Video_26-07-10_15-20-01.mp4,h/Video_26-07-10_15-19-01.mp4,/Video_26-07-10_15-20-01.mp4,/Video_26-07-10_15-19-01.mp4,/Video_26-07-10_15-20-01.mp4,/Video_26-07-10_15-19-01.mp4,/Video_26-07-10_15-20-01.mp4,/Video_26-07-10_15-19-01.mp4,/Video_26-07-10_15-20-01.mp4" introduction: "测试" studyHours: 100 studyStatus: 2 videoList: [{id: 312, title: "测试十个课件7-15",…}, {id: 313, title: "测试十个课件7-15",…}, {id: 314, title: "测试十个课件7-15",…},…] 0: {id: 312, title: "测试十个课件7-15",…} id: 312 title: "测试十个课件7-15" videoTime: 7 videoUrl: "/Video_26-07-10_15-19-01.mp4" 1: {id: 313, title: "测试十个课件7-15",…} id: 313 title: "测试十个课件7-15" videoTime: 9 videoUrl: "/Video_26-07-10_15-20-01.mp4" 2: {id: 314, title: "测试十个课件7-15",…} id: 314 title: "测试十个课件7-15" videoTime: 7 videoUrl: "/Video_26-07-10_15-19-01.mp4"

4.4视频进度工具计算方法

// 计算总观看秒数 calculateTotalSeconds(currentIndex, currentTime){ const { videoList } = this.data let total = 0 for(let i = 0; i < currentIndex; i++){ total += videoList[i].videoTime } total += currentTime return total }, // 根据累加秒数计算当前视频索引和进度 calculateVideoProgress(totalSeconds, videoList){ let remaining = totalSeconds for(let i = 0; i < videoList.length; i++){ const videoDuration = videoList[i].videoTime if(remaining < videoDuration){ return { videoIndex: i, progress: remaining } } remaining -= videoDuration } // 如果所有视频都看完了 return { videoIndex: videoList.length - 1, progress: videoList[videoList.length - 1].videoTime } },

4.5 视频播放实时监听

//视频播放中 videoTimeUpdate(e){ const planStatus = this.data.planStatus if(e.detail.currentTime > 0.5){ // 如果培训已结束 则不更新状态 if(planStatus ===3 ){ return } const index = e.currentTarget.dataset.index; // 视频已学习完成,不再更新 const videoDuration = this.data.videoList[index].videoTime; const currentProgress = this.data.videoProgress[index] || 0; const currentVideoIndex = this.data.currentVideoIndex if ((currentProgress >= videoDuration) && currentVideoIndex > index) { return; } const currentTime = e.detail.currentTime; this.setData({ videoProgress: this.data.videoProgress, videoCurrentTime:currentTime }) // 计算总观看秒数 const totalSeconds = this.calculateTotalSeconds(index, currentTime) // 防抖+节流保存进度 this.to_invited(totalSeconds); this.to_invitedTwo(totalSeconds); } },

4.6 视频播放完成回调逻辑

//视频播放完成/学习完成 videoEnd(e){ const index = e.currentTarget.dataset.index; const { videoList,videoCurrentTime } = this.data; // 已完成视频不再执行 if(!videoCurrentTime){ return } const totalSeconds = this.calculateTotalSeconds(index, videoCurrentTime) // 强制标记当前视频为已完成 const videoProgress = { ...this.data.videoProgress }; videoProgress[index] = videoList[index].videoTime; this.setData({ videoProgress: videoProgress }) this.saveLearnTime(totalSeconds, { type: 'videoEnd', index: index, isLast: index === videoList.length - 1 }) },

4.7进度持久化 防抖+节流

//节流 5分钟执行一次 to_invitedTwo:throttle( function(time){ this.saveLearnTime(time,1) },5*60000), //防抖 1秒延迟执行 to_invited:debounce( function(time){ this.saveLearnTime(time[0]) },1000), //观看进度/学习进度保存接口 saveLearnTime(time,extraData){ const { currentLength, studyStatus, studyStatusView,planStatus } = this.data let ltime = time - currentLength // 培训结束不更新进度 if(planStatus === 3)return if(ltime <=0) return if(studyStatusView === 2 && ltime <=0) return if(studyStatus !== 2) { if(studyStatusView === 3){ this.completeStudy() } return } // 调用接口保存进度 $http({ url: '/education-service/trainingTask/progress/update', method: 'post', data: { "currentLength": time, "courseId": this.data.courseId } }).then(res => { this.setData({ currentLength: time }) // 视频播放完成后的切换逻辑 if(extraData && extraData.type === 'videoEnd'){ const { index, isLast } = extraData const { videoList } = this.data if(isLast){ this.setData({ studyStatusView: 3 }) this.completeStudy() } else { const nextIndex = index + 1 setTimeout(() => { this.setData({ currentIndex: nextIndex, currentVideoIndex: nextIndex, currentVideoProgress: 0 }) // 暂停所有视频,懒初始化下一个视频 const { videoContexts } = this.data; Object.values(videoContexts).forEach(ctx => ctx.pause()); this.lazyInitVideoContext(nextIndex); }, 800) } } if(studyStatusView === 3){ this.completeStudy() } }) },

4.8 学习完成复盘倒计时回调,默认是45分钟,倒计时结束之后会调接口,后端处理后会增加学分绩点

// 完成课程学习 completeStudy(repeatView){ if(this.data.studyStatus===3 && !repeatView) return $http({ url: '/education-service/trainingTask/complete?id='+this.data.id, method: 'post', }).then(res => { this.setData({ studyStatus: 3, repeatView: repeatView||false }); }) }, // 复盘学习倒计时结束 countdownFinish2(){ this.completeStudy(true) },

4.9 页面每个生命周期 对视频的处理逻辑

onLoad(options) { this.setData({ id: options.id, examStatus: options.examStatus, planStatus:Number(options.planStatus) }) this.getDeatil() }, onReady() { countDown = this.selectComponent('#control-count-down'); const systemInfo = getApp().globalData.systemInfo this.setData({ x: systemInfo.windowWidth/2-30, y: systemInfo.windowHeight - 180 }) }, onShow(){ const {webViewTime,fileType,time2} = this.data if(countDown2 && fileType!==2){ countDown2.start() } if(!webViewTime) return const pages = getCurrentPages(); let preurl = pages[pages.length-1].__displayReporter.showReferpagepath if(preurl.indexOf("/webview")){ let nowTime = new Date().getTime() let interval = nowTime-webViewTime if(interval>10*60*1000){ this.countdownFinish() } if(countDown2){ let interval2 = time2 - interval if(!interval2){ this.countdownFinish2() } this.setData({ time2: interval2>0?interval2:0 }) countDown2.reset() } } }, // 页面隐藏暂停复盘倒计时 onHide(){ if(countDown2){ countDown2.pause() } }, // 页面卸载销毁所有视频实例 onUnload() { const { videoContexts } = this.data; Object.values(videoContexts).forEach(ctx => { ctx.pause(); }) if(countDown2){ countDown2.pause() } }

5、总结

  • 性能优化亮点:视频懒初始化:摒弃一次性初始化所有视频实例,滑动切换时才初始化对应视频,解决多视频页面内存溢出、卡顿问题。

  • 体验优化亮点:进度精准恢复:页面刷新后,通过currentLength累加时长自动计算对应视频下标+播放进度,精准还原上次学习位置。

  • 业务亮点:顺序解锁机制:通过currentVideoIndex控制视频解锁权限,未完成上一集无法播放下一集,严格贴合培训学习业务规则。

  • 架构亮点:防抖+节流双保险:1s防抖实时修正进度、5分钟节流兜底保存,既保证进度实时性,又避免频繁接口请求。

  • 细节亮点:全生命周期视频管控:页面隐藏/卸载自动暂停视频,杜绝后台播放、进度异常问题。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询