From 1ecf5ae7d39d557552671193944ede17ed2d250b Mon Sep 17 00:00:00 2001 From: thzxx <1440196015@qq.com> Date: Mon, 17 Aug 2026 23:26:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.4.0=20=E5=B7=AE=E5=BC=82=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=20worker=20=E5=8C=96=20-=20=E5=A4=A7=E6=96=87?= =?UTF-8?q?=E4=BB=B6=20diff=20=E7=A7=BB=E5=85=A5=20Worker=20=E5=90=8E?= =?UTF-8?q?=E5=8F=B0=E8=AE=A1=E7=AE=97=E4=B8=8D=E9=98=BB=E5=A1=9E=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E3=80=81=E8=BE=93=E5=85=A5=E5=8E=BB=E6=8A=96=E4=B8=8E?= =?UTF-8?q?=E8=BF=87=E6=9C=9F=E4=BB=BB=E5=8A=A1=E4=B8=A2=E5=BC=83=EF=BC=88?= =?UTF-8?q?=E7=BB=88=E6=AD=A2=E9=87=8D=E5=BB=BA=E5=AE=9E=E7=8E=B0=E5=8F=96?= =?UTF-8?q?=E6=B6=88=EF=BC=89=E3=80=81=E5=B0=8F=E8=BE=93=E5=85=A5=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E5=BF=AB=E8=B7=AF=E5=BE=84=E3=80=81=E6=97=A0=20Worker?= =?UTF-8?q?=20=E7=8E=AF=E5=A2=83=E5=9B=9E=E9=80=80=E3=80=81=E8=AE=A1?= =?UTF-8?q?=E7=AE=97=E4=B8=AD=E9=81=AE=E7=BD=A9=E4=B8=8E=E5=AF=BC=E5=87=BA?= =?UTF-8?q?=E7=A6=81=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 +- docs/应用开发与版本迭代规范.md | 7 +- docs/测试策略与方案.md | 20 +- package.json | 2 +- src/renderer/index.html | 2 +- src/renderer/src/App.tsx | 49 +++-- src/renderer/src/__tests__/App.test.tsx | 21 +++ src/renderer/src/components/Toolbar.test.tsx | 8 +- src/renderer/src/components/Toolbar.tsx | 10 +- .../src/diff/createDiffWorker.test.ts | 36 ++++ src/renderer/src/diff/createDiffWorker.ts | 17 ++ src/renderer/src/diff/diffWorker.ts | 27 +++ src/renderer/src/diff/textUtils.ts | 2 +- src/renderer/src/hooks/useDiff.test.ts | 174 ++++++++++++++++++ src/renderer/src/hooks/useDiff.ts | 77 ++++++++ src/renderer/src/styles/global.css | 37 ++++ vitest.config.ts | 8 +- 17 files changed, 472 insertions(+), 29 deletions(-) create mode 100644 src/renderer/src/diff/createDiffWorker.test.ts create mode 100644 src/renderer/src/diff/createDiffWorker.ts create mode 100644 src/renderer/src/diff/diffWorker.ts create mode 100644 src/renderer/src/hooks/useDiff.test.ts create mode 100644 src/renderer/src/hooks/useDiff.ts diff --git a/README.md b/README.md index 4811203..ceb4920 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ DiffLens 是一款跨平台桌面文本对比工具,帮助你快速定位两 - **双栏并排对比**:左(原文件)右(对比文件)对照,行号清晰对齐 - **行级 + 词级双重高亮**:整行增删一目了然,同一行内的细微改动也能被精确定位 - **虚拟滚动**:仅渲染可见行,数万行大文件滚动依然流畅 +- **后台差异计算**:大文件的差异计算在 Worker 线程后台进行(含去抖与过期任务丢弃),界面始终保持流畅响应 - **差异导航**:一键“上一处 / 下一处”在差异之间跳跃,支持 F7 / Shift+F7 快捷键 - **仅看差异**:一键过滤未更改行,连续相同内容折叠为提示行,专注浏览差异 - **交换左右侧**:一键互换两侧内容与视角 @@ -37,6 +38,7 @@ DiffLens 是一款跨平台桌面文本对比工具,帮助你快速定位两 | 构建 | electron-vite (Vite 5) | | 打包 | electron-builder | | Diff 引擎 | jsdiff(行级 LCS + 词级词法 diff) | +| 后台计算 | Web Worker(Vite 内联打包) | | 编码 | iconv-lite | --- @@ -93,7 +95,7 @@ src/ - 作者:thzxx - 组织:MetonaTeam - 许可证:MIT License(见 [LICENSE](./LICENSE)) -- 版本:0.3.2 +- 版本:0.4.0 --- diff --git a/docs/应用开发与版本迭代规范.md b/docs/应用开发与版本迭代规范.md index ba86198..20b3e18 100644 --- a/docs/应用开发与版本迭代规范.md +++ b/docs/应用开发与版本迭代规范.md @@ -20,7 +20,7 @@ z = 补丁版本号(Patch) > **`x` 永远是 `0`,永远不要提升到 `1.0.0`。** > 版本迭代**只允许修改 `y` 和 `z`**,`x` 保持 `0` 不变。 -当前基线版本:**`0.3.2`** +当前基线版本:**`0.4.0`** --- @@ -65,8 +65,9 @@ z = 补丁版本号(Patch) 0.2.6 ← 主进程异步收尾与边角防护(已发布):保存报告异步写盘 · 拖拽解码 handler 异步化 · 空白粘贴拦截 · 超多行卡顿预警 · 清理 mac 无效签名配置 · 文档与测试同步 0.3.0 ← 大文件性能与差异导航增强(已发布):自研虚拟滚动 · 仅看差异过滤(折叠提示行) · F7/Shift+F7 导航快捷键 · 交换左右侧 · 忽略空行 0.3.1 ← 稳健性与测试补强(已发布):超长单行词级 diff 防护 · 打开对话框补“所有文件”筛选 · 虚拟滚动 minWidth 测试覆盖 · 滚动同步去锁化重构 -0.3.2 ← 报告对齐与性能收尾(当前):纯文本报告行号右对齐 · 超长行列宽封顶防卡顿 · 对话框与拖拽扩展名清单对齐 · 右键菜单打开时导航快捷键守卫 · 清理导出死属性 -0.4.0 ← 新增功能(候选:主进程解码 worker 化 · 渲染进程 diff 计算 worker 化 · Playwright E2E · 文件夹对比) +0.3.2 ← 报告对齐与性能收尾(已发布):纯文本报告行号右对齐 · 超长行列宽封顶防卡顿 · 对话框与拖拽扩展名清单对齐 · 右键菜单打开时导航快捷键守卫 · 清理导出死属性 +0.4.0 ← 差异计算 worker 化(当前):大文件 diff 移入 Worker 后台计算不阻塞界面 · 输入去抖与过期任务丢弃(终止重建实现取消) · 小输入同步快路径 · 无 Worker 环境回退 · 计算中遮罩与导出禁用 +0.5.0 ← 新增功能(候选:主进程解码 worker 化 · Playwright E2E · 文件夹对比) ... 0.y.z ← 长期停留,永不进入 1.x ``` diff --git a/docs/测试策略与方案.md b/docs/测试策略与方案.md index c216b89..bd1f04a 100644 --- a/docs/测试策略与方案.md +++ b/docs/测试策略与方案.md @@ -47,15 +47,22 @@ src/renderer/src/ │ └── setup.ts # 全局 setup:jest-dom 匹配器 + 自动 cleanup ├── diff/ │ ├── diffEngine.test.ts # diff 引擎单元测试 +│ ├── diffWorker.ts # diff 计算 Web Worker(内联打包,覆盖率排除,逻辑由 diffEngine 覆盖) +│ ├── createDiffWorker.test.ts # worker 工厂(环境回退/实例化) +│ ├── createDiffWorker.ts # worker 工厂:环境不支持时返回 null 由调用方回退 │ ├── report.test.ts # 报告生成测试 │ └── textUtils.test.ts # 文本扩展名与行数统计测试 +├── hooks/ +│ ├── useDiff.test.ts # worker 化计算 hook(快路径/去抖/过期丢弃/回退/卸载清理) +│ ├── useDiff.ts +│ └── useDismiss.ts ├── components/ │ ├── ContextMenu.test.tsx # 右键菜单(含边缘防溢出) │ ├── DiffView.test.tsx # 对比视图(渲染/滚动同步/右键/拖放) │ ├── TextInputModal.test.tsx # 粘贴文本弹窗 -│ └── Toolbar.test.tsx # 工具栏(选项/导航/导出菜单) +│ └── Toolbar.test.tsx # 工具栏(选项/导航/导出菜单/计算中禁用) └── __tests__/ - ├── App.test.tsx # 应用集成级测试(加载/拖拽/粘贴/导出/容错) + ├── App.test.tsx # 应用集成级测试(加载/拖拽/粘贴/导出/容错/大输入回退) └── main.test.ts # 入口挂载冒烟测试 ``` @@ -98,6 +105,15 @@ src/renderer/src/ - 仅看差异视图:未更改行折叠、折叠提示出现、关闭后恢复 - F7 / Shift+F7 导航快捷键;粘贴弹窗打开时快捷键不抢占 - 交换左右侧后面板文件互换 +- 大输入(超快路径阈值)在无 Worker 环境回退同步计算,无计算中遮罩 + +### 5.6 diff 计算 worker 化(`useDiff.ts` / `createDiffWorker.ts`) +- 快路径:两侧总行数不超过 2000 时同步计算,不创建 worker,结果与 computeDiff 一致 +- 重路径:进入 computing 且 diff 置空;200ms 去抖后才派发 worker;响应按 jobId 匹配,错误/过期 jobId 被丢弃 +- 输入变更:旧 worker 终止重建(等价取消);去抖期内变更则旧任务未派发即取消(零浪费计算) +- worker 不可用(环境缺失/工厂返回 null):主线程同步回退 +- 卸载:终止 worker 并清理去抖定时器 +- 工厂:无 Worker 环境返回 null;可用环境返回实例 --- diff --git a/package.json b/package.json index ab3a362..6b81822 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "DiffLens", - "version": "0.3.2", + "version": "0.4.0", "description": "DiffLens — 精美酷炫的文本对比桌面应用", "author": "thzxx", "license": "MIT", diff --git a/src/renderer/index.html b/src/renderer/index.html index 557079f..f5ad805 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -6,7 +6,7 @@
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 2a0b714..eea4503 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -7,8 +7,7 @@ import { type ReactElement, type MouseEvent as ReactMouseEvent } from 'react' -import { computeDiff } from './diff/diffEngine' -import type { DiffOptions, DiffResult, DiffSummary } from './diff/diffEngine' +import type { DiffOptions, DiffSummary } from './diff/diffEngine' import DiffView, { type DisplayItem, type FoldMarker } from './components/DiffView' import Toolbar from './components/Toolbar' import ContextMenu, { type ContextMenuItem } from './components/ContextMenu' @@ -16,6 +15,7 @@ import TextInputModal from './components/TextInputModal' import { buildReport, REPORT_EXT, type ReportFormat, type ReportContext } from './diff/report' import { isTextFile, countLines, HEAVY_LINES } from './diff/textUtils' import { useDismiss } from './hooks/useDismiss' +import { useDiff } from './hooks/useDiff' import type { PaneMeta } from './components/DiffView' import type { SideCell } from './diff/diffEngine' @@ -31,6 +31,9 @@ function stamp(): string { /** 拖拽导入的文件大小上限(与主进程 file:open 的 10MB 限制保持一致) */ const MAX_FILE_SIZE = 10 * 1024 * 1024 +/** 计算中/无内容时的空统计(保持状态栏与徽章渲染稳定) */ +const EMPTY_SUMMARY: DiffSummary = { changedLines: 0, inserted: 0, deleted: 0, modified: 0 } + interface PaneState { meta: PaneMeta path: string @@ -125,11 +128,11 @@ export default function App(): ReactElement { } }, []) - // 超多行预警:行数超过阈值时提示计算耗时(虚拟化已解决渲染卡顿,剩余瓶颈在 diff 计算本身) + // 超多行预警:行数超过阈值时提示后台计算耗时(渲染已虚拟化、计算已 worker 化,均不阻塞界面) const warnHeavyLines = useCallback( (text: string): void => { const lines = countLines(text) - if (lines > HEAVY_LINES) showToast(`行数较多(${lines} 行),计算与首次加载可能耗时`) + if (lines > HEAVY_LINES) showToast(`行数较多(${lines} 行),差异将在后台计算,可能需要一些时间`) }, [showToast] ) @@ -205,28 +208,28 @@ export default function App(): ReactElement { [showToast, warnHeavyLines] ) - const diff: DiffResult = useMemo(() => { - return computeDiff(paneL?.text ?? '', paneR?.text ?? '', options) - }, [paneL, paneR, options]) + // diff 计算已 worker 化:大输入后台计算(computing 期间 diff 为 null),小输入同步快路径 + const { diff, computing } = useDiff(paneL?.text ?? '', paneR?.text ?? '', options) - const changedRows = useMemo(() => diff.rows.filter((r) => r.isChanged), [diff]) - const summary: DiffSummary = diff.summary + const rows = useMemo(() => diff?.rows ?? [], [diff]) + const changedRows = useMemo(() => rows.filter((r) => r.isChanged), [rows]) + const summary: DiffSummary = diff?.summary ?? EMPTY_SUMMARY // “仅看差异”视图行集:过滤未更改行,在连续省略处插入折叠提示行(占一个虚拟行位) const displayRows = useMemo(() => { - if (!onlyDiff) return diff.rows as DisplayItem[] - if (diff.rows.length === 0) return [] as DisplayItem[] + if (!onlyDiff) return rows as DisplayItem[] + if (rows.length === 0) return [] as DisplayItem[] if (changedRows.length === 0) { const m: FoldMarker = { fold: true, - skipped: diff.rows.length, + skipped: rows.length, note: '两文件内容完全一致,已折叠全部未更改行' } return [m] } const out: DisplayItem[] = [] let skipped = 0 - for (const r of diff.rows) { + for (const r of rows) { if (r.isChanged) { if (skipped > 0) { out.push({ fold: true, skipped }) @@ -239,7 +242,7 @@ export default function App(): ReactElement { } if (skipped > 0) out.push({ fold: true, skipped }) return out - }, [diff, onlyDiff, changedRows.length]) + }, [rows, onlyDiff, changedRows.length]) // 交换左右两侧(React 18 事件回调内自动批处理,两态一同更新) const swapPanes = useCallback((): void => { @@ -308,6 +311,8 @@ export default function App(): ReactElement { const exportReport = useCallback( async (fmt: ReportFormat) => { + // 计算中(diff 为 null)不导出:正常情况下导出按钮已禁用,此处兜底防御 + if (!diff) return const ctx: ReportContext = { leftName: paneL?.meta.name ?? '', rightName: paneR?.meta.name ?? '', @@ -316,7 +321,7 @@ export default function App(): ReactElement { leftPath: paneL?.path || undefined, rightPath: paneR?.path || undefined } - const content = buildReport(diff.rows, ctx, summary, fmt) + const content = buildReport(rows, ctx, summary, fmt) const defaultName = `DiffLens-report-${stamp()}.${REPORT_EXT[fmt]}` let res: { ok: boolean; path: string | null } try { @@ -333,7 +338,7 @@ export default function App(): ReactElement { }) } }, - [diff, paneL, paneR, summary, showToast] + [diff, rows, paneL, paneR, summary, showToast] ) const anyPane = paneL !== null || paneR !== null @@ -438,6 +443,7 @@ export default function App(): ReactElement { navCount={changedRows.length} onNav={go} onExport={(fmt) => void exportReport(fmt)} + computing={computing} onlyDiff={onlyDiff} onOnlyDiffChange={setOnlyDiff} onSwap={swapPanes} @@ -464,7 +470,9 @@ export default function App(): ReactElement {