新增:文档体系重构+CHANGELOG补充+发布产物清理
This commit is contained in:
571
docs/02-架构设计/OOP架构/OOP-Composition组合方案.md
Normal file
571
docs/02-架构设计/OOP架构/OOP-Composition组合方案.md
Normal file
@@ -0,0 +1,571 @@
|
||||
# OOP + Composition API 组合使用方案
|
||||
|
||||
**日期**: 2026-01-31
|
||||
**核心理念**: 取长补短,渐进式迁移
|
||||
|
||||
---
|
||||
|
||||
## 🎯 设计原则
|
||||
|
||||
### OOP 负责什么
|
||||
- ✅ **核心业务逻辑**(复杂的状态管理)
|
||||
- ✅ **需要严格初始化顺序的功能**(如 ZIP 浏览)
|
||||
- ✅ **可复用的服务**(文件操作、预览等)
|
||||
- ✅ **需要依赖注入和测试的模块**
|
||||
|
||||
### Composition API 负责什么
|
||||
- ✅ **Vue 组件的响应式状态**
|
||||
- ✅ **简单的 UI 逻辑**
|
||||
- ✅ **生命周期钩子**
|
||||
- ✅ **DOM 事件处理**
|
||||
|
||||
### 分层架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ Vue 组件层 (Composition) │ ← UI 交互、生命周期
|
||||
│ index.vue | 组件 <script setup> │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ 使用
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ 适配器层 (Composables) │ ← 轻量桥接、响应式转换
|
||||
│ useFileSystem() | useZipBrowser() │
|
||||
└──────────────┬──────────────────────┘
|
||||
│ 调用
|
||||
┌──────────────▼──────────────────────┐
|
||||
│ 服务层 (OOP Classes) │ ← 核心逻辑、初始化保证
|
||||
│ FileSystemService | ZipService │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 实际代码示例
|
||||
|
||||
### 1. 服务层(OOP)- 解决初始化问题
|
||||
|
||||
```typescript
|
||||
// services/ZipBrowserService.ts
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FileApiService } from './FileApiService'
|
||||
import type { FilePreviewService } from './FilePreviewService'
|
||||
import type { FileItem } from '@/types/file-system'
|
||||
|
||||
/**
|
||||
* ZIP 浏览服务
|
||||
* 使用 OOP 封装,构造函数保证初始化顺序
|
||||
*/
|
||||
export class ZipBrowserService {
|
||||
// ========== 状态(私有 ref) ==========
|
||||
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>
|
||||
) {
|
||||
console.log('[ZipBrowserService] 初始化完成')
|
||||
}
|
||||
|
||||
// ========== 公共接口(访问器) ==========
|
||||
|
||||
/** 是否正在浏览 ZIP(返回 ref) */
|
||||
get isBrowsingZip(): Ref<boolean> {
|
||||
return this._isBrowsingZip
|
||||
}
|
||||
|
||||
/** 当前 ZIP 路径(返回 ref) */
|
||||
get currentZipPath(): Ref<string> {
|
||||
return this._currentZipPath
|
||||
}
|
||||
|
||||
/** 当前 ZIP 目录(返回 ref) */
|
||||
get currentZipDirectory(): Ref<string> {
|
||||
return this._currentZipDirectory
|
||||
}
|
||||
|
||||
/** 显示路径(计算属性) */
|
||||
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()
|
||||
Message.success('进入 ZIP 浏览模式')
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出 ZIP 浏览模式
|
||||
*/
|
||||
exitZipMode(): void {
|
||||
this._isBrowsingZip.value = false
|
||||
this._currentZipPath.value = ''
|
||||
this._currentZipDirectory.value = ''
|
||||
this.filePath.value = this._pathBeforeZip.value
|
||||
Message.info('退出 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> {
|
||||
// 加载目录逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 适配器层(Composable)- 桥接服务
|
||||
|
||||
```typescript
|
||||
// composables/useZipBrowser.ts
|
||||
import { useRef } from '@/utils/singleton'
|
||||
import { ZipBrowserService } from '@/services/ZipBrowserService'
|
||||
import type { UseZipBrowserOptions } from './types'
|
||||
|
||||
/**
|
||||
* ZIP 浏览 Composable
|
||||
* 轻量级适配器,桥接 Vue 和服务层
|
||||
*/
|
||||
export function useZipBrowser(options: UseZipBrowserOptions) {
|
||||
// 使用单例模式,确保服务只创建一次
|
||||
const service = useRef(() => {
|
||||
console.log('[useZipBrowser] 创建 ZipBrowserService 实例')
|
||||
|
||||
return new ZipBrowserService(
|
||||
options.fileApi, // 依赖注入
|
||||
options.previewService, // ✅ 保证预览服务已初始化
|
||||
options.fileList,
|
||||
options.filePath
|
||||
)
|
||||
}, 'zipBrowserService')
|
||||
|
||||
// 返回响应式接口(Composition API 风格)
|
||||
return {
|
||||
// 状态(直接返回 ref)
|
||||
isBrowsingZip: service.isBrowsingZip,
|
||||
currentZipPath: service.currentZipPath,
|
||||
currentZipDirectory: service.currentZipDirectory,
|
||||
displayPath: service.displayPath,
|
||||
|
||||
// 方法(绑定到服务实例)
|
||||
enterZipMode: (path: string) => service.enterZipMode(path),
|
||||
exitZipMode: () => service.exitZipMode(),
|
||||
navigateToZipDirectory: (path: string) => service.navigateToZipDirectory(path),
|
||||
getZipFileName: (path: string) => service.getZipFileName(path),
|
||||
getZipBreadcrumbs: () => service.getZipBreadcrumbs(),
|
||||
|
||||
// 服务实例(可选,用于高级用法)
|
||||
$service: service
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 单例工具(避免重复创建)
|
||||
|
||||
```typescript
|
||||
// utils/singleton.ts
|
||||
const singletons = new Map<string, any>()
|
||||
|
||||
/**
|
||||
* 创建或获取单例
|
||||
* @param factory 工厂函数
|
||||
* @param key 单例键名
|
||||
*/
|
||||
export function useRef<T>(factory: () => T, key: string): T {
|
||||
if (!singletons.has(key)) {
|
||||
const instance = factory()
|
||||
singletons.set(key, instance)
|
||||
console.log(`[Singleton] 创建 ${key}`)
|
||||
} else {
|
||||
console.log(`[Singleton] 复用 ${key}`)
|
||||
}
|
||||
return singletons.get(key) as T
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除单例(测试用)
|
||||
*/
|
||||
export function clearRef(key?: string): void {
|
||||
if (key) {
|
||||
singletons.delete(key)
|
||||
} else {
|
||||
singletons.clear()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 在组件中使用(Composition API)
|
||||
|
||||
```typescript
|
||||
// components/FileSystem/index.vue
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useFileOperations } from './composables/useFileOperations'
|
||||
import { useZipBrowser } from './composables/useZipBrowser'
|
||||
|
||||
// ========== 1. 初始化基础服务 ==========
|
||||
const { listDirectory, extractZipFile, getFileServerURL, fileApi } =
|
||||
useFileOperations()
|
||||
|
||||
// ========== 2. 初始化依赖服务 ==========
|
||||
const { previewService } = useFilePreview({ filePath })
|
||||
|
||||
// ========== 3. 初始化 ZIP 浏览(依赖预览服务) ==========
|
||||
const zipBrowser = useZipBrowser({
|
||||
fileApi,
|
||||
previewService, // ✅ 依赖注入,保证顺序
|
||||
fileList,
|
||||
filePath
|
||||
})
|
||||
|
||||
// ========== 4. 使用(和之前一样) ==========
|
||||
const toolbarConfig = computed(() => ({
|
||||
isBrowsingZip: zipBrowser.isBrowsingZip.value, // ✅ ref
|
||||
displayPath: zipBrowser.displayPath.value, // ✅ ref
|
||||
zipFileName: zipBrowser.getZipFileName(zipBrowser.currentZipPath.value),
|
||||
zipBreadcrumbs: zipBrowser.getZipBreadcrumbs()
|
||||
}))
|
||||
|
||||
// ========== 5. 事件处理 ==========
|
||||
const handleEnterZipMode = async (zipPath: string) => {
|
||||
await zipBrowser.enterZipMode(zipPath) // ✅ 简单调用
|
||||
}
|
||||
|
||||
const handleExitZip = () => {
|
||||
zipBrowser.exitZipMode() // ✅ 简单调用
|
||||
}
|
||||
|
||||
// ========== 6. 高级用法(可选) ==========
|
||||
// 直接访问服务实例
|
||||
const zipService = zipBrowser.$service
|
||||
console.log(zipService.currentZipPath.value)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 使用方式不变 -->
|
||||
<Toolbar
|
||||
:isBrowsingZip="zipBrowser.isBrowsingZip"
|
||||
:zipFileName="zipBrowser.getZipFileName(zipBrowser.currentZipPath)"
|
||||
@enter-zip="handleEnterZipMode"
|
||||
@exit-zip="handleExitZip"
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔄 渐进式迁移策略
|
||||
|
||||
### 阶段 1:新功能使用组合方案(立即开始)
|
||||
|
||||
```typescript
|
||||
// ✅ 新功能:使用 OOP 服务
|
||||
const newFeature = useNewFeature({
|
||||
service: new NewFeatureService()
|
||||
})
|
||||
```
|
||||
|
||||
### 阶段 2:问题模块优先迁移(本周)
|
||||
|
||||
```typescript
|
||||
// ❌ 旧代码(有问题)
|
||||
const zipBrowser = useZipBrowser({ ... })
|
||||
|
||||
// ✅ 新代码(使用服务)
|
||||
const zipBrowser = useZipBrowser({
|
||||
fileApi,
|
||||
previewService, // 依赖注入
|
||||
fileList,
|
||||
filePath
|
||||
})
|
||||
```
|
||||
|
||||
**优先迁移:**
|
||||
1. `useZipBrowser` - 初始化顺序问题最多
|
||||
2. `useFilePreview` - 返回值过多
|
||||
3. `useFileEdit` - 状态管理复杂
|
||||
|
||||
### 阶段 3:其他功能逐步迁移(1-2周)
|
||||
|
||||
```typescript
|
||||
// 老代码保持不变,新代码用新方案
|
||||
const oldFeature = useOldFeature() // 保持原样
|
||||
const newFeature = useNewFeature({ // 新方案
|
||||
service: new NewFeatureService()
|
||||
})
|
||||
```
|
||||
|
||||
### 阶段 4:完全迁移后(1个月后)
|
||||
|
||||
```typescript
|
||||
// 全部使用组合方案
|
||||
const { fileSystem, preview, zip, edit } = useServices({
|
||||
services: {
|
||||
fileSystem: new FileSystemService(),
|
||||
preview: new FilePreviewService(),
|
||||
zip: new ZipBrowserService()
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 使用场景对比
|
||||
|
||||
### 场景 1:简单 UI 逻辑(用 Composition API)
|
||||
|
||||
```typescript
|
||||
// ✅ 简单的响应式状态
|
||||
const showDialog = ref(false)
|
||||
const dialogMessage = ref('')
|
||||
|
||||
const openDialog = (msg: string) => {
|
||||
dialogMessage.value = msg
|
||||
showDialog.value = true
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2:复杂业务逻辑(用 OOP 服务)
|
||||
|
||||
```typescript
|
||||
// ✅ 复杂的状态管理
|
||||
class ZipBrowserService {
|
||||
constructor(
|
||||
private preview: FilePreviewService, // 依赖注入
|
||||
private fileApi: FileApiService
|
||||
) {}
|
||||
|
||||
async enterZipMode(path: string) {
|
||||
// 复杂的初始化逻辑
|
||||
await this.preview.cleanup()
|
||||
this._isBrowsingZip.value = true
|
||||
await this.loadZipContents()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 3:需要组合两者(组合使用)
|
||||
|
||||
```typescript
|
||||
// 服务层(OOP)- 核心逻辑
|
||||
class FilePreviewService {
|
||||
async previewImage(path: string) {
|
||||
const url = await this.fileApi.getImageUrl(path)
|
||||
this._previewUrl.value = url
|
||||
}
|
||||
}
|
||||
|
||||
// Composable - 桥接到 Vue
|
||||
function useFilePreview() {
|
||||
const service = new FilePreviewService(...)
|
||||
|
||||
return {
|
||||
// 响应式状态(Composition API)
|
||||
previewUrl: service.previewUrl,
|
||||
|
||||
// 方法(委托给服务)
|
||||
previewImage: (path: string) => service.previewImage(path),
|
||||
|
||||
// 服务实例(可选)
|
||||
$service: service
|
||||
}
|
||||
}
|
||||
|
||||
// 组件 - 使用
|
||||
const { previewUrl, previewImage } = useFilePreview()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎓 最佳实践
|
||||
|
||||
### 1. 服务类设计原则
|
||||
|
||||
```typescript
|
||||
class GoodService {
|
||||
// ✅ 状态用 ref
|
||||
private readonly _state = ref<State>(initialState)
|
||||
|
||||
// ✅ 构造函数注入依赖
|
||||
constructor(
|
||||
private readonly dependency: OtherService
|
||||
) {}
|
||||
|
||||
// ✅ 提供访问器
|
||||
get state(): Ref<State> {
|
||||
return this._state
|
||||
}
|
||||
|
||||
// ✅ 方法返回值(不返回 ref)
|
||||
doSomething(): void {
|
||||
this._state.value = { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Composable 设计原则
|
||||
|
||||
```typescript
|
||||
function useGoodService(options: Options) {
|
||||
// ✅ 创建服务实例
|
||||
const service = new GoodService(options.dependency)
|
||||
|
||||
return {
|
||||
// ✅ 返回 ref(响应式)
|
||||
state: service.state,
|
||||
|
||||
// ✅ 绑定方法
|
||||
doSomething: () => service.doSomething(),
|
||||
|
||||
// ✅ 可选:暴露服务
|
||||
$service: service
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 组件使用原则
|
||||
|
||||
```typescript
|
||||
// ✅ 简单场景:只用返回值
|
||||
const { state, doSomething } = useGoodService()
|
||||
|
||||
// ✅ 复杂场景:访问服务实例
|
||||
const { $service } = useGoodService()
|
||||
$service.advancedMethod()
|
||||
|
||||
// ✅ 生命周期钩子(Composition API)
|
||||
onMounted(() => {
|
||||
$service.initialize()
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 对比总结
|
||||
|
||||
| 维度 | OOP 服务 | Composable | 组件使用 |
|
||||
|-----|---------|-----------|---------|
|
||||
| **适用场景** | 复杂逻辑、初始化顺序 | 简单逻辑、UI 状态 | 组合使用 |
|
||||
| **状态管理** | ref 私有字段 | ref 变量 | ref 变量 |
|
||||
| **依赖注入** | 构造函数 | 函数参数 | 函数参数 |
|
||||
| **测试性** | ✅ 容易 | ⚠️ 中等 | ⚠️ 中等 |
|
||||
| **Vue 兼容** | ⚠️ 需要适配 | ✅ 完美 | ✅ 完美 |
|
||||
| **初始化保证** | ✅ 构造函数 | ❌ 手动保证 | - |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速开始模板
|
||||
|
||||
### 创建服务类
|
||||
|
||||
```bash
|
||||
# 1. 创建服务文件
|
||||
services/MyFeatureService.ts
|
||||
|
||||
# 2. 创建 composable 适配器
|
||||
composables/useMyFeature.ts
|
||||
|
||||
# 3. 在组件中使用
|
||||
components/MyComponent.vue
|
||||
```
|
||||
|
||||
### 模板代码
|
||||
|
||||
```typescript
|
||||
// 1. 服务类
|
||||
export class MyFeatureService {
|
||||
constructor(private dep: DependencyService) {}
|
||||
get state() { return this._state }
|
||||
doSomething() { ... }
|
||||
}
|
||||
|
||||
// 2. Composable
|
||||
export function useMyFeature() {
|
||||
const service = new MyFeatureService(dep)
|
||||
return {
|
||||
state: service.state,
|
||||
doSomething: () => service.doSomething(),
|
||||
$service: service
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 组件
|
||||
const { state, doSomething } = useMyFeature()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 总结
|
||||
|
||||
### 组合方案的优势
|
||||
|
||||
1. **✅ 解决初始化问题** - OOP 构造函数保证顺序
|
||||
2. **✅ 保持开发体验** - Composition API 风格
|
||||
3. **✅ 渐进式迁移** - 不需要大规模重构
|
||||
4. **✅ 高内聚低耦合** - 服务封装,适配器桥接
|
||||
5. **✅ 易于测试** - 服务层独立测试
|
||||
|
||||
### 核心理念
|
||||
|
||||
> **OOP 负责复杂逻辑,Composition 负责 UI 交互**
|
||||
|
||||
---
|
||||
|
||||
**生成时间**: 2026-01-31
|
||||
**下一步**: 创建第一个 OOP 服务示例(ZipBrowserService)?
|
||||
Reference in New Issue
Block a user