新增:文档体系重构+CHANGELOG补充+发布产物清理
This commit is contained in:
544
docs/02-架构设计/OOP架构/OOP-vs-Composables架构对比.md
Normal file
544
docs/02-架构设计/OOP架构/OOP-vs-Composables架构对比.md
Normal file
@@ -0,0 +1,544 @@
|
||||
# OOP vs Composables 架构对比分析
|
||||
|
||||
**日期**: 2026-01-31
|
||||
**目的**: 探讨使用面向对象方式减少初始化顺序问题的可行性
|
||||
|
||||
---
|
||||
|
||||
## 1. 问题场景回顾
|
||||
|
||||
### 当前遇到的初始化问题
|
||||
|
||||
```typescript
|
||||
// 问题1: 解构遗漏
|
||||
const { previewUrl, isImageView, isAudioView } = useFilePreview()
|
||||
// ❌ 忘记解构 updatePreviewUrl,导致后续 undefined
|
||||
|
||||
// 问题2: 函数定义顺序
|
||||
const config = computed(() => useHelper()) // Line 362
|
||||
const useHelper = () => { ... } // Line 869
|
||||
// ❌ 相差507行,导致初始化错误
|
||||
|
||||
// 问题3: 返回值过多
|
||||
const {
|
||||
previewUrl, updatePreviewUrl, isImageView,
|
||||
isVideoView, isAudioView, isPdfFile,
|
||||
isHtmlFile, isMarkdownFile,
|
||||
getPreviewUrl, previewImage,
|
||||
// ... 还有15+个
|
||||
} = useFilePreview()
|
||||
// ❌ 难以维护,容易出错
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 方案对比
|
||||
|
||||
### 方案A: Composables(当前方案)
|
||||
|
||||
#### 代码示例
|
||||
|
||||
```typescript
|
||||
// composables/useFilePreview.ts
|
||||
export function useFilePreview(options: UseFilePreviewOptions) {
|
||||
const previewUrl = ref('')
|
||||
const imageLoading = ref(false)
|
||||
|
||||
const updatePreviewUrl = (url: string) => {
|
||||
previewUrl.value = url
|
||||
}
|
||||
|
||||
const previewImage = async (path: string) => {
|
||||
imageLoading.value = true
|
||||
// ...
|
||||
}
|
||||
|
||||
// 返回15+个值
|
||||
return {
|
||||
previewUrl,
|
||||
imageLoading,
|
||||
updatePreviewUrl,
|
||||
previewImage,
|
||||
isImageView,
|
||||
isVideoView,
|
||||
// ... 还有10+个
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用方式
|
||||
|
||||
```typescript
|
||||
// index.vue
|
||||
const {
|
||||
previewUrl,
|
||||
updatePreviewUrl,
|
||||
isImageView
|
||||
} = useFilePreview({ filePath })
|
||||
|
||||
// 使用
|
||||
updatePreviewUrl(url)
|
||||
```
|
||||
|
||||
#### 优点
|
||||
- ✅ 符合 Vue 3 Composition API 理念
|
||||
- ✅ 函数式,灵活性高
|
||||
- ✅ 可以选择性解构需要的值
|
||||
- ✅ Tree-shaking 友好
|
||||
|
||||
#### 缺点
|
||||
- ❌ 解构时容易遗漏函数
|
||||
- ❌ 返回值过多时难以管理
|
||||
- ❌ 初始化顺序依赖手动保证
|
||||
- ❌ 状态分散,内聚性低
|
||||
|
||||
---
|
||||
|
||||
### 方案B: OOP + Class(提议方案)
|
||||
|
||||
#### 代码示例
|
||||
|
||||
```typescript
|
||||
// services/FilePreviewService.ts
|
||||
export class FilePreviewService {
|
||||
// ========== 状态 ==========
|
||||
private readonly _previewUrl = ref<string>('')
|
||||
private readonly _imageLoading = ref<boolean>(false)
|
||||
private readonly _currentImageDimensions = ref<string>('')
|
||||
|
||||
// ========== 依赖注入 ==========
|
||||
constructor(
|
||||
private readonly filePath: Ref<string>,
|
||||
private readonly fileServer: FileServerService,
|
||||
private readonly options: FilePreviewOptions = {}
|
||||
) {
|
||||
// 构造函数保证初始化顺序
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
// ========== 公共接口 ==========
|
||||
|
||||
// Getter(访问器)
|
||||
get previewUrl(): string {
|
||||
return this._previewUrl.value
|
||||
}
|
||||
|
||||
get imageLoading(): boolean {
|
||||
return this._imageLoading.value
|
||||
}
|
||||
|
||||
// 方法
|
||||
updatePreviewUrl(url: string): void {
|
||||
this._previewUrl.value = url
|
||||
}
|
||||
|
||||
async previewImage(path: string): Promise<void> {
|
||||
this._imageLoading.value = true
|
||||
try {
|
||||
const url = await this.fileServer.getPreviewUrl(path)
|
||||
this.updatePreviewUrl(url)
|
||||
} finally {
|
||||
this._imageLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
isImageFile(path: string): boolean {
|
||||
return FileTypes.isImage(path)
|
||||
}
|
||||
|
||||
// ========== 私有方法 ==========
|
||||
|
||||
private initialize(): void {
|
||||
// 初始化逻辑,保证顺序
|
||||
this.loadPreviewSettings()
|
||||
}
|
||||
|
||||
private async loadPreviewSettings(): Promise<void> {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
// 工厂函数(可选)
|
||||
export function createFilePreviewService(
|
||||
filePath: Ref<string>,
|
||||
fileServer?: FileServerService
|
||||
): FilePreviewService {
|
||||
return new FilePreviewService(
|
||||
filePath,
|
||||
fileServer ?? new FileServerService()
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用方式
|
||||
|
||||
```typescript
|
||||
// index.vue
|
||||
// 方式1: 直接实例化
|
||||
const previewService = new FilePreviewService(
|
||||
filePath,
|
||||
new FileServerService()
|
||||
)
|
||||
|
||||
// 方式2: 工厂函数
|
||||
const previewService = createFilePreviewService(filePath)
|
||||
|
||||
// 使用
|
||||
previewService.updatePreviewUrl(url)
|
||||
const isLoading = previewService.imageLoading
|
||||
```
|
||||
|
||||
#### 优点
|
||||
- ✅ **构造函数保证初始化顺序**
|
||||
- ✅ **状态和行为绑定(高内聚)**
|
||||
- ✅ **类型安全,IDE 自动完成更好**
|
||||
- ✅ **不会遗漏方法(都有实例.提示)**
|
||||
- ✅ **依赖注入,易于测试**
|
||||
- ✅ **私有方法封装性好**
|
||||
|
||||
#### 缺点
|
||||
- ❌ 与 Vue 3 Composition API 理念不完全一致
|
||||
- ❌ 需要手动管理实例生命周期
|
||||
- ❌ 失去 composables 的部分灵活性
|
||||
- ❌ 可能带来额外的内存开销
|
||||
|
||||
---
|
||||
|
||||
## 3. 混合方案(推荐)
|
||||
|
||||
### Composables + Service Layer
|
||||
|
||||
```typescript
|
||||
// composables/useFilePreview.ts(轻量级)
|
||||
import { FilePreviewService } from '@/services/FilePreviewService'
|
||||
|
||||
export function useFilePreview(options: UseFilePreviewOptions) {
|
||||
// 创建服务实例
|
||||
const service = new FilePreviewService(
|
||||
options.filePath,
|
||||
new FileServerService()
|
||||
)
|
||||
|
||||
// 返回响应式接口
|
||||
return {
|
||||
// 响应式状态(直接返回 ref)
|
||||
previewUrl: service.previewUrlRef,
|
||||
imageLoading: service.imageLoadingRef,
|
||||
|
||||
// 方法(绑定实例)
|
||||
updatePreviewUrl: (url: string) => service.updatePreviewUrl(url),
|
||||
previewImage: (path: string) => service.previewImage(path),
|
||||
|
||||
// 服务实例(可选,用于高级用法)
|
||||
$service: service
|
||||
}
|
||||
}
|
||||
|
||||
// services/FilePreviewService.ts(核心逻辑)
|
||||
export class FilePreviewService {
|
||||
private readonly _previewUrl = ref<string>('')
|
||||
|
||||
constructor(
|
||||
private readonly filePath: Ref<string>,
|
||||
private readonly fileServer: FileServerService
|
||||
) {}
|
||||
|
||||
get previewUrlRef(): Ref<string> {
|
||||
return this._previewUrl
|
||||
}
|
||||
|
||||
updatePreviewUrl(url: string): void {
|
||||
this._previewUrl.value = url
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 使用方式
|
||||
|
||||
```typescript
|
||||
// 简单使用(和之前一样)
|
||||
const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath })
|
||||
|
||||
// 高级使用(直接访问服务)
|
||||
const { $service } = useFilePreview({ filePath })
|
||||
$service.previewImage(path) // 访问所有方法
|
||||
```
|
||||
|
||||
#### 优势
|
||||
- ✅ 保留 Composition API 的便利性
|
||||
- ✅ 核心逻辑使用 OOP,保证初始化顺序
|
||||
- ✅ 两种使用方式,灵活性高
|
||||
- ✅ 渐进式重构,成本低
|
||||
|
||||
---
|
||||
|
||||
## 4. 实际应用示例
|
||||
|
||||
### 文件系统服务架构
|
||||
|
||||
```typescript
|
||||
// ========== 服务层(核心逻辑) ==========
|
||||
|
||||
// services/FileSystemService.ts
|
||||
export class FileSystemService {
|
||||
constructor(
|
||||
private readonly fileApi: FileApiService,
|
||||
private readonly previewService: FilePreviewService,
|
||||
private readonly zipService: ZipBrowserService
|
||||
) {
|
||||
this.initializeServices()
|
||||
}
|
||||
|
||||
private initializeServices(): void {
|
||||
// 保证服务初始化顺序
|
||||
this.previewService.initialize()
|
||||
this.zipService.initialize()
|
||||
}
|
||||
|
||||
async loadDirectory(path: string): Promise<FileItem[]> {
|
||||
return await this.fileApi.listDirectory(path)
|
||||
}
|
||||
|
||||
async previewFile(file: FileItem): Promise<void> {
|
||||
if (file.is_dir) return
|
||||
|
||||
if (this.zipService.isBrowsingZip) {
|
||||
await this.zipService.previewZipFile(file.path)
|
||||
} else {
|
||||
await this.previewService.previewFile(file.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// services/FilePreviewService.ts
|
||||
export class FilePreviewService {
|
||||
private readonly _previewUrl = ref<string>('')
|
||||
private readonly _fileContent = ref<string>('')
|
||||
|
||||
constructor(
|
||||
private readonly fileApi: FileApiService,
|
||||
private readonly filePath: Ref<string>
|
||||
) {}
|
||||
|
||||
async previewFile(path: string): Promise<void> {
|
||||
const ext = FileTypes.getExtension(path)
|
||||
|
||||
if (FileTypes.isImage(ext)) {
|
||||
await this.previewImage(path)
|
||||
} else if (FileTypes.isCode(ext)) {
|
||||
await this.loadCodeContent(path)
|
||||
}
|
||||
}
|
||||
|
||||
private async previewImage(path: string): Promise<void> {
|
||||
const url = await this.fileApi.getImageUrl(path)
|
||||
this._previewUrl.value = url
|
||||
}
|
||||
}
|
||||
|
||||
// services/ZipBrowserService.ts
|
||||
export class ZipBrowserService {
|
||||
private readonly _isBrowsingZip = ref<boolean>(false)
|
||||
private readonly _currentZipPath = ref<string>('')
|
||||
|
||||
constructor(
|
||||
private readonly fileApi: FileApiService,
|
||||
private readonly previewService: FilePreviewService
|
||||
) {
|
||||
// 依赖注入,保证 previewService 已初始化
|
||||
}
|
||||
|
||||
async enterZipMode(zipPath: string): Promise<void> {
|
||||
this._currentZipPath.value = zipPath
|
||||
this._isBrowsingZip.value = true
|
||||
await this.loadZipRoot()
|
||||
}
|
||||
|
||||
async previewZipFile(filePath: string): Promise<void> {
|
||||
// 可以安全地调用 previewService
|
||||
await this.previewService.previewFile(filePath)
|
||||
}
|
||||
}
|
||||
|
||||
// ========== Composables 层(轻量封装) ==========
|
||||
|
||||
// composables/useFileSystem.ts
|
||||
export function useFileSystem() {
|
||||
// 创建服务实例(初始化顺序由构造函数保证)
|
||||
const fileApi = new FileApiService()
|
||||
const previewService = new FilePreviewService(fileApi, filePath)
|
||||
const zipService = new ZipBrowserService(fileApi, previewService)
|
||||
const fileSystemService = new FileSystemService(
|
||||
fileApi,
|
||||
previewService,
|
||||
zipService
|
||||
)
|
||||
|
||||
// 返回响应式接口
|
||||
return {
|
||||
// 状态
|
||||
fileList: ref<FileItem[]>([]),
|
||||
fileLoading: ref(false),
|
||||
|
||||
// 方法(委托给服务)
|
||||
loadDirectory: (path: string) => fileSystemService.loadDirectory(path),
|
||||
previewFile: (file: FileItem) => fileSystemService.previewFile(file),
|
||||
|
||||
// 服务实例(可选)
|
||||
$services: {
|
||||
fileSystem: fileSystemService,
|
||||
preview: previewService,
|
||||
zip: zipService
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```typescript
|
||||
// index.vue
|
||||
const {
|
||||
fileList,
|
||||
fileLoading,
|
||||
loadDirectory,
|
||||
previewFile,
|
||||
$services // 访问完整服务
|
||||
} = useFileSystem()
|
||||
|
||||
// 简单使用
|
||||
await loadDirectory(path)
|
||||
|
||||
// 高级使用(访问所有服务方法)
|
||||
if ($services.zip.isBrowsingZip) {
|
||||
await $services.zip.navigateToZipDirectory(path)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 对比总结表
|
||||
|
||||
| 维度 | Composables | OOP + Class | 混合方案 |
|
||||
|-----|-------------|-------------|---------|
|
||||
| **初始化顺序保证** | ❌ 手动保证 | ✅ 构造函数保证 | ✅ 构造函数保证 |
|
||||
| **内聚性** | ⚠️ 状态分散 | ✅ 高 | ✅ 高 |
|
||||
| **类型安全** | ⚠️ 解构容易出错 | ✅ 严格 | ✅ 严格 |
|
||||
| **IDE 支持** | ⚠️ 中等 | ✅ 优秀 | ✅ 优秀 |
|
||||
| **代码复用** | ✅ 灵活 | ⚠️ 需要继承 | ✅ 灵活 |
|
||||
| **测试性** | ⚠️ 需要模拟依赖 | ✅ 依赖注入 | ✅ 依赖注入 |
|
||||
| **学习曲线** | ✅ 平缓 | ⚠️ 需要OOP经验 | ⚠️ 中等 |
|
||||
| **重构成本** | ✅ 低 | ❌ 高 | ⚠️ 中等 |
|
||||
| **与 Vue 3 兼容** | ✅ 完美 | ⚠️ 需要适配 | ✅ 良好 |
|
||||
| **性能** | ✅ 轻量 | ⚠️ 可能有开销 | ✅ 轻量 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 推荐方案
|
||||
|
||||
### 短期(1-2周):保持 Composables + 改进规范
|
||||
|
||||
```typescript
|
||||
// 添加分区注释,保证顺序
|
||||
// ========== 1. 工具函数 ==========
|
||||
const isEditableWithPreview = (filename: string): boolean => { ... }
|
||||
|
||||
// ========== 2. 状态变量 ==========
|
||||
const fileList = ref<FileItem[]>([])
|
||||
|
||||
// ========== 3. Composables ==========
|
||||
const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath })
|
||||
|
||||
// ========== 4. Computed ==========
|
||||
const config = computed(() => ({
|
||||
canPreview: isEditableWithPreview(filename) // ✅ 函数已定义
|
||||
}))
|
||||
```
|
||||
|
||||
### 中期(1-2月):混合方案
|
||||
|
||||
```typescript
|
||||
// 新功能使用 OOP 服务
|
||||
// 老功能保持 Composables
|
||||
// 逐步迁移
|
||||
```
|
||||
|
||||
### 长期(3-6月):全面 OOP 架构
|
||||
|
||||
```typescript
|
||||
// 所有核心逻辑使用服务类
|
||||
// Composables 仅作为轻量级适配器
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 实施建议
|
||||
|
||||
### 如果采用混合方案,分步骤:
|
||||
|
||||
#### 步骤1:创建服务层(不影响现有代码)
|
||||
|
||||
```typescript
|
||||
// services/FilePreviewService.ts
|
||||
export class FilePreviewService {
|
||||
// 新代码使用 OOP
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤2:创建适配器 Composable
|
||||
|
||||
```typescript
|
||||
// composables/useFilePreview.ts
|
||||
export function useFilePreview(options) {
|
||||
const service = new FilePreviewService(...)
|
||||
|
||||
return {
|
||||
// 响应式接口
|
||||
previewUrl: service.previewUrlRef,
|
||||
// 方法
|
||||
updatePreviewUrl: (url) => service.updatePreviewUrl(url)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 步骤3:逐步迁移现有代码
|
||||
|
||||
```typescript
|
||||
// 老代码
|
||||
const { previewUrl, updatePreviewUrl } = useFilePreview({ filePath })
|
||||
|
||||
// 新代码(可以直接使用服务)
|
||||
const service = new FilePreviewService(filePath)
|
||||
service.updatePreviewUrl(url)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 结论
|
||||
|
||||
### OOP 方案能解决当前问题吗?
|
||||
|
||||
**✅ 能解决:**
|
||||
1. 初始化顺序问题(构造函数保证)
|
||||
2. 解构遗漏问题(实例.调用)
|
||||
3. 返回值过多问题(清晰的接口)
|
||||
4. 内聚性问题(状态+行为绑定)
|
||||
|
||||
### 但需要权衡:
|
||||
|
||||
**❌ 潜在问题:**
|
||||
1. 与 Vue 3 理念不完全一致
|
||||
2. 重构成本较高
|
||||
3. 团队学习曲线
|
||||
|
||||
### 最佳方案:
|
||||
|
||||
**🎯 推荐:混合方案**
|
||||
- 核心逻辑使用 OOP 服务类
|
||||
- Composables 作为轻量适配器
|
||||
- 渐进式迁移,降低风险
|
||||
|
||||
---
|
||||
|
||||
**生成时间**: 2026-01-31
|
||||
**下一步**: 是否创建一个示例服务类验证可行性?
|
||||
Reference in New Issue
Block a user