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

14 KiB
Raw Permalink Blame History

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

const canSaveFile = computed(() => {
  return isEditMode.value && contentChanged.value
})

FileSystem.vue:2997

const canSaveFile = computed(() => isEditableView.value && contentChanged.value)

问题

  • isEditMode: 简单的布尔值 ref来自 localStorage
  • isEditableView: 复杂的 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  // 三级优先级
  // ...
}

三级优先级

  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 逻辑)

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 为什么成功?

  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 隐式假设

// 假设 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: 保持现状 + 提取工具函数(推荐)

理由

  • 功能完整性优先
  • 避免破坏性重构
  • 渐进式优化

行动

  1. 保留 useNavigation 集成

  2. 删除 useFileEdituseFilePreview(或作为参考文档)

  3. 提取真正的通用工具函数:

    // 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折中

// 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 成功的关键:

  1. 清晰的边界: 只负责导航历史
  2. 回调接口: 不直接操作文件系统
  3. 状态简单: 只依赖 filePath
  4. 无副作用: 不涉及 UI 状态

教训: 如果要提取 Composables应该遵循同样的原则。


九、最终建议

推荐:方案 A - 提取工具函数

原因

  1. 风险最低: 不破坏现有功能
  2. 收益明确: 减少代码重复(路径处理、文件类型判断)
  3. 时间可控: 1周内完成
  4. 渐进式: 为未来重构铺路

具体行动

// 第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%
  • 维护成本降低
  • 为长期重构打好基础