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

25 KiB
Raw Blame History

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 — 标签页状态

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: 'split' | 'editor' | 'preview'  // 视图模式
  darkMode: boolean                          // 暗色主题
  splitRatio: number                         // 分屏比例 (20~80)
}

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, 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.tssrc/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          解析为 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 开发模式

npm run dev          # electron-vite devHMR 热更新)

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 规则