# OOP 服务层实施方案 - 彻底解决初始化问题

**日期**: 2026-01-31
**问题**: 第5次依然出现 "Cannot access before initialization" 错误
**方案**: 采用面向对象的服务层架构

---

## 🎯 核心问题

当前 Composition API 方案存在**根本性缺陷**：

```typescript
// 问题1: 函数定义顺序依赖手动保证
const config = computed(() => useHelper())  // Line 362
const useHelper = () => { ... }             // Line 869 ❌

// 问题2: 解构容易遗漏
const { previewUrl, isImageView } = useFilePreview()
// ❌ 忘记解构 updatePreviewUrl

// 问题3: 返回值过多
const { 15+个返回值 } = useFilePreview()

// 问题4: 状态分散
const state1 = ref()
const state2 = ref()
const state3 = ref()
// 状态和行为分离，内聚性差
```

---

## 💡 OOP 解决方案

### 架构设计

```
┌─────────────────────────────────────────┐
│           View Layer (Vue)              │
│  <template> | index.vue | 组件           │
└─────────────┬───────────────────────────┘
              │ 调用
┌─────────────▼───────────────────────────┐
│       Adapter Layer (Composables)        │
│  轻量级适配器，提供响应式接口             │
└─────────────┬───────────────────────────┘
              │ 使用
┌─────────────▼───────────────────────────┐
│        Service Layer (OOP)              │
│  核心业务逻辑，状态+行为封装              │
└─────────────┬───────────────────────────┘
              │ 依赖
┌─────────────▼───────────────────────────┐
│         Infrastructure Layer            │
│  文件API、ZIP处理等底层服务               │
└─────────────────────────────────────────┘
```

---

## 📝 具体实现

### 1. 服务基类

```typescript
// core/ServiceBase.ts
export abstract class ServiceBase {
  protected readonly logger = console
  protected readonly scope: string

  constructor(scope: string) {
    this.scope = scope
    this.initialize()
  }

  protected initialize(): void {
    // 子类可以覆盖
  }

  protected log(message: string, ...args: any[]): void {
    this.logger.log(`[${this.scope}]`, message, ...args)
  }

  protected error(message: string, error: Error): void {
    this.logger.error(`[${this.scope}]`, message, error)
  }
}
```

### 2. 文件预览服务

```typescript
// services/FilePreviewService.ts
import { ref, type Ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { ServiceBase } from '@/core/ServiceBase'
import type { FileApiService } from './FileApiService'
import { FileTypes } from '@/utils/fileTypeHelpers'

/**
 * 文件预览服务
 * 负责处理各种文件类型的预览逻辑
 */
export class FilePreviewService extends ServiceBase {
  // ========== 状态（私有） ==========
  private readonly _previewUrl = ref<string>('')
  private readonly _imageLoading = ref<boolean>(false)
  private readonly _currentImageDimensions = ref<string>('')

  // ========== 依赖注入 ==========
  constructor(
    private readonly fileApi: FileApiService,
    private readonly filePath: Ref<string>
  ) {
    super('FilePreviewService')
  }

  // ========== 公共接口（访问器） ==========

  /** 获取预览URL（响应式） */
  get previewUrl(): Ref<string> {
    return this._previewUrl
  }

  /** 获取图片加载状态（响应式） */
  get imageLoading(): Ref<boolean> {
    return this._imageLoading
  }

  /** 获取图片尺寸（响应式） */
  get currentImageDimensions(): Ref<string> {
    return this._currentImageDimensions
  }

  // ========== 公共方法 ==========

  /**
   * 更新预览URL
   */
  updatePreviewUrl(url: string): void {
    this._previewUrl.value = url
  }

  /**
   * 预览文件
   */
  async previewFile(filePath: string): Promise<void> {
    const ext = FileTypes.getExtension(filePath)

    if (this.isImageFile(filePath)) {
      await this.previewImage(filePath)
    } else if (this.isVideoFile(filePath)) {
      await this.previewVideo(filePath)
    } else if (this.isAudioFile(filePath)) {
      await this.previewAudio(filePath)
    } else if (this.isPdfFile(filePath)) {
      await this.previewPdf(filePath)
    }
  }

  /**
   * 判断是否为图片文件
   */
  isImageFile(path: string): boolean {
    return FileTypes.isImage(path)
  }

  /**
   * 判断是否为视频文件
   */
  isVideoFile(path: string): boolean {
    return FileTypes.isVideo(path)
  }

  /**
   * 判断是否为音频文件
   */
  isAudioFile(path: string): boolean {
    return FileTypes.isAudio(path)
  }

  /**
   * 判断是否为PDF文件
   */
  isPdfFile(path: string): boolean {
    return FileTypes.isPdf(path)
  }

  /**
   * 判断是否为HTML文件
   */
  isHtmlFile(path: string): boolean {
    return FileTypes.isHtml(path)
  }

  /**
   * 判断是否为Markdown文件
   */
  isMarkdownFile(path: string): boolean {
    return FileTypes.isMarkdown(path)
  }

  /**
   * 判断是否支持编辑/预览切换
   */
  isEditableWithPreview(filename: string): boolean {
    const ext = filename.split('.').pop()?.toLowerCase() || ''
    return ['html', 'htm', 'md', 'markdown'].includes(ext)
  }

  // ========== 私有方法 ==========

  private async previewImage(path: string): Promise<void> {
    this._imageLoading.value = true
    try {
      const url = await this.fileApi.getImageUrl(path)
      this.updatePreviewUrl(url)
      this.log('图片预览加载成功', path)
    } catch (error) {
      this.error('图片预览加载失败', error as Error)
      Message.error('图片加载失败')
    } finally {
      this._imageLoading.value = false
    }
  }

  private async previewVideo(path: string): Promise<void> {
    const url = await this.fileApi.getFileUrl(path)
    this.updatePreviewUrl(url)
  }

  private async previewAudio(path: string): Promise<void> {
    const url = await this.fileApi.getFileUrl(path)
    this.updatePreviewUrl(url)
  }

  private async previewPdf(path: string): Promise<void> {
    const url = await this.fileApi.getFileUrl(path)
    this.updatePreviewUrl(url)
  }
}
```

### 3. ZIP浏览服务

```typescript
// services/ZipBrowserService.ts
import { ref, computed, type Ref } from 'vue'
import { Message } from '@arco-design/web-vue'
import { ServiceBase } from '@/core/ServiceBase'
import type { FileApiService } from './FileApiService'
import type { FilePreviewService } from './FilePreviewService'
import type { FileItem } from '@/types/file-system'

/**
 * ZIP浏览服务
 * 负责ZIP文件浏览逻辑
 */
export class ZipBrowserService extends ServiceBase {
  // ========== 状态 ==========
  private readonly _isBrowsingZip = ref<boolean>(false)
  private readonly _currentZipPath = ref<string>('')
  private readonly _currentZipDirectory = ref<string>('')
  private readonly _pathBeforeZip = ref<string>('')

  // ========== 依赖注入 ==========
  constructor(
    private readonly fileApi: FileApiService,
    private readonly previewService: FilePreviewService,
    private readonly fileList: Ref<FileItem[]>,
    private readonly filePath: Ref<string>
  ) {
    super('ZipBrowserService')
    // 构造函数保证依赖已初始化
  }

  // ========== 计算属性 ==========

  /** 是否正在浏览ZIP */
  get isBrowsingZip(): Ref<boolean> {
    return this._isBrowsingZip
  }

  /** 当前ZIP路径 */
  get currentZipPath(): Ref<string> {
    return this._currentZipPath
  }

  /** 显示路径 */
  get displayPath(): Ref<string> {
    return computed(() => {
      if (this._currentZipDirectory.value) {
        return `📦 ${this._currentZipPath.value} [${this._currentZipDirectory.value}]`
      }
      return `📦 ${this._currentZipPath.value}`
    })
  }

  // ========== 公共方法 ==========

  /**
   * 进入ZIP浏览模式
   */
  async enterZipMode(zipPath: string): Promise<void> {
    this._pathBeforeZip.value = this.filePath.value
    this._currentZipPath.value = zipPath
    this._isBrowsingZip.value = true
    this._currentZipDirectory.value = ''

    await this.loadZipDirectory()
    this.log('进入ZIP浏览模式', zipPath)
  }

  /**
   * 退出ZIP浏览模式
   */
  exitZipMode(): void {
    this._isBrowsingZip.value = false
    this._currentZipPath.value = ''
    this._currentZipDirectory.value = ''
    this.log('退出ZIP浏览模式')
  }

  /**
   * 获取ZIP文件名
   */
  getZipFileName(zipPath: string): string {
    const parts = zipPath.split(/[/\\]/)
    return parts[parts.length - 1] || zipPath
  }

  /**
   * 获取面包屑
   */
  getZipBreadcrumbs(): ZipBreadcrumbItem[] {
    const crumbs: ZipBreadcrumbItem[] = [
      { name: this.getZipFileName(this._currentZipPath.value), path: '' }
    ]

    if (this._currentZipDirectory.value) {
      const parts = this._currentZipDirectory.value.split('/')
      let currentPath = ''
      for (const part of parts) {
        currentPath += (currentPath ? '/' : '') + part
        crumbs.push({ name: part, path: currentPath })
      }
    }

    return crumbs
  }

  /**
   * 导航到指定目录
   */
  async navigateToZipDirectory(path: string): Promise<void> {
    this._currentZipDirectory.value = path
    await this.loadZipDirectory()
  }

  // ========== 私有方法 ==========

  private async loadZipDirectory(): Promise<void> {
    // 加载逻辑
  }
}
```

### 4. 文件系统服务（聚合服务）

```typescript
// services/FileSystemService.ts
import { ref, type Ref } from 'vue'
import { ServiceBase } from '@/core/ServiceBase'
import { FilePreviewService } from './FilePreviewService'
import { ZipBrowserService } from './ZipBrowserService'
import { FileEditService } from './FileEditService'
import type { FileItem } from '@/types/file-system'

/**
 * 文件系统服务（门面）
 * 聚合所有文件相关服务
 */
export class FileSystemService extends ServiceBase {
  // ========== 状态 ==========
  private readonly _fileList = ref<FileItem[]>([])
  private readonly _fileLoading = ref<boolean>(false)
  private readonly _selectedFile = ref<FileItem | null>(null)

  // ========== 子服务（依赖注入） ==========
  readonly preview: FilePreviewService
  readonly zip: ZipBrowserService
  readonly edit: FileEditService

  // ========== 构造函数（保证初始化顺序） ==========
  constructor(
    private readonly fileApi: FileApiService,
    private readonly filePath: Ref<string>
  ) {
    super('FileSystemService')

    // 按顺序初始化子服务
    // 1. 预览服务（无依赖）
    this.preview = new FilePreviewService(this.fileApi, this.filePath)

    // 2. 编辑服务（依赖预览服务）
    this.edit = new FileEditService(this.fileApi, this.preview)

    // 3. ZIP服务（依赖预览服务）
    this.zip = new ZipBrowserService(
      this.fileApi,
      this.preview,
      this._fileList,
      this.filePath
    )

    this.log('文件系统服务初始化完成')
  }

  // ========== 公共接口 ==========

  /** 文件列表（响应式） */
  get fileList(): Ref<FileItem[]> {
    return this._fileList
  }

  /** 加载状态（响应式） */
  get fileLoading(): Ref<boolean> {
    return this._fileLoading
  }

  /** 选中文件（响应式） */
  get selectedFile(): Ref<FileItem | null> {
    return this._selectedFile
  }

  /**
   * 加载目录
   */
  async loadDirectory(path: string): Promise<void> {
    this._fileLoading.value = true
    try {
      const files = await this.fileApi.listDirectory(path)
      this._fileList.value = files
      this.log('目录加载成功', path, files.length, '个文件')
    } catch (error) {
      this.error('目录加载失败', error as Error)
      Message.error('加载目录失败')
    } finally {
      this._fileLoading.value = false
    }
  }

  /**
   * 预览文件
   */
  async previewFile(file: FileItem): Promise<void> {
    if (this.zip.isBrowsingZip.value) {
      await this.zip.previewZipFile(file.path)
    } else {
      await this.preview.previewFile(file.path)
    }
  }
}
```

### 5. Composable 适配器

```typescript
// composables/useFileSystem.ts
import { createSingleton } from '@/utils/singleton'
import { FileSystemService } from '@/services/FileSystemService'

/**
 * 文件系统 Composable
 * 轻量级适配器，桥接 Vue 响应式系统和服务层
 */
export function useFileSystem(options: UseFileSystemOptions = {}) {
  // 创建或获取服务单例
  const service = createSingleton(() => {
    const fileApi = new FileApiService()
    const filePath = ref(options.initialPath || '')
    return new FileSystemService(fileApi, filePath)
  }, 'fileSystemService')

  // 返回响应式接口
  return {
    // 状态（直接返回 ref）
    fileList: service.fileList,
    fileLoading: service.fileLoading,
    selectedFile: service.selectedFile,

    // 方法（委托给服务）
    loadDirectory: (path: string) => service.loadDirectory(path),
    previewFile: (file: FileItem) => service.previewFile(file),

    // 类型判断（委托给预览服务）
    isImageFile: (path: string) => service.preview.isImageFile(path),
    isVideoFile: (path: string) => service.preview.isVideoFile(path),
    isPdfFile: (path: string) => service.preview.isPdfFile(path),

    // 服务实例（可选，用于高级用法）
    $service: service
  }
}
```

### 6. 在 Vue 中使用

```typescript
// index.vue
<script setup lang="ts">
import { useFileSystem } from './composables/useFileSystem'

// 简单使用（和之前一样）
const {
  fileList,
  fileLoading,
  loadDirectory,
  previewFile
} = useFileSystem()

// 或者访问完整服务
const { $service } = useFileSystem()

// 可以访问所有服务方法
$service.preview.updatePreviewUrl(url)
$service.zip.enterZipMode(path)

// Computed 配置
const fileEditorPanelConfig = computed(() => ({
  // 使用服务方法，不会出现初始化问题
  isImageView: $service.preview.isImageFile(currentFileName),
  canPreviewFile: $service.preview.isEditableWithPreview(currentFileName),
  // ...
}))
</script>
```

---

## ✅ 方案优势

### 1. 解决初始化顺序问题

```typescript
// ❌ 之前：依赖手动保证顺序
const config = computed(() => useHelper())
const useHelper = () => { ... }  // 太晚了

// ✅ 现在：构造函数保证顺序
class Service {
  constructor(helper: HelperService) {  // 必须先创建 helper
    this.helper = helper
  }
}
```

### 2. 依赖注入，避免循环依赖

```typescript
class FileSystemService {
  constructor(
    private preview: FilePreviewService,  // 先初始化
    private zip: ZipBrowserService        // 可以依赖 preview
  ) {}
}
```

### 3. 高内聚，状态和行为绑定

```typescript
class FilePreviewService {
  private _previewUrl = ref('')  // 状态

  updatePreviewUrl(url: string) {  // 行为
    this._previewUrl.value = url
  }
}
```

### 4. 类型安全，IDE 友好

```typescript
const service = new FilePreviewService(...)
service.  // IDE 自动提示所有公共方法
```

### 5. 易于测试

```typescript
// Mock 依赖
const mockApi = new MockFileApiService()
const service = new FilePreviewService(mockApi, filePath)

// 测试方法
expect(service.isImageFile('test.jpg')).toBe(true)
```

---

## 📋 实施步骤

### 阶段1：创建服务层（1周）

1. 创建核心基类 `ServiceBase`
2. 实现 `FilePreviewService`
3. 实现 `ZipBrowserService`
4. 实现 `FileSystemService`

### 阶段2：创建适配器（3天）

1. 实现 `useFileSystem` composable
2. 确保向后兼容

### 阶段3：迁移功能（2周）

1. 逐步迁移现有功能到服务层
2. 保持老代码可用
3. 充分测试

### 阶段4：清理优化（1周）

1. 移除旧的 composables
2. 优化性能
3. 完善文档

---

## 🎯 总结

### 当前问题的根本原因

Composition API + 函数式方案**无法从架构层面**保证初始化顺序，只能依赖开发者手动保证。

### OOP 方案的核心优势

**构造函数 + 依赖注入**可以从编译器和运行时两个层面保证初始化顺序。

### 建议

立即采用 OOP 服务层方案，彻底解决初始化顺序问题。

---

**生成时间**: 2026-01-31
**预计工作量**: 3-4周
**风险等级**: 中等（需要重构，但可以渐进式迁移）
