{ "project": "MarkLite", "version": "0.1.1", "analysisDate": "2026-06-03", "techStack": { "electron": "28.3.3", "react": "18.3.1", "typescript": "5.6.3", "vite": "electron-vite 3.0.0", "stateManagement": "zustand 5.0.6", "editor": "CodeMirror 6", "database": "Dexie (IndexedDB)" }, "currentStatus": { "typescriptCheck": "PASS", "eslintConfig": "MISSING", "testCoverage": "NONE", "documentation": "NONE", "ci": "NONE" }, "codebaseStats": { "totalSourceFiles": 44, "mainProcessFiles": 5, "preloadFiles": 1, "rendererFiles": 36, "sharedFiles": 2, "components": 14, "stores": 3, "hooks": 7, "dbRepositories": 3, "typeDefinitions": 5, "styleFiles": 3 }, "optimizationRequirements": { "1_codeQuality": { "title": "代码质量与规范", "priority": "HIGH", "items": [ { "id": "CQ-01", "title": "配置 ESLint (Flat Config)", "priority": "P0-CRITICAL", "description": "package.json 中已有 @typescript-eslint/eslint-plugin、eslint、eslint-plugin-react-hooks 等依赖,但缺少 eslint.config.js 配置文件。npm run lint 无法正常工作。", "currentState": "devDependencies 已安装但无配置文件", "expectedState": "创建 eslint.config.js (ESLint v9 flat config 格式),配置 TypeScript + React + React Hooks 规则", "effort": "small", "details": { "configFormat": "ESM flat config (eslint.config.js)", "plugins": ["@typescript-eslint/eslint-plugin", "eslint-plugin-react-hooks", "eslint-plugin-react-refresh"], "rules": [ "typescript-eslint/recommended", "react-hooks/recommended", "no-unused-vars (error)", "no-console (warn)", "prefer-const" ] } }, { "id": "CQ-02", "title": "添加 Prettier 代码格式化", "priority": "P1-HIGH", "description": "项目无统一代码格式化工具,代码风格靠人工维护(缩进、引号、分号等不一致风险高)。", "currentState": "无 Prettier 配置", "expectedState": "添加 .prettierrc 配置 + 与 ESLint 集成", "effort": "small", "details": { "config": { "semi": false, "singleQuote": true, "tabWidth": 2, "trailingComma": "none", "printWidth": 100 } } }, { "id": "CQ-03", "title": "添加 .editorconfig", "priority": "P2-MEDIUM", "description": "统一编辑器行为配置,确保不同开发者环境一致。", "currentState": "无 .editorconfig", "expectedState": "添加 .editorconfig 文件", "effort": "trivial" }, { "id": "CQ-04", "title": "修复 TypeScript 严格模式下的隐式 any", "priority": "P1-HIGH", "description": "虽然 tsc --noEmit 通过,但部分回调参数缺少类型标注(如 CustomEvent detail),依赖类型推断。", "currentState": "编译通过但存在隐式类型", "expectedState": "所有函数参数显式标注类型", "effort": "medium" }, { "id": "CQ-05", "title": "统一错误处理模式", "priority": "P1-HIGH", "description": "多处 catch 块为空或仅 console.error,用户看不到有意义的错误信息。App.tsx 中 showToast('操作失败') 过于笼统。", "currentState": "分散的 try-catch,错误消息不具体", "expectedState": "统一的错误处理函数,包含具体错误描述和日志", "effort": "medium" } ] }, "2_performance": { "title": "性能优化", "priority": "HIGH", "items": [ { "id": "PF-01", "title": "Markdown 渲染处理器缓存", "priority": "P0-CRITICAL", "description": "renderMarkdown() 每次调用都创建新的 unified() 处理器管道(含 7 个插件),大文件渲染时开销显著。unified 处理器创建后应缓存复用。", "currentState": "每次渲染创建新 processor (7个插件)", "expectedState": "缓存 processor 实例,仅在 filePath 变化时重建", "effort": "small", "impact": "HIGH - 减少每次渲染的初始化开销", "details": { "location": "src/renderer/lib/markdown.ts", "solution": "使用 Map 按 filePath 缓存,或使用 useMemo 在组件层面缓存" } }, { "id": "PF-02", "title": "Zustand 选择器优化 - 避免订阅整个 tabs 数组", "priority": "P0-CRITICAL", "description": "Editor.tsx、Preview.tsx、StatusBar.tsx、Sidebar.tsx 都订阅 tabs 数组然后 .find() 查找 activeTab。每次任何 tab 的 content 变化都会触发这些组件重渲染。", "currentState": "const tabs = useTabStore(s => s.tabs); const activeTab = tabs.find(...)", "expectedState": "使用 useShallow 或自定义选择器仅订阅 activeTab", "effort": "medium", "impact": "HIGH - 减少不必要的重渲染", "affectedFiles": [ "src/renderer/components/Editor/Editor.tsx", "src/renderer/components/Preview/Preview.tsx", "src/renderer/components/StatusBar/StatusBar.tsx", "src/renderer/components/Sidebar/Sidebar.tsx" ], "solution": "在 tabStore 中添加 getActiveTab 选择器方法,或使用 zustand useShallow" }, { "id": "PF-03", "title": "React.memo 包装纯展示组件", "priority": "P1-HIGH", "description": "Toolbar、StatusBar、Icons 等纯展示组件未使用 React.memo,父组件重渲染时会连带重渲染。", "currentState": "无任何组件使用 React.memo", "expectedState": "对 Toolbar、StatusBar、TabBar、Preview 等组件使用 memo 包装", "effort": "small", "impact": "MEDIUM" }, { "id": "PF-04", "title": "CodeMirror 主题切换优化", "priority": "P1-HIGH", "description": "useCodeMirror.ts 中 darkMode 变化时销毁并重建整个 EditorView(第93行),导致编辑器完全重新初始化。", "currentState": "darkMode 变化 → 整个 EditorView 销毁重建", "expectedState": "使用 CodeMirror Compartment 动态切换主题扩展", "effort": "medium", "impact": "HIGH - 切换主题时保持编辑器状态", "details": { "location": "src/renderer/components/Editor/useCodeMirror.ts", "solution": "使用 @codemirror/state 的 Compartment API 动态 reconfigure 主题扩展" } }, { "id": "PF-05", "title": "CodeMirror 内容更新优化", "priority": "P2-MEDIUM", "description": "setContent() 使用 from:0 到 length 全量替换,对大文件(10万行+)性能差。应使用事务 diff 或仅在内容实际变化时替换。", "currentState": "全量替换: changes: { from: 0, to: current.length, insert: newContent }", "expectedState": "仅在内容不同时替换,大文件考虑增量更新", "effort": "medium" }, { "id": "PF-06", "title": "Bundle 大小优化 - 代码分割", "priority": "P2-MEDIUM", "description": "所有渲染进程代码打包为单一 bundle。rehype-highlight (含 highlight.js)、CodeMirror 等大型库未做懒加载。", "currentState": "单 bundle,无代码分割", "expectedState": "动态 import 预览相关库、分割 vendor chunk", "effort": "medium", "details": { "chunks": [ "vendor-react: react + react-dom", "vendor-codemirror: @codemirror/*", "vendor-markdown: unified + remark + rehype 生态", "app: 业务代码" ] } }, { "id": "PF-07", "title": "Preview 渲染防抖", "priority": "P2-MEDIUM", "description": "Preview.tsx 在 activeTab?.content 变化时立即触发异步渲染。快速输入时会频繁调用 renderMarkdown。", "currentState": "content 变化立即渲染", "expectedState": "添加 150-300ms 防抖,避免快速输入时频繁渲染", "effort": "small" }, { "id": "PF-08", "title": "保存到 IndexedDB 的去抖优化", "priority": "P2-MEDIUM", "description": "tabStore 中多处使用 setTimeout(() => saveToDB(), 0) 异步保存,但没有去抖。频繁操作时会多次写入。", "currentState": "setTimeout(fn, 0) 立即触发", "expectedState": "使用 debounce (300-500ms) 合并多次保存", "effort": "small" } ] }, "3_architecture": { "title": "架构优化", "priority": "MEDIUM", "items": [ { "id": "AR-01", "title": "App.tsx 拆分 - About 对话框提取", "priority": "P1-HIGH", "description": "App.tsx 有 307 行,About 对话框 JSX 内联(约 40 行),文件打开/保存逻辑也堆积在组件中。", "currentState": "App.tsx 包含 About 弹窗、文件操作、事件监听等所有逻辑", "expectedState": "提取 AboutDialog 组件、提取 useFileOperations hook", "effort": "medium", "details": { "extractions": [ { "name": "AboutDialog", "from": "App.tsx lines 261-300", "to": "src/renderer/components/AboutDialog/AboutDialog.tsx" }, { "name": "useFileOperations", "description": "提取 handleOpenFile, handleSave, handleSaveAs, handleOpenRecent 为自定义 hook", "to": "src/renderer/hooks/useFileOperations.ts" } ] } }, { "id": "AR-02", "title": "Sidebar 组件拆分", "priority": "P1-HIGH", "description": "Sidebar.tsx 有 241 行,包含 FileTree 递归组件、resize 逻辑、独立文件列表等。应拆分为独立文件。", "currentState": "FileTree 组件定义在 Sidebar.tsx 内", "expectedState": "FileTree 提取为独立组件,resize 逻辑提取为 hook", "effort": "medium", "details": { "extractions": [ { "name": "FileTree", "from": "Sidebar.tsx lines 180-239", "to": "src/renderer/components/Sidebar/FileTree.tsx" }, { "name": "useSidebarResize", "description": "提取 resize 相关逻辑为自定义 hook", "to": "src/renderer/hooks/useSidebarResize.ts" } ] } }, { "id": "AR-03", "title": "统一设置加载 - 消除 useTheme 和 useSettings 的重复加载", "priority": "P2-MEDIUM", "description": "useTheme 和 useSettings 各自独立调用 settingsRepository.load(),导致启动时两次 IndexedDB 读取。", "currentState": "useTheme.load() + useSettings.load() = 两次 DB 读取", "expectedState": "统一为单个 useSettingsInit hook,一次加载分发到 store", "effort": "small" }, { "id": "AR-04", "title": "消除 DOM CustomEvent 中间层", "priority": "P2-MEDIUM", "description": "useFileWatch.ts 通过 window.dispatchEvent(CustomEvent) 传递文件修改事件,App.tsx 再 addEventListener 监听。应直接通过 Zustand store 或回调传递。", "currentState": "IPC → CustomEvent DOM → App.tsx listener", "expectedState": "IPC → Zustand store action → 组件响应", "effort": "small" }, { "id": "AR-05", "title": "添加 React ErrorBoundary", "priority": "P1-HIGH", "description": "无全局错误边界。任何渲染错误会导致白屏。Electron 应用中尤其需要,因为用户无法刷新页面恢复。", "currentState": "无 ErrorBoundary", "expectedState": "添加 ErrorBoundary 组件包裹 App,显示错误信息和重启按钮", "effort": "small" }, { "id": "AR-06", "title": "Icons 组件按需加载", "priority": "P2-MEDIUM", "description": "Icons.tsx 有 216 行,包含 16 个 SVG 图标组件。虽然是函数组件有 tree-shaking,但单文件过大影响维护。", "currentState": "16 个图标定义在单文件", "expectedState": "按功能模块拆分: toolbar-icons.tsx, sidebar-icons.tsx, app-icons.tsx", "effort": "small" }, { "id": "AR-07", "title": "tabStore 异步操作模式统一", "priority": "P2-MEDIUM", "description": "tabStore 中大量使用 setTimeout(() => saveToDB(), 0) 和 setTimeout(() => tabRepository.saveActiveTabId(), 0)。模式不统一且容易出错。", "currentState": "分散的 setTimeout 调用", "expectedState": "使用 Zustand middleware (subscribeWithSelector) 或统一的 persist 逻辑", "effort": "medium" } ] }, "4_userExperience": { "title": "用户体验", "priority": "MEDIUM", "items": [ { "id": "UX-01", "title": "替换原生 confirm() 为自定义确认对话框", "priority": "P1-HIGH", "description": "TabBar.tsx 和 useUnsavedWarning.ts 使用浏览器原生 confirm() 对话框,样式丑陋且不可自定义。Electron 应用应使用自定义模态框或 electron.dialog。", "currentState": "5 处使用 confirm() 原生对话框", "expectedState": "自定义 ConfirmDialog 组件或使用 electron.dialog.showMessageBox", "effort": "medium", "affectedLocations": [ "src/renderer/components/TabBar/TabBar.tsx (4处)", "src/renderer/hooks/useUnsavedWarning.ts (1处)" ] }, { "id": "UX-02", "title": "添加加载状态指示", "priority": "P1-HIGH", "description": "文件打开、目录树加载、Markdown 渲染等异步操作无加载状态。用户无法感知操作是否正在进行。", "currentState": "无 loading 状态", "expectedState": "添加文件打开 spinner、目录树加载骨架屏、预览渲染指示器", "effort": "medium" }, { "id": "UX-03", "title": "添加编辑器+预览分屏模式", "priority": "P2-MEDIUM", "description": "当前只有 'editor' 和 'preview' 两种模式,缺少 VS Code 风格的左右分屏同时编辑和预览。", "currentState": "viewMode: 'editor' | 'preview'", "expectedState": "新增 'split' 模式: 左侧编辑器 + 右侧实时预览", "effort": "large", "details": { "implementation": [ "Settings.ViewMode 新增 'split'", "Editor 和 Preview 同时渲染", "编辑器滚动同步预览", "Ctrl+3 快捷键" ] } }, { "id": "UX-04", "title": "TabBar 拖拽排序", "priority": "P3-LOW", "description": "标签页不支持拖拽排序,无法自定义标签顺序。", "currentState": "标签页固定顺序", "expectedState": "支持拖拽重新排列标签页", "effort": "large" }, { "id": "UX-05", "title": "改进 Toast 通知系统", "priority": "P2-MEDIUM", "description": "当前 Toast 仅支持单条消息,3秒后消失。不支持不同类型(success/error/warning)和堆叠。", "currentState": "单条 toast,无类型区分", "expectedState": "支持多条堆叠、不同类型(成功/错误/警告)、可关闭", "effort": "small" }, { "id": "UX-06", "title": "添加文件搜索功能 (Ctrl+P)", "priority": "P3-LOW", "description": "无快速文件搜索/打开功能。当打开文件夹后,应支持按文件名快速搜索打开。", "currentState": "仅支持文件夹树浏览", "expectedState": "Ctrl+P 快速搜索面板,模糊匹配文件名", "effort": "large" }, { "id": "UX-07", "title": "可访问性增强", "priority": "P2-MEDIUM", "description": "部分组件缺少 ARIA 属性。Sidebar resize handle 无键盘支持。文件树展开/折叠无键盘导航。", "currentState": "基本的 role/aria-label 已添加", "expectedState": "完整的键盘导航、focus 管理、屏幕阅读器支持", "effort": "medium", "details": { "missing": [ "resize handle 需要 role='separator' + 键盘调整", "文件树需要 aria-expanded、aria-owns", "右键菜单需要 role='menu' + 键盘导航", "About 对话框需要 focus trap" ] } } ] }, "5_developmentExperience": { "title": "开发体验", "priority": "MEDIUM", "items": [ { "id": "DX-01", "title": "添加单元测试框架", "priority": "P1-HIGH", "description": "项目零测试覆盖。核心逻辑(tabStore、fileUtils、markdown 渲染)应有单元测试。", "currentState": "无测试文件、无测试框架", "expectedState": "Vitest + React Testing Library,核心模块测试覆盖 > 60%", "effort": "large", "details": { "framework": "vitest + @testing-library/react + jsdom", "priority_test_targets": [ "src/renderer/stores/tabStore.ts - 标签页 CRUD 逻辑", "src/renderer/lib/fileUtils.ts - 文件名/路径工具", "src/renderer/lib/markdown.ts - Markdown 渲染", "src/renderer/db/schema.ts - 数据库 schema", "src/main/file-system.ts - 文件系统操作" ], "scripts": { "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage" } } }, { "id": "DX-02", "title": "强化 preload 类型安全", "priority": "P2-MEDIUM", "description": "types/ipc.ts 定义了 ElectronAPI 接口和 IpcInvokeMap,但 preload/index.ts 未使用这些类型约束。contextBridge.exposeInMainWorld 返回值无类型检查。", "currentState": "preload 使用字面量对象,未引用 ElectronAPI 类型", "expectedState": "preload 实现强制匹配 ElectronAPI 接口类型", "effort": "small", "details": { "solution": "const api: ElectronAPI = { ... } 在 preload 中强制类型检查" } }, { "id": "DX-03", "title": "添加类型安全的 IPC 通信层", "priority": "P2-MEDIUM", "description": "types/ipc.ts 定义了 IpcInvokeMap 但未被使用。应建立类型安全的 IPC 调用包装器。", "currentState": "IpcInvokeMap 已定义但未使用", "expectedState": "渲染进程通过类型安全的 invoke 函数调用 IPC", "effort": "medium" }, { "id": "DX-04", "title": "添加项目文档", "priority": "P2-MEDIUM", "description": "无 README.md、无 CONTRIBUTING.md、无开发环境搭建文档。", "currentState": "无文档", "expectedState": "README.md 含项目介绍、安装步骤、开发指南、构建说明", "effort": "small", "details": { "files": [ "README.md - 项目介绍、截图、安装、开发、构建", "CONTRIBUTING.md - 贡献指南、代码规范、PR 流程", "CHANGELOG.md - 版本变更记录" ] } }, { "id": "DX-05", "title": "添加 Git hooks (husky + lint-staged)", "priority": "P2-MEDIUM", "description": "无 pre-commit 钩子,代码质量无法在提交时保证。", "currentState": "无 Git hooks", "expectedState": "husky + lint-staged: 提交前自动运行 ESLint + 类型检查", "effort": "small" }, { "id": "DX-06", "title": "添加 CI/CD 配置", "priority": "P3-LOW", "description": "无持续集成配置。应添加 GitHub Actions 或 Gitee CI 配置。", "currentState": "无 CI 配置", "expectedState": "CI 流水线: lint → typecheck → test → build", "effort": "small" }, { "id": "DX-07", "title": "添加路径别名到 Vite 配置", "priority": "P3-LOW", "description": "tsconfig.json 定义了 @renderer/*、@main/*、@shared/* 路径别名,但 electron.vite.config.ts 中 renderer 的 alias 只有 @renderer 和 @shared,缺少 @main。", "currentState": "renderer alias 缺少 @main 映射", "expectedState": "补全所有路径别名映射", "effort": "trivial" } ] }, "6_security": { "title": "安全加固", "priority": "HIGH", "items": [ { "id": "SE-01", "title": "CSP (Content Security Policy) 配置", "priority": "P1-HIGH", "description": "未配置 CSP。Preview 使用 dangerouslySetInnerHTML 渲染 HTML,虽然 rehype-sanitize 做了清理,但缺少 CSP 作为第二道防线。", "currentState": "无 CSP 配置", "expectedState": "在 index.html 和 BrowserWindow 中配置严格 CSP", "effort": "small" }, { "id": "SE-02", "title": "Preview 中 file:// 协议安全", "priority": "P1-HIGH", "description": "rehypeFixImages 将相对路径图片转为 file:// URL。虽然拒绝了 '..' 但未做更严格的路径校验(如 symlink 跟踪)。", "currentState": "仅检查 '..' 路径遍历", "expectedState": "使用 path.resolve + 检查结果路径是否在允许目录内", "effort": "small" }, { "id": "SE-03", "title": "Electron 安全最佳实践检查", "priority": "P2-MEDIUM", "description": "需确认 BrowserWindow 配置中 webPreferences 的安全设置(nodeIntegration、contextIsolation、sandbox 等)。", "currentState": "未检查 window-manager.ts 中的 webPreferences", "expectedState": "确保 nodeIntegration=false、contextIsolation=true、sandbox=true", "effort": "small" } ] } }, "implementationPlan": { "phase1_foundation": { "name": "基础补全 (1-2天)", "priority": "P0", "items": ["CQ-01", "CQ-02", "CQ-03", "AR-05", "DX-07"], "description": "ESLint + Prettier + EditorConfig + ErrorBoundary + 路径别名修复" }, "phase2_criticalPerf": { "name": "关键性能修复 (1-2天)", "priority": "P0", "items": ["PF-01", "PF-02", "PF-04", "PF-07", "PF-08"], "description": "Markdown 缓存 + Zustand 选择器 + CodeMirror Compartment + 防抖" }, "phase3_codeQuality": { "name": "代码质量提升 (2-3天)", "priority": "P1", "items": ["CQ-04", "CQ-05", "AR-01", "AR-02", "AR-03", "AR-04", "PF-03"], "description": "类型完善 + 错误处理统一 + 组件拆分 + 重复逻辑消除" }, "phase4_userExperience": { "name": "用户体验改进 (2-3天)", "priority": "P1", "items": ["UX-01", "UX-02", "UX-05", "UX-07"], "description": "自定义确认框 + 加载状态 + Toast 改进 + 可访问性" }, "phase5_developerExperience": { "name": "开发体验 (2-3天)", "priority": "P2", "items": ["DX-01", "DX-02", "DX-04", "DX-05"], "description": "单元测试 + 类型安全 + 文档 + Git hooks" }, "phase6_advanced": { "name": "高级功能 (3-5天)", "priority": "P2-P3", "items": ["PF-06", "UX-03", "UX-04", "UX-06", "SE-01", "SE-02", "SE-03", "DX-06"], "description": "代码分割 + 分屏模式 + 拖拽排序 + 搜索 + 安全加固 + CI" } }, "riskAssessment": { "highRisk": [ { "id": "PF-04", "risk": "CodeMirror Compartment 重构可能影响编辑器初始化和标签切换逻辑", "mitigation": "保留现有逻辑作为 fallback,逐步迁移到 Compartment" }, { "id": "UX-03", "risk": "分屏模式需要同时维护两个编辑器实例,内存和性能需关注", "mitigation": "使用虚拟化、延迟加载预览面板" } ], "mediumRisk": [ { "id": "PF-02", "risk": "Zustand 选择器重构可能遗漏某些订阅点", "mitigation": "使用 React DevTools Profiler 验证重渲染情况" }, { "id": "SE-01", "risk": "CSP 配置过于严格可能阻断 CodeMirror 样式注入", "mitigation": "使用 nonce 或 hash 方式允许 CodeMirror 内联样式" } ] } }