重构:文件系统模块化架构,增强 Markdown 渲染
- 拆分 FileSystem.vue 为模块化组件架构 - 新增 Markdown Mermaid 图表渲染支持 - 新增 180+ 编程语言代码高亮 - 修复编辑/预览模式切换渲染问题 - 优化亮色/暗色模式主题适配 - 新增 TypeScript 类型定义
This commit is contained in:
508
docs/代码审查/composable-integration-failure-analysis.md
Normal file
508
docs/代码审查/composable-integration-failure-analysis.md
Normal file
@@ -0,0 +1,508 @@
|
||||
# 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%
|
||||
- 维护成本降低
|
||||
- 为长期重构打好基础
|
||||
Reference in New Issue
Block a user