view-anchor 0.1.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +118 -34
- package/README.zh-CN.md +128 -44
- package/dist/index.d.ts +4 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -14
- package/dist/measure-loop.d.ts +9 -28
- package/dist/measure-loop.d.ts.map +1 -1
- package/dist/measure-loop.js +37 -16
- package/dist/protocol-publisher.d.ts +41 -0
- package/dist/protocol-publisher.d.ts.map +1 -0
- package/dist/protocol-publisher.js +191 -0
- package/dist/protocol-types.d.ts +36 -0
- package/dist/protocol-types.d.ts.map +1 -0
- package/dist/protocol-types.js +10 -0
- package/dist/protocol.d.ts +35 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +131 -0
- package/dist/react.d.ts +18 -30
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +125 -122
- package/dist/size-advertiser.d.ts +9 -14
- package/dist/size-advertiser.d.ts.map +1 -1
- package/dist/size-advertiser.js +20 -28
- package/dist/types.d.ts +29 -73
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -15
- package/dist/view-anchor.d.ts +36 -77
- package/dist/view-anchor.d.ts.map +1 -1
- package/dist/view-anchor.js +206 -174
- package/docs/bidirectional-design.md +64 -96
- package/docs/{anchor-3d.html → index.html} +215 -73
- package/docs/mechanism.mdx +55 -49
- package/docs/performance-report.md +63 -0
- package/docs/protocol.md +79 -0
- package/package.json +30 -4
- package/src/index.ts +6 -15
- package/src/measure-loop.ts +36 -41
- package/src/protocol-publisher.ts +236 -0
- package/src/protocol-types.ts +43 -0
- package/src/protocol.ts +193 -0
- package/src/react.ts +186 -141
- package/src/size-advertiser.ts +24 -31
- package/src/types.ts +34 -79
- package/src/view-anchor.ts +228 -212
package/docs/mechanism.mdx
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: view-anchor
|
|
3
|
-
description:
|
|
3
|
+
description: 将主进程原生视图(如 Electron WebContentsView)与 DOM 元素的几何位置保持同步。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# view-anchor
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
让宿主外部的视图(例如 Electron 的 `WebContentsView`)实时对齐某个 DOM 元素的屏幕位置。核心逻辑通过 `getBoundingClientRect()` 测量目标元素,把矩形数据交给注入的 `publish` 回调(通常由调用方转发 IPC 到 `setBounds`),并在元素移动或缩放时同步更新。
|
|
9
9
|
|
|
10
|
-
核心不依赖 React、Electron
|
|
10
|
+
核心不依赖 React、Electron 或特定的布局引擎;React 相关的逻辑均隔离在 `view-anchor/react` 适配层中。
|
|
11
11
|
|
|
12
12
|
<iframe
|
|
13
|
-
src="./
|
|
13
|
+
src="./index.html"
|
|
14
14
|
title="view-anchor 交互演示"
|
|
15
15
|
style={{ width: '100%', height: '560px', border: '0', borderRadius: '12px' }}
|
|
16
16
|
/>
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## 运行机制
|
|
19
19
|
|
|
20
20
|
```mermaid
|
|
21
21
|
flowchart LR
|
|
@@ -29,91 +29,97 @@ flowchart LR
|
|
|
29
29
|
VA -->|"publish(bounds)<br/>IPC → setBounds"| WCV
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
`WebContentsView`
|
|
32
|
+
`WebContentsView` 运行在主进程,位置由主进程的 `setBounds` 设置;页面布局则由渲染进程通过 CSS 计算。两者不在同一个进程中,view-anchor 作为桥梁连接它们:占位 div 参与 DOM 布局,外部视图悬浮在上方,由 view-anchor 维持对齐。`publish` 是外部注入的回调,核心模块本身完全不感知 Electron。
|
|
33
33
|
|
|
34
34
|
## createViewAnchor(target, opts)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
命令式核心接口,将原生视图绑定到目标元素,返回 `{ update, dispose }`。
|
|
37
37
|
|
|
38
38
|
```ts
|
|
39
39
|
const handle = createViewAnchor(target, {
|
|
40
|
-
present: true, //
|
|
41
|
-
publish: (bounds) => { ... }, //
|
|
40
|
+
present: true, // 是否显示原生视图
|
|
41
|
+
publish: (bounds) => { ... }, // 接收最新矩形,转发给 setBounds
|
|
42
42
|
})
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
| 状态 /
|
|
45
|
+
| 状态 / 操作 | 行为 |
|
|
46
46
|
|---|---|
|
|
47
|
-
| `present: true` |
|
|
48
|
-
| `present: false` | 停止观察,发布一次 `{0,0,0,0}`。 |
|
|
49
|
-
| `update(opts)` |
|
|
50
|
-
| `dispose()` |
|
|
47
|
+
| `present: true` | 立即发布测量矩形,之后每次 `ResizeObserver` 触发或窗口 `resize` 时同步重发。 |
|
|
48
|
+
| `present: false` | 停止观察,发布一次 `{ x: 0, y: 0, width: 0, height: 0 }`。 |
|
|
49
|
+
| `update(opts)` | 应用新选项,重置去重缓存并立即重新发布一次。 |
|
|
50
|
+
| `dispose()` | 停止所有监听并释放资源,此后不再发布。 |
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
测量结果使用 `Math.round` 取整。`width` 和 `height` 会限制为 `>= 0`(0 代表收起),但 `x` 和 `y` 允许为负数。当元素滚动出视口上边缘或左边缘时,原点自然会是负值,保留负值可以让视图正常跟随元素滚出屏幕。
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
**为什么同步发布而不走 RAF:** 原生 overlay 处于另一个进程,IPC 传递到 `setBounds` 相比渲染进程本身的绘制已经有一帧左右的合成延迟。如果测量和发布再进一次 RAF,拖拽时就会叠加第二帧延迟,造成明显的跟随拖尾。在 observer 回调中直接同步测量和发布可以省掉这层额外延迟。同帧多次触发的防抖则交由同值去重处理:只有矩形数值与上一次不同时才触发 `publish`。调用 `update` 时会先清除去重基线,确保外部状态变化(如缩放系数变化)时即使几何数据未变也能重新发布。
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
调用 `dispose()` 或更新为 `present: false` 后,内部状态会立即置为停用,后续任何异步触发都会直接返回,避免过期数据覆盖新状态。
|
|
57
57
|
|
|
58
|
-
##
|
|
58
|
+
## 收起与零矩形
|
|
59
59
|
|
|
60
|
-
- **`present
|
|
61
|
-
- **`{0,0,0,0}
|
|
62
|
-
- **dispose
|
|
60
|
+
- **`present`**:标识原生视图当前是否需要显示。
|
|
61
|
+
- **`{ x: 0, y: 0, width: 0, height: 0 }`(零矩形)**:收起信号。宿主可以将零面积理解为“从窗口中移出视图,但保留其 `WebContents` 实例”,实现秒开复用而不是反复销毁重建。
|
|
62
|
+
- **dispose 行为**:`dispose()` 只停止监听,不补发零矩形。如果需要在元素移除时通知宿主收起视图,应当在 dispose 之前调用 `update({ present: false, ... })`,React 适配层已自动处理了该生命周期。
|
|
63
63
|
|
|
64
|
-
## createPlacementAnchor
|
|
64
|
+
## 显式可见性(createPlacementAnchor)
|
|
65
65
|
|
|
66
|
-
`createViewAnchor`
|
|
66
|
+
`createViewAnchor` 使用零矩形表示收起,无法区分“隐藏”与“元素确实存在但尺寸为 0x0”的情况。`createPlacementAnchor` 采用显式的 `Placement` 判别联合类型:
|
|
67
67
|
|
|
68
68
|
```ts
|
|
69
69
|
const handle = createPlacementAnchor(target, {
|
|
70
|
-
visible: true, //
|
|
70
|
+
visible: true, // 显式控制可见性
|
|
71
71
|
publish: (placement) => { ... },
|
|
72
72
|
})
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
-
|
|
|
75
|
+
| 选项 / 操作 | 行为 |
|
|
76
76
|
|---|---|
|
|
77
|
-
| `visible: true` |
|
|
78
|
-
| `visible: false` |
|
|
79
|
-
| `guardDisplayNone`(默认 false) |
|
|
80
|
-
| `followScroll`(默认 false) |
|
|
81
|
-
| `followGeometry`(默认 false) |
|
|
77
|
+
| `visible: true` | 发布 `{ visible: true, bounds }`,并在尺寸变化时同步重发。 |
|
|
78
|
+
| `visible: false` | 发布 `{ visible: false }`,停止观察。 |
|
|
79
|
+
| `guardDisplayNone`(默认 false) | 开启后,测出零面积的元素发布 `{ visible: false }`,并挂载 `IntersectionObserver` 捕捉 `display: none` 切换。 |
|
|
80
|
+
| `followScroll`(默认 false) | 在捕获阶段监听 `window` 的 `scroll` 事件,祖先容器滚动时重新测量。 |
|
|
81
|
+
| `followGeometry`(默认 false) | 在需要时启动单帧 RAF 轮询,捕获没有 DOM 事件的祖先 transform 或布局位移;几何稳定后自动停止。也可通过 `pulse()` 手动触发。 |
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
去重逻辑会同时对比 `visible` 状态,因此可见性切换不会被同值去重误吞。
|
|
84
84
|
|
|
85
85
|
## useViewAnchor(opts)
|
|
86
86
|
|
|
87
|
-
React
|
|
87
|
+
React 适配层,从 `view-anchor/react` 导出,返回用于挂载在占位元素上的 ref 回调。
|
|
88
88
|
|
|
89
89
|
```tsx
|
|
90
|
+
import { useViewAnchor } from 'view-anchor/react'
|
|
91
|
+
|
|
90
92
|
const ref = useViewAnchor({
|
|
91
93
|
present, // boolean
|
|
92
|
-
publish, //
|
|
93
|
-
deps: [signature], //
|
|
94
|
+
publish, // Publisher<Bounds>,同步返回 false 表示未接收
|
|
95
|
+
deps: [signature], // 可选:影响位置但不会触发 ResizeObserver 的外部依赖
|
|
94
96
|
})
|
|
95
97
|
return <div ref={ref} />
|
|
96
98
|
```
|
|
97
99
|
|
|
98
100
|
| 时机 | 行为 |
|
|
99
101
|
|---|---|
|
|
100
|
-
| 挂载 |
|
|
101
|
-
| `opts`
|
|
102
|
-
|
|
|
102
|
+
| 挂载 | 创建 anchor 实例并开始监听 |
|
|
103
|
+
| `opts` 或 `deps` 变化 | 调用 `update` 更新参数 |
|
|
104
|
+
| 卸载 | 在 microtask 内发布零矩形并释放资源 |
|
|
105
|
+
|
|
106
|
+
**`deps` 参数**:`ResizeObserver` 只在元素自身的 border-box 改变时触发。如果页面上有某些状态会移动元素位置却不改变其尺寸(例如兄弟节点切换、路由跳转、复杂的外部布局更新),可将这些状态放入 `deps`。
|
|
107
|
+
|
|
108
|
+
React 18 在卸载时会传入 `ref(null)`,React 19 支持 ref 清理函数。适配层将卸载通知推迟一个微任务执行:React 19 在 StrictMode 下开发阶段的快速卸载重挂载会被就地取消,真正的卸载则在微任务内正常触发清理。
|
|
103
109
|
|
|
104
|
-
|
|
110
|
+
## usePlacementAnchor(opts)
|
|
105
111
|
|
|
106
|
-
|
|
112
|
+
同样导出自 `view-anchor/react`,将 `createPlacementAnchor` 的 `visible`、`followScroll`、`followGeometry` 与 `guardDisplayNone` 接入 React ref 生命周期。真正卸载时会发布 `{ visible: false }` 并释放监听。
|
|
107
113
|
|
|
108
|
-
##
|
|
114
|
+
## 模块结构
|
|
109
115
|
|
|
110
|
-
| 文件 |
|
|
116
|
+
| 文件 | 用途 |
|
|
111
117
|
|---|---|
|
|
112
|
-
| `src/view-anchor.ts` |
|
|
113
|
-
| `src/react.ts` | React
|
|
114
|
-
| `src/size-advertiser.ts` |
|
|
115
|
-
| `src/measure-loop.ts` | 反向专用的 RAF
|
|
116
|
-
| `src/types.ts` |
|
|
117
|
-
| `src/index.ts` |
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
| `src/view-anchor.ts` | 正向命令式核心:`createViewAnchor`、`createPlacementAnchor`、`measurePlacement`。不含 React 和 Electron 依赖。 |
|
|
119
|
+
| `src/react.ts` | React 适配层:`useViewAnchor` 与 `usePlacementAnchor`。 |
|
|
120
|
+
| `src/size-advertiser.ts` | 反向核心:`createSizeAdvertiser`。 |
|
|
121
|
+
| `src/measure-loop.ts` | 反向专用的 RAF 调度与去重循环(内部实现,不对外导出)。 |
|
|
122
|
+
| `src/types.ts` | 类型定义(`Bounds`、`Placement`、各模块配置与句柄)。 |
|
|
123
|
+
| `src/index.ts` | 核心入口,默认不引入 React。 |
|
|
124
|
+
|
|
125
|
+
核心运行时仅依赖标准 Web API(`ResizeObserver`、`getBoundingClientRect`、`addEventListener`);`requestAnimationFrame` 仅在反向模块与可选的 `followGeometry` 中按需使用。
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# view-anchor 性能报告
|
|
2
|
+
|
|
3
|
+
本报告只保留当前工作树可由 `pnpm benchmark` 直接重建的绝对数据。命令启动 3 个全新的 Node.js 进程;每个进程预热 2 次、保留 7 个样本。CPU 表的“当前中位数”是三个进程中位数的中位数,完整 JSON 含全部 21 个原始样本。
|
|
4
|
+
|
|
5
|
+
本次环境:Node.js 24.18.0、macOS arm64、Apple M4(10 个逻辑核心)。这些数字只适合同机比较,不是浏览器、Electron IPC 或 DOM layout 的耗时承诺。
|
|
6
|
+
|
|
7
|
+
## CPU
|
|
8
|
+
|
|
9
|
+
| 场景 | 数据量 | 当前中位数 | 三进程中位数(ms) |
|
|
10
|
+
| --- | ---: | ---: | --- |
|
|
11
|
+
| 所有锚点进入下一 generation | 10,000 | 1.7407ms | 1.5772、1.7407、1.8334 |
|
|
12
|
+
| 逐个 `clear(anchorId)` | 10,000 | 1.0757ms | 1.0757、1.0241、1.0794 |
|
|
13
|
+
| 已有 100,000 个历史锚点后只 flush 1 条 | 1 | 0.0082ms | 0.0102、0.0068、0.0082 |
|
|
14
|
+
| 发布 placement envelope | 1,000,000 次 | 7.2710ms | 7.4852、7.2113、7.2710 |
|
|
15
|
+
| 同一 anchor 连续 publish + flush | 100,000 次 | 8.8899ms | 8.8899、8.8434、9.2207 |
|
|
16
|
+
| 解码合法 batch | 100,000 条 | 3.0990ms | 3.0990、2.9431、3.2170 |
|
|
17
|
+
| 解码末项非法 batch | 100,000 条 | 3.3382ms | 3.3195、3.6282、3.3382 |
|
|
18
|
+
| sequence guard 接收并清理 | 100,000 条 | 12.8928ms | 12.9085、12.8928、12.8151 |
|
|
19
|
+
| `measurePlacement` | 1,000,000 次 | 8.9269ms | 8.9269、8.5498、9.0800 |
|
|
20
|
+
|
|
21
|
+
## 内存
|
|
22
|
+
|
|
23
|
+
每个状态在独立 Node.js 进程中执行,并在读数前显式 GC。表中为三个独立进程的中位数增量;heap 是 V8 保留堆,RSS 是进程常驻集合大小,二者不应混用。清理对象后,分配器也不一定立即把内存页归还给操作系统,所以 `afterClear` 的 RSS 不能当作仍有等量 JavaScript 对象存活。
|
|
24
|
+
|
|
25
|
+
性能 harness 将 `queueMicrotask` 替换为 no-op,然后显式调用 `flush()`;这是刻意测量 batcher 的 pending/retained 状态,**不是**真实宿主 microtask 调度路径。
|
|
26
|
+
|
|
27
|
+
| 状态(100,000 个 anchor) | heap 增量 | RSS 增量 |
|
|
28
|
+
| --- | ---: | ---: |
|
|
29
|
+
| batcher 待 flush | 26,320,176B | 45,858,816B |
|
|
30
|
+
| batcher 成功 flush 后 | 13,303,872B | 43,696,128B |
|
|
31
|
+
| batcher `clear()` 后 | 34,432B | 40,026,112B |
|
|
32
|
+
| sequence guard 保留状态 | 14,088,840B | 27,099,136B |
|
|
33
|
+
| sequence guard `clear()` 后 | 19,568B | 23,461,888B |
|
|
34
|
+
|
|
35
|
+
## 实际导出代码体积
|
|
36
|
+
|
|
37
|
+
使用 esbuild bundle、tree-shaking 和 minify,且将 `react` 作为 peer external。测量的是实际执行 JavaScript,不是 npm tarball、source map 或声明文件。
|
|
38
|
+
|
|
39
|
+
| 完整入口 | raw | gzip | brotli |
|
|
40
|
+
| --- | ---: | ---: | ---: |
|
|
41
|
+
| `view-anchor` | 6,627B | 2,588B | 2,297B |
|
|
42
|
+
| `view-anchor/protocol` | 3,581B | 1,412B | 1,276B |
|
|
43
|
+
| `view-anchor/react` | 5,472B | 2,023B | 1,797B |
|
|
44
|
+
|
|
45
|
+
| 单独导出 | raw | gzip | brotli |
|
|
46
|
+
| --- | ---: | ---: | ---: |
|
|
47
|
+
| `createViewAnchor` | 987B | 528B | 472B |
|
|
48
|
+
| `createPlacementAnchor` | 3,246B | 1,246B | 1,120B |
|
|
49
|
+
| `measurePlacement` | 282B | 196B | 167B |
|
|
50
|
+
| `createSizeAdvertiser` | 1,350B | 780B | 680B |
|
|
51
|
+
| `decodeGeometryWireValue` | 1,472B | 664B | 570B |
|
|
52
|
+
| `createGeometrySequenceGuard` | 396B | 258B | 229B |
|
|
53
|
+
| `createGeometryBatcher` | 1,357B | 637B | 579B |
|
|
54
|
+
| `createPlacementMessagePublisher` | 200B | 165B | 135B |
|
|
55
|
+
| `createSizeMessagePublisher` | 185B | 162B | 129B |
|
|
56
|
+
| `useViewAnchor` | 2,116B | 984B | 885B |
|
|
57
|
+
| `usePlacementAnchor` | 4,421B | 1,718B | 1,549B |
|
|
58
|
+
|
|
59
|
+
## V8 与边界
|
|
60
|
+
|
|
61
|
+
运行 `pnpm benchmark:v8 > /tmp/view-anchor-v8.log 2>&1` 时,三个新的 benchmark 进程会继承 `--trace-opt`、`--trace-deopt` 与 `--trace-turbo-inlining`。本文没有保留无对应当前 trace 工件的历史优化/反优化结论。
|
|
62
|
+
|
|
63
|
+
当前测量不覆盖真实浏览器/Electron、DOM layout、structured clone、IPC 或生产工作负载;这些路径需在目标运行时另行测量。
|
package/docs/protocol.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# 通信协议
|
|
2
|
+
|
|
3
|
+
`view-anchor` 核心模块只负责测量并同步调用 `publish`。如果需要在进程或 iframe 边界上传输几何数据,可以使用可选的 `view-anchor/protocol` 模块。它提供带版本的消息结构、输入校验、消息防乱序和微任务批处理,不绑定具体的传输通道。
|
|
4
|
+
|
|
5
|
+
## 消息结构
|
|
6
|
+
|
|
7
|
+
协议消息包含以下字段:
|
|
8
|
+
|
|
9
|
+
- `v: 1`:协议版本,非支持版本直接拒绝。
|
|
10
|
+
- `kind`:消息类型,`placement` 或 `size`。
|
|
11
|
+
- `anchorId`:锚点的唯一逻辑标识符。
|
|
12
|
+
- `generation`:代次编号。锚点重建或需要完全重置序列时递增,用于丢弃上一代残留的迟到消息。
|
|
13
|
+
- `seq`:同一 publisher 内单调递增的序列号。
|
|
14
|
+
- `placement` 或 `size`:具体的位置或尺寸数据。
|
|
15
|
+
|
|
16
|
+
`generation` 作用于同一个 `anchorId`:一旦接收到新一代的消息,所有该锚点的旧代消息都会被直接丢弃。`generation` 与 `seq` 只负责保证消息顺序,不作为鉴权凭据。接收端依然应当在分发前验证发送方身份(如校验 `senderFrame`、域名或访问令牌)。
|
|
17
|
+
|
|
18
|
+
## 发送端
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import {
|
|
22
|
+
createGeometryBatcher,
|
|
23
|
+
createPlacementMessagePublisher,
|
|
24
|
+
} from 'view-anchor/protocol'
|
|
25
|
+
|
|
26
|
+
const batcher = createGeometryBatcher(
|
|
27
|
+
(batch) => ipc.send('geometry', batch),
|
|
28
|
+
{ onError: (err) => console.error(err) },
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
const publish = createPlacementMessagePublisher(
|
|
32
|
+
{ anchorId: 'editor', generation: 1 },
|
|
33
|
+
batcher.publish,
|
|
34
|
+
)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
使用 publisher 时需注意两点:
|
|
38
|
+
|
|
39
|
+
1. **每个 `{ anchorId, generation }` 保持单个 publisher 实例**。在 React 中应使用 `useMemo` 或 `useRef` 保存,不要在每次渲染时重新创建。接收端的 `createGeometrySequenceGuard` 会记录见过的最大序列号;如果在同一代次下重建 publisher,序列号会从 1 重新计数,导致新发出的消息被当成过期消息丢弃。确实需要重置时,请将 `generation` 加一。
|
|
40
|
+
2. **批处理与重试**。`createGeometryBatcher` 会在当前微任务中合并同一事件循环内的多次更新,每个锚点只保留最新的一条。如果下游发送失败或抛出异常,未成功发送的消息会保留在队列中,等待下次调用或显式 `flush()` 时重试。
|
|
41
|
+
|
|
42
|
+
当锚点销毁时,可以调用 `batcher.clear(anchorId)` 清理对应队列;调用 `dispose()` 会彻底停用批处理器。
|
|
43
|
+
|
|
44
|
+
## 接收端
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
createGeometrySequenceGuard,
|
|
49
|
+
decodeGeometryWireValue,
|
|
50
|
+
} from 'view-anchor/protocol'
|
|
51
|
+
|
|
52
|
+
const guard = createGeometrySequenceGuard()
|
|
53
|
+
const result = decodeGeometryWireValue(event.payload, { maxMessages: 100 })
|
|
54
|
+
|
|
55
|
+
if (result.ok) {
|
|
56
|
+
const messages = result.value.kind === 'batch'
|
|
57
|
+
? result.value.messages
|
|
58
|
+
: [result.value]
|
|
59
|
+
|
|
60
|
+
for (const message of messages) {
|
|
61
|
+
if (
|
|
62
|
+
isAuthorized(event.senderFrame, message.anchorId) &&
|
|
63
|
+
guard.accept(message)
|
|
64
|
+
) {
|
|
65
|
+
applyGeometry(message)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`decodeGeometryWireValue` 接收 `unknown` 类型的原始数据,执行严格的类型和边界检查(包括整数范围、非负尺寸、批次大小限制等),不会向外抛出异常。
|
|
72
|
+
|
|
73
|
+
## publish 回调的同步约定
|
|
74
|
+
|
|
75
|
+
所有 `Publisher<T>` 均为同步函数:
|
|
76
|
+
- 返回 `true` 或 `void`:表示数据已成功接收或已加入发送队列。
|
|
77
|
+
- 返回 `false`:表示当前未接收。核心会在下一次测量触发时重新尝试该值。
|
|
78
|
+
|
|
79
|
+
如果底层传输是异步的(例如异步 IPC 或网络请求),应当先在同步回调中将消息放入本地发送队列并返回 `true`,后续的重试由发送队列自行管理。
|
package/package.json
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "view-anchor",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "High-performance geometry bridge that keeps anything outside the DOM (an Electron WebContentsView, a native webview, a cross-origin iframe) aligned to a DOM element: synchronous, deduplicated updates, optional versioned message batching, React adapter. ~2.6 KB gzipped, framework-free core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"electron",
|
|
7
7
|
"webcontentsview",
|
|
8
|
+
"browserview",
|
|
9
|
+
"webview",
|
|
10
|
+
"native view",
|
|
11
|
+
"iframe",
|
|
12
|
+
"postmessage",
|
|
13
|
+
"ipc",
|
|
14
|
+
"dom",
|
|
15
|
+
"geometry",
|
|
16
|
+
"bounds",
|
|
8
17
|
"layout",
|
|
9
|
-
"
|
|
18
|
+
"overlay",
|
|
19
|
+
"resizeobserver",
|
|
20
|
+
"resize observer",
|
|
21
|
+
"react",
|
|
22
|
+
"react hook",
|
|
23
|
+
"typescript",
|
|
24
|
+
"tree-shakeable"
|
|
10
25
|
],
|
|
11
26
|
"type": "module",
|
|
12
27
|
"sideEffects": false,
|
|
@@ -29,6 +44,14 @@
|
|
|
29
44
|
".": {
|
|
30
45
|
"types": "./dist/index.d.ts",
|
|
31
46
|
"default": "./dist/index.js"
|
|
47
|
+
},
|
|
48
|
+
"./protocol": {
|
|
49
|
+
"types": "./dist/protocol.d.ts",
|
|
50
|
+
"default": "./dist/protocol.js"
|
|
51
|
+
},
|
|
52
|
+
"./react": {
|
|
53
|
+
"types": "./dist/react.d.ts",
|
|
54
|
+
"default": "./dist/react.js"
|
|
32
55
|
}
|
|
33
56
|
},
|
|
34
57
|
"files": [
|
|
@@ -78,6 +101,9 @@
|
|
|
78
101
|
"lint": "eslint . --max-warnings 0",
|
|
79
102
|
"test": "vitest run --reporter=default --reporter=json --outputFile.json=test-report.json --coverage.enabled --coverage.reporter=json-summary --coverage.reportsDirectory=coverage",
|
|
80
103
|
"test:dev": "vitest",
|
|
81
|
-
"test:coverage": "vitest run --coverage"
|
|
104
|
+
"test:coverage": "vitest run --coverage",
|
|
105
|
+
"benchmark": "node --expose-gc scripts/performance-report.mjs",
|
|
106
|
+
"benchmark:v8": "node --trace-opt --trace-deopt --trace-turbo-inlining --expose-gc scripts/performance-report.mjs",
|
|
107
|
+
"check-package": "node scripts/check-package.mjs"
|
|
82
108
|
}
|
|
83
109
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* view-anchor
|
|
3
|
-
* view (Electron `WebContentsView`) aligned to a DOM element's geometry.
|
|
4
|
-
*
|
|
5
|
-
* Public surface:
|
|
6
|
-
* - `createViewAnchor` — forward: DOM rect → native view bounds.
|
|
7
|
-
* - `useViewAnchor` — React adapter returning a ref callback.
|
|
8
|
-
* - `createSizeAdvertiser`— reverse: downstream content size → host.
|
|
9
|
-
* - `Bounds` / `AdvertisedSize` / option + handle types.
|
|
10
|
-
*
|
|
11
|
-
* Self-contained on purpose: the only runtime deps are `react` (adapter
|
|
12
|
-
* only) and browser APIs (`ResizeObserver` / `requestAnimationFrame` /
|
|
13
|
-
* `getBoundingClientRect`). See the design notes and the interactive 3D
|
|
14
|
-
* walkthrough in `docs/` (`mechanism.mdx` / `anchor-3d.html`).
|
|
2
|
+
* view-anchor: keeps an external surface aligned with a DOM element's geometry.
|
|
15
3
|
*/
|
|
4
|
+
|
|
16
5
|
export {
|
|
17
6
|
createViewAnchor,
|
|
18
7
|
measurePlacement,
|
|
@@ -25,15 +14,17 @@ export type {
|
|
|
25
14
|
export type {
|
|
26
15
|
Bounds,
|
|
27
16
|
Placement,
|
|
17
|
+
Publisher,
|
|
18
|
+
PublishResult,
|
|
28
19
|
ViewAnchorOptions,
|
|
29
20
|
ViewAnchorHandle,
|
|
30
21
|
} from './types.js'
|
|
31
|
-
export { useViewAnchor } from './react.js'
|
|
32
|
-
export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js'
|
|
33
22
|
export { createSizeAdvertiser } from './size-advertiser.js'
|
|
23
|
+
export { useViewAnchor } from './react.js'
|
|
34
24
|
export type {
|
|
35
25
|
AdvertisedAxis,
|
|
36
26
|
AdvertisedSize,
|
|
37
27
|
SizeAdvertiserOptions,
|
|
38
28
|
SizeAdvertiserHandle,
|
|
39
29
|
} from './types.js'
|
|
30
|
+
export type { UseViewAnchorOptions, ViewAnchorRef } from './react.js'
|
package/src/measure-loop.ts
CHANGED
|
@@ -1,30 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Internal
|
|
3
|
-
*
|
|
2
|
+
* Internal helper: animation-frame scheduling and deduplication loop for
|
|
3
|
+
* `createSizeAdvertiser`.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* frame late, and a RAF stacked a second frame of visible trailing). The
|
|
8
|
-
* reverse direction is different — it is a cross-process FEEDBACK loop
|
|
9
|
-
* (advertise → host resizes the view → content re-measures → re-advertise), so
|
|
10
|
-
* the RAF's one-publish-per-frame coalescing is a useful damper. The two
|
|
11
|
-
* directions thus have different optimal emit timing; this engine serves only
|
|
12
|
-
* the reverse.
|
|
13
|
-
*
|
|
14
|
-
* NOT exported from the package: it is pure mechanism with no knowledge of
|
|
15
|
-
* direction, the DOM, `ResizeObserver`, or the structure of the value `T` it
|
|
16
|
-
* carries. The wrapping primitive injects `produce` / `same` / `sink` and
|
|
17
|
-
* drives the lifecycle.
|
|
18
|
-
*
|
|
19
|
-
* - `schedule()` — coalesce a burst of triggers into ONE RAF; the frame
|
|
20
|
-
* body re-`produce()`s, dedupes against the last emit (`same`), and `sink`s.
|
|
21
|
-
* Bails if inactive or disposed (stale-RAF safe).
|
|
22
|
-
* - `emitNow(v)` — explicit synchronous emit (create / update path). Always
|
|
23
|
-
* fires, bypassing the dedupe check, and refreshes the dedupe baseline.
|
|
24
|
-
* - `setActive` — gate the observer stream; a queued frame bails on `!active`.
|
|
25
|
-
* - `cancel` — drop any in-flight RAF.
|
|
26
|
-
* - `dispose` — cancel + go inert; after dispose nothing emits again.
|
|
5
|
+
* Coalesces resize triggers into a single requestAnimationFrame, drops
|
|
6
|
+
* duplicate measurements, and manages disposal.
|
|
27
7
|
*/
|
|
8
|
+
|
|
28
9
|
export interface MeasureLoop<T> {
|
|
29
10
|
schedule(): void
|
|
30
11
|
emitNow(value: T): void
|
|
@@ -34,18 +15,34 @@ export interface MeasureLoop<T> {
|
|
|
34
15
|
}
|
|
35
16
|
|
|
36
17
|
export function createMeasureLoop<T>(cfg: {
|
|
37
|
-
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
18
|
+
/**
|
|
19
|
+
* Produce the value to emit in the animation frame. Return null to skip
|
|
20
|
+
* the frame (e.g. for non-finite measurements).
|
|
21
|
+
*/
|
|
40
22
|
produce: () => T | null
|
|
41
23
|
same: (a: T, b: T) => boolean
|
|
42
|
-
sink: (
|
|
24
|
+
sink: import('./types.js').Publisher<T>
|
|
43
25
|
}): MeasureLoop<T> {
|
|
44
26
|
const { produce, same, sink } = cfg
|
|
45
27
|
let rafId: number | null = null
|
|
46
28
|
let active = false
|
|
47
29
|
let disposed = false
|
|
48
30
|
let last: T | null = null
|
|
31
|
+
let publicationRevision = 0
|
|
32
|
+
|
|
33
|
+
const deliver = (value: T): boolean => {
|
|
34
|
+
const previous = last
|
|
35
|
+
const attempt = ++publicationRevision
|
|
36
|
+
last = value
|
|
37
|
+
try {
|
|
38
|
+
const accepted = sink(value) !== false
|
|
39
|
+
if (!accepted && publicationRevision === attempt) last = previous
|
|
40
|
+
return accepted
|
|
41
|
+
} catch (error) {
|
|
42
|
+
if (publicationRevision === attempt) last = previous
|
|
43
|
+
throw error
|
|
44
|
+
}
|
|
45
|
+
}
|
|
49
46
|
|
|
50
47
|
const cancel = (): void => {
|
|
51
48
|
if (rafId !== null) {
|
|
@@ -54,25 +51,23 @@ export function createMeasureLoop<T>(cfg: {
|
|
|
54
51
|
}
|
|
55
52
|
}
|
|
56
53
|
|
|
54
|
+
const frame = (): void => {
|
|
55
|
+
rafId = null
|
|
56
|
+
if (disposed || !active) return
|
|
57
|
+
const value = produce()
|
|
58
|
+
if (value === null) return
|
|
59
|
+
if (last !== null && same(value, last)) return
|
|
60
|
+
deliver(value)
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
return {
|
|
58
64
|
schedule(): void {
|
|
59
65
|
if (disposed || !active || rafId !== null) return
|
|
60
|
-
rafId = requestAnimationFrame(
|
|
61
|
-
rafId = null
|
|
62
|
-
if (disposed || !active) return
|
|
63
|
-
const value = produce()
|
|
64
|
-
if (value === null) return // producer declined this frame
|
|
65
|
-
// last-value dedupe: a frame whose produced value equals the last one
|
|
66
|
-
// we emitted costs nothing (no IPC / setBounds).
|
|
67
|
-
if (last !== null && same(value, last)) return
|
|
68
|
-
last = value
|
|
69
|
-
sink(value)
|
|
70
|
-
})
|
|
66
|
+
rafId = requestAnimationFrame(frame)
|
|
71
67
|
},
|
|
72
68
|
emitNow(value: T): void {
|
|
73
69
|
if (disposed) return
|
|
74
|
-
|
|
75
|
-
sink(value)
|
|
70
|
+
deliver(value)
|
|
76
71
|
},
|
|
77
72
|
setActive(on: boolean): void {
|
|
78
73
|
active = on
|