微信小程序封装音乐播放器组件

微信小程序的官方文档中,已经有 audio 组件。


<audio poster="{
      {poster}}" name="{
      {name}}" author="{
      {author}}" src="{
      {src}}" id="myAudio" controls loop>audio>

<button type="primary" bindtap="audioPlay">播放button>
<button type="primary" bindtap="audioPause">暂停button>
<button type="primary" bindtap="audio14">设置当前播放时间为14秒button>
<button type="primary" bindtap="audioStart">回到开头button>
// audio.js
Page({
     
  onReady: function (e) {
     
    // 使用 wx.createAudioContext 获取 audio 上下文 context
    this.audioCtx = wx.createAudioContext('myAudio')
  },
  data: {
     
    poster: 'http://y.gtimg.cn/music/photo_new/T002R300x300M000003rsKF44GyaSk.jpg?max_age=2592000',
    name: '此时此刻',
    author: '许巍',
    src: 'http://ws.stream.qqmusic.qq.com/M500001VfvsJ21xFqb.mp3?guid=ffffffff82def4af4b12b3cd9337d5e7&uin=346897220&vkey=6292F51E1E384E06DCBDC9AB7C49FD713D632D313AC4858BACB8DDD29067D3C601481D36E62053BF8DFEAF74C0A5CCFADD6471160CAF3E6A&fromtag=46',
  },
  audioPlay: function () {
     
    this.audioCtx.play()
  },
  audioPause: function () {
     
    this.audioCtx.pause()
  },
  audio14: function () {
     
    this.audioCtx.seek(14)
  },
  audioStart: function () {
     
    this.audioCtx.seek(0)
  }
})

嗯,该有的大部分功能,其实官方已经都告诉我们了。但是我们凭着不折腾不死心的快落。自己封装一个好看一点点的。

微信小程序封装音乐播放器组件_第1张图片

大概的功能就是点击歌单列表中某一首歌曲,同时传给封装的组件这首歌的歌曲id和位于歌单中 index。

id,通过 id 请求歌曲的详细信息

index,来切换上下歌曲

(既然我们实现的是播放器组件,那么歌单啥的,以及数据id和index如何传入,那就不是我们要关心的事情了)
微信小程序封装音乐播放器组件_第2张图片


理论存在,实践开始

微信小程序封装音乐播放器组件_第3张图片

pages 目录下,新建一个 player

页面样式大致分为三个部分。

歌曲封面信息(就是中间那个大转盘)
微信小程序封装音乐播放器组件_第4张图片

歌曲进度条
在这里插入图片描述

播放(暂停)按钮,前进(后退)按钮

在这里插入图片描述

歌词组件(由于笔者使用的是 github 上开源的 api,这个歌词接口炸了。。。),所以我的歌词组件不能用了。当然在文章中一起解释。如果有歌词的小伙伴就可以正常使用了。

微信小程序封装音乐播放器组件_第5张图片

1. player.wxml

<view class="player-container" style="background:url({
        {picUrl}}) center/cover no-repeat">view>
<view class="player-mask">view>

<view class="player-info">
  
  <view class="player-disc {
      {isPlaying?'play': ''}}" bind:tap="onChangeLyricShow" hidden="{
      {isLyricShow}}">
    <image class="player-img rotation {
      {isPlaying?'':'rotation-paused'}}" src="{
      {picUrl}}">image>
  view>

  <x-lyric class="lyric" isLyricShow="{
      {!isLyricShow}}" bind:tap="onChangeLyricShow" lyric="{
      {lyric}}" />

  <view class="progress-bar">
    <x-progress-bar bind:musicEnd="onNext" bind:timeUpdate="timeUpdate" bind:musicPlay="onPlay" bind:musicPause="onPause" isSame="{
      {isSame}}" />
  view>

  <view class="control">
    <text class="iconfont icon-047caozuo_shangyishou" bind:tap="onPrev">text>
    <text class="iconfont  {
      { isPlaying ? 'icon-icon-' : 'icon-icon-1' }}" bind:tap="togglePlaying">text>
    <text class="iconfont icon-49xiayishou" bind:tap="onNext">text>
  view>
view>

2. player.js

// pages/player/player.js
let musiclist = [] // 音乐列表
let nowPlayingIndex = 0 // 现在播放的歌曲在歌曲列表中的 index,(这个是为了实现切换上一首,下一首)

// 获取全局位移的背景音频管理器
const backgroundAudioManger = wx.getBackgroundAudioManager()

// app 全局对象
const app = getApp()

Page({
     

  /**
   * 页面的初始数据
   */
  data: {
     
    picUrl: '',  // 封面图片(大转盘中间的那个)
    isPlaying: false, // 是否这在播放
    isLyricShow: false, // 歌词是否展示
    lyric: '', // 歌词信息
    isSame: false, // 是否是同一首歌曲
  },

  /**
   * 生命周期函数--监听页面加载
   */
  onLoad: function (options) {
     
    // 获取传入的 index,(一般都是点击歌曲列表中的某一首歌曲进去播放页面的)
    nowPlayingIndex = options.index
    // 从缓存当中读取音乐列表(当然你也可以选择不这么做)
    musiclist = wx.getStorageSync('musiclist')
    // 通过传入歌曲 ID,获取歌曲的详细信息
    this._loadMusicDetail(options.musicId)
  },

  _loadMusicDetail(musicId) {
     
    // 在其他页面保存当前的歌曲 ID,(有点类似于 vuex,redux 的作用)
    if (musicId == app.getPlayMusicId()) {
     
      // 如果是当前正在播放的歌曲
      this.setData({
     
        isSame: true
      })
    } else {
     
      this.setData({
     
        isSame: false
      })
    }
    // 如果不是当前正在播放的歌曲,直接暂停
    if (!this.data.isSame) {
     
      backgroundAudioManger.stop()

    }
    // 获取当前正在播放的音乐
    let music = musiclist[nowPlayingIndex]
    
    // 设置导航栏的 title 为音乐的名称
    wx.setNavigationBarTitle({
     
      title: music.name,
    })
	// 赋值封面图片的信息,设置正在播放为 false
    this.setData({
     
      picUrl: music.al.picUrl,
      isPlaying: false
    })
	// 向 app 这个全局对象的 setPlayMusicId 方法设置 musicId
    app.setPlayMusicId(musicId)
	// 刚进入时,进行 loading 动画加载
    wx.showLoading({
     
      title: '歌曲加载中',
    })
    // 调用 云函数 music,传入 musicId,使用云函数向后端发起请求
    wx.cloud.callFunction({
     
      name: 'music',
      data: {
     
        musicId,
        $url: 'musicUrl',
      }
    }).then((res) => {
     
      // 解析返回数据
      let result = JSON.parse(res.result)
      // 处理异常
      if (result.data[0].url == null) {
     
        wx.showToast({
     
          title: '无权限播放',
        })
        return
      }
      // 对于歌曲不同,进行重新的信息赋值操作
      if (!this.data.isSame) {
     
        // 必填项
        backgroundAudioManger.src = result.data[0].url
        backgroundAudioManger.title = music.name
        // 必填项
        backgroundAudioManger.coverImgUrl = music.al.picUrl
        backgroundAudioManger.singer = music.ar[0].name
        backgroundAudioManger.epname = music.al.name

        // 保存播放历史
        this.savePlayHistory()
      }

      this.setData({
     
        isPlaying: true
      })
      wx.hideLoading()

      // 加载歌词
      wx.cloud.callFunction({
     
        name: 'music',
        data: {
     
          musicId,
          $url: 'lyric',
        }
      }).then((res) => {
     
        let lyric = '暂无歌词'
        const lrc = JSON.parse(res.result).lrc
        if (lrc) {
     
          lyric = lrc.lyric
        }
        // 设置歌词
        this.setData({
     
          lyric
        })
      })
    })
  },
  // 切换播放状态
  togglePlaying() {
     
    // 正在播放,则调用暂停函数
    if (this.data.isPlaying) {
     
      backgroundAudioManger.pause()
    } else {
     
    // 暂停播放,则调用播放函数
      backgroundAudioManger.play()
    }
    this.setData({
     
      isPlaying: !this.data.isPlaying
    })
  },
  // 切换上一首
  onPrev() {
     
    // 相对应的 index 减小
    nowPlayingIndex--
    // 如果 index < 0,证明已经切换到第一首了,下一首是最后一首,实现循环播放
    if (nowPlayingIndex < 0) {
     
      nowPlayingIndex = musiclist.length - 1
    }
    // 调用函数,重新加载歌曲信息
    this._loadMusicDetail(musiclist[nowPlayingIndex].id)
  },
  // 下一首
  onNext() {
     
    // 相对应的 index 增加
    nowPlayingIndex++
    // 如果 index = musiclist.length,证明已经切换到最后一首,下一首是第一首,实现循环播放
    if (nowPlayingIndex === musiclist.length) {
     
      nowPlayingIndex = 0
    }
    // 调用函数,重新加载歌曲信息
    this._loadMusicDetail(musiclist[nowPlayingIndex].id)
  },
  // 决定是展示封面还是歌词组件
  onChangeLyricShow() {
     
    this.setData({
     
      isLyricShow: !this.data.isLyricShow
    })
  },

  // 获取歌词组件,更新当前的时间,实现歌词与进度条的联动
  timeUpdate(event) {
     
    this.selectComponent('.lyric').update(event.detail.currentTime)
  },

  // 设置播放状态
  onPlay() {
     
    this.setData({
     
      isPlaying: true,
    })
  },
  onPause() {
     
    this.setData({
     
      isPlaying: false,
    })
  },

  // 保存播放历史
  savePlayHistory() {
     
    // 当前正在播放的歌曲
    const music = musiclist[nowPlayingIndex]
    // 获取全局对象存储的 openid
    const openid = app.globalData.openid
    // 获取缓存的 history
    const history = wx.getStorageSync(openid)
    let bHave = false
    for (let i = 0, len = history.length; i < len; i++) {
     
      // 遍历历史记录(如果历史当中已存在,则不添加,否则向其中添加一条记录)
      if (history[i].id == music.id) {
     
        bHave = true
        break
      }
    }
    if (!bHave) {
     
      history.unshift(music)
      // 设置缓存
      wx.setStorage({
     
        key: openid,
        data: history,
      })
    }
  }
})

做到这一步,不出意外的话,恭喜你,拥有了一个可以播放音乐的组件,同时可以实现前进,后退,还可以获取播放的历史信息。

3. progress-bar.wxml

进度条组件,使用微信小程序自带的 moveable-area 组件

<view class="container">
  <text class="time">{
    {showTime.currentTime}}text>
    <view class="control">
      <movable-area class="movable-area">
        <movable-view direction="horizontal" class="movable-view"
          damping="1000"
          x="{
      {movableDis}}"
          bindchange="onChange"
          bindtouchend="onTouchEnd"
        />
      movable-area>
      <progress
        stroke-width="4" 
        backgroundColor="#fff"
        activeColor="#d2568c"
        percent="{
      {progress}}"
      >progress>
    view>
  <text class="time">{
    {showTime.totalTime}}text>
view>

4. progress-bar.js

// components/progress-bar/progress-bar.js
let movableAreaWidth = 0
let movableViewWidth = 0

const backgroundAudioManager = wx.getBackgroundAudioManager()

// 当前的秒数
let currentSec = -1
// 进度
let duration = 0
// 是否移动
let isMoving = false

Component({
     
  /**
   * 组件的属性列表
   */
  properties: {
     
    // 是否是同一首(父组件调用此组件时,应该传入数据)
    isSame: Boolean
  },

  /**
   * 组件的初始数据
   */
  data: {
     
    showTime: {
     
      // 当前时间
      currentTime: '00:00',
      // 总共时间
      totalTime: '00:00'
    },
    // 移动距离
    movableDis: 0,
    // 进度
    progress: 0
  },
  // 组件的生命周期,dom 渲染之后执行
  lifetimes: {
     
    ready() {
     
      if (this.properties.isSame && this.data.showTime.totalTime == '00:00') {
     
        // 设置时间
        this._setTime()
      }
      // 获取移动的距离
      this._getMovableDis()
      // 获取背景音乐
      this._bindBGMEvent()
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {
     
    // 当用户拖动进度条的时候触发的函数
    onChange(event) {
     
      if (event.detail.source == 'touch') {
     
        this.data.progress = event.detail.x / (movableAreaWidth - movableViewWidth) * 100
        this.data.movableDis = event.detail.x
        isMoving = true
      }
    },
    // 用户拖动进度条结束时,重新给背景音乐的各项赋值
    onTouchEnd() {
     
      const currentTimeFmt = this._dateFormat(Math.floor(backgroundAudioManager.currentTime))
      this.setData({
     
        progress: this.data.progress,
        movableDis: this.data.movableDis,
        ['showTime.currentTime']: `${
       currentTimeFmt.min} : ${
       currentTimeFmt.sec}`
      })
      // 获取用户拖动进度条对应的时间,然后赋值给这个 API,音乐就可以跳转到相对应的时间
      backgroundAudioManager.seek(duration * this.data.progress / 100)
      isMoving = false
    },
    // 获取移动的距离
    _getMovableDis() {
     
      const query = this.createSelectorQuery()
      // 获取元素的宽度
      // 获取 movable-view,movable-area的宽度
      query.select('.movable-area').boundingClientRect()
      query.select('.movable-view').boundingClientRect()
      query.exec((rect) => {
     
        movableAreaWidth = rect[0].width
        movableViewWidth = rect[1].width
      })
    },
    // 获取背景音乐
    _bindBGMEvent() {
     
      backgroundAudioManager.onPlay(() => {
     
        console.log('onPlay')
        isMoving = false
        this.triggerEvent('musicPlay')
      })

      backgroundAudioManager.onStop(() => {
     
        console.log('onStop')
      })

      backgroundAudioManager.onPause(() => {
     
        console.log('Pause')
        this.triggerEvent('musicPause')
      })

      backgroundAudioManager.onWaiting(() => {
     
        console.log('onWaiting')
      })

      backgroundAudioManager.onCanplay(() => {
     
        console.log('onCanplay')
        console.log(backgroundAudioManager.duration)
        // 音乐播放
        if (typeof backgroundAudioManager.duration != 'undefined') {
     
          this._setTime()
        } else {
     
          setTimeout(() => {
     
            this._setTime()
          }, 1000)
        }
      })

      backgroundAudioManager.onTimeUpdate(() => {
     
        if (!isMoving) {
     
          const currentTime = backgroundAudioManager.currentTime
          const duration = backgroundAudioManager.duration
          // 播放时间优化(progress的进度条)
          const sec = currentTime.toString().split('.')[0]
          if (sec != currentSec) {
     
            const currentTimeFmt = this._dateFormat(currentTime)
            this.setData({
     
              movableDis: (movableAreaWidth - movableViewWidth) * currentTime / duration,
              progress: currentTime / duration * 100,
              ['showTime.currentTime']: `${
       currentTimeFmt.min} : ${
       currentTimeFmt.sec}`
            })
            currentSec = sec
            // 联动歌词
            this.triggerEvent('timeUpdate', {
     
              currentTime
            })
          }
        }
      })
      // 一首歌曲播放完成会触发这个函数,把这个事件抛出给 父组件
      // 告诉父组件这首歌曲已经播放完成
      backgroundAudioManager.onEnded(() => {
     
        console.log("onEnded")
        this.triggerEvent('musicEnd')
      })

      backgroundAudioManager.onError((res) => {
     
        console.error(res.errMsg)
        console.error(res.errCode)
        wx.showToast({
     
          title: '错误:' + res.errCode,
        })
      })
    },

    _setTime() {
     
      // 获取音乐播放的总时长,获取的时间是毫秒级的,需要格式化
      duration = backgroundAudioManager.duration
      // 格式化时间
      const durationFmt = this._dateFormat(duration)
      // 向 data 中的数据进行赋值
      this.setData({
     
        ['showTime.totalTime']: `${
       durationFmt.min}:${
       durationFmt.sec}`
      })
    },
	
    // 格式化时间
    _dateFormat(sec) {
     
      const min = Math.floor(sec / 60)
      sec = Math.floor(sec % 60)
      return {
     
        'min': this._pares0(min),
        'sec': this._pares0(sec)
      }
    },

    _pares0(sec) {
     
      return sec < 10 ? '0' + sec : sec
    }
  }
})

5. lyric.wxml

<scroll-view hidden="{
      {isLyricShow}}" class="lyric-scroll" scroll-y scroll-top="{
      {scrollTop}}" scroll-with-animation="true">
  <view class="lyric-panel">
    <block wx:for="{
      {lrcList}}" wx:key="item">
      <view class="lyric {
      {index==nowLyricIndex?'hightlight-lyric': ''}}">{
    {item.lrc}}view>
    block>
  view>
scroll-view>

6. lyric.js

// components/lyric/lyric.js
let lyricHeight = 0

Component({
     
  /**S
   * 组件的属性列表
   */
  properties: {
     
    // 是否显示歌词,(调用的父组件应该将相应的值传给子组件)
    isLyricShow: {
     
      type: Boolean,
      value: false
    },
    // 歌词信息
    lyric: String
  },

  observers: {
     
    lyric(lrc) {
     
      if (lrc === '暂无歌词') {
     
        this.setData({
     
          lrcList: [
            {
     
              lrc,
              time: 0
            }
          ],
          nowLyricIndex: -1
        })
      } else {
     
        // 解析歌词
        this._parseLyric(lrc)
      }
    }
  },

  /**
   * 组件的初始数据
   */
  data: {
     
    // 歌词列表
    lrcList: [],
    // 播放的歌词函数
    nowLyricIndex: 0,
    // 距离顶部的高度(歌词是需要进行上下滚动的,因此需要一个值,来记录当前距离顶部的值)
    scrollTop: 0
  },

  lifetimes: {
     
    ready() {
     
      wx.getSystemInfo({
     
        success(res) {
     
          lyricHeight = res.screenWidth / 750 * 64
        },
      })
    }
  },

  /**
   * 组件的方法列表
   */
  methods: {
     
    // 与组件之间进行歌词联动
    update(currentTime) {
     
      // 歌词列表
      let lrcList = this.data.lrcList
      if (lrcList.length === 0) {
     
        return
      }
      // 当没有歌词的时候,不高亮任何一句歌词
      if (currentTime > lrcList[lrcList.length - 1].time) {
     
        if (this.data.nowLyricIndex != -1) {
     
          this.setData({
     
            nowLyricIndex: -1,
            scrollTop: lrcList.length * lyricHeight
          })
        }
      }
      for(let i = 0, len = lrcList.length; i < len ; i++) {
     
        if (currentTime <= lrcList[i].time) {
     
          this.setData({
     
            nowLyricIndex: i -1,
            scrollTop: (i - 1) * lyricHeight
          })
          break
        }
      }
    },
    // 歌词解析
    _parseLyric(sLyric) {
     
      let line = sLyric.split('\n')
      let _lrcList = []
      line.forEach((elem) => {
     
        let time = elem.match(/\[(\d{2,}):(\d{2})(?:\.(\d{2,3}))?]/g)
        if (time != null) {
     
          let lrc = elem.split(time)[1]
          let timeReg = time[0].match(/(\d{2,}):(\d{2})(?:\.(\d{2,3}))?/)
          let time2Seconds = parseInt(timeReg[1]) * 60 + parseInt(timeReg[2]) + parseInt(timeReg[3]) / 1000

          _lrcList.push({
     
            lrc,
            time: time2Seconds
          })
        }
      })
      this.setData({
     
        lrcList: _lrcList
      })
    }
  }
})

走到这一步,不出意外的话,你的页面就可以显示出来了。

当然了,前面一直提到的 app 全局对象,需要在根目录下 app.js 中进行定义。

//app.js
App({
     
  onLaunch: function () {
     
    
    if (!wx.cloud) {
     
      console.error('请使用 2.2.3 或以上的基础库以使用云能力')
    } else {
     
      wx.cloud.init({
     
        env: '',
        traceUser: true,
      })
    }

    this.globalData = {
     }
    this.userInfo = {
     }
    this.userMessage = []

    this.getOpenid()

    this.globalData = {
     
      playingMusicId: -1,
      openid: -1
    }
  },

  setPlayMusicId(musicId) {
     
    this.globalData.playingMusicId = musicId
  },

  getPlayMusicId() {
     
    return this.globalData.playingMusicId
  },

  getOpenid() {
     
    wx.cloud.callFunction({
     
      name: 'login'
    }).then((res) => {
     
      console.log('res')
      const openid = res.result.openid
      this.globalData.openid = openid
      if (wx.getStorageSync(openid) == '') {
     
        console.log('执行了')
        wx.setStorageSync(openid, [])
      }
    })
  },
})

这就是播放器组件的全部了。

云函数的内容也很简单了。不过本文不再提及。播放器组件用到的 musicUrl,与 lyric 云函数如下

// 云函数入口文件
const cloud = require('wx-server-sdk')

const TcbRouter = require('tcb-router')

const rp = require('request-promise')

// 这个接口地址是 github 上开源的免费音乐接口(感谢作者的开源,棒棒哒~(。≧3≦)ノ⌒☆)
const BASE_URL = 'http://musicapi.leanapp.cn/'

cloud.init()

// 云函数入口函数
exports.main = async (event, context) => {
     

  const app = new TcbRouter({
     
    event
  })

  app.router('musicUrl', async (ctx, next) => {
     
    console.log(event.musicId)
    ctx.body = await rp(BASE_URL + 'music/url?id=' + parseInt(event.musicId)).then((res) => {
     
      return res
    })
  })

  app.router('lyric', async (ctx, next) => {
     
    console.log(event.musicId)
    ctx.body = await rp(BASE_URL + '/lyric?id=' + parseInt(event.musicId)).then((res) => {
     
      return res
    })
  })

  return app.serve()
}

结束语:

可能你照着上面的代码会运行不出来。这很正常,笔者的这篇文章是项目完成之后大概 8 个月之后,为了面试而总结的。其中的细节可能遗漏了,各位请见谅。需要完整代码的,可以访问我的 gitee,获取详细代码。✔️

你可能感兴趣的:(小程序)