witch@&*weaves

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
}

主要是熟悉媒体元素的属性,方法和事件。

属性:

方法:

事件:

总之望文生义,视奏(额,视码?)一样地写完了,写完是写完了但写得太不优雅很傻,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;
}

playpause是事件,也是视频的状态,每当视频暂停或者播放,视频是按钮就会更新内容。

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总结以上可以抽象为三类事件:

  1. 命令事件(Intent)
  1. 状态事件(Truth)
  1. 映射函数(Projection)

新的方法调用方式

另外就是用了我没见过的方法调用法:

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);

#code