Skip to content

Commit 6246909

Browse files
chaxusclaude
andcommitted
refactor(editor): split the 1975-line onlyoffice-editor into a facade and one module per concern
lib/onlyoffice-editor.ts had grown to seven unrelated concerns in one file, and prepareEditorIframe alone was 584 lines: nine independent vendor patches inside a single frame loop, each with its own `win as unknown as {...}` cast and `__oo*` flag, with a "everything is in place" check reading five locals scattered through it. It is now a facade -- mount, rebuild, loadEditorApi -- over lib/onlyoffice/: iframe-guards (orchestration) with one file per guard under guards/, open-state, open-failure, font-system, save-stream, viewport, sdk-api, readonly, ui-theme, file-helpers. Each guard file leads with the production defect it exists for. The public surface is unchanged: the facade re-exports every symbol lib/embed-api, lib/web-mcp, lib/converter and the unit tests import, so not a line of test code moves. Two rules the split follows: - Cross-module state has exactly one owner. documentContentReady / documentOpenError / lastFrameError were module-level variables read and written from three places; they now live in open-state and everyone else calls its accessors. Readonly belongs to readonly.ts, the in-flight save request to save-stream.ts (the guard fails it through failPendingSaveConversion instead of reaching into another module's state). Duplicating any of it is the one thing that would actually introduce bugs. - No cycles. The retry path needs createEditorInstance, which lives in the facade, so it goes through an injected runner (setOpenRunner) the same way the converter and UI callbacks already do. Behaviour is unchanged by construction: every line of the old file was matched verbatim against the new tree, and the 39 differences are all deliberate (added `export`, module variables read through accessors, the injected runner, moved imports). Guards report "in place" as a return value; iframe-guards combines the same five as before. Verified: tsc + oxlint clean, 617 unit tests green (untouched), full local e2e against the real editor 82 passed / 16 skipped / 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent bc96ff0 commit 6246909

23 files changed

Lines changed: 2005 additions & 1670 deletions

‎CLAUDE.md‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,21 @@ lib/ # 应用层(纯 TypeScript,只在本站点用)
6060
events.ts # MessageCodec 事件处理(桌面端集成)
6161
file-types.ts # OnlyOffice 文件类型常量映射
6262
loading.ts # 加载状态 UI
63-
onlyoffice-editor.ts # 编辑器实例生命周期、保存、只读模式、运行时守卫
63+
onlyoffice-editor.ts # 编辑器生命周期门面:挂载/重建/loadEditorApi,并对外统一导出下面这些模块
64+
onlyoffice/ # 编辑器周边(2026-08-19 从 1975 行的单文件拆出,公开导出面不变)
65+
iframe-guards.ts # 9 条运行时守卫的编排;每条守卫一个文件在 guards/
66+
guards/ # chrome / shared-worker / fetch-fonts / image-pipeline /
67+
# serverless-save / long-action / series-settings /
68+
# font-loading / comment-selection / canvas-loss
69+
open-state.ts # 就绪、打开失败、frame 首个错误(三处共用的单一状态源)
70+
open-failure.ts # 失败分类、-82 guard、环境类失败重开一次(经 setOpenRunner 注入避免环)
71+
font-system.ts # 字体系统就绪判定 + awaitFontSystem(#144)
72+
save-stream.ts # 保存通道:请求生命周期、asc_DownloadAs 触发、file-stream 回收、CSV 往返
73+
viewport.ts # 紧凑视口判定与布局同步(#145)
74+
sdk-api.ts # 同源 iframe 里的 Asc.editor 访问、restriction 常量
75+
readonly.ts # 运行时只读(挂载永远可编辑,加载后加 restriction)
76+
ui-theme.ts # 默认经典主题与站点主题跟随
77+
file-helpers.ts # 文件名/MIME/字节形状小工具
6478
ui.ts # 控制面板、菜单、FAB 等 UI 组件
6579
analytics.ts # Cloudflare Web Analytics(刻意不用 GA;线上实际走 CF 面板边缘注入,因此 embed 视图会被计入、/embed-demo 双计,读数规则见 docs/explorations/2026-08-17-analytics-edge-injection-double-count.md)
6680
pending-open.ts # 静态落地页经 IndexedDB 交接文件(?open=local)
@@ -106,13 +120,19 @@ editor.html # `/editor`:编辑器页面(?new= ?file= ?src= ?embed=
106120

107121
使用 `?embedOrigin=https://example.com` 可限制消息来源。
108122

109-
### onlyoffice-editor.ts — 编辑器生命周期
123+
### onlyoffice-editor.ts — 编辑器生命周期(门面)
110124

111125
- `createEditorInstance(config)` — 创建/重建编辑器,内部有操作队列防并发
112-
- `setReadonlyMode(bool)` / `getReadonlyMode()` — 只读模式
113-
- `requestSaveDocument(targetExt, options)` — 触发编辑器保存并返回 File,180s 超时(慢链路首存要先下 10 MB wasm;打开失败会立即拒绝)
126+
- `setReadonlyMode(bool)` / `getReadonlyMode()` — 只读模式(实现在 `onlyoffice/readonly.ts`)
127+
- `requestSaveDocument(targetExt, options)` — 触发编辑器保存并返回 File,180s 超时(慢链路首存要先下 10 MB wasm;打开失败会立即拒绝;实现在 `onlyoffice/save-stream.ts`)
114128
- `setConverterCallbacks(...)` — 注入转换器(解耦循环依赖)
115129

130+
**改这一块前先看清归属**:本文件只剩挂载/重建/加载 API 三件事,其余按上面的目录
131+
各有其主;跨模块状态一律只有一个持有者(就绪与打开失败在 `open-state.ts`,只读在
132+
`readonly.ts`,保存请求在 `save-stream.ts`),要读就调它导出的访问器,别再复制一份。
133+
新增厂商运行时补丁 = 在 `onlyoffice/guards/` 新建一个文件并在 `iframe-guards.ts`
134+
里挂上,别往门面里塞。
135+
116136
### packages/shared/src/store.ts — 全局状态
117137

118138
```ts
@@ -570,9 +590,11 @@ v7 代码分支(OO_VARIANT、页面级 x2t 打开转换、empty_bin 模板、v
570590

571591
## 测试覆盖说明
572592

573-
### 为什么 onlyoffice-editor.ts 覆盖率低
593+
### 为什么 onlyoffice-editor.ts / onlyoffice/ 覆盖率低
574594

575-
这是预期行为,**不需要强行提升**。该文件大量代码是 OnlyOffice 编辑器的事件回调,必须有真实编辑器运行才能触发:
595+
这是预期行为,**不需要强行提升**。这些代码大量是 OnlyOffice 编辑器的事件回调与
596+
厂商运行时补丁(`onlyoffice/guards/**` 整目录只有真实编辑器跑起来才会执行),
597+
必须有真实编辑器运行才能触发:
576598

577599
| 函数 | 无法单测的原因 |
578600
| -------------------------------- | --------------------------------------------------------------------- |
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# 把 1975 行的 onlyoffice-editor.ts 拆成一个门面 + 一层模块
2+
3+
日期:2026-08-19
4+
5+
## 一、为什么要拆
6+
7+
`lib/onlyoffice-editor.ts` 长到 1975 行,里面塞着七类彼此无关的东西:编辑器挂载、
8+
保存通道、打开失败处理、字体系统竞态、9 条厂商运行时补丁、移动端布局、只读开关。
9+
最直接的代价是 `prepareEditorIframe` 一个函数就有 **584 行**——9 条补丁挤在同一个
10+
`for (frame)` 循环里,每条都自带一套 `win as unknown as {...}` 断言和一个 `__oo*`
11+
标志位,改任何一条都要在这团里翻页找边界,而且函数末尾那句"全部就位"判定读的是 5
12+
个散落在各处的局部变量。
13+
14+
## 二、拆法
15+
16+
**一个门面 + 一层各司其职的模块**,公开导出面(`lib/embed-api`、`lib/web-mcp`、
17+
`lib/converter` 与全部单测所依赖的符号)**一个都没变**:门面用 `export ... from`
18+
把它们原样转出去,所以这次拆分对调用方和测试是零改动。
19+
20+
```
21+
lib/onlyoffice-editor.ts 387 行 挂载 / 重建 / loadEditorApi + 公开导出面
22+
lib/onlyoffice/
23+
iframe-guards.ts 64 行 9 条守卫的编排(谁装了、装没装全)
24+
guards/*.ts 19~121 行 一条守卫一个文件,缺陷背景写在文件头
25+
open-state.ts 120 行 就绪 / 打开失败 / frame 首个错误
26+
open-failure.ts 218 行 失败分类、-82 guard、环境类失败重开一次
27+
font-system.ts 133 行 字体系统就绪判定 + awaitFontSystem
28+
save-stream.ts 327 行 保存请求生命周期 + 导出触发 + file-stream 回收
29+
viewport.ts 201 行 紧凑视口判定与布局同步
30+
sdk-api.ts 77 行 同源 iframe 里的 Asc.editor 访问
31+
readonly.ts 46 行 运行时只读
32+
ui-theme.ts 26 行 默认主题 / 站点主题跟随
33+
file-helpers.ts 30 行 文件名 / MIME / 字节形状
34+
```
35+
36+
拆的时候只守两条规矩:
37+
38+
1. **跨模块状态只有一个持有者**。`documentContentReady` / `documentOpenError` /
39+
`lastFrameError` 过去是三个模块级变量,被守卫、保存路径、错误 toast 三处直接
40+
读写;现在全部归 `open-state.ts`,其它模块一律调 `isDocumentContentReady()` /
41+
`getDocumentOpenError()` / `getLastFrameError()`。只读状态归 `readonly.ts`,
42+
在途保存请求归 `save-stream.ts`(守卫要让它失败就调
43+
`failPendingSaveConversion`,不再自己伸手进别人的变量)。复制一份状态是这种拆分
44+
唯一会真正制造 bug 的做法。
45+
2. **不许出现环**。`open-failure` 的"环境类失败重开一次"要调 `createEditorInstance`,
46+
而后者在门面里——沿用仓库既有的回调注入约定(`setConverterCallbacks` /
47+
`setUICallbacks`),门面在末尾 `setOpenRunner(createEditorInstance)`,
48+
`open-failure` 只认一个 `OpenRunner` 类型。
49+
50+
守卫的返回值也统一了:每个 `install*Guard(win)` 返回"这条守卫现在是否在位"(无论
51+
是本次装上还是早就装过),`iframe-guards` 用其中 5 个必须就位的返回值合成原来的
52+
`fullyApplied`——与旧代码逐位等价,只是判定从"读 5 个局部变量"变成"读 5 个返回值"。
53+
54+
## 三、怎么保证行为没变
55+
56+
- **逐行核对**:脚本把旧文件的每一行(去掉纯注释/括号)拿到新目录里全文匹配,
57+
剩下 39 行差异逐条人工确认,全部是刻意改动:加 `export`、把直接读模块变量改成
58+
调访问器、`createEditorInstance(` → 注入的 `runOpen(`、以及移动过的 import。
59+
没有一行逻辑被顺手"改进"——这次提交只搬家,不改行为。
60+
- `tsc --noEmit` + oxlint 全绿;单测 32 文件 617 例全绿(测试文件一行没动,
61+
因为公开导出面没变)。
62+
- 本地 E2E 全套(真实编辑器 + 真实 x2t)**82 passed / 16 skipped / 0 failed**
63+
——守卫全在这一层被真正执行到,它是这次拆分唯一有意义的验收:单测覆盖不到
64+
`guards/**` 的任何一行。(第一遍跑时 `open-retry` 的字体等待预算断言红过一次,
65+
2350 ms vs 2000 ms;单跑 2.6 s 通过,机器当时还在跑 docker 套件——是并发下的
66+
时序抖动,不是拆分引入的。空载重跑全绿。)
67+
68+
## 四、以后往哪加
69+
70+
- 新的厂商运行时补丁 = `onlyoffice/guards/` 新建一个文件 + 在 `iframe-guards.ts`
71+
挂上,别再往门面里塞;
72+
- 需要读"文档是否就绪 / 为什么打不开"就调 `open-state` 的访问器;
73+
- 门面只该有三件事:挂载、重建、加载 API。

0 commit comments

Comments
 (0)