)
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 classarea movable-view wx:if{{examStatus5}} classarea-box directionall x{{x}} y{{y}} view复盘学习/view view wx:if{{repeatView}}已完成/view van-count-down idcontrol-count-down2 formatmm:ss time{{ time2 }} auto-start{{false}} bind:finishcountdownFinish2/ /movable-view view wx:if{{fileUrl}} classtop-box view classcourse-banner swiper indicator-dotstrue indicator-colorrgba(255,255,255,0.5) indicator-active-color#fff verticaltrue circulartrue current{{currentIndex}} bindchangeswiperChange swiper-item wx:for{{fileUrls}} wx:keyitem view classimg-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.examStatus5){ 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.fileType1){ 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.mp44.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.studyStatus3 !repeatView) return $http({ url: /education-service/trainingTask/complete?idthis.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(interval10*60*1000){ this.countdownFinish() } if(countDown2){ let interval2 time2 - interval if(!interval2){ this.countdownFinish2() } this.setData({ time2: interval20?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分钟节流兜底保存既保证进度实时性又避免频繁接口请求。细节亮点全生命周期视频管控页面隐藏/卸载自动暂停视频杜绝后台播放、进度异常问题。