如何快速开发一个自定义的视频播放器(7)——原生组件化
原生组件
实际上,所有的前端框架底层都是基于浏览器提供的api实现,所有的浏览器都是基于html、css、ECMAScript三个协议标准进行实现。同系统语言一样,我们的封装性越低,我们的可以移植性越强,这也是许多语言都提供了编译c或者编译汇编等功能的根本原因。本文暂不探讨前端框架的编译方式,仅从原生代码实现来基本阐述我们这个视频播放器是如何实现框架无关性的。
运行时
实际上,原生组件实现也是从html、css、javascript三方面来进行代码组装,基本思路即模仿浏览器实现页面渲染的生命周期:
- 加载css
- 加载html
- 运行javascript
我们可以利用现有vue框架在浏览器中渲染的最终结果,读取我们需要的html和css,只需要我们运行项目然后打开浏览器查看代码即可:
这里我们即可将编译后的css代码整体复制一份出来:
export const cssTxt = `
.video-wrapper-inner {
width: 100%;
position: relative;
width: 7.5rem;
height: 4.2168rem;
}
.video-wrapper-inner .player {
width: 100%;
height: 100%;
}
.video-wrapper-inner .poster {
width: 100%;
height: 100%;
position: absolute;
z-index: 1;
top: 0;
left: 0;
}
.video-wrapper-inner .poster img {
width: 100%;
height: 100%;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 2;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .play-btn-wrapper {
width: 1.2rem;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: 0 auto;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .play-btn-wrapper img {
width: 100%;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel {
width: 100%;
height: 0.68rem;
background: rgba(23, 23, 26, 0.6);
position: absolute;
bottom: 0;
transition: opacity linear 0.2s;
opacity: 0;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper {
width: 100%;
height: 100%;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .mini-play-btn-wrapper {
width: 0.36rem;
height: 0.36rem;
margin: 0 0.24rem;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .mini-play-btn-wrapper img {
width: 100%;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .full-btn-wrapper {
width: 0.36rem;
height: 0.36rem;
margin: 0 0.24rem;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .full-btn-wrapper img {
width: 100%;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .current-time {
width: 0.59rem;
height: 100%;
margin-right: 0.24rem;
font-size: 0.22rem;
color: #fff;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
-ms-flex-flow: row-reverse;
flex-flow: row-reverse;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .duration-time {
width: 0.59rem;
height: 100%;
margin-left: 0.24rem;
font-size: 0.22rem;
color: #fff;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress {
width: 4.39rem;
height: 100%;
position: relative;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress .progress-line {
width: 100%;
height: 0.04rem;
background: #ffffff;
border-radius: 0.02rem;
opacity: 0.4;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
position: absolute;
margin: 0 auto;
z-index: 1;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress .progress-current-line {
height: 0.04rem;
background: linear-gradient(90deg, #2cd3d7 0%, #2acf6f 100%);
border-radius: 0.02rem;
width: 0;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
position: absolute;
z-index: 2;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress .current-indicator-wrapper {
top: 50%;
transform: translateY(-50%) translateX(-0.16rem);
position: absolute;
z-index: 3;
width: 0.32rem;
height: 0.32rem;
transition: left linear 0.03s;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress .current-indicator-wrapper .current-indicator {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #2cd3d7 0%, #2acf6f 100%);
opacity: 0.4;
border-radius: 50%;
}
.video-wrapper-inner .fz-player-controls-panel-wrapper .fz-player-controls-panel .progress-wrapper .progress .current-indicator-wrapper .current-indicator-inner {
width: 0.2rem;
height: 0.2rem;
position: absolute;
top: 50%;
transform: translateY(-50%);
left: 0;
right: 0;
margin: 0 auto;
border-radius: 50%;
background: linear-gradient(90deg, #2cd3d7 0%, #2acf6f 100%);
}`
同理,我们从浏览器中看一下html代码
copy一份代码:
export const videoHtml = `<div class="video-wrapper-inner">
<video src="" id="video" class="player"
style="object-fit:fill" webkit-playsinline="true" playsinline="true" x5-playsinline preload="auto"
poster="https://cdn.cn/2024-03-19/171083686254726059014.jpg-thumb"
x-webkit-airplay="true"></video>
<div class="poster" id="poster">
<img src="https://cdn.cn/2024-03-19/171083686254726059014.jpg-thumb" class="poster-icon">
</div>
<img src="https://cdn.cn/common/loading-icon.gif" id="loadingIcon" class="loading-icon">
<div class="fz-player-controls-panel-wrapper" id="playerPanel">
<a href="javascript:;" class="play-btn-wrapper" id="playBtn">
<img src="https://cdn.cn/common/play-icon.png">
</a>
<div class="fz-player-controls-panel" id="panelControls">
<div class="progress-wrapper">
<a href="javascript:;" class="mini-play-btn-wrapper" id="miniPlayBtn">
<img src="https://cdn.cn/common/play-mini.png?a=1" id='playBtnIcon' />
</a>
<div class="current-time" id="currentTime">00:00</div>
<div class="progress" id="progress">
<div class="progress-line"></div>
<div class="progress-current-line" id="progressCurrentLine"></div>
<div class="current-indicator-wrapper" id="indicator">
<div class="current-indicator"></div>
<div class="current-indicator-inner"></div>
</div>
</div>
<div class="duration-time" id="durationTime"></div>
<a href="javascript:;" class="full-btn-wrapper" id="fullBtn">
<img src="https://cdn.cn/common/full-icon.png" />
</a>
</div>
</div>
</div>
</div>`
以上,我们可以通过运行时的结果获取我们的渲染代码。
动态写入css
简单写一个加载css代码的函数,大概思路即:
1.构造style标签
2.写入css内容
3.将标签加入head标签中
export function LoadCssStr(css) {
const cssDom = document.createElement('style')
cssDom.setAttribute('type', 'text/css')
cssDom.innerHTML = css
document.head.appendChild(cssDom)
}
动态写入html
动态写入html就很简单了,直接设置容器元素的innerHTML即可,注意不要把innerHTML写错哦,后面的HTML都是大写。代码如下:
function LoadHtmlStr(target, html) {
let targetDom = null
if (typeof target === 'string') {
targetDom = document.querySelector(target)
} else if (target instanceof HTMLElement) {
targetDom = target
}
targetDom.innerHTML = html
}
改造视频播放的相关逻辑
视频播放相关的事件逻辑,肯定是在html和css渲染到浏览器之后再做所以我们可以先定下这三个顺序:
// video为容器对象或者容器选择器
export default function LoadVideo(video, config) {
LoadCssStr(cssTxt)
LoadHtmlStr(video, videoHtml)
// 做video初始化
BindVideo('video', config)
}
然后我们详细实现video的初始化,首先同vue组件不同,我们没有this对象,也没有模板中的dom对象,所以我们的一些属性可以转移到video对象上(以防变量污染全局),然后所有dom对象单独抽象出来,一些对象的属性也都提前计算出来,如下:
function BindVideo(videoId, config) {
// 可配置参数
var props = config || {}
var src = props.src
var posterPic = props.poster
if (!src || src === '') {
// 可以提示问题
return
}
// 视频dom对象
var video = document.getElementById(videoId)
video.setAttribute('src', src)
// 进度条容器dom对象
var progress = document.getElementById('progress')
// 封面dom对象
var poster = document.getElementById('poster')
// 加载dom对象
var loadingIcon = document.getElementById('loadingIcon')
// control-panel父容器dom对象
var panelWrapper = document.getElementById('playerPanel')
// control-panel dom对象
var panelControls = document.getElementById('panelControls')
// 左下播放按钮 dom对象
var miniPlayBtn = document.getElementById('miniPlayBtn')
// 播放按钮icon dom对象
var playBtnIcon = document.getElementById('playBtnIcon')
// currentTime dom对象
var currentTime = document.getElementById('currentTime')
// 进度条 dom对象
var progressCurrentLine = document.getElementById('progressCurrentLine')
// 总时长 dom对象
var durationTime = document.getElementById('durationTime')
// 全屏按钮 dom对象
var fullBtn = document.getElementById('fullBtn')
// 大播放按钮 dom对象
var playBtn = document.getElementById('playBtn')
var playBtnSrc = 'https://cdn.cn/common/play-mini.png?a=1'
var pauseBtnSrc = 'https://cdn.cn/common/stop-mini.png?a=1'
// indicator dom对象
var indicator = document.getElementById('indicator')
var progressWidth = progress.offsetWidth
var progressLeft = progress.offsetLeft
var indicatorWidth = indicator.offsetWidth
var indicatorLeft = indicator.offsetLeft
var minx = progressLeft - indicatorWidth / 2
var maxx = progressLeft + progressWidth - indicatorWidth / 2
// video替代this对象
video.video = {
target: null,
currentTime: 0,
currentTimeString: '00:00',
duration: 0,
durationString: '00:00',
status: 'pause',
progress: 0,
starting: false,
showTime: 2000,
showTimer: null,
activeIndicator: false,
showPannel: true,
indicatorStart: minx,
minx,
maxx,
progressWidth,
progressLeft,
indicatorWidth,
indicatorLeft,
offsetCurrentTime: 0,
isSlider: false
}
// ...other code...
}
然后我们的所有方法都可以绑定到video上,以免方法污染全局,并且把之前组件里面的this都替换为video,那么这里仅展示 3 个方法的改造,其他都一样的道理:
video.GetTime = function (duration) {
var minute = ~~(duration / 60)
var second = ~~(duration % 60)
var hour = ~~((duration - second) / 60 / 60)
var ms = minute > 9 ? minute : `0${minute}`
var ss = second > 9 ? second : `0${second}`
var hs = hour > 9 ? hour : `0${hour}`
return hour === 0 ? [ms, ss].join(':') : [hs, ms, ss].join(':')
}
video.Seek = function (e) {
e.preventDefault()
e.stopPropagation()
video.video.target.currentTime =
(e.offsetX / video.video.progressWidth) * video.video.duration
video.play()
}
video.ShowControls = function (time) {
video.video.showTime = time || 2000
video.video.showPannel = true
panelControls.style.opacity = '1'
if (video.video.showTimer) {
clearTimeout(video.video.showTimer)
}
video.video.showTimer = setTimeout(function () {
panelControls.style.opacity = '0'
video.video.showPannel = false
}, video.video.showTime)
}
// ... othercode
以上我们基本的方法改造全部完成,那么如何调用呢,很简单:
// html 我们在html加入这个容器
<section class="video"></section>
// 引入我们封装好的函数,在script中调用
LoadVideo(document.getElementsByClassName('video')[0], {
src:
'https://cdn.cn/dubbing/2018-08-01/1533112489556mdamt929nwz.mp4',
poster:'poster.cn/xxx.jpg'
})
运行一下,发现和我们之前的组件一样的效果,我们如果想加一些自定义的方法监听,都可以通过config传入我们的LoadVideo方法中,然后在方法中添加到video的各个生命周期之中。最后可以使用一些js工具将我们构造好的js文件进行压缩,扔到cdn就可以各个地方进行引用了。
总结
原生组件的封装还是需要我们对html、css和原生javascript的基础有一定程度的了解,实际上就把我们的组件函数化,把html+css都通过字符串的方式动态渲染到html中,然后加载我们的业务逻辑。这个过程中需要注意的是避免变量和方法的全局污染。实际上也是我们所使用的各种框架底层所做的工作。
系列结束
最后,通过这几篇文章我们系统的以“需求分析->模板定义->方法实现->调试->代码优化->组件封装->原生组件化”这个路径详细阐述了自定义的视频播放器从无到有的实现,讲了一些coding技巧和思路。
可以发现只要理好思路,基础扎实,coding可以是一件快乐的事情。
希望能为读者带来一定帮助,感谢大家的阅读。
转载自:https://juejin.cn/post/7398046883637182504