30daysJS-11 - Custom Video Player
看了眼要做什么就自己先手搓了一遍:
const player = document.querySelector(".player");
const video = player.querySelector(".viewer");
const progress = player.querySelector(".progress");
const progressBar = player.querySelector(".progress__filled");
const toggle = player.querySelector(".toggle");
const skipButtons = player.querySelectorAll("[data-skip]");
const ranges = player.querySelectorAll(".player__slider");
toggle.addEventListener("click", () => {
if (toggle.textContent === "►") {
toggle.textContent = "||";
video.play();
} else if (toggle.textContent === "||") {
toggle.textContent = "►";
video.pause();
}
});
ranges.forEach((range) => {
range.addEventListener("change", (e) => {
let valueChange = e.target.value;
if (e.target.name === "volume") {
video.volume = valueChange;
} else if (e.target.name === "playbackRate") {
video.playbackRate = valueChange;
}
});
});
skipButtons.forEach(
button =>{
button.addEventListener(
'click',()=>{
let timer = video.currentTime ? video.currentTime: 0
video.currentTime = timer + Number(button.dataset.skip)
if(video.currentTime <= 0){
video.currentTime = 0
}
if(video.currentTime > video.duration ){
video.currentTime = video.duration
}
let timeProgress = (video.currentTime / video.duration )* 100
progressBar.style.flexBasis = `${timeProgress}`+'%'
}
)
}
)
video.addEventListener('timeupdate',()=>{
let timeProgress = (video.currentTime / video.duration )* 100
progressBar.style.flexBasis = `${timeProgress}`+'%'
})
progress.addEventListener("click",TimeBarProgress)
progress.addEventListener("dragend",TimeBarProgress)
function TimeBarProgress(e){
const offsetX = e.offsetX;
let timeProgress = (offsetX / progress.clientWidth)
progressBar.style.flexBasis = `${timeProgress * 100}`+'%'
video.currentTime = video.duration * timeProgress
}
主要是熟悉媒体元素的属性,方法和事件。
属性:
volume控制声音,从0-1currentTime现在播放的时间,以秒为单位duration媒体总长度,以秒为单位playbackRate播放速率,正常播放速率乘以该值表示当前的播放速率,1.0是正常速率,1 × 1是1所以正常播放速率为1.
方法:
.play().pause()
事件:
timeupdate当currentTime更新时会触发此事件。
总之望文生义,视奏(额,视码?)一样地写完了,写完是写完了但写得太不优雅很傻,dom操作和状态控制混在一起,还写了个莫名其妙的drag事件,很有能跑就行的味,看了眼范例感觉差太多了,于是思考了一下。
思考
| 操作 | dom元素 | 事件 |
|---|---|---|
| 播放,暂停 | player__button | click |
| 音量调节 | player__slider | change |
| 速率调节 | player__slider | change |
| 播放进度调整:前进与后退按钮 | player__button | click |
| 播放进度调整:progress样式调整 | progress | click |
唯一真相源
标准答案里使用的是视频状态驱动的方式,视频元素 (HTMLVideoElement) 是唯一的状态源和真相来源,所有UI组件的状态都是视频元素状态的映射,最明显的是两个按钮的更新。
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
function updateButton() {
const icon = this.paused ? '►' : '❚ ❚';
toggle.textContent = icon;
}
play和pause是事件,也是视频的状态,每当视频暂停或者播放,视频是按钮就会更新内容。
video.addEventListener('timeupdate', handleProgress);
function handleRangeUpdate() {
video[this.name] = this.value;
}
timeupdate是事件,当currentTime更新时会触发timeupdate事件,currentTime是视频的状态,所以也是状态改变事件自动触发,它们都不是因为用户操控事件改变的。
所以,其实用户操控控件本质是修改视频的属性值,是告诉浏览器,而不是重建,而根据视频状态添加的事件,便会随着事件被自动调用。
标准答案里还加入了根据鼠标位置来控制进度条状态的事件:
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
事件中设定点击传入e.offsetX数值,初始化scrubTime,修改video.currentTime,所以能够自动触发timeupdate事件函数,更进一步的是,将mouse事件也加入了考虑,鼠标移动时触发事件,点下时也触发事件,但松开时事件不被触发,通过布尔变量mousedown来作为开关,每次重新赋值以确定mousemove移动的起点和终点。
抽象
gpt总结以上可以抽象为三类事件:
- 命令事件(Intent)
- click
- mousedown / mousemove 用户表达“想要什么”
- 状态事件(Truth)
- play
- pause
- timeupdate
- 映射函数(Projection)
- updateButton
- handleProgress
新的方法调用方式
另外就是用了我没见过的方法调用法:
video[this.name] = this.value;
video[method]()
这个方法就避免了if的判断,直接执行了语句。
老师的答案:
/* Get Our Elements */
const player = document.querySelector('.player');
const video = player.querySelector('.viewer');
const progress = player.querySelector('.progress');
const progressBar = player.querySelector('.progress__filled');
const toggle = player.querySelector('.toggle');
const skipButtons = player.querySelectorAll('[data-skip]');
const ranges = player.querySelectorAll('.player__slider');
/* Build out functions */
function togglePlay() {
const method = video.paused ? 'play' : 'pause';
video[method]();
}
function updateButton() {
const icon = this.paused ? '►' : '❚ ❚';
console.log(icon);
toggle.textContent = icon;
}
function skip() {
video.currentTime += parseFloat(this.dataset.skip);
}
function handleRangeUpdate() {
video[this.name] = this.value;
}
function handleProgress() {
const percent = (video.currentTime / video.duration) * 100;
progressBar.style.flexBasis = `${percent}%`;
}
function scrub(e) {
const scrubTime = (e.offsetX / progress.offsetWidth) * video.duration;
video.currentTime = scrubTime;
}
/* Hook up the event listeners */
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
video.addEventListener('timeupdate', handleProgress);
toggle.addEventListener('click', togglePlay);
skipButtons.forEach(button => button.addEventListener('click', skip));
ranges.forEach(range => range.addEventListener('change', handleRangeUpdate));
ranges.forEach(range => range.addEventListener('mousemove', handleRangeUpdate));
let mousedown = false;
progress.addEventListener('click', scrub);
progress.addEventListener('mousemove', (e) => mousedown && scrub(e));
progress.addEventListener('mousedown', () => mousedown = true);
progress.addEventListener('mouseup', () => mousedown = false);