Thinking系列,旨在利用10分钟的时间传达一种可落地的编程思想。
为减少页面包体积,videojs
相关资源,动态追加到页面。
使用 vue 封装组件 <VideoPlayer>
<template>
<video ref="videoPlayer" style="width: 100%; height: 100%" class="video-js vjs-big-play-centered" />
</template>
<script>
export default {
name: 'VideoPlayer',
data () {
return {
player: null
}
},
methods: {
// 获取资源
loadResources() {
if (window.videojs) return Promise.resolve();
return Promise.all([
// 见下述,被封装成了Promise
loadScript('//xxx/js/video-umdV7.18.1-711dd1be5e.js'),
loadScript('//xxx/js/videojs-ccs-umdV7.18.1-59190bdaf5.css')
])
},
// 初始化
initVideoPlayer() {
// 需要依赖 videojs 构造函数
this.player = window.videojs(this.$refs.videoPlayer, {...}, () => {});
}
},
async mounted() {
await this.loadResources();
this.initVideoPlayer();
},
beforeDestroy() {
this.player && this.player.dispose();
},
}
</script>
调用方:
🐬单个组件调用:运转良好
<VideoPlayer v-bind="$attrs"></VideoPlayer>
🐋 循环调用:展示没啥问题,但加载了多份
<div v-for="videoItem in list" :key="videoItem.key">
<VideoPlayer v-bind="$attrs"></VideoPlayer>
</div>
⚠️ 问题出现了:js 和 css 都被加载了多份(list.length
)!
🐾 原因分析:在 loadResources()
中,已经追加了前置判断 if (window.videojs) return Promise.resolve();
来避免重复资源的加载,从而达到只加载一份的目的。没有生效❓
video.js
资源加载&解析执行完,会在window
上挂载videojs
属性。
循环过程会产生多个 <VideoPlayer>
组件实例,进而产生多个获取资源 loadResources()
的调用。加载资源是异步过程,window.videojs
资源加载完才会追加。
⚠️注意:在 Chrome 浏览器下,只会发送一个。怀疑 Chrome 做了对应的缓存机制或脚本去重机制,欢迎大家补充&指正~~~
🐬 解决思路:
- 将实例创建改成串行,确保第二个初始化时,第一个已经处理完毕(存在),这样
window.videojs
的判断可以生效。=> 可以解决,但不是最佳方案,整体页面渲染效率下降 - 初始化多个
<VideoPlayer>
实例(并行加载),确保资源只加载一份。=> ⭐ 资源全局单例
// 借助window实现单例
window.resourceLoader = {
loadingPromise: null,
loadResources: function () {
if (!window.resourceLoader.loadingPromise) {
window.resourceLoader.loadingPromise = new Promise((resolve, reject) => {
Promise.all([
// 见下述,被封装成了Promise
loadScript('//xxx/js/video-umdV7.18.1-711dd1be5e.js'),
loadScript('//xxx/js/videojs-ccs-umdV7.18.1-59190bdaf5.css')
]).then(() => {
resolve();
});
});
}
return window.resourceLoader.loadingPromise;
},
};
export default {
name: 'VideoPlayer',
data () {
return {
player: null
}
},
methods: {
// 获取资源(这里使用window)
loadResources() {
return window.resourceLoader.loadResources();
},
// 初始化
initVideoPlayer() {
// 需要依赖 videojs 构造函数
this.player = window.videojs(this.$refs.videoPlayer, {...}, () => {});
}
},
async mounted() {
await this.loadResources();
this.initVideoPlayer();
},
beforeDestroy() {
this.player && this.player.dispose();
},
}
🌲 window 单例,确保了全局 window.resourceLoader.loadingPromise
的唯一性!
🌲 loadingPromise
:资源加载管理标志,以确保资源只被加载一次。
- 如果
loadingPromise
为null
,则意味着资源尚未加载过,加载资源,返回loadingPromise
; - 否则,返回现有的
loadingPromise
,防止冗余加载。
附:body 追加资源
function loadScript(url) {
// 已加载直接返回
if (loadScript[url]) return Promise.resolve();
return new Promise((resolve, reject) => {
let dom;
if (url.endsWith('.js')) { // js资源
dom = document.createElement('script');
dom.type = 'text/javascript';
dom.src = url;
} else { // css资源
dom = document.createElement('link');
dom.setAttribute('rel', 'stylesheet');
dom.setAttribute('type', 'text/css');
dom.setAttribute('href', url);
}
dom.setAttribute('ignore', 'true');
document.head.appendChild(dom);
dom.onload = function () {
loadScript[url] = true;
resolve();
};
dom.onerror = reject;
});
}
🌴 缓存策略:通过将加载过的脚本URL作为对象的属性记录在loadScript
上,实现加载过的资源复用,减少不必要的网络请求。
🌴 动态元素创建:根据URL后缀判断资源类型,如果是.js
文件,则创建<script>
标签;如果是其他类型(假设为CSS),则创建<link>
标签。
🌴 资源加载与事件监听:为新创建的DOM元素添加 onload
和 onerror
事件处理器,分别在资源成功加载和加载失败时调用 resolve
或 reject
,从而改变 Promise 状态,通知调用者加载结果。