uni-app x跨平台开发实战:爱影家免费观影APP音乐播放功能实现详解

2026-03-04 09:56:58
107次阅读
0个评论

在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的“爱影家”影视app项目,找个跨多端的技术栈再玩一把。免费听歌看电影,一次性的支持android、IOS、harmonyOS、Web和小程序等所有平台。

1. 项目概述

爱影家是一款基于uni-app x框架开发的跨平台影视音乐客户端,其中音乐播放功能是其重要组成部分。本文将详细介绍音乐播放功能的实现原理、核心技术和关键代码。

注:该项目仅用于学习研究,严禁用于其用途!

该项目的开源地址https://gitcode.com/qq8864/uniappx_imovie

在这里插入图片描述

2. 音乐播放功能架构

2.1 核心模块

音乐播放功能主要由以下几个模块组成:

  • API模块:负责与后端服务器交互,获取音乐数据
  • 播放器模块:负责音乐播放控制、进度管理和UI展示
  • 播放列表模块:负责管理播放队列
  • 数据存储模块:负责音乐相关数据的存储和管理

2.2 数据流

  1. 用户从歌单列表选择歌曲
  2. 歌曲信息被添加到播放列表
  3. 跳转到播放器页面
  4. 播放器加载歌曲资源并开始播放
  5. 播放器实时更新播放状态和歌词

3. 核心功能实现

3.1 音乐API接口

音乐API模块封装了与后端服务器的交互,提供了获取每日推荐、歌单列表和歌词的功能。

// api/music.uts
export class MusicApi {
  static async getDailyRecommend(start: number, count: number): Promise<MusicMenusResult> {
    const body: any = { count: count, kind: 'topWyMusic', start: start } as any
    const res = await request.post<any>('/musicmenus', body, null)
    const full: any = res as any
    return {
      list: full.data as MusicItem[],
      total: full.total as number,
      title: full.title as string
    } as MusicMenusResult
  }

  static async getSongMenus(start: number, count: number): Promise<SongMenuResult> {
    const params: any = { start: start, count: count } as any
    const res = await request.get<any>('/getsongmenu', params, null)
    const full: any = res as any
    return {
      list: full.data as SongMenu[],
      total: full.total as number
    } as SongMenuResult
  }

  static async getLyric(sid: string): Promise<LyricResult> {
    const params: any = { id: sid, kind: 'wy' } as any
    const res = await request.get<any>('/musicsearchlrc', params, null)
    const full: any = res as any
    const data: any = full.data as any
    return {
      sid: data.sid as string,
      lyric: data.lyric as string,
      ver: data.ver as number
    } as LyricResult
  }
}

3.2 播放列表管理

播放列表模块使用单例模式实现,确保跨页面共享播放列表数据。

// store/playlistStore.uts
// 播放列表模块级单例,跨页面共享、跨播放会话积累
export type PlayItem = {
  sid: string
  song: string
  sing: string
  url: string
  cover: string
}

let _playlist: PlayItem[] = []

// 添加歌曲(sid 相同则跳过,避免重复)
export const addToPlaylist = (item: PlayItem): void => {
  for (let i = 0; i < _playlist.length; i++) {
    if (_playlist[i].sid === item.sid) return
  }
  _playlist.push(item)
}

// 获取当前完整播放列表
export const getPlaylist = (): PlayItem[] => {
  return _playlist
}

// 根据 sid 从列表中移除歌曲
export const removeFromPlaylist = (sid: string): void => {
  for (let i = 0; i < _playlist.length; i++) {
    if (_playlist[i].sid === sid) {
      _playlist.splice(i, 1)
      return
    }
  }
}

// 根据 sid 获取在列表中的索引,找不到返回 -1
export const getIndexBySid = (sid: string): number => {
  for (let i = 0; i < _playlist.length; i++) {
    if (_playlist[i].sid === sid) return i
  }
  return -1
}

3.3 音乐播放器实现

音乐播放器是整个功能的核心,实现了播放控制、进度管理、歌词同步和UI动画等功能。

3.3.1 播放器核心功能

  1. 音频控制:使用 uni.createInnerAudioContext() 创建音频上下文,实现播放、暂停、快进、快退等功能。

  2. 进度管理:通过 onTimeUpdate 事件实时更新播放进度,并同步到进度条。

  3. 歌词同步:解析LRC歌词格式,根据当前播放时间匹配对应的歌词行。

  4. 唱片动画:使用 setInterval 实现唱片旋转效果,播放时旋转,暂停时停止。

  5. 播放列表管理:支持查看、切换和删除播放列表中的歌曲。

3.3.2 关键代码

音频上下文初始化

const ctx = uni.createInnerAudioContext()
ctx.autoplay = true

ctx.onPlay(() => {
  isPlaying.value = true
  startDisc()
})

ctx.onPause(() => {
  isPlaying.value = false
  stopDisc()
})

ctx.onTimeUpdate(() => {
  if (isChanging.value) return
  currentTime.value = ctx.currentTime
  sliderValue.value = ctx.currentTime
  if (ctx.duration > 0) {
    duration.value = ctx.duration
  }
  // 同步歌词高亮
  const idx = findLyricIndex(currentTime.value)
  if (idx !== currentLyricIndex.value) {
    currentLyricIndex.value = idx
  }
})

ctx.onEnded(() => {
  isPlaying.value = false
  stopDisc()
  currentTime.value = 0
  sliderValue.value = 0
  currentLyricIndex.value = -1

  // 自动播放播放列表中的下一首
  const playlist = getPlaylist()
  const curIdx = getIndexBySid(sid.value)
  if (curIdx >= 0 && curIdx < playlist.length - 1) {
    switchSong(playlist[curIdx + 1])
  }
})

歌词解析

const parseLyrics = (lyricStr: string): LyricLine[] => {
  const result: LyricLine[] = []
  const lines = lyricStr.split('\n')
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i].trim()
    if (line.length < 3 || line.charAt(0) !== '[') continue
    const closeBracket = line.indexOf(']')
    if (closeBracket <= 0) continue
    const timeStr = line.substring(1, closeBracket)
    const text = line.substring(closeBracket + 1).trim()
    if (text.length === 0) continue
    const colonIdx = timeStr.indexOf(':')
    if (colonIdx <= 0) continue
    const mins = parseInt(timeStr.substring(0, colonIdx))
    const secs = parseFloat(timeStr.substring(colonIdx + 1))
    result.push({ time: mins * 60 + secs, text: text } as LyricLine)
  }
  return result
}

唱片旋转动画

const discAngle = ref<number>(0)
let animTimer: number = 0

const discStyle = computed(() : string => {
  return 'transform: rotate(' + discAngle.value.toString() + 'deg)'
})

const startDisc = () => {
  if (animTimer !== 0) return
  animTimer = setInterval(() => {
    discAngle.value = (discAngle.value + 1.8) % 360
  }, 50) as number
}

const stopDisc = () => {
  if (animTimer !== 0) {
    clearInterval(animTimer)
    animTimer = 0
  }
}

3.3.3 player.uvue 页面结构(Template)

在实际项目中,音乐播放器页面位于 pages/music/player.uvue,使用 uni-app x 的 uvue 单文件页面形式组织模板、逻辑和样式。

整体结构可以概括为:

  • 顶层 page 容器,负责深色背景和整体布局
  • 背景封面层:使用专辑封面大图 + 半透明遮罩营造氛围感
  • 主内容区 main:竖向排列唱片、歌曲信息、进度条、控制按钮和歌词区域
  • 播放列表浮层:通过条件渲染 v-if="showPlaylist" 实现底部弹出的播放列表面板

关键模板结构示例:

<template>
  <view class="page">
    <!-- 背景封面(低透明度) -->
    <image v-if="cover.length > 0" class="bg-cover" :src="cover" mode="aspectFill" />
    <view class="bg-overlay" />

    <!-- 主内容 -->
    <view class="main">
      <!-- 唱片区:JS 驱动旋转 -->
      <view class="disc-section">
        <view class="disc" :style="discStyle">
          <image class="disc-art" :src="cover" mode="aspectFill" />
        </view>
      </view>

      <!-- 歌曲信息 -->
      <text class="song-name">{{ song }}</text>
      <text class="song-artist">{{ sing }}</text>

      <!-- 进度条 + 时间 -->
      <view class="progress-wrap">
        <slider
          class="progress-slider"
          :value="sliderValue"
          :min="0"
          :max="duration > 0 ? duration : 100"
          active-color="#e67e22"
          @changing="onChanging"
          @change="onChange"
        />
        <view class="time-row">
          <text class="time-text">{{ formatTime(currentTime) }}</text>
          <text class="time-text">{{ formatTime(duration) }}</text>
        </view>
      </view>

      <!-- 播放控制按钮 -->
      <view class="controls">
        <view class="list-btn" @click="togglePlaylist">
          <text class="list-icon">☰</text>
          <text class="list-label">列表</text>
        </view>
        <view class="ctrl-item" @click="seekBack">
          <text class="ctrl-text">⏪</text>
          <text class="ctrl-label">-10s</text>
        </view>
        <view class="play-btn" @click="togglePlay">
          <text class="play-icon">{{ isPlaying ? '⏸' : '▶' }}</text>
        </view>
        <view class="ctrl-item" @click="seekForward">
          <text class="ctrl-text">⏩</text>
          <text class="ctrl-label">+10s</text>
        </view>
      </view>

      <!-- 歌词滚动区 -->
      <view class="lyric-section">
        <text class="lyric-sep">— 歌 词 —</text>
        <scroll-view
          v-if="parsedLyrics.length > 0"
          class="lyric-scroll"
          direction="vertical"
          :scroll-into-view="currentLyricId"
          :scroll-with-animation="true"
        >
          <!-- ...歌词行渲染... -->
        </scroll-view>
      </view>
    </view>

    <!-- 播放列表浮层 -->
    <view v-if="showPlaylist" class="pl-overlay">
      <!-- 点击上方空白区域关闭 -->
      <view class="pl-close-area" @click="closePlaylist" />
      <!-- 底部面板展示播放列表 -->
      <view class="pl-panel">
        <!-- ...播放列表内容... -->
      </view>
    </view>
  </view>
</template>

通过这种结构划分,播放器页面在视觉和交互上都非常清晰:主视图聚焦当前播放的歌曲信息和控制,播放列表则作为二级浮层出现,既不打断播放,又便于管理队列。

3.3.4 player.uvue 脚本逻辑(Script Setup + 生命周期)

player.uvue<script setup lang="uts"> 部分使用 UTS 强类型语法,结合 uni-app x 生命周期函数实现完整的播放流程:

  • 状态定义:使用 ref 定义歌曲信息、播放状态、进度、歌词、播放列表等状态
  • 歌词解析与同步parseLyrics + findLyricIndex 负责将 LRC 文本解析为时间轴,并在 onTimeUpdate 中实时更新 currentLyricIndex
  • 播放控制togglePlayseekBackseekForward 分别控制播放/暂停和 10 秒快进/快退
  • 播放列表交互togglePlaylistonPlaylistItemTapremoveItemplaylistStore 模块协作,实现列表展示、切歌和删除

核心生命周期逻辑示例:

onLoad((options : any) => {
  // 1. 接收上一页面通过 URL 传递的参数
  const opts = options as any
  const sidVal = opts['sid']
  if (sidVal != null) sid.value = sidVal as string
  const songVal = opts['song']
  if (songVal != null) song.value = decodeURIComponent(songVal as string) ?? ''
  const singVal = opts['sing']
  if (singVal != null) sing.value = decodeURIComponent(singVal as string) ?? ''
  const urlVal = opts['url']
  if (urlVal != null) audioUrl.value = decodeURIComponent(urlVal as string) ?? ''
  const coverVal = opts['cover']
  if (coverVal != null) cover.value = decodeURIComponent(coverVal as string) ?? ''

  // 2. 自动把当前歌曲加入播放列表(模块级单例)
  if (sid.value.length > 0) {
    addToPlaylist({
      sid: sid.value,
      song: song.value,
      sing: sing.value,
      url: audioUrl.value,
      cover: cover.value
    } as PlayItem)
  }
})

onReady(() => {
  // 3. 创建音频上下文并绑定事件
  const ctx = uni.createInnerAudioContext()
  ctx.autoplay = true

  ctx.onPlay(() => {
    isPlaying.value = true
    startDisc()
  })

  ctx.onPause(() => {
    isPlaying.value = false
    stopDisc()
  })

  ctx.onTimeUpdate(() => {
    if (isChanging.value) return
    currentTime.value = ctx.currentTime
    sliderValue.value = ctx.currentTime
    if (ctx.duration > 0) {
      duration.value = ctx.duration
    }
    // 歌词高亮同步
    const idx = findLyricIndex(currentTime.value)
    if (idx !== currentLyricIndex.value) {
      currentLyricIndex.value = idx
    }
  })

  ctx.onEnded(() => {
    isPlaying.value = false
    stopDisc()
    currentTime.value = 0
    sliderValue.value = 0
    currentLyricIndex.value = -1

    // 自动播放播放列表中的下一首
    const playlist = getPlaylist()
    const curIdx = getIndexBySid(sid.value)
    if (curIdx >= 0 && curIdx < playlist.length - 1) {
      switchSong(playlist[curIdx + 1])
    }
  })

  ctx.src = audioUrl.value
  audioCtx.value = ctx

  // 4. 页面准备完毕后加载对应歌曲的歌词
  loadLyric()
})

onUnload(() => {
  // 5. 页面销毁时清理资源
  stopDisc()
  if (audioCtx.value != null) {
    audioCtx.value!.destroy()
    audioCtx.value = null
  }
})

通过以上逻辑,player.uvue 完整串联起“接收参数 → 创建音频上下文 → 播放与进度更新 → 歌词&动画同步 → 播放结束自动切歌 → 页面卸载清理资源”的生命周期闭环,充分展示了在 uni-app x 中实现音乐播放页面的最佳实践。

3.4 歌曲列表页面

歌曲列表页面展示歌单中的歌曲,支持播放和加入播放列表功能。

// 点击 + 按钮:仅加入播放列表
const addSong = (item: SongItem) => {
  addToPlaylist({
    sid: item.sid,
    song: item.song,
    sing: item.sing,
    url: item.url,
    cover: item.cover
  } as PlayItem)
  uni.showToast({ title: '已加入播放列表', icon: 'none', duration: 1500 })
}

const playSong = (item: SongItem) => {
  const params = 'sid=' + item.sid
    + '&song=' + encodeURIComponent(item.song)
    + '&sing=' + encodeURIComponent(item.sing)
    + '&url=' + encodeURIComponent(item.url)
    + '&cover=' + encodeURIComponent(item.cover)
  uni.navigateTo({ url: '/pages/music/player?' + params })
}

4. 技术亮点

4.1 跨平台兼容性

使用uni-app x框架和UTS语言,确保音乐播放功能在不同平台(HarmonyOS、Android、iOS等)上的一致性体验。

4.2 性能优化

  1. 资源预加载:在歌曲切换时提前加载音频资源。
  2. 歌词解析:使用高效的歌词解析算法,确保歌词同步的准确性。
  3. 动画性能:使用CSS transform实现唱片旋转动画,减少性能消耗。

4.3 用户体验

  1. 直观的播放控制:提供清晰的播放/暂停、快进/快退按钮。
  2. 实时进度反馈:通过进度条和时间显示,让用户了解当前播放状态。
  3. 歌词同步:自动滚动显示当前播放的歌词,提升用户体验。
  4. 播放列表管理:支持查看和管理播放队列,方便用户切换歌曲。
  5. 自动播放:歌曲结束后自动播放下一首,提供连续的音乐体验。

4.4 代码质量

  1. 类型安全:使用UTS的强类型系统,确保代码的类型安全。
  2. 模块化设计:将功能拆分为多个模块,提高代码的可维护性。
  3. 错误处理:对网络请求和音频播放错误进行适当的处理,提高应用的稳定性。

5. 实现效果

5.1 播放器界面

播放器采用深色主题设计,包含以下元素:

  • 背景封面(低透明度)
  • 旋转的唱片封面
  • 歌曲名称和歌手信息
  • 进度条和时间显示
  • 播放控制按钮(播放/暂停、快进/快退)
  • 歌词显示区域
  • 播放列表按钮

5.2 播放列表界面

播放列表以底部弹窗的形式展示,包含:

  • 播放列表标题和歌曲数量
  • 歌曲列表(显示序号、歌曲名、歌手)
  • 当前播放歌曲的标识
  • 删除歌曲按钮

6. 技术挑战与解决方案

6.1 跨平台音频API差异

挑战:不同平台的音频API存在差异,可能导致播放行为不一致。

解决方案:使用uni-app x提供的统一音频API uni.createInnerAudioContext(),该API会根据不同平台自动适配。

6.2 歌词同步精度

挑战:确保歌词与音乐播放进度的精确同步。

解决方案

  1. 精确解析LRC歌词的时间标签
  2. onTimeUpdate 事件中实时计算当前应该显示的歌词行
  3. 使用 scroll-into-view 实现歌词的自动滚动

6.3 播放列表状态管理

挑战:在不同页面间共享和管理播放列表状态。

解决方案:使用模块级单例模式实现播放列表管理,确保跨页面共享数据的一致性。

6.4 动画性能

挑战:唱片旋转动画在不同设备上的性能表现可能不同。

解决方案

  1. 使用CSS transform实现旋转,利用GPU加速
  2. 合理设置动画帧率(每50ms更新一次,10秒/圈)
  3. 在组件卸载时及时清除定时器,避免内存泄漏

7. 总结

通过以上技术实现,爱影家项目成功构建了一个功能完整、体验良好的音乐播放系统。该系统不仅支持基本的音乐播放功能,还提供了歌词同步、播放列表管理、唱片动画等增强功能,为用户带来了专业级的音乐播放体验。

使用uni-app x框架和UTS语言,使得该功能可以在多个平台上无缝运行,展现了跨平台开发的优势。同时,模块化的设计和良好的代码质量,为后续功能的扩展和维护奠定了坚实的基础。

未来,可以考虑添加更多功能,如:

  • 音乐缓存功能,支持离线播放
  • 音效设置(如均衡器)
  • 睡眠定时器
  • 歌词翻译功能
  • 音乐分享功能

这些功能将进一步提升用户体验,使爱影家成为一款更加完善的娱乐客户端。

收藏00

登录 后评论。没有帐号? 注册 一个。

csdn猫哥(qq8864)

  • 4回答
  • 8粉丝
  • 5关注