@wsxjs/wsx-core 0.0.19 → 0.0.21
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/dist/chunk-AR3DIDLV.mjs +906 -0
- package/dist/index.js +837 -98
- package/dist/index.mjs +137 -17
- package/dist/jsx-runtime.js +707 -86
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +707 -86
- package/dist/jsx.mjs +1 -1
- package/package.json +2 -2
- package/src/base-component.ts +15 -0
- package/src/dom-cache-manager.ts +135 -0
- package/src/jsx-factory.ts +73 -190
- package/src/light-component.ts +3 -2
- package/src/reactive-decorator.ts +9 -0
- package/src/render-context.ts +40 -0
- package/src/utils/cache-key.ts +114 -0
- package/src/utils/dom-utils.ts +119 -0
- package/src/utils/element-creation.ts +140 -0
- package/src/utils/element-marking.ts +80 -0
- package/src/utils/element-update.ts +377 -0
- package/src/utils/props-utils.ts +307 -0
- package/src/web-component.ts +2 -1
- package/dist/chunk-UH5BDYGI.mjs +0 -283
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Element Update Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for fine-grained DOM updates (RFC 0037 Phase 4).
|
|
5
|
+
* These functions update only changed props and children, avoiding full element recreation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { shouldUseSVGNamespace, getSVGAttributeName } from "./svg-utils";
|
|
9
|
+
import { flattenChildren, type JSXChildren } from "./dom-utils";
|
|
10
|
+
import { setSmartProperty, isFrameworkInternalProp } from "./props-utils";
|
|
11
|
+
import { shouldPreserveElement } from "./element-marking";
|
|
12
|
+
import type { DOMCacheManager } from "../dom-cache-manager";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Removes a property from an element.
|
|
16
|
+
*/
|
|
17
|
+
function removeProp(
|
|
18
|
+
element: HTMLElement | SVGElement,
|
|
19
|
+
key: string,
|
|
20
|
+
oldValue: unknown,
|
|
21
|
+
tag: string
|
|
22
|
+
): void {
|
|
23
|
+
const isSVG = shouldUseSVGNamespace(tag);
|
|
24
|
+
|
|
25
|
+
// 处理特殊属性
|
|
26
|
+
if (key === "ref") {
|
|
27
|
+
// ref 是回调,不需要移除
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (key === "className" || key === "class") {
|
|
32
|
+
if (isSVG) {
|
|
33
|
+
element.removeAttribute("class");
|
|
34
|
+
} else {
|
|
35
|
+
(element as HTMLElement).className = "";
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key === "style") {
|
|
41
|
+
element.removeAttribute("style");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (key.startsWith("on") && typeof oldValue === "function") {
|
|
46
|
+
// 事件监听器:需要移除(但无法获取原始监听器,所以跳过)
|
|
47
|
+
// 注意:这可能导致内存泄漏,但在实际使用中,事件监听器通常不会变化
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (key === "value") {
|
|
52
|
+
if (
|
|
53
|
+
element instanceof HTMLInputElement ||
|
|
54
|
+
element instanceof HTMLTextAreaElement ||
|
|
55
|
+
element instanceof HTMLSelectElement
|
|
56
|
+
) {
|
|
57
|
+
element.value = "";
|
|
58
|
+
} else {
|
|
59
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
60
|
+
element.removeAttribute(attributeName);
|
|
61
|
+
}
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 过滤框架内部属性(不应该从 DOM 移除,因为它们本来就不应该存在)
|
|
66
|
+
if (isFrameworkInternalProp(key)) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 移除其他属性
|
|
71
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
72
|
+
element.removeAttribute(attributeName);
|
|
73
|
+
|
|
74
|
+
// 尝试移除 JavaScript 属性
|
|
75
|
+
try {
|
|
76
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
|
+
delete (element as any)[key];
|
|
78
|
+
} catch {
|
|
79
|
+
// 忽略删除失败
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Applies a single prop to an element (same logic as element-creation.ts).
|
|
85
|
+
*/
|
|
86
|
+
function applySingleProp(
|
|
87
|
+
element: HTMLElement | SVGElement,
|
|
88
|
+
key: string,
|
|
89
|
+
value: unknown,
|
|
90
|
+
tag: string,
|
|
91
|
+
isSVG: boolean
|
|
92
|
+
): void {
|
|
93
|
+
if (value === null || value === undefined || value === false) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 处理ref回调
|
|
98
|
+
if (key === "ref" && typeof value === "function") {
|
|
99
|
+
value(element);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 处理className和class
|
|
104
|
+
if (key === "className" || key === "class") {
|
|
105
|
+
if (isSVG) {
|
|
106
|
+
element.setAttribute("class", value as string);
|
|
107
|
+
} else {
|
|
108
|
+
(element as HTMLElement).className = value as string;
|
|
109
|
+
}
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 处理style
|
|
114
|
+
if (key === "style" && typeof value === "string") {
|
|
115
|
+
element.setAttribute("style", value);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 处理事件监听器
|
|
120
|
+
if (key.startsWith("on") && typeof value === "function") {
|
|
121
|
+
const eventName = key.slice(2).toLowerCase();
|
|
122
|
+
// 注意:这里会重复添加事件监听器,但这是预期的行为
|
|
123
|
+
// 在实际使用中,事件监听器通常不会频繁变化
|
|
124
|
+
element.addEventListener(eventName, value as EventListener);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 处理布尔属性
|
|
129
|
+
if (typeof value === "boolean") {
|
|
130
|
+
if (value) {
|
|
131
|
+
element.setAttribute(key, "");
|
|
132
|
+
}
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 特殊处理 input/textarea/select 的 value 属性
|
|
137
|
+
if (key === "value") {
|
|
138
|
+
if (
|
|
139
|
+
element instanceof HTMLInputElement ||
|
|
140
|
+
element instanceof HTMLTextAreaElement ||
|
|
141
|
+
element instanceof HTMLSelectElement
|
|
142
|
+
) {
|
|
143
|
+
element.value = String(value);
|
|
144
|
+
} else {
|
|
145
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
146
|
+
element.setAttribute(attributeName, String(value));
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// 过滤框架内部属性(不应该渲染到 DOM)
|
|
152
|
+
if (isFrameworkInternalProp(key)) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 处理其他属性 - 使用智能属性设置函数
|
|
157
|
+
setSmartProperty(element, key, value, tag);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Updates element props by comparing old and new props.
|
|
162
|
+
* Only updates changed properties.
|
|
163
|
+
*/
|
|
164
|
+
export function updateProps(
|
|
165
|
+
element: HTMLElement | SVGElement,
|
|
166
|
+
oldProps: Record<string, unknown> | null | undefined,
|
|
167
|
+
newProps: Record<string, unknown> | null | undefined,
|
|
168
|
+
tag: string
|
|
169
|
+
): void {
|
|
170
|
+
const isSVG = shouldUseSVGNamespace(tag);
|
|
171
|
+
const old = oldProps || {};
|
|
172
|
+
const new_ = newProps || {};
|
|
173
|
+
|
|
174
|
+
// 移除旧属性(在新 props 中不存在的)
|
|
175
|
+
for (const key in old) {
|
|
176
|
+
if (!(key in new_)) {
|
|
177
|
+
removeProp(element, key, old[key], tag);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// 添加/更新新属性(值发生变化的)
|
|
182
|
+
// 优化:跳过相同值的属性,减少不必要的 DOM 操作
|
|
183
|
+
for (const key in new_) {
|
|
184
|
+
const oldValue = old[key];
|
|
185
|
+
const newValue = new_[key];
|
|
186
|
+
|
|
187
|
+
// 快速路径:引用相等,跳过
|
|
188
|
+
if (oldValue === newValue) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 深度比较:对于对象和数组,进行浅比较
|
|
193
|
+
if (
|
|
194
|
+
typeof oldValue === "object" &&
|
|
195
|
+
oldValue !== null &&
|
|
196
|
+
typeof newValue === "object" &&
|
|
197
|
+
newValue !== null
|
|
198
|
+
) {
|
|
199
|
+
// 浅比较:只比较第一层属性
|
|
200
|
+
if (JSON.stringify(oldValue) === JSON.stringify(newValue)) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 值确实变化了,更新属性
|
|
206
|
+
applySingleProp(element, key, newValue, tag, isSVG);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Updates element children by comparing old and new children.
|
|
212
|
+
* Simple version: only handles same number of children.
|
|
213
|
+
*/
|
|
214
|
+
export function updateChildren(
|
|
215
|
+
element: HTMLElement | SVGElement,
|
|
216
|
+
oldChildren: JSXChildren[],
|
|
217
|
+
newChildren: JSXChildren[]
|
|
218
|
+
): void {
|
|
219
|
+
const flatOld = flattenChildren(oldChildren);
|
|
220
|
+
const flatNew = flattenChildren(newChildren);
|
|
221
|
+
|
|
222
|
+
// 阶段 4 简化版:只处理相同数量的子节点
|
|
223
|
+
const minLength = Math.min(flatOld.length, flatNew.length);
|
|
224
|
+
|
|
225
|
+
// 更新现有子节点
|
|
226
|
+
for (let i = 0; i < minLength; i++) {
|
|
227
|
+
const oldChild = flatOld[i];
|
|
228
|
+
const newChild = flatNew[i];
|
|
229
|
+
|
|
230
|
+
// 如果是文本节点,更新文本内容
|
|
231
|
+
if (typeof oldChild === "string" || typeof oldChild === "number") {
|
|
232
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
233
|
+
const textNode = element.childNodes[i];
|
|
234
|
+
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
|
|
235
|
+
textNode.textContent = String(newChild);
|
|
236
|
+
} else {
|
|
237
|
+
// 替换为新的文本节点
|
|
238
|
+
const newTextNode = document.createTextNode(String(newChild));
|
|
239
|
+
if (textNode) {
|
|
240
|
+
element.replaceChild(newTextNode, textNode);
|
|
241
|
+
} else {
|
|
242
|
+
element.appendChild(newTextNode);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
// 类型变化:替换节点
|
|
247
|
+
const textNode = element.childNodes[i];
|
|
248
|
+
if (textNode) {
|
|
249
|
+
// 检查是否应该保留旧节点
|
|
250
|
+
if (!shouldPreserveElement(textNode)) {
|
|
251
|
+
element.removeChild(textNode);
|
|
252
|
+
}
|
|
253
|
+
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
254
|
+
}
|
|
255
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
256
|
+
element.appendChild(document.createTextNode(String(newChild)));
|
|
257
|
+
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
258
|
+
element.appendChild(newChild);
|
|
259
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
260
|
+
element.appendChild(newChild);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} else if (oldChild instanceof HTMLElement || oldChild instanceof SVGElement) {
|
|
264
|
+
// 如果是元素节点,检查是否是同一个元素
|
|
265
|
+
if (newChild === oldChild) {
|
|
266
|
+
// 同一个元素,不需要更新
|
|
267
|
+
continue;
|
|
268
|
+
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
269
|
+
// 不同的元素,替换
|
|
270
|
+
const oldNode = element.childNodes[i];
|
|
271
|
+
if (oldNode) {
|
|
272
|
+
// 检查是否应该保留旧节点
|
|
273
|
+
if (!shouldPreserveElement(oldNode)) {
|
|
274
|
+
// 只有当新子元素不在当前位置时才替换
|
|
275
|
+
if (oldNode !== newChild) {
|
|
276
|
+
element.replaceChild(newChild, oldNode);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
// 应该保留旧节点,只添加新节点(不替换)
|
|
280
|
+
if (newChild.parentNode !== element) {
|
|
281
|
+
element.appendChild(newChild);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
} else {
|
|
285
|
+
if (newChild.parentNode !== element) {
|
|
286
|
+
element.appendChild(newChild);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
// 类型变化:替换节点
|
|
291
|
+
const oldNode = element.childNodes[i];
|
|
292
|
+
if (oldNode) {
|
|
293
|
+
// 检查是否应该保留旧节点
|
|
294
|
+
if (!shouldPreserveElement(oldNode)) {
|
|
295
|
+
element.removeChild(oldNode);
|
|
296
|
+
}
|
|
297
|
+
// 如果应该保留,不删除(但可能仍需要添加新节点)
|
|
298
|
+
}
|
|
299
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
300
|
+
element.appendChild(document.createTextNode(String(newChild)));
|
|
301
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
302
|
+
element.appendChild(newChild);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// 添加新子节点
|
|
309
|
+
for (let i = minLength; i < flatNew.length; i++) {
|
|
310
|
+
const newChild = flatNew[i];
|
|
311
|
+
if (newChild === null || newChild === undefined || newChild === false) {
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (typeof newChild === "string" || typeof newChild === "number") {
|
|
316
|
+
element.appendChild(document.createTextNode(String(newChild)));
|
|
317
|
+
} else if (newChild instanceof HTMLElement || newChild instanceof SVGElement) {
|
|
318
|
+
// 确保子元素正确添加到当前父容器
|
|
319
|
+
// appendChild 会自动从旧父容器移除并添加到新父容器
|
|
320
|
+
// 但我们需要确保不重复添加已存在的子元素
|
|
321
|
+
if (newChild.parentNode !== element) {
|
|
322
|
+
element.appendChild(newChild);
|
|
323
|
+
}
|
|
324
|
+
} else if (newChild instanceof DocumentFragment) {
|
|
325
|
+
element.appendChild(newChild);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// 移除多余子节点(阶段 5:正确处理元素保留)
|
|
330
|
+
// 关键:需要跳过"应该保留"的元素(第三方库注入的元素)
|
|
331
|
+
const nodesToRemove: Node[] = [];
|
|
332
|
+
for (let i = flatNew.length; i < element.childNodes.length; i++) {
|
|
333
|
+
const child = element.childNodes[i];
|
|
334
|
+
if (!shouldPreserveElement(child)) {
|
|
335
|
+
// 只有不应该保留的节点才添加到移除列表
|
|
336
|
+
nodesToRemove.push(child);
|
|
337
|
+
}
|
|
338
|
+
// 如果应该保留,跳过(不添加到移除列表)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 统一移除(从后往前移除,避免索引变化)
|
|
342
|
+
for (let i = nodesToRemove.length - 1; i >= 0; i--) {
|
|
343
|
+
const node = nodesToRemove[i];
|
|
344
|
+
if (node.parentNode === element) {
|
|
345
|
+
element.removeChild(node);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Updates an element with new props and children.
|
|
352
|
+
* Stores metadata in cache manager for next update.
|
|
353
|
+
*/
|
|
354
|
+
export function updateElement(
|
|
355
|
+
element: HTMLElement | SVGElement,
|
|
356
|
+
newProps: Record<string, unknown> | null,
|
|
357
|
+
newChildren: JSXChildren[],
|
|
358
|
+
tag: string,
|
|
359
|
+
cacheManager: DOMCacheManager
|
|
360
|
+
): void {
|
|
361
|
+
// 获取旧的元数据
|
|
362
|
+
const oldMetadata = cacheManager.getMetadata(element);
|
|
363
|
+
const oldProps = (oldMetadata?.props as Record<string, unknown>) || null;
|
|
364
|
+
const oldChildren = (oldMetadata?.children as JSXChildren[]) || [];
|
|
365
|
+
|
|
366
|
+
// 更新 props
|
|
367
|
+
updateProps(element, oldProps, newProps, tag);
|
|
368
|
+
|
|
369
|
+
// 更新 children
|
|
370
|
+
updateChildren(element, oldChildren, newChildren);
|
|
371
|
+
|
|
372
|
+
// 保存新的元数据
|
|
373
|
+
cacheManager.setMetadata(element, {
|
|
374
|
+
props: newProps || {},
|
|
375
|
+
children: newChildren,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Props Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure functions for handling element properties and attributes (RFC 0037).
|
|
5
|
+
* These functions implement the HTML First strategy for property setting.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getSVGAttributeName, shouldUseSVGNamespace } from "./svg-utils";
|
|
9
|
+
import { createLogger } from "./logger";
|
|
10
|
+
const logger = createLogger("Props Utilities");
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 检查是否是框架内部属性
|
|
14
|
+
* 这些属性不应该被渲染到 DOM 元素上
|
|
15
|
+
*
|
|
16
|
+
* @param key - 属性名
|
|
17
|
+
* @returns 是否是框架内部属性
|
|
18
|
+
*/
|
|
19
|
+
export function isFrameworkInternalProp(key: string): boolean {
|
|
20
|
+
// JSX 标准:key 不应该渲染到 DOM
|
|
21
|
+
if (key === "key") {
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 框架内部属性(用于缓存和优化)
|
|
26
|
+
if (key === "__wsxPositionId" || key === "__wsxIndex") {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 测试辅助属性(不应该渲染到 DOM)
|
|
31
|
+
if (key === "__testId") {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ref 已经在 applySingleProp 中处理,但这里也标记为内部属性
|
|
36
|
+
// 确保不会传递到 setSmartProperty
|
|
37
|
+
if (key === "ref") {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 检查是否是 HTML 标准属性
|
|
46
|
+
* HTML First 策略的核心:优先识别标准属性
|
|
47
|
+
*
|
|
48
|
+
* @param key - 属性名
|
|
49
|
+
* @returns 是否是标准 HTML 属性
|
|
50
|
+
*/
|
|
51
|
+
export function isStandardHTMLAttribute(key: string): boolean {
|
|
52
|
+
// 标准 HTML 属性集合(常见属性)
|
|
53
|
+
const standardAttributes = new Set([
|
|
54
|
+
// 全局属性
|
|
55
|
+
"id",
|
|
56
|
+
"class",
|
|
57
|
+
"className",
|
|
58
|
+
"style",
|
|
59
|
+
"title",
|
|
60
|
+
"lang",
|
|
61
|
+
"dir",
|
|
62
|
+
"hidden",
|
|
63
|
+
"tabindex",
|
|
64
|
+
"accesskey",
|
|
65
|
+
"contenteditable",
|
|
66
|
+
"draggable",
|
|
67
|
+
"spellcheck",
|
|
68
|
+
"translate",
|
|
69
|
+
"autocapitalize",
|
|
70
|
+
"autocorrect",
|
|
71
|
+
// 表单属性
|
|
72
|
+
"name",
|
|
73
|
+
"value",
|
|
74
|
+
"type",
|
|
75
|
+
"placeholder",
|
|
76
|
+
"required",
|
|
77
|
+
"disabled",
|
|
78
|
+
"readonly",
|
|
79
|
+
"checked",
|
|
80
|
+
"selected",
|
|
81
|
+
"multiple",
|
|
82
|
+
"min",
|
|
83
|
+
"max",
|
|
84
|
+
"step",
|
|
85
|
+
"autocomplete",
|
|
86
|
+
"autofocus",
|
|
87
|
+
"form",
|
|
88
|
+
"formaction",
|
|
89
|
+
"formenctype",
|
|
90
|
+
"formmethod",
|
|
91
|
+
"formnovalidate",
|
|
92
|
+
"formtarget",
|
|
93
|
+
// 链接属性
|
|
94
|
+
"href",
|
|
95
|
+
"target",
|
|
96
|
+
"rel",
|
|
97
|
+
"download",
|
|
98
|
+
"hreflang",
|
|
99
|
+
"ping",
|
|
100
|
+
// 媒体属性
|
|
101
|
+
"src",
|
|
102
|
+
"alt",
|
|
103
|
+
"width",
|
|
104
|
+
"height",
|
|
105
|
+
"poster",
|
|
106
|
+
"preload",
|
|
107
|
+
"controls",
|
|
108
|
+
"autoplay",
|
|
109
|
+
"loop",
|
|
110
|
+
"muted",
|
|
111
|
+
"playsinline",
|
|
112
|
+
"crossorigin",
|
|
113
|
+
// ARIA 属性(部分常见)
|
|
114
|
+
"role",
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const lowerKey = key.toLowerCase();
|
|
118
|
+
|
|
119
|
+
// 检查是否是标准属性
|
|
120
|
+
if (standardAttributes.has(lowerKey)) {
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 检查是否是 data-* 属性(必须使用连字符)
|
|
125
|
+
// 注意:单独的 "data" 不是标准属性,不在这个列表中
|
|
126
|
+
// data 可以检查 JavaScript 属性,data-* 只使用 setAttribute
|
|
127
|
+
if (lowerKey.startsWith("data-")) {
|
|
128
|
+
return true; // 标准属性,只使用 setAttribute,不检查对象属性
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 检查是否是 aria-* 属性
|
|
132
|
+
if (lowerKey.startsWith("aria-")) {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 检查是否是 SVG 命名空间属性
|
|
137
|
+
if (key.startsWith("xml:") || key.startsWith("xlink:")) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 检查是否是特殊属性(已有专门处理逻辑的属性)
|
|
146
|
+
* 这些属性不应该进入通用属性处理流程
|
|
147
|
+
*/
|
|
148
|
+
export function isSpecialProperty(key: string, value: unknown): boolean {
|
|
149
|
+
return (
|
|
150
|
+
key === "ref" ||
|
|
151
|
+
key === "className" ||
|
|
152
|
+
key === "class" ||
|
|
153
|
+
key === "style" ||
|
|
154
|
+
(key.startsWith("on") && typeof value === "function") ||
|
|
155
|
+
typeof value === "boolean" ||
|
|
156
|
+
key === "value"
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 智能属性设置函数
|
|
162
|
+
* HTML First 策略:优先使用 HTML 属性,避免与标准属性冲突
|
|
163
|
+
*
|
|
164
|
+
* @param element - DOM 元素
|
|
165
|
+
* @param key - 属性名
|
|
166
|
+
* @param value - 属性值
|
|
167
|
+
* @param tag - HTML 标签名(用于判断是否是 SVG)
|
|
168
|
+
*/
|
|
169
|
+
export function setSmartProperty(
|
|
170
|
+
element: HTMLElement | SVGElement,
|
|
171
|
+
key: string,
|
|
172
|
+
value: unknown,
|
|
173
|
+
tag: string
|
|
174
|
+
): void {
|
|
175
|
+
const isSVG = shouldUseSVGNamespace(tag);
|
|
176
|
+
|
|
177
|
+
// 1. 检查是否是特殊属性(已有处理逻辑的属性)
|
|
178
|
+
if (isSpecialProperty(key, value)) {
|
|
179
|
+
return; // 由现有逻辑处理
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 2. HTML First: 优先检查是否是 HTML 标准属性
|
|
183
|
+
if (isStandardHTMLAttribute(key)) {
|
|
184
|
+
// 标准 HTML 属性:直接使用 setAttribute,不检查 JavaScript 属性
|
|
185
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
186
|
+
|
|
187
|
+
// 对于复杂类型,尝试序列化
|
|
188
|
+
if (typeof value === "object" && value !== null) {
|
|
189
|
+
try {
|
|
190
|
+
const serialized = JSON.stringify(value);
|
|
191
|
+
// 检查长度限制(保守估计 1MB)
|
|
192
|
+
if (serialized.length > 1024 * 1024) {
|
|
193
|
+
logger.warn(
|
|
194
|
+
`[WSX] Attribute "${key}" value too large, ` +
|
|
195
|
+
`consider using a non-standard property name instead`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
element.setAttribute(attributeName, serialized);
|
|
199
|
+
} catch (error) {
|
|
200
|
+
// 无法序列化(如循环引用),警告并跳过
|
|
201
|
+
logger.warn(`Cannot serialize attribute "${key}":`, error);
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
element.setAttribute(attributeName, String(value));
|
|
205
|
+
}
|
|
206
|
+
// 重要:标准属性只使用 setAttribute,不使用 JavaScript 属性
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// 3. SVG 元素特殊处理:对于 SVG 元素,很多属性应该直接使用 setAttribute
|
|
211
|
+
// 因为 SVG 元素的很多属性是只读的(如 viewBox)
|
|
212
|
+
if (element instanceof SVGElement) {
|
|
213
|
+
const attributeName = getSVGAttributeName(key);
|
|
214
|
+
// 对于复杂类型,尝试序列化
|
|
215
|
+
if (typeof value === "object" && value !== null) {
|
|
216
|
+
try {
|
|
217
|
+
const serialized = JSON.stringify(value);
|
|
218
|
+
element.setAttribute(attributeName, serialized);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
logger.warn(`Cannot serialize SVG attribute "${key}":`, error);
|
|
221
|
+
}
|
|
222
|
+
} else {
|
|
223
|
+
element.setAttribute(attributeName, String(value));
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 4. 非标准属性:检查元素是否有该 JavaScript 属性
|
|
229
|
+
const hasProperty = key in element || Object.prototype.hasOwnProperty.call(element, key);
|
|
230
|
+
|
|
231
|
+
if (hasProperty) {
|
|
232
|
+
// 检查是否是只读属性
|
|
233
|
+
let isReadOnly = false;
|
|
234
|
+
try {
|
|
235
|
+
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), key);
|
|
236
|
+
if (descriptor) {
|
|
237
|
+
isReadOnly =
|
|
238
|
+
(descriptor.get !== undefined && descriptor.set === undefined) ||
|
|
239
|
+
(descriptor.writable === false && descriptor.set === undefined);
|
|
240
|
+
}
|
|
241
|
+
} catch {
|
|
242
|
+
// 忽略错误,继续尝试设置
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (isReadOnly) {
|
|
246
|
+
// 只读属性使用 setAttribute
|
|
247
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
248
|
+
// 对于复杂类型,尝试序列化
|
|
249
|
+
if (typeof value === "object" && value !== null) {
|
|
250
|
+
try {
|
|
251
|
+
const serialized = JSON.stringify(value);
|
|
252
|
+
element.setAttribute(attributeName, serialized);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
logger.warn(`Cannot serialize readonly property "${key}":`, error);
|
|
255
|
+
}
|
|
256
|
+
} else {
|
|
257
|
+
element.setAttribute(attributeName, String(value));
|
|
258
|
+
}
|
|
259
|
+
} else {
|
|
260
|
+
// 使用 JavaScript 属性赋值(支持任意类型)
|
|
261
|
+
try {
|
|
262
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
263
|
+
(element as any)[key] = value;
|
|
264
|
+
} catch {
|
|
265
|
+
// 如果赋值失败,回退到 setAttribute
|
|
266
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
267
|
+
// 对于复杂类型,尝试序列化
|
|
268
|
+
if (typeof value === "object" && value !== null) {
|
|
269
|
+
try {
|
|
270
|
+
const serialized = JSON.stringify(value);
|
|
271
|
+
element.setAttribute(attributeName, serialized);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
logger.warn(
|
|
274
|
+
`[WSX] Cannot serialize property "${key}" for attribute:`,
|
|
275
|
+
error
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
element.setAttribute(attributeName, String(value));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
} else {
|
|
284
|
+
// 没有 JavaScript 属性,使用 setAttribute
|
|
285
|
+
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
286
|
+
|
|
287
|
+
// 对于复杂类型,尝试序列化
|
|
288
|
+
if (typeof value === "object" && value !== null) {
|
|
289
|
+
try {
|
|
290
|
+
const serialized = JSON.stringify(value);
|
|
291
|
+
// 检查长度限制
|
|
292
|
+
if (serialized.length > 1024 * 1024) {
|
|
293
|
+
logger.warn(
|
|
294
|
+
`[WSX] Property "${key}" value too large for attribute, ` +
|
|
295
|
+
`consider using a JavaScript property instead`
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
element.setAttribute(attributeName, serialized);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
// 无法序列化,警告并跳过
|
|
301
|
+
logger.warn(`Cannot serialize property "${key}" for attribute:`, error);
|
|
302
|
+
}
|
|
303
|
+
} else {
|
|
304
|
+
element.setAttribute(attributeName, String(value));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/web-component.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
12
12
|
import { StyleManager } from "./styles/style-manager";
|
|
13
13
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
14
|
+
import { RenderContext } from "./render-context";
|
|
14
15
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
15
16
|
|
|
16
17
|
const logger = createLogger("WebComponent");
|
|
@@ -183,7 +184,7 @@ export abstract class WebComponent extends BaseComponent {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
// 4. 重新渲染JSX
|
|
186
|
-
const content = this.render();
|
|
187
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
187
188
|
|
|
188
189
|
// 5. 在添加到 DOM 之前恢复值
|
|
189
190
|
if (focusState && focusState.key && focusState.value !== undefined) {
|