Files
MarkLite/DESIGN.md
T
thzxx 5e1c89d280 refactor: v2.0 全量重构 — TypeScript + React + Zustand + IndexedDB
技术栈升级:
- JavaScript → TypeScript 5.6(全量类型安全)
- 原生 DOM → React 18(函数组件 + Hooks)
- 全局变量 → Zustand 5(轻量状态管理)
- localStorage → IndexedDB / Dexie.js 4(大容量、异步、索引)
- marked.js → unified / remark / rehype(插件化渲染管线)
- 无打包 → electron-vite 3(HMR 热更新)
- 纯 CSS → CSS Variables + CSS Modules

新增功能:
- 标签页状态 IndexedDB 持久化(关闭后可恢复)
- 最近打开文件列表
- 大文件虚拟化行号(>2000 行)
- 搜索高亮二分查找优化 O(log N)
- rehype-sanitize HTML 安全过滤

文件结构:
- 7 个源文件 → 51 个模块化文件
- src/main/     主进程(6 文件)
- src/preload/  预加载(1 文件)
- src/renderer/ 渲染进程(42 文件:组件/hooks/stores/lib/db/types/styles)
- src/shared/   共享类型(2 文件)

构建验证:
- TypeScript 检查零错误
- electron-vite build 成功
- 产物:main 15kB + preload 2kB + renderer 1.5MB
2026-05-27 20:29:23 +08:00

540 lines
25 KiB
Markdown
Raw 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.
# 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/)与 UIcomponents/)解耦,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 │ │- splitPct│ │- 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: 'split' | 'editor' | 'preview' // 视图模式
darkMode: boolean // 暗色主题
splitRatio: number // 分屏比例 (20~80)
}
```
### 3.4 sidebarStore — 侧边栏状态
```typescript
interface SidebarState {
isVisible: boolean // 是否显示
rootPath: string | null // 当前打开的文件夹路径
tree: FileNode[] // 目录树数据
expandedDirs: Set<string> // 已展开的目录集合
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, splitRatio, 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<OpenFileResponse>
readFile: (filePath: string) => Promise<ReadFileResult>
saveFile: (data: SaveFilePayload) => Promise<SaveFileResult>
// ... 共 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 解析为 MDASTMarkdown AST
remark-gfm 扩展 GFM 语法(表格、任务列表、删除线)
remark-rehype 转换为 HASTHTML 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 内联元素
- GFMGitHub 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 + 3` | 纯预览模式 |
| `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 devHMR 热更新)
```
### 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 规则 |