uni-app x跨平台开发实战:爱影家免费观影APP音乐播放功能实现详解
在玩中学,直接上手实战是猫哥一贯的自学方法心得。假期期间实在无聊!不睡懒觉、不看电影、也不刷手机、不玩游戏、也无处可去。那么我干嘛嘞?感觉假期好没意思啊。做什么呢? 于是翻出来之前做过的“爱影家”影视app项目,找个跨多端的技术栈再玩一把。免费听歌看电影,一次性的支持android、IOS、harmonyOS、Web和小程序等所有平台。
1. 项目概述
爱影家是一款基于uni-app x框架开发的跨平台影视音乐客户端,其中音乐播放功能是其重要组成部分。本文将详细介绍音乐播放功能的实现原理、核心技术和关键代码。
注:该项目仅用于学习研究,严禁用于其用途!
该项目的开源地址:https://gitcode.com/qq8864/uniappx_imovie

2. 音乐播放功能架构
2.1 核心模块
音乐播放功能主要由以下几个模块组成:
- API模块:负责与后端服务器交互,获取音乐数据
- 播放器模块:负责音乐播放控制、进度管理和UI展示
- 播放列表模块:负责管理播放队列
- 数据存储模块:负责音乐相关数据的存储和管理
2.2 数据流
- 用户从歌单列表选择歌曲
- 歌曲信息被添加到播放列表
- 跳转到播放器页面
- 播放器加载歌曲资源并开始播放
- 播放器实时更新播放状态和歌词
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 播放器核心功能
-
音频控制:使用
uni.createInnerAudioContext()创建音频上下文,实现播放、暂停、快进、快退等功能。 -
进度管理:通过
onTimeUpdate事件实时更新播放进度,并同步到进度条。 -
歌词同步:解析LRC歌词格式,根据当前播放时间匹配对应的歌词行。
-
唱片动画:使用
setInterval实现唱片旋转效果,播放时旋转,暂停时停止。 -
播放列表管理:支持查看、切换和删除播放列表中的歌曲。
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 - 播放控制:
togglePlay、seekBack、seekForward分别控制播放/暂停和 10 秒快进/快退 - 播放列表交互:
togglePlaylist、onPlaylistItemTap、removeItem与playlistStore模块协作,实现列表展示、切歌和删除
核心生命周期逻辑示例:
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 性能优化
- 资源预加载:在歌曲切换时提前加载音频资源。
- 歌词解析:使用高效的歌词解析算法,确保歌词同步的准确性。
- 动画性能:使用CSS transform实现唱片旋转动画,减少性能消耗。
4.3 用户体验
- 直观的播放控制:提供清晰的播放/暂停、快进/快退按钮。
- 实时进度反馈:通过进度条和时间显示,让用户了解当前播放状态。
- 歌词同步:自动滚动显示当前播放的歌词,提升用户体验。
- 播放列表管理:支持查看和管理播放队列,方便用户切换歌曲。
- 自动播放:歌曲结束后自动播放下一首,提供连续的音乐体验。
4.4 代码质量
- 类型安全:使用UTS的强类型系统,确保代码的类型安全。
- 模块化设计:将功能拆分为多个模块,提高代码的可维护性。
- 错误处理:对网络请求和音频播放错误进行适当的处理,提高应用的稳定性。
5. 实现效果
5.1 播放器界面
播放器采用深色主题设计,包含以下元素:
- 背景封面(低透明度)
- 旋转的唱片封面
- 歌曲名称和歌手信息
- 进度条和时间显示
- 播放控制按钮(播放/暂停、快进/快退)
- 歌词显示区域
- 播放列表按钮
5.2 播放列表界面
播放列表以底部弹窗的形式展示,包含:
- 播放列表标题和歌曲数量
- 歌曲列表(显示序号、歌曲名、歌手)
- 当前播放歌曲的标识
- 删除歌曲按钮
6. 技术挑战与解决方案
6.1 跨平台音频API差异
挑战:不同平台的音频API存在差异,可能导致播放行为不一致。
解决方案:使用uni-app x提供的统一音频API uni.createInnerAudioContext(),该API会根据不同平台自动适配。
6.2 歌词同步精度
挑战:确保歌词与音乐播放进度的精确同步。
解决方案:
- 精确解析LRC歌词的时间标签
- 在
onTimeUpdate事件中实时计算当前应该显示的歌词行 - 使用
scroll-into-view实现歌词的自动滚动
6.3 播放列表状态管理
挑战:在不同页面间共享和管理播放列表状态。
解决方案:使用模块级单例模式实现播放列表管理,确保跨页面共享数据的一致性。
6.4 动画性能
挑战:唱片旋转动画在不同设备上的性能表现可能不同。
解决方案:
- 使用CSS transform实现旋转,利用GPU加速
- 合理设置动画帧率(每50ms更新一次,10秒/圈)
- 在组件卸载时及时清除定时器,避免内存泄漏
7. 总结
通过以上技术实现,爱影家项目成功构建了一个功能完整、体验良好的音乐播放系统。该系统不仅支持基本的音乐播放功能,还提供了歌词同步、播放列表管理、唱片动画等增强功能,为用户带来了专业级的音乐播放体验。
使用uni-app x框架和UTS语言,使得该功能可以在多个平台上无缝运行,展现了跨平台开发的优势。同时,模块化的设计和良好的代码质量,为后续功能的扩展和维护奠定了坚实的基础。
未来,可以考虑添加更多功能,如:
- 音乐缓存功能,支持离线播放
- 音效设置(如均衡器)
- 睡眠定时器
- 歌词翻译功能
- 音乐分享功能
这些功能将进一步提升用户体验,使爱影家成为一款更加完善的娱乐客户端。
- 4回答
- 8粉丝
- 5关注
