Files
MarkLite/docs/optimization-architecture.json
T
thzxx 7a4e2b0a67 v0.2.0: 全面代码质量优化
- ESLint flat config + Prettier + EditorConfig
- Markdown处理器LRU缓存
- Zustand选择器优化减少重渲染
- CodeMirror Compartment主题热切换
- Preview防抖(150ms) + IndexedDB去抖(500ms)
- 组件拆分: App.tsx 310→104行, Sidebar.tsx 244→90行
- 统一错误处理 errorHandler.ts
- ConfirmDialog替代原生confirm
- LoadingSpinner加载状态
- Toast多条堆叠+类型区分
- 可访问性增强(ARIA属性、键盘导航)
- Vitest测试框架(78个测试用例)
- Git hooks(husky + lint-staged)
- 项目文档(README.md, CONTRIBUTING.md)
2026-06-03 22:13:32 +08:00

974 lines
40 KiB
JSON
Raw Permalink 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.
{
"planVersion": "1.0.0",
"generatedDate": "2026-06-03",
"project": "MarkLite",
"basedOnRequirements": "docs/optimization-requirements.json",
"optimization_phases": [
{
"phase": 1,
"name": "基础设施与代码规范",
"priority": "P0-CRITICAL",
"estimatedEffort": "1-2 天",
"description": "建立代码质量基线:ESLint + Prettier + EditorConfig + ErrorBoundary + 路径别名修复。这些是后续所有优化的前提。",
"items": ["CQ-01", "CQ-02", "CQ-03", "AR-05", "DX-07"],
"rationale": "CQ-01 (ESLint) 是所有代码质量改进的基础;AR-05 (ErrorBoundary) 保护后续重构不会导致白屏;DX-07 (路径别名) 修复后简化后续 import。",
"exitCriteria": [
"npm run lint 通过且返回 0",
"npm run format 能格式化全部源文件",
"ErrorBoundary 包裹 App 组件",
"TypeScript 路径别名在 vite config 中完整映射"
]
},
{
"phase": 2,
"name": "关键性能修复",
"priority": "P0-CRITICAL",
"estimatedEffort": "1-2 天",
"description": "修复影响日常使用的核心性能瓶颈:Markdown 处理器缓存、Zustand 选择器重渲染、CodeMirror Compartment 主题切换、Preview 防抖、IndexedDB 去抖。",
"items": ["PF-01", "PF-02", "PF-04", "PF-07", "PF-08"],
"rationale": "PF-01 和 PF-02 是 P0-CRITICAL 级性能问题,直接影响用户感知;PF-04 的 Compartment 重构是 PF-03 (React.memo) 的前置;PF-07/08 是低风险高收益的防抖优化。",
"exitCriteria": [
"Markdown processor 实例复用率 > 90%",
"编辑时仅 activeTab 相关组件重渲染(React DevTools Profiler 验证)",
"darkMode 切换不销毁 EditorView",
"Preview 渲染频率 < 10 次/秒(快速输入场景)",
"IndexedDB 写入频率 < 3 次/秒(连续编辑场景)"
]
},
{
"phase": 3,
"name": "架构重构与代码质量",
"priority": "P1-HIGH",
"estimatedEffort": "2-3 天",
"description": "组件拆分、TypeScript 类型完善、错误处理统一、消除架构反模式(DOM CustomEvent、重复设置加载)。",
"items": ["AR-01", "AR-02", "AR-03", "AR-04", "CQ-04", "CQ-05", "PF-03"],
"rationale": "组件拆分 (AR-01/02) 和 React.memo (PF-03) 依赖 Phase 1 的 ESLint 和 Phase 2 的 Zustand 优化;类型完善 (CQ-04) 和错误处理 (CQ-05) 可与架构重构并行。",
"exitCriteria": [
"App.tsx < 150 行",
"Sidebar.tsx < 120 行",
"所有函数参数有显式类型标注",
"无空 catch 块,所有错误有具体描述",
"设置加载只触发一次 IndexedDB 读取"
]
},
{
"phase": 4,
"name": "用户体验改进",
"priority": "P1-HIGH",
"estimatedEffort": "2-3 天",
"description": "替换原生 confirm()、添加加载状态指示、改进 Toast 通知系统、增强可访问性。",
"items": ["UX-01", "UX-02", "UX-05", "UX-07"],
"rationale": "UX 改进依赖 Phase 3 的组件拆分(新的 ConfirmDialog、Toast 组件需要独立目录)和 ErrorBoundary(统一错误处理模式)。",
"exitCriteria": [
"零处原生 confirm() 调用",
"文件打开、目录加载、Markdown 渲染有 loading 状态",
"Toast 支持多条堆叠 + 类型区分 (success/error/warning)",
"所有交互元素有 ARIA 属性"
]
},
{
"phase": 5,
"name": "开发体验与测试",
"priority": "P2-MEDIUM",
"estimatedEffort": "2-3 天",
"description": "建立单元测试框架、强化类型安全、添加项目文档和 Git hooks。",
"items": ["DX-01", "DX-02", "DX-04", "DX-05"],
"rationale": "测试框架应在代码稳定后添加(Phase 1-4 的重构会导致测试频繁失效);DX-02 (preload 类型安全) 依赖 CQ-04 (类型完善)。",
"exitCriteria": [
"Vitest + React Testing Library 可运行",
"核心模块测试覆盖 > 60%",
"README.md 包含完整开发指南",
"pre-commit 钩子执行 ESLint + typecheck"
]
},
{
"phase": 6,
"name": "高级功能与安全加固",
"priority": "P2-P3",
"estimatedEffort": "3-5 天",
"description": "代码分割、分屏模式、拖拽排序、文件搜索、安全加固 (CSP/file:// 协议)、CI/CD 配置。",
"items": ["PF-06", "UX-03", "UX-04", "UX-06", "SE-01", "SE-02", "SE-03", "DX-06"],
"rationale": "这些是「锦上添花」的优化,不阻塞核心功能。代码分割 (PF-06) 和分屏模式 (UX-03) 是大规模功能,需要在架构稳定后实施。",
"exitCriteria": [
"renderer bundle 拆分为 3+ 个 chunk",
"分屏模式可正常编辑+预览",
"CSP header 配置且预览正常渲染",
"GitHub Actions CI 流水线 lint → typecheck → test → build"
]
}
],
"file_manifest": {
"new_files": [
{
"path": "eslint.config.js",
"phase": 1,
"item": "CQ-01",
"description": "ESLint v9 flat config 格式配置文件",
"size_estimate": "small"
},
{
"path": ".prettierrc",
"phase": 1,
"item": "CQ-02",
"description": "Prettier 代码格式化配置",
"size_estimate": "trivial"
},
{
"path": ".prettierignore",
"phase": 1,
"item": "CQ-02",
"description": "Prettier 忽略文件列表",
"size_estimate": "trivial"
},
{
"path": ".editorconfig",
"phase": 1,
"item": "CQ-03",
"description": "编辑器统一行为配置",
"size_estimate": "trivial"
},
{
"path": "src/renderer/components/ErrorBoundary/ErrorBoundary.tsx",
"phase": 1,
"item": "AR-05",
"description": "React ErrorBoundary 组件,显示错误信息和重启按钮",
"size_estimate": "small"
},
{
"path": "src/renderer/components/ErrorBoundary/index.ts",
"phase": 1,
"item": "AR-05",
"description": "ErrorBoundary barrel export",
"size_estimate": "trivial"
},
{
"path": "src/renderer/components/AboutDialog/AboutDialog.tsx",
"phase": 3,
"item": "AR-01",
"description": "从 App.tsx 提取的 About 对话框组件",
"size_estimate": "medium"
},
{
"path": "src/renderer/components/AboutDialog/index.ts",
"phase": 3,
"item": "AR-01",
"description": "AboutDialog barrel export",
"size_estimate": "trivial"
},
{
"path": "src/renderer/hooks/useFileOperations.ts",
"phase": 3,
"item": "AR-01",
"description": "从 App.tsx 提取的文件操作 hookopen/save/saveAs/openRecent",
"size_estimate": "medium"
},
{
"path": "src/renderer/components/Sidebar/FileTree.tsx",
"phase": 3,
"item": "AR-02",
"description": "从 Sidebar.tsx 提取的递归文件树组件",
"size_estimate": "medium"
},
{
"path": "src/renderer/hooks/useSidebarResize.ts",
"phase": 3,
"item": "AR-02",
"description": "从 Sidebar.tsx 提取的 resize 逻辑 hook",
"size_estimate": "small"
},
{
"path": "src/renderer/components/ConfirmDialog/ConfirmDialog.tsx",
"phase": 4,
"item": "UX-01",
"description": "自定义确认对话框组件,替代原生 confirm()",
"size_estimate": "medium"
},
{
"path": "src/renderer/components/ConfirmDialog/index.ts",
"phase": 4,
"item": "UX-01",
"description": "ConfirmDialog barrel export",
"size_estimate": "trivial"
},
{
"path": "src/renderer/hooks/useConfirm.ts",
"phase": 4,
"item": "UX-01",
"description": "确认对话框状态管理 hook",
"size_estimate": "small"
},
{
"path": "src/renderer/components/LoadingSpinner/LoadingSpinner.tsx",
"phase": 4,
"item": "UX-02",
"description": "通用加载指示器组件",
"size_estimate": "small"
},
{
"path": "src/renderer/components/icons/toolbar-icons.tsx",
"phase": 3,
"item": "AR-06",
"description": "工具栏相关 SVG 图标组件",
"size_estimate": "small"
},
{
"path": "src/renderer/components/icons/sidebar-icons.tsx",
"phase": 3,
"item": "AR-06",
"description": "侧边栏相关 SVG 图标组件",
"size_estimate": "small"
},
{
"path": "src/renderer/components/icons/app-icons.tsx",
"phase": 3,
"item": "AR-06",
"description": "应用级 SVG 图标组件",
"size_estimate": "small"
},
{
"path": "src/renderer/components/icons/index.ts",
"phase": 3,
"item": "AR-06",
"description": "Icons barrel export(保持向后兼容)",
"size_estimate": "trivial"
},
{
"path": "src/renderer/lib/markdown-processor.ts",
"phase": 2,
"item": "PF-01",
"description": "Markdown processor 缓存管理器",
"size_estimate": "small"
},
{
"path": "src/renderer/hooks/useSettingsInit.ts",
"phase": 3,
"item": "AR-03",
"description": "统一设置加载 hook,一次 IndexedDB 读取分发到 store",
"size_estimate": "small"
},
{
"path": "vitest.config.ts",
"phase": 5,
"item": "DX-01",
"description": "Vitest 测试框架配置",
"size_estimate": "small"
},
{
"path": "src/renderer/stores/__tests__/tabStore.test.ts",
"phase": 5,
"item": "DX-01",
"description": "tabStore 单元测试",
"size_estimate": "medium"
},
{
"path": "src/renderer/lib/__tests__/fileUtils.test.ts",
"phase": 5,
"item": "DX-01",
"description": "fileUtils 单元测试",
"size_estimate": "small"
},
{
"path": "src/renderer/lib/__tests__/markdown.test.ts",
"phase": 5,
"item": "DX-01",
"description": "Markdown 渲染单元测试",
"size_estimate": "medium"
},
{
"path": "README.md",
"phase": 5,
"item": "DX-04",
"description": "项目介绍、安装、开发、构建说明",
"size_estimate": "medium"
},
{
"path": "CONTRIBUTING.md",
"phase": 5,
"item": "DX-04",
"description": "贡献指南、代码规范、PR 流程",
"size_estimate": "small"
},
{
"path": ".github/workflows/ci.yml",
"phase": 6,
"item": "DX-06",
"description": "GitHub Actions CI 配置",
"size_estimate": "small"
}
],
"modified_files": [
{
"path": "package.json",
"phase": [1, 2, 5],
"item": ["CQ-02", "DX-05", "DX-01"],
"changes": [
"添加 prettier、eslint-config-prettier 依赖",
"添加 vitest、@testing-library/react、jsdom 依赖",
"添加 husky、lint-staged 依赖",
"添加 test、test:watch、test:coverage、format 脚本"
]
},
{
"path": "electron.vite.config.ts",
"phase": [1, 6],
"item": ["DX-07", "PF-06"],
"changes": [
"renderer.resolve.alias 添加 @main 映射",
"renderer.build.rollupOptions.output 添加 manualChunks 分割策略"
]
},
{
"path": "src/renderer/main.tsx",
"phase": 1,
"item": "AR-05",
"changes": [
"用 ErrorBoundary 包裹 App 组件"
]
},
{
"path": "src/renderer/App.tsx",
"phase": [1, 3],
"item": ["AR-01", "AR-05", "CQ-05"],
"changes": [
"提取 About 对话框到 AboutDialog 组件",
"提取文件操作逻辑到 useFileOperations hook",
"统一错误处理模式"
]
},
{
"path": "src/renderer/lib/markdown.ts",
"phase": 2,
"item": "PF-01",
"changes": [
"引入 markdown-processor.ts 缓存管理器",
"renderMarkdown 改为使用缓存的 processor 实例",
"添加 processor 缓存失效策略(仅 filePath 变化时重建)"
]
},
{
"path": "src/renderer/stores/tabStore.ts",
"phase": [2, 3],
"item": ["PF-02", "PF-08", "AR-07"],
"changes": [
"添加 getActiveTab() 选择器方法(返回 activeTab 对象引用)",
"添加 getActiveTabId() 选择器方法",
"将 setTimeout(saveToDB, 0) 替换为 debounce(saveToDB, 500)",
"统一异步保存模式为 Zustand subscribeWithSelector middleware"
]
},
{
"path": "src/renderer/components/Editor/Editor.tsx",
"phase": 2,
"item": "PF-02",
"changes": [
"替换 const tabs = useTabStore(s => s.tabs) 为 const activeTab = useTabStore(s => s.getActiveTab())",
"消除 tabs.find() 调用"
]
},
{
"path": "src/renderer/components/Preview/Preview.tsx",
"phase": [2],
"item": ["PF-02", "PF-07"],
"changes": [
"替换 tabs 订阅为 activeTab 选择器",
"添加 150ms 防抖(useDebounce hook",
"使用新的 markdown processor 缓存"
]
},
{
"path": "src/renderer/components/StatusBar/StatusBar.tsx",
"phase": 2,
"item": "PF-02",
"changes": [
"替换 tabs 订阅为 activeTab 选择器"
]
},
{
"path": "src/renderer/components/Sidebar/Sidebar.tsx",
"phase": [2, 3],
"item": ["PF-02", "AR-02"],
"changes": [
"替换 tabs 订阅为 activeTab 选择器",
"提取 FileTree 组件到独立文件",
"提取 resize 逻辑到 useSidebarResize hook"
]
},
{
"path": "src/renderer/components/Editor/useCodeMirror.ts",
"phase": 2,
"item": "PF-04",
"changes": [
"引入 Compartment API@codemirror/state",
"theme 扩展放入 Compartment",
"darkMode 变化时通过 compartment.reconfigure() 切换主题",
"移除 EditorView 销毁重建逻辑"
]
},
{
"path": "src/renderer/components/Toolbar/Toolbar.tsx",
"phase": 3,
"item": "PF-03",
"changes": [
"用 React.memo 包装组件导出"
]
},
{
"path": "src/renderer/components/TabBar/TabBar.tsx",
"phase": [3, 4],
"item": ["PF-03", "UX-01"],
"changes": [
"用 React.memo 包装组件导出",
"替换 4 处 confirm() 为自定义 ConfirmDialog"
]
},
{
"path": "src/renderer/hooks/useUnsavedWarning.ts",
"phase": 4,
"item": "UX-01",
"changes": [
"替换 confirm() 为 electron.dialog.showMessageBox 或自定义 ConfirmDialog"
]
},
{
"path": "src/renderer/hooks/useTheme.ts",
"phase": 3,
"item": "AR-03",
"changes": [
"移除独立的 settingsRepository.load() 调用",
"改为从统一的 useSettingsInit hook 获取设置"
]
},
{
"path": "src/renderer/hooks/useSettings.ts",
"phase": 3,
"item": "AR-03",
"changes": [
"移除独立的 settingsRepository.load() 调用",
"改为从统一的 useSettingsInit hook 获取设置"
]
},
{
"path": "src/renderer/hooks/useFileWatch.ts",
"phase": 3,
"item": "AR-04",
"changes": [
"移除 window.dispatchEvent(CustomEvent) 调用",
"改为调用 Zustand store action 更新文件状态"
]
},
{
"path": "src/renderer/components/Icons.tsx",
"phase": 3,
"item": "AR-06",
"changes": [
"拆分为 icons/toolbar-icons.tsx、icons/sidebar-icons.tsx、icons/app-icons.tsx",
"原文件改为 re-export barrel 文件(向后兼容)"
]
},
{
"path": "src/renderer/components/Toast/Toast.tsx",
"phase": 4,
"item": "UX-05",
"changes": [
"支持多条堆叠 Toast",
"添加 success/error/warning 类型",
"添加手动关闭按钮"
]
},
{
"path": "src/renderer/styles/global.css",
"phase": 4,
"item": ["UX-01", "UX-02", "UX-05"],
"changes": [
"添加 ConfirmDialog 样式",
"添加 LoadingSpinner 样式",
"添加 Toast 堆叠和类型样式"
]
},
{
"path": "src/preload/index.ts",
"phase": 5,
"item": "DX-02",
"changes": [
"添加 ElectronAPI 类型约束: const api: ElectronAPI = { ... }",
"确保 contextBridge.exposeInMainWorld 返回值类型匹配"
]
},
{
"path": "src/renderer/types/settings.ts",
"phase": 6,
"item": "UX-03",
"changes": [
"ViewMode 类型扩展为 'editor' | 'preview' | 'split'"
]
},
{
"path": "src/renderer/index.html",
"phase": 6,
"item": "SE-01",
"changes": [
"添加 CSP meta tag 或在 main process BrowserWindow 中配置 CSP header"
]
},
{
"path": "src/main/window-manager.ts",
"phase": 6,
"item": "SE-01",
"changes": [
"在 BrowserWindow webPreferences 中配置 CSP header",
"确认 nodeIntegration=false, contextIsolation=true, sandbox=true"
]
},
{
"path": "src/renderer/lib/markdown.ts",
"phase": 6,
"item": "SE-02",
"changes": [
"rehypeFixImages 中的路径校验从简单 '..' 检查改为 path.resolve + allowlist 检查"
]
}
],
"deleted_files": []
},
"technical_solutions": {
"CQ-01_eslint_config": {
"id": "CQ-01",
"title": "ESLint v9 Flat Config",
"solution": "创建 eslint.config.js,使用 ESM flat config 格式。",
"implementation": {
"file": "eslint.config.js",
"format": "ESM (export default [...])",
"config_content": {
"imports": [
"eslint-plugin-react-hooks",
"eslint-plugin-react-refresh",
"@typescript-eslint/eslint-plugin"
],
"ignores": ["dist/**", "node_modules/**", "out/**"],
"languageOptions": {
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": { "jsx": true }
}
},
"rules": {
"@typescript-eslint/no-unused-vars": "error",
"no-console": "warn",
"prefer-const": "error",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"react-refresh/only-export-components": ["warn", { "allowConstantExport": true }]
}
},
"notes": "ESLint v9 不再支持 --ext 参数,flat config 中通过 files glob 控制。需更新 package.json 的 lint 脚本。"
},
"dependencies": [],
"risk": "LOW"
},
"PF-01_markdown_cache": {
"id": "PF-01",
"title": "Markdown Processor 缓存",
"solution": "将 unified processor 实例提取到独立模块,使用 Map 按 filePath 缓存。",
"implementation": {
"new_file": "src/renderer/lib/markdown-processor.ts",
"approach": "Module-level singleton cache",
"cache_strategy": {
"key": "filePath (string)",
"value": "Processor instance (unified processor with all 7 plugins)",
"invalidation": "filePath 变化时创建新 processor,旧实例保留在 cache 中供回退",
"max_cache_size": 10,
"eviction": "LRU - 超过 max_cache_size 时移除最久未使用的"
},
"code_sketch": "const processorCache = new Map<string, Processor>();\nexport function getProcessor(filePath: string): Processor {\n if (processorCache.has(filePath)) return processorCache.get(filePath)!;\n const p = unified().use(remarkParse)...;\n processorCache.set(filePath, p);\n return p;\n}"
},
"dependencies": [],
"risk": "LOW - unified processor 是无状态的,缓存安全"
},
"PF-02_zustand_selectors": {
"id": "PF-02",
"title": "Zustand 选择器优化",
"solution": "在 tabStore 中添加 getActiveTab 派生选择器,各组件只订阅 activeTab 引用。",
"implementation": {
"approach": "在 store 中添加 getter 方法 + useShallow",
"store_changes": {
"new_methods": [
"getActiveTab(): Tab | undefined — 从 state.tabs 和 state.activeTabId 派生",
"getActiveTabId(): string | null — 直接返回 state.activeTabId"
],
"notes": "Zustand 5 中 useShallow 来自 'zustand/react/shallow'"
},
"component_changes": {
"before": "const tabs = useTabStore(s => s.tabs);\nconst activeTab = tabs.find(t => t.id === activeTabId);",
"after": "const activeTab = useTabStore(s => s.tabs.find(t => t.id === s.activeTabId));",
"note": "直接在选择器中 find,当 activeTab 的引用未变时不会触发重渲染。或使用 useShallow + getActiveTab 方法。"
},
"affected_files": [
"src/renderer/components/Editor/Editor.tsx",
"src/renderer/components/Preview/Preview.tsx",
"src/renderer/components/StatusBar/StatusBar.tsx",
"src/renderer/components/Sidebar/Sidebar.tsx"
]
},
"dependencies": ["PF-01"],
"risk": "MEDIUM - 需验证所有订阅点,遗漏会导致功能异常"
},
"PF-04_codemirror_compartment": {
"id": "PF-04",
"title": "CodeMirror Compartment 主题切换",
"solution": "使用 @codemirror/state 的 Compartment API 封装主题扩展,darkMode 变化时 reconfigure 而非销毁重建。",
"implementation": {
"file": "src/renderer/components/Editor/useCodeMirror.ts",
"approach": "Compartment + reconfigure",
"code_sketch": "import { Compartment } from '@codemirror/state';\nconst themeCompartment = new Compartment();\n\n// 创建时:\nconst extensions = [\n // ... other extensions\n themeCompartment.of(darkMode ? oneDark : [])\n];\n\n// darkMode 变化时:\nuseEffect(() => {\n if (editorView) {\n editorView.dispatch({\n effects: themeCompartment.reconfigure(darkMode ? oneDark : [])\n });\n }\n}, [darkMode]);",
"key_changes": [
"移除 useEffect 中 EditorView 销毁重建逻辑",
"theme 扩展改为 Compartment.of() 包装",
"darkMode 变化改为 dispatch reconfigure effect",
"保留其他扩展不变(keymap、language 等)"
]
},
"dependencies": [],
"risk": "HIGH - 影响编辑器核心初始化流程,需仔细测试切换前后编辑器状态保留"
},
"PF-07_preview_debounce": {
"id": "PF-07",
"title": "Preview 渲染防抖",
"solution": "创建 useDebounce hook,在 Preview.tsx 中对 content 变化进行 150ms 防抖。",
"implementation": {
"new_file": "src/renderer/hooks/useDebounce.ts",
"approach": "标准 useDebounce hook + useEffect",
"code_sketch": "function useDebounce<T>(value: T, delay: number): T {\n const [debouncedValue, setDebouncedValue] = useState(value);\n useEffect(() => {\n const timer = setTimeout(() => setDebouncedValue(value), delay);\n return () => clearTimeout(timer);\n }, [value, delay]);\n return debouncedValue;\n}",
"usage_in_preview": "const content = activeTab?.content ?? '';\nconst debouncedContent = useDebounce(content, 150);\nuseEffect(() => {\n // renderMarkdown(debouncedContent)\n}, [debouncedContent]);"
},
"dependencies": ["PF-01", "PF-02"],
"risk": "LOW"
},
"PF-08_indexeddb_debounce": {
"id": "PF-08",
"title": "IndexedDB 保存去抖",
"solution": "将 tabStore 中的 setTimeout(saveToDB, 0) 替换为 debounce(saveToDB, 500)。",
"implementation": {
"file": "src/renderer/stores/tabStore.ts",
"approach": "引入 debounce 工具函数",
"code_sketch": "// utils/debounce.ts\nexport function debounce<T extends (...args: any[]) => any>(fn: T, ms: number): T {\n let timer: ReturnType<typeof setTimeout>;\n return ((...args: any[]) => {\n clearTimeout(timer);\n timer = setTimeout(() => fn(...args), ms);\n }) as T;\n}\n\n// tabStore.ts\nconst debouncedSaveToDB = debounce(() => saveToDB(), 500);\nconst debouncedSaveActiveTab = debounce((id: string) => tabRepository.saveActiveTabId(id), 300);",
"affected_patterns": [
"setTimeout(() => saveToDB(), 0) → debouncedSaveToDB()",
"setTimeout(() => tabRepository.saveActiveTabId(activeTabId), 0) → debouncedSaveActiveTab(activeTabId)"
]
},
"dependencies": [],
"risk": "LOW - 去抖延迟可配置,用户关闭窗口前需 flush"
},
"AR-05_error_boundary": {
"id": "AR-05",
"title": "React ErrorBoundary",
"solution": "创建 class component ErrorBoundary,捕获渲染错误,显示友好错误页面。",
"implementation": {
"file": "src/renderer/components/ErrorBoundary/ErrorBoundary.tsx",
"features": [
"componentDidCatch + getDerivedStateFromError",
"显示错误信息、错误栈(开发模式)",
"「重新加载」按钮调用 location.reload() 或 window.electronAPI 重启",
"「复制错误信息」按钮",
"支持 fallback 自定义 UI"
],
"integration": "在 main.tsx 中 <ErrorBoundary><App /></ErrorBoundary>"
},
"dependencies": [],
"risk": "LOW"
},
"AR-01_app_split": {
"id": "AR-01",
"title": "App.tsx 拆分",
"solution": "提取 AboutDialog 组件和 useFileOperations hook。",
"implementation": {
"extractions": [
{
"name": "AboutDialog",
"source": "App.tsx 内联 JSX (~40 行)",
"target": "src/renderer/components/AboutDialog/AboutDialog.tsx",
"props": "{ visible: boolean; onClose: () => void; version: string }"
},
{
"name": "useFileOperations",
"source": "App.tsx 中的 handleOpenFile, handleSave, handleSaveAs, handleOpenRecent 函数",
"target": "src/renderer/hooks/useFileOperations.ts",
"returns": "{ handleOpenFile, handleSave, handleSaveAs, handleOpenRecent }"
}
],
"expected_result": "App.tsx 从 ~307 行减少到 ~120 行"
},
"dependencies": ["CQ-01", "AR-05"],
"risk": "LOW - 提取式重构,行为不变"
},
"AR-03_unified_settings": {
"id": "AR-03",
"title": "统一设置加载",
"solution": "创建 useSettingsInit hook,一次性从 IndexedDB 加载所有设置,分发到各 store。",
"implementation": {
"new_file": "src/renderer/hooks/useSettingsInit.ts",
"approach": "单次加载 + Zustand setState 分发",
"code_sketch": "export function useSettingsInit() {\n const [loaded, setLoaded] = useState(false);\n useEffect(() => {\n settingsRepository.load().then(settings => {\n useThemeStore.getState().applySettings(settings);\n useEditorStore.getState().applySettings(settings);\n setLoaded(true);\n });\n }, []);\n return loaded;\n}",
"affected_files": [
"src/renderer/hooks/useTheme.ts — 移除独立 load()",
"src/renderer/hooks/useSettings.ts — 移除独立 load()"
]
},
"dependencies": [],
"risk": "MEDIUM - 设置加载时序变化可能影响首次渲染"
},
"UX-01_confirm_dialog": {
"id": "UX-01",
"title": "自定义确认对话框",
"solution": "创建 ConfirmDialog 组件 + useConfirm hook,替代原生 confirm()。",
"implementation": {
"approach": "Promise-based confirm API",
"code_sketch": "// useConfirm.ts\nexport function useConfirm() {\n const [state, setState] = useState<{ message: string; resolve: (v: boolean) => void } | null>(null);\n const confirm = (message: string) => new Promise<boolean>(resolve => setState({ message, resolve }));\n const handleConfirm = () => { state?.resolve(true); setState(null); };\n const handleCancel = () => { state?.resolve(false); setState(null); };\n return { confirm, dialog: state ? <ConfirmDialog .../> : null };\n}",
"affected_locations": [
"TabBar.tsx — 4 处 confirm() 调用",
"useUnsavedWarning.ts — 1 处 confirm() 调用"
]
},
"dependencies": ["AR-01"],
"risk": "MEDIUM - Promise-based API 与原生 confirm() 的同步行为不同,需确保调用方正确 await"
},
"UX-03_split_mode": {
"id": "UX-03",
"title": "分屏模式",
"solution": "扩展 ViewMode 为 'split',布局组件根据 mode 同时渲染 Editor 和 Preview。",
"implementation": {
"approach": "CSS Grid/ResizeObserver 双栏布局",
"key_changes": [
"types/settings.ts: ViewMode = 'editor' | 'preview' | 'split'",
"App.tsx 或新 Layout 组件: 根据 viewMode 条件渲染",
"split 模式: 左 50% Editor + 右 50% Preview,可拖拽调整比例",
"添加 Ctrl+3 快捷键切换到 split 模式",
"可选:编辑器滚动同步预览滚动位置"
]
},
"dependencies": ["PF-01", "PF-02", "PF-04"],
"risk": "HIGH - 需要同时维护两个视图实例,内存和性能需关注"
},
"PF-06_code_splitting": {
"id": "PF-06",
"title": "代码分割",
"solution": "在 electron.vite.config.ts 中配置 Rollup manualChunks,按依赖类型分割。",
"implementation": {
"file": "electron.vite.config.ts",
"approach": "manualChunks + dynamic import",
"chunks_config": {
"vendor-react": ["react", "react-dom"],
"vendor-codemirror": ["@codemirror/state", "@codemirror/view", "@codemirror/commands", "@codemirror/lang-markdown", "@codemirror/language", "@codemirror/search", "@codemirror/theme-one-dark"],
"vendor-markdown": ["unified", "remark-parse", "remark-gfm", "remark-rehype", "rehype-raw", "rehype-sanitize", "rehype-stringify", "rehype-highlight"],
"vendor-db": ["dexie"]
},
"dynamic_import_targets": [
"rehype-highlight (仅 Preview 组件需要)"
]
},
"dependencies": [],
"risk": "MEDIUM - 代码分割后需验证 Electron 打包正确性和加载时序"
},
"SE-01_csp": {
"id": "SE-01",
"title": "CSP 配置",
"solution": "在 main process 的 BrowserWindow 中通过 session.defaultSession.webRequest 配置 CSP header。",
"implementation": {
"file": "src/main/window-manager.ts",
"approach": "session.webRequest.onHeadersReceived",
"csp_policy": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' file: data:; font-src 'self' data:;",
"notes": "style-src 需要 'unsafe-inline' 因为 CodeMirror 通过 JS 注入内联样式。rehype-highlight 引入 highlight.js 样式也需要考虑。"
},
"dependencies": ["PF-04"],
"risk": "MEDIUM - CSP 可能阻断 CodeMirror 样式注入,需测试"
}
},
"dependency_graph": {
"description": "优化项之间的依赖关系。箭头表示 'A -> B' 意味着 B 依赖 AA 需先完成)。",
"edges": [
{ "from": "CQ-01", "to": "CQ-02", "reason": "Prettier 需与 ESLint 集成 (eslint-config-prettier)" },
{ "from": "CQ-01", "to": "AR-01", "reason": "组件拆分前需确保代码质量基线" },
{ "from": "CQ-01", "to": "AR-02", "reason": "组件拆分前需确保代码质量基线" },
{ "from": "CQ-01", "to": "PF-03", "reason": "React.memo 添加前需 ESLint 规则配合" },
{ "from": "CQ-01", "to": "DX-05", "reason": "lint-staged 依赖 ESLint 配置" },
{ "from": "CQ-01", "to": "DX-01", "reason": "测试代码也需遵循 ESLint 规则" },
{ "from": "CQ-04", "to": "DX-02", "reason": "preload 类型安全依赖类型完善" },
{ "from": "PF-01", "to": "PF-02", "reason": "Zustand 选择器优化涉及 Preview 组件,需配合 Markdown 缓存" },
{ "from": "PF-01", "to": "PF-07", "reason": "Preview 防抖依赖渲染管道已优化" },
{ "from": "PF-02", "to": "PF-07", "reason": "Preview 防抖依赖 activeTab 选择器" },
{ "from": "PF-02", "to": "PF-03", "reason": "React.memo 效果依赖选择器优化减少不必要更新" },
{ "from": "AR-01", "to": "UX-01", "reason": "ConfirmDialog 依赖组件拆分后的目录结构" },
{ "from": "AR-01", "to": "UX-02", "reason": "LoadingSpinner 依赖组件拆分后的目录结构" },
{ "from": "PF-01", "to": "UX-03", "reason": "分屏模式的预览面板依赖 Markdown 缓存" },
{ "from": "PF-02", "to": "UX-03", "reason": "分屏模式需要高效的 store 订阅" },
{ "from": "PF-04", "to": "UX-03", "reason": "分屏模式的编辑器依赖 Compartment 动态配置" },
{ "from": "PF-04", "to": "SE-01", "reason": "CSP 配置需在 Compartment 稳定后进行,避免相互影响" },
{ "from": "AR-05", "to": "AR-01", "reason": "ErrorBoundary 应在 App.tsx 拆分前完成,保护重构过程" }
],
"parallelizable": [
["CQ-01", "AR-05", "DX-07"],
["PF-01", "PF-04", "PF-08"],
["CQ-04", "CQ-05", "AR-03", "AR-04"],
["UX-01", "UX-02", "UX-05"],
["DX-01", "DX-04"]
]
},
"risk_assessment": {
"high_risk": [
{
"id": "PF-04",
"title": "CodeMirror Compartment 重构",
"risk_level": "HIGH",
"description": "影响编辑器核心初始化流程。Compartment reconfigure 如果处理不当,可能导致编辑器状态丢失(光标位置、选区、undo 历史)。",
"impact": "编辑器完全不可用",
"probability": "MEDIUM",
"mitigation": [
"保留现有销毁重建逻辑作为 fallbackfeature flag 控制)",
"编写测试验证切换主题后光标位置和 undo 历史保留",
"逐步迁移:先用 Compartment 切换主题,验证稳定后再扩展到其他扩展"
],
"rollback": "revert useCodeMirror.ts 到使用前的版本"
},
{
"id": "UX-03",
"title": "分屏模式",
"risk_level": "HIGH",
"description": "需要同时维护 Editor 和 Preview 两个实例,可能增加内存占用和 CPU 消耗。滚动同步逻辑复杂。",
"impact": "性能下降、内存溢出(大文件场景)",
"probability": "MEDIUM",
"mitigation": [
"Preview 面板使用延迟加载(只在 split 模式激活时挂载)",
"大文件场景下限制预览刷新频率(500ms debounce",
"考虑虚拟化预览(只渲染可视区域)"
],
"rollback": "ViewMode 从 'editor' | 'preview' | 'split' 移除 'split' 值"
}
],
"medium_risk": [
{
"id": "PF-02",
"title": "Zustand 选择器重构",
"risk_level": "MEDIUM",
"description": "选择器重构可能遗漏某些订阅点,导致某些组件在 activeTab 变化时不再更新。",
"impact": "编辑器/预览/状态栏内容不同步",
"probability": "LOW",
"mitigation": [
"使用 React DevTools Profiler 逐一验证每个受影响组件的重渲染行为",
"逐个文件修改,每修改一个文件后手动测试",
"编写 tabStore 选择器的单元测试"
],
"rollback": "逐文件恢复原始订阅方式"
},
{
"id": "SE-01",
"title": "CSP 配置",
"risk_level": "MEDIUM",
"description": "CSP 配置过于严格可能阻断 CodeMirror 内联样式注入或 rehype-highlight 的样式。",
"impact": "编辑器或预览样式丢失",
"probability": "LOW",
"mitigation": [
"使用 'unsafe-inline' 允许 style-srcCodeMirror 依赖)",
"分步启用 CSP 先 report-only 模式测试",
"测试所有主题切换和 Markdown 语法高亮场景"
],
"rollback": "移除 CSP header 配置"
},
{
"id": "AR-03",
"title": "统一设置加载",
"risk_level": "MEDIUM",
"description": "将两次 IndexedDB 读取合并为一次可能改变设置加载时序,导致首次渲染闪烁或主题闪烁。",
"impact": "首次加载时短暂显示默认主题再切换到用户主题",
"probability": "LOW",
"mitigation": [
"在设置加载完成前显示 loading 状态(不渲染 App",
"使用 init() 方式在 main.tsx 中等待设置加载完成后再渲染"
],
"rollback": "恢复 useTheme 和 useSettings 各自独立加载"
},
{
"id": "PF-08",
"title": "IndexedDB 去抖",
"risk_level": "MEDIUM",
"description": "去抖延迟期间如果 Electron 进程被杀或用户关闭窗口,可能丢失最后一次保存。",
"impact": "用户数据丢失(最后一次编辑)",
"probability": "LOW",
"mitigation": [
"在 beforeunload / window close 事件中 flush debounce",
"使用 navigator.sendBeacon 或 Electron app.on('before-quit') 事件触发同步保存",
"记录 dirty flag,关闭前检查"
],
"rollback": "恢复 setTimeout(fn, 0) 立即保存"
}
],
"low_risk": [
{
"id": "CQ-01",
"title": "ESLint 配置",
"risk_level": "LOW",
"description": "纯增量添加,不影响运行时行为。",
"mitigation": ["确保 .eslintignore 排除构建产物"]
},
{
"id": "PF-01",
"title": "Markdown 处理器缓存",
"risk_level": "LOW",
"description": "unified processor 是无状态管道,缓存复用安全。",
"mitigation": ["设置 max cache size 防止内存泄漏"]
},
{
"id": "PF-07",
"title": "Preview 防抖",
"risk_level": "LOW",
"description": "防抖只是延迟渲染,不影响最终结果。",
"mitigation": ["防抖时间可配置,默认 150ms"]
},
{
"id": "AR-05",
"title": "ErrorBoundary",
"risk_level": "LOW",
"description": "纯增量添加,只在出错时生效。",
"mitigation": ["测试注入错误验证 ErrorBoundary 正常捕获"]
}
]
},
"summary": {
"total_optimization_items": 35,
"total_new_files": 28,
"total_modified_files": 22,
"total_phases": 6,
"estimated_total_effort": "12-18 天",
"critical_path": "CQ-01 → AR-05 → AR-01 → UX-01 → DX-05",
"parallel_speedup_potential": "约 30-40%Phase 1 和 Phase 2 可部分并行)",
"highest_impact_items": ["PF-02", "PF-01", "PF-04", "AR-05"],
"recommended_execution_order": [
"Phase 1 (Day 1-2): ESLint + Prettier + ErrorBoundary — 建立基线",
"Phase 2 (Day 3-4): 性能修复 — 解决核心瓶颈",
"Phase 3 (Day 5-7): 架构重构 — 提升可维护性",
"Phase 4 (Day 8-10): UX 改进 — 提升用户体验",
"Phase 5 (Day 11-13): 测试 + 文档 — 建立质量保障",
"Phase 6 (Day 14-18): 高级功能 — 锦上添花"
]
}
}