From ebd955ce4bca13141b2a5cd5f70fa9db7e976052 Mon Sep 17 00:00:00 2001 From: hunter-dev Date: Thu, 27 Aug 2026 15:48:19 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=A4=A7=E6=96=87=E6=A1=A3=E6=80=A7?= =?UTF-8?q?=E8=83=BD=E4=BC=98=E5=8C=96=E2=80=94=E2=80=941.getMarkdown=20?= =?UTF-8?q?=E9=98=B2=E6=8A=96=20300ms:=202000-5000=20=E8=A1=8C=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E6=AF=8F=E6=AC=A1=E6=8C=89=E9=94=AE=E5=BA=8F=E5=88=97?= =?UTF-8?q?=E5=8C=96=E5=85=A8=E9=87=8F=20markdown=20=E5=BC=80=E9=94=80?= =?UTF-8?q?=E5=A4=A7,=20=E6=94=B9=E4=B8=BA=20debounce=20300ms;=202.dsh-flu?= =?UTF-8?q?sh-body=20=E4=BA=8B=E4=BB=B6:=20Ctrl+S/=E5=85=B3=E7=AA=97/?= =?UTF-8?q?=E5=88=87=E6=96=87=E6=A1=A3=E5=89=8D=E5=90=8C=E6=AD=A5=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=86=85=E5=AE=B9=E5=88=B0=20store,=20?= =?UTF-8?q?=E7=A1=AE=E4=BF=9D=E4=BF=9D=E5=AD=98=E4=B8=8D=E4=B8=A2=E5=86=85?= =?UTF-8?q?=E5=AE=B9;=203.openDoc=20=E4=B8=8D=20await=20saveNow:=20?= =?UTF-8?q?=E6=97=A7=E6=96=87=E6=A1=A3=E4=BF=9D=E5=AD=98=E4=B8=8E=E6=96=B0?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E8=AF=BB=E5=8F=96=E5=B9=B6=E8=A1=8C,=20?= =?UTF-8?q?=E5=8A=A0=E5=BF=AB=E5=88=87=E6=8D=A2=E9=80=9F=E5=BA=A6;=204.?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=BF=9D=E5=AD=98=E9=97=B4=E9=9A=94=201s?= =?UTF-8?q?=E2=86=923s:=20=E5=A4=A7=E6=96=87=E6=A1=A3=E6=AF=8F=E7=A7=92?= =?UTF-8?q?=E5=86=99=E7=9B=98=20I/O=20=E5=8E=8B=E5=8A=9B=E5=A4=A7;=205.?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20save-mechanism=20E2E=20=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E6=97=B6=E5=BA=8F=E9=80=82=E9=85=8D=E6=96=B0=E9=98=B2=E6=8A=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/renderer/App.tsx | 4 ++-- .../renderer/features/editor/TiptapEditor.tsx | 21 ++++++++++++++++++- apps/desktop/src/renderer/store.ts | 7 ++++--- apps/desktop/tests/e2e/save-mechanism.spec.ts | 8 +++++-- 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/apps/desktop/src/renderer/App.tsx b/apps/desktop/src/renderer/App.tsx index 429c950..b6eae5c 100644 --- a/apps/desktop/src/renderer/App.tsx +++ b/apps/desktop/src/renderer/App.tsx @@ -49,7 +49,7 @@ export function App() { }).catch(() => {}); } }, [activeId]); // FR-S4:关窗前冲刷未保存内容 useEffect(() => { - const onBeforeUnload = () => { try { useStore.getState().saveNow(); } catch {} }; + const onBeforeUnload = () => { try { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); } catch {} }; window.addEventListener('beforeunload', onBeforeUnload); return () => window.removeEventListener('beforeunload', onBeforeUnload); }, []); @@ -62,7 +62,7 @@ export function App() { await useStore.getState().loadTree(); await useStore.getState().openDoc(path); }, - onSave: () => useStore.getState().saveNow(), + onSave: () => { window.dispatchEvent(new CustomEvent('dsh-flush-body')); useStore.getState().saveNow(); }, onFocusSearch: () => { const el = document.querySelector('input[placeholder*="搜索"]') as HTMLInputElement; el?.focus(); }, onToggleOutline: () => layout.toggleOutline(), onCheat: () => setShowCheat(true), diff --git a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx index 7f60c13..bac14a4 100644 --- a/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx +++ b/apps/desktop/src/renderer/features/editor/TiptapEditor.tsx @@ -44,6 +44,7 @@ export function TiptapEditor() { const setBody = useStore(s => s.setBody); const skipUpdate = useRef(false); const editorRef = useRef(null); + const mdTimer = useRef | null>(null); // editor 实例稳定,不随文档切换重建(避免销毁/重建过渡期 commands 为 null) const editor = useEditor({ @@ -92,7 +93,14 @@ export function TiptapEditor() { return false; }, }, - onUpdate: ({ editor }) => { if (!skipUpdate.current) setBody((editor.storage as Record).markdown.getMarkdown()); }, + onUpdate: ({ editor }) => { + if (skipUpdate.current) return; + // 大文档防抖——getMarkdown() 对 2000+ 行文档开销显著,不必每次按键都序列化 + if (mdTimer.current) clearTimeout(mdTimer.current); + mdTimer.current = setTimeout(() => { + setBody((editor.storage as Record).markdown.getMarkdown()); + }, 300); + }, }); // 切换文档时只同步内容,不重建 editor @@ -111,6 +119,17 @@ export function TiptapEditor() { skipUpdate.current = false; }, [currentPath, editor]); + // 监听保存前冲刷——Ctrl+S / 关窗前同步编辑器内容到 store + useEffect(() => { + if (!editor) return; + const onFlush = () => { + if (mdTimer.current) { clearTimeout(mdTimer.current); mdTimer.current = null; } + setBody((editor.storage as Record).markdown.getMarkdown()); + }; + window.addEventListener('dsh-flush-body', onFlush); + return () => window.removeEventListener('dsh-flush-body', onFlush); + }, [editor, setBody]); + if (!currentPath) { return (
diff --git a/apps/desktop/src/renderer/store.ts b/apps/desktop/src/renderer/store.ts index df0a18f..f51b7ba 100644 --- a/apps/desktop/src/renderer/store.ts +++ b/apps/desktop/src/renderer/store.ts @@ -66,8 +66,9 @@ export const useStore = create((set, get) => ({ }, openDoc: async (path: string) => { - // 切换前冲刷当前文档 - await get().saveNow(); + // 切换前冲刷当前文档——先同步编辑器内容到 store 再保存 + window.dispatchEvent(new CustomEvent('dsh-flush-body')); + void get().saveNow(); const doc: ReadDocResult = await invoke(IPC.docRead, { path }); set({ currentPath: path, body: doc.body, meta: doc.meta, title: doc.meta.title ?? path.split('/').pop()!.replace(/\.md$/, ''), saveState: 'saved', docMtime: doc.mtime }); // 同步到标签页(如已存在则激活,否则新建) @@ -103,7 +104,7 @@ export const useStore = create((set, get) => ({ set({ body, saveState: 'saving' }); // FR-S1:防抖自动保存——间隔读 settings.autoSaveInterval(秒→毫秒) if (saveTimer) clearTimeout(saveTimer); - const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 1) * 1000; } catch { return 1000; } })(); + const interval = (() => { try { const s = JSON.parse(localStorage.getItem('dsh-settings') ?? '{}'); return (s.autoSaveInterval ?? 3) * 1000; } catch { return 3000; } })(); saveTimer = setTimeout(() => { get().saveNow(); }, interval); }, diff --git a/apps/desktop/tests/e2e/save-mechanism.spec.ts b/apps/desktop/tests/e2e/save-mechanism.spec.ts index b913367..01de35f 100644 --- a/apps/desktop/tests/e2e/save-mechanism.spec.ts +++ b/apps/desktop/tests/e2e/save-mechanism.spec.ts @@ -62,7 +62,8 @@ test('FR-S4:关窗冲刷未保存内容', async () => { await a.page.keyboard.type('before-close-flush'); // 等一下让内容进入 store await a.page.waitForTimeout(200); - // 手动触发 saveNow(模拟 beforeunload 冲刷) + // 冲刷编辑器内容到 store 再保存(模拟 beforeunload 冲刷) + await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body'))); await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.()); await a.page.waitForTimeout(500); await a.app.close(); @@ -86,7 +87,10 @@ test('FR-S6:reloadDoc 检测外部改动', async () => { await a.page.locator('.ProseMirror').click(); await a.page.keyboard.type('original'); - await a.page.waitForTimeout(2000); // 等待自动保存 + await a.page.waitForTimeout(200); + await a.page.evaluate(() => window.dispatchEvent(new CustomEvent('dsh-flush-body'))); + await a.page.evaluate(() => (window as any).__STORE__?.getState?.().saveNow?.()); + await a.page.waitForTimeout(500); // 等待保存完成 // 外部修改文件 const filePath = nodePath.join(vault, '重载测试.md');