ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

UniApp仿抖音视频组件:单实例播放与可视区自动暂停实现

UniApp仿抖音视频组件:单实例播放与可视区自动暂停实现 简介这份源码包面向使用UniApp开发跨平台短视频应用的开发者聚焦抖音式交互组件的完整实现。内含videoList、videoPlayer、listRight、listLeft等6个vue组件配合json配置文件、iconfont样式与说明文档组成可直接运行的项目骨架。资源共15个文件压缩后约47KB结构精炼便于快速阅读与二次开发。项目实现了滑动切换视频、双击点赞、首个视频自动播放等核心功能并通过props与emit的配合演示父子组件间传值与方法调用涵盖视频缓冲、播放、暂停、全屏切换等播放细节。适合具备Vue基础、希望掌握跨端组件化开发技巧的前端工程师已有169人学习下载对理解短视频场景下的组件拆分与通信机制具有实用参考价值。 说实话第一次看到仿抖音视频组件这个需求的时候我心里是有点打鼓的。市面上插件市场一搜一大把现成的为什么还要自己写但真正动手做下来才发现这类组件最大的问题恰恰是能用和好用之间的差距。我这次用UniApp从零搭了一个视频信息流组件核心要解决的就是那句话限制一个视频播放视频滑出可视区自动暂停。这篇就把整个实现过程拆开揉碎讲清楚从页面骨架到单实例播放管理从可视区判定到真机踩坑适合正在做短视频、视频列表、H5信息流这类需求的开发者参考。先交代一下背景。我做的这个组件基于Vue3版本的UniApp目标平台覆盖微信小程序和App端视频源是常规的MP4格式整体交互参考抖音全屏卡片上下滑动一屏一视频滑入自动播放滑出立即暂停列表数据分页加载。下面直接进入正题。1. 先回答一个问题为什么建议自己写而不是用插件市场的现成方案很多人的第一反应是去插件市场搜抖音视频确实能搜到不少封装好的组件下载量看着也很可观。但我实际用过几个之后发现它们在业务落地时普遍存在三个绕不过去的坎。第一个坎是视频实例太多导致的内存问题。不少现成组件的实现方式很粗暴列表有多少条数据就渲染多少个video标签虽然用v-if控制显示隐藏但底层仍然会创建大量播放器实例。Android低端机上滑动不到二十个视频App就开始卡顿甚至闪退iOS上WebView的内存告警也随之而来。抖音自己是原生实现天然没有这个问题但跨端方案里这恰恰是最容易翻车的点。第二个坎是定制不灵活。短视频业务很少只做播放视频这一个动作——通常还要叠加点赞动画、评论抽屉、关注按钮、话题标签、购物车入口甚至左右滑切换Tab。插件市场里的组件往往把交互写死了改起来比重新写还痛苦尤其在多端同步上线的时候每端表现还不一致修bug都能修到怀疑人生。第三个坎是版本维护没有保障。很多现成组件作者更新时间停留在两年前UniApp升级到Vue3和新的编译体系之后旧组件会出现各种兼容问题到时候你连找谁修都不知道。所以我的判断是如果只是临时演示可以用现成的但凡要正式上线、要持续迭代自己写一个可控制的组件是更稳妥的选择。而且核心逻辑并不复杂——一个竖向滚动的容器、一个当前索引、一个播放器实例再加一段可视区判定整个核心代码量其实很小但你能完全掌控性能和交互。我这次组件化之后核心代码只有两个文件VideoFeed.vue负责信息流容器和可视区计算VideoCard.vue负责单个视频卡的渲染和播放总代码量不超过五百行。后面所有截图和代码片段都来自这套实现。2. 页面骨架竖向滚动容器到底用scroll-view还是swiper这是做仿抖音组件遇到的第一个方案选型问题。大多数人的直觉是用swiper组件因为官方demo里就有垂直方向的轮播示例item里面放video看起来正好能实现一屏一视频的滑动效果。swiper最大的优点是自带惯性滚动和翻页动画滑动结束自动对齐到整屏不需要自己计算滚动位置。但它的问题也很明显本质上它是轮播图逻辑每一项是固定屏高的页想做数据预加载、想做滑动距离超过50%才翻页这种抖音式手感都要额外靠change事件去模拟灵活性受限。而且swiper一次性渲染所有子项视频多的时候和只留一个播放实例这个目标直接冲突。我最终选择的是scroll-view方案。原因有三点第一scroll-view是一个真正的滚动容器滚动距离、方向、速度这些参数都能拿到方便自己做可视区计算和播放控制第二它天然支持数据分页列表末尾触底加载下一页很自然第三滚动过程中的内容都是真实渲染的DOM在微信小程序端配合同层渲染video标签能正常覆盖其他元素不会出现层级错乱。先上一个最基础的页面结构template view classvideo-feed scroll-view classfeed-scroll :scroll-ytrue :scroll-topscrollTop :scroll-with-animationfalse :style{ height: pageHeight px } :show-scrollbarfalse :enhancedtrue :bouncesfalse scrollonScroll view v-for(item, index) in videoList :keyitem.id classfeed-item :style{ height: pageHeight px } VideoCard :video-srcitem.videoUrl :coveritem.coverUrl :activecurrentIndex index :need-playindex currentIndex / /view view v-ifloading classloading-tip加载中.../view /scroll-view /view /template几个关键点的解释pageHeight不能用100%因为竖向列表中每一项的高度必须是固定像素值百分比高度在部分小程序端会解析异常所以我在onReady里用uni.getSystemInfoSync().windowHeight取到窗口高度作为每张卡片的固定高度也是后续可视区计算的基准单位。scroll-with-animation我设成了false。可能有人会问抖音切换视频时不是有那种过渡吗这个过渡是滑动本身带来的不是scroll-view的动画。如果把这个参数设成true调用scrollTop跳转会有一段平移动画反而拖慢手感和检测逻辑。enhanced在微信小程序端是开启scroll-view的增强特性让滚动事件返回更精确的滚动距离值bounces{false}是关闭iOS橡皮筋效果防止滑过边界后出现白屏也避免拖拽时视频画面被拽走。接下来是滚动监听的计算逻辑这段是仿抖音手势的灵魂onScroll(e) { // 滚动中的实时逻辑 const scrollTop e.detail.scrollTop; const rawIndex scrollTop / this.pageHeight; // 还没翻过一整屏时不切换保证手感和抖音接近 if (rawIndex this.currentIndex - 0.5 || rawIndex this.currentIndex 0.5) { this.switchByIndex(Math.min( this.videoList.length - 1, Math.max(0, Math.round(rawIndex)) )); } }为什么要判断0.5这个阈值因为scroll事件的触发频率很高用户手指轻轻滑动、还没决定翻页的时候如果立刻切视频会出现画面还没稳定视频已经切走的跳动感。抖音的做法是滑动过程基本不做切换动作抬手后滑动速度降到阈值以下才翻页。0.5这个阈值模拟的就是这个确认翻页的判断当前视频偏移超过半屏才把currentIndex切到新的整数位置。不过只用scroll事件有一个缺陷用户快速Fling的时候scroll事件会连续触发可能从位置A直接跳到位置C中间视频还没来得及播就又被切走。所以我又加了一层防抖节流切视频操作统一放到requestAnimationFrame里保证同一帧内不会多次切换let frameId null; onScroll(e) { const scrollTop e.detail.scrollTop; const rawIndex scrollTop / this.pageHeight; if (frameId) cancelAnimationFrame(frameId); frameId requestAnimationFrame(() { if (rawIndex this.currentIndex - 0.5 || rawIndex this.currentIndex 0.5) { const target Math.min(this.videoList.length - 1, Math.max(0, Math.round(rawIndex))); if (target ! this.currentIndex) { this.switchByIndex(target); } } }); }总结一下scroll-view方案比swiper好在可控性代价是要自己写一点手势判断逻辑但这部分代码量很小、逻辑也很直观做一次之后完全值得。3. 核心逻辑为什么必须只留一个播放实例以及可视区判定的两种实现这是整个组件最核心的部分。很多人仿抖音做失败都是败在这里每个视频卡里都放一个video标签用户滑到哪条播哪条其他video用v-show隐藏。表面看能出效果但实际运行一段时间就会发现问题每个隐藏的video仍然占着播放器内核资源音频解码器、视频解码器都还挂着内存和CPU占用持续走高。测试过二十个视频卡的页面Android低端机上内存峰值能到600MB以上滚几次系统就开始杀进程了。所以我在设计上定了一条铁律任何时刻最多只保留两个video实例——当前正在播放的那个加上它的相邻上一条或下一条用于预加载让滑动切换更丝滑。其他地方的理论依据是人在滑动信息流时眼睛只能看到当前屏和下一屏再远的视频根本没有必要提前建播放器等滑到再说。实现上我在VideoCard里用v-if控制template view classvideo-card video v-ifshouldRenderVideo :srcvideoSrc :postercover :autoplayactive :controlsfalse :mutedfalse :looptrue :show-center-play-btnfalse :show-play-btnfalse object-fitcover playonPlay pauseonPause /video view v-else classvideo-cover :style{ backgroundImage: url(${cover}) } !-- 封面兜底降低内存消耗 -- /view /view /templateshouldRenderVideo的值由父组件传入只有index currentIndex || Math.abs(index - currentIndex) 1时才为真。其余卡片一律只渲染一个封面图内存占用基本可以忽略。这个策略实测效果很明显滑动二十个视频内存峰值稳定在200MB上下用户完全无感。接下来就是题目里说的那个硬需求——滑出可视区自动暂停。这里我对比了两种实现方式。第一种是上面那个scroll事件配合currentIndex判断。这种方式的原理是容器每屏的高度固定当前屏索引就是scrollTop / pageHeight索引变化了就说明滑出了可视区。切换时调用播放器上下文执行pause()和play()。这种方式实现简单、兼容性好但只能精确到屏级别如果视频卡高度不等于屏幕高度或者页面里有固定头部、底部导航条计算就会失真。第二种是用IntersectionObserver来判定每个卡片与视口的交叉比例。UniApp在微信小程序端和App端都提供了createIntersectionObserver接口。这种方式更灵活能精确判断每个卡片有多少面积露出来createIntersectionObserver(that, { thresholds: [0.6] }) .relativeToViewport() .observe(.feed-item, (res) { if (res.intersectionRatio 0.6) { const targetIndex res.dataset.index; // 这里拿到的是出现在视口比例大于60%的那一项 that.switchByIndex(targetIndex); } });IntersectionObserver的原理是浏览器定期检测目标元素与根元素这里是视口的交叉区域比例超过设定阈值就回调。相比scroll事件它的优势是不会因为滚动事件频繁触发而写一堆防抖逻辑而且语义更清晰——哪个卡片进入视野了就直接处理。缺点是小程序端的thresholds数组在iOS上偶尔会有兼容问题部分基础库版本对多阈值支持不完整还需要降级判断。我最终采用的是方案一的scroll判断为主、方案二作为App端增强验证。实际原因很简单scroll计算在两端表现最一致不用操心基础库版本差异IntersectionObserver在小程序端的坑比想象中多iOS上从底部往上滑时偶尔会出现回调抖动一旦抖动就会导致视频反复暂停和播放体验非常糟。切到新视频时的播放控制也很关键。这里要小心一个坑直接用uni.createVideoContext(videoId)去拿播放器上下文时如果这个video刚被v-if创建出来上下文还没初始化完成调用play()会被静默丢弃。所以我在VideoCard内部做了一层换挡逻辑watch到active变成true后等下一个tick再调用播放watch: { active(newVal) { if (newVal) { this.$nextTick(() { setTimeout(() { this.videoContext uni.createVideoContext(this.videoId, this); this.videoContext.play(); }, 50); }); } }, }这50ms的延时是我在真机上反复调出来的经验值太短拿不到上下文太长视频出现有黑屏等待。不同性能的手机这个值可能略有差异但50ms在绝大多数机型上都能稳定生效。4. 滚动手感之外的细节分页加载与封面预渲染只解决一个视频播放还不够真正让组件像抖音的还有滑动的节奏感和数据的加载效率。这部分比较容易被忽视但直接决定用户体验。分页加载这块我在scroll-view的触底事件scrolltolower里接入加载逻辑onScrollToLower() { if (this.loading || this.noMore) return; this.loading true; this.pageNum; this.fetchVideoList(this.pageNum).then((newList) { this.videoList this.videoList.concat(newList); this.loading false; if (newList.length 0) this.noMore true; }); }这里有一个容易踩的坑如果视频数据本身没有视频尺寸信息新加载进来的视频还没拿到宽高video标签默认会以300x150的尺寸渲染卡片高度就会塌陷导致滚动位置错乱。我拿到的数据结构里每个视频都有宽高比我强制设置了卡片高度为pageHeight视频用object-fit: cover填充避免因为视频比例不同导致的高度跳动。如果你们接口没给尺寸建议在封面上先给一个固定比例占位视频加载后再替换这样后续的scrollTop计算才稳定。封面预渲染也值得说。视频首次播放前会有一段黑屏等待抖音用模糊背景和加载中动画扛过去了但我们既然已经拿到了视频封面图就应该把封面显示出来。我的做法是video标签保留poster属性同时封面图层采用background-image渲染。这里有个体验细节封面图不要用视频首帧截图而应该用接口单独返回的高清封面首帧截图往往偏暗而且在部分手机上截取需要消耗额外性能。另外列表里每张卡片的高度都是pageHeight这个设定看似平常实际上它承担了两个作用一是让滚动计算和可视区判定变得非常直观二是天然实现了抖音那种一格一屏的翻页节奏不需要额外的scroll-snap配置。如果你后续要在卡片上叠加热门区、话题区等不占满全屏的内容可以在卡片内部用position: absolute做自由布局注意这些浮层元素需要设置pointer-events避免挡住video的手势事件。5. 多端适配实战微信小程序和App端最常见的五个坑UniApp的优点是一套代码多端运行但视频组件恰恰是一套代码各有各的脾气的重灾区。我这次同时测了微信小程序端和App端把遇到的典型问题和解决方案整理出来这些都是文档里不容易查到的实战经验。第一个坑微信小程序端video的层级穿透。在小程序里video是原生组件老基础库会把原生组件压在普通组件之上你的点赞按钮、评论按钮全部可能被video挡住点不到。解决办法是开启同层渲染video标签上什么都不用做只要保证微信基础库版本在2.10.0以上同层渲染默认开启。如果你还在用旧基础库那就只能在video区域外放操作按钮或者升级基础库。实测基础库2.32.0上同层渲染稳定按钮能正常浮在video上一层。第二个坑video在scroll-view里快速滚动会出现白屏闪烁。这是微信小程序的老问题video标签在滚动容器中表现不稳定快速滑动时视频画面跟不上滚动速度露出白色背景。我的处理方式有两个一是把视频画面尺寸撑满卡片再用object-fit: cover裁切让画面边缘超出卡片边界滚动时视觉残留会比较少二是在滚动期间暂时给video加一个半透明黑色遮罩等滚动结束后再撤掉。这个遮罩的切换逻辑可以用scrollBegin和scrollEnd事件配合实测能显著降低闪烁感。第三个坑Android端播放器上下文与视频初始化时序。微信小程序里createVideoContext必须在onReady之后调用而Android端有时候video元素还没渲染完成上下文创建之后play()不报错也不生效。我的解决方式是用setTimeout延时播放但在特别老的Android机型上延时不够还需要重试。所以我写了一个重试机制在play()之后监听错误事件如果两秒内没有进入播放状态重新创建上下文再次尝试最多重试三次。这部分代码虽然丑但确实救命。第四个坑App端视频全屏播放后会退出全屏。App端如果在video上加了requestFullScreen的逻辑调起全屏之后再次退出部分系统版本会导致video状态异常、无法自动恢复播放。这个其实和组件无关是UniApp的video组件在全屏接口上的兼容性问题。稳妥的做法是不要主动调起全屏用controls属性让用户自己控制全屏如果一定要代码控制记得在fullscreenchange事件里重建视频上下文。第五个坑视频编码格式的兼容性。这个看着像后端问题其实前端也必须知道。Android上很多手机的WebView支持H.264和VP8/VP9但iOS微信小程序端只支持H.264编码的MP4如果源视频是MOV格式或者HEVC编码播放会直接失败或者只有声音没有画面。我当时的排查经历是用户反馈iOS上黑屏但Android没问题抓包看了半天发现视频源编码是HEVC。最终方案是要求上传端统一转码为H.264 Baseline Profile的MP4音频转成AAC兼容性最好。这个在接口文档里就约定好能少踩很多生产事故。多端调试的时候不要只看微信开发者工具工具正常不代表真机正常。我每次发版前都会用Android低端机内存4G以下和iPhone SE这类小内存设备各测一遍滑动播放链路。低性能设备才是检验资源释放是否到位的标尺。6. 组件封装技巧让调用方只关注数据不关心播放逻辑信息流组件做完不能只在当前页面用早晚要嵌入业务。我把它抽成了一个带完整props和events的独立组件使用方只需要传一个数据源和几个回调函数播放暂停的逻辑全部收在内部。组件对外暴露的接口设计如下props: { videoList: { type: Array, required: true }, pageHeight: { type: Number, required: true }, preloadCount: { type: Number, default: 1 } }, emits: [change, loadmore, videoClick],change事件在currentIndex变化时触发返回当前播放的视频对象方便外部联动标题、评论数、点赞数等UIloadmore在触底时触发通知父组件加载下一页videoClick处理视频卡片的点击行为比如跳转详情页。组件内部再拆分出VideoCard子组件这样有几个好处每个卡片的播放器上下文作用域独立不会互相污染后续要在卡片上叠加评论、打赏、分享等业务入口只需要在VideoCard内部加slot即可不会影响父组件的滚动逻辑。有一点需要特别提醒组件化之后父子组件之间的通信要克制。不要每个子组件都直接操作全局事件或Vuex播放器状态属于高频变化数据频繁走全局总线会影响性能。我让VideoCard自身维护playing状态父组件只下发active标志子组件把自己观察到的播放状态通过update:status事件上报。这样数据流清晰也没有多余的性能开销。7. 收尾调试一套可以抄作业的上线前检查清单组件功能做完不代表能直接上线我每次发布前都会按下面这个清单过一遍。这不是理论推演全是真机踩坑后沉淀下来的项目经验分享出来省得大家再走一遍弯路。检查列表引擎确认pageHeight获取时机不要在onLoad里取要在onReady后取否则手机上偶尔拿到的是不带导航栏的纯窗口高度导致底部露出黑边。确认currentIndex边界初始化一定要设置成0且等第一张卡片渲染完成后再调switchByIndex(0)否则首屏视频不自动播放。确认视频失败回调给video绑定error事件加载失败时给用户一个视频加载失败点击重试的兜底UI不能让黑屏从头挂到尾。确认videoContext.stop()的清理离开页面或切到其他Tab时调用所有已创建上下文的stop()并置空释放播放器资源。确认触底加载与滚动末端的联动抖音在滑动到倒数第二条就会触发预加载下一页我这里的scrolltolower阈值默认是底部50px数据量大的时候建议根据屏幕高度调整这个阈值让加载更早发生。Manifest配置检查App端需要在manifest的App常用其它设置里勾选视频播放相关的权限和模块。如果用了video组件但不打包原生播放器控件controlsfalse有些第三方统计SDK会把它识别成未使用音视频能力但这不影响功能。微信小程序端要确认app.json里没有禁用scroll-view的增强特性真机预览时打开不校验合法域名否则视频CDN域名未配置时视频直接加载不出来。正式发布前一定把视频域名加进小程序的downloadFile合法域名列表注意不是request域名是downloadFile。低端机压力测试连续快速滑动50个视频监控内存是否有持续上涨如果内存回收正常峰值会稳定在一个平台如果一直上涨说明有播放器实例没被释放。iOS设备上连续滑动后返回监听一下音频会话部分系统版本会出现视频暂停但音频还在播的异常。碰到这种情况在生命周期onHide里强制停掉所有播放即可。断网状态下进入列表封面图能显示视频静止在首帧不能出现白屏和卡死用户点击重试后网络恢复要能继续播。我个人做这款组件最大的感受是仿抖音交互的难点根本不在UI还原而在资源管理和时机控制。UI和样式一天就能做好真正需要反复调的是什么时候播、什么时候停、什么时候预加载、什么时候释放这套循环。希望这篇能帮你在做的时候少踩几个坑把精力放在真正有价值的业务逻辑上。本文还有配套的精品资源点击获取
返回列表