@box3lab/react-ui 0.0.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 +89 -0
- package/dist/client/src/elements.d.ts +100 -0
- package/dist/client/src/elements.js +39 -0
- package/dist/client/src/hostConfig.d.ts +45 -0
- package/dist/client/src/hostConfig.js +296 -0
- package/dist/client/src/reconciler.d.ts +3 -0
- package/dist/client/src/reconciler.js +4 -0
- package/dist/client/src/renderer.d.ts +13 -0
- package/dist/client/src/renderer.js +23 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
## 神岛 React(实验版)
|
|
2
|
+
|
|
3
|
+
提供一个适用于 **神岛 Arena 客户端 UI** 的 React 渲染器(实验版)。
|
|
4
|
+
|
|
5
|
+
- 基于官方 `react` 实现
|
|
6
|
+
- 使用 JSX 描述 UI,通过自定义渲染器映射到神岛 UI
|
|
7
|
+
|
|
8
|
+
React 中文官网:https://zh-hans.react.dev/
|
|
9
|
+
|
|
10
|
+
### 安装
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm install @box3lab/react-ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`@box3lab/react-ui` 导出结构:
|
|
17
|
+
|
|
18
|
+
- `@box3lab/react-ui`:内置 UI 元素组件(`Box`、`Text`、`Image`...)
|
|
19
|
+
- `@box3lab/react-ui/dom`:渲染入口(`createRoot` 等)
|
|
20
|
+
|
|
21
|
+
### 基本用法
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
import React, { useState } from 'react';
|
|
25
|
+
import { Box, Text } from '@box3lab/react-ui';
|
|
26
|
+
import { createRoot } from '@box3lab/react-ui/dom';
|
|
27
|
+
|
|
28
|
+
function App() {
|
|
29
|
+
const [count, setCount] = useState(0);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<Box
|
|
33
|
+
style={{
|
|
34
|
+
backgroundOpacity: 0.6,
|
|
35
|
+
position: {
|
|
36
|
+
offset: Vec2.create({ x: 0, y: 0 }),
|
|
37
|
+
size: Vec2.create({ x: 0, y: 0 }),
|
|
38
|
+
},
|
|
39
|
+
}}
|
|
40
|
+
onClick={() => setCount((c: number) => c + 1)}
|
|
41
|
+
>
|
|
42
|
+
<Text>你好,React!你点我了 {count} 次</Text>
|
|
43
|
+
</Box>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ui 由宿主环境(神岛 Arena)提供,对应根 UI 节点
|
|
48
|
+
const root = createRoot(ui);
|
|
49
|
+
root.render(<App />);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 内置元素组件(Intrinsic Elements)
|
|
53
|
+
|
|
54
|
+
这些组件会通过自定义渲染器映射到对应的 Box3 UI 元素:
|
|
55
|
+
|
|
56
|
+
- `Box`:基础容器
|
|
57
|
+
- `Text`:文本显示,`children` 会自动映射到 `textContent`
|
|
58
|
+
- `Image`:图片元素,支持 `onLoad` 等事件
|
|
59
|
+
- `Input`:输入框,支持 `onFocus` / `onBlur` 等事件
|
|
60
|
+
- `ScrollBox`:滚动容器
|
|
61
|
+
|
|
62
|
+
示例:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
import { Box, Text, Image, Input, ScrollBox } from '@box3lab/react-ui';
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### TypeScript 支持
|
|
69
|
+
|
|
70
|
+
该包基于 TypeScript 编写,并提供完整的类型信息:
|
|
71
|
+
|
|
72
|
+
- 组件 props 里会自动推导底层 `UiBox` / `UiText` / `UiInput` / `UiImage` / `UiScrollBox` 类型
|
|
73
|
+
- 事件参数类型为 `UiEvent<T>`,可以获得更好的开发体验
|
|
74
|
+
|
|
75
|
+
在你的项目中开启 JSX 支持(以 React JSX 为例):
|
|
76
|
+
|
|
77
|
+
```jsonc
|
|
78
|
+
// tsconfig.json
|
|
79
|
+
{
|
|
80
|
+
"compilerOptions": {
|
|
81
|
+
"jsx": "react",
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 注意事项
|
|
87
|
+
|
|
88
|
+
- 本项目目前为 **实验版**,API 可能在后续版本中发生变动
|
|
89
|
+
- 仅适用于神岛 Arena 提供的 UI 运行环境,无法在普通浏览器中直接使用
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* 通用 UI 元素属性定义,抽象出所有控件共享的基础能力
|
|
4
|
+
*/
|
|
5
|
+
type CommonElementProps<T> = {
|
|
6
|
+
name?: string;
|
|
7
|
+
style?: Omit<Partial<T>, 'position' | 'size'> & {
|
|
8
|
+
/** 位置配置 */
|
|
9
|
+
position?: {
|
|
10
|
+
/** X/Y偏移量 */
|
|
11
|
+
offset?: Vec2;
|
|
12
|
+
/** 宽高缩放比例 */
|
|
13
|
+
size?: Vec2;
|
|
14
|
+
};
|
|
15
|
+
/** 尺寸配置 */
|
|
16
|
+
size?: {
|
|
17
|
+
/** 尺寸偏移量 */
|
|
18
|
+
offset?: Vec2;
|
|
19
|
+
/** 尺寸缩放比例 */
|
|
20
|
+
size?: Vec2;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/** 点击事件处理器 */
|
|
24
|
+
onClick?: (event: UiEvent<T>) => void;
|
|
25
|
+
/** 鼠标按下事件处理器 */
|
|
26
|
+
onMouseDown?: (event: UiEvent<T>) => void;
|
|
27
|
+
/** 鼠标释放事件处理器 */
|
|
28
|
+
onMouseUp?: (event: UiEvent<T>) => void;
|
|
29
|
+
/** 元素挂载完成回调 */
|
|
30
|
+
onMount?: (element: T) => void;
|
|
31
|
+
/** 元素卸载前回调 */
|
|
32
|
+
onUnmount?: (element: T) => void;
|
|
33
|
+
/** 元素引用,获取底层DOM实例 */
|
|
34
|
+
ref?: ((element: T) => void) | {
|
|
35
|
+
current: T | null;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* 组件 props = UiX 自身的可选属性 + 公共 CommonElementProps<T> 属性
|
|
40
|
+
*/
|
|
41
|
+
type BaseUiProps<T> = React.PropsWithChildren<CommonElementProps<T>>;
|
|
42
|
+
/**
|
|
43
|
+
* Input 组件专用 props,基于基础 UI 属性扩展输入框相关事件
|
|
44
|
+
*/
|
|
45
|
+
type InputProps = BaseUiProps<UiInput> & {
|
|
46
|
+
/** 获得焦点事件处理器 */
|
|
47
|
+
onFocus?: (event: UiEvent<UiInput>) => void;
|
|
48
|
+
/** 失去焦点事件处理器 */
|
|
49
|
+
onBlur?: (event: UiEvent<UiInput>) => void;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Image 组件专用 props,基于基础 UI 属性扩展图片相关事件
|
|
53
|
+
*/
|
|
54
|
+
type ImageProps = BaseUiProps<UiImage> & {
|
|
55
|
+
/** 图片加载完成事件处理器 */
|
|
56
|
+
onLoad?: (event: UiEvent<UiImage>) => void;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* Box:基础容器组件,对应底层 UiBox
|
|
60
|
+
*/
|
|
61
|
+
export declare function Box(props: BaseUiProps<UiBox>): React.ReactElement<CommonElementProps<UiBox> & {
|
|
62
|
+
children?: React.ReactNode | undefined;
|
|
63
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
64
|
+
/**
|
|
65
|
+
* Text:文本组件,对应底层 UiText
|
|
66
|
+
* 会把 children 转成 textContent 传给宿主环境
|
|
67
|
+
*/
|
|
68
|
+
export declare function Text(props: BaseUiProps<UiText>): React.ReactElement<CommonElementProps<UiText> & {
|
|
69
|
+
children?: React.ReactNode | undefined;
|
|
70
|
+
} & {
|
|
71
|
+
textContent?: string;
|
|
72
|
+
size?: Coord2;
|
|
73
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
74
|
+
/**
|
|
75
|
+
* Image:图片组件,对应底层 UiImage
|
|
76
|
+
*/
|
|
77
|
+
export declare function Image(props: ImageProps): React.ReactElement<CommonElementProps<UiImage> & {
|
|
78
|
+
children?: React.ReactNode | undefined;
|
|
79
|
+
} & {
|
|
80
|
+
/** 图片加载完成事件处理器 */
|
|
81
|
+
onLoad?: (event: UiEvent<UiImage>) => void;
|
|
82
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
83
|
+
/**
|
|
84
|
+
* Input:输入框组件,对应底层 UiInput
|
|
85
|
+
*/
|
|
86
|
+
export declare function Input(props: InputProps): React.ReactElement<CommonElementProps<UiInput> & {
|
|
87
|
+
children?: React.ReactNode | undefined;
|
|
88
|
+
} & {
|
|
89
|
+
/** 获得焦点事件处理器 */
|
|
90
|
+
onFocus?: (event: UiEvent<UiInput>) => void;
|
|
91
|
+
/** 失去焦点事件处理器 */
|
|
92
|
+
onBlur?: (event: UiEvent<UiInput>) => void;
|
|
93
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
94
|
+
/**
|
|
95
|
+
* ScrollBox:可滚动容器组件,对应底层 UiScrollBox
|
|
96
|
+
*/
|
|
97
|
+
export declare function ScrollBox(props: BaseUiProps<UiScrollBox>): React.ReactElement<CommonElementProps<UiScrollBox> & {
|
|
98
|
+
children?: React.ReactNode | undefined;
|
|
99
|
+
}, string | React.JSXElementConstructor<any>>;
|
|
100
|
+
export {};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Box:基础容器组件,对应底层 UiBox
|
|
4
|
+
*/
|
|
5
|
+
export function Box(props) {
|
|
6
|
+
return React.createElement('Box', props);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Text:文本组件,对应底层 UiText
|
|
10
|
+
* 会把 children 转成 textContent 传给宿主环境
|
|
11
|
+
*/
|
|
12
|
+
export function Text(props) {
|
|
13
|
+
const { children, ...rest } = props;
|
|
14
|
+
const nextProps = {
|
|
15
|
+
...rest,
|
|
16
|
+
};
|
|
17
|
+
if (children) {
|
|
18
|
+
nextProps.textContent = children.toString();
|
|
19
|
+
}
|
|
20
|
+
return React.createElement('Text', nextProps);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Image:图片组件,对应底层 UiImage
|
|
24
|
+
*/
|
|
25
|
+
export function Image(props) {
|
|
26
|
+
return React.createElement('Image', props);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Input:输入框组件,对应底层 UiInput
|
|
30
|
+
*/
|
|
31
|
+
export function Input(props) {
|
|
32
|
+
return React.createElement('Input', props);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* ScrollBox:可滚动容器组件,对应底层 UiScrollBox
|
|
36
|
+
*/
|
|
37
|
+
export function ScrollBox(props) {
|
|
38
|
+
return React.createElement('ScrollBox', props);
|
|
39
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Type = 'Box' | 'Text' | 'Image' | 'Input' | 'ScrollBox';
|
|
2
|
+
type Props = Record<string, unknown>;
|
|
3
|
+
type Container = UiScreen;
|
|
4
|
+
type Instance = UiElement;
|
|
5
|
+
type TextInstance = never;
|
|
6
|
+
type PublicInstance = Instance;
|
|
7
|
+
type HostContext = object;
|
|
8
|
+
type UpdatePayload = Record<string, unknown> | null;
|
|
9
|
+
declare const hostConfig: {
|
|
10
|
+
now: () => number;
|
|
11
|
+
supportsMutation: boolean;
|
|
12
|
+
supportsPersistence: boolean;
|
|
13
|
+
supportsHydration: boolean;
|
|
14
|
+
scheduleTimeout: typeof setTimeout;
|
|
15
|
+
cancelTimeout: typeof clearTimeout;
|
|
16
|
+
noTimeout: number;
|
|
17
|
+
isPrimaryRenderer: boolean;
|
|
18
|
+
warnsIfNotActing: boolean;
|
|
19
|
+
supportsMicrotasks: boolean;
|
|
20
|
+
scheduleMicrotask: (callback: () => void) => void;
|
|
21
|
+
getRootHostContext(): HostContext;
|
|
22
|
+
getChildHostContext(parentHostContext: HostContext): HostContext;
|
|
23
|
+
shouldSetTextContent(): boolean;
|
|
24
|
+
createInstance(type: Type, props: Props): Instance;
|
|
25
|
+
createTextInstance(): TextInstance;
|
|
26
|
+
appendInitialChild(parentInstance: Instance, child: Instance): void;
|
|
27
|
+
finalizeInitialChildren(): boolean;
|
|
28
|
+
prepareUpdate(instance: Instance, type: Type, oldProps: Props, newProps: Props): UpdatePayload;
|
|
29
|
+
commitUpdate(instance: Instance, updatePayload: UpdatePayload, _type: Type, oldProps: Props, newProps: Props): void;
|
|
30
|
+
appendChild(parentInstance: Instance, child: Instance): void;
|
|
31
|
+
appendChildToContainer(container: Container, child: Instance): void;
|
|
32
|
+
removeChild(parentInstance: Instance, child: Instance): void;
|
|
33
|
+
removeChildFromContainer(container: Container, child: Instance): void;
|
|
34
|
+
clearContainer(): void;
|
|
35
|
+
prepareForCommit(): unknown;
|
|
36
|
+
resetAfterCommit(): void;
|
|
37
|
+
getPublicInstance(instance: Instance): PublicInstance;
|
|
38
|
+
commitTextUpdate(): void;
|
|
39
|
+
resetTextContent(): void;
|
|
40
|
+
hideInstance(): void;
|
|
41
|
+
hideTextInstance(): void;
|
|
42
|
+
unhideInstance(): void;
|
|
43
|
+
unhideTextInstance(): void;
|
|
44
|
+
};
|
|
45
|
+
export default hostConfig;
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
// 保存每个节点的生命周期回调
|
|
2
|
+
const nodeLifecycleCallbacks = new WeakMap();
|
|
3
|
+
// 记录已经触发过 onMount 的节点,避免重复调用
|
|
4
|
+
const mountedNodes = new WeakSet();
|
|
5
|
+
// 辅助:处理 Coord2(position / size)
|
|
6
|
+
function applyCoordProperty(nodeCoord, styleCoord) {
|
|
7
|
+
if (!styleCoord) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
// 处理偏移
|
|
11
|
+
if (styleCoord.offset) {
|
|
12
|
+
if (typeof styleCoord.offset.copy === 'function') {
|
|
13
|
+
nodeCoord.offset.copy(styleCoord.offset);
|
|
14
|
+
}
|
|
15
|
+
else if (typeof styleCoord.offset === 'object') {
|
|
16
|
+
nodeCoord.offset.x = styleCoord.offset.x ?? nodeCoord.offset.x;
|
|
17
|
+
nodeCoord.offset.y = styleCoord.offset.y ?? nodeCoord.offset.y;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
// 处理缩放
|
|
21
|
+
if (styleCoord.scale) {
|
|
22
|
+
if (typeof styleCoord.scale.copy === 'function') {
|
|
23
|
+
nodeCoord.scale.copy(styleCoord.scale);
|
|
24
|
+
}
|
|
25
|
+
else if (typeof styleCoord.scale === 'object') {
|
|
26
|
+
nodeCoord.scale.x = styleCoord.scale.x ?? nodeCoord.scale.x;
|
|
27
|
+
nodeCoord.scale.y = styleCoord.scale.y ?? nodeCoord.scale.y;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// 事件处理器缓存:用于在更新时解绑旧的监听器
|
|
32
|
+
const nodeEventHandlers = new WeakMap();
|
|
33
|
+
// React 风格事件名到底层 UI 事件名的映射
|
|
34
|
+
const eventNameMap = {
|
|
35
|
+
onClick: 'pointerdown',
|
|
36
|
+
onMouseDown: 'pointerdown',
|
|
37
|
+
onMouseUp: 'pointerup',
|
|
38
|
+
onFocus: 'focus',
|
|
39
|
+
onBlur: 'blur',
|
|
40
|
+
onLoad: 'load',
|
|
41
|
+
};
|
|
42
|
+
// 辅助:处理颜色 Vec3(xxxColor)
|
|
43
|
+
function applyColorProperty(nodeColor, styleColor) {
|
|
44
|
+
if (!styleColor) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (typeof styleColor.copy === 'function') {
|
|
48
|
+
nodeColor.copy(styleColor);
|
|
49
|
+
}
|
|
50
|
+
else if (typeof styleColor === 'object' &&
|
|
51
|
+
'r' in styleColor &&
|
|
52
|
+
'g' in styleColor &&
|
|
53
|
+
'b' in styleColor) {
|
|
54
|
+
nodeColor.r = styleColor.r;
|
|
55
|
+
nodeColor.g = styleColor.g;
|
|
56
|
+
nodeColor.b = styleColor.b;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function applyProps(instance, _oldProps, newProps) {
|
|
60
|
+
const target = instance;
|
|
61
|
+
// 1. 先单独处理 style
|
|
62
|
+
const styleProp = newProps.style;
|
|
63
|
+
if (styleProp && typeof styleProp === 'object') {
|
|
64
|
+
const style = styleProp;
|
|
65
|
+
// 先处理 position / size 这类复合属性
|
|
66
|
+
if (style.position && 'position' in instance) {
|
|
67
|
+
applyCoordProperty(instance.position, style.position);
|
|
68
|
+
}
|
|
69
|
+
if (style.size && 'size' in instance) {
|
|
70
|
+
applyCoordProperty(instance.size, style.size);
|
|
71
|
+
}
|
|
72
|
+
// 处理其余样式字段
|
|
73
|
+
for (const styleKey in style) {
|
|
74
|
+
const styleValue = style[styleKey];
|
|
75
|
+
// 已在上面单独处理
|
|
76
|
+
if (styleKey === 'position' || styleKey === 'size') {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// 如果实例上没有这个字段,跳过
|
|
80
|
+
if (!(styleKey in target)) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const targetField = target[styleKey];
|
|
84
|
+
if (styleValue && typeof styleValue === 'object') {
|
|
85
|
+
// 颜色属性:xxxColor
|
|
86
|
+
if (styleKey.toLowerCase().includes('color') &&
|
|
87
|
+
typeof targetField === 'object') {
|
|
88
|
+
applyColorProperty(targetField, styleValue);
|
|
89
|
+
}
|
|
90
|
+
else if (targetField &&
|
|
91
|
+
typeof targetField === 'object' &&
|
|
92
|
+
typeof targetField.copy === 'function') {
|
|
93
|
+
// 具有 copy 方法的对象(Vec2 / Vec3 等)
|
|
94
|
+
targetField.copy(styleValue);
|
|
95
|
+
}
|
|
96
|
+
else if (targetField && typeof targetField === 'object') {
|
|
97
|
+
// 其他对象,做浅合并
|
|
98
|
+
Object.assign(targetField, styleValue);
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
// 否则直接赋值
|
|
102
|
+
target[styleKey] = styleValue;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
// 简单类型直接赋值
|
|
107
|
+
target[styleKey] = styleValue;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// 2. 再处理除 style 以外的普通 props
|
|
112
|
+
for (const key in newProps) {
|
|
113
|
+
if (key === 'style' || key === 'children' || key === 'ref') {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (key.startsWith('on')) {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const value = newProps[key];
|
|
120
|
+
if (key in target) {
|
|
121
|
+
target[key] = value;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// 3. 处理事件 props(onClick / onMouseDown / onMouseUp)
|
|
125
|
+
const node = instance;
|
|
126
|
+
const eventsHost = node.events;
|
|
127
|
+
if (eventsHost && typeof eventsHost === 'object') {
|
|
128
|
+
// 先解绑旧的事件监听器
|
|
129
|
+
const prevHandlers = nodeEventHandlers.get(node);
|
|
130
|
+
if (prevHandlers) {
|
|
131
|
+
for (const [eventName, handler] of Object.entries(prevHandlers)) {
|
|
132
|
+
if (typeof eventsHost.off === 'function') {
|
|
133
|
+
eventsHost.off(eventName, handler);
|
|
134
|
+
}
|
|
135
|
+
else if (typeof eventsHost.remove === 'function') {
|
|
136
|
+
eventsHost.remove(eventName, handler);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const nextHandlers = {};
|
|
141
|
+
for (const key in newProps) {
|
|
142
|
+
if (!key.startsWith('on')) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const handler = newProps[key];
|
|
146
|
+
if (typeof handler !== 'function') {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const eventName = eventNameMap[key] || key.toLowerCase().substring(2);
|
|
150
|
+
if (typeof eventsHost.on === 'function') {
|
|
151
|
+
eventsHost.on(eventName, handler);
|
|
152
|
+
nextHandlers[eventName] = handler;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (Object.keys(nextHandlers).length > 0) {
|
|
156
|
+
nodeEventHandlers.set(node, nextHandlers);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
nodeEventHandlers.delete(node);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
// 4. 处理生命周期回调(onMount / onUnmount)
|
|
163
|
+
const lifecycle = nodeLifecycleCallbacks.get(node) ?? {};
|
|
164
|
+
if (typeof newProps.onMount === 'function') {
|
|
165
|
+
lifecycle.onMount = newProps.onMount;
|
|
166
|
+
}
|
|
167
|
+
if (typeof newProps.onUnmount === 'function') {
|
|
168
|
+
lifecycle.onUnmount = newProps.onUnmount;
|
|
169
|
+
}
|
|
170
|
+
if (lifecycle.onMount || lifecycle.onUnmount) {
|
|
171
|
+
nodeLifecycleCallbacks.set(node, lifecycle);
|
|
172
|
+
}
|
|
173
|
+
// 首次挂载后异步触发 onMount,避免在尚未完全挂载时执行用户回调
|
|
174
|
+
if (!mountedNodes.has(node) && lifecycle.onMount) {
|
|
175
|
+
mountedNodes.add(node);
|
|
176
|
+
const { onMount } = lifecycle;
|
|
177
|
+
setTimeout(() => {
|
|
178
|
+
// 确保节点仍然挂在树上再调用回调
|
|
179
|
+
if (node.parent && onMount) {
|
|
180
|
+
onMount(node);
|
|
181
|
+
}
|
|
182
|
+
}, 0);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// 辅助:执行 onUnmount 并清理生命周期相关状态
|
|
186
|
+
function callUnmountLifecycle(node) {
|
|
187
|
+
const lifecycle = nodeLifecycleCallbacks.get(node);
|
|
188
|
+
if (lifecycle?.onUnmount) {
|
|
189
|
+
lifecycle.onUnmount(node);
|
|
190
|
+
}
|
|
191
|
+
nodeLifecycleCallbacks.delete(node);
|
|
192
|
+
mountedNodes.delete(node);
|
|
193
|
+
nodeEventHandlers.delete(node);
|
|
194
|
+
}
|
|
195
|
+
// HostConfig 仅在运行时被 react-reconciler 使用,这里让 TS 按对象字面量推断类型,避免显式 any
|
|
196
|
+
const hostConfig = {
|
|
197
|
+
// 时间相关能力,给 React 调度使用
|
|
198
|
+
now: Date.now,
|
|
199
|
+
// 采用 mutation 模式(直接修改现有节点)
|
|
200
|
+
supportsMutation: true,
|
|
201
|
+
// 不使用持久化 / Hydration,这里显式关闭
|
|
202
|
+
supportsPersistence: false,
|
|
203
|
+
supportsHydration: false,
|
|
204
|
+
// 定时器调度:React 18 在某些场景会调用这些方法
|
|
205
|
+
scheduleTimeout: setTimeout,
|
|
206
|
+
cancelTimeout: clearTimeout,
|
|
207
|
+
noTimeout: -1,
|
|
208
|
+
// 其它运行时能力标志
|
|
209
|
+
isPrimaryRenderer: true,
|
|
210
|
+
warnsIfNotActing: false,
|
|
211
|
+
supportsMicrotasks: true,
|
|
212
|
+
scheduleMicrotask: (callback) => {
|
|
213
|
+
// 使用 Promise 模拟 microtask 队列
|
|
214
|
+
Promise.resolve().then(callback);
|
|
215
|
+
},
|
|
216
|
+
getRootHostContext() {
|
|
217
|
+
return {};
|
|
218
|
+
},
|
|
219
|
+
getChildHostContext(parentHostContext) {
|
|
220
|
+
return parentHostContext;
|
|
221
|
+
},
|
|
222
|
+
shouldSetTextContent() {
|
|
223
|
+
return false;
|
|
224
|
+
},
|
|
225
|
+
createInstance(type, props) {
|
|
226
|
+
let node;
|
|
227
|
+
switch (type) {
|
|
228
|
+
case 'Box':
|
|
229
|
+
node = UiBox.create();
|
|
230
|
+
break;
|
|
231
|
+
case 'Text':
|
|
232
|
+
node = UiText.create();
|
|
233
|
+
break;
|
|
234
|
+
case 'Image':
|
|
235
|
+
node = UiImage.create();
|
|
236
|
+
break;
|
|
237
|
+
case 'Input':
|
|
238
|
+
node = UiInput.create();
|
|
239
|
+
break;
|
|
240
|
+
case 'ScrollBox':
|
|
241
|
+
node = UiScrollBox.create();
|
|
242
|
+
break;
|
|
243
|
+
default:
|
|
244
|
+
throw new Error(`Unknown type: ${type}`);
|
|
245
|
+
}
|
|
246
|
+
applyProps(node, {}, props);
|
|
247
|
+
return node;
|
|
248
|
+
},
|
|
249
|
+
createTextInstance() {
|
|
250
|
+
throw new Error('Text nodes are not supported; use <Text> component instead.');
|
|
251
|
+
},
|
|
252
|
+
appendInitialChild(parentInstance, child) {
|
|
253
|
+
child.parent = parentInstance;
|
|
254
|
+
},
|
|
255
|
+
finalizeInitialChildren() {
|
|
256
|
+
return false;
|
|
257
|
+
},
|
|
258
|
+
prepareUpdate(instance, type, oldProps, newProps) {
|
|
259
|
+
return newProps;
|
|
260
|
+
},
|
|
261
|
+
commitUpdate(instance, updatePayload, _type, oldProps, newProps) {
|
|
262
|
+
if (!updatePayload) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
applyProps(instance, oldProps, newProps);
|
|
266
|
+
},
|
|
267
|
+
appendChild(parentInstance, child) {
|
|
268
|
+
child.parent = parentInstance;
|
|
269
|
+
},
|
|
270
|
+
appendChildToContainer(container, child) {
|
|
271
|
+
child.parent = container;
|
|
272
|
+
},
|
|
273
|
+
removeChild(parentInstance, child) {
|
|
274
|
+
callUnmountLifecycle(child);
|
|
275
|
+
child.parent = undefined;
|
|
276
|
+
},
|
|
277
|
+
removeChildFromContainer(container, child) {
|
|
278
|
+
callUnmountLifecycle(child);
|
|
279
|
+
child.parent = undefined;
|
|
280
|
+
},
|
|
281
|
+
clearContainer() { },
|
|
282
|
+
prepareForCommit() {
|
|
283
|
+
return null;
|
|
284
|
+
},
|
|
285
|
+
resetAfterCommit() { },
|
|
286
|
+
getPublicInstance(instance) {
|
|
287
|
+
return instance;
|
|
288
|
+
},
|
|
289
|
+
commitTextUpdate() { },
|
|
290
|
+
resetTextContent() { },
|
|
291
|
+
hideInstance() { },
|
|
292
|
+
hideTextInstance() { },
|
|
293
|
+
unhideInstance() { },
|
|
294
|
+
unhideTextInstance() { },
|
|
295
|
+
};
|
|
296
|
+
export default hostConfig;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* createRoot:在指定的 UI 节点上创建一个 React 根容器
|
|
4
|
+
*/
|
|
5
|
+
export declare function createRoot(container: UiNode): {
|
|
6
|
+
render(element: React.ReactNode): void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* render:一个简单的快捷方法,等价于 createRoot(container).render(element)
|
|
10
|
+
*/
|
|
11
|
+
export declare function render(element: React.ReactNode, container: UiNode): {
|
|
12
|
+
render(element: React.ReactNode): void;
|
|
13
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import reconciler from './reconciler';
|
|
2
|
+
/**
|
|
3
|
+
* createRoot:在指定的 UI 节点上创建一个 React 根容器
|
|
4
|
+
*/
|
|
5
|
+
export function createRoot(container) {
|
|
6
|
+
const createContainer = reconciler.createContainer;
|
|
7
|
+
const root = createContainer(container, 0, null, false, null, '', () => null, null);
|
|
8
|
+
// 返回一个带有 render 方法的对象,兼容 React 18 的 root API
|
|
9
|
+
return {
|
|
10
|
+
// 调用时会触发一次完整的协调与渲染
|
|
11
|
+
render(element) {
|
|
12
|
+
reconciler.updateContainer(element, root, null, () => null);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* render:一个简单的快捷方法,等价于 createRoot(container).render(element)
|
|
18
|
+
*/
|
|
19
|
+
export function render(element, container) {
|
|
20
|
+
const root = createRoot(container);
|
|
21
|
+
root.render(element);
|
|
22
|
+
return root;
|
|
23
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@box3lab/react-ui",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "npm-run-all -p dev:*",
|
|
7
|
+
"build": "npm-run-all build:*",
|
|
8
|
+
"tsc": "npm-run-all -p tsc:*",
|
|
9
|
+
"tsc:check": "npm-run-all tsc:check:*",
|
|
10
|
+
"eslint:fix": "eslint --fix",
|
|
11
|
+
"prettier:write": "prettier . --write",
|
|
12
|
+
"tsc:client": "tsc -p client/tsconfig.json",
|
|
13
|
+
"tsc:server": "tsc -p server/tsconfig.json",
|
|
14
|
+
"tsc:check:client": "tsc -noEmit -p client/tsconfig.json",
|
|
15
|
+
"tsc:check:server": "tsc -noEmit -p server/tsconfig.json",
|
|
16
|
+
"dev:server": "cross-env VITE_BUILD_TARGET=server vite build --mode development --watch",
|
|
17
|
+
"dev:client": "cross-env VITE_BUILD_TARGET=client vite build --mode development --watch",
|
|
18
|
+
"build:server": "cross-env VITE_BUILD_TARGET=server vite build --mode production",
|
|
19
|
+
"build:client": "cross-env VITE_BUILD_TARGET=client vite build --mode production",
|
|
20
|
+
"debug:server": "cross-env VITE_BUILD_TARGET=server vite build --mode debug",
|
|
21
|
+
"debug:client": "cross-env VITE_BUILD_TARGET=client vite build --mode debug",
|
|
22
|
+
"prepare": "husky"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/node": "^25.0.2",
|
|
26
|
+
"@types/react": "^19.2.7",
|
|
27
|
+
"@types/react-reconciler": "^0.32.3",
|
|
28
|
+
"cross-env": "^10.1.0",
|
|
29
|
+
"eslint": "^9.39.2",
|
|
30
|
+
"husky": "^9.1.7",
|
|
31
|
+
"npm-run-all": "^4.1.5",
|
|
32
|
+
"prettier": "^3.7.4",
|
|
33
|
+
"typescript": "^5.9.3",
|
|
34
|
+
"typescript-eslint": "^8.49.0",
|
|
35
|
+
"vite": "^5.4.21",
|
|
36
|
+
"vite-plugin-arenapro-script": "^0.2.8",
|
|
37
|
+
"vite-plugin-checker": "^0.11.0",
|
|
38
|
+
"vite-tsconfig-paths": "^5.1.4"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"react": "18.3.1",
|
|
42
|
+
"react-reconciler": "^0.29.2"
|
|
43
|
+
},
|
|
44
|
+
"exports": {
|
|
45
|
+
".": {
|
|
46
|
+
"import": "./dist/client/src/elements.js",
|
|
47
|
+
"types": "./dist/client/src/elements.d.ts"
|
|
48
|
+
},
|
|
49
|
+
"./dom": {
|
|
50
|
+
"import": "./dist/client/src/renderer.js",
|
|
51
|
+
"types": "./dist/client/src/renderer.d.ts"
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"files": [
|
|
55
|
+
"dist/**/*"
|
|
56
|
+
]
|
|
57
|
+
}
|