- 拆分 FileSystem.vue 为模块化组件架构 - 新增 Markdown Mermaid 图表渲染支持 - 新增 180+ 编程语言代码高亮 - 修复编辑/预览模式切换渲染问题 - 优化亮色/暗色模式主题适配 - 新增 TypeScript 类型定义
14 KiB
Composable 集成失败根因分析报告
日期: 2025-01-30 目标: 分析为什么 useFileEdit 和 useFilePreview 无法集成到 FileSystem.vue
执行摘要
集成尝试失败的根本原因:Composables 和本地实现在设计哲学、API 契约、功能范围上存在系统性差异。
- ❌ useFileEdit: 不兼容(状态变量不匹配:
isEditModevsisEditableView) - ❌ 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
const canSaveFile = computed(() => {
return isEditMode.value && contentChanged.value
})
FileSystem.vue:2997
const canSaveFile = computed(() => isEditableView.value && contentChanged.value)
问题:
isEditMode: 简单的布尔值 ref,来自 localStorageisEditableView: 复杂的 computed,依赖预览状态
// 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
const isFileModified = computed(() => {
return originalContent.value !== undefined &&
originalContent.value !== fileContent.value
})
FileSystem.vue:2977-2988
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
const encodedPath = encodeURIComponent(pathToPreview)
previewUrl.value = `${fileServerURL.value}/file?path=${encodedPath}`
FileSystem.vue:1503
previewUrl.value = `${fileServerURL.value}/localfs/${normalizeFilePath(pathToPreview, true)}`
问题:
- Composable:
/file?path=xxx(查询参数格式) - FileSystem.vue:
/localfs/xxx(路径格式,需要规范化)
不兼容原因:
- 后端可能只支持其中一种格式
normalizeFilePath()可能有特殊处理(如 Windows 路径转换)
2.2 路径参数优先级差异
useFilePreview.js:148
const previewImage = async (targetPath) => {
const pathToPreview = targetPath || filePath.value // 只用 filePath
// ...
}
FileSystem.vue:1487
const previewImageLocal = async (targetPath) => {
const pathToPreview = targetPath || selectedFilePath.value || filePath.value // 三级优先级
// ...
}
三级优先级:
targetPath(显式传入)selectedFilePath(当前选中的文件)filePath(当前目录)
影响:
- Composable 在"选中文件但未传参"时会失败
- FileSystem.vue 可以自动回退到
selectedFilePath
2.3 computed 属性功能差异
currentFileName 对比:
| 功能 | useFilePreview | FileSystem.vue | 差异 |
|---|---|---|---|
| ZIP 模式支持 | ❌ 无 | ✅ 有 | 关键差异 |
| 目录检测 | ❌ 无 | ✅ 有 | UX 增强 |
| 路径截断 | ❌ 无 | ✅ 有 | UX 增强 |
| 错误处理 | ❌ 无 | ✅ try-catch | 健壮性 |
FileSystem.vue:1437-1460 (23行,包含 ZIP 逻辑)
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行,无特殊逻辑)
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)
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 为什么成功?
- 清晰的回调接口:
onListDirectory作为回调,连接到本地实现 - 状态变量简单: 只依赖
filePath,没有复杂的 computed 依赖 - 无 API 假设: 不涉及 URL 格式、网络请求等
- 功能独立: 导航逻辑不依赖预览、编辑等其他模块
3.3 集成模式
┌─────────────────┐
│ useNavigation │
└────────┬────────┘
│
│ onListDirectory(path)
▼
┌─────────────────┐
│ FileSystem.vue │
│ listDirectory()│
└─────────────────┘
这种模式清晰、解耦、易于测试。
四、根因总结
4.1 设计哲学差异
| 维度 | Composables | FileSystem.vue |
|---|---|---|
| 复杂度 | 追求简洁、纯粹 | 追求功能完整 |
| 假设 | 单一路径、标准API | 多路径源、自定义API |
| 范围 | 单一职责 | 全功能 |
| 演进 | 从头设计 | 增量演进(ZIP、新建文件等) |
4.2 API 契议不匹配
Composable 隐式假设:
// 假设 1: URL 格式
`${fileServerURL}/file?path=${encodedPath}`
// 假设 2: 路径来源
const path = filePath.value // 单一来源
// 假设 3: 状态变量
const canSave = isEditMode && changed // 简单布尔值
FileSystem.vue 实际:
// 实际 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: 保持现状 + 提取工具函数(推荐)
理由:
- 功能完整性优先
- 避免破坏性重构
- 渐进式优化
行动:
-
保留
useNavigation集成 -
删除
useFileEdit和useFilePreview(或作为参考文档) -
提取真正的通用工具函数:
// 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) -
减少调试日志(65 → 10)
方案 B: 重构 FileSystem.vue(激进)
风险: 高 时间: 2-3周 收益: 长期可维护性
步骤:
- 统一状态管理(单一
filePathvsselectedFilePath) - 标准化 API(统一 URL 格式)
- 组件化拆分(子组件)
- 然后重新集成 Composables
方案 C: 创建轻量级 Composables(折中)
// 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)
七、检查清单
立即行动(本周)
- 分析集成失败根因
- 修复
loadDraft is not defined运行时错误 - 决定方案 A/B/C
- 执行决定
短期优化(2周)
- 提取路径工具函数
- 提取文件类型判断函数
- 统一 localStorage 键名
- 减少调试日志
长期重构(1个月)
- 组件化拆分(子组件)
- 状态管理优化
- TypeScript 迁移
- 单元测试覆盖
八、关键发现
发现 1: Composables 是"理想版本"
Composables 基于理想假设设计:
- 单一路径来源
- 标准 API
- 简单状态
- 纯净功能
但 FileSystem.vue 是现实版本:
- 多路径源(历史包袱)
- 自定义 API(性能优化)
- 复杂状态(功能完整)
- 增量演进(业务需求)
发现 2: 命名体系反映演进历史
所有预览函数都有 Local 后缀:
previewImageLocal // 表明"本地实现"
previewVideoLocal // 避免"全局冲突"
这说明开发者在添加这些函数时,已经意识到可能存在外部冲突,因此添加后缀。
如果强行使用无后缀的 Composable 版本,会破坏这种防御性设计。
发现 3: useNavigation 成功的启示
useNavigation 成功的关键:
- 清晰的边界: 只负责导航历史
- 回调接口: 不直接操作文件系统
- 状态简单: 只依赖
filePath - 无副作用: 不涉及 UI 状态
教训: 如果要提取 Composables,应该遵循同样的原则。
九、最终建议
推荐:方案 A - 提取工具函数
原因:
- 风险最低: 不破坏现有功能
- 收益明确: 减少代码重复(路径处理、文件类型判断)
- 时间可控: 1周内完成
- 渐进式: 为未来重构铺路
具体行动:
// 第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%
- 维护成本降低
- 为长期重构打好基础