新增:文档体系重构+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)?
|
||||
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
|
||||
**下一步**: 是否创建一个示例服务类验证可行性?
|
||||
648
docs/02-架构设计/OOP架构/OOP服务层实施方案.md
Normal file
648
docs/02-架构设计/OOP架构/OOP服务层实施方案.md
Normal file
@@ -0,0 +1,648 @@
|
||||
# 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周
|
||||
**风险等级**: 中等(需要重构,但可以渐进式迁移)
|
||||
15
docs/02-架构设计/OOP架构/README.md
Normal file
15
docs/02-架构设计/OOP架构/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# OOP 架构设计文档
|
||||
|
||||
本目录包含面向对象编程(OOP)架构设计的分析和方案文档。
|
||||
|
||||
## 📄 文档列表
|
||||
|
||||
- [OOP-vs-Composables架构对比.md](./OOP-vs-Composables架构对比.md) - OOP 与 Composables 架构对比
|
||||
- [OOP-Composition组合方案.md](./OOP-Composition组合方案.md) - OOP Composition 组合方案
|
||||
- [OOP服务层实施方案.md](./OOP服务层实施方案.md) - OOP 服务层实施方案
|
||||
- [全部OOP的理性分析.md](./全部OOP的理性分析.md) - 全面 OOP 的理性分析
|
||||
- [临时解决方案-OOP重写ZIP.md](./临时解决方案-OOP重写ZIP.md) - 临时解决方案
|
||||
|
||||
## 🎯 设计目标
|
||||
|
||||
探索使用 OOP 模式替代 Composition API 的可行性,提供更清晰的代码组织结构。
|
||||
148
docs/02-架构设计/OOP架构/临时解决方案-OOP重写ZIP.md
Normal file
148
docs/02-架构设计/OOP架构/临时解决方案-OOP重写ZIP.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# 临时解决方案:使用 OOP 重写 ZIP 浏览
|
||||
|
||||
**目的**: 彻底解决初始化顺序问题
|
||||
**方法**: 用 OOP 服务类替换现有的 useZipBrowser
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速实施步骤
|
||||
|
||||
### 步骤1:创建 OOP 服务(15分钟)
|
||||
|
||||
创建 `services/ZipBrowserService.ts`:
|
||||
|
||||
```typescript
|
||||
import { ref, computed, type Ref } from 'vue'
|
||||
import { Message } from '@arco-design/web-vue'
|
||||
import type { FileItem } from '@/types/file-system'
|
||||
|
||||
/**
|
||||
* ZIP 浏览服务(OOP 版本)
|
||||
* 使用类封装,构造函数保证初始化顺序
|
||||
*/
|
||||
export class ZipBrowserService {
|
||||
// ========== 状态 ==========
|
||||
private readonly _isBrowsingZip = ref<boolean>(false)
|
||||
private readonly _currentZipPath = ref<string>('')
|
||||
private readonly _currentZipDirectory = ref<string>('')
|
||||
private readonly _pathBeforeZip = ref<string>('')
|
||||
|
||||
// ========== 构造函数(保证初始化) ==========
|
||||
constructor() {
|
||||
console.log('[ZipBrowserService] 初始化完成')
|
||||
}
|
||||
|
||||
// ========== 公共接口(访问器) ==========
|
||||
|
||||
get isBrowsingZip(): Ref<boolean> {
|
||||
return this._isBrowsingZip
|
||||
}
|
||||
|
||||
get currentZipPath(): Ref<string> {
|
||||
return this._currentZipPath
|
||||
}
|
||||
|
||||
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}`
|
||||
})
|
||||
}
|
||||
|
||||
// ========== 公共方法 ==========
|
||||
|
||||
async enterZipMode(zipPath: string): Promise<void> {
|
||||
this._pathBeforeZip.value = '' // 保存之前的路径
|
||||
this._currentZipPath.value = zipPath
|
||||
this._isBrowsingZip.value = true
|
||||
this._currentZipDirectory.value = ''
|
||||
|
||||
Message.success('进入 ZIP 浏览模式')
|
||||
}
|
||||
|
||||
exitZipMode(): void {
|
||||
this._isBrowsingZip.value = false
|
||||
this._currentZipPath.value = ''
|
||||
this._currentZipDirectory.value = ''
|
||||
|
||||
Message.info('退出 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
|
||||
// 加载目录逻辑
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤2:在组件中使用(5分钟)
|
||||
|
||||
```typescript
|
||||
// index.vue
|
||||
|
||||
// 导入服务类
|
||||
import { ZipBrowserService } from '@/services/ZipBrowserService'
|
||||
|
||||
// 创建服务实例(在所有状态变量之前)
|
||||
const zipService = new ZipBrowserService()
|
||||
|
||||
// 使用(和之前一样)
|
||||
const toolbarConfig = computed(() => ({
|
||||
isBrowsingZip: zipService.isBrowsingZip.value,
|
||||
displayPath: zipService.displayPath.value,
|
||||
zipFileName: zipService.getZipFileName(zipService.currentZipPath.value),
|
||||
zipBreadcrumbs: zipService.getZipBreadcrumbs()
|
||||
}))
|
||||
|
||||
const handleExitZip = () => {
|
||||
zipService.exitZipMode()
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤3:测试(2分钟)
|
||||
|
||||
```bash
|
||||
wails build
|
||||
.\build\bin\u-desk.exe
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⏱️ 总时间:约 22 分钟
|
||||
|
||||
如果成功,我们可以:
|
||||
1. 将其他功能也迁移到 OOP 服务
|
||||
2. 逐步完善 ZIP 浏览功能
|
||||
3. 彻底解决初始化问题
|
||||
|
||||
---
|
||||
|
||||
**要不要试试这个方案?**
|
||||
618
docs/02-架构设计/OOP架构/全部OOP的理性分析.md
Normal file
618
docs/02-架构设计/OOP架构/全部OOP的理性分析.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# 全部使用 OOP 的理性分析
|
||||
|
||||
**日期**: 2026-01-31
|
||||
**问题**: 长期全部使用 OOP 真的好吗?
|
||||
**结论**: ❌ 不推荐,应该**混合使用**
|
||||
|
||||
---
|
||||
|
||||
## 📊 对比分析
|
||||
|
||||
### Vue 3 的设计哲学
|
||||
|
||||
```typescript
|
||||
// ✅ Vue 3 的设计理念(函数式、组合式)
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
export function useCounter() {
|
||||
const count = ref(0)
|
||||
const doubled = computed(() => count.value * 2)
|
||||
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubled, increment }
|
||||
}
|
||||
```
|
||||
|
||||
**Vue 3 官方推荐:**
|
||||
- ✅ Composition API(函数式)
|
||||
- ✅ Composables(可组合)
|
||||
- ✅ 响应式系统(ref/reactive)
|
||||
|
||||
### 全部 OOP 与 Vue 3 的冲突
|
||||
|
||||
```typescript
|
||||
// ❌ 完全 OOP 方式(与 Vue 3 理念相悖)
|
||||
class CounterService {
|
||||
private readonly _count = ref(0)
|
||||
private readonly _doubled = computed(() => this._count.value * 2)
|
||||
|
||||
increment(): void {
|
||||
this._count.value++
|
||||
}
|
||||
}
|
||||
|
||||
// 还需要适配器
|
||||
function useCounter() {
|
||||
const service = new CounterService()
|
||||
return {
|
||||
count: service.count,
|
||||
doubled: service.doubled,
|
||||
increment: () => service.increment()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ❌ 全部 OOP 的问题
|
||||
|
||||
### 1. 与 Vue 3 生态不一致
|
||||
|
||||
**官方文档和生态:**
|
||||
- Vue 3 官方文档都是 Composition API
|
||||
- Vuetify、Element Plus、Arco Design 都是 Composables
|
||||
- 社区最佳实践都是函数式
|
||||
|
||||
**后果:**
|
||||
```typescript
|
||||
// ❌ 你的代码
|
||||
class MyService {
|
||||
constructor(...) {}
|
||||
}
|
||||
|
||||
// ❌ Vue 官方示例
|
||||
export function useMyFeature() {
|
||||
const count = ref(0)
|
||||
return { count }
|
||||
}
|
||||
|
||||
// 团队需要维护两套思维模式
|
||||
```
|
||||
|
||||
### 2. 代码量增加
|
||||
|
||||
**当前方式(Composition API):**
|
||||
```typescript
|
||||
// composables/useZipBrowser.ts
|
||||
export function useZipBrowser(options) {
|
||||
const isBrowsingZip = ref(false)
|
||||
|
||||
const enterZipMode = async (path) => {
|
||||
isBrowsingZip.value = true
|
||||
}
|
||||
|
||||
return { isBrowsingZip, enterZipMode }
|
||||
}
|
||||
// 约 50 行代码
|
||||
```
|
||||
|
||||
**OOP 方式:**
|
||||
```typescript
|
||||
// services/ZipBrowserService.ts
|
||||
class ZipBrowserService {
|
||||
private readonly _isBrowsingZip = ref(false)
|
||||
private readonly fileApi: FileApiService
|
||||
private readonly previewService: FilePreviewService
|
||||
|
||||
constructor(
|
||||
fileApi: FileApiService,
|
||||
previewService: FilePreviewService
|
||||
) {
|
||||
this.fileApi = fileApi
|
||||
this.previewService = previewService
|
||||
}
|
||||
|
||||
get isBrowsingZip() {
|
||||
return this._isBrowsingZip
|
||||
}
|
||||
|
||||
async enterZipMode(path: string) {
|
||||
this._isBrowsingZip.value = true
|
||||
}
|
||||
}
|
||||
// 约 80 行代码
|
||||
|
||||
// composables/useZipBrowser.ts(适配器)
|
||||
export function useZipBrowser(options) {
|
||||
const service = new ZipBrowserService(
|
||||
options.fileApi,
|
||||
options.previewService
|
||||
)
|
||||
|
||||
return {
|
||||
isBrowsingZip: service.isBrowsingZip,
|
||||
enterZipMode: (path) => service.enterZipMode(path)
|
||||
}
|
||||
}
|
||||
// 约 30 行代码
|
||||
|
||||
// 总计:110 行代码(是原来的 2.2 倍)
|
||||
```
|
||||
|
||||
### 3. 失去 Composition API 的灵活性
|
||||
|
||||
**Composable 的优势:组合**
|
||||
```typescript
|
||||
// ✅ 可以灵活组合
|
||||
function useFileSystem() {
|
||||
const fileOps = useFileOperations()
|
||||
const preview = useFilePreview({ filePath })
|
||||
const zip = useZipBrowser({ preview })
|
||||
|
||||
return {
|
||||
...fileOps,
|
||||
...preview,
|
||||
...zip
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**OOP 的限制:**
|
||||
```typescript
|
||||
// ❌ 需要复杂的继承或组合
|
||||
class FileSystemService {
|
||||
constructor(
|
||||
private fileOps: FileOperationsService,
|
||||
private preview: FilePreviewService,
|
||||
private zip: ZipBrowserService
|
||||
) {}
|
||||
|
||||
// 需要转发所有方法
|
||||
listFile() { return this.fileOps.listFile() }
|
||||
previewFile() { return this.preview.previewFile() }
|
||||
enterZip() { return this.zip.enterZip() }
|
||||
// ... 20+ 个方法转发
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 性能开销
|
||||
|
||||
**类实例化开销:**
|
||||
```typescript
|
||||
// ❌ 每次使用都需要 new
|
||||
const service1 = new MyService()
|
||||
const service2 = new MyService()
|
||||
const service3 = new MyService()
|
||||
|
||||
// 需要单例管理增加复杂度
|
||||
```
|
||||
|
||||
**Composable 开销:**
|
||||
```typescript
|
||||
// ✅ 轻量级,无实例化开销
|
||||
const { count } = useCount()
|
||||
const { doubled } = useDoubled()
|
||||
```
|
||||
|
||||
### 5. 响应式系统结合复杂
|
||||
|
||||
```typescript
|
||||
// ❌ OOP + 响应式很别扭
|
||||
class MyService {
|
||||
private readonly _count = ref(0) // 私有 ref
|
||||
|
||||
get count(): number {
|
||||
return this._count.value // 需要 getter
|
||||
}
|
||||
|
||||
set count(value: number) {
|
||||
this._count.value = value // 需要 setter
|
||||
}
|
||||
}
|
||||
|
||||
// ✅ 响应式很自然
|
||||
const count = ref(0)
|
||||
```
|
||||
|
||||
### 6. Tree-shaking 问题
|
||||
|
||||
```typescript
|
||||
// ❌ OOP 可能导致 Tree-shaking 不彻底
|
||||
class MyService {
|
||||
method1() {}
|
||||
method2() {}
|
||||
method3() {}
|
||||
}
|
||||
|
||||
// 即使只用 method1,整个类都会被打包
|
||||
|
||||
// ✅ Composable Tree-shaking 友好
|
||||
export function useFeature() {
|
||||
const method1 = () => {}
|
||||
return { method1 }
|
||||
}
|
||||
|
||||
// 只打包用到的代码
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 什么情况下应该用 OOP?
|
||||
|
||||
### 场景 1:复杂的状态管理
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 OOP
|
||||
class GameStateManager {
|
||||
private readonly _players = ref<Map<string, Player>>(new Map())
|
||||
private readonly _currentTurn = ref<number>(0)
|
||||
private readonly _gameState = ref<'idle' | 'playing' | 'paused'>('idle')
|
||||
|
||||
// 复杂的初始化逻辑
|
||||
constructor(config: GameConfig) {
|
||||
this.initializeGame(config)
|
||||
}
|
||||
|
||||
// 多个关联的状态操作
|
||||
nextTurn(): void {
|
||||
if (this._gameState.value !== 'playing') return
|
||||
|
||||
this._currentTurn.value++
|
||||
this.updatePlayerScores()
|
||||
this.checkWinCondition()
|
||||
}
|
||||
|
||||
// 私有方法,封装复杂逻辑
|
||||
private updatePlayerScores(): void { ... }
|
||||
private checkWinCondition(): void { ... }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2:需要严格的初始化顺序
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 OOP(构造函数保证顺序)
|
||||
class ZipBrowserService {
|
||||
constructor(
|
||||
private preview: FilePreviewService, // 必须先创建
|
||||
private fileApi: FileApiService
|
||||
) {
|
||||
// TypeScript 编译时检查
|
||||
// 运行时保证依赖已初始化
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 3:需要依赖注入和测试
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 OOP
|
||||
class UserService {
|
||||
constructor(
|
||||
private api: ApiService, // 可以注入 Mock
|
||||
private cache: CacheService
|
||||
) {}
|
||||
|
||||
async getUser(id: string): Promise<User> {
|
||||
// 测试时可以注入 MockApiService
|
||||
return await this.api.getUser(id)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 4:业务规则复杂,需要高内聚
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 OOP
|
||||
class OrderService {
|
||||
// 相关的状态和行为封装在一起
|
||||
private readonly _orders = ref<Order[]>([])
|
||||
|
||||
createOrder(data: OrderData): Order {
|
||||
this.validateOrder(data)
|
||||
this.calculateDiscount(data)
|
||||
const order = this.buildOrder(data)
|
||||
this._orders.value.push(order)
|
||||
return order
|
||||
}
|
||||
|
||||
private validateOrder(data: OrderData): void { ... }
|
||||
private calculateDiscount(data: OrderData): void { ... }
|
||||
private buildOrder(data: OrderData): Order { ... }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 什么情况下应该用 Composition API?
|
||||
|
||||
### 场景 1:简单的 UI 状态
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 Composition API
|
||||
function useDialog() {
|
||||
const visible = ref(false)
|
||||
const message = ref('')
|
||||
|
||||
const open = (msg: string) => {
|
||||
message.value = msg
|
||||
visible.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
return { visible, message, open, close }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 2:需要灵活组合
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 Composition API
|
||||
function useForm() {
|
||||
const data = ref({})
|
||||
const errors = ref({})
|
||||
return { data, errors, validate, reset }
|
||||
}
|
||||
|
||||
function useAsyncForm() {
|
||||
const form = useForm()
|
||||
const loading = ref(false)
|
||||
|
||||
const submit = async () => {
|
||||
loading.value = true
|
||||
await api.post(form.data.value)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return { ...form, loading, submit }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 3:简单的数据获取
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 Composition API
|
||||
function useUserList() {
|
||||
const users = ref<User[]>([])
|
||||
const loading = ref(false)
|
||||
|
||||
const fetch = async () => {
|
||||
loading.value = true
|
||||
users.value = await api.getUsers()
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
return { users, loading, fetch }
|
||||
}
|
||||
```
|
||||
|
||||
### 场景 4:与 Vue 生态系统集成
|
||||
|
||||
```typescript
|
||||
// ✅ 适合 Composition API
|
||||
function useTable() {
|
||||
const { data, loading } = useAsyncData(() => api.getItems())
|
||||
|
||||
const columns = [
|
||||
{ title: 'Name', dataIndex: 'name' },
|
||||
{ title: 'Age', dataIndex: 'age' }
|
||||
]
|
||||
|
||||
return { columns, data, loading }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 推荐的混合策略
|
||||
|
||||
### 原则:80% Composition + 20% OOP
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ UI 层(组件) │
|
||||
│ 100% Composition API │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ 业务逻辑层(Composables) │
|
||||
│ 90% Composition API │
|
||||
│ 10% OOP 服务(复杂逻辑) │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ 核心服务层(Services) │
|
||||
│ 50% OOP(复杂业务逻辑) │
|
||||
│ 50% 函数式(简单工具) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 具体分配
|
||||
|
||||
| 层级 | Composition API | OOP | 比例 |
|
||||
|-----|----------------|-----|------|
|
||||
| **UI 组件** | 100% | 0% | 0:100 |
|
||||
| **Composables** | 90% | 10% | 9:1 |
|
||||
| **Services** | 50% | 50% | 1:1 |
|
||||
|
||||
---
|
||||
|
||||
## 📋 决策树
|
||||
|
||||
```
|
||||
需要实现新功能
|
||||
│
|
||||
├─ 是 UI 状态?→ Composition API
|
||||
│ - 对话框开关
|
||||
│ - 表单输入
|
||||
│ - 加载状态
|
||||
│
|
||||
├─ 是简单数据获取?→ Composition API
|
||||
│ - 获取用户列表
|
||||
│ - 加载文件内容
|
||||
│
|
||||
├─ 需要灵活组合?→ Composition API
|
||||
│ - 多个 composable 组合
|
||||
│ - 可选功能
|
||||
│
|
||||
├─ 有复杂初始化顺序?→ OOP
|
||||
│ - ZIP 浏览(依赖预览服务)
|
||||
│ - 游戏状态管理
|
||||
│
|
||||
├─ 需要依赖注入?→ OOP
|
||||
│ - 测试需要 Mock
|
||||
│ - 多个实现版本
|
||||
│
|
||||
├─ 业务规则复杂?→ OOP
|
||||
│ - 订单处理
|
||||
│ - 工作流引擎
|
||||
│
|
||||
└─ 其他?→ Composition API(默认)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 实际建议
|
||||
|
||||
### 短期(解决当前问题)
|
||||
|
||||
**不要全面 OOP**,而是:
|
||||
|
||||
1. **修复代码组织问题**
|
||||
```typescript
|
||||
// ✅ 严格按顺序组织代码
|
||||
// 1. 工具函数
|
||||
// 2. 状态变量
|
||||
// 3. Composables
|
||||
// 4. Computed
|
||||
// 5. 事件处理
|
||||
```
|
||||
|
||||
2. **添加 ESLint 规则**
|
||||
```javascript
|
||||
// .eslintrc.js
|
||||
rules: {
|
||||
'no-use-before-define': ['error', { functions: false }]
|
||||
}
|
||||
```
|
||||
|
||||
3. **使用 TypeScript 严格模式**
|
||||
```json
|
||||
// tsconfig.json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"strict": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 中期(局部使用 OOP)
|
||||
|
||||
仅在特定场景使用 OOP:
|
||||
|
||||
```typescript
|
||||
// ✅ 仅 ZIP 浏览用 OOP(解决初始化问题)
|
||||
class ZipBrowserService {
|
||||
constructor(preview: FilePreviewService) {}
|
||||
}
|
||||
|
||||
// ❌ 不要全部用 OOP
|
||||
// class FilePreviewService { ... }
|
||||
// class FileEditService { ... }
|
||||
// class FileOperationsService { ... }
|
||||
```
|
||||
|
||||
### 长期(保持混合)
|
||||
|
||||
保持 **80% Composition + 20% OOP**:
|
||||
|
||||
- **新功能**:默认 Composition API
|
||||
- **复杂逻辑**:考虑 OOP
|
||||
- **优先级**:简单 > 优雅
|
||||
|
||||
---
|
||||
|
||||
## 🎓 经验教训
|
||||
|
||||
### Vue 2 到 Vue 3 的演进
|
||||
|
||||
```
|
||||
Vue 2 Options API → Vue 3 Composition API
|
||||
(OOP 风格) (函数式风格)
|
||||
```
|
||||
|
||||
**为什么?**
|
||||
- Options API 的选项(data, methods, computed)分散逻辑
|
||||
- Composition API 可以按功能组织代码
|
||||
- 更好的 TypeScript 支持
|
||||
- 更灵活的组合
|
||||
|
||||
**如果我们全面使用 OOP:**
|
||||
- 相当于回到了 Options API 的组织方式
|
||||
- 失去 Composition API 的优势
|
||||
- 与 Vue 3 的发展方向背道而驰
|
||||
|
||||
---
|
||||
|
||||
## ✅ 最终建议
|
||||
|
||||
### ❌ 不要做的事情
|
||||
|
||||
1. **不要**全面使用 OOP
|
||||
2. **不要**为了 OOP 而 OOP
|
||||
3. **不要**与 Vue 3 生态对抗
|
||||
4. **不要**增加团队的学习负担
|
||||
|
||||
### ✅ 应该做的事情
|
||||
|
||||
1. **优先**使用 Composition API
|
||||
2. **仅在**特定场景使用 OOP:
|
||||
- 复杂状态管理
|
||||
- 严格初始化顺序
|
||||
- 依赖注入和测试
|
||||
3. **保持**简单,避免过度设计
|
||||
4. **遵循** Vue 3 官方最佳实践
|
||||
|
||||
### 🎯 当前问题的正确解决方式
|
||||
|
||||
```typescript
|
||||
// 1. 严格按顺序组织代码(已修复)
|
||||
// 工具函数 → 状态 → Composables → Computed
|
||||
|
||||
// 2. 仅 ZIP 浏览使用 OOP(可选)
|
||||
class ZipBrowserService {
|
||||
constructor(preview: FilePreviewService) {}
|
||||
}
|
||||
|
||||
// 3. 其他保持 Composition API
|
||||
function useFilePreview() { ... }
|
||||
function useFileEdit() { ... }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 总结
|
||||
|
||||
| 维度 | 全部 OOP | 混合方案 | 推荐 |
|
||||
|-----|---------|---------|------|
|
||||
| **与 Vue 3 一致性** | ❌ 低 | ✅ 高 | 混合 |
|
||||
| **代码量** | ❌ 多 | ✅ 少 | 混合 |
|
||||
| **灵活性** | ❌ 低 | ✅ 高 | 混合 |
|
||||
| **初始化保证** | ✅ 有 | ⚠️ 部分 | 看场景 |
|
||||
| **学习成本** | ❌ 高 | ✅ 低 | 混合 |
|
||||
| **维护成本** | ❌ 高 | ✅ 低 | 混合 |
|
||||
| **性能** | ⚠️ 中 | ✅ 好 | 混合 |
|
||||
| **生态兼容** | ❌ 差 | ✅ 好 | 混合 |
|
||||
|
||||
**结论:混合方案(80% Composition + 20% OOP)是最佳选择。**
|
||||
|
||||
---
|
||||
|
||||
**生成时间**: 2026-01-31
|
||||
**建议**: 保持理性,不要为了技术而技术
|
||||
Reference in New Issue
Block a user