release: v0.5.0 — 升级 MetonaEditor 0.4.0 / MetonaToast 0.5.0,数据库迁移 MetonaSqlark 0.4.1
- 编辑器升级 0.4.0:TypeScript 全模块重构、启用浮动格式工具栏 - 通知升级 0.5.0:钩子系统、新增动画 - 移除 Dexie.js,迁移至 MetonaSqlark 0.4.1(AriaEngine:LSM-Tree + WAL + MVCC) - 数据库更换为 MarkLiteV2,旧 Dexie 数据放弃 - 修复 Editor 组件无条件重渲染(getActiveTab 返回新引用) - 更新关于弹框、README、DESIGN 文档
This commit is contained in:
@@ -1,420 +1,433 @@
|
||||
# MarkLite v0.4.3 — 架构设计文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 MetonaEditor v0.1.14 编辑器(三模式视图 + 插件系统)、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
|
||||
|
||||
### 1.1 核心原则
|
||||
|
||||
1. **类型安全** — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义
|
||||
2. **模块化** — 源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types)
|
||||
3. **安全隔离** — contextIsolation + nodeIntegration:false + CSP + rehype-sanitize
|
||||
4. **可测试性** — 业务逻辑(lib/)与 UI(components/)解耦
|
||||
|
||||
## 2. 技术架构
|
||||
|
||||
### 2.1 技术栈
|
||||
|
||||
| 组件 | 技术 | 版本 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
|
||||
| 前端框架 | React | v18 | 函数组件 + Hooks |
|
||||
| 类型系统 | TypeScript | v5.6 | 全量类型安全 |
|
||||
| 编辑器 | MetonaEditor | v0.1.14 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
||||
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
|
||||
| 持久化 | Dexie.js (IndexedDB) | v4 | 标签页状态 / 用户设置 / 最近文件 |
|
||||
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线(作为 MetonaEditor render 钩子) |
|
||||
| 代码高亮 | rehype-highlight | v7 | 基于 highlight.js |
|
||||
| Toast | @metona-team/metona-toast | v2.0.1 | 通知提示组件 |
|
||||
| 构建工具 | electron-vite | v3 | Electron + Vite,HMR 热更新 |
|
||||
| 打包工具 | electron-builder | v25 | Windows NSIS 安装包 |
|
||||
| 样式 | CSS Variables | — | 主题驱动,亮色/暗色 |
|
||||
|
||||
### 2.2 进程架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Main Process (src/main/) 5 文件 │
|
||||
│ index.ts 入口:窗口创建、app 生命周期、单实例锁 │
|
||||
│ ipc-handlers.ts 所有 ipcMain.handle 注册 │
|
||||
│ file-system.ts 文件读写、目录树构建、BOM 剥离 │
|
||||
│ file-watcher.ts fs.watch 封装(单文件 + 目录监听) │
|
||||
│ window-manager.ts 窗口创建、关闭拦截、单实例锁 │
|
||||
└───────────────────────┬──────────────────────────────────────┘
|
||||
│ contextBridge (安全隔离)
|
||||
┌───────────────────────▼──────────────────────────────────────┐
|
||||
│ Preload Script (src/preload/) 1 文件 │
|
||||
│ index.ts contextBridge 类型安全暴露 │
|
||||
│ electronAPI 22 个方法/事件的类型安全接口 │
|
||||
└───────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────▼──────────────────────────────────────┐
|
||||
│ Renderer Process (src/renderer/) React 18 │
|
||||
│ │
|
||||
│ components/ Toolbar · TabBar · Editor · Sidebar │
|
||||
│ FileTree · OutlinePanel · WelcomeScreen │
|
||||
│ ConfirmDialog · ModifiedBanner │
|
||||
│ DropOverlay · ErrorBoundary · AboutDialog│
|
||||
│ LoadingSpinner · Icons │
|
||||
│ │
|
||||
│ stores/ (4) tabStore · editorStore · sidebarStore │
|
||||
│ autoSaveStore │
|
||||
│ hooks/ (15) useTheme · useSettingsInit · useKeyboard │
|
||||
│ useDragDrop · useFileWatch · useAutoSave │
|
||||
│ useIpcListeners · useFileOperations ... │
|
||||
│ lib/ (4) markdown · fileUtils · errorHandler │
|
||||
│ toast · constants │
|
||||
│ db/ (4) schema · tabRepository · settingsRepo │
|
||||
│ recentFilesRepository │
|
||||
│ types/ (5) tab · file · settings · ipc · index │
|
||||
│ styles/ (3) variables · global · markdown-body │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Shared (src/shared/) 3 文件 │
|
||||
│ ipc-channels.ts IPC 通道名常量 │
|
||||
│ types.ts 共享类型定义 │
|
||||
│ constants.ts 共享常量 (版本号、文件大小限制等) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 安全模型
|
||||
|
||||
| 层级 | 措施 | 说明 |
|
||||
|------|------|------|
|
||||
| Electron | `contextIsolation: true` | 渲染进程与主进程隔离 |
|
||||
| Electron | `nodeIntegration: false` | 渲染进程无法访问 Node.js API |
|
||||
| IPC | `contextBridge.exposeInMainWorld` | 仅暴露 18 个类型安全方法 + 4 个事件订阅 |
|
||||
| CSP | `default-src 'self'; script-src 'self'` | 阻断内联脚本、外部资源 |
|
||||
| HTML | `rehype-sanitize` | 渲染 Markdown 时过滤危险标签/属性 |
|
||||
| 链接 | 协议白名单 | 仅允许 `http:` / `https:` / `#` 锚点 |
|
||||
| 路径 | `validatePath()` | 防止路径遍历攻击 |
|
||||
|
||||
## 3. 状态管理架构
|
||||
|
||||
### 3.1 Zustand Stores
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ App.tsx (根组件) │
|
||||
├─────────┬──────────┬──────────┬──────────────────────────┤
|
||||
│Toolbar │ TabBar │ Sidebar │ Editor (MetonaEditor) │
|
||||
├─────────┴──────────┴──────────┴──────────────────────────┤
|
||||
│ Zustand Stores │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
||||
│ │ tabStore │ │editorStore│ │sidebarStore│ │autoSaveStore│ │
|
||||
│ │ - tabs │ │- viewMode│ │- tree │ │- isSaving │ │
|
||||
│ │- activeId│ │- darkMode│ │- expanded │ │- enabled │ │
|
||||
│ │ - mru │ │- extMod │ │- rootPath │ │ │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────▼────────────▼────────────▼──────────────▼──────┐ │
|
||||
│ │ IndexedDB (Dexie.js) │ │
|
||||
│ │ tabSnapshots │ settings │ recentFiles │ activeTab │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 tabStore — 标签页状态
|
||||
|
||||
```typescript
|
||||
interface TabState {
|
||||
tabs: Tab[] // 所有标签页
|
||||
activeTabId: string | null // 当前活动标签 ID
|
||||
mruStack: string[] // MRU 标签栈(Ctrl+Tab 切换)
|
||||
|
||||
createTab(filePath?, content?) // 创建标签(同文件不重复打开)
|
||||
closeTab(tabId) // 关闭标签(自动切换到相邻标签)
|
||||
switchToTab(tabId) // 切换标签
|
||||
updateTabContent(tabId, content) // 更新内容(标记修改)
|
||||
setModified(tabId, modified) // 设置修改状态
|
||||
getActiveTab() // 获取当前标签
|
||||
updateTabScroll(tabId, scroll) // 更新滚动/光标位置
|
||||
saveToDB() // 保存到 IndexedDB(防抖 500ms)
|
||||
loadFromDB() // 从 IndexedDB 加载
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 editorStore — 编辑器状态
|
||||
|
||||
```typescript
|
||||
interface EditorState {
|
||||
viewMode: 'editor' | 'preview' | 'source' // 视图模式
|
||||
darkMode: boolean // 暗色主题
|
||||
externallyModified: { filePath: string } | null // 外部修改检测状态
|
||||
loadingStates: Record<string, boolean> // 全局加载状态
|
||||
}
|
||||
|
||||
// 模块级 getter — 供 Sidebar/OutlinePanel 访问 MetonaEditor 实例
|
||||
function getMetonaEditor(): MarkdownEditor | null
|
||||
function setMetonaEditorGetter(fn: () => MarkdownEditor | null): void
|
||||
```
|
||||
|
||||
### 3.4 sidebarStore — 侧边栏状态
|
||||
|
||||
```typescript
|
||||
interface SidebarState {
|
||||
isVisible: boolean // 是否显示
|
||||
rootPath: string | null // 当前打开的文件夹路径
|
||||
tree: FileNode[] // 目录树数据
|
||||
expandedDirs: string[] // 已展开的目录集合
|
||||
sidebarWidth: number // 侧边栏宽度 (180~500)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 数据持久化 — IndexedDB
|
||||
|
||||
通过 Dexie.js 封装 IndexedDB:
|
||||
|
||||
### 4.1 数据库 Schema
|
||||
|
||||
```typescript
|
||||
const db = new Dexie('MarkLite')
|
||||
|
||||
db.version(1).stores({
|
||||
tabSnapshots: 'id, filePath, updatedAt', // 标签页快照
|
||||
settings: 'id', // 用户设置
|
||||
recentFiles: '++id, filePath, lastOpened', // 最近打开文件
|
||||
activeTab: 'id' // 当前活动标签
|
||||
})
|
||||
```
|
||||
|
||||
### 4.2 数据模型
|
||||
|
||||
| Store | 字段 | 说明 |
|
||||
|-------|------|------|
|
||||
| `tabSnapshots` | id, filePath, content, scrollTop, selectionStart, selectionEnd, isModified, updatedAt | 标签页状态快照 |
|
||||
| `settings` | id, darkMode, viewMode, sidebarCollapsed, sidebarWidth | 用户偏好设置 |
|
||||
| `recentFiles` | ++id, filePath, lastOpened | 最近打开文件列表 |
|
||||
| `activeTab` | id, activeTabId | 当前活动标签 ID |
|
||||
|
||||
## 5. IPC 通信设计
|
||||
|
||||
### 5.1 渲染进程 → 主进程(invoke)
|
||||
|
||||
| 通道 | 参数 | 返回值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `dialog:openFile` | 无 | `OpenFileResponse` | 打开文件对话框 |
|
||||
| `file:read` | `filePath` | `ReadFileResult` | 读取文件内容 |
|
||||
| `file:save` | `{ filePath, content }` | `SaveFileResult` | 保存文件 |
|
||||
| `file:saveAs` | `{ content }` | `SaveFileResult` | 另存为 |
|
||||
| `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 |
|
||||
| `file:stats` | `filePath` | `FileStatsResult` | 获取文件元信息 |
|
||||
| `file:reload` | 无 | `ReloadFileResult` | 重新加载当前文件 |
|
||||
| `tab:switched` | `filePath \| null` | `void` | 通知主进程切换活动文件 |
|
||||
| `window:forceClose` | 无 | `void` | 强制关闭窗口 |
|
||||
| `window:cancelClose` | 无 | `void` | 取消关闭 |
|
||||
| `dir:readTree` | `dirPath` | `ReadDirTreeResult` | 递归读取目录树 |
|
||||
| `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 |
|
||||
| `dir:watch` | `dirPath` | `void` | 监听目录变化 |
|
||||
| `dir:unwatch` | 无 | `void` | 停止监听目录变化 |
|
||||
|
||||
### 5.2 主进程 → 渲染进程(send)
|
||||
|
||||
| 通道 | 数据 | 说明 |
|
||||
|------|------|------|
|
||||
| `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 |
|
||||
| `file:externallyModified` | `filePath` | 文件被外部修改 |
|
||||
| `window:confirmClose` | 无 | 请求确认关闭 |
|
||||
| `sidebar:dirChanged` | 无 | 目录结构变化 |
|
||||
|
||||
## 6. 编辑器架构 — MetonaEditor v0.1.14
|
||||
|
||||
### 6.1 三模式视图
|
||||
|
||||
- **编辑模式 (edit)**:纯文本编辑器,显示 Markdown 源码
|
||||
- **分屏模式 (split)**:左侧编辑、右侧实时预览,同步滚动
|
||||
- **预览模式 (preview)**:仅显示渲染后的 HTML
|
||||
|
||||
MetonaEditor 内置模式切换工具栏,与应用层的 viewMode store 双向同步,切换时自动持久化到 IndexedDB。
|
||||
|
||||
### 6.2 内置功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 格式化工具栏 | bold / italic / strikethrough / underline / code / h1-h3 / quote / ul / ol / indent / outdent / link / image / table / hr |
|
||||
| 搜索替换 | Ctrl+F / Ctrl+H,支持正则、大小写敏感 |
|
||||
| 历史栈 | undo / redo,防抖合并,可配置上限 |
|
||||
| 主题 | light / dark / auto / warm,CSS 变量驱动 |
|
||||
| 国际化 | zh-CN / en-US 完整翻译 |
|
||||
| 全屏模式 | 编辑器全屏展示 |
|
||||
|
||||
### 6.3 插件体系
|
||||
|
||||
通过 `plugins` 配置数组安装 MetonaEditor 预设插件:
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| searchReplace | Ctrl+F 查找、Ctrl+H 替换面板 |
|
||||
| imagePaste | Ctrl+V 粘贴剪贴板图片,自动转 base64 |
|
||||
|
||||
> 注:autoSave 插件仅支持 localStorage,而 MarkLite 需要文件系统保存(Electron IPC),因此使用自定义 useAutoSave hook。
|
||||
|
||||
### 6.4 渲染管线集成
|
||||
|
||||
通过 MetonaEditor 的 `render` 钩子接入 unified/rehype 管线,实现:
|
||||
|
||||
- **相对路径图片解析**:将相对路径转换为 `file://` 绝对路径
|
||||
- **XSS 防护**:rehype-sanitize 过滤危险标签
|
||||
- **代码高亮**:rehype-highlight 语法高亮
|
||||
- **处理器缓存**:LRU 缓存(最多 20 个),按文件路径分桶
|
||||
|
||||
```
|
||||
Markdown 源码
|
||||
│
|
||||
▼
|
||||
unified 管线(renderMarkdownSync)
|
||||
├── remark-parse 解析为 MDAST
|
||||
├── remark-gfm GFM 扩展
|
||||
├── remark-rehype 转换为 HAST
|
||||
├── rehype-raw 解析内联 HTML
|
||||
├── rehype-sanitize 安全过滤
|
||||
├── rehype-fixImages 相对路径 → file://
|
||||
├── rehype-highlight 代码高亮
|
||||
└── rehype-stringify 序列化为 HTML
|
||||
│
|
||||
▼
|
||||
MetonaEditor 预览区渲染
|
||||
```
|
||||
|
||||
### 6.5 主题切换
|
||||
|
||||
MetonaEditor 的 CSS 样式通过 wrapper 元素上的 inline `--md-*` CSS 变量驱动。主题切换流程:
|
||||
|
||||
1. `MeEditor.setTheme(dark/light)` — 更新 documentElement 全局变量 + localStorage
|
||||
2. 手动覆写 `.me-wrapper` 上的 inline CSS 变量(`style.setProperty`)
|
||||
3. 双向同步:应用工具栏暗色按钮 ⇄ 编辑器主题
|
||||
|
||||
### 6.6 内容同步
|
||||
|
||||
- **编辑 → 存储**:`onChange` 回调 → `updateTabContent` + `setModified`
|
||||
- **标签切换**:`setValue(content, { silent: true })` 静默更新,避免重复触发 onChange
|
||||
- **滚动持久化**:切换标签时通过 DOM 查询 `textarea` / `.me-preview` 保存/恢复滚动位置
|
||||
|
||||
## 7. Markdown 渲染管线
|
||||
|
||||
```
|
||||
Markdown 文本
|
||||
│
|
||||
▼
|
||||
remark-parse 解析为 MDAST
|
||||
│
|
||||
▼
|
||||
remark-gfm 扩展 GFM 语法
|
||||
│
|
||||
▼
|
||||
remark-rehype 转换为 HAST
|
||||
│
|
||||
▼
|
||||
rehype-raw 解析内联 HTML
|
||||
│
|
||||
▼
|
||||
rehype-sanitize 安全过滤
|
||||
│
|
||||
▼
|
||||
rehype-fixImages 相对路径图片转 file:// URL
|
||||
│
|
||||
▼
|
||||
rehype-highlight 代码语法高亮
|
||||
│
|
||||
▼
|
||||
rehype-stringify 序列化为 HTML
|
||||
│
|
||||
▼
|
||||
Renderer (MetonaEditor preview / Preview component)
|
||||
```
|
||||
|
||||
## 8. UI 设计
|
||||
|
||||
### 8.1 色彩方案
|
||||
|
||||
#### 亮色主题
|
||||
|
||||
| 角色 | CSS 变量 | 色值 |
|
||||
|------|----------|------|
|
||||
| 主色调 | `--primary` | `#1a73e8` |
|
||||
| 背景色 | `--bg` | `#ffffff` |
|
||||
| 次级背景 | `--bg-secondary` | `#f8f9fa` |
|
||||
| 文字色 | `--text` | `#333333` |
|
||||
| 边框色 | `--border` | `#e1e4e8` |
|
||||
|
||||
#### 暗色主题
|
||||
|
||||
| 角色 | CSS 变量 | 色值 |
|
||||
|------|----------|------|
|
||||
| 主色调 | `--primary` | `#8ab4f8` |
|
||||
| 背景色 | `--bg` | `#1e1e1e` |
|
||||
| 次级背景 | `--bg-secondary` | `#252526` |
|
||||
| 文字色 | `--text` | `#d4d4d4` |
|
||||
| 边框色 | `--border` | `#3e3e3e` |
|
||||
|
||||
### 8.2 布局
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ MarkLite - filename.md ─ □ ✕ │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ 📁 打开 │ 💾 保存 │ 自动 │ 🌙 🌐 ℹ️ │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ [file1.md] [file2.md] [未命名] [+] │
|
||||
├──────────┬───────────────────────────────────────────────────────────────┤
|
||||
│ 资源管理器 │ │
|
||||
│ ▼ project │ ┌─────────────────────────────────────────────┐ │
|
||||
│ 📁 src │ │ B I S U </> │ H1 H2 H3 │ " 1. 2. ≡ ⇥ ⇤ │ │ │
|
||||
│ 📄 file1│ │ 🔗 🖼 ⊞ — │ ↶ ↷ │ 📝 ⇔ 👁 ⊞ │ │ │
|
||||
│ 📄 file2│ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ # Title │ Title │ │
|
||||
│ 文档大纲 │ │ │ ─────── │ │
|
||||
│ · Title │ │ content... │ content... │ │
|
||||
├──────────┴──┴─────────────────────────────────────────────┴──────────────┤
|
||||
│ (MetonaEditor 底栏: 字数/行数/阅读时间) │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 9. 构建与发布
|
||||
|
||||
### 9.1 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev # electron-vite dev(HMR 热更新)
|
||||
```
|
||||
|
||||
### 9.2 生产构建
|
||||
|
||||
```bash
|
||||
npm run build # electron-vite build + electron-builder --win
|
||||
npm run build:portable # 便携版(免安装)
|
||||
```
|
||||
|
||||
### 9.3 打包配置
|
||||
|
||||
- 输出格式:NSIS 安装包(.exe)
|
||||
- 目标平台:Windows x64
|
||||
- 应用图标:assets/icon.ico
|
||||
- 文件关联:`.md` / `.markdown` / `.txt`
|
||||
- 支持自定义安装目录、桌面/开始菜单快捷方式
|
||||
|
||||
## 10. 依赖清单
|
||||
|
||||
### 运行时依赖
|
||||
|
||||
| 包名 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| react / react-dom | ^18.3 | UI 框架 |
|
||||
| zustand | ^5.0 | 状态管理 |
|
||||
| dexie | ^4.0 | IndexedDB 封装 |
|
||||
| nanoid | ^5.0 | 唯一 ID 生成 |
|
||||
| @metona-team/metona-editor | 0.1.14 | Markdown 编辑器(零依赖) |
|
||||
| @metona-team/metona-toast | ^2.0.1 | Toast 通知组件 |
|
||||
| unified / remark / rehype | ^11.0 | Markdown 渲染管线 |
|
||||
| rehype-highlight | ^7.0 | 代码语法高亮 |
|
||||
|
||||
### 开发依赖
|
||||
|
||||
| 包名 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| electron | ^28.0 | 桌面框架 |
|
||||
| electron-builder | ^25.0 | 打包工具 |
|
||||
| electron-vite | ^3.0 | 构建工具 |
|
||||
| typescript | ^5.6 | 类型系统 |
|
||||
| eslint | ^9.0 | 代码检查 |
|
||||
# MarkLite v0.4.3 — 架构设计文档
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
MarkLite 是一款轻量级的 Windows 本地 Markdown 编辑器桌面应用程序。基于 Electron + React + TypeScript 构建,采用 MetonaEditor v0.1.14 编辑器(三模式视图 + 插件系统)、Zustand 状态管理、IndexedDB 持久化、unified/rehype Markdown 渲染管线。
|
||||
|
||||
### 1.1 核心原则
|
||||
|
||||
1. **类型安全** — 全量 TypeScript,所有 IPC 通信、状态、接口均有类型定义
|
||||
2. **模块化** — 源文件按职责分层:主进程 / 预加载 / 渲染进程(组件 / stores / hooks / lib / db / types)
|
||||
3. **安全隔离** — contextIsolation + nodeIntegration:false + CSP + rehype-sanitize
|
||||
4. **可测试性** — 业务逻辑(lib/)与 UI(components/)解耦
|
||||
|
||||
## 2. 技术架构
|
||||
|
||||
### 2.1 技术栈
|
||||
|
||||
| 组件 | 技术 | 版本 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 桌面框架 | Electron | v28 | 跨平台桌面应用框架 |
|
||||
| 前端框架 | React | v18 | 函数组件 + Hooks |
|
||||
| 类型系统 | TypeScript | v5.6 | 全量类型安全 |
|
||||
| 编辑器 | MetonaEditor | v0.4.0 | 零依赖 Markdown 编辑器,三模式视图 + 插件系统 |
|
||||
| 状态管理 | Zustand | v5 | 轻量级状态管理 |
|
||||
| 持久化 | MetonaSqlark (IndexedDB) | v0.4.1 | 标签页状态 / 用户设置 / 最近文件(AriaEngine) |
|
||||
| Markdown 解析 | unified / remark / rehype | v11 | 插件化渲染管线(作为 MetonaEditor render 钩子) |
|
||||
| 代码高亮 | rehype-highlight | v7 | 基于 highlight.js |
|
||||
| Toast | @metona-team/metona-toast | v0.5.0 | 通知提示组件 |
|
||||
| 构建工具 | electron-vite | v3 | Electron + Vite,HMR 热更新 |
|
||||
| 打包工具 | electron-builder | v25 | Windows NSIS 安装包 |
|
||||
| 样式 | CSS Variables | — | 主题驱动,亮色/暗色 |
|
||||
|
||||
### 2.2 进程架构
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Main Process (src/main/) 5 文件 │
|
||||
│ index.ts 入口:窗口创建、app 生命周期、单实例锁 │
|
||||
│ ipc-handlers.ts 所有 ipcMain.handle 注册 │
|
||||
│ file-system.ts 文件读写、目录树构建、BOM 剥离 │
|
||||
│ file-watcher.ts fs.watch 封装(单文件 + 目录监听) │
|
||||
│ window-manager.ts 窗口创建、关闭拦截、单实例锁 │
|
||||
└───────────────────────┬──────────────────────────────────────┘
|
||||
│ contextBridge (安全隔离)
|
||||
┌───────────────────────▼──────────────────────────────────────┐
|
||||
│ Preload Script (src/preload/) 1 文件 │
|
||||
│ index.ts contextBridge 类型安全暴露 │
|
||||
│ electronAPI 22 个方法/事件的类型安全接口 │
|
||||
└───────────────────────┬──────────────────────────────────────┘
|
||||
│
|
||||
┌───────────────────────▼──────────────────────────────────────┐
|
||||
│ Renderer Process (src/renderer/) React 18 │
|
||||
│ │
|
||||
│ components/ Toolbar · TabBar · Editor · Sidebar │
|
||||
│ FileTree · OutlinePanel · WelcomeScreen │
|
||||
│ ConfirmDialog · ModifiedBanner │
|
||||
│ DropOverlay · ErrorBoundary · AboutDialog│
|
||||
│ LoadingSpinner · Icons │
|
||||
│ │
|
||||
│ stores/ (4) tabStore · editorStore · sidebarStore │
|
||||
│ autoSaveStore │
|
||||
│ hooks/ (15) useTheme · useSettingsInit · useKeyboard │
|
||||
│ useDragDrop · useFileWatch · useAutoSave │
|
||||
│ useIpcListeners · useFileOperations ... │
|
||||
│ lib/ (4) markdown · fileUtils · errorHandler │
|
||||
│ toast · constants │
|
||||
│ db/ (4) schema · tabRepository · settingsRepo │
|
||||
│ recentFilesRepository │
|
||||
│ types/ (5) tab · file · settings · ipc · index │
|
||||
│ styles/ (3) variables · global · markdown-body │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ Shared (src/shared/) 3 文件 │
|
||||
│ ipc-channels.ts IPC 通道名常量 │
|
||||
│ types.ts 共享类型定义 │
|
||||
│ constants.ts 共享常量 (版本号、文件大小限制等) │
|
||||
└──────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.3 安全模型
|
||||
|
||||
| 层级 | 措施 | 说明 |
|
||||
|------|------|------|
|
||||
| Electron | `contextIsolation: true` | 渲染进程与主进程隔离 |
|
||||
| Electron | `nodeIntegration: false` | 渲染进程无法访问 Node.js API |
|
||||
| IPC | `contextBridge.exposeInMainWorld` | 仅暴露 18 个类型安全方法 + 4 个事件订阅 |
|
||||
| CSP | `default-src 'self'; script-src 'self'` | 阻断内联脚本、外部资源 |
|
||||
| HTML | `rehype-sanitize` | 渲染 Markdown 时过滤危险标签/属性 |
|
||||
| 链接 | 协议白名单 | 仅允许 `http:` / `https:` / `#` 锚点 |
|
||||
| 路径 | `validatePath()` | 防止路径遍历攻击 |
|
||||
|
||||
## 3. 状态管理架构
|
||||
|
||||
### 3.1 Zustand Stores
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ App.tsx (根组件) │
|
||||
├─────────┬──────────┬──────────┬──────────────────────────┤
|
||||
│Toolbar │ TabBar │ Sidebar │ Editor (MetonaEditor) │
|
||||
├─────────┴──────────┴──────────┴──────────────────────────┤
|
||||
│ Zustand Stores │
|
||||
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │
|
||||
│ │ tabStore │ │editorStore│ │sidebarStore│ │autoSaveStore│ │
|
||||
│ │ - tabs │ │- viewMode│ │- tree │ │- isSaving │ │
|
||||
│ │- activeId│ │- darkMode│ │- expanded │ │- enabled │ │
|
||||
│ │ - mru │ │- extMod │ │- rootPath │ │ │ │
|
||||
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │
|
||||
│ │ │ │ │ │
|
||||
│ ┌────▼────────────▼────────────▼──────────────▼──────┐ │
|
||||
│ │ IndexedDB (MetonaSqlark) │ │
|
||||
│ │ tabSnapshots │ settings │ recentFiles │ activeTab │ │
|
||||
│ └───────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3.2 tabStore — 标签页状态
|
||||
|
||||
```typescript
|
||||
interface TabState {
|
||||
tabs: Tab[] // 所有标签页
|
||||
activeTabId: string | null // 当前活动标签 ID
|
||||
mruStack: string[] // MRU 标签栈(Ctrl+Tab 切换)
|
||||
|
||||
createTab(filePath?, content?) // 创建标签(同文件不重复打开)
|
||||
closeTab(tabId) // 关闭标签(自动切换到相邻标签)
|
||||
switchToTab(tabId) // 切换标签
|
||||
updateTabContent(tabId, content) // 更新内容(标记修改)
|
||||
setModified(tabId, modified) // 设置修改状态
|
||||
getActiveTab() // 获取当前标签
|
||||
updateTabScroll(tabId, scroll) // 更新滚动/光标位置
|
||||
saveToDB() // 保存到 IndexedDB(防抖 500ms)
|
||||
loadFromDB() // 从 IndexedDB 加载
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 editorStore — 编辑器状态
|
||||
|
||||
```typescript
|
||||
interface EditorState {
|
||||
viewMode: 'editor' | 'preview' | 'source' // 视图模式
|
||||
darkMode: boolean // 暗色主题
|
||||
externallyModified: { filePath: string } | null // 外部修改检测状态
|
||||
loadingStates: Record<string, boolean> // 全局加载状态
|
||||
}
|
||||
|
||||
// 模块级 getter — 供 Sidebar/OutlinePanel 访问 MetonaEditor 实例
|
||||
function getMetonaEditor(): MarkdownEditor | null
|
||||
function setMetonaEditorGetter(fn: () => MarkdownEditor | null): void
|
||||
```
|
||||
|
||||
### 3.4 sidebarStore — 侧边栏状态
|
||||
|
||||
```typescript
|
||||
interface SidebarState {
|
||||
isVisible: boolean // 是否显示
|
||||
rootPath: string | null // 当前打开的文件夹路径
|
||||
tree: FileNode[] // 目录树数据
|
||||
expandedDirs: string[] // 已展开的目录集合
|
||||
sidebarWidth: number // 侧边栏宽度 (180~500)
|
||||
}
|
||||
```
|
||||
|
||||
## 4. 数据持久化 — IndexedDB
|
||||
|
||||
v0.5.0: 由 Dexie.js 迁移至 MetonaSqlark(AriaEngine:自研 LSM-Tree + WAL + MVCC 存储引擎,对标 SQLite)。
|
||||
MetonaSqlark 的 create() 为异步,采用懒加载单例(getDb()),
|
||||
表结构幂等创建(查表名后 defineTable),数据库名更换为 MarkLiteV2(旧 Dexie 数据已放弃)。
|
||||
|
||||
### 4.1 数据库 Schema
|
||||
|
||||
```typescript
|
||||
// schema.ts — 懒加载单例
|
||||
let dbPromise: Promise<MetonaSqlark> | null = null
|
||||
export function getDb(): Promise<MetonaSqlark> {
|
||||
if (!dbPromise) {
|
||||
dbPromise = MetonaSqlark.create({
|
||||
name: 'MarkLiteV2',
|
||||
mode: 'aria', // AriaEngine: LSM-Tree + WAL + MVCC 快照隔离
|
||||
diskEngine: 'indexeddb', // 底层存储后端(indexeddb | opfs | memory)
|
||||
version: 1,
|
||||
}).then(async (db) => {
|
||||
if (!(await db.getTableNames()).includes('tabSnapshots')) {
|
||||
await db.defineTable('tabSnapshots', { id: { type: 'string', primaryKey: true }, ... })
|
||||
}
|
||||
// settings / recentFiles / activeTab 同理
|
||||
return db
|
||||
})
|
||||
}
|
||||
return dbPromise
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 数据模型
|
||||
|
||||
| Store | 字段 | 说明 |
|
||||
|-------|------|------|
|
||||
| `tabSnapshots` | id(PK), filePath, content, scrollTop, selectionStart, selectionEnd, isModified, updatedAt(index) | 标签页状态快照 |
|
||||
| `settings` | id(PK), themeMode, viewMode, sidebarCollapsed, sidebarWidth | 用户偏好设置 |
|
||||
| `recentFiles` | filePath(PK), lastOpened(index) | 最近打开文件列表(v0.5.0 改用 filePath 主键,sqlark 无自增) |
|
||||
| `activeTab` | id(PK), activeTabId | 当前活动标签 ID |
|
||||
|
||||
## 5. IPC 通信设计
|
||||
|
||||
### 5.1 渲染进程 → 主进程(invoke)
|
||||
|
||||
| 通道 | 参数 | 返回值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `dialog:openFile` | 无 | `OpenFileResponse` | 打开文件对话框 |
|
||||
| `file:read` | `filePath` | `ReadFileResult` | 读取文件内容 |
|
||||
| `file:save` | `{ filePath, content }` | `SaveFileResult` | 保存文件 |
|
||||
| `file:saveAs` | `{ content }` | `SaveFileResult` | 另存为 |
|
||||
| `file:getCurrentPath` | 无 | `string \| null` | 获取当前文件路径 |
|
||||
| `file:stats` | `filePath` | `FileStatsResult` | 获取文件元信息 |
|
||||
| `file:reload` | 无 | `ReloadFileResult` | 重新加载当前文件 |
|
||||
| `tab:switched` | `filePath \| null` | `void` | 通知主进程切换活动文件 |
|
||||
| `window:forceClose` | 无 | `void` | 强制关闭窗口 |
|
||||
| `window:cancelClose` | 无 | `void` | 取消关闭 |
|
||||
| `dir:readTree` | `dirPath` | `ReadDirTreeResult` | 递归读取目录树 |
|
||||
| `dir:openDialog` | 无 | `string \| null` | 打开文件夹选择对话框 |
|
||||
| `dir:watch` | `dirPath` | `void` | 监听目录变化 |
|
||||
| `dir:unwatch` | 无 | `void` | 停止监听目录变化 |
|
||||
|
||||
### 5.2 主进程 → 渲染进程(send)
|
||||
|
||||
| 通道 | 数据 | 说明 |
|
||||
|------|------|------|
|
||||
| `file:openInTab` | `{ filePath, content }` | 在新标签中打开文件 |
|
||||
| `file:externallyModified` | `filePath` | 文件被外部修改 |
|
||||
| `window:confirmClose` | 无 | 请求确认关闭 |
|
||||
| `sidebar:dirChanged` | 无 | 目录结构变化 |
|
||||
|
||||
## 6. 编辑器架构 — MetonaEditor v0.1.14
|
||||
|
||||
### 6.1 三模式视图
|
||||
|
||||
- **编辑模式 (edit)**:纯文本编辑器,显示 Markdown 源码
|
||||
- **分屏模式 (split)**:左侧编辑、右侧实时预览,同步滚动
|
||||
- **预览模式 (preview)**:仅显示渲染后的 HTML
|
||||
|
||||
MetonaEditor 内置模式切换工具栏,与应用层的 viewMode store 双向同步,切换时自动持久化到 IndexedDB。
|
||||
|
||||
### 6.2 内置功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 格式化工具栏 | bold / italic / strikethrough / underline / code / h1-h3 / quote / ul / ol / indent / outdent / link / image / table / hr |
|
||||
| 搜索替换 | Ctrl+F / Ctrl+H,支持正则、大小写敏感 |
|
||||
| 历史栈 | undo / redo,防抖合并,可配置上限 |
|
||||
| 主题 | light / dark / auto / warm,CSS 变量驱动 |
|
||||
| 国际化 | zh-CN / en-US 完整翻译 |
|
||||
| 全屏模式 | 编辑器全屏展示 |
|
||||
|
||||
### 6.3 插件体系
|
||||
|
||||
通过 `plugins` 配置数组安装 MetonaEditor 预设插件:
|
||||
|
||||
| 插件 | 说明 |
|
||||
|------|------|
|
||||
| searchReplace | Ctrl+F 查找、Ctrl+H 替换面板 |
|
||||
| imagePaste | Ctrl+V 粘贴剪贴板图片,自动转 base64 |
|
||||
|
||||
> 注:autoSave 插件仅支持 localStorage,而 MarkLite 需要文件系统保存(Electron IPC),因此使用自定义 useAutoSave hook。
|
||||
|
||||
### 6.4 渲染管线集成
|
||||
|
||||
通过 MetonaEditor 的 `render` 钩子接入 unified/rehype 管线,实现:
|
||||
|
||||
- **相对路径图片解析**:将相对路径转换为 `file://` 绝对路径
|
||||
- **XSS 防护**:rehype-sanitize 过滤危险标签
|
||||
- **代码高亮**:rehype-highlight 语法高亮
|
||||
- **处理器缓存**:LRU 缓存(最多 20 个),按文件路径分桶
|
||||
|
||||
```
|
||||
Markdown 源码
|
||||
│
|
||||
▼
|
||||
unified 管线(renderMarkdownSync)
|
||||
├── remark-parse 解析为 MDAST
|
||||
├── remark-gfm GFM 扩展
|
||||
├── remark-rehype 转换为 HAST
|
||||
├── rehype-raw 解析内联 HTML
|
||||
├── rehype-sanitize 安全过滤
|
||||
├── rehype-fixImages 相对路径 → file://
|
||||
├── rehype-highlight 代码高亮
|
||||
└── rehype-stringify 序列化为 HTML
|
||||
│
|
||||
▼
|
||||
MetonaEditor 预览区渲染
|
||||
```
|
||||
|
||||
### 6.5 主题切换
|
||||
|
||||
MetonaEditor 的 CSS 样式通过 wrapper 元素上的 inline `--md-*` CSS 变量驱动。主题切换流程:
|
||||
|
||||
1. `MeEditor.setTheme(dark/light)` — 更新 documentElement 全局变量 + localStorage
|
||||
2. 手动覆写 `.me-wrapper` 上的 inline CSS 变量(`style.setProperty`)
|
||||
3. 双向同步:应用工具栏暗色按钮 ⇄ 编辑器主题
|
||||
|
||||
### 6.6 内容同步
|
||||
|
||||
- **编辑 → 存储**:`onChange` 回调 → `updateTabContent` + `setModified`
|
||||
- **标签切换**:`setValue(content, { silent: true })` 静默更新,避免重复触发 onChange
|
||||
- **滚动持久化**:切换标签时通过 DOM 查询 `textarea` / `.me-preview` 保存/恢复滚动位置
|
||||
|
||||
## 7. Markdown 渲染管线
|
||||
|
||||
```
|
||||
Markdown 文本
|
||||
│
|
||||
▼
|
||||
remark-parse 解析为 MDAST
|
||||
│
|
||||
▼
|
||||
remark-gfm 扩展 GFM 语法
|
||||
│
|
||||
▼
|
||||
remark-rehype 转换为 HAST
|
||||
│
|
||||
▼
|
||||
rehype-raw 解析内联 HTML
|
||||
│
|
||||
▼
|
||||
rehype-sanitize 安全过滤
|
||||
│
|
||||
▼
|
||||
rehype-fixImages 相对路径图片转 file:// URL
|
||||
│
|
||||
▼
|
||||
rehype-highlight 代码语法高亮
|
||||
│
|
||||
▼
|
||||
rehype-stringify 序列化为 HTML
|
||||
│
|
||||
▼
|
||||
Renderer (MetonaEditor preview / Preview component)
|
||||
```
|
||||
|
||||
## 8. UI 设计
|
||||
|
||||
### 8.1 色彩方案
|
||||
|
||||
#### 亮色主题
|
||||
|
||||
| 角色 | CSS 变量 | 色值 |
|
||||
|------|----------|------|
|
||||
| 主色调 | `--primary` | `#1a73e8` |
|
||||
| 背景色 | `--bg` | `#ffffff` |
|
||||
| 次级背景 | `--bg-secondary` | `#f8f9fa` |
|
||||
| 文字色 | `--text` | `#333333` |
|
||||
| 边框色 | `--border` | `#e1e4e8` |
|
||||
|
||||
#### 暗色主题
|
||||
|
||||
| 角色 | CSS 变量 | 色值 |
|
||||
|------|----------|------|
|
||||
| 主色调 | `--primary` | `#8ab4f8` |
|
||||
| 背景色 | `--bg` | `#1e1e1e` |
|
||||
| 次级背景 | `--bg-secondary` | `#252526` |
|
||||
| 文字色 | `--text` | `#d4d4d4` |
|
||||
| 边框色 | `--border` | `#3e3e3e` |
|
||||
|
||||
### 8.2 布局
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ MarkLite - filename.md ─ □ ✕ │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ 📁 打开 │ 💾 保存 │ 自动 │ 🌙 🌐 ℹ️ │
|
||||
├──────────────────────────────────────────────────────────────────────────┤
|
||||
│ [file1.md] [file2.md] [未命名] [+] │
|
||||
├──────────┬───────────────────────────────────────────────────────────────┤
|
||||
│ 资源管理器 │ │
|
||||
│ ▼ project │ ┌─────────────────────────────────────────────┐ │
|
||||
│ 📁 src │ │ B I S U </> │ H1 H2 H3 │ " 1. 2. ≡ ⇥ ⇤ │ │ │
|
||||
│ 📄 file1│ │ 🔗 🖼 ⊞ — │ ↶ ↷ │ 📝 ⇔ 👁 ⊞ │ │ │
|
||||
│ 📄 file2│ ├─────────────────────────────────────────────┤ │
|
||||
│ │ │ # Title │ Title │ │
|
||||
│ 文档大纲 │ │ │ ─────── │ │
|
||||
│ · Title │ │ content... │ content... │ │
|
||||
├──────────┴──┴─────────────────────────────────────────────┴──────────────┤
|
||||
│ (MetonaEditor 底栏: 字数/行数/阅读时间) │
|
||||
└──────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 9. 构建与发布
|
||||
|
||||
### 9.1 开发模式
|
||||
|
||||
```bash
|
||||
npm run dev # electron-vite dev(HMR 热更新)
|
||||
```
|
||||
|
||||
### 9.2 生产构建
|
||||
|
||||
```bash
|
||||
npm run build # electron-vite build + electron-builder --win
|
||||
npm run build:portable # 便携版(免安装)
|
||||
```
|
||||
|
||||
### 9.3 打包配置
|
||||
|
||||
- 输出格式:NSIS 安装包(.exe)
|
||||
- 目标平台:Windows x64
|
||||
- 应用图标:assets/icon.ico
|
||||
- 文件关联:`.md` / `.markdown` / `.txt`
|
||||
- 支持自定义安装目录、桌面/开始菜单快捷方式
|
||||
|
||||
## 10. 依赖清单
|
||||
|
||||
### 运行时依赖
|
||||
|
||||
| 包名 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| react / react-dom | ^18.3 | UI 框架 |
|
||||
| zustand | ^5.0 | 状态管理 |
|
||||
| @metona-team/metona-sqlark | 0.4.1 | 前端关系型数据库(IndexedDB) |
|
||||
| nanoid | ^5.0 | 唯一 ID 生成 |
|
||||
| @metona-team/metona-editor | 0.4.0 | Markdown 编辑器(零依赖) |
|
||||
| @metona-team/metona-toast | 0.5.0 | Toast 通知组件 |
|
||||
| unified / remark / rehype | ^11.0 | Markdown 渲染管线 |
|
||||
| rehype-highlight | ^7.0 | 代码语法高亮 |
|
||||
|
||||
### 开发依赖
|
||||
|
||||
| 包名 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| electron | ^28.0 | 桌面框架 |
|
||||
| electron-builder | ^25.0 | 打包工具 |
|
||||
| electron-vite | ^3.0 | 构建工具 |
|
||||
| typescript | ^5.6 | 类型系统 |
|
||||
| eslint | ^9.0 | 代码检查 |
|
||||
|
||||
Reference in New Issue
Block a user