ARTICLE DETAIL

资讯详情

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

Vue中video-player首次加载黑屏?彻底解决props异步传值播放器不播放问题

Vue中video-player首次加载黑屏?彻底解决props异步传值播放器不播放问题 做 Vue 开发的同学估计都跟 video-player 这个视频插件打过交道。它本质上是 Video.js 的 Vue 封装用起来省事一个组件就能把播放器渲染出来。但有个问题几乎每个人都会碰到父组件通过 props 把视频地址传给子组件第一次进入页面播放器死活没有画面控制台也没有明显报错地址看起来也传了可就是不播。我最早被这个坑卡住时排查了大半天试了各种写法最后才把整个链路梳理清楚。问题基本不在 video-player 本身而在组件渲染时序、props 变更监听以及播放器实例方法的调用方式上。这篇文章把我在项目里的完整排查过程和能直接抄的代码写出来给同样被卡的兄弟一个参照。我尽量把这些年踩过的坑、验证过的写法、还有 Vue 2 / Vue 3 的差异都放进来这篇文章读完这类问题基本可以一次解决。1. 问题出现的真实场景与根因分析1.1 第一次监听不到的本质渲染时序错位先复现一下大家最常写的结构。父组件从接口拿视频地址然后传给子组件template div VideoPlayer :video-urlvideoUrl / /div /template script export default { data() { return { videoUrl: } }, mounted() { getVideoDetail().then(res { this.videoUrl res.data.url }) } } /script子组件用 props 接收并在 data 里构造播放器配置template video-player refvideoPlayer :optionsplayerOptions / /template script export default { props: { videoUrl: { type: String, default: } }, data() { return { playerOptions: { sources: [ { src: this.videoUrl, type: video/mp4 } ] } } }, mounted() { console.log(this.videoUrl) // 这里往往是空字符串 } } /script这段代码看起来没问题但实际跑起来第一次进页面就是黑的。原因一句话就能概括父组件接口还没返回时子组件就已经 mounted 并初始化播放器了。props 的初始值就是空字符串播放器 options 里的 sources 自然也是空的。等父组件请求完成后videoUrl 更新了但子组件 data 里的 playerOptions 并不会跟着变已经创建好的播放器也不会主动去拉取新地址。所以“第一次无法监听父组件传过来的值”这个说法并不完全准确。不是 Vue 响应式没有触发 props 变化而是 props 变化触发后没有代码去正确处理播放器实例的更新。如果父组件在渲染子组件时用了 :video-urlvideoUrl然后数据 1 秒后才到位你去看 Vue Devtools子组件 props 其实已经变了但播放器画面就是不动。1.2 另一个隐蔽原因data 里缓存了依赖 props 的初始值还有一类很隐蔽的情况问题出在“data 里拷贝了 props 的初始值”。Vue 的 data 初始化发生在 props 初始化之后所以在 data 里用this.videoUrl确实能拿到初始值但这只是在创建组件时“拍了一张快照”。之后 props 怎么变data 里那个字段都不会自动同步。很多人误以为 props 是响应式的data 里存一份就可以高枕无忧。实际上playerOptions里的 sources 在组件创建后就是一份固定的引用。除非你自己写代码去改它否则它不会跟着 props 刷新。这就是为什么后面 watch 监听后用this.playerOptions.sources[0].src newVal有时候不生效——你改了 sources但是不是真的让 Video.js 重新加载了地址是另一回事。打个比方props 是快递员送来的新包裹data 是门口的信箱。信箱里一开始是空的播放器已经按“空包裹”的状态开始工作了。快递员第二次送包裹过来时信箱没有自动被顶开播放器自然也不知道有新包裹。你需要主动打开信箱把新包裹拿出来再塞给播放器播放器才会重新开工。2. 五种可落地的解决思路先给结论再讲原理2.1 v-if 控制子组件渲染时机最简单粗暴兼容性最好先推荐一个零学习成本的方案父组件在数据到位之前别渲染子组件或者子组件内部把播放器整体用 v-if 包一层。template div video-player v-ifvideoUrl :optionsplayerOptions / /div /template script export default { props: { videoUrl: { type: String, default: } }, computed: { playerOptions() { return { sources: [ { src: this.videoUrl, type: video/mp4 } ] } } } } /script这里的核心变化有两个一是 v-if 保证播放器只在有地址时才创建从根上消除时序问题二是把 playerOptions 从 data 改成 computed这样 props 变化会触发配置重新计算。两个配合起来非常稳。这个写法的优点很突出代码量少、不需要理解 Video.js 的实例方法、也不会遇到 ref 没挂载的坑。缺点也有如果你后续需要切换多个视频地址每次切换都会销毁并重建整个组件会有一瞬间白屏而且浏览器需要重新加载视频。所以它最适合“第一次加载”这种初始化场景。2.2 watch immediate 监听 prop回调里调用播放器实例方法v-if 不是万能的如果需求是要在同一个播放器里动态切换视频就必须用 watch 主动处理。template video-player refvideoPlayer :optionsplayerOptions / /template script export default { props: { videoUrl: { type: String, default: } }, data() { return { playerOptions: { sources: [] } } }, watch: { videoUrl: { handler(newVal) { if (!newVal) return this.$nextTick(() { const player this.$refs.videoPlayer.player player.src({ src: newVal, type: this.getVideoType(newVal) }) player.load() }) }, immediate: true } }, methods: { getVideoType(url) { if (url.includes(.m3u8)) return application/x-mpegURL return video/mp4 } } } /script这段代码里每一行都有讲究。watch 里的 immediate 是为了处理“父组件传值很快子组件 mounted 时已经有值”的情况。很多人在这个坑里栽跟头是因为没写 immediate结果 props 在 mounted 之前就已经赋值完毕watch 监听的时候没触发——这个值和刷新页面时已经缓存过的情形很像最容易忽略。$nextTick 是确保播放器 ref 已经在 DOM 上渲染完成。如果这一步不包经常会在首次触发时拿不到 player报Cannot read property player of undefined。最关键的是player.src()和player.load()这两行。只改 options 里的 sources 而不调播放器实例方法播放器画面永远不会动。这一点我下面会重点再讲一次。2.3 把播放器配置改成 computed避免 data 快照问题把 playerOptions 从 data 挪到 computed本质上是避免“拍快照”问题。如果你只有一个视频地址、且不需要频繁切换computed 配合 v-if 是够用的。但要注意不要把 computed 直接传给 video-player 组件然后指望它自动更新播放源。video-player 内部对 options 的监听并不保证会对 sources 做深度处理很多版本只处理首次初始化。所以更稳的组合是computed 生成配置 watch 主动调用实例方法而不是只用 computed。computed: { playerOptions() { return { controls: true, autoplay: false, muted: false, sources: [ { src: this.videoUrl, type: this.getVideoType(this.videoUrl) } ] } } }这样在模板里用:optionsplayerOptions传给播放器再配合 watch 中的player.src()和player.load()双保险基本不会出问题。2.4 拿到 player 实例后直接操作 src 和 load这个思路是解决“地址变了但画面不变”的核心。你需要明白 video-player 组件的层级关系this.$refs.videoPlayer是 vue-video-player 包装组件的实例它下面有一个player属性这才是 Video.js 的原生实例。调用方法要用下面这个形式const player this.$refs.videoPlayer.player player.src({ src: https://example.com/video.mp4, type: video/mp4 }) player.load()src()负责设置新的播放源load()负责让 Video.js 重新解析并加载这个源。缺了 loadVideo.js 不知道资源已经更换播放器就不会有任何动作。在需要切换视频的业务里这个方法几乎是唯一正解。比如点击侧边栏的视频列表右边播放器要切到对应的地址就可以在列表点击事件里统一调它。2.5 视频列表和多个视频实例的处理思路如果页面上同时渲染多个 video-player用 v-for 渲染时每个播放器的 ref 会变成一个数组。这时候不能直接this.$refs.videoPlayer.player要按索引取template div v-for(item, index) in videoList :keyitem.id video-player :refvideoPlayer${index} :optionsgetPlayerOptions(item.url) / /div /template script export default { methods: { switchVideo(index, url) { const player this.$refs[videoPlayer${index}][0].player player.src({ src: url, type: this.getVideoType(url) }) player.load() } } } /script同时要保证每个播放器的 DOM id 是唯一的不然 Video.js 在多实例场景下会出现串台。我一般会在 data 里生成一个随机后缀data() { return { playerId: player_ Math.random().toString(36).slice(2, 9) } }然后把 playerOptions 里的 id 设置成 playerId。千万别多个播放器共用一个固定的 id。五种方案我整理成一个表方便对照方案实现复杂度适用场景推荐度v-if 控制渲染低首次加载、数据异步到达高watch immediate src/load中需要频繁切换视频地址高computed 生成配置低简单场景配合 v-if 使用中ref 直接操作 player 实例中列表切换、自定义交互中v-for 多实例渲染较高同时展示多个视频按需求3. 实操过程从复现到修通的完整记录3.1 搭建最小可复现项目我建议你在解决现有项目问题之前先搭一个最小可复现的 demo把问题链路跑通。这样既方便验证方案也不会把业务代码搞乱。以 Vue 2 为例安装依赖npm install vue-video-player video.jsmain.js 里引入import Vue from vue import VideoPlayer from vue-video-player import video.js/dist/video-js.css import vue-video-player/src/custom-theme.css Vue.use(VideoPlayer)注意 vue-video-player 这个包主要支持 Vue 2。如果你用的是 Vue 3要换成 video-player-next别装错了包不然组件注册成功后模板却渲染不出来。项目目录可以这样组织src/ views/ Parent.vue ChildPlayer.vueParent 模拟接口异步返回ChildPlayer 接收 props 并渲染播放器。3.2 第一次尝试在 mounted 里直接读 propsParent.vue 里模拟接口延迟 2 秒返回template div ChildPlayer :video-urlvideoUrl / /div /template script import ChildPlayer from ./ChildPlayer.vue export default { components: { ChildPlayer }, data() { return { videoUrl: } }, mounted() { setTimeout(() { this.videoUrl https://www.w3schools.com/html/mov_bbb.mp4 }, 2000) } } /scriptChildPlayer.vue 里在 mounted 时打印 propstemplate video-player refvideoPlayer :optionsplayerOptions / /template script export default { props: { videoUrl: { type: String, default: } }, data() { return { playerOptions: { sources: [ { src: this.videoUrl, type: video/mp4 } ] } } }, mounted() { console.log(mounted videoUrl:, this.videoUrl) } } /script运行后控制台会先打印空字符串2 秒后 props 更新但播放器一片黑。到这里问题就稳定复现了。3.3 第一次修复尝试只加 watch我在第一次踩坑时想到的自然是加 watch 监听script export default { watch: { videoUrl(val) { console.log(watch videoUrl:, val) this.playerOptions.sources[0].src val } } } /script控制台里 watch 确实打印了正确的地址但播放器依然没有画面。原因就是前面说的直接改 data 里的 playerOptions并不等于让已经创建的播放器去加载新地址。sources 变了Video.js 内部没有感知到这个变化。这一步非常关键能让你在以后遇到同类问题时少走很多弯路。监视到值只是第一步把值真正交给播放器实例才是目的。3.4 正确的修复拿到 player 实例src 之后 load修改 watch 回调script export default { watch: { videoUrl: { handler(newVal) { if (!newVal) return this.$nextTick(() { const player this.$refs.videoPlayer.player player.src({ src: newVal, type: this.getVideoType(newVal) }) player.load() }) }, immediate: true } }, methods: { getVideoType(url) { if (url.includes(.m3u8)) { return application/x-mpegURL } return video/mp4 } } } /script这次再运行2 秒后播放器就开始加载视频了。排查完成后我建议把 immediate 也加上。因为某些场景下父组件可能同步传值子组件 mounted 时值已经存在没有 immediate 的话第一次渲染仍然拿不到地址。如果你还需要自动播放可以再加一行player.play()。但要注意浏览器的自动播放策略直接 play 可能会被拦截稳妥的做法是先设置 muted监听用户点击后再取消静音。视频的自动播放和静音策略是所有浏览器通用的限制不是 video-player 特有的问题。3.5 m3u8 直播流和点播流的情况很多项目里播放的不是 mp4而是 m3u8 流。这时候需要在项目里引入 hls 插件npm install videojs-contrib-hls然后在组件或 main.js 里引入import videojs-contrib-hls播放 m3u8 时的 type 是application/x-mpegURL。如果不引入这个插件播放器会直接报错或者黑屏不播。我在项目里会用封装函数统一处理类型getVideoType(url) { if (url.includes(.m3u8)) { return application/x-mpegURL } if (url.includes(.flv)) { return video/x-flv } return video/mp4 }切换直播流的地址时同样走player.src()player.load()的链路。3.6 Vue 3 下的版本适配说明如果你在用 Vue 3直接装 vue-video-player 会容易出问题。这个包和 Vue 3 的兼容性并不好推荐用 video-player-next它同样支持 Video.jsAPI 设计也很接近。Vue 3 版本里获取播放器实例的方式稍有不同但思路一致script setup import { ref, watch } from vue import { VideoPlayer } from video-player-next const videoPlayerRef ref(null) const videoUrl ref() watch(videoUrl, (newVal) { if (!newVal) return const player videoPlayerRef.value.player player.src({ src: newVal, type: newVal.includes(.m3u8) ? application/x-mpegURL : video/mp4 }) player.load() }) /script核心还是拿到 player 实例调用 src 和 load。底层原理只要理解了换哪个版本都不慌。4. 常见问题与排查技巧实录4.1 地址变了但画面没有更新现象控制台打印 props 已经变了options 里 sources 也变了播放器黑屏。原因没有调用player.load()或者只改了 data 里的配置没调用player.src()。排查路径在 watch 回调里加一行console.log(this.$refs.videoPlayer.player)能拿到 player 实例继续看player.src()之后的反应。如果 player 为 undefined说明 $refs 还没挂载需要等 nextTick。处理方式前面已经给过这里只强调一句话src 设置新地址后必须立即调用 load。这两行代码就是视频地址切换的最后一公里。4.2 watch 触发了但 player 是 undefined现象watch 回调里访问this.$refs.videoPlayer.player报错。原因watch 触发时组件还没有渲染完成尤其是父子组件同时挂载的情况下子组件的 ref 还没赋值。处理方式用$nextTick包一层或者判断 ref 是否存在handler(newVal) { if (!newVal) return this.$nextTick(() { if (this.$refs.videoPlayer this.$refs.videoPlayer.player) { const player this.$refs.videoPlayer.player player.src({ src: newVal, type: video/mp4 }) player.load() } }) }另外v-for 渲染的播放器 ref 是数组取的时候要带索引比如this.$refs.videoPlayer[0]或者给每个播放器绑定不同的 ref。4.3 多个视频实例互相干扰只播同一个地址现象几个播放器同时存在点一个播放其他的也跟着切地址。原因所有播放器共用了同一个 options 对象或者播放器 DOM id 重复。排查技巧打开控制台查看生成的 video 元素 id如果多个播放器的 id 一样马上就能发现。处理方式每个视频单独生成配置id 用唯一值。getPlayerOptions(url) { return { id: player_ this.uid _ url.length, sources: [{ src: url, type: this.getVideoType(url) }] } }4.4 组件销毁时播放器没有释放现象页面反复进入退出内存占用越来越高切换路由后再回来播放器卡顿甚至连接数爆掉。原因播放器实例没有销毁Video.js 创建的 DOM 和事件监听残留。处理方式在组件销毁前手动释放beforeDestroy() { if (this.$refs.videoPlayer this.$refs.videoPlayer.player) { this.$refs.videoPlayer.player.dispose() } }Vue 3 里对应的是 onBeforeUnmount。这个细节很容易被忽略但做视频类的页面一定要养成分散资源的习惯。4.5 自动播放被浏览器拦截现象配置了 autoplay: true但还是停在第一帧需要手动点一下才播。原因浏览器自动播放策略限制带声音的视频不能被自动播放服务端也没做相关策略。处理方式先静音播放再用用户交互事件取消静音const player this.$refs.videoPlayer.player player.muted(true) player.play() // 用户点击播放按钮后 player.muted(false)4.6 父组件传入的是对象watch 监听不到如果父组件传的不是字符串地址而是整个视频对象ChildPlayer :video-infovideoInfo /子组件里需要深度监听或者直接监听对象里的具体字段watch: { videoInfo.url: { handler(newVal) { // ... } } }如果 videoInfo 整个对象被替换用deep: true也没问题但要注意 observe 一个复杂对象有额外开销。能监听到具体字段就不要整对象深度监听。我在实际项目里的体会是凡是“第一次加载播放不了”的优先级最高的就是检查时序看看是不是播放器在数据到位之前就初始化了凡是“后面切换地址播放不了”的第一反应检查有没有调用 load。把这两条刻在脑子里这个系列的问题基本就是一次性的了。最后再分享一个小技巧开发时把 player 实例挂到 window 上调试起来会方便很多const player this.$refs.videoPlayer.player window.currentPlayer player这样在任何地方打开控制台都能直接调currentPlayer.src()、currentPlayer.load()、currentPlayer.play()来验证方案比反复改代码热更新要快得多。
返回列表