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
|
@@ -1,140 +1,108 @@
|
|
|
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
|
-
| 位置 / 另一轴 / z-order 锁定 | 宿主 | 下游无任何途径改变 → 杜绝全窗覆盖类点击劫持 |
|
|
80
|
-
|
|
81
|
-
**target 选错(反馈环)的防护——克制为主,不过度防御。** 关键事实:违反单轴所有权时,RAF 合并把「死循环」**封顶为每帧一次**——它退化成可见的抖动 / 每帧一次冗余 IPC,而**不是冻死 UI**。既然不是灾难性故障,就不在每帧路径上堆检测器。只保留:
|
|
82
|
-
|
|
83
|
-
- **一条构造期廉价守卫**:`target` 是 `<body>`/`<html>` 时 `console.warn`——这是「stable-but-wrong」(占位永远撑成视图高、不缩到内容)最常见的成因,一次性、零每帧成本。
|
|
84
|
-
- **其余靠文档**:JSDoc 与本节把「target 必须主导轴 shrink-to-fit、不被宿主灌入的尺寸反向决定」讲清。
|
|
85
|
-
- **明确不做**:每帧的 2-cycle / 单调发散检测器(复杂度与「非灾难性、RAF 封顶」的故障不成正比)、运行时熔断(会把可见症状盖成静默失效)、向宿主回传灌入值做对比(会亲手造出 §3 要消灭的反馈边)。发散的真正兜底是宿主侧 `clamp(max)`。
|
|
64
|
+
| 数值取整 | view-anchor | 避免传递非整数像素 |
|
|
65
|
+
| 过滤 NaN / Infinity | view-anchor | 丢弃异常无效数值 |
|
|
66
|
+
| 负数归零 | view-anchor | 保证尺寸非负,反映真实测量结果 |
|
|
67
|
+
| 视口限制(clamp) | 宿主 | 依据当前窗口可用空间对上报值做范围约束,防止异常大值 |
|
|
68
|
+
| 发送方身份校验 | 宿主 | 在接收 IPC 或 postMessage 时验证 senderFrame、origin 或 token |
|
|
69
|
+
| 轴白名单校验 | 宿主 | 检查 `axis` 是否与宿主预期的控制轴一致 |
|
|
70
|
+
| 位置与层级锁定 | 宿主 | 下游不能擅自修改自身的坐标位置或 z-index |
|
|
86
71
|
|
|
87
|
-
|
|
72
|
+
**针对错误选择 target 的提醒**:
|
|
73
|
+
如果把 `target` 设为 `document.body` 或 `documentElement`,其尺寸直接等于宿主给定的视口大小,会导致无法正确收缩。对此 `createSizeAdvertiser` 在初始化时提供了一次性控制台警告,帮助快速排查问题。
|
|
88
74
|
|
|
89
|
-
|
|
75
|
+
## 5. 宿主与下游的配合方式
|
|
90
76
|
|
|
91
|
-
|
|
92
|
-
- **`createViewAnchor` 读**占位 div 的矩形——占位在哪、多大,原生视图就贴哪、多大。
|
|
77
|
+
正向与反向并不直接通信,它们通过宿主中的**占位 DOM 元素**进行连接:
|
|
93
78
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
### 闭环(以 toolbar 为例)
|
|
79
|
+
- 下游的 `createSizeAdvertiser` 将内容尺寸通知宿主,宿主更新占位元素的高度。
|
|
80
|
+
- 宿主的 `createViewAnchor` 监听占位元素的矩形变化,将新位置同步给外部视图。
|
|
97
81
|
|
|
98
82
|
```
|
|
99
|
-
宿主渲染进程 宿主主进程
|
|
83
|
+
宿主渲染进程 宿主主进程 下游渲染进程(如工具栏页面)
|
|
100
84
|
────────── ──────── ────────────────────────────
|
|
101
|
-
[占位 div] [
|
|
85
|
+
[占位 div] [内容容器(自适应高度)]
|
|
102
86
|
│ ▲ │
|
|
103
|
-
│ │ ②
|
|
104
|
-
│ │ div.style.height = clamp(size.extent) │
|
|
105
|
-
│ └─────────── IPC
|
|
87
|
+
│ │ ② 宿主校验并更新占位高度 │ ① createSizeAdvertiser
|
|
88
|
+
│ │ div.style.height = clamp(size.extent) │ 测量容器高度
|
|
89
|
+
│ └─────────── IPC / postMessage ◀───────────────────────┘ publish(size) ──▶
|
|
106
90
|
│
|
|
107
|
-
│ ③
|
|
108
|
-
│
|
|
91
|
+
│ ③ 占位尺寸变化,ResizeObserver 触发
|
|
92
|
+
│ 测量占位新矩形 → publish(bounds)
|
|
109
93
|
▼
|
|
110
|
-
──── IPC ──▶ ④ view.setBounds(bounds) ──▶ [WebContentsView
|
|
111
|
-
│
|
|
112
|
-
└──▶
|
|
94
|
+
──── IPC ──▶ ④ view.setBounds(bounds) ──▶ [WebContentsView / 原生视图]
|
|
95
|
+
│ 视图尺寸更新,下游视口变化
|
|
96
|
+
└──▶ 下游内容重新排版(单步收敛)
|
|
113
97
|
```
|
|
114
98
|
|
|
115
|
-
1.
|
|
116
|
-
2.
|
|
117
|
-
3. **宿主**:占位 div
|
|
118
|
-
4.
|
|
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
|
-
|
|
134
|
-
宿主若在 Electron 且能接受零下游代码,可用 `enablePreferredSizeMode` + `preferred-size-changed` 作为反向「**源**」喂给同一个 `decide`,省掉下游注入。但它是 Electron 私有,且依赖两个前提:① 在 `WebContentsView` 上触发且上报值吸收 zoom;② `setBounds.height` 不回灌 layout(否则正反馈震荡)。
|
|
99
|
+
1. **下游**:通过 `createSizeAdvertiser` 测量高度并通过 IPC 发给宿主。
|
|
100
|
+
2. **宿主**:对收到的高度做合规性限制(例如限制在 `minHeight` 和 `maxHeight` 之间),并写入占位 div 的样式。
|
|
101
|
+
3. **宿主**:占位 div 尺寸改变,`createViewAnchor` 的 `ResizeObserver` 触发,测量出新的绝对矩形并发给主进程。
|
|
102
|
+
4. **宿主主进程**:调用 `setBounds` 更新原生视图位置与尺寸。
|
|
135
103
|
|
|
136
|
-
|
|
104
|
+
## 6. 与 Electron preferred-size 的关系
|
|
137
105
|
|
|
138
|
-
|
|
106
|
+
在纯 Electron 环境下,也可以使用 `enablePreferredSizeMode` 和 `preferred-size-changed` 事件由 Electron 主进程自动获取网页期望大小。
|
|
139
107
|
|
|
140
|
-
view-anchor
|
|
108
|
+
view-anchor 的反向方案是平台无关的实现,适用于跨域 iframe、第三方 webview 或需要针对特定内部 DOM 节点测量尺寸的场景。两者并不冲突,可以根据具体的宿主架构按需选择。
|