# MarkLite v2.0 — 架构设计文档 ## 1. 项目概述 MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线,提供类型安全、模块化、可测试的现代化架构。 ### 1.1 核心原则 1. **类型安全** — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义 2. **模块化** — 51 个源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types) 3. **功能 100% 兼容** — 重构不丢失任何现有功能 4. **可测试性** — 业务逻辑(lib/)与 UI(components/)解耦,stores 和 lib 可独立测试 ## 2. 技术架构 ### 2.1 技术栈 | 组件 | 技术选型 | 版本 | 说明 | |------|----------|------|------| | 桌面框架 | Electron | v28 | 跨平台桌面应用框架 | | 前端框架 | React | v18 | 函数组件 + Hooks | | 类型系统 | TypeScript | v5.6 | 全量类型安全 | | 状态管理 | Zustand | v5 | 轻量级状态管理,selector 优化 | | 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 | | Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线 | | 代码高亮 | rehype-highlight | v7 | 基于 highlight.js 的语法高亮 | | 构建工具 | electron-vite | v3 | Electron + Vite 集成,HMR 热更新 | | 打包工具 | electron-builder | v25 | 生成 Windows NSIS 安装包 | | 样式 | CSS Variables + CSS Modules | — | 主题驱动,样式隔离 | ### 2.2 进程架构 ``` ┌──────────────────────────────────────────────────────────────┐ │ Main Process (src/main/) 6 文件 │ │ index.ts 入口:窗口创建、app 生命周期、单实例锁 │ │ ipc-handlers.ts 所有 ipcMain.handle 注册 │ │ file-system.ts 文件读写、目录树构建、BOM 剥离 │ │ file-watcher.ts fs.watch 封装(单文件 + 目录监听) │ │ window-manager.ts 窗口创建、关闭拦截、单实例锁 │ │ ipc-channels.ts IPC 通道名常量 │ └───────────────────────┬──────────────────────────────────────┘ │ contextBridge (安全隔离) ┌───────────────────────▼──────────────────────────────────────┐ │ Preload Script (src/preload/) 1 文件 │ │ index.ts contextBridge 类型安全暴露 │ │ electronAPI 22 个方法/事件的类型安全接口 │ └───────────────────────┬──────────────────────────────────────┘ │ ┌───────────────────────▼──────────────────────────────────────┐ │ Renderer Process (src/renderer/) 42 文件 React 18 │ │ │ │ ┌─ components/ (11) ──────────────────────────────────────┐ │ │ │ Toolbar · TabBar · Editor · Preview · Sidebar │ │ │ │ SearchBar · StatusBar · WelcomeScreen · Toast │ │ │ │ ModifiedBanner · DropOverlay │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ stores/ (4) ───────────────────────────────────────────┐ │ │ │ tabStore · editorStore · sidebarStore · searchStore │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ hooks/ (6) ────────────────────────────────────────────┐ │ │ │ useTheme · useSettings · useKeyboard · useDragDrop │ │ │ │ useFileWatch · useUnsavedWarning │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ lib/ (5) ──────────────────────────────────────────────┐ │ │ │ markdown · scrollSync · searchEngine · fileUtils │ │ │ │ constants │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ db/ (4) ───────────────────────────────────────────────┐ │ │ │ schema · tabRepository · settingsRepository │ │ │ │ recentFilesRepository │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ types/ (6) ────────────────────────────────────────────┐ │ │ │ tab · file · settings · search · ipc · index │ │ │ └──────────────────────────────────────────────────────────┘ │ │ ┌─ styles/ (3) ───────────────────────────────────────────┐ │ │ │ variables.css · global.css · markdown-body.css │ │ │ └──────────────────────────────────────────────────────────┘ │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Shared (src/shared/) 2 文件 │ │ ipc-channels.ts IPC 通道名常量(主进程/渲染进程共用) │ │ types.ts 共享类型定义(FileNode, 结果类型等) │ └──────────────────────────────────────────────────────────────┘ ``` ### 2.3 安全模型 | 层级 | 措施 | 说明 | |------|------|------| | Electron | `contextIsolation: true` | 渲染进程与主进程隔离 | | Electron | `nodeIntegration: false` | 渲染进程无法访问 Node.js API | | IPC | `contextBridge.exposeInMainWorld` | 仅暴露 22 个类型安全方法/事件 | | CSP | `default-src 'self'; script-src 'self'` | 阻断内联脚本、外部资源加载 | | HTML | `rehype-sanitize` | 渲染 Markdown 时过滤危险标签/属性/JS 协议 | | 链接 | 协议白名单 | 仅允许 `http:` / `https:` / `#` 锚点 | ## 3. 状态管理架构 ### 3.1 Zustand Stores ``` ┌─────────────────────────────────────────────────────────┐ │ App.tsx (根组件) │ ├─────────┬──────────┬──────────┬──────────┬──────────────┤ │Toolbar │ TabBar │ Sidebar │ Editor │ Preview │ │ │ │ │ (CM6) │ │ ├─────────┴──────────┴──────────┴──────────┴──────────────┤ │ Zustand Stores │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ tabStore │ │editorStore│ │sidebarStore│ │searchStore│ │ │ │ - tabs │ │- viewMode│ │- tree │ │- matches │ │ │ │- activeId│ │- darkMode│ │- expanded │ │- index │ │ │ │ - mru │ │- viewMode│ │- rootPath │ │- options │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └──────────┘ │ │ │ │ │ │ │ ┌────▼────────────▼────────────▼────────────────────┐ │ │ │ IndexedDB (Dexie.js) │ │ │ │ tabSnapshots │ settings │ recentFiles │ │ │ └───────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` ### 3.2 tabStore — 标签页状态 ```typescript interface TabState { tabs: Tab[] // 所有标签页 activeTabId: string | null // 当前活动标签 ID mruStack: string[] // MRU 标签栈(Ctrl+Tab 切换) createTab(filePath?, content?) // 创建标签(同文件不重复打开) closeTab(tabId) // 关闭标签(自动切换到相邻标签) switchToTab(tabId) // 切换标签(保存当前状态 → MRU 记录 → 加载目标) updateTabContent(tabId, content) // 更新内容(标记修改) setModified(tabId, modified) // 设置修改状态 getActiveTab() // 获取当前标签 updateTabScroll(tabId, scroll) // 更新滚动/光标位置 } ``` ### 3.3 editorStore — 编辑器状态 ```typescript interface EditorState { viewMode: 'editor' | 'preview' // 视图模式 } ``` ### 3.4 sidebarStore — 侧边栏状态 ```typescript interface SidebarState { isVisible: boolean // 是否显示 rootPath: string | null // 当前打开的文件夹路径 tree: FileNode[] // 目录树数据 expandedDirs: Set // 已展开的目录集合 sidebarWidth: number // 侧边栏宽度 (180~500) } ``` ### 3.5 searchStore — 搜索状态 ```typescript interface SearchState { isVisible: boolean // 搜索栏是否可见 showReplace: boolean // 替换行是否展开 searchText: string // 搜索文本 replaceText: string // 替换文本 matches: SearchMatch[] // 所有匹配项 currentIndex: number // 当前匹配索引 options: SearchOptions // { caseSensitive, useRegex } } ``` ## 4. 数据持久化 — IndexedDB 通过 Dexie.js 封装 IndexedDB,替代 localStorage: ### 4.1 数据库 Schema ```typescript // db/schema.ts const db = new Dexie('MarkLite') db.version(1).stores({ tabSnapshots: 'id, filePath, updatedAt', // 标签页快照 settings: 'id', // 用户设置 recentFiles: '++id, filePath, lastOpened' // 最近打开文件 }) ``` ### 4.2 数据模型 | Store | 字段 | 说明 | |-------|------|------| | `tabSnapshots` | id, filePath, content, scrollTop, scrollLeft, selectionStart, selectionEnd, previewScrollTop, isModified, updatedAt | 标签页状态快照,关闭时保存,启动时恢复 | | `settings` | id(固定'default'), darkMode, viewMode, sidebarCollapsed, sidebarWidth | 用户偏好设置 | | `recentFiles` | ++id, filePath, lastOpened | 最近打开文件列表 | ### 4.3 IndexedDB 优势(vs localStorage) | 维度 | localStorage | IndexedDB | |------|-------------|-----------| | 容量 | ~5-10MB | 数百 MB | | API | 同步 | 异步,不阻塞 UI | | 索引 | 无 | 支持索引查询 | | 大文件 | 无法缓存 20MB 文件 | 可缓存任意大小文件 | | 标签恢复 | 关闭后丢失 | 可持久化恢复所有标签 | ## 5. IPC 通信设计 ### 5.1 渲染进程 → 主进程(invoke) | 通道 | 参数 | 返回值 | 说明 | |------|------|--------|------| | `dialog:openFile` | 无 | `OpenFileResponse` | 打开文件对话框 | | `file:read` | `filePath: string` | `ReadFileResult` | 读取文件内容 | | `file:save` | `{ filePath, content }` | `SaveFileResult` | 保存文件 | | `file:saveAs` | `{ content }` | `SaveFileResult` | 另存为 | | `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 | | `file:stats` | `filePath: string` | `FileStatsResult` | 获取文件元信息 | | `file:reload` | 无 | `ReloadFileResult` | 重新加载当前文件 | | `tab:switched` | `filePath: string \| null` | `void` | 通知主进程切换活动文件 | | `window:forceClose` | 无 | `void` | 强制关闭窗口 | | `window:cancelClose` | 无 | `void` | 取消关闭 | | `dir:readTree` | `dirPath: string` | `ReadDirTreeResult` | 递归读取目录树 | | `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 | | `dir:watch` | `dirPath: string` | `void` | 监听目录变化 | | `dir:unwatch` | 无 | `void` | 停止监听目录变化 | ### 5.2 主进程 → 渲染进程(send) | 通道 | 数据 | 说明 | |------|------|------| | `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 | | `file:externallyModified` | `filePath: string` | 文件被外部修改 | | `window:confirmClose` | 无 | 请求确认关闭 | | `sidebar:dirChanged` | 无 | 目录结构变化,通知刷新树 | ### 5.3 类型安全 所有 IPC 通道通过 `src/shared/types.ts` 和 `src/renderer/types/ipc.ts` 定义类型: ```typescript // src/renderer/types/ipc.ts export interface ElectronAPI { openFile: () => Promise readFile: (filePath: string) => Promise saveFile: (data: SaveFilePayload) => Promise // ... 共 22 个方法/事件 } ``` ## 6. 标签页数据模型 ```typescript // src/renderer/types/tab.ts export interface Tab { id: string // 唯一标识(自增计数器) filePath: string | null // 文件路径(未命名标签为 null) content: string // 编辑器内容 isModified: boolean // 是否已修改 scrollTop: number // 编辑器滚动位置 scrollLeft: number selectionStart: number // 光标选区 selectionEnd: number previewScrollTop: number // 预览面板滚动位置 } ``` 切换标签时自动保存当前状态、恢复目标状态。使用 `execCommand('insertText')` 替换内容以保留 undo/redo 历史。 ## 7. Markdown 渲染管线 ### 7.1 渲染流程 ``` Markdown 文本 │ ▼ remark-parse 解析为 MDAST(Markdown AST) │ ▼ remark-gfm 扩展 GFM 语法(表格、任务列表、删除线) │ ▼ remark-rehype 转换为 HAST(HTML AST) │ ▼ rehype-raw 解析内联 HTML │ ▼ rehype-sanitize 安全过滤(移除 script/iframe/on* 事件/javascript: 协议) │ ▼ rehype-highlight 代码块语法高亮(180+ 语言) │ ▼ rehype-stringify 序列化为 HTML 字符串 │ ▼ dangerouslySetInnerHTML 渲染到 React DOM ``` ### 7.2 支持的语法 - 标题(h1-h6)、段落、换行 - **粗体**、*斜体*、~~删除线~~ - 有序/无序列表、任务列表 `- [x]` - 代码块(围栏式 + 语法高亮,180+ 语言)、行内代码 - 链接、图片(支持相对路径转 file:// URL) - 表格、引用块、水平线 - HTML 内联元素 - GFM(GitHub Flavored Markdown) ## 8. 滚动同步算法 基于**块级元素 DOM 位置映射**的编辑器-预览滚动同步: ``` 1. 识别编辑器中的块级元素起始行 - 标题 (#)、列表 (-/*/+)、引用 (>)、表格 (|) - 围栏代码块 (```)、水平线 (---)、段落分隔(连续空行) 2. 获取预览面板中每个 DOM 子元素的 offsetTop 3. 将块起始行号映射到预览 DOM 位置 - 块起始行数 : 预览 DOM 元素数 = 线性对应 4. 线性插值填充所有中间行 - 对于非块起始行,找到前后最近的已映射行 - 按行号比例插值计算预览位置 5. 滚动时通过映射表直接查找目标位置 - O(1) 查找,无需遍历 ``` ## 9. 搜索替换引擎 ### 9.1 匹配算法 ``` 普通文本搜索: - 大小写不敏感时:haystack.toLowerCase().indexOf(needle) - 大小写敏感时:haystack.indexOf(needle) - O(N) 线性扫描 正则表达式搜索: - new RegExp(text, flags) - 循环 exec() 收集所有匹配 - 空匹配保护:regex.lastIndex++ ``` ### 9.2 高亮渲染 ``` 字符位置 → 像素坐标转换: 1. 构建 lineStarts 缓存(每行首字符的偏移量) 2. 二分查找 O(log N):字符位置 → 行号 3. 列号 = 字符位置 - lineStarts[行号] 4. top = 行号 × 行高 + paddingTop 5. left = 列号 × 字符宽度 + paddingLeft 搜索高亮 overlay: - 绝对定位 div,覆盖在 textarea 上方 - pointer-events: none 不拦截编辑器交互 - 滚动时同步更新 overlay top 偏移 ``` ### 9.3 替换策略 - **替换当前**:使用 `execCommand('insertText')` 保留 undo 历史 - **全部替换**:从后往前拼接,一次 `execCommand('insertText')` 原子操作,Ctrl+Z 可一次性撤销 ## 10. UI 设计 ### 10.1 色彩方案 #### 亮色主题 | 角色 | CSS 变量 | 色值 | |------|----------|------| | 主色调 | `--primary` | `#1a73e8` | | 背景色 | `--bg` | `#ffffff` | | 次级背景 | `--bg-secondary` | `#f8f9fa` | | 三级背景 | `--bg-tertiary` | `#f1f3f4` | | 文字色 | `--text` | `#333333` | | 次级文字 | `--text-secondary` | `#5f6368` | | 代码块背景 | `--code-bg` | `#f6f8fa` | | 边框色 | `--border` | `#e1e4e8` | #### 暗色主题 | 角色 | CSS 变量 | 色值 | |------|----------|------| | 主色调 | `--primary` | `#8ab4f8` | | 背景色 | `--bg` | `#1e1e1e` | | 次级背景 | `--bg-secondary` | `#252526` | | 三级背景 | `--bg-tertiary` | `#2d2d2d` | | 文字色 | `--text` | `#d4d4d4` | | 次级文字 | `--text-secondary` | `#9e9e9e` | | 代码块背景 | `--code-bg` | `#2d2d2d` | | 边框色 | `--border` | `#3e3e3e` | ### 10.2 字体 - **UI 字体**: `system-ui, -apple-system, "Segoe UI", Roboto, sans-serif` - **编辑器字体**: `"Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace` - **预览字体**: 同 UI 字体 ### 10.3 布局 ``` ┌──────────────────────────────────────────────────────────────────────────┐ │ MarkLite - filename.md ─ □ ✕ │ ├──────────────────────────────────────────────────────────────────────────┤ │ 📁 打开 │ 💾 保存 │ ✏️ 编辑 │ 👁 预览 │ 🌙 │ ├──────────────────────────────────────────────────────────────────────────┤ │ [file1.md] [file2.md] [未命名] [+] │ ├──────────────────────────────────────────────────────────────────────────┤ │ ⚠️ 文件已被外部程序修改 [重新加载] [忽略] │ ├──────────┬─────────────────────────┬─────────────────────────────────────┤ │ 资源管理器│ │ │ │ ▼ project │ 🔍 查找... 2/5 │ Title │ │ 📁 src │ [替换... ] [替换][全部]│ ─────── │ │ 📄 file1│ │ │ │ 📄 file2│ 1 # Title │ Title │ │ 📁 lib │ 2 │ ─────── │ │ │ 3 content... │ content... │ │ │ 4 │ │ ├──────────┴─────────────────────────┴─────────────────────────────────────┤ │ filename.md │ UTF-8 │ Markdown │ 1.2 KB │ 行 3, 列 1 │ └──────────────────────────────────────────────────────────────────────────┘ ``` ## 11. 快捷键 | 快捷键 | 功能 | |:-------|:-----| | `Ctrl + T` | 新建标签页 | | `Ctrl + W` | 关闭当前标签页 | | `Ctrl + Tab` | 切换到下一个标签页(MRU 顺序) | | `Ctrl + Shift + Tab` | 切换到上一个标签页 | | `Ctrl + O` | 打开文件 | | `Ctrl + S` | 保存文件 | | `Ctrl + Shift + S` | 另存为 | | `Ctrl + 1` | 编辑模式 | | `Ctrl + 2` | 预览模式 | | `Ctrl + F` | 搜索 | | `Ctrl + H` | 搜索并替换 | | `Enter` / `Shift+Enter` | 下一个 / 上一个匹配 | | `Alt + C` | 切换区分大小写 | | `Alt + R` | 切换正则表达式 | | `Ctrl + Shift + G` | 替换当前匹配 | | `Ctrl + Shift + H` | 全部替换 | | `Esc` | 关闭搜索栏 | ## 12. 构建与发布 ### 12.1 开发模式 ```bash npm run dev # electron-vite dev(HMR 热更新) ``` ### 12.2 生产构建 ```bash npm run build # electron-vite build + electron-builder --win npm run build:portable # 便携版(免安装) ``` ### 12.3 构建产物 ``` dist/ ├── main/index.js 15 kB 主进程(TypeScript 编译) ├── preload/index.js 2 kB 预加载脚本 └── renderer/ ├── index.html 入口 HTML ├── assets/index-*.css 18 kB 样式 └── assets/index-*.js 1.4 MB React 应用 ``` ### 12.4 代码检查 ```bash npm run typecheck # TypeScript 类型检查 npm run lint # ESLint 代码检查 ``` ### 12.5 打包配置 - 输出格式:NSIS 安装包(.exe) - 目标平台:Windows x64 - 应用图标:assets/icon.ico - 文件关联:`.md` / `.markdown` / `.txt` - 支持自定义安装目录、桌面/开始菜单快捷方式 详见 [DEVSETUP.md](DEVSETUP.md)。 ## 13. 依赖清单 ### 运行时依赖 | 包名 | 版本 | 用途 | |------|------|------| | react | ^18.3 | UI 框架 | | react-dom | ^18.3 | React DOM 渲染 | | zustand | ^5.0 | 状态管理 | | dexie | ^4.0 | IndexedDB 封装 | | nanoid | ^5.0 | 唯一 ID 生成 | | unified | ^11.0 | Markdown 处理管线 | | remark-parse | ^11.0 | Markdown 解析器 | | remark-gfm | ^4.0 | GFM 扩展 | | remark-rehype | ^11.1 | MDAST → HAST 转换 | | rehype-raw | ^7.0 | 内联 HTML 解析 | | rehype-sanitize | ^6.0 | HTML 安全过滤 | | rehype-stringify | ^10.0 | HAST → HTML 序列化 | | rehype-highlight | ^7.0 | 代码语法高亮 | ### 开发依赖 | 包名 | 版本 | 用途 | |------|------|------| | electron | ^28.0 | 桌面框架 | | electron-builder | ^25.0 | 打包工具 | | electron-vite | ^3.0 | 构建工具 | | @vitejs/plugin-react | ^4.3 | Vite React 插件 | | typescript | ^5.6 | 类型系统 | | @types/react | ^18.3 | React 类型 | | @types/react-dom | ^18.3 | React DOM 类型 | | eslint | ^9.0 | 代码检查 | | @typescript-eslint/eslint-plugin | ^8.0 | TypeScript ESLint 规则 |