## 删除分屏功能 - 删除 ViewMode 'split' 类型,仅保留 editor/preview - 删除 splitRatio 状态和 Resizer 组件 - 删除滚动同步模块 (scrollSync.ts, rehypeSourceLine.ts) - 更新快捷键: Ctrl+1 编辑, Ctrl+2 预览 ## Bug 修复 - 修复 Toast setTimeout 内存泄漏 (App.tsx) - 修复 ModifiedBanner reload 更新错误标签 (改用 modifiedFilePath 匹配) - 修复 FileWatcher error 未重置 isSelfWriting (file-watcher.ts) - 修复另存为时未 stop watcher (ipc-handlers.ts) ## 死代码清理 (10 项) - 删除 main/ipc-channels.ts (与 shared/ 重复) - 删除 main/file-system.ts 未使用的 formatBytes - 删除 constants.ts 6 个未使用常量 - 删除 fileUtils.ts 未使用的 formatBytes - 删除 Icons.tsx 5 个未使用图标 (SplitView/ChevronUp/ChevronDown/X/ArrowUp/ArrowDown) - 删除 useCodeMirror 未使用的 getContent/scrollTo - 删除 TabBar 未使用的 menuRef - 删除 Tab.scrollLeft/previewScrollTop 字段 - 删除 tabStore 未使用的 getTabIndex - 删除 Toast/ModifiedBanner 多余 React import ## 安全加固 - ipc-handlers: 添加路径遍历防护 (validatePath 函数) - preload: openExternal 仅允许 http/https 协议 - window-manager: 启用 sandbox: true - preload: removeAllListeners 改为精确取消订阅 (返回 Unsubscribe 函数) ## 状态持久化 - activeTabId 持久化到 IndexedDB (刷新后恢复正确标签) - sidebar 状态持久化到 IndexedDB (isVisible/sidebarWidth) ## 代码优化 - preload 使用 IPC_CHANNELS 常量替代硬编码字符串 - ipc-handlers _event 类型改为 IpcMainInvokeEvent - settingsRepository 删除 splitRatio 字段
25 KiB
25 KiB
MarkLite v2.0 — 架构设计文档
1. 项目概述
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线,提供类型安全、模块化、可测试的现代化架构。
1.1 核心原则
- 类型安全 — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义
- 模块化 — 51 个源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types)
- 功能 100% 兼容 — 重构不丢失任何现有功能
- 可测试性 — 业务逻辑(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 — 标签页状态
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 — 编辑器状态
interface EditorState {
viewMode: 'editor' | 'preview' // 视图模式
}
3.4 sidebarStore — 侧边栏状态
interface SidebarState {
isVisible: boolean // 是否显示
rootPath: string | null // 当前打开的文件夹路径
tree: FileNode[] // 目录树数据
expandedDirs: Set<string> // 已展开的目录集合
sidebarWidth: number // 侧边栏宽度 (180~500)
}
3.5 searchStore — 搜索状态
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
// 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 定义类型:
// src/renderer/types/ipc.ts
export interface ElectronAPI {
openFile: () => Promise<OpenFileResponse>
readFile: (filePath: string) => Promise<ReadFileResult>
saveFile: (data: SaveFilePayload) => Promise<SaveFileResult>
// ... 共 22 个方法/事件
}
6. 标签页数据模型
// 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 开发模式
npm run dev # electron-vite dev(HMR 热更新)
12.2 生产构建
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 代码检查
npm run typecheck # TypeScript 类型检查
npm run lint # ESLint 代码检查
12.5 打包配置
- 输出格式:NSIS 安装包(.exe)
- 目标平台:Windows x64
- 应用图标:assets/icon.ico
- 文件关联:
.md/.markdown/.txt - 支持自定义安装目录、桌面/开始菜单快捷方式
详见 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 规则 |