@wsxjs/wsx-core 0.0.20 → 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 +671 -111
- package/dist/index.mjs +137 -17
- package/dist/jsx-runtime.js +541 -99
- package/dist/jsx-runtime.mjs +1 -1
- package/dist/jsx.js +541 -99
- 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 -451
- 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-7FXISNME.mjs +0 -462
- package/dist/tsconfig.tsbuildinfo +0 -1
package/src/jsx-factory.ts
CHANGED
|
@@ -11,283 +11,18 @@
|
|
|
11
11
|
|
|
12
12
|
// JSX 类型声明已移至 types/wsx-types.d.ts
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const standardAttributes = new Set([
|
|
27
|
-
// 全局属性
|
|
28
|
-
"id",
|
|
29
|
-
"class",
|
|
30
|
-
"className",
|
|
31
|
-
"style",
|
|
32
|
-
"title",
|
|
33
|
-
"lang",
|
|
34
|
-
"dir",
|
|
35
|
-
"hidden",
|
|
36
|
-
"tabindex",
|
|
37
|
-
"accesskey",
|
|
38
|
-
"contenteditable",
|
|
39
|
-
"draggable",
|
|
40
|
-
"spellcheck",
|
|
41
|
-
"translate",
|
|
42
|
-
"autocapitalize",
|
|
43
|
-
"autocorrect",
|
|
44
|
-
// 表单属性
|
|
45
|
-
"name",
|
|
46
|
-
"value",
|
|
47
|
-
"type",
|
|
48
|
-
"placeholder",
|
|
49
|
-
"required",
|
|
50
|
-
"disabled",
|
|
51
|
-
"readonly",
|
|
52
|
-
"checked",
|
|
53
|
-
"selected",
|
|
54
|
-
"multiple",
|
|
55
|
-
"min",
|
|
56
|
-
"max",
|
|
57
|
-
"step",
|
|
58
|
-
"autocomplete",
|
|
59
|
-
"autofocus",
|
|
60
|
-
"form",
|
|
61
|
-
"formaction",
|
|
62
|
-
"formenctype",
|
|
63
|
-
"formmethod",
|
|
64
|
-
"formnovalidate",
|
|
65
|
-
"formtarget",
|
|
66
|
-
// 链接属性
|
|
67
|
-
"href",
|
|
68
|
-
"target",
|
|
69
|
-
"rel",
|
|
70
|
-
"download",
|
|
71
|
-
"hreflang",
|
|
72
|
-
"ping",
|
|
73
|
-
// 媒体属性
|
|
74
|
-
"src",
|
|
75
|
-
"alt",
|
|
76
|
-
"width",
|
|
77
|
-
"height",
|
|
78
|
-
"poster",
|
|
79
|
-
"preload",
|
|
80
|
-
"controls",
|
|
81
|
-
"autoplay",
|
|
82
|
-
"loop",
|
|
83
|
-
"muted",
|
|
84
|
-
"playsinline",
|
|
85
|
-
"crossorigin",
|
|
86
|
-
// ARIA 属性(部分常见)
|
|
87
|
-
"role",
|
|
88
|
-
]);
|
|
89
|
-
|
|
90
|
-
const lowerKey = key.toLowerCase();
|
|
91
|
-
|
|
92
|
-
// 检查是否是标准属性
|
|
93
|
-
if (standardAttributes.has(lowerKey)) {
|
|
94
|
-
return true;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// 检查是否是 data-* 属性(必须使用连字符)
|
|
98
|
-
// 注意:单独的 "data" 不是标准属性,不在这个列表中
|
|
99
|
-
// data 可以检查 JavaScript 属性,data-* 只使用 setAttribute
|
|
100
|
-
if (lowerKey.startsWith("data-")) {
|
|
101
|
-
return true; // 标准属性,只使用 setAttribute,不检查对象属性
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 检查是否是 aria-* 属性
|
|
105
|
-
if (lowerKey.startsWith("aria-")) {
|
|
106
|
-
return true;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// 检查是否是 SVG 命名空间属性
|
|
110
|
-
if (key.startsWith("xml:") || key.startsWith("xlink:")) {
|
|
111
|
-
return true;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* 检查是否是特殊属性(已有专门处理逻辑的属性)
|
|
119
|
-
* 这些属性不应该进入通用属性处理流程
|
|
120
|
-
*/
|
|
121
|
-
function isSpecialProperty(key: string, value: unknown): boolean {
|
|
122
|
-
return (
|
|
123
|
-
key === "ref" ||
|
|
124
|
-
key === "className" ||
|
|
125
|
-
key === "class" ||
|
|
126
|
-
key === "style" ||
|
|
127
|
-
(key.startsWith("on") && typeof value === "function") ||
|
|
128
|
-
typeof value === "boolean" ||
|
|
129
|
-
key === "value"
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 智能属性设置函数
|
|
135
|
-
* HTML First 策略:优先使用 HTML 属性,避免与标准属性冲突
|
|
136
|
-
*
|
|
137
|
-
* @param element - DOM 元素
|
|
138
|
-
* @param key - 属性名
|
|
139
|
-
* @param value - 属性值
|
|
140
|
-
* @param isSVG - 是否是 SVG 元素
|
|
141
|
-
*/
|
|
142
|
-
function setSmartProperty(
|
|
143
|
-
element: HTMLElement | SVGElement,
|
|
144
|
-
key: string,
|
|
145
|
-
value: unknown,
|
|
146
|
-
isSVG: boolean = false
|
|
147
|
-
): void {
|
|
148
|
-
// 1. 检查是否是特殊属性(已有处理逻辑的属性)
|
|
149
|
-
if (isSpecialProperty(key, value)) {
|
|
150
|
-
return; // 由现有逻辑处理
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// 2. HTML First: 优先检查是否是 HTML 标准属性
|
|
154
|
-
if (isStandardHTMLAttribute(key)) {
|
|
155
|
-
// 标准 HTML 属性:直接使用 setAttribute,不检查 JavaScript 属性
|
|
156
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
157
|
-
|
|
158
|
-
// 对于复杂类型,尝试序列化
|
|
159
|
-
if (typeof value === "object" && value !== null) {
|
|
160
|
-
try {
|
|
161
|
-
const serialized = JSON.stringify(value);
|
|
162
|
-
// 检查长度限制(保守估计 1MB)
|
|
163
|
-
if (serialized.length > 1024 * 1024) {
|
|
164
|
-
console.warn(
|
|
165
|
-
`[WSX] Attribute "${key}" value too large, ` +
|
|
166
|
-
`consider using a non-standard property name instead`
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
element.setAttribute(attributeName, serialized);
|
|
170
|
-
} catch (error) {
|
|
171
|
-
// 无法序列化(如循环引用),警告并跳过
|
|
172
|
-
console.warn(`[WSX] Cannot serialize attribute "${key}":`, error);
|
|
173
|
-
}
|
|
174
|
-
} else {
|
|
175
|
-
element.setAttribute(attributeName, String(value));
|
|
176
|
-
}
|
|
177
|
-
// 重要:标准属性只使用 setAttribute,不使用 JavaScript 属性
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// 3. SVG 元素特殊处理:对于 SVG 元素,很多属性应该直接使用 setAttribute
|
|
182
|
-
// 因为 SVG 元素的很多属性是只读的(如 viewBox)
|
|
183
|
-
if (element instanceof SVGElement) {
|
|
184
|
-
const attributeName = getSVGAttributeName(key);
|
|
185
|
-
// 对于复杂类型,尝试序列化
|
|
186
|
-
if (typeof value === "object" && value !== null) {
|
|
187
|
-
try {
|
|
188
|
-
const serialized = JSON.stringify(value);
|
|
189
|
-
element.setAttribute(attributeName, serialized);
|
|
190
|
-
} catch (error) {
|
|
191
|
-
console.warn(`[WSX] Cannot serialize SVG attribute "${key}":`, error);
|
|
192
|
-
}
|
|
193
|
-
} else {
|
|
194
|
-
element.setAttribute(attributeName, String(value));
|
|
195
|
-
}
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// 4. 非标准属性:检查元素是否有该 JavaScript 属性
|
|
200
|
-
const hasProperty = key in element || Object.prototype.hasOwnProperty.call(element, key);
|
|
201
|
-
|
|
202
|
-
if (hasProperty) {
|
|
203
|
-
// 检查是否是只读属性
|
|
204
|
-
let isReadOnly = false;
|
|
205
|
-
try {
|
|
206
|
-
const descriptor = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(element), key);
|
|
207
|
-
if (descriptor) {
|
|
208
|
-
isReadOnly =
|
|
209
|
-
(descriptor.get !== undefined && descriptor.set === undefined) ||
|
|
210
|
-
(descriptor.writable === false && descriptor.set === undefined);
|
|
211
|
-
}
|
|
212
|
-
} catch {
|
|
213
|
-
// 忽略错误,继续尝试设置
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
if (isReadOnly) {
|
|
217
|
-
// 只读属性使用 setAttribute
|
|
218
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
219
|
-
// 对于复杂类型,尝试序列化
|
|
220
|
-
if (typeof value === "object" && value !== null) {
|
|
221
|
-
try {
|
|
222
|
-
const serialized = JSON.stringify(value);
|
|
223
|
-
element.setAttribute(attributeName, serialized);
|
|
224
|
-
} catch (error) {
|
|
225
|
-
console.warn(`[WSX] Cannot serialize readonly property "${key}":`, error);
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
element.setAttribute(attributeName, String(value));
|
|
229
|
-
}
|
|
230
|
-
} else {
|
|
231
|
-
// 使用 JavaScript 属性赋值(支持任意类型)
|
|
232
|
-
try {
|
|
233
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
234
|
-
(element as any)[key] = value;
|
|
235
|
-
} catch {
|
|
236
|
-
// 如果赋值失败,回退到 setAttribute
|
|
237
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
238
|
-
// 对于复杂类型,尝试序列化
|
|
239
|
-
if (typeof value === "object" && value !== null) {
|
|
240
|
-
try {
|
|
241
|
-
const serialized = JSON.stringify(value);
|
|
242
|
-
element.setAttribute(attributeName, serialized);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
console.warn(
|
|
245
|
-
`[WSX] Cannot serialize property "${key}" for attribute:`,
|
|
246
|
-
error
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
} else {
|
|
250
|
-
element.setAttribute(attributeName, String(value));
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
} else {
|
|
255
|
-
// 没有 JavaScript 属性,使用 setAttribute
|
|
256
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
257
|
-
|
|
258
|
-
// 对于复杂类型,尝试序列化
|
|
259
|
-
if (typeof value === "object" && value !== null) {
|
|
260
|
-
try {
|
|
261
|
-
const serialized = JSON.stringify(value);
|
|
262
|
-
// 检查长度限制
|
|
263
|
-
if (serialized.length > 1024 * 1024) {
|
|
264
|
-
console.warn(
|
|
265
|
-
`[WSX] Property "${key}" value too large for attribute, ` +
|
|
266
|
-
`consider using a JavaScript property instead`
|
|
267
|
-
);
|
|
268
|
-
}
|
|
269
|
-
element.setAttribute(attributeName, serialized);
|
|
270
|
-
} catch (error) {
|
|
271
|
-
// 无法序列化,警告并跳过
|
|
272
|
-
console.warn(`[WSX] Cannot serialize property "${key}" for attribute:`, error);
|
|
273
|
-
}
|
|
274
|
-
} else {
|
|
275
|
-
element.setAttribute(attributeName, String(value));
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// JSX子元素类型
|
|
281
|
-
export type JSXChildren =
|
|
282
|
-
| string
|
|
283
|
-
| number
|
|
284
|
-
| HTMLElement
|
|
285
|
-
| SVGElement
|
|
286
|
-
| DocumentFragment
|
|
287
|
-
| JSXChildren[]
|
|
288
|
-
| null
|
|
289
|
-
| undefined
|
|
290
|
-
| boolean;
|
|
14
|
+
import { flattenChildren, type JSXChildren } from "./utils/dom-utils";
|
|
15
|
+
import { generateCacheKey, getComponentId } from "./utils/cache-key";
|
|
16
|
+
import { markElement } from "./utils/element-marking";
|
|
17
|
+
import { RenderContext } from "./render-context";
|
|
18
|
+
import { createElementWithPropsAndChildren } from "./utils/element-creation";
|
|
19
|
+
import { updateElement } from "./utils/element-update";
|
|
20
|
+
import type { BaseComponent } from "./base-component";
|
|
21
|
+
import type { DOMCacheManager } from "./dom-cache-manager";
|
|
22
|
+
import { createLogger } from "./utils/logger";
|
|
23
|
+
const logger = createLogger("JSX Factory");
|
|
24
|
+
// JSX子元素类型(从 dom-utils 重新导出以保持向后兼容)
|
|
25
|
+
export type { JSXChildren } from "./utils/dom-utils";
|
|
291
26
|
|
|
292
27
|
/**
|
|
293
28
|
* 纯原生JSX工厂函数
|
|
@@ -307,198 +42,85 @@ export function h(
|
|
|
307
42
|
props: Record<string, unknown> | null = {},
|
|
308
43
|
...children: JSXChildren[]
|
|
309
44
|
): HTMLElement | SVGElement {
|
|
310
|
-
//
|
|
45
|
+
// 处理组件函数(不受缓存影响)
|
|
311
46
|
if (typeof tag === "function") {
|
|
312
47
|
return tag(props, children);
|
|
313
48
|
}
|
|
314
49
|
|
|
315
|
-
//
|
|
316
|
-
const
|
|
50
|
+
// 检查上下文(阶段 3.2:启用缓存机制)
|
|
51
|
+
const context = RenderContext.getCurrentComponent();
|
|
52
|
+
const cacheManager = context ? RenderContext.getDOMCache() : null;
|
|
317
53
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
const isSVG = shouldUseSVGNamespace(tag);
|
|
321
|
-
|
|
322
|
-
Object.entries(props).forEach(([key, value]) => {
|
|
323
|
-
if (value === null || value === undefined || value === false) {
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
// 处理ref回调
|
|
328
|
-
if (key === "ref" && typeof value === "function") {
|
|
329
|
-
value(element);
|
|
330
|
-
}
|
|
331
|
-
// 处理className和class
|
|
332
|
-
else if (key === "className" || key === "class") {
|
|
333
|
-
if (isSVG) {
|
|
334
|
-
// SVG元素使用class属性
|
|
335
|
-
element.setAttribute("class", value as string);
|
|
336
|
-
} else {
|
|
337
|
-
// HTML元素可以使用className
|
|
338
|
-
(element as HTMLElement).className = value as string;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
// 处理style
|
|
342
|
-
else if (key === "style" && typeof value === "string") {
|
|
343
|
-
element.setAttribute("style", value);
|
|
344
|
-
}
|
|
345
|
-
// 处理事件监听器
|
|
346
|
-
else if (key.startsWith("on") && typeof value === "function") {
|
|
347
|
-
const eventName = key.slice(2).toLowerCase();
|
|
348
|
-
element.addEventListener(eventName, value as EventListener);
|
|
349
|
-
}
|
|
350
|
-
// 处理布尔属性
|
|
351
|
-
else if (typeof value === "boolean") {
|
|
352
|
-
if (value) {
|
|
353
|
-
element.setAttribute(key, "");
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// 特殊处理 input/textarea/select 的 value 属性
|
|
357
|
-
// 使用 .value 而不是 setAttribute,因为 .value 是当前值,setAttribute 是初始值
|
|
358
|
-
else if (key === "value") {
|
|
359
|
-
if (
|
|
360
|
-
element instanceof HTMLInputElement ||
|
|
361
|
-
element instanceof HTMLTextAreaElement ||
|
|
362
|
-
element instanceof HTMLSelectElement
|
|
363
|
-
) {
|
|
364
|
-
element.value = String(value);
|
|
365
|
-
} else {
|
|
366
|
-
// 对于其他元素,使用 setAttribute
|
|
367
|
-
const attributeName = isSVG ? getSVGAttributeName(key) : key;
|
|
368
|
-
element.setAttribute(attributeName, String(value));
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
// 处理其他属性 - 使用智能属性设置函数
|
|
372
|
-
else {
|
|
373
|
-
setSmartProperty(element, key, value, isSVG);
|
|
374
|
-
}
|
|
375
|
-
});
|
|
54
|
+
if (context && cacheManager) {
|
|
55
|
+
return tryUseCacheOrCreate(tag, props, children, context, cacheManager);
|
|
376
56
|
}
|
|
377
57
|
|
|
378
|
-
//
|
|
379
|
-
|
|
380
|
-
flatChildren.forEach((child) => {
|
|
381
|
-
if (child === null || child === undefined || child === false) {
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (typeof child === "string" || typeof child === "number") {
|
|
386
|
-
element.appendChild(document.createTextNode(String(child)));
|
|
387
|
-
} else if (child instanceof HTMLElement || child instanceof SVGElement) {
|
|
388
|
-
element.appendChild(child);
|
|
389
|
-
} else if (child instanceof DocumentFragment) {
|
|
390
|
-
element.appendChild(child);
|
|
391
|
-
}
|
|
392
|
-
});
|
|
393
|
-
|
|
394
|
-
return element;
|
|
58
|
+
// 无上下文:使用旧逻辑(向后兼容)
|
|
59
|
+
return createElementWithPropsAndChildren(tag, props, children);
|
|
395
60
|
}
|
|
396
61
|
|
|
397
62
|
/**
|
|
398
|
-
*
|
|
399
|
-
* 使用更严格的检测:必须包含完整的 HTML 标签(开始和结束标签,或自闭合标签)
|
|
63
|
+
* Tries to use cached element or creates a new one.
|
|
400
64
|
*/
|
|
401
|
-
function
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
65
|
+
function tryUseCacheOrCreate(
|
|
66
|
+
tag: string,
|
|
67
|
+
props: Record<string, unknown> | null,
|
|
68
|
+
children: JSXChildren[],
|
|
69
|
+
context: BaseComponent,
|
|
70
|
+
cacheManager: DOMCacheManager
|
|
71
|
+
): HTMLElement | SVGElement {
|
|
72
|
+
try {
|
|
73
|
+
const componentId = getComponentId();
|
|
74
|
+
const cacheKey = generateCacheKey(tag, props, componentId, context);
|
|
75
|
+
const cachedElement = cacheManager.get(cacheKey);
|
|
76
|
+
|
|
77
|
+
if (cachedElement) {
|
|
78
|
+
// ✅ 缓存命中:复用元素并更新内容(阶段 4:细粒度更新)
|
|
79
|
+
// 缓存键已经确保了唯一性(componentId + tag + position/key/index)
|
|
80
|
+
// 不需要再检查标签名(可能导致错误复用)
|
|
81
|
+
const element = cachedElement as HTMLElement | SVGElement;
|
|
82
|
+
updateElement(element, props, children, tag, cacheManager);
|
|
83
|
+
return element;
|
|
84
|
+
}
|
|
416
85
|
|
|
417
|
-
|
|
86
|
+
// ❌ 缓存未命中:创建新元素
|
|
87
|
+
const element = createElementWithPropsAndChildren(tag, props, children);
|
|
88
|
+
cacheManager.set(cacheKey, element);
|
|
89
|
+
markElement(element, cacheKey);
|
|
90
|
+
// 保存初始元数据(用于下次更新)
|
|
91
|
+
cacheManager.setMetadata(element, {
|
|
92
|
+
props: props || {},
|
|
93
|
+
children: children,
|
|
94
|
+
});
|
|
95
|
+
return element;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// 缓存失败:降级到创建新元素
|
|
98
|
+
return handleCacheError(error, tag, props, children);
|
|
99
|
+
}
|
|
418
100
|
}
|
|
419
101
|
|
|
420
102
|
/**
|
|
421
|
-
*
|
|
422
|
-
* 自动检测HTML字符串并转换为DOM节点
|
|
423
|
-
*
|
|
424
|
-
* @param children - 子元素数组
|
|
425
|
-
* @param skipHTMLDetection - 是否跳过HTML检测(用于已解析的节点,避免无限递归)
|
|
426
|
-
* @param depth - 当前递归深度(防止无限递归,最大深度为 10)
|
|
103
|
+
* Handles cache errors by logging and falling back to creating a new element.
|
|
427
104
|
*/
|
|
428
|
-
function
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
)
|
|
438
|
-
|
|
439
|
-
(
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
}
|
|
443
|
-
const result: (
|
|
444
|
-
| string
|
|
445
|
-
| number
|
|
446
|
-
| HTMLElement
|
|
447
|
-
| SVGElement
|
|
448
|
-
| DocumentFragment
|
|
449
|
-
| boolean
|
|
450
|
-
| null
|
|
451
|
-
| undefined
|
|
452
|
-
)[] = [];
|
|
453
|
-
|
|
454
|
-
for (const child of children) {
|
|
455
|
-
if (child === null || child === undefined || child === false) {
|
|
456
|
-
continue;
|
|
457
|
-
} else if (Array.isArray(child)) {
|
|
458
|
-
// 递归处理数组,保持 skipHTMLDetection 状态,增加深度
|
|
459
|
-
result.push(...flattenChildren(child, skipHTMLDetection, depth + 1));
|
|
460
|
-
} else if (typeof child === "string") {
|
|
461
|
-
// 如果跳过HTML检测,直接添加字符串(避免无限递归)
|
|
462
|
-
if (skipHTMLDetection) {
|
|
463
|
-
result.push(child);
|
|
464
|
-
} else if (isHTMLString(child)) {
|
|
465
|
-
// 自动检测HTML字符串并转换为DOM节点
|
|
466
|
-
// 使用 try-catch 防止解析失败导致崩溃
|
|
467
|
-
try {
|
|
468
|
-
const nodes = parseHTMLToNodes(child);
|
|
469
|
-
// 递归处理转换后的节点数组,标记为已解析,避免再次检测HTML
|
|
470
|
-
// parseHTMLToNodes 返回的字符串是纯文本节点,不应该再次被检测为HTML
|
|
471
|
-
// 但是为了安全,我们仍然设置 skipHTMLDetection = true
|
|
472
|
-
if (nodes.length > 0) {
|
|
473
|
-
// 直接添加解析后的节点,不再递归处理(避免无限递归)
|
|
474
|
-
// parseHTMLToNodes 已经完成了所有解析工作
|
|
475
|
-
for (const node of nodes) {
|
|
476
|
-
if (typeof node === "string") {
|
|
477
|
-
// 文本节点直接添加,不再检测 HTML(已解析)
|
|
478
|
-
result.push(node);
|
|
479
|
-
} else {
|
|
480
|
-
// DOM 元素直接添加
|
|
481
|
-
result.push(node);
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
} else {
|
|
485
|
-
// 如果解析失败,回退到纯文本
|
|
486
|
-
result.push(child);
|
|
487
|
-
}
|
|
488
|
-
} catch (error) {
|
|
489
|
-
// 如果解析失败,回退到纯文本,避免崩溃
|
|
490
|
-
console.warn("[WSX] Failed to parse HTML string, treating as text:", error);
|
|
491
|
-
result.push(child);
|
|
492
|
-
}
|
|
493
|
-
} else {
|
|
494
|
-
result.push(child);
|
|
495
|
-
}
|
|
496
|
-
} else {
|
|
497
|
-
result.push(child);
|
|
105
|
+
function handleCacheError(
|
|
106
|
+
error: unknown,
|
|
107
|
+
tag: string,
|
|
108
|
+
props: Record<string, unknown> | null,
|
|
109
|
+
children: JSXChildren[]
|
|
110
|
+
): HTMLElement | SVGElement {
|
|
111
|
+
// 在开发环境输出警告,帮助调试
|
|
112
|
+
try {
|
|
113
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
114
|
+
const nodeEnv = (typeof (globalThis as any).process !== "undefined" &&
|
|
115
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
116
|
+
(globalThis as any).process.env?.NODE_ENV) as string | undefined;
|
|
117
|
+
if (nodeEnv === "development") {
|
|
118
|
+
logger.warn("[WSX DOM Cache] Cache error, falling back to create new element:", error);
|
|
498
119
|
}
|
|
120
|
+
} catch {
|
|
121
|
+
// 忽略环境变量检查错误
|
|
499
122
|
}
|
|
500
|
-
|
|
501
|
-
return result;
|
|
123
|
+
return createElementWithPropsAndChildren(tag, props, children);
|
|
502
124
|
}
|
|
503
125
|
|
|
504
126
|
/**
|
package/src/light-component.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import { h, type JSXChildren } from "./jsx-factory";
|
|
11
11
|
import { BaseComponent, type BaseComponentConfig } from "./base-component";
|
|
12
|
+
import { RenderContext } from "./render-context";
|
|
12
13
|
import { createLogger } from "@wsxjs/wsx-logger";
|
|
13
14
|
|
|
14
15
|
const logger = createLogger("LightComponent");
|
|
@@ -95,7 +96,7 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
95
96
|
childrenToRemove.forEach((child) => child.remove());
|
|
96
97
|
|
|
97
98
|
// 渲染JSX内容到Light DOM
|
|
98
|
-
const content = this.render();
|
|
99
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
99
100
|
this.appendChild(content);
|
|
100
101
|
|
|
101
102
|
// 确保样式元素在第一个位置(如果存在)
|
|
@@ -178,7 +179,7 @@ export abstract class LightComponent extends BaseComponent {
|
|
|
178
179
|
|
|
179
180
|
try {
|
|
180
181
|
// 3. 重新渲染JSX内容
|
|
181
|
-
const content = this.render();
|
|
182
|
+
const content = RenderContext.runInContext(this, () => this.render());
|
|
182
183
|
|
|
183
184
|
// 4. 在添加到 DOM 之前恢复值,避免浏览器渲染状态值
|
|
184
185
|
if (focusState && focusState.key && focusState.value !== undefined) {
|
|
@@ -78,6 +78,15 @@ export function state(
|
|
|
78
78
|
// Compatibility with Babel plugin which is required for this decorator to work properly
|
|
79
79
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
80
|
): any {
|
|
81
|
+
// RFC 0037 Phase 0: Test Infrastructure Support
|
|
82
|
+
// Allow runtime decorator in tests to bypass Babel plugin requirement
|
|
83
|
+
// Use globalThis to safely access process in all environments
|
|
84
|
+
|
|
85
|
+
const globalProcess =
|
|
86
|
+
typeof globalThis !== "undefined" ? (globalThis as any).process : undefined;
|
|
87
|
+
if (globalProcess?.env?.NODE_ENV === "test") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
81
90
|
/**
|
|
82
91
|
* @state decorator MUST be processed by Babel plugin at compile time.
|
|
83
92
|
* If this function is executed at runtime, it means Babel plugin did not process it.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { BaseComponent } from "./base-component";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* RenderContext
|
|
5
|
+
*
|
|
6
|
+
* Tracks the currently rendering component instance.
|
|
7
|
+
* improved for RFC 0037 to support DOM caching and optimization.
|
|
8
|
+
*/
|
|
9
|
+
export class RenderContext {
|
|
10
|
+
private static current: BaseComponent | null = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Executes a function within the context of a component.
|
|
14
|
+
* @param component The component instance currently rendering.
|
|
15
|
+
* @param fn The function to execute (usually the render method).
|
|
16
|
+
*/
|
|
17
|
+
static runInContext<T>(component: BaseComponent, fn: () => T): T {
|
|
18
|
+
const prev = RenderContext.current;
|
|
19
|
+
RenderContext.current = component;
|
|
20
|
+
try {
|
|
21
|
+
return fn();
|
|
22
|
+
} finally {
|
|
23
|
+
RenderContext.current = prev;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Gets the currently rendering component.
|
|
29
|
+
*/
|
|
30
|
+
static getCurrentComponent(): BaseComponent | null {
|
|
31
|
+
return RenderContext.current;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gets the current component's DOM cache.
|
|
36
|
+
*/
|
|
37
|
+
static getDOMCache() {
|
|
38
|
+
return RenderContext.current?.getDomCache();
|
|
39
|
+
}
|
|
40
|
+
}
|