vue3实现移动端音乐播放器中歌词相关功能

1,730 阅读4分钟

歌词滚动和拖动定位

一、简介

一些零基础小白学习vue3的开发记录,目标实现web端音乐播放器。github开发代码,欢迎一起交流学习。

上一篇记录进度条实现的原理

二、相关准备

需求分析

这也是在网易云手机端的功能复制:

  1. 歌曲播放时,显示歌词并随着时间滚动和显示高亮
  2. 拖动歌词界面,显示定位条,可在选择歌词处跳转播放
  3. 切换歌词和专辑封面时,保持歌词的播放进度
  • 第一点需求,解析的每条歌词对应有时间戳和内容,渲染在DOM上后,通过watch函数监听歌曲的currentTime。找到对应的Element后用了一款滚动插件进行滚动,原生实现可以设置translatetransform属性。
  • 第二点需求,单独拿出来时,拖动歌词页面和显示定位条容易实现,歌词的跳转播放即通过点击直接改变当前音频的currentTime。难点是拖动和播放时自动滚动功能的分离。
  • 第三点需求,单独的拿出来分析可以用vue<keep-alive>组件进行状态的缓存。

难点分析

歌词拖动和播放自动滚动,通过watch函数返回的取消监听currentTime的歌词跳动可以实现,这个和之前进度条实现的原理差不多。

但是额外的要求是:查看歌词时可能会不断的进行滚动,想要的效果是不断滚动后,停止的5s后再进行自动滚动。假如每次@touchstartunwatch@touchend时又进行watch,在实现的过程中5s的间隔会然监听乱套。

最后是利用防抖函数,取消@touchend不断的滚动watch的执行,在停止的5s后执行一次。

三、实现过程

歌词原始数据的解析

这里就不放代码了,具体就是用正则匹配各行歌词,然后提取出{time,lyric,trans}即时间、原歌词和翻译三类。用到的正则如下: const regexp = /\[\d{2}:\d{2}.\d{2,3}\]/g;

歌词高亮显示和自动滚动

其中比较重要的是lyricWatcherjumpWatcher,这里把用于跳转的watcher拿出来,可以单独进行控制。其他的功能函数就不展开了。

// ./composables/lyricScroll.js
import { ref, watchEffect } from "vue";
export default function lyricScroll(currentTime, lyricsArr) {
  // 记录播放歌词位置
  let currentLyricRef = null;
  let oldLyricRef = null;

  // 用于歌词滚动
  const scroll = ref(null);

  const jumper = (currentEl = undefined, time = 1000, offsetY = 255) => {
    ···
  };

  // 改变歌词样式
  const styler = (prevEl, currentEl, color = "#fff") => {
    ···
  };

  // 该时间点所处的时间段的index
  const findLyricRef = time => {
    ···
  };

  const lyricWatcher = log => {
    return watchEffect(() => {
      findLyricRef(currentTime.value);
      styler(oldLyricRef, currentLyricRef);
    });
  };

  const jumpWatcher = log => {
    return watchEffect(() => {
      const watch = currentTime.value;
      jumper(currentLyricRef, undefined, undefined);
    });
  };

  return {
    scroll,
    currentLyricRef,
    lyricWatcher,
    jumpWatcher,
    jumper,
    styler
  };
}

拖动歌词和跳转对应歌词播放

这里关键是:

  1. touchstart回调中取消监听以及取消之前已有的防抖
  2. 同时把用到的touchend回调中的操作进行防抖处理

其中防抖函数的取消主要参考了lodash源码中的实现。

// ./composables/lyricTouch.js
import { computed, ref } from "vue";
import { debounce } from "common/utils";
export default function lyricTouch(
  emit,
  jumpWatcher,
  styler,
  lyricsArr,
  verticalOffset
) {
  let isTouch = false;

  let unwatchJump = ref(null);

  // 一些用于触摸滚动的变量
  ···

  const debouncedTouchEnd = debounce(() => {
    unwatchJump.value = jumpWatcher();
    emit("touching", false);
    isTouch = false;
  }, 5000);

  const handleTouchStart = e => {
    isTouch = true;
    emit("touching", true);
    unwatchJump.value();
    // 长按情况,取消歌词跳动
    if (debouncedTouchEnd) {
      debouncedTouchEnd.cancel();
    }
  };

  const handleTouchEnd = e => {
    debouncedTouchEnd();
  };

  const handleTouchMove = pos => {
    // 跳过自动滚动情况
    if (!isTouch) {
      return;
    }
    
  // 以下为改变歌词样式和找到对应时间戳传给定位条
  // 具体不展开
  ···
  };

  return {
    unwatchJump,
    handleTouchStart,
    handleTouchEnd,
    handleTouchMove
  };
}

保持歌词进度

这里就比较简单了,在onActivated回调中每次直接跳转到当前歌词。<Scroll/>是包装插件的组件。 我这里浪费点性能,直接放在onMounted执行一次看着简单点,其实应该在onActivated中开始watch,然后在onDeactivatedunwatch,这样是比较好的。

// player/comps/ContentLyric.vue
<template>
  <Scroll
    ref="scroll"
    class="scroll"
    @touchstart="handleTouchStart"
    @touchend="handleTouchEnd"
    @scroll="handleTouchMove"
  >
    <div class="content-lyric">
      ··· //歌词相关HTML代码
    </div>
  </Scroll>
</template>
<script>
import Scroll from "components/common/scroll/Scroll";

import lyricParse from "./composables/lyricParse";
import lyricScroll from "./composables/lyricScroll";
import lyricTouch from "./composables/lyricTouch";

import {
  defineComponent,
  ref,
  computed,
  onActivated,
  onDeactivated,
  onMounted
} from "vue";
import { useStore } from "vuex";
export default defineComponent({
  name: "ContentLyric",
  components: {
    Scroll
  },
  emits: {
    touching: null,
    scrollTime: null
  },
  setup(props, { emit }) {
    const $store = useStore();

    const currentTime = computed(() => $store.state.currentTime);

    const currentSong = computed(() => $store.state.currentSong);

    /* 歌词解析 */
    let lyricsArr = ref({
      lyric: [], 
      trans: [],
      el: []
    });

    // 获取各行歌词的DOM元素
    const setLyricRefs = el => {
      lyricsArr.value.el.push(el);
    };
    ··· //lyricParse

    /* 滚动监听相关 */
    const verticalOffset = 255;
    ··· //lyricScroll

    /* 歌词滑动相关 */
    ··· //lyricTouch

    /* 执行与回调相关 */
    onMounted(() => {
      parseLyrics(currentSong.value.lyrics);
      const unwatchLyric = lyricWatcher();
      unwatchJump.value = jumpWatcher();
    });

    onActivated(() => {
      jumper(currentLyricRef, 0, undefined);
    });

    return {
      scroll,
      lyricsArr,
      setLyricRefs,
      verticalOffset,
      handleTouchStart,
      handleTouchEnd,
      handleTouchMove
    };
  }
});

四、总结

完整的详细代码放在github

用vue3的组合式api还是很舒服,个人感觉没有this和可以自由组合拆分的特点,可以更好的维护,从vue2到vue3删了很多没有用和重复的代码,感觉很丝滑。

五、参考资料整理