view-anchor 0.1.2 → 0.2.1
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 +111 -39
- package/README.zh-CN.md +119 -47
- package/dist/index.d.ts +6 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -15
- package/dist/measure-loop.d.ts +9 -28
- package/dist/measure-loop.d.ts.map +1 -1
- package/dist/measure-loop.js +57 -17
- package/dist/protocol-publisher.d.ts +41 -0
- package/dist/protocol-publisher.d.ts.map +1 -0
- package/dist/protocol-publisher.js +207 -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 +128 -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 +29 -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 +230 -181
- package/docs/bidirectional-design.md +78 -106
- package/docs/index.html +772 -0
- package/docs/mechanism.md +116 -0
- package/docs/performance-report.md +63 -0
- package/docs/protocol.md +108 -0
- package/package.json +37 -14
- package/src/index.ts +8 -24
- package/src/measure-loop.ts +56 -42
- package/src/protocol-publisher.ts +254 -0
- package/src/protocol-types.ts +43 -0
- package/src/protocol.ts +181 -0
- package/src/react.ts +175 -139
- package/src/size-advertiser.ts +33 -31
- package/src/types.ts +35 -82
- package/src/view-anchor.ts +259 -236
- package/docs/anchor-3d.html +0 -615
- package/docs/mechanism.mdx +0 -119
|
@@ -1,140 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 双向几何设计
|
|
2
2
|
|
|
3
|
-
view-anchor
|
|
3
|
+
view-anchor 支持双向几何同步:
|
|
4
4
|
|
|
5
|
-
-
|
|
6
|
-
-
|
|
5
|
+
- **正向(`createViewAnchor`)**:宿主测量 DOM 占位元素的位置和尺寸,通过 `publish(bounds)` 发送给主进程或外部容器,更新原生视图(如 `WebContentsView.setBounds`)。
|
|
6
|
+
- **反向(`createSizeAdvertiser`)**:下游视图内部测量自身内容尺寸,通过 `publish(size)` 通知宿主调整占位大小,宿主再通过正向更新视图位置。
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
常见场景:嵌套在宿主中的工具栏或面板,其宽度由宿主布局决定,高度则由子视图自身的内容决定。
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
## 1. 为什么正向走同步、反向走 RAF
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
两个方向在性能和交互上的要求不同,因此没有共用同一套调度逻辑:
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
- **正向(`createViewAnchor`)采用同步发布。**
|
|
15
|
+
原生视图的 `setBounds` 需要跨进程通信,相比渲染进程本身的页面绘制通常已经有大约一帧的延迟。如果测量和发布再走一次 `requestAnimationFrame`,拖拽时就会产生两帧以上的视觉延迟,出现明显的边框脱节。因此正向在 `ResizeObserver` 和窗口 `resize` 回调中**同步测量并发布**,高频触发的防抖则依赖前后数值的比对(相同矩形直接跳过)。
|
|
16
|
+
- **反向(`createSizeAdvertiser`)采用 RAF 调度(`createMeasureLoop`)。**
|
|
17
|
+
反向构成了一条跨进程的反馈环:下游上报尺寸 → 宿主调整占位大小 → 下游重新布局与测量 → 再次上报。在这个链路中,将上报频率限制在每帧至多一次(与屏幕刷新率对齐)能够有效避免高频震荡,同时提供平滑的缓冲。
|
|
15
18
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
**反向 `createSizeAdvertiser` = RAF 合并(内部 `createMeasureLoop`,反向专用)。** 反向是一条**跨进程反馈环**:advertise → 宿主 resize 这块视图 → 下游内容 remeasure → 再 advertise。RAF 的「每帧≤1 次发布」对这条环是合理的阻尼。`createMeasureLoop<T>` 只服务反向(正向不用它),通过注入 `produce`/`same`/`sink` 三元组把方向细节关在外层,核心零 flag:
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
function createMeasureLoop<T>(cfg: {
|
|
22
|
-
produce: () => T | null // RAF 体内取值:读最近一帧 borderBoxSize(null=跳过帧)
|
|
23
|
-
same: (a: T, b: T) => boolean // 去重谓词(反向 = extent 相等)
|
|
24
|
-
sink: (value: T) => void // = 注入的 publish
|
|
25
|
-
}): { schedule, emitNow, setActive, cancel, dispose }
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
**为什么不对称是对的**:正向是单向跟随者,少一帧直接消除可见拖尾;反向是反馈环,多一帧阻尼更稳。把两者塞进一个核心要么逼出 `if(direction)` flag,要么把正向也拖进 RAF(带回拖尾)。所以**正向内联同步、反向用 RAF 引擎**,各取所需。
|
|
29
|
-
|
|
30
|
-
## 2. 反向原语契约
|
|
19
|
+
## 2. 反向接口说明
|
|
31
20
|
|
|
32
21
|
```ts
|
|
33
22
|
export type AdvertisedAxis = 'block' | 'inline'
|
|
34
23
|
|
|
35
|
-
// 帧载荷:纯标量,连「哪条轴」都无从携带 → 单轴在类型层不可拼写。
|
|
36
24
|
export interface AdvertisedSize {
|
|
37
|
-
readonly axis: AdvertisedAxis //
|
|
38
|
-
readonly extent: number //
|
|
25
|
+
readonly axis: AdvertisedAxis // 固定轴,供宿主做白名单检查
|
|
26
|
+
readonly extent: number // 内容尺寸(CSS 像素),四舍五入且 >= 0
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
export interface SizeAdvertiserOptions {
|
|
42
|
-
axis: AdvertisedAxis
|
|
43
|
-
publish:
|
|
30
|
+
axis: AdvertisedAxis // 创建后固定,每个 advertiser 只负责一条轴
|
|
31
|
+
publish: Publisher<AdvertisedSize> // 接收尺寸发布的回调
|
|
44
32
|
}
|
|
45
33
|
|
|
46
34
|
export interface SizeAdvertiserHandle {
|
|
47
|
-
update(publish:
|
|
48
|
-
dispose(): void
|
|
35
|
+
update(publish: Publisher<AdvertisedSize>): void // 切换 publish 回调
|
|
36
|
+
dispose(): void // 停止监听并取消 RAF
|
|
49
37
|
}
|
|
50
38
|
|
|
51
|
-
export function createSizeAdvertiser(
|
|
39
|
+
export function createSizeAdvertiser(
|
|
40
|
+
target: HTMLElement,
|
|
41
|
+
opts: SizeAdvertiserOptions,
|
|
42
|
+
): SizeAdvertiserHandle
|
|
52
43
|
```
|
|
53
44
|
|
|
54
|
-
-
|
|
55
|
-
- `
|
|
56
|
-
-
|
|
57
|
-
-
|
|
45
|
+
- 运行在下游视图的渲染环境中;从 `ResizeObserverEntry.borderBoxSize` 直接读取尺寸,避免在回调中触发 `getBoundingClientRect` 引起强制回流(reflow)。
|
|
46
|
+
- 上报前会执行 `Math.round` 取整并钳位至 `>= 0`。
|
|
47
|
+
- 目标元素 `target` 应当在所负责的轴上根据内容自适应(shrink-to-fit),不能被宿主设置的尺寸反向影响。
|
|
48
|
+
- 如果需要同时汇报宽和高,应当针对不同轴分别创建两个独立的 advertiser,而不是放在同一条消息中,以保持数据流向的清晰。
|
|
58
49
|
|
|
59
|
-
## 3.
|
|
50
|
+
## 3. 单轴控制与收敛性
|
|
60
51
|
|
|
61
|
-
|
|
62
|
-
- 为什么收敛:宽 = 宿主输入(无回边);高 = block layout 的**输出**而非输入(无回边)→ 整条尺寸传播是单向 DAG,**一步收敛**。违反单轴 = 跨进程双 RAF 乒乓,是抖动/极限环根因。去环靠**拓扑**(单轴 + 类型不可拼写),不靠 epsilon 死区/低通滤波。
|
|
52
|
+
为了避免死循环,必须遵循单轴控制原则:
|
|
63
53
|
|
|
64
|
-
|
|
54
|
+
- 一个 advertiser 只测量并上报它负责的那条轴;另一条轴由宿主通过 `setBounds` 单向传入,下游只读。
|
|
55
|
+
- 典型案例:宿主决定宽度,下游决定高度。因为高度是内容流式排版的结果而不是输入,整个尺寸传递是一条单向有向无环图(DAG),更新可以在单步内收敛。
|
|
56
|
+
- 如果下游的高度又反过来改变了下游的宽度(或测量了 `<body>`/`<html>`),就会形成跨进程的循环调整,导致界面抖动。
|
|
65
57
|
|
|
66
|
-
|
|
58
|
+
## 4. 职责与信任边界
|
|
67
59
|
|
|
68
|
-
|
|
60
|
+
下游视图可能运行不可信或第三方代码,因此上报的尺寸应当被当作不可信输入处理。
|
|
69
61
|
|
|
70
|
-
|
|
|
62
|
+
| 职责 | 负责方 | 说明 |
|
|
71
63
|
|---|---|---|
|
|
72
|
-
|
|
|
73
|
-
|
|
|
74
|
-
|
|
|
75
|
-
|
|
|
76
|
-
|
|
|
77
|
-
|
|
|
78
|
-
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
64
|
+
| 数值取整 | view-anchor | 避免传递非整数像素 |
|
|
65
|
+
| 过滤 NaN / Infinity | view-anchor | 丢弃异常无效数值 |
|
|
66
|
+
| 负数归零 | view-anchor | 保证尺寸非负,反映真实测量结果 |
|
|
67
|
+
| 视口限制(clamp) | 宿主 | 依据当前窗口可用空间对上报值做范围约束,防止异常大值 |
|
|
68
|
+
| 发送方身份校验 | 宿主 | 在接收 IPC 或 postMessage 时验证 senderFrame、origin 或 token |
|
|
69
|
+
| 轴白名单校验 | 宿主 | 检查 `axis` 是否与宿主预期的控制轴一致 |
|
|
70
|
+
| 位置与层级锁定 | 宿主 | 下游不能擅自修改自身的坐标位置或 z-index |
|
|
71
|
+
|
|
72
|
+
**针对错误选择 target 的提醒**:
|
|
73
|
+
如果把 `target` 设为 `document.body` 或 `documentElement`,其尺寸直接等于宿主给定的视口大小,会导致无法正确收缩。对此 `createSizeAdvertiser` 在初始化时提供了一次性控制台警告,帮助快速排查问题。
|
|
74
|
+
|
|
75
|
+
## 5. 宿主与下游的配合方式
|
|
76
|
+
|
|
77
|
+
正向与反向并不直接通信,它们通过宿主中的**占位 DOM 元素**进行连接:
|
|
78
|
+
|
|
79
|
+
- 下游的 `createSizeAdvertiser` 将内容尺寸通知宿主,宿主更新占位元素的高度。
|
|
80
|
+
- 宿主的 `createViewAnchor` 监听占位元素的矩形变化,将新位置同步给外部视图。
|
|
81
|
+
|
|
82
|
+
```mermaid
|
|
83
|
+
flowchart LR
|
|
84
|
+
subgraph DOWN["下游渲染进程(如工具栏页面)"]
|
|
85
|
+
C["内容容器<br/>高度由自身内容决定"]
|
|
86
|
+
end
|
|
87
|
+
subgraph HOST["宿主渲染进程"]
|
|
88
|
+
H["宿主消息处理器<br/>校验来源、clamp 数值"]
|
|
89
|
+
DIV["占位 div"]
|
|
90
|
+
VA["createViewAnchor"]
|
|
91
|
+
end
|
|
92
|
+
subgraph MAIN["宿主主进程"]
|
|
93
|
+
NV["WebContentsView / 原生视图"]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
C -->|"① publish(size)"| H
|
|
97
|
+
H -->|"② clamp 后写入 style.height"| DIV
|
|
98
|
+
DIV -->|"ResizeObserver 观测"| VA
|
|
99
|
+
VA -->|"③ 测出新矩形,publish(bounds)"| NV
|
|
100
|
+
NV -->|"④ setBounds 后下游视口变化,内容重排"| C
|
|
98
101
|
```
|
|
99
|
-
宿主渲染进程 宿主主进程 下游渲染进程(toolbar 自己的页面)
|
|
100
|
-
────────── ──────── ────────────────────────────
|
|
101
|
-
[占位 div] [内容 wrapper(shrink-to-fit)]
|
|
102
|
-
│ ▲ │
|
|
103
|
-
│ │ ② decide 把高写进占位 div │ ① createSizeAdvertiser
|
|
104
|
-
│ │ div.style.height = clamp(size.extent) │ 量 wrapper 的 block-size
|
|
105
|
-
│ └─────────── IPC ◀─────────────────────────────────────┘ publish(size) ──▶
|
|
106
|
-
│
|
|
107
|
-
│ ③ 占位 div 尺寸变 → createViewAnchor 的 ResizeObserver 触发
|
|
108
|
-
│ 量占位新矩形 → publish(bounds)
|
|
109
|
-
▼
|
|
110
|
-
──── IPC ──▶ ④ view.setBounds(bounds) ──▶ [WebContentsView(这块 toolbar)]
|
|
111
|
-
│ 视图变 → 下游 viewport 变 → 内容重新布局
|
|
112
|
-
└──▶ 回到 ① advertiser 再量(收敛)
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
1. **下游**:`createSizeAdvertiser(wrapper, { axis:'block', publish })` 量内容高 → IPC 发回宿主。
|
|
116
|
-
2. **宿主**:纯函数 `decide(size, { axis, min, max, available })` 夹一夹,把结果写进**占位 div 的高**(`div.style.height = …`)。
|
|
117
|
-
3. **宿主**:占位 div 高一变,`createViewAnchor` 挂在它上的 `ResizeObserver` 立刻触发 → 量出占位新矩形(宽 = 宿主布局给的满宽,高 = 刚写进去的内容高)→ `publish(bounds)`。
|
|
118
|
-
4. **宿主主进程**:`view.setBounds(bounds)` → toolbar 这块 `WebContentsView` 变成新尺寸 → 下游 viewport 变 → 内容重新布局 → 回到 ①。
|
|
119
|
-
|
|
120
|
-
### 谁负责什么
|
|
121
|
-
|
|
122
|
-
| 角色 | 谁提供 | 在 view-anchor 里? |
|
|
123
|
-
|---|---|---|
|
|
124
|
-
| `createSizeAdvertiser`(量内容 → 发 size) | view-anchor | ✅ |
|
|
125
|
-
| `createViewAnchor`(量占位 → 发 bounds) | view-anchor | ✅ |
|
|
126
|
-
| `decide`(把 size 写进占位 div 的高)+ 两条 IPC 通道 | **宿主(workbench)** | ❌(刻意,属宿主胶水) |
|
|
127
|
-
| 占位 div、shrink-to-fit wrapper | 调用方的 DOM | ❌ |
|
|
128
|
-
|
|
129
|
-
- 收敛逻辑收进宿主一个**纯函数** `decide(...)`(可单测、无跨进程协议)。`decide` 住宿主侧,**不进 view-anchor**——包只出两个单向原语,不出收敛策略,否则就从原语滑向框架(§7)。
|
|
130
|
-
- 连续交互(拖窗改宽)走宿主**单向**路径,不进闭环;反向只服务下游内容的**离散**尺寸变化。
|
|
131
|
-
|
|
132
|
-
## 6. 与 Electron preferred-size 的关系(宿主侧可选数据源,非替代)
|
|
133
102
|
|
|
134
|
-
|
|
103
|
+
1. **下游**:通过 `createSizeAdvertiser` 测量高度并通过 IPC 发给宿主。
|
|
104
|
+
2. **宿主**:对收到的高度做合规性限制(例如限制在 `minHeight` 和 `maxHeight` 之间),并写入占位 div 的样式。
|
|
105
|
+
3. **宿主**:占位 div 尺寸改变,`createViewAnchor` 的 `ResizeObserver` 触发,测量出新的绝对矩形并发给主进程。
|
|
106
|
+
4. **宿主主进程**:调用 `setBounds` 更新原生视图位置与尺寸。
|
|
135
107
|
|
|
136
|
-
|
|
108
|
+
## 6. 与 Electron preferred-size 的关系
|
|
137
109
|
|
|
138
|
-
|
|
110
|
+
在纯 Electron 环境下,也可以使用 `enablePreferredSizeMode` 和 `preferred-size-changed` 事件由 Electron 主进程自动获取网页期望大小。
|
|
139
111
|
|
|
140
|
-
view-anchor
|
|
112
|
+
view-anchor 的反向方案是平台无关的实现,适用于跨域 iframe、第三方 webview 或需要针对特定内部 DOM 节点测量尺寸的场景。两者并不冲突,可以根据具体的宿主架构按需选择。
|