Private
Public Access
1
0
Files
u-desk/docs/代码审查/composable-integration-failure-analysis.md
绝尘 a5d30684ed 重构:文件系统模块化架构,增强 Markdown 渲染
- 拆分 FileSystem.vue 为模块化组件架构
- 新增 Markdown Mermaid 图表渲染支持
- 新增 180+ 编程语言代码高亮
- 修复编辑/预览模式切换渲染问题
- 优化亮色/暗色模式主题适配
- 新增 TypeScript 类型定义
2026-02-04 03:32:46 +08:00

509 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Composable 集成失败根因分析报告
**日期**: 2025-01-30
**目标**: 分析为什么 useFileEdit 和 useFilePreview 无法集成到 FileSystem.vue
---
## 执行摘要
集成尝试失败的根本原因:**Composables 和本地实现在设计哲学、API 契约、功能范围上存在系统性差异**。
-**useFileEdit**: 不兼容(状态变量不匹配:`isEditMode` vs `isEditableView`
-**useFilePreview**: 不兼容URL 格式、路径处理、ZIP 模式支持差异)
-**useNavigation**: 兼容(已成功集成)
---
## 一、useFileEdit.js vs FileSystem.vue
### 1.1 状态变量差异
| 功能点 | useFileEdit.js | FileSystem.vue | 兼容性 |
|--------|----------------|----------------|--------|
| **编辑模式开关** | `isEditMode` (简单 ref) | `isEditableView` (复杂 computed) | ❌ 不兼容 |
| **路径来源** | `filePath` (单一) | `selectedFilePath` \| `filePath` (双重) | ❌ 不兼容 |
| **文件修改检测** | 简单比较 | 复杂逻辑(含新建文件) | ❌ 不兼容 |
### 1.2 致命差异:`canSaveFile` 的条件
**useFileEdit.js:87-89**
```javascript
const canSaveFile = computed(() => {
return isEditMode.value && contentChanged.value
})
```
**FileSystem.vue:2997**
```javascript
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
```
**问题**
- `isEditMode`: 简单的布尔值 ref来自 localStorage
- `isEditableView`: 复杂的 computed依赖预览状态
```javascript
// FileSystem.vue:2968-2974
const isEditableView = computed(() => {
return !isImageView.value &&
!isVideoView.value &&
!isAudioView.value &&
!isPdfFile.value &&
!isBinaryFile.value
})
```
**影响**
- 使用 `isEditMode` → 保存按钮可能在图片预览时也显示(错误)
- 使用 `isEditableView` → 保存按钮只在文本编辑时显示(正确)
### 1.3 致命差异:`isFileModified` 的逻辑
**useFileEdit.js:71-74**
```javascript
const isFileModified = computed(() => {
return originalContent.value !== undefined &&
originalContent.value !== fileContent.value
})
```
**FileSystem.vue:2977-2988**
```javascript
const isFileModified = computed(() => {
const hasContent = fileContent.value !== '' && fileContent.value.trim() !== ''
const hasModified = selectedFilePath.value && fileContent.value !== originalContent.value
const isNewFile = !selectedFilePath.value && hasContent // ← 新建文件检测
return isEditableView.value && (hasModified || isNewFile)
})
```
**缺失功能**
- Composable 版本**不支持新建文件场景**
- FileSystem.vue 版本可以检测到"未选择文件路径但有内容"的新建文件状态
### 1.4 依赖图对比
**useFileEdit 依赖树**:
```
canSaveFile
├─ isEditMode (ref)
└─ contentChanged
├─ fileContent
└─ originalContent
```
**FileSystem.vue 依赖树**:
```
canSaveFile
├─ isEditableView (computed)
│ ├─ isImageView
│ ├─ isVideoView
│ ├─ isAudioView
│ ├─ isPdfFile
│ └─ isBinaryFile
└─ contentChanged
├─ fileContent
└─ originalContent
```
**结论**: FileSystem.vue 的依赖更复杂Composable 过于简化
---
## 二、useFilePreview.js vs FileSystem.vue
### 2.1 URL 构建差异(致命)
**useFilePreview.js:163**
```javascript
const encodedPath = encodeURIComponent(pathToPreview)
previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}`
```
**FileSystem.vue:1503**
```javascript
previewUrl.value = `${fileServerURL.value}/localfs/${normalizeFilePath(pathToPreview, true)}`
```
**问题**
- Composable: `/file?path=xxx` (查询参数格式)
- FileSystem.vue: `/localfs/xxx` (路径格式,需要规范化)
**不兼容原因**
- 后端可能只支持其中一种格式
- `normalizeFilePath()` 可能有特殊处理(如 Windows 路径转换)
### 2.2 路径参数优先级差异
**useFilePreview.js:148**
```javascript
const previewImage = async (targetPath) => {
const pathToPreview = targetPath || filePath.value // 只用 filePath
// ...
}
```
**FileSystem.vue:1487**
```javascript
const previewImageLocal = async (targetPath) => {
const pathToPreview = targetPath || selectedFilePath.value || filePath.value // 三级优先级
// ...
}
```
**三级优先级**
1. `targetPath` (显式传入)
2. `selectedFilePath` (当前选中的文件)
3. `filePath` (当前目录)
**影响**
- Composable 在"选中文件但未传参"时会失败
- FileSystem.vue 可以自动回退到 `selectedFilePath`
### 2.3 computed 属性功能差异
**currentFileName** 对比:
| 功能 | useFilePreview | FileSystem.vue | 差异 |
|------|----------------|----------------|------|
| **ZIP 模式支持** | ❌ 无 | ✅ 有 | 关键差异 |
| **目录检测** | ❌ 无 | ✅ 有 | UX 增强 |
| **路径截断** | ❌ 无 | ✅ 有 | UX 增强 |
| **错误处理** | ❌ 无 | ✅ try-catch | 健壮性 |
**FileSystem.vue:1437-1460** (23行包含 ZIP 逻辑)
```javascript
const currentFileNameDisplay = computed(() => {
if (isBrowsingZip.value && selectedFilePath.value) {
// ZIP 模式:从 zip 内路径中提取文件名
const parts = selectedFilePath.value.split('/')
return parts[parts.length - 1] || parts[parts.length - 2] || ''
}
if (selectedFilePath.value) {
// 正常模式:如果文件在当前目录,只显示文件名;否则显示完整路径
try {
if (isFileInCurrentDirectory.value) {
return getFileName(selectedFilePath.value)
} else {
return selectedFilePath.value // 返回完整路径
}
} catch (error) {
debugWarn('[currentFileName] 计算失败,返回文件名:', error)
return getFileName(selectedFilePath.value)
}
}
return ''
})
```
**useFilePreview.js:122-126** (5行无特殊逻辑)
```javascript
const currentFileName = computed(() => {
if (!filePath.value) return ''
const parts = filePath.value.split(/[/\\]/)
return parts[parts.length - 1]
})
```
### 2.4 函数命名体系差异
| 功能 | useFilePreview | FileSystem.vue |
|------|----------------|----------------|
| 图片预览 | `previewImage` | `previewImageLocal` |
| 视频预览 | `previewVideo` | `previewVideoLocal` |
| 音频预览 | `previewAudio` | `previewAudioLocal` |
| PDF 预览 | `previewPdf` | `previewPdfLocal` |
| HTML 预览 | `previewHtml` | `previewHtmlLocal` |
| Markdown 预览 | `previewMarkdown` | `previewMarkdownLocal` |
**Local 后缀的意义**
- 表明这是本地实现,避免与外部库或全局函数冲突
- 如果替换为 Composable需要全局重命名模板中的所有调用点30+ 处)
---
## 三、useNavigation.js vs FileSystem.vue
### 3.1 集成状态
**已成功集成** (FileSystem.vue:605-625)
```javascript
const {
navHistory,
navIndex,
isNavigating,
canGoBack,
canGoForward,
addToHistory,
pushNav,
goBack,
goForward,
onPathSelect,
onPathEnter,
browseDirectory,
} = useNavigation({
filePath,
onListDirectory: async (path) => {
filePath.value = path
await listDirectory()
}
})
```
### 3.2 为什么成功?
1. **清晰的回调接口**: `onListDirectory` 作为回调,连接到本地实现
2. **状态变量简单**: 只依赖 `filePath`,没有复杂的 computed 依赖
3. **无 API 假设**: 不涉及 URL 格式、网络请求等
4. **功能独立**: 导航逻辑不依赖预览、编辑等其他模块
### 3.3 集成模式
```
┌─────────────────┐
│ useNavigation │
└────────┬────────┘
│ onListDirectory(path)
┌─────────────────┐
│ FileSystem.vue │
│ listDirectory()│
└─────────────────┘
```
这种模式清晰、解耦、易于测试。
---
## 四、根因总结
### 4.1 设计哲学差异
| 维度 | Composables | FileSystem.vue |
|------|-------------|----------------|
| **复杂度** | 追求简洁、纯粹 | 追求功能完整 |
| **假设** | 单一路径、标准API | 多路径源、自定义API |
| **范围** | 单一职责 | 全功能 |
| **演进** | 从头设计 | 增量演进ZIP、新建文件等 |
### 4.2 API 契议不匹配
**Composable 隐式假设**
```javascript
// 假设 1: URL 格式
`${fileServerURL}/file?path=${encodedPath}`
// 假设 2: 路径来源
const path = filePath.value // 单一来源
// 假设 3: 状态变量
const canSave = isEditMode && changed // 简单布尔值
```
**FileSystem.vue 实际**
```javascript
// 实际 1: URL 格式
`${fileServerURL}/localfs/${normalizeFilePath(path, true)}`
// 实际 2: 路径来源
const path = targetPath || selectedFilePath || filePath // 三级优先级
// 实际 3: 状态变量
const canSave = isEditableView && changed // 复杂 computed
```
### 4.3 功能演进差距
**FileSystem.vue 独有功能**
- ✅ ZIP 文件浏览模式
- ✅ 新建文件检测
- ✅ 目录感知显示
- ✅ 路径规范化
- ✅ 文件是否在当前目录检测
**useFileEdit/useFilePreview 创建时未考虑这些功能**
---
## 五、集成失败的三个层次
### 层次 1: 语法层面(易于发现)
```
❌ ReferenceError: loadDraft is not defined
❌ Identifier 'previewImage' has already been declared
```
### 层次 2: 语义层面(运行时错误)
```
❌ 保存按钮在图片预览时也显示 (isEditMode vs isEditableView)
❌ URL 404 错误 (/file?path= vs /localfs/)
❌ 新建文件无法保存
```
### 层次 3: 设计层面(深层不兼容)
```
❌ 单一路径模型 vs 多路径源
❌ 简单布尔值 vs 复杂 computed
❌ 标准API vs 自定义API
❌ 静态功能 vs 增量演进
```
---
## 六、解决方案
### 方案 A: 保持现状 + 提取工具函数(推荐)
**理由**
- 功能完整性优先
- 避免破坏性重构
- 渐进式优化
**行动**
1. 保留 `useNavigation` 集成
2. 删除 `useFileEdit``useFilePreview`(或作为参考文档)
3. 提取真正的通用工具函数:
```javascript
// utils/pathHelpers.js
export const splitPath = (path) => path.split(/[/\\]/)
export const getFileName = (path) => { /* ... */ }
export const getParentPath = (path) => { /* ... */ }
// utils/fileHelpers.js
export const isImageFile = (ext) => FILE_EXTENSIONS.IMAGE.includes(ext)
export const isVideoFile = (ext) => FILE_EXTENSIONS.VIDEO_BROWSER.includes(ext)
```
4. 减少调试日志65 → 10
### 方案 B: 重构 FileSystem.vue激进
**风险**: 高
**时间**: 2-3周
**收益**: 长期可维护性
**步骤**
1. 统一状态管理(单一 `filePath` vs `selectedFilePath`
2. 标准化 API统一 URL 格式)
3. 组件化拆分(子组件)
4. 然后重新集成 Composables
### 方案 C: 创建轻量级 Composables折中
```javascript
// useFileEditMinimal.js
export function useFileEditMinimal({ fileContent, originalContent }) {
const contentChanged = computed(() =>
fileContent.value !== '' &&
fileContent.value !== originalContent.value
)
return { contentChanged }
}
// FileSystem.vue
const { contentChanged } = useFileEditMinimal({ fileContent, originalContent })
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
```
---
## 七、检查清单
### 立即行动(本周)
- [x] 分析集成失败根因
- [ ] 修复 `loadDraft is not defined` 运行时错误
- [ ] 决定方案 A/B/C
- [ ] 执行决定
### 短期优化2周
- [ ] 提取路径工具函数
- [ ] 提取文件类型判断函数
- [ ] 统一 localStorage 键名
- [ ] 减少调试日志
### 长期重构1个月
- [ ] 组件化拆分(子组件)
- [ ] 状态管理优化
- [ ] TypeScript 迁移
- [ ] 单元测试覆盖
---
## 八、关键发现
### 发现 1: Composables 是"理想版本"
Composables 基于**理想假设**设计:
- 单一路径来源
- 标准 API
- 简单状态
- 纯净功能
但 FileSystem.vue 是**现实版本**
- 多路径源(历史包袱)
- 自定义 API性能优化
- 复杂状态(功能完整)
- 增量演进(业务需求)
### 发现 2: 命名体系反映演进历史
所有预览函数都有 `Local` 后缀:
```javascript
previewImageLocal // 表明"本地实现"
previewVideoLocal // 避免"全局冲突"
```
这说明开发者在添加这些函数时,**已经意识到可能存在外部冲突**,因此添加后缀。
如果强行使用无后缀的 Composable 版本,会破坏这种防御性设计。
### 发现 3: useNavigation 成功的启示
useNavigation 成功的关键:
1. **清晰的边界**: 只负责导航历史
2. **回调接口**: 不直接操作文件系统
3. **状态简单**: 只依赖 `filePath`
4. **无副作用**: 不涉及 UI 状态
**教训**: 如果要提取 Composables应该遵循同样的原则。
---
## 九、最终建议
### 推荐:方案 A - 提取工具函数
**原因**
1. **风险最低**: 不破坏现有功能
2. **收益明确**: 减少代码重复(路径处理、文件类型判断)
3. **时间可控**: 1周内完成
4. **渐进式**: 为未来重构铺路
**具体行动**
```javascript
// 第1步提取工具函数
// utils/pathHelpers.js
// utils/fileTypeHelpers.js
// 第2步替换重复代码
// path.split(/[/\\/]/) → splitPath(path)
// 第3步删除未使用的 Composables
// rm useFileEdit.js useFilePreview.js
// 第4步减少调试日志
// 保留 10 个关键日志,删除 55 个
```
**预期结果**
- 代码减少 ~200 行
- DRY 评分改善 5%
- 维护成本降低
- 为长期重构打好基础