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
This commit is contained in:
@@ -1,163 +1,418 @@
|
||||
# MarkLite - 设计文档
|
||||
# MarkLite v2.0 — 架构设计文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron 框架构建,提供简洁现代的用户界面,支持多标签页、Markdown 文件的打开、编辑和实时预览,支持亮色/暗色主题切换,支持搜索替换和文件树侧边栏。
|
||||
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 | 跨平台桌面应用框架 |
|
||||
| 前端 | HTML + CSS + JavaScript | 原生前端技术,无框架依赖 |
|
||||
| Markdown 解析 | marked.js v12 | 高性能 Markdown 解析库(本地离线) |
|
||||
| 代码高亮 | highlight.js v11 | 代码块语法高亮(本地离线) |
|
||||
| 打包 | electron-builder | 生成 Windows NSIS 安装包 |
|
||||
| 组件 | 技术选型 | 版本 | 说明 |
|
||||
|------|----------|------|------|
|
||||
| 桌面框架 | 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 (main.js) │
|
||||
│ - 窗口管理 │
|
||||
│ - 文件系统操作 (fs) │
|
||||
│ - 文件监听 (fs.watch) │
|
||||
│ - 目录树读取与监听 │
|
||||
│ - 未保存提醒拦截 │
|
||||
│ - IPC 通信主端 │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│ IPC (contextBridge)
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ Preload Script (preload.js) │
|
||||
│ - 安全的 API 桥接 │
|
||||
│ - exposeInMainWorld │
|
||||
└──────────────┬──────────────────────────┘
|
||||
│
|
||||
┌──────────────▼──────────────────────────┐
|
||||
│ Renderer Process (renderer/) │
|
||||
│ - 多标签页管理 │
|
||||
│ - UI 渲染(亮色/暗色主题) │
|
||||
│ - Markdown 编辑与预览 │
|
||||
│ - 搜索替换(高亮、正则、大小写) │
|
||||
│ - 文件树侧边栏 │
|
||||
│ - 拖拽文件处理 │
|
||||
│ - 快捷键处理 │
|
||||
│ - 用户设置持久化 (localStorage) │
|
||||
└─────────────────────────────────────────┘
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 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 安全模型
|
||||
|
||||
- `contextIsolation: true` + `nodeIntegration: false`
|
||||
- 通过 `contextBridge.exposeInMainWorld` 安全暴露 API
|
||||
- CSP 策略:`default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:`
|
||||
- 渲染进程无法直接访问 Node.js API
|
||||
| 层级 | 措施 | 说明 |
|
||||
|------|------|------|
|
||||
| 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. 状态管理架构
|
||||
|
||||
### 3.1 核心功能
|
||||
### 3.1 Zustand Stores
|
||||
|
||||
1. **多标签页**
|
||||
- Ctrl+T 新建空白标签
|
||||
- Ctrl+W 关闭当前标签(未保存时确认)
|
||||
- Ctrl+Tab / Ctrl+Shift+Tab 切换标签
|
||||
- 点击标签切换,hover 显示关闭按钮
|
||||
- 同一文件不会重复打开(自动切换到已有标签)
|
||||
- 每个标签独立保存:内容、滚动位置、光标位置、修改状态
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 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 │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
2. **文件打开**
|
||||
- 工具栏 → 打开按钮(支持 .md / .txt / .markdown)
|
||||
- 拖拽文件到窗口打开(支持多文件同时拖入)
|
||||
- 支持 Windows 文件关联(双击 .md 文件打开)
|
||||
- 支持命令行参数传入文件路径
|
||||
### 3.2 tabStore — 标签页状态
|
||||
|
||||
3. **文件保存**
|
||||
- Ctrl+S 快捷键保存
|
||||
- 工具栏 → 保存按钮
|
||||
- Ctrl+Shift+S 另存为
|
||||
```typescript
|
||||
interface TabState {
|
||||
tabs: Tab[] // 所有标签页
|
||||
activeTabId: string | null // 当前活动标签 ID
|
||||
mruStack: string[] // MRU 标签栈(Ctrl+Tab 切换)
|
||||
|
||||
4. **实时预览**
|
||||
- 左侧编辑器 + 右侧预览(默认分屏模式)
|
||||
- 编辑 150ms 防抖后更新预览
|
||||
- 基于 DOM 位置映射的滚动同步
|
||||
createTab(filePath?, content?) // 创建标签(同文件不重复打开)
|
||||
closeTab(tabId) // 关闭标签(自动切换到相邻标签)
|
||||
switchToTab(tabId) // 切换标签(保存当前状态 → MRU 记录 → 加载目标)
|
||||
updateTabContent(tabId, content) // 更新内容(标记修改)
|
||||
setModified(tabId, modified) // 设置修改状态
|
||||
getActiveTab() // 获取当前标签
|
||||
updateTabScroll(tabId, scroll) // 更新滚动/光标位置
|
||||
}
|
||||
```
|
||||
|
||||
5. **视图模式**
|
||||
- 编辑+预览(Split View)— Ctrl+1
|
||||
- 纯编辑模式 — Ctrl+2
|
||||
- 纯预览模式 — Ctrl+3
|
||||
- 记忆上次使用的视图模式(localStorage)
|
||||
### 3.3 editorStore — 编辑器状态
|
||||
|
||||
6. **文件修改检测**
|
||||
- 主进程通过 `fs.watch` 监听当前活动标签的文件
|
||||
- 外部修改时显示黄色提示横幅
|
||||
- 支持「重新加载」或「忽略」
|
||||
- 切换标签时自动切换监听目标
|
||||
```typescript
|
||||
interface EditorState {
|
||||
viewMode: 'split' | 'editor' | 'preview' // 视图模式
|
||||
darkMode: boolean // 暗色主题
|
||||
splitRatio: number // 分屏比例 (20~80)
|
||||
}
|
||||
```
|
||||
|
||||
7. **未保存提醒**
|
||||
- 关闭窗口时检测所有标签的未保存修改
|
||||
- 弹出确认对话框,防止误操作
|
||||
- 主进程 5 秒超时兜底,防止渲染进程无响应时窗口卡死
|
||||
### 3.4 sidebarStore — 侧边栏状态
|
||||
|
||||
8. **暗色主题**
|
||||
- 工具栏右侧月亮/太阳图标切换
|
||||
- CSS 变量驱动,一键切换整套配色
|
||||
- 主题偏好持久化(localStorage)
|
||||
```typescript
|
||||
interface SidebarState {
|
||||
isVisible: boolean // 是否显示
|
||||
rootPath: string | null // 当前打开的文件夹路径
|
||||
tree: FileNode[] // 目录树数据
|
||||
expandedDirs: Set<string> // 已展开的目录集合
|
||||
sidebarWidth: number // 侧边栏宽度 (180~500)
|
||||
}
|
||||
```
|
||||
|
||||
9. **搜索替换**
|
||||
- Ctrl+F 打开搜索栏,Ctrl+H 打开搜索+替换栏
|
||||
- 实时高亮所有匹配项,当前匹配用不同颜色标识
|
||||
- Enter / Shift+Enter 导航下一个/上一个匹配
|
||||
- 区分大小写(Alt+C)、正则表达式(Alt+R)切换
|
||||
- 替换当前(Ctrl+Shift+G)、全部替换(Ctrl+Shift+H)
|
||||
- 自动将选中文本填充到搜索框
|
||||
- Esc 关闭搜索栏
|
||||
### 3.5 searchStore — 搜索状态
|
||||
|
||||
10. **文件树侧边栏**
|
||||
- 工具栏按钮打开文件夹选择对话框
|
||||
- 递归读取目录结构,只显示 .md/.markdown/.txt 文件
|
||||
- 自动跳过 node_modules、.git、dist 等无关目录
|
||||
- 点击文件夹展开/折叠,点击文件在新标签页打开
|
||||
- 已打开的文件自动切换到对应标签
|
||||
- fs.watch 监听目录变化,自动刷新树
|
||||
- 侧边栏折叠状态持久化(localStorage)
|
||||
```typescript
|
||||
interface SearchState {
|
||||
isVisible: boolean // 搜索栏是否可见
|
||||
showReplace: boolean // 替换行是否展开
|
||||
searchText: string // 搜索文本
|
||||
replaceText: string // 替换文本
|
||||
matches: SearchMatch[] // 所有匹配项
|
||||
currentIndex: number // 当前匹配索引
|
||||
options: SearchOptions // { caseSensitive, useRegex }
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 UI 设计
|
||||
## 4. 数据持久化 — IndexedDB
|
||||
|
||||
#### 色彩方案(亮色)
|
||||
通过 Dexie.js 封装 IndexedDB,替代 localStorage:
|
||||
|
||||
| 角色 | 色值 |
|
||||
|------|------|
|
||||
| 主色调 | `#1a73e8` |
|
||||
| 背景色 | `#ffffff` |
|
||||
| 次级背景 | `#f8f9fa` |
|
||||
| 三级背景 | `#f1f3f4` |
|
||||
| 文字色 | `#333333` |
|
||||
| 次级文字 | `#5f6368` |
|
||||
| 代码块背景 | `#f6f8fa` |
|
||||
| 边框色 | `#e1e4e8` |
|
||||
### 4.1 数据库 Schema
|
||||
|
||||
#### 色彩方案(暗色)
|
||||
```typescript
|
||||
// db/schema.ts
|
||||
const db = new Dexie('MarkLite')
|
||||
|
||||
| 角色 | 色值 |
|
||||
|------|------|
|
||||
| 主色调 | `#8ab4f8` |
|
||||
| 背景色 | `#1e1e1e` |
|
||||
| 次级背景 | `#252526` |
|
||||
| 三级背景 | `#2d2d2d` |
|
||||
| 文字色 | `#d4d4d4` |
|
||||
| 次级文字 | `#9e9e9e` |
|
||||
| 代码块背景 | `#2d2d2d` |
|
||||
| 边框色 | `#3e3e3e` |
|
||||
db.version(1).stores({
|
||||
tabSnapshots: 'id, filePath, updatedAt', // 标签页快照
|
||||
settings: 'id', // 用户设置
|
||||
recentFiles: '++id, filePath, lastOpened' // 最近打开文件
|
||||
})
|
||||
```
|
||||
|
||||
#### 字体
|
||||
### 4.2 数据模型
|
||||
|
||||
- **UI 字体**: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif
|
||||
- **编辑器字体**: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace
|
||||
| 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 解析为 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 布局
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
@@ -182,128 +437,13 @@ MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 4. 文件结构
|
||||
|
||||
```
|
||||
MarkLite/
|
||||
├── package.json # 项目配置与依赖
|
||||
├── main.js # Electron 主进程
|
||||
├── preload.js # 预加载脚本(IPC 桥接)
|
||||
├── renderer/
|
||||
│ ├── index.html # 主页面结构(含侧边栏、搜索栏)
|
||||
│ ├── style.css # UI 样式(亮色/暗色主题、侧边栏、搜索栏)
|
||||
│ └── renderer.js # 渲染进程逻辑(标签页、编辑、预览、搜索、文件树)
|
||||
├── lib/
|
||||
│ ├── marked.min.js # Markdown 解析库(离线)
|
||||
│ ├── highlight.min.js # 代码高亮库(离线)
|
||||
│ └── highlight-github.css # 代码高亮主题
|
||||
├── assets/
|
||||
│ └── icon.ico # 应用图标(多尺寸)
|
||||
├── DESIGN.md # 设计文档(本文件)
|
||||
├── DEVSETUP.md # 开发环境配置指南
|
||||
├── README.md # 项目说明
|
||||
├── LICENSE # MIT 许可证
|
||||
└── .gitignore # Git 忽略文件
|
||||
```
|
||||
|
||||
## 5. IPC 通信设计
|
||||
|
||||
### 5.1 渲染进程 → 主进程(invoke)
|
||||
|
||||
| 通道 | 参数 | 返回值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `dialog:openFile` | 无 | `{ filePath, content }` 或 `null` | 打开文件对话框 |
|
||||
| `file:read` | `filePath` | `{ success, content }` | 读取文件内容 |
|
||||
| `file:save` | `{ filePath, content }` | `{ success, filePath }` | 保存文件 |
|
||||
| `file:saveAs` | `{ content }` | `{ success, filePath }` | 另存为 |
|
||||
| `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 |
|
||||
| `file:stats` | `filePath` | `{ success, size, mtime }` | 获取文件元信息 |
|
||||
| `file:reload` | 无 | `{ success, content, filePath }` | 重新加载当前文件 |
|
||||
| `tab:switched` | `filePath` | 无 | 通知主进程切换活动文件 |
|
||||
| `window:forceClose` | 无 | 无 | 强制关闭窗口(跳过未保存检查) |
|
||||
| `dir:readTree` | `dirPath` | `{ success, tree, rootPath }` | 递归读取目录树 |
|
||||
| `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 |
|
||||
| `dir:watch` | `dirPath` | 无 | 监听目录变化 |
|
||||
| `dir:unwatch` | 无 | 无 | 停止监听目录变化 |
|
||||
|
||||
### 5.2 主进程 → 渲染进程(send)
|
||||
|
||||
| 通道 | 数据 | 说明 |
|
||||
|------|------|------|
|
||||
| `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 |
|
||||
| `file:opened` | `{ filePath, content }` | 文件已打开(兼容旧路径) |
|
||||
| `file:externallyModified` | `filePath` | 文件被外部修改 |
|
||||
| `menu:save` | 无 | 菜单触发保存 |
|
||||
| `menu:saveAs` | 无 | 菜单触发另存为 |
|
||||
| `menu:viewMode` | `mode` | 菜单切换视图模式 |
|
||||
| `window:confirmClose` | 无 | 请求确认关闭 |
|
||||
| `window:closing` | 无 | 窗口即将关闭 |
|
||||
| `sidebar:dirChanged` | 无 | 目录结构变化,通知渲染进程刷新树 |
|
||||
|
||||
## 6. 标签页数据模型
|
||||
|
||||
每个标签页在渲染进程中维护独立状态:
|
||||
|
||||
```javascript
|
||||
{
|
||||
id: Number, // 唯一标识
|
||||
filePath: String | null, // 文件路径(未命名标签为 null)
|
||||
content: String, // 编辑器内容
|
||||
isModified: Boolean, // 是否已修改
|
||||
scrollTop: Number, // 编辑器滚动位置
|
||||
scrollLeft: Number,
|
||||
selectionStart: Number, // 光标选区
|
||||
selectionEnd: Number,
|
||||
previewScrollTop: Number // 预览面板滚动位置
|
||||
}
|
||||
```
|
||||
|
||||
切换标签时自动保存当前状态、恢复目标状态。
|
||||
|
||||
## 7. 数据持久化
|
||||
|
||||
通过 `localStorage` 存储用户偏好,key 为 `marklite-settings`:
|
||||
|
||||
```json
|
||||
{
|
||||
"darkMode": false,
|
||||
"viewMode": "split",
|
||||
"splitRatio": 50,
|
||||
"sidebarCollapsed": false
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `darkMode` | boolean | 暗色主题开关 |
|
||||
| `viewMode` | string | 视图模式:`split` / `editor` / `preview` |
|
||||
| `splitRatio` | number | 分屏比例(20~80) |
|
||||
| `sidebarCollapsed` | boolean | 侧边栏是否折叠 |
|
||||
|
||||
## 8. Markdown 渲染支持
|
||||
|
||||
支持标准 Markdown 和 GFM(GitHub Flavored Markdown):
|
||||
|
||||
- 标题(h1-h6)
|
||||
- 段落、换行
|
||||
- **粗体**、*斜体*、~~删除线~~
|
||||
- 有序/无序列表
|
||||
- 任务列表(`- [x]`)
|
||||
- 代码块(围栏式 + 语法高亮,180+ 语言)
|
||||
- 行内代码
|
||||
- 链接、图片
|
||||
- 表格
|
||||
- 引用块
|
||||
- 水平线
|
||||
- HTML 内联
|
||||
|
||||
## 9. 快捷键
|
||||
## 11. 快捷键
|
||||
|
||||
| 快捷键 | 功能 |
|
||||
|:-------|:-----|
|
||||
| `Ctrl + T` | 新建标签页 |
|
||||
| `Ctrl + W` | 关闭当前标签页 |
|
||||
| `Ctrl + Tab` | 切换到下一个标签页 |
|
||||
| `Ctrl + Tab` | 切换到下一个标签页(MRU 顺序) |
|
||||
| `Ctrl + Shift + Tab` | 切换到上一个标签页 |
|
||||
| `Ctrl + O` | 打开文件 |
|
||||
| `Ctrl + S` | 保存文件 |
|
||||
@@ -320,15 +460,80 @@ MarkLite/
|
||||
| `Ctrl + Shift + H` | 全部替换 |
|
||||
| `Esc` | 关闭搜索栏 |
|
||||
|
||||
## 10. 构建与发布
|
||||
## 12. 构建与发布
|
||||
|
||||
使用 `electron-builder` 打包:
|
||||
### 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`
|
||||
- 支持自定义安装目录、桌面/开始菜单快捷方式
|
||||
- 便携版:`npm run build:portable`
|
||||
|
||||
详见 [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 规则 |
|
||||
|
||||
Reference in New Issue
Block a user