JSX + Vue3 + Vue3 实现音乐播放器核心

673 阅读3分钟

整体效果

仓库地址

前言

本篇我们将实现Mini播放器。应该是最后一篇。整篇下来其实没什么技术难点,技术太浅且毫无新意。赶紧结束掉吧。React Native开发实在。就像上篇所说,快速且无成本切换框架是我们使用JSX的理由,同时扩展我们的思路、仅此而已。

实现功能

完整Mini和全屏播放器功能。如: 特效、拖动、播放/暂停、歌词解析滚动后面补上。

  • Vant3组件使用。例: Popup、Slider、Circle。
  • Vue3特性使用。例: Vuex、reactive、refs、computed
  • JSX基本操作

前菜: 掌握JSX基础语法

  • 替换v-model语法
// template
<van-circle  v-model:current-rate="currentRate" />

// jsx
<van-circle  v-model={[state.currentRate, 'current-rate']} />
  • 替换v-for语法
/* key尽量使用item.id。迫不得已使用index */
const list = [1,2,3]

<li v-for="item in [1,2,3]" :key="item">{{item}}</li>

// jsx
{
  list.map(item => (
    <li key={item}>{item}</li>
  ))
}
  • 替换v-if跟v-else语法
const flag = true;

/* v-if */

<div v-if="flag" /> 

// jsx
{ flag && <div /> }

/* v-else */

<div v-if="flag" />
<div v-else />

// jsx
{
  flag ? <div>真</div> : <div>假</div>
}
  • 替换@click等事件
<div @click="handleClick" />
<div @change="handleChange">

// jsx
<div onClick={handleClick} />
<div onChange={handleChange}>
  • 替换插槽
<van-nav-bar title="标题" left-text="返回" left-arrow>
  <template #right>
    <van-icon name="search" size="18" />
  </template>
</van-nav-bar>

// jsx
<van-nav-bar title="标题" left-text="返回" left-arrow 
	v-slots={{ 'left': () => <van-icon name="search" size="18" /> }}
/>

第一部: Mini播放器的变量和事件

效果图: 效果图

  • 变量和事件代码(setup)
import { reactive, ref, onMounted, computed } from 'vue'
// 使用vuex
import { useStore }  from 'vuex';

const Player = ({

  // setup没有this
  setup() {
  	// 获得audio refs
  	const audioRef = ref(null);
        // 使用vuex
        const store = useStore();
        // 实时监控isMusicPlay状态
        const isMusicPlay = computed(() => {
            return store.state.isMusicPlay
        });
        // 本地状态
        const state = reactive({
            rate: 0, // 播放进度
            timer: null, // 计时器
            allSec: '00:00', // 总时长
            curSec: '00:00', // 正在时长
            fullScreenShow: false, // 全屏
        })
        /* 基础事件 */
        
        // 处理播放/暂停点击
        const handlePlayClick = () => {
            // vuex更改状态
            store.commit('handleChangeIsMusicPlay', !isMusicPlay.value);
            isMusicPlay.value ? audioRef.value.play() : audioRef.value.pause() ;
            audioListener();
            state.allSec = secondIntoMin(state.audioDuration);
        };
        // 秒转分秒
        const secondIntoMin = (SECONDS) => {
            let allMin = Math.floor(SECONDS / 60);
            let allSec = Math.floor(SECONDS) - allMin * 60; 
            allMin = allMin >= 10 ? allMin : '0' + allMin ;
            allSec = allSec >= 10 ? allSec : '0' + allSec;
            return `${allMin}:${allSec}`
        };
        // 监听音乐播放
        const audioListener = () => {
            if (isMusicPlay.value) {
                state.timer = setInterval(() => {
                    try {
                        state.currentTime = audioRef.value.currentTime;
                        const rate = parseInt(audioRef.value.currentTime / audioRef.value.duration * 100);
                        state.rate = rate;
                        state.curSec = secondIntoMin(state.currentTime);
                        if (rate === 100) {
                            // 播放完毕复原状态、等待下次播放
                            clearInterval(state.timer);
                            state.rate = 0;
                            state.curSec = '00:00';
                            store.commit('handleChangeIsMusicPlay', false);
                        }
                    } catch (e) {
                        clearInterval(state.timer);
                    }
                }, 1000)
            } else {
                clearInterval(state.timer);
            }
        };
        // 生命周期
        onMounted(() => {
            // 获得audio元素
            audioRef.value.load();
            audioRef.value.oncanplay = () => {
                state.audioDuration = audioRef.value.duration;
            };
        });
        // 最终的渲染
        return () => (
       		 {renderMiniPlay()}
               	 {renderFullScreenPlayer()}
                 <audio id="audio" src={testMp3} ref={audioRef}/>
                 // 第二步代码内容
        )
  }
})

第二步: Mini音乐播放器渲染(Render)

  • 渲染代码(render)

    // 渲染圆形进度条, 使用Circle组件。其中v-model:current-rate转jsx需要注意语法
    const renderCircle = () => {
    	// 图标插槽。通过播放/暂停状态切换图标
               const renderWhichPlayStatus = isMusicPlay.value ?
                  <van-icon onClick={handlePlayClick} color='#d44439' name="pause" size="22"/>
                  : <van-icon onClick={handlePlayClick} color='#d44439' name="play" size="22"/>;
              // 逻辑处理完再return
              return (
                  <van-circle
                      v-model={[state.currentRate, 'current-rate']}
                      rate={state.rate} layer-color="#ebedf0"
                      size="32"
                      color="#d44439"
                      stroke-width="60"
                      v-slots={{
                          'default': () => renderWhichPlayStatus
                      }}
                  />
              )
          };
    // 渲染Mini播放器
    const renderMiniPlay = () => (
              <div className='miniPlay'>
                // CD图片: 点击切换全屏播放器
                  <div className="icon" onClick={() => state.fullScreenShow = true}>
                      <div className="imgWrapper">
                          <img className={ isMusicPlay.value ? 'play' : 'play pause'}
                               src="http://p4.music.126.net/FJWZe1aQV2-iuYeq8gUR5A==/19022650672277889.jpg"
                               width="40" height="40" alt="img"/>
                      </div>
                  </div>
                  // 文字说明
                  <div className="text">
                      <div className="name">Out of Love</div>
                      <div className="desc">Peter Manos</div>
                  </div>
                 
                  <div className='console'>
                      // 进度条: 点击图标播放/暂停
                      <div>
                          {renderCircle()}
                      </div>
                      // 控制器: 专辑弹窗
                      <div>
                          <van-icon name="wap-nav" size="29" color="#d44439" onClick={() => {
                              state.actionsSheetShow = true
                          }}/>
                      </div>
                  </div>
              </div>
          );
    
    

第三步: 样式

// 公共变量
$bgColor: #f2f3f4;
$fontColor: #2E3030;

// 增加旋转效果
@keyframes rotate {
  0% {
    transform: rotate(0);
  }
  100%{
    transform: rotate(360deg);
  }
}
.miniPlay {
  display: flex;
  align-items: center;
  width: 100%;
  height: 60px;
  background: #fff;
  padding: 0 10px 0 20px;
  justify-content: space-between;
  .console{
    display: flex;
    align-items: center;
    div:first-child{
      margin-right: 6px;
    }
  }
  .icon {
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    img{
      border-radius: 50%;
      &.play {
        animation: rotate 10s ease-in infinite;
      }
      &.pause {
        animation-play-state: paused;
      }
    }
  }
  .text{
    margin-left: 10px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    flex: 1;
    line-height: 20px;
    overflow: hidden;
    .name{
      margin-bottom: 2px;
      font-size: 14px;
      color: #2E3030;
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
    }
    .desc {
      font-size: 12px;
      color: #bba8a8;
      text-overflow: ellipsis;
      overflow: hidden;
      white-space: nowrap;
    }
  }
  .van-circle{
    display: flex;
    justify-content: center;
    align-items: center;
  }
}