fix: v0.4.1 worker 创建开销优化与文档对齐 - worker 延迟至去抖到期才创建(去抖期内取消零创建开销、无 Worker 回退同样走去抖)、质量门禁文档补 npm test、README 架构树补 hooks、Markdown 报告标题移出引用块、安装包排除开发期文件
This commit is contained in:
@@ -12,7 +12,7 @@ DiffLens 是一款跨平台桌面文本对比工具,帮助你快速定位两
|
||||
- **双栏并排对比**:左(原文件)右(对比文件)对照,行号清晰对齐
|
||||
- **行级 + 词级双重高亮**:整行增删一目了然,同一行内的细微改动也能被精确定位
|
||||
- **虚拟滚动**:仅渲染可见行,数万行大文件滚动依然流畅
|
||||
- **后台差异计算**:大文件的差异计算在 Worker 线程后台进行(含去抖与过期任务丢弃),界面始终保持流畅响应
|
||||
- **后台差异计算**:大文件的差异计算在 Worker 线程后台进行(去抖合并输入、worker 按需创建、过期任务丢弃),界面始终保持流畅响应
|
||||
- **差异导航**:一键“上一处 / 下一处”在差异之间跳跃,支持 F7 / Shift+F7 快捷键
|
||||
- **仅看差异**:一键过滤未更改行,连续相同内容折叠为提示行,专注浏览差异
|
||||
- **交换左右侧**:一键互换两侧内容与视角
|
||||
@@ -81,7 +81,8 @@ src/
|
||||
├── main/ # 主进程:窗口、菜单、文件读取与编码探测、IPC
|
||||
├── preload/ # 预加载:contextBridge 安全桥,仅暴露最小 API
|
||||
└── renderer/ # 渲染进程:React 界面与全部 diff 逻辑
|
||||
├── diff/ # diff 引擎(行级 + 词级高亮)
|
||||
├── diff/ # diff 引擎(行级 + 词级高亮、Worker 后台计算)
|
||||
├── hooks/ # 状态 hooks(diff 计算调度、下拉关闭)
|
||||
├── components/ # 界面组件(DiffView / Toolbar 等)
|
||||
└── styles/ # 全局主题样式
|
||||
```
|
||||
@@ -95,7 +96,7 @@ src/
|
||||
- 作者:thzxx
|
||||
- 组织:MetonaTeam
|
||||
- 许可证:MIT License(见 [LICENSE](./LICENSE))
|
||||
- 版本:0.4.0
|
||||
- 版本:0.4.1
|
||||
|
||||
---
|
||||
|
||||
|
||||
+5
-3
@@ -20,7 +20,7 @@ z = 补丁版本号(Patch)
|
||||
> **`x` 永远是 `0`,永远不要提升到 `1.0.0`。**
|
||||
> 版本迭代**只允许修改 `y` 和 `z`**,`x` 保持 `0` 不变。
|
||||
|
||||
当前基线版本:**`0.4.0`**
|
||||
当前基线版本:**`0.4.1`**
|
||||
|
||||
---
|
||||
|
||||
@@ -66,7 +66,8 @@ z = 补丁版本号(Patch)
|
||||
0.3.0 ← 大文件性能与差异导航增强(已发布):自研虚拟滚动 · 仅看差异过滤(折叠提示行) · F7/Shift+F7 导航快捷键 · 交换左右侧 · 忽略空行
|
||||
0.3.1 ← 稳健性与测试补强(已发布):超长单行词级 diff 防护 · 打开对话框补“所有文件”筛选 · 虚拟滚动 minWidth 测试覆盖 · 滚动同步去锁化重构
|
||||
0.3.2 ← 报告对齐与性能收尾(已发布):纯文本报告行号右对齐 · 超长行列宽封顶防卡顿 · 对话框与拖拽扩展名清单对齐 · 右键菜单打开时导航快捷键守卫 · 清理导出死属性
|
||||
0.4.0 ← 差异计算 worker 化(当前):大文件 diff 移入 Worker 后台计算不阻塞界面 · 输入去抖与过期任务丢弃(终止重建实现取消) · 小输入同步快路径 · 无 Worker 环境回退 · 计算中遮罩与导出禁用
|
||||
0.4.0 ← 差异计算 worker 化(已发布):大文件 diff 移入 Worker 后台计算不阻塞界面 · 输入去抖与过期任务丢弃(终止重建实现取消) · 小输入同步快路径 · 无 Worker 环境回退 · 计算中遮罩与导出禁用
|
||||
0.4.1 ← worker 创建开销优化与文档对齐(当前):worker 延迟至去抖到期才创建(去抖期内取消零创建开销,无 Worker 回退同样走去抖) · 质量门禁文档补 npm test · README 架构树补 hooks · Markdown 报告标题移出引用块 · 安装包排除开发期文件
|
||||
0.5.0 ← 新增功能(候选:主进程解码 worker 化 · Playwright E2E · 文件夹对比)
|
||||
...
|
||||
0.y.z ← 长期停留,永不进入 1.x
|
||||
@@ -126,10 +127,11 @@ main
|
||||
|
||||
```bash
|
||||
npm run typecheck # 类型检查(node + web 双端)
|
||||
npm test # Vitest 单元 + 组件测试(覆盖率 80% 硬门禁)
|
||||
npm run build # 生产构建(main/preload/renderer 三端)
|
||||
```
|
||||
|
||||
> 任一命令失败则禁止提交;修复通过后再提交。
|
||||
> 任一命令失败则禁止提交;修复通过后再提交。与《测试策略与方案》第 7 节保持一致。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+5
-5
@@ -105,14 +105,14 @@ src/renderer/src/
|
||||
- 仅看差异视图:未更改行折叠、折叠提示出现、关闭后恢复
|
||||
- F7 / Shift+F7 导航快捷键;粘贴弹窗打开时快捷键不抢占
|
||||
- 交换左右侧后面板文件互换
|
||||
- 大输入(超快路径阈值)在无 Worker 环境回退同步计算,无计算中遮罩
|
||||
- 大输入(超快路径阈值)在无 Worker 环境去抖到期后回退同步计算,完成后无计算中遮罩
|
||||
|
||||
### 5.6 diff 计算 worker 化(`useDiff.ts` / `createDiffWorker.ts`)
|
||||
- 快路径:两侧总行数不超过 2000 时同步计算,不创建 worker,结果与 computeDiff 一致
|
||||
- 重路径:进入 computing 且 diff 置空;200ms 去抖后才派发 worker;响应按 jobId 匹配,错误/过期 jobId 被丢弃
|
||||
- 输入变更:旧 worker 终止重建(等价取消);去抖期内变更则旧任务未派发即取消(零浪费计算)
|
||||
- worker 不可用(环境缺失/工厂返回 null):主线程同步回退
|
||||
- 卸载:终止 worker 并清理去抖定时器
|
||||
- 重路径:进入 computing 且 diff 置空;200ms 去抖到期后才创建 worker 并派发;响应按 jobId 匹配,错误/过期 jobId 被丢弃
|
||||
- 输入变更:已派发的旧 worker 终止重建(等价取消);去抖期内变更则旧任务未创建即取消(零创建与计算开销)
|
||||
- worker 不可用(环境缺失/工厂返回 null):去抖到期后主线程同步回退
|
||||
- 卸载:去抖期内卸载则 worker 从未创建;去抖到期后卸载则终止 worker,去抖定时器均被清理
|
||||
- 工厂:无 Worker 环境返回 null;可用环境返回实例
|
||||
|
||||
---
|
||||
|
||||
@@ -13,9 +13,13 @@ files:
|
||||
- '!**/.vscode/*'
|
||||
- '!src/*'
|
||||
- '!electron.vite.config.{js,ts,mjs,cjs}'
|
||||
- '!vitest.config.{js,ts,mjs,cjs}'
|
||||
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
|
||||
- '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
|
||||
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
|
||||
# 开发期专用文件不进入安装包(assets 仅为打包期图标源,运行时图标在 resources/)
|
||||
- '!docs/*'
|
||||
- '!assets/*'
|
||||
|
||||
asarUnpack:
|
||||
- resources/**
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "DiffLens",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "DiffLens — 精美酷炫的文本对比桌面应用",
|
||||
"author": "thzxx",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('App - 提示与容错', () => {
|
||||
})
|
||||
|
||||
describe('App - 大输入与 worker 回退', () => {
|
||||
it('超过快路径阈值的文件在无 Worker 环境回退同步计算', async () => {
|
||||
it('超过快路径阈值的文件在无 Worker 环境去抖到期后回退同步计算', async () => {
|
||||
const big = Array.from({ length: 2100 }, (_, i) => `row${i}`).join('\n')
|
||||
window.api = mockApi({
|
||||
openFile: async () => ({
|
||||
@@ -376,9 +376,9 @@ describe('App - 大输入与 worker 回退', () => {
|
||||
render(<App />)
|
||||
fireEvent.click(screen.getByText('打开左侧'))
|
||||
expect(await screen.findByText('导出报告')).toBeInTheDocument()
|
||||
// jsdom 无 Worker:同步回退完成计算,不出现计算中遮罩,行已渲染
|
||||
// jsdom 无 Worker:去抖到期后同步回退完成计算,行渲染且计算中遮罩消失
|
||||
expect(await screen.findByText(/row0/)).toBeInTheDocument()
|
||||
expect(screen.queryByText('正在计算差异…')).not.toBeInTheDocument()
|
||||
expect(screen.getAllByText(/row0/).length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -48,10 +48,11 @@ describe('buildTxtReport', () => {
|
||||
})
|
||||
|
||||
describe('buildMarkdownReport', () => {
|
||||
it('输出表格化报告', () => {
|
||||
it('输出表格化报告,标题独立成行不进引用块', () => {
|
||||
const { rows, summary } = computeDiff('a', 'b')
|
||||
const md = buildMarkdownReport(rows, ctx, summary)
|
||||
expect(md).toContain('# DiffLens 差异报告')
|
||||
expect(md.startsWith('# DiffLens 差异报告')).toBe(true)
|
||||
expect(md).not.toContain('> # DiffLens 差异报告')
|
||||
expect(md).toContain('| 类型 |')
|
||||
expect(md).toContain('| 左行号 |')
|
||||
})
|
||||
|
||||
@@ -179,7 +179,9 @@ export function buildMarkdownReport(
|
||||
summary: DiffSummary
|
||||
): string {
|
||||
const head = headLines(ctx, summary, '# DiffLens 差异报告')
|
||||
const out: string[] = [...head.map((h) => `> ${h}`), '']
|
||||
// 标题独立成行(不套进引用块),其余元信息行以引用块展示
|
||||
const [title, ...meta] = head
|
||||
const out: string[] = [title, '', ...meta.map((h) => `> ${h}`), '']
|
||||
out.push('| 类型 | 左行号 | 左文件 | 右行号 | 右文件 |')
|
||||
out.push('| --- | --- | --- | --- | --- |')
|
||||
for (const row of rows) {
|
||||
|
||||
@@ -66,15 +66,15 @@ describe('useDiff - 快路径', () => {
|
||||
})
|
||||
|
||||
describe('useDiff - worker 后台计算', () => {
|
||||
it('重输入先进入 computing,去抖后派发,回传后更新', () => {
|
||||
it('重输入先进入 computing,去抖到期才创建 worker 并派发,回传后更新', () => {
|
||||
const factory = makeFactory()
|
||||
const left = heavyText(1100)
|
||||
const right = heavyText(1100)
|
||||
const { result } = renderHook(() => useDiff(left, right, NO_OPTIONS, factory))
|
||||
expect(result.current.computing).toBe(true)
|
||||
expect(result.current.diff).toBeNull()
|
||||
// 去抖期内尚未派发
|
||||
expect(factory.workers[0].posted).toHaveLength(0)
|
||||
// 去抖期内尚未创建 worker(零创建开销)
|
||||
expect(factory.workers).toHaveLength(0)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
@@ -117,11 +117,11 @@ describe('useDiff - worker 后台计算', () => {
|
||||
w1.respond(jobId1, computeDiff('a', 'a', {}))
|
||||
})
|
||||
expect(hook.result.current.diff).toBeNull()
|
||||
// 新 worker 的结果生效
|
||||
const w2 = factory.workers[1]
|
||||
// 新 worker 的结果生效(新任务同样在去抖到期才创建 worker)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
const w2 = factory.workers[1]
|
||||
const jobId2 = w2.posted[0].jobId
|
||||
expect(jobId2).toBeGreaterThan(jobId1)
|
||||
const res2 = computeDiff('m', 'n', {})
|
||||
@@ -132,7 +132,7 @@ describe('useDiff - worker 后台计算', () => {
|
||||
expect(hook.result.current.computing).toBe(false)
|
||||
})
|
||||
|
||||
it('去抖期内输入变更:旧任务未派发即取消(零浪费计算)', () => {
|
||||
it('去抖期内输入变更:旧任务未创建即取消(零创建与计算开销)', () => {
|
||||
const factory = makeFactory()
|
||||
const hook = renderHook((p: string) => useDiff(p, '', NO_OPTIONS, factory), {
|
||||
initialProps: heavyText(2100)
|
||||
@@ -141,34 +141,51 @@ describe('useDiff - worker 后台计算', () => {
|
||||
vi.advanceTimersByTime(100)
|
||||
})
|
||||
hook.rerender(heavyText(2300))
|
||||
expect(factory.workers[0].posted).toHaveLength(0)
|
||||
expect(factory.workers[0].terminated).toBe(true)
|
||||
// 去抖期内取消:worker 从未被创建
|
||||
expect(factory.workers).toHaveLength(0)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300)
|
||||
})
|
||||
expect(factory.workers[1].posted).toHaveLength(1)
|
||||
expect(factory.workers).toHaveLength(1)
|
||||
expect(factory.workers[0].posted).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('worker 不可用时回退主线程同步计算', () => {
|
||||
it('worker 不可用时去抖到期回退主线程同步计算', () => {
|
||||
const left = heavyText(2100)
|
||||
// 工厂必须稳定引用:内联箭头函数每次渲染变引用会引发 effect 无限重跑
|
||||
const nullFactory: DiffWorkerFactory = () => null
|
||||
const { result } = renderHook(() => useDiff(left, 'x', NO_OPTIONS, nullFactory))
|
||||
// 去抖期内同样处于 computing(无 Worker 回退不豁免去抖)
|
||||
expect(result.current.computing).toBe(true)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
expect(result.current.diff).toEqual(computeDiff(left, 'x', {}))
|
||||
expect(result.current.computing).toBe(false)
|
||||
})
|
||||
|
||||
it('卸载时终止 worker 并清理去抖定时器', () => {
|
||||
it('去抖期内卸载:worker 从未创建且不再派发', () => {
|
||||
const factory = makeFactory()
|
||||
const left = heavyText(2100)
|
||||
const hook = renderHook(() => useDiff(left, '', NO_OPTIONS, factory))
|
||||
const w = factory.workers[0]
|
||||
hook.unmount()
|
||||
expect(w.terminated).toBe(true)
|
||||
// 卸载后去抖到期不再派发
|
||||
expect(factory.workers).toHaveLength(0)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(300)
|
||||
})
|
||||
expect(w.posted).toHaveLength(0)
|
||||
expect(factory.workers).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('去抖到期派发后卸载:worker 被终止', () => {
|
||||
const factory = makeFactory()
|
||||
const left = heavyText(2100)
|
||||
const hook = renderHook(() => useDiff(left, '', NO_OPTIONS, factory))
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(250)
|
||||
})
|
||||
const w = factory.workers[0]
|
||||
expect(w.posted).toHaveLength(1)
|
||||
hook.unmount()
|
||||
expect(w.terminated).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -19,7 +19,8 @@ export interface DiffState {
|
||||
/**
|
||||
* worker 化的 diff 计算 hook。
|
||||
* 小输入同步快路径(行为与 worker 化之前完全一致);
|
||||
* 大输入去抖后派发 worker 后台计算,输入变化时终止旧 worker 重建(JS worker 无法中断计算,销毁重建即取消),
|
||||
* 大输入去抖到期后才创建 worker 并派发后台计算(去抖期内取消的任务零创建开销),
|
||||
* 输入变化时终止旧 worker 重建(JS worker 无法中断计算,销毁重建即取消),
|
||||
* 响应按 jobId 匹配丢弃过期任务;worker 不可用的环境回退主线程同步计算。
|
||||
* workerFactory 仅测试注入用,生产调用保持缺省;注入时需保证引用稳定。
|
||||
*/
|
||||
@@ -49,7 +50,12 @@ export function useDiff(
|
||||
if (!heavy) return
|
||||
// 重输入先清空旧结果:避免交换左右等场景下展示与新面板不对应的过期差异
|
||||
setAsyncDiff(null)
|
||||
const w = factory()
|
||||
setComputing(true)
|
||||
let w: Worker | null = null
|
||||
// worker 延迟到去抖到期才创建:内联 worker 实例化含 base64 解码与脚本编译,
|
||||
// 去抖期内输入再变时旧任务未创建即取消,零创建与计算开销
|
||||
const timer = window.setTimeout(() => {
|
||||
w = factory()
|
||||
if (!w) {
|
||||
// 无 Worker 支持(jsdom/老环境):主线程同步回退
|
||||
setAsyncDiff(computeDiff(left, right, options))
|
||||
@@ -57,8 +63,6 @@ export function useDiff(
|
||||
return
|
||||
}
|
||||
const jobId = ++jobIdRef.current
|
||||
setComputing(true)
|
||||
const timer = window.setTimeout(() => {
|
||||
w.onmessage = (e: MessageEvent<DiffWorkerResponse>): void => {
|
||||
// 丢弃过期任务:输入已变化时旧计算结果不再生效
|
||||
if (e.data.jobId !== jobId) return
|
||||
@@ -69,7 +73,7 @@ export function useDiff(
|
||||
}, DEBOUNCE_MS)
|
||||
return () => {
|
||||
window.clearTimeout(timer)
|
||||
w.terminate()
|
||||
w?.terminate()
|
||||
}
|
||||
}, [left, right, options, heavy, factory])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user