Funkwhale 是一个开源的去中心化音乐流媒体服务,任何人都能够在自己的服务器上搭建音乐库,并通过 ActivityPub 协议与 Fediverse 网络中的其他实例互通。它的官方前端本身就采用 Vue 技术栈,所以当我们想为 Funkwhale 做一个定制化客户端,或者从零实现一个自己的去中心化音乐播放界面时,Vue 3 加上一套完善的工程化配置是最顺手的组合。本文会围绕项目搭建、API 对接、播放器封装和状态管理这几个核心环节,完整讲清楚整个开发流程。

一、搭建基于 Vite 的 Vue 3 工程化项目骨架
Funkwhale 官方前端早期基于 Vue 2 和 Webpack 构建,而新项目完全可以直接采用 Vue 3 加 Vite 的组合。Vite 的冷启动速度和按需编译能力,在音乐平台这种包含大量列表页、播放器组件的项目里优势明显。首先用脚手架初始化项目:
npm create vue@latest funkwhale-client cd funkwhale-client npm install npm install pinia axios vue-router
项目结构上,建议按照功能模块划分目录,而不是简单地按文件类型划分。一个比较合理的结构是 src/api 存放所有与 Funkwhale 服务端交互的模块,src/stores 存放 Pinia 状态仓库,src/components/player 存放播放器相关组件,src/views 存放路由页面。这样划分的好处在于,当你要支持另一个 Funkwhale 实例或者调整接口逻辑时,改动范围被限定在 api 目录内,不会污染视图层代码。
TypeScript 是强烈建议开启的。Funkwhale 的 API 返回结构相对复杂,比如曲目对象里嵌套了 album、artist、uploads 等多层数据,如果没有类型约束,后期维护成本会急剧上升。可以在 src/types 目录下为 Track、Album、Artist 等核心实体定义接口类型,供 API 层和组件层共用。
二、对接 Funkwhale 的 REST API 与认证机制
Funkwhale 提供了一套完整的 REST API,文档中把它称为 API 控制台,支持获取音乐库、艺术家、专辑、曲目、播放队列等资源。所有请求的基础地址形如 https://你的实例域名/api/v1/,例如获取曲目列表的接口是 /api/v1/tracks/。认证方式有两种:一种是应用级 Token,适合只读公开数据的场景;另一种是 OAuth2,适合需要代表用户操作的场景,比如收藏曲目、管理播放列表。
下面的代码演示了如何用 Axios 封装一个带认证的 API 客户端,并把实例地址抽成环境变量,方便切换不同的 Funkwhale 实例:
// src/api/client.ts
import axios from 'axios'
const client = axios.create({
baseURL: import.meta.env.VITE_FUNKWHALE_INSTANCE + '/api/v1/',
timeout: 15000,
})
client.interceptors.request.use((config) => {
const token = localStorage.getItem('fw_token')
if (token) {
config.headers.Authorization = 'Bearer ' + token
}
return config
})
export default client
需要注意的是,Funkwhale 的列表接口全部采用分页返回,响应里包含 count、next、previous 和 results 四个字段。在组件层做无限滚动的曲目列表时,要基于 next 字段拼接下一页请求,而不是自己累加页码,因为服务端的分页顺序可能会因为内容更新而变化。另外,跨域问题在开发阶段可以通过 Vite 的 proxy 配置解决,把 /api 代理到目标实例即可。
三、封装音频播放器与播放队列状态管理
音乐平台的核心体验就是播放器。在 Vue 3 里推荐用 Composition API 把播放逻辑抽成一个 usePlayer 组合式函数,内部管理一个原生的 Audio 对象,对外暴露播放、暂停、切歌、进度控制等方法以及当前曲目、播放状态、音量等响应式状态。Funkwhale 的音频文件地址通常通过 track 对象中的 uploads 或 listen URL 获取,拼接时同样要带上实例域名。
// src/composables/usePlayer.ts
import { reactive } from 'vue'
import type { Track } from '@/types'
const state = reactive({
current: null as Track | null,
playing: false,
volume: 0.8,
})
let audio = new Audio()
export function usePlayer() {
function play(track: Track) {
state.current = track
audio.src = track.uploads[0].listen_url
audio.volume = state.volume
audio.play()
state.playing = true
}
function toggle() {
if (state.playing) audio.pause()
else audio.play()
state.playing = !state.playing
}
return { state, play, toggle }
}
播放队列则适合放到 Pinia 里统一管理。队列仓库要维护待播列表、当前索引、循环模式、随机模式等信息,切歌时调用 usePlayer 的播放方法即可。把播放器实例和队列状态分开设计的好处是:播放器是全局唯一的音频输出,而队列可以在不同页面之间共享和修改,比如在专辑页点“全部播放”就是往队列仓库写入一组曲目并从头开始。
还有几个工程细节值得注意。第一,组件卸载时不要销毁全局 Audio 对象,否则切页面音乐会中断,正确做法是把播放器做成脱离路由生命周期的单例。第二,如果要支持 Media Session API,让用户能通过系统通知栏或耳机按键控制播放,需要在每次切歌时更新 navigator.mediaSession.metadata。第三,对于大列表渲染,曲目数上千时要配合虚拟滚动库使用,避免一次性渲染过多 DOM 节点导致卡顿。
四、构建产物优化与多实例适配
去中心化平台的一个特点是用户可能使用不同的实例。可以把实例地址完全交给运行时配置,比如在应用启动时让用户输入或选择实例,再动态初始化 Axios 客户端,这样一份构建产物就能适配所有 Funkwhale 实例。打包层面,Vite 默认的代码分割已经足够好,再配合路由级别的懒加载,把播放页、库管理页拆成独立 chunk,首屏体积能控制得很小。
总结一下,用 Vue 3 工程化开发 Funkwhale 前端的关键点在于:清晰的模块化目录、类型化的 API 层、单例播放器与 Pinia 队列的解耦设计,以及运行时可配置的实例地址。把这些基础打牢之后,无论是做个人自用的播放客户端,还是贡献给官方前端的二次开发,都会顺畅很多。