架构设计

应用编排层、数据服务层、状态层、视图层——OFPlayer 的分层设计让每个模块职责清晰。

文档内容

应用编排层、数据服务层、状态层、视图层——OFPlayer 的分层设计让每个模块职责清晰。

页面说明

架构设计 是 OFPlayer 本地优先音乐工作流的一部分。这个页面会先说明功能边界、适用场景和下一步入口,再交给网页播放器或工具页面完成具体操作。

OFPlayer 面向有个人音乐收藏的用户,重点放在本地文件播放、WebDAV 与 Subsonic/Navidrome 音乐库连接、歌词和音频工具、隐私优先的数据处理,以及足够克制的桌面式界面。

如果你只是想试用,可以从网页版开始导入本地音频;如果已经有 NAS、服务器或远程曲库,可以继续阅读教程,把 OpenList、WebDAV 或 Navidrome 接到同一套播放体验里。

页面内容会尽量说明真实可用的能力,不把尚未完成的方向包装成已经上线的功能。遇到部署、连接或格式问题时,可以顺着相关文档和工具逐步检查,而不是在播放器里反复猜配置。

对于搜索、分享或禁用脚本的访问场景,这份静态内容也会保留核心信息:页面主题、产品边界、相关入口和下一步操作都能被直接读取,不依赖前端应用完成后才出现。

每个入口都会尽量连接到相邻的说明页,避免只留下单点页面,内容也会随着产品发布持续更新。

从这里可以继续访问 OFPlayer 文档、OFPlayer 首页、GitHub。这些内部入口覆盖产品介绍、教程、文档、下载和实际工具,方便从了解能力一路走到部署、整理曲库和开始播放。

架构设计

分层架构概览

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 的 refcomputed
  • 不使用 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 })
// 下次部署时自动丢弃旧数据