架构设计
分层架构概览
OFPlayer 采用经典的分层架构,自上而下分为以下层级:
┌─────────────────────────────────────────────────────────────┐
│ View Layer (视图层) │
│ Vue Components / Pages / Router │
├─────────────────────────────────────────────────────────────┤
│ App Orchestration (应用编排层) │
│ ofplayerApp.js │
├─────────────────────────────────────────────────────────────┤
│ State Layer (状态层) │
│ libraryStore / playerStore / preferencesStore │
├─────────────────────────────────────────────────────────────┤
│ Service Layer (服务层) │
│ libraryService / playlistService / trackService │
├─────────────────────────────────────────────────────────────┤
│ Model Layer (模型层) │
│ library / playlist / track / collection │
├─────────────────────────────────────────────────────────────┤
│ Data Layer (数据层) │
│ IndexedDB / localStorage / DataService │
└─────────────────────────────────────────────────────────────┘
各层职责
1. View Layer (视图层)
位置: src/components/, src/pages/, src/router/
职责:
- 渲染 UI 界面
- 处理用户交互事件
- 通过 Props 接收数据,通过 Emit 发送事件
- 不直接访问服务层或数据层
关键设计:
- 组件通过
useOFPlayerApp()获取应用实例 - 应用实例提供所有业务方法和响应式状态
- 组件不持有业务逻辑,仅负责展示和事件转发
2. App Orchestration Layer (应用编排层)
位置: src/app/ofplayerApp.js
职责:
- 作为中央门面(Facade),统一协调所有服务和状态
- 通过 Vue 的
provide/inject机制分发应用实例 - 管理应用生命周期(创建、水合、销毁)
- 处理跨 Store 的业务逻辑
核心方法:
// 创建应用实例(同步)
const ofplayer = createOFPlayerApp()
// 安装到 Vue 应用
installOFPlayerApp(app, ofplayer)
// 水合数据(异步,在 mount 后调用)
await ofplayer.hydrate()
// 在组件中获取
const ofplayer = useOFPlayerApp()
提供的 API:
| 类别 | API |
|---|---|
| 状态 | libraries, playlists, tracks, currentTrack, isPlaying, currentTime, duration, volume |
| 播放控制 | selectTrack, togglePlayback, playNext, playPrevious, seek, setVolume |
| 库管理 | createLibrary, renameLibrary, deleteLibrary, setActiveLibrary |
| 播放列表 | createPlaylist, renamePlaylist, deletePlaylist, addTrackToPlaylist, removeTrackFromPlaylist |
| 导入导出 | importFiles, exportAllTracks, clearAllData |
| 偏好设置 | setTheme, setColorScheme, setLanguage, setVolume |
3. State Layer (状态层)
位置: src/stores/
职责:
- 管理响应式状态
- 提供状态访问和修改方法
- 与服务层交互进行数据持久化
- 维护状态一致性
Store 列表:
| Store | 职责 |
|---|---|
libraryStore |
库、播放列表、曲目、关系管理 |
playerStore |
播放状态、当前曲目、播放历史 |
preferencesStore |
用户偏好设置 |
sessionStore |
会话状态、队列管理 |
entitlementStore |
功能权限管理 |
uiStore |
UI 状态 |
设计特点:
- 使用 Vue Composition API 的
ref和computed - 不使用 Vuex 或 Pinia
- 每个 Store 是一个工厂函数,返回响应式状态和方法
- Store 之间通过组合方式协作
4. Service Layer (服务层)
位置: src/services/
职责:
- 实现业务逻辑
- 协调数据模型和数据访问层
- 提供领域特定的操作接口
服务列表:
| 服务 | 职责 |
|---|---|
libraryService |
库的 CRUD 操作 |
playlistService |
播放列表操作、曲目关系管理 |
trackService |
文件导入、曲目对象创建、资源释放 |
metadataService |
音频元数据提取 |
externalLibraryService |
外部库连接(WebDAV、Subsonic) |
albumViewService |
专辑浏览视图逻辑 |
lyricsStorage |
歌词持久化 |
telemetryService |
遥测分析 |
5. Model Layer (模型层)
位置: src/models/
职责:
- 定义数据结构和形状
- 提供工厂函数创建标准化对象
- 数据验证和规范化
模型列表:
| 模型 | 实体 |
|---|---|
library.js |
Library |
playlist.js |
Playlist |
track.js |
Track |
collection.js |
Collection (抽象) |
playback.js |
Playback State |
preferences.js |
Preferences |
session.js |
Session |
6. Data Layer (数据层)
位置: src/services/data/
职责:
- 提供数据持久化抽象
- 支持多种存储驱动
- 处理数据序列化和反序列化
驱动实现:
| 驱动 | 实现 |
|---|---|
indexedDbDataService.js |
IndexedDB 实现(主驱动) |
localDataService.js |
localStorage 实现(降级方案) |
数据流
典型数据流(以播放曲目为例)
用户点击曲目
↓
View Layer: PlayerPanel emit('select-track', trackId)
↓
App Layer: ofplayer.selectTrack(trackId)
↓
State Layer: sessionStore.setCurrentTrack(trackId)
↓
Service Layer: externalLibraryService.resolvePlayableTrack(track)
↓
Data Layer: dataService.catalog.getTrackAssets(trackId)
↓
State Layer: playerStore.loadTrack(track)
↓
View Layer: 响应式更新 UI
初始化流程
main.js
↓
createOFPlayerApp() // 同步创建 Store 实例
↓
app.mount('#app') // Vue 应用挂载,UI 立即可交互
↓
ofplayer.hydrate() // 异步加载持久化数据
↓
Promise.all([
preferencesStore.hydrate(),
libraryStore.hydrate(),
sessionStore.hydrate(),
playerStore.hydrate()
])
↓
syncQueueWithCatalog() // 同步队列与目录
↓
isHydrating.value = false // 加载完成
模块依赖关系
ofplayerApp.js
├── libraryStore
│ ├── libraryService
│ ├── playlistService
│ └── trackService
├── playerStore
│ └── useAudioPlayer
├── sessionStore
└── preferencesStore
所有 Service
└── dataService (Data Layer)
Data Layer
├── indexedDbDataService
└── localDataService
关键设计模式
1. Facade 模式
ofplayerApp.js 作为中央门面,简化了组件与复杂子系统的交互:
// 组件无需了解内部细节
const ofplayer = useOFPlayerApp()
ofplayer.selectTrack(trackId) // 一个方法调用
// 内部协调了 sessionStore、playerStore、externalLibraryService
2. Factory 模式
所有 Store 和 Service 都是工厂函数:
export function createLibraryStore({ libraryService, playlistService, trackService }) {
// 创建并返回响应式状态和方法
return { libraries, playlists, tracks, ... }
}
3. Repository 模式
数据访问层抽象了存储实现:
// 统一接口
dataService.catalog.putLibrary(library)
dataService.catalog.getTrack(trackId)
// 可切换实现
createDataService({ driver: 'indexeddb' }) // 或 'local'
4. Composition API 模式
使用组合式函数封装逻辑:
export function useAudioPlayer({ initialVolume, onPlay, onPause }) {
const isPlaying = ref(false)
const currentTime = ref(0)
// ...
return { isPlaying, currentTime, play, pause, ... }
}
性能优化策略
1. 虚拟列表
当曲目数量超过 90 首时,自动启用虚拟列表:
const VIRTUALIZATION_THRESHOLD = 90
const shouldVirtualizeTracks = computed(() =>
visibleTracks.value.length >= VIRTUALIZATION_THRESHOLD
)
2. O(1) 查找表
使用 Map 维护 ID 到对象的映射:
const tracksById = computed(() => {
const m = new Map()
for (const t of tracks.value) m.set(t.id, t)
return m
})
3. 懒加载
路由组件使用动态导入:
{
path: '/',
component: () => import('../pages/PlayerPage.vue')
}
4. 版本化存储
使用构建 ID 自动失效过期数据:
writeVersioned('ofp:visual', { theme, colorScheme, motion })
// 下次部署时自动丢弃旧数据