Files
MarkLite/REFACTOR-PLAN.md
T

826 lines
27 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 重构方案:TypeScript + React + IndexedDB
## 一、重构目标与原则
| 维度 | 现状 | 目标 |
|------|------|------|
| 语言 | JavaScript (无类型) | **TypeScript 5.x** (全量类型) |
| UI 框架 | 原生 DOM 操作 | **React 18** (函数组件 + Hooks) |
| 状态管理 | 全局变量 + 闭包 | **Zustand** (轻量状态管理) |
| 持久化 | localStorage | **IndexedDB** (via Dexie.js) |
| 样式 | 纯 CSS | **CSS Modules** + CSS 变量 |
| 构建 | 无打包 | **Vite** + electron-vite |
| Markdown | marked.min.js (离线) | **unified/remark/rehype** 生态 |
| 代码高亮 | highlight.js (离线) | **Shiki****rehype-highlight** |
| 编辑器 | `<textarea>` | **CodeMirror 6** (专业编辑器) |
**核心原则**
1. **功能 100% 兼容** — 不丢失任何现有功能
2. **渐进式重构** — 主进程/预加载脚本改动最小,重点重构渲染进程
3. **类型安全** — 所有接口、IPC 通信、状态都有 TypeScript 类型
4. **可测试性** — 业务逻辑与 UI 解耦,便于单元测试
---
## 二、新项目结构
```
MarkLite/
├── package.json
├── tsconfig.json # TypeScript 配置
├── electron.vite.config.ts # electron-vite 构建配置
├── .eslintrc.cjs # ESLint + TS 规则
├── src/
│ ├── main/ # ===== 主进程 =====
│ │ ├── index.ts # 入口:窗口创建、app 生命周期
│ │ ├── ipc-handlers.ts # 所有 ipcMain.handle 注册
│ │ ├── file-system.ts # 文件读写、目录树构建
│ │ ├── file-watcher.ts # fs.watch 封装(单文件 + 目录)
│ │ ├── window-manager.ts # 窗口创建、关闭拦截、单实例锁
│ │ └── menu.ts # 原生菜单(可选)
│ │
│ ├── preload/ # ===== 预加载 =====
│ │ └── index.ts # contextBridge 类型安全暴露
│ │
│ ├── renderer/ # ===== 渲染进程 (React) =====
│ │ ├── index.html # 入口 HTML
│ │ ├── main.tsx # React 入口 (createRoot)
│ │ ├── App.tsx # 根组件:布局编排
│ │ │
│ │ ├── components/ # ===== UI 组件 =====
│ │ │ ├── Toolbar/
│ │ │ │ ├── Toolbar.tsx
│ │ │ │ └── Toolbar.module.css
│ │ │ ├── TabBar/
│ │ │ │ ├── TabBar.tsx
│ │ │ │ ├── TabItem.tsx
│ │ │ │ └── TabBar.module.css
│ │ │ ├── Editor/
│ │ │ │ ├── Editor.tsx # CodeMirror 6 封装
│ │ │ │ ├── EditorToolbar.tsx # 格式化工具栏(加粗/斜体/链接等)
│ │ │ │ ├── useCodeMirror.ts # CodeMirror 初始化 Hook
│ │ │ │ └── Editor.module.css
│ │ │ ├── Preview/
│ │ │ │ ├── Preview.tsx # Markdown 渲染面板
│ │ │ │ ├── MarkdownRenderer.tsx # rehype/remark 处理
│ │ │ │ ├── useScrollSync.ts # 滚动同步 Hook
│ │ │ │ └── Preview.module.css
│ │ │ ├── Sidebar/
│ │ │ │ ├── Sidebar.tsx
│ │ │ │ ├── FileTree.tsx # 文件树递归组件
│ │ │ │ ├── FileTreeItem.tsx
│ │ │ │ ├── IndependentFiles.tsx
│ │ │ │ └── Sidebar.module.css
│ │ │ ├── SearchBar/
│ │ │ │ ├── SearchBar.tsx
│ │ │ │ └── SearchBar.module.css
│ │ │ ├── StatusBar/
│ │ │ │ ├── StatusBar.tsx
│ │ │ │ └── StatusBar.module.css
│ │ │ ├── WelcomeScreen/
│ │ │ │ ├── WelcomeScreen.tsx
│ │ │ │ └── WelcomeScreen.module.css
│ │ │ ├── ModifiedBanner/
│ │ │ │ └── ModifiedBanner.tsx
│ │ │ ├── DropOverlay/
│ │ │ │ └── DropOverlay.tsx
│ │ │ └── Toast/
│ │ │ ├── Toast.tsx
│ │ │ └── Toast.module.css
│ │ │
│ │ ├── stores/ # ===== 状态管理 (Zustand) =====
│ │ │ ├── tabStore.ts # 标签页状态
│ │ │ ├── editorStore.ts # 编辑器状态(视图模式、暗色主题等)
│ │ │ ├── sidebarStore.ts # 侧边栏状态
│ │ │ └── searchStore.ts # 搜索替换状态
│ │ │
│ │ ├── hooks/ # ===== 自定义 Hooks =====
│ │ │ ├── useTheme.ts # 暗色/亮色主题
│ │ │ ├── useKeyboard.ts # 全局快捷键
│ │ │ ├── useDragDrop.ts # 拖拽打开
│ │ │ ├── useFileWatch.ts # 外部修改监听
│ │ │ ├── useUnsavedWarning.ts # 未保存提醒
│ │ │ └── useSettings.ts # 用户设置读写
│ │ │
│ │ ├── lib/ # ===== 工具库 =====
│ │ │ ├── markdown.ts # Markdown 渲染管线(remark + rehype
│ │ │ ├── sanitize.ts # HTML 安全过滤
│ │ │ ├── scrollSync.ts # 滚动同步算法(行号→DOM 映射)
│ │ │ ├── searchEngine.ts # 搜索匹配引擎(普通/正则/大小写)
│ │ │ ├── fileUtils.ts # 文件大小格式化、扩展名检查等
│ │ │ └── constants.ts # 常量定义(文件大小限制、扩展名列表等)
│ │ │
│ │ ├── db/ # ===== IndexedDB 层 =====
│ │ │ ├── schema.ts # Dexie 数据库定义 + 类型
│ │ │ ├── tabRepository.ts # 标签页状态 CRUD
│ │ │ ├── settingsRepository.ts # 用户设置 CRUD
│ │ │ └── recentFilesRepository.ts # 最近打开文件
│ │ │
│ │ ├── types/ # ===== 全局类型 =====
│ │ │ ├── tab.ts # Tab 接口
│ │ │ ├── file.ts # FileNode, DirTree 等
│ │ │ ├── settings.ts # Settings 接口
│ │ │ ├── search.ts # SearchMatch, SearchOptions 等
│ │ │ └── ipc.ts # IPC 通道类型映射
│ │ │
│ │ └── styles/ # ===== 全局样式 =====
│ │ ├── variables.css # CSS 变量(亮色/暗色)
│ │ ├── global.css # 全局 reset + 通用样式
│ │ ├── markdown-body.css # Markdown 预览样式
│ │ └── code-theme.css # 代码高亮主题
│ │
│ └── shared/ # ===== 主进程/渲染进程共享 =====
│ ├── ipc-channels.ts # IPC 通道名常量
│ └── types.ts # 共享类型定义
├── assets/
│ └── icon.ico
├── DESIGN.md
├── DEVSETUP.md
├── README.md
├── LICENSE
└── REFACTOR-PLAN.md # 本文件
```
---
## 三、技术选型详解
### 1. TypeScript — 类型安全
```typescript
// src/renderer/types/tab.ts
export interface Tab {
id: string; // nanoid 生成
filePath: string | null;
content: string;
isModified: boolean;
scrollTop: number;
scrollLeft: number;
selectionStart: number;
selectionEnd: number;
previewScrollTop: number;
}
// src/renderer/types/ipc.ts — IPC 通道类型映射
export interface IpcInvokeMap {
'dialog:openFile': [void, OpenFileResult | null];
'file:read': [string, ReadFileResult];
'file:save': [SaveFilePayload, SaveFileResult];
'file:saveAs': [SaveAsPayload, SaveFileResult];
'file:reload': [void, ReloadFileResult];
'dir:readTree': [string, ReadDirTreeResult];
'dir:openDialog': [void, string | null];
'dir:watch': [string, void];
'tab:switched': [string | null, void];
'window:forceClose': [void, void];
}
// 类型安全的 IPC 调用封装
export function invoke<K extends keyof IpcInvokeMap>(
channel: K,
...args: IpcInvokeMap[K][0] extends void ? [] : [IpcInvokeMap[K][0]]
): Promise<IpcInvokeMap[K][1]> {
return window.electronAPI.invoke(channel, ...args);
}
```
### 2. Zustand — 状态管理
```typescript
// src/renderer/stores/tabStore.ts
import { create } from 'zustand';
import { nanoid } from 'nanoid';
import type { Tab } from '../types/tab';
interface TabState {
tabs: Tab[];
activeTabId: string | null;
mruStack: string[];
// Actions
createTab: (filePath?: string | null, content?: string) => Tab;
closeTab: (tabId: string) => void;
switchToTab: (tabId: string) => void;
updateTabContent: (tabId: string, content: string) => void;
setModified: (tabId: string, modified: boolean) => void;
getActiveTab: () => Tab | null;
}
export const useTabStore = create<TabState>((set, get) => ({
tabs: [],
activeTabId: null,
mruStack: [],
createTab: (filePath = null, content = '') => {
// 检查是否已打开
if (filePath) {
const existing = get().tabs.find(t => t.filePath === filePath);
if (existing) {
get().switchToTab(existing.id);
return existing;
}
}
const tab: Tab = {
id: nanoid(),
filePath,
content,
isModified: false,
scrollTop: 0,
scrollLeft: 0,
selectionStart: 0,
selectionEnd: 0,
previewScrollTop: 0,
};
set(state => ({
tabs: [...state.tabs, tab],
activeTabId: tab.id,
}));
return tab;
},
closeTab: (tabId) => {
set(state => {
const index = state.tabs.findIndex(t => t.id === tabId);
if (index === -1) return state;
const newTabs = state.tabs.filter(t => t.id !== tabId);
const newMru = state.mruStack.filter(id => id !== tabId);
let newActiveId = state.activeTabId;
if (state.activeTabId === tabId) {
if (newTabs.length === 0) {
newActiveId = null;
} else {
const newIndex = Math.min(index, newTabs.length - 1);
newActiveId = newTabs[newIndex].id;
}
}
return {
tabs: newTabs,
activeTabId: newActiveId,
mruStack: newMru,
};
});
},
switchToTab: (tabId) => {
set(state => {
if (state.activeTabId === tabId) return state;
return {
activeTabId: tabId,
mruStack: [
state.activeTabId!,
...state.mruStack.filter(id => id !== state.activeTabId),
],
};
});
},
updateTabContent: (tabId, content) => {
set(state => ({
tabs: state.tabs.map(t =>
t.id === tabId ? { ...t, content, isModified: true } : t
),
}));
},
setModified: (tabId, modified) => {
set(state => ({
tabs: state.tabs.map(t =>
t.id === tabId ? { ...t, isModified: modified } : t
),
}));
},
getActiveTab: () => {
const { tabs, activeTabId } = get();
return tabs.find(t => t.id === activeTabId) ?? null;
},
}));
```
### 3. IndexedDB (Dexie.js) — 持久化
```typescript
// src/renderer/db/schema.ts
import Dexie, { type EntityTable } from 'dexie';
interface TabSnapshot {
id: string; // 与 Tab.id 对应
filePath: string | null;
content: string;
scrollTop: number;
scrollLeft: number;
selectionStart: number;
selectionEnd: number;
previewScrollTop: number;
isModified: boolean;
updatedAt: number; // 时间戳
}
interface Settings {
id: string; // 固定为 'default'
darkMode: boolean;
viewMode: 'split' | 'editor' | 'preview';
splitRatio: number;
sidebarCollapsed: boolean;
sidebarWidth: number;
}
interface RecentFile {
id?: number; // 自增主键
filePath: string;
lastOpened: number;
}
const db = new Dexie('MarkLite') as Dexie & {
tabSnapshots: EntityTable<TabSnapshot, 'id'>;
settings: EntityTable<Settings, 'id'>;
recentFiles: EntityTable<RecentFile, 'id'>;
};
db.version(1).stores({
tabSnapshots: 'id, filePath, updatedAt',
settings: 'id',
recentFiles: '++id, filePath, lastOpened',
});
export { db };
export type { TabSnapshot, Settings, RecentFile };
```
```typescript
// src/renderer/db/tabRepository.ts
import { db, type TabSnapshot } from './schema';
export const tabRepository = {
/** 保存所有标签快照(关闭时/定期) */
async saveAll(tabs: TabSnapshot[]): Promise<void> {
await db.transaction('rw', db.tabSnapshots, async () => {
await db.tabSnapshots.clear();
await db.tabSnapshots.bulkAdd(tabs);
});
},
/** 加载上次的标签快照 */
async loadAll(): Promise<TabSnapshot[]> {
return db.tabSnapshots.orderBy('updatedAt').toArray();
},
/** 清除所有快照(用户主动清除时) */
async clearAll(): Promise<void> {
await db.tabSnapshots.clear();
},
};
```
### 4. CodeMirror 6 — 编辑器
```typescript
// src/renderer/components/Editor/useCodeMirror.ts
import { useEffect, useRef, useCallback } from 'react';
import { EditorState, type Extension } from '@codemirror/state';
import { EditorView, keymap, lineNumbers, highlightActiveLine } from '@codemirror/view';
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
import { syntaxHighlighting, defaultHighlightStyle } from '@codemirror/language';
import { oneDark } from '@codemirror/theme-one-dark';
interface UseCodeMirrorOptions {
content: string;
onChange: (value: string) => void;
darkMode: boolean;
}
export function useCodeMirror({ content, onChange, darkMode }: UseCodeMirrorOptions) {
const containerRef = useRef<HTMLDivElement>(null);
const viewRef = useRef<EditorView | null>(null);
// 初始化
useEffect(() => {
if (!containerRef.current) return;
const extensions: Extension[] = [
lineNumbers(),
highlightActiveLine(),
history(),
keymap.of([...defaultKeymap, ...historyKeymap]),
markdown({ base: markdownLanguage }),
syntaxHighlighting(defaultHighlightStyle),
EditorView.updateListener.of(update => {
if (update.docChanged) {
onChange(update.state.doc.toString());
}
}),
EditorView.lineWrapping,
];
if (darkMode) extensions.push(oneDark);
const state = EditorState.create({
doc: content,
extensions,
});
const view = new EditorView({
state,
parent: containerRef.current,
});
viewRef.current = view;
return () => {
view.destroy();
viewRef.current = null;
};
}, [darkMode]); // darkMode 变化时重建
// 外部内容更新(切换标签时)
const setContent = useCallback((newContent: string) => {
const view = viewRef.current;
if (!view) return;
const current = view.state.doc.toString();
if (current !== newContent) {
view.dispatch({
changes: { from: 0, to: current.length, insert: newContent },
});
}
}, []);
return { containerRef, viewRef, setContent };
}
```
### 5. Markdown 渲染管线
```typescript
// src/renderer/lib/markdown.ts
import { unified } from 'unified';
import remarkParse from 'remark-parse';
import remarkGfm from 'remark-gfm';
import remarkRehype from 'remark-rehype';
import rehypeRaw from 'rehype-raw';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
import rehypeStringify from 'rehype-stringify';
import rehypeHighlight from 'rehype-highlight';
const processor = unified()
.use(remarkParse)
.use(remarkGfm)
.use(remarkRehype, { allowDangerousHtml: true })
.use(rehypeRaw)
.use(rehypeSanitize, {
...defaultSchema,
attributes: {
...defaultSchema.attributes,
img: [...(defaultSchema.attributes?.img ?? []), ['src']],
},
})
.use(rehypeHighlight)
.use(rehypeStringify);
export async function renderMarkdown(content: string): Promise<string> {
const result = await processor.process(content);
return String(result);
}
```
---
## 四、IndexedDB 替代 localStorage 的数据模型
| 原 localStorage Key | IndexedDB Store | 字段 | 说明 |
|---------------------|-----------------|------|------|
| `marklite-settings` | `settings` | darkMode, viewMode, splitRatio, sidebarCollapsed, sidebarWidth | 用户偏好 |
| *(无,标签状态丢失)* | `tabSnapshots` | id, filePath, content, scrollTop, ... | **新增**:标签页状态持久化 |
| *(无)* | `recentFiles` | filePath, lastOpened | **新增**:最近打开文件列表 |
**IndexedDB 优势**
- 存储容量大(localStorage 约 5-10MBIndexedDB 可达数百 MB
- 支持大文件内容缓存(当前 20MB 限制的文件也能存)
- 支持索引查询(按 filePath、updatedAt 查询)
- 异步 API,不阻塞 UI
- 标签页状态可持久化(关闭窗口后恢复所有标签)
---
## 五、状态管理架构
```
┌─────────────────────────────────────────────────────────┐
│ 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 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 六、主进程重构(改动最小)
主进程保持 Electron 原生,仅将 JS → TS,逻辑不变:
```typescript
// src/main/file-system.ts — 类型安全的文件操作
import { readFile, writeFile, stat, readdir } from 'fs/promises';
import { join, extname } from 'path';
import type { FileNode, ReadFileResult, SaveFileResult } from '../shared/types';
const ALLOWED_EXTENSIONS = ['.md', '.markdown', '.txt'] as const;
const MAX_FILE_SIZE = 20 * 1024 * 1024; // 20MB
export async function readFileContent(filePath: string): Promise<ReadFileResult> {
const stats = await stat(filePath);
if (stats.size > MAX_FILE_SIZE) {
return { success: false, error: `文件过大 (${formatBytes(stats.size)})` };
}
let content = await readFile(filePath, 'utf-8');
// 去除 BOM
if (content.charCodeAt(0) === 0xFEFF) content = content.slice(1);
return { success: true, content };
}
export async function buildDirTree(dirPath: string): Promise<FileNode[]> {
const entries = await readdir(dirPath, { withFileTypes: true });
const result: FileNode[] = [];
for (const entry of entries) {
if (shouldSkip(entry.name)) continue;
const fullPath = join(dirPath, entry.name);
if (entry.isDirectory()) {
result.push({
name: entry.name,
path: fullPath,
type: 'dir',
children: await buildDirTree(fullPath),
});
} else if (ALLOWED_EXTENSIONS.includes(extname(entry.name).toLowerCase() as any)) {
result.push({ name: entry.name, path: fullPath, type: 'file' });
}
}
return result.sort((a, b) => {
if (a.type !== b.type) return a.type === 'dir' ? -1 : 1;
return a.name.localeCompare(b.name);
});
}
```
---
## 七、滚动同步算法(保留现有逻辑)
现有基于**块级元素 DOM 位置映射**的算法非常优秀,直接用 TypeScript 重写:
```typescript
// src/renderer/lib/scrollSync.ts
interface ScrollMap {
lineToPreviewMap: number[];
}
export function buildScrollMap(
markdown: string,
previewContainer: HTMLElement
): ScrollMap {
const lines = markdown.split('\n');
const totalLines = lines.length;
const lineToPreviewMap = new Array(totalLines).fill(0);
// 1. 识别块级元素起始行
const blockStarts = identifyBlockStarts(lines);
// 2. 获取预览 DOM 元素位置
const previewPositions = getPreviewPositions(previewContainer);
// 3. 块起始行 → 预览位置映射
// 4. 线性插值填充中间行
interpolateBlockPositions(blockStarts, previewPositions, lineToPreviewMap, totalLines);
return { lineToPreviewMap };
}
function identifyBlockStarts(lines: string[]): Set<number> {
const starts = new Set<number>();
let inCodeBlock = false;
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line.match(/^```/)) {
if (!inCodeBlock) starts.add(i);
inCodeBlock = !inCodeBlock;
continue;
}
if (inCodeBlock) continue;
if (line.match(/^#{1,6}\s/) || line.match(/^[-*+]\s/) || line.match(/^\d+\.\s/) ||
line.match(/^>/) || line.match(/^\|/) || line.match(/^---/) || line === '') {
starts.add(i);
}
}
return starts;
}
```
---
## 八、CSS Modules + CSS 变量
```css
/* src/renderer/styles/variables.css */
:root {
/* 亮色主题 */
--color-primary: #1a73e8;
--color-primary-dark: #1557b0;
--color-primary-light: rgba(26, 115, 232, 0.1);
--color-bg: #ffffff;
--color-bg-secondary: #f8f9fa;
--color-bg-tertiary: #f1f3f4;
--color-text: #333333;
--color-text-secondary: #5f6368;
--color-text-tertiary: #80868b;
--color-border: #e1e4e8;
--color-border-light: #f0f0f0;
--color-code-bg: #f6f8fa;
/* 布局 */
--toolbar-height: 36px;
--tabbar-height: 34px;
--statusbar-height: 24px;
--sidebar-width: 220px;
/* 字体 */
--font-ui: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: "Cascadia Code", "Fira Code", "JetBrains Mono", Consolas, monospace;
--font-size: 14px;
/* 圆角/阴影 */
--radius: 6px;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-lg: 0 4px 12px rgba(0,0,0,0.15);
}
:root.dark {
--color-primary: #8ab4f8;
--color-primary-dark: #669df6;
--color-primary-light: rgba(138, 180, 248, 0.15);
--color-bg: #1e1e1e;
--color-bg-secondary: #252526;
--color-bg-tertiary: #2d2d2d;
--color-text: #d4d4d4;
--color-text-secondary: #9e9e9e;
--color-text-tertiary: #6e6e6e;
--color-border: #3e3e3e;
--color-border-light: #333333;
--color-code-bg: #2d2d2d;
}
```
---
## 九、构建配置
```typescript
// electron.vite.config.ts
import { defineConfig, externalizeDepsPlugin } from 'electron-vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
main: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist/main',
rollupOptions: {
input: { index: 'src/main/index.ts' },
},
},
},
preload: {
plugins: [externalizeDepsPlugin()],
build: {
outDir: 'dist/preload',
rollupOptions: {
input: { index: 'src/preload/index.ts' },
},
},
},
renderer: {
plugins: [react()],
root: 'src/renderer',
build: {
outDir: 'dist/renderer',
rollupOptions: {
input: { index: 'src/renderer/index.html' },
},
},
},
});
```
```json
// package.json (关键 scripts)
{
"scripts": {
"dev": "electron-vite dev",
"build": "electron-vite build && electron-builder",
"build:portable": "electron-vite build && electron-builder --win portable",
"lint": "eslint src --ext .ts,.tsx",
"typecheck": "tsc --noEmit"
}
}
```
---
## 十、依赖清单
```json
{
"dependencies": {
"react": "^18.3",
"react-dom": "^18.3",
"zustand": "^5.0",
"dexie": "^4.0",
"nanoid": "^5.0",
"@codemirror/view": "^6.35",
"@codemirror/state": "^6.5",
"@codemirror/lang-markdown": "^6.3",
"@codemirror/theme-one-dark": "^6.1",
"@codemirror/commands": "^6.7",
"@codemirror/language": "^6.10",
"unified": "^11.0",
"remark-parse": "^11.0",
"remark-gfm": "^4.0",
"remark-rehype": "^11.1",
"rehype-raw": "^7.0",
"rehype-sanitize": "^6.0",
"rehype-stringify": "^10.0",
"rehype-highlight": "^7.0"
},
"devDependencies": {
"electron": "^28.0",
"electron-builder": "^25.0",
"electron-vite": "^3.0",
"@vitejs/plugin-react": "^4.3",
"typescript": "^5.6",
"@types/react": "^18.3",
"@types/react-dom": "^18.3",
"eslint": "^9.0",
"@typescript-eslint/eslint-plugin": "^8.0"
}
}
```
---
## 十一、分阶段实施计划
| 阶段 | 内容 | 预估工时 |
|------|------|----------|
| **Phase 0** | 脚手架搭建:electron-vite + React + TypeScript + ESLint | 0.5 天 |
| **Phase 1** | 主进程 TypeScript 化(文件系统、IPC、窗口管理) | 1 天 |
| **Phase 2** | IndexedDB 层 + Zustand stores + 设置持久化 | 1 天 |
| **Phase 3** | 基础 UIApp 布局 + Toolbar + TabBar + StatusBar | 1 天 |
| **Phase 4** | CodeMirror 6 编辑器集成 + 暗色主题 | 1 天 |
| **Phase 5** | Markdown 预览(unified/rehype 管线)+ 滚动同步 | 1.5 天 |
| **Phase 6** | 侧边栏文件树 + 独立文件区 | 1 天 |
| **Phase 7** | 搜索替换(引擎 + 高亮 + 快捷键) | 1 天 |
| **Phase 8** | 拖拽打开 + 文件监听 + 未保存提醒 + Toast | 0.5 天 |
| **Phase 9** | 测试 + 修复 + 打包验证 | 1 天 |
| **总计** | | **约 9.5 天** |
---
## 十二、关键决策说明
| 决策 | 理由 |
|------|------|
| **CodeMirror 6 替代 textarea** | 原生 undo/redo、语法高亮、行号内置、大文件性能优、扩展性强 |
| **Zustand 替代全局变量** | React 原生状态管理,比 Redux 轻量 10 倍,支持 selector 优化 |
| **Dexie.js 替代 localStorage** | 异步 API、支持索引、容量大、支持复杂查询 |
| **unified/rehype 替代 marked** | 插件化架构、GFM 原生支持、安全过滤内置、社区活跃 |
| **electron-vite 替代手动配置** | 开箱即用的 Electron + Vite 集成,HMR 热更新 |
| **CSS Modules 替代纯 CSS** | 样式隔离、TypeScript 类型提示、避免类名冲突 |
| **主进程保持 Electron 原生** | 主进程改动最小化,降低重构风险 |