weifuwu 0.50.0 → 0.51.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 +50 -52
- package/dist/client/app.d.ts +9 -3
- package/dist/client/index.js +134 -36
- package/dist/client/render.d.ts +1 -0
- package/dist/client/types.d.ts +11 -5
- package/dist/client/vnode.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ npm install weifuwu
|
|
|
16
16
|
|
|
17
17
|
**两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook。
|
|
18
18
|
|
|
19
|
-
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val`
|
|
19
|
+
**Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val` 自动触发当前组件的 VDOM patch。也支持手动 `ctx.ui.render()` 精确控制渲染时机。无需手动调用 `useState`/`useEffect`。
|
|
20
20
|
|
|
21
21
|
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` 等),Handler/组件从 `ctx` 读取。
|
|
22
22
|
|
|
@@ -116,7 +116,7 @@ createApp()
|
|
|
116
116
|
|------|------|------|
|
|
117
117
|
| 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
|
|
118
118
|
| 读取 | handler 读取 ctx | 组件读取 ctx |
|
|
119
|
-
| 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()`
|
|
119
|
+
| 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()` / `$.x = val` 触发局部 VDOM patch |
|
|
120
120
|
|
|
121
121
|
### Closeable 接口
|
|
122
122
|
|
|
@@ -812,11 +812,14 @@ h('div', { class: 'x' }, child1, child2)
|
|
|
812
812
|
|
|
813
813
|
### Render 机制总览
|
|
814
814
|
|
|
815
|
-
| API | 触发时机 | 渲染方式 | 使用场景 |
|
|
816
|
-
|
|
817
|
-
| `$.x = val` | 赋值后自动 | 微任务批量(异步) | **日常 UI 状态** —
|
|
818
|
-
| `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | **绕过 Proxy 后手动标记**
|
|
819
|
-
| `ctx.ui.render()` | 主动调用 | 立即同步 | **需要立即拿到最新 DOM** — DOM
|
|
815
|
+
| API | 触发时机 | 渲染方式 | 作用域 | 使用场景 |
|
|
816
|
+
|------|---------|---------|--------|---------|
|
|
817
|
+
| `$.x = val` | 赋值后自动 | 微任务批量(异步) | 当前组件 | **日常 UI 状态** — 表单输入、切换开关、异步数据加载等 |
|
|
818
|
+
| `ctx.ui.dirty()` | 主动调用 | 微任务批量(异步) | 当前/指定 | **绕过 Proxy 后手动标记** |
|
|
819
|
+
| `ctx.ui.render()` | 主动调用 | 立即同步 | 当前/指定 | **需要立即拿到最新 DOM** — DOM 测量、动画触发 |
|
|
820
|
+
| `ctx.ui.render(['id'])` | 主动调用 | 立即同步 | 指定组件 | **跨组件精准刷新** — 全局事件、Portal 远程控制 |
|
|
821
|
+
|
|
822
|
+
`render()` 和 `dirty()` 无参 = 当前组件,传参 = 指定组件列表。三套 API 同一 scope 机制。
|
|
820
823
|
|
|
821
824
|
### 闭包变量 + `ctx.ui.render()`(简单场景)
|
|
822
825
|
|
|
@@ -862,23 +865,13 @@ const FormPage: Component = (_init, ctx) => {
|
|
|
862
865
|
- 不需要触发渲染的内部缓存(用闭包变量 `let`)
|
|
863
866
|
- 简单组件只有一两个状态变量(闭包变量 + `render()` 更轻量)
|
|
864
867
|
|
|
865
|
-
### `ctx.ui.dirty()` —
|
|
866
|
-
|
|
867
|
-
当你绕过 Proxy 直接操作底层数据后,调用 `dirty()` 通知框架在下个微任务批量重渲染:
|
|
868
|
-
|
|
869
|
-
```tsx
|
|
870
|
-
// 实际场景:在 mount 阶段需要手动触发渲染
|
|
871
|
-
// mount 期间 $.x = val 自动静默(不触发渲染)
|
|
872
|
-
$.initialized = true
|
|
873
|
-
// 如果非要在这里触发渲染,需要手动调用 dirty():
|
|
874
|
-
ctx.ui.dirty()
|
|
875
|
-
```
|
|
868
|
+
### `ctx.ui.dirty()` — 异步标记脏
|
|
876
869
|
|
|
877
|
-
|
|
870
|
+
异步版本,无参 = 当前组件,传参 = 指定组件列表。多次调用合并为一次微任务渲染。`$` 内部就是调 `dirty()`。
|
|
878
871
|
|
|
879
872
|
### `ctx.ui.render()` — 同步强制渲染
|
|
880
873
|
|
|
881
|
-
与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM
|
|
874
|
+
与 `dirty()` 的微任务批量不同,`render()` 是**同步执行**的。调用后立即执行 VDOM diff + patch,DOM 立刻更新。无参时只刷新当前组件,传参时可精准刷新指定组件。
|
|
882
875
|
|
|
883
876
|
**何时必须用 `render()`**:
|
|
884
877
|
|
|
@@ -913,55 +906,57 @@ onClick: () => {
|
|
|
913
906
|
### 三种方式速查
|
|
914
907
|
|
|
915
908
|
```tsx
|
|
916
|
-
//
|
|
909
|
+
// 自动:$.x = val — 微任务批量,绑定当前组件
|
|
917
910
|
const $ = ctx.ui.$()
|
|
918
911
|
$.count++
|
|
919
|
-
$.name = 'hello' //
|
|
912
|
+
$.name = 'hello' // 多次赋值合并为一次渲染
|
|
920
913
|
|
|
921
|
-
//
|
|
914
|
+
// 手动:ctx.ui.render() — 同步,无参=当前,传参=指定
|
|
922
915
|
let count = 0
|
|
923
916
|
count++
|
|
924
917
|
ctx.ui.render() // DOM 立刻更新
|
|
918
|
+
ctx.ui.render(['stats']) // 精准刷新指定组件
|
|
925
919
|
|
|
926
|
-
//
|
|
920
|
+
// 异步:ctx.ui.dirty() — 微任务批量,同 render() 作用域
|
|
921
|
+
ctx.ui.dirty()
|
|
922
|
+
ctx.ui.dirty(['stats']) // 批处理合并
|
|
927
923
|
```
|
|
928
924
|
|
|
929
925
|
**性能说明**:
|
|
930
|
-
- `$.x = val` 和 `dirty()`
|
|
931
|
-
- `render()` 每次调用都触发一次完整 diff/patch
|
|
926
|
+
- `$.x = val` 和 `dirty()` 都是微任务批量合并
|
|
927
|
+
- `render()` 每次调用都触发一次完整 diff/patch
|
|
928
|
+
- 三个入口同一套 scope 机制,不想要的渲染不触发
|
|
932
929
|
|
|
933
|
-
###
|
|
930
|
+
### 实践建议
|
|
934
931
|
|
|
935
|
-
|
|
932
|
+
**组件库**(可分享组件)推荐手动模式:
|
|
936
933
|
|
|
937
|
-
|
|
934
|
+
```tsx
|
|
935
|
+
const DatePicker = (_init, ctx) => {
|
|
936
|
+
let show = false // let 不触发渲染
|
|
937
|
+
return (props) =>
|
|
938
|
+
h('input', {
|
|
939
|
+
onClick: () => { show = true; ctx.ui.render() }
|
|
940
|
+
})
|
|
941
|
+
}
|
|
942
|
+
```
|
|
943
|
+
|
|
944
|
+
行为只由 `render()` 显式控制,不依赖 `$`,测试中 `render()` 直接 mock 为空函数。
|
|
945
|
+
|
|
946
|
+
**业务层**推荐自动模式:
|
|
938
947
|
|
|
939
948
|
```tsx
|
|
940
|
-
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
return {
|
|
945
|
-
add(item: ToastItem) {
|
|
946
|
-
items = [...items, item]
|
|
947
|
-
ctx.ui.render() // 显式同步渲染,确保 DOM 立即可见
|
|
948
|
-
},
|
|
949
|
-
remove(id: string) {
|
|
950
|
-
items = items.filter(i => i.id !== id)
|
|
951
|
-
ctx.ui.dirty() // 显式标记脏,下个微任务批量渲染
|
|
952
|
-
},
|
|
953
|
-
render: (props) =>
|
|
954
|
-
h('div', { class: 'toast-container' },
|
|
955
|
-
items.map(item => h('div', { key: item.id }, item.msg))
|
|
956
|
-
),
|
|
957
|
-
}
|
|
949
|
+
const OrderPage = (_init, ctx) => {
|
|
950
|
+
const $ = ctx.ui.$
|
|
951
|
+
$.orders = [] // $ 赋值自动触发渲染
|
|
952
|
+
$.loading = false
|
|
953
|
+
return (props) => h('div', {}, $.loading ? h(Spinner) : h(OrderList, { orders: $.orders }))
|
|
958
954
|
}
|
|
959
955
|
```
|
|
960
956
|
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
- 消费方不需要知道组件内部用 `$` 还是闭包,只需调用 API
|
|
957
|
+
省事、安全、`$` 绑定所属组件不波及兄弟。
|
|
958
|
+
|
|
959
|
+
同一个组件内可以按变量混用两种模式:需要渲染的用 `$`,不需要的用 `let`。
|
|
965
960
|
|
|
966
961
|
---
|
|
967
962
|
|
|
@@ -1282,7 +1277,7 @@ createApp()
|
|
|
1282
1277
|
|
|
1283
1278
|
// 运行时切换语言
|
|
1284
1279
|
ctx.i18n?.setLocale('en-US')
|
|
1285
|
-
// →
|
|
1280
|
+
// → 自动触发根组件重渲染(所有组件使用新语言文案)
|
|
1286
1281
|
```
|
|
1287
1282
|
|
|
1288
1283
|
| I18nOptions | 类型 | 默认值 | 说明 |
|
|
@@ -1574,6 +1569,9 @@ props 变化 ──────────────────────
|
|
|
1574
1569
|
| `onmounted` | `ref` 的 `if (el)` 分支 |
|
|
1575
1570
|
| `onunmount` | `ref` 的 `else` 分支 |
|
|
1576
1571
|
| `onupdate` | render 内层函数收新 props 自行比较 |
|
|
1572
|
+
| `全局刷新` | `ctx.ui.render(['_wf_root'])` |
|
|
1573
|
+
| `局部刷新` | `ctx.ui.render()` 或 `$.x = val` |
|
|
1574
|
+
| `跨组件刷新` | `ctx.ui.selfId('name')` + `render(['name'])` |
|
|
1577
1575
|
|
|
1578
1576
|
## 组件列表
|
|
1579
1577
|
|
package/dist/client/app.d.ts
CHANGED
|
@@ -4,9 +4,15 @@
|
|
|
4
4
|
* createApp() → app.use(mw) → app.mount('#root', RootComponent)
|
|
5
5
|
*
|
|
6
6
|
* ctx.ui 在 mount 时注入:
|
|
7
|
-
* ctx.ui.render()
|
|
8
|
-
* ctx.ui.
|
|
9
|
-
* ctx.ui
|
|
7
|
+
* ctx.ui.render() 同步刷新当前组件
|
|
8
|
+
* ctx.ui.render(['#id']) 同步刷新指定组件
|
|
9
|
+
* ctx.ui.dirty() 异步刷新当前组件(微任务批处理)
|
|
10
|
+
* ctx.ui.$() 响应式状态容器($.x = val 自动 dirty)
|
|
11
|
+
*
|
|
12
|
+
* render / dirty / $ 通过 prototype chain 实现组件级 scope:
|
|
13
|
+
* 每个组件 mount 时创建 childCtx.ui = Object.create(ctx.ui)
|
|
14
|
+
* 并设置 childCtx.ui._selfId = 组件 ID
|
|
15
|
+
* render() 无参时从 this._selfId 取当前组件 ID
|
|
10
16
|
*/
|
|
11
17
|
import type { WfuiContext, AppMiddleware } from './types.ts';
|
|
12
18
|
import type { Component } from './vnode.ts';
|
package/dist/client/index.js
CHANGED
|
@@ -39,6 +39,8 @@ function createPortal(children, portalKey) {
|
|
|
39
39
|
// src/client/render.ts
|
|
40
40
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
41
41
|
var SVG_TAGS = /* @__PURE__ */ new Set(["svg", "path", "circle", "line", "rect", "text", "g", "polyline", "polygon", "ellipse", "defs", "use", "clipPath", "mask", "linearGradient", "radialGradient", "stop", "tspan"]);
|
|
42
|
+
var _idCounter = 0;
|
|
43
|
+
var idRegistry = /* @__PURE__ */ new Map();
|
|
42
44
|
function render(input, ctx) {
|
|
43
45
|
return renderValue(input, ctx);
|
|
44
46
|
}
|
|
@@ -76,7 +78,15 @@ function renderValue(v, ctx) {
|
|
|
76
78
|
} else {
|
|
77
79
|
const flatChildren = flattenChildren(vnode.props?.children);
|
|
78
80
|
for (const child of flatChildren) {
|
|
79
|
-
|
|
81
|
+
const childNode = renderValue(child, ctx);
|
|
82
|
+
el.appendChild(childNode);
|
|
83
|
+
if (child && typeof child === "object" && typeof child.type === "function") {
|
|
84
|
+
const childVNode = child;
|
|
85
|
+
if (!childVNode._parentNode) {
|
|
86
|
+
childVNode._parentNode = el;
|
|
87
|
+
childVNode._refNode = childNode;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
80
90
|
}
|
|
81
91
|
}
|
|
82
92
|
if (selectValue !== void 0) {
|
|
@@ -89,9 +99,17 @@ function renderValue(v, ctx) {
|
|
|
89
99
|
function renderComponent(Comp, props, vnode, ctx) {
|
|
90
100
|
;
|
|
91
101
|
ctx.ui = ctx.ui ?? {};
|
|
102
|
+
if (!vnode._id) {
|
|
103
|
+
vnode._id = `_wf_${_idCounter++}`;
|
|
104
|
+
idRegistry.set(vnode._id, vnode);
|
|
105
|
+
}
|
|
106
|
+
const childCtx = Object.create(ctx);
|
|
107
|
+
childCtx.ui = Object.create(ctx.ui);
|
|
108
|
+
childCtx.ui._selfId = vnode._id;
|
|
109
|
+
childCtx.ui._selfVNode = vnode;
|
|
92
110
|
let childVNode;
|
|
93
111
|
try {
|
|
94
|
-
childVNode = Comp(props,
|
|
112
|
+
childVNode = Comp(props, childCtx);
|
|
95
113
|
if (typeof childVNode !== "function") {
|
|
96
114
|
throw new Error(
|
|
97
115
|
`Component ${Comp.name || "anonymous"} must return a render function. Use (init_props, ctx) => (props) => VNode pattern.`
|
|
@@ -236,14 +254,23 @@ function patchValue(parent, oldNode, oldInput, newInput, ctx) {
|
|
|
236
254
|
const oldV = oldInput;
|
|
237
255
|
if (typeof newV.type === "function") {
|
|
238
256
|
const comp = newV.type;
|
|
239
|
-
|
|
240
|
-
|
|
257
|
+
if (oldV._render) {
|
|
258
|
+
newV._render = oldV._render;
|
|
259
|
+
newV._id = oldV._id;
|
|
260
|
+
if (newV._id) idRegistry.set(newV._id, newV);
|
|
261
|
+
}
|
|
262
|
+
newV._parentNode = parent;
|
|
263
|
+
newV._refNode = oldNode;
|
|
264
|
+
const childCtx = Object.create(ctx);
|
|
265
|
+
childCtx.ui = Object.create(ctx.ui);
|
|
266
|
+
childCtx.ui._selfId = newV._id;
|
|
267
|
+
childCtx.ui._selfVNode = newV;
|
|
241
268
|
let childNew;
|
|
242
269
|
try {
|
|
243
270
|
if (typeof newV._render === "function") {
|
|
244
271
|
childNew = newV._render(newV.props);
|
|
245
272
|
} else {
|
|
246
|
-
childNew = comp(newV.props,
|
|
273
|
+
childNew = comp(newV.props, childCtx);
|
|
247
274
|
if (typeof childNew === "function") {
|
|
248
275
|
newV._render = childNew;
|
|
249
276
|
childNew = childNew(newV.props);
|
|
@@ -618,7 +645,49 @@ function createApp() {
|
|
|
618
645
|
let container = null;
|
|
619
646
|
let rootComponent = null;
|
|
620
647
|
let oldVNode = null;
|
|
621
|
-
let
|
|
648
|
+
let _rendering = false;
|
|
649
|
+
let _dirtyBatch = /* @__PURE__ */ new Set();
|
|
650
|
+
let _dirtyScheduled = false;
|
|
651
|
+
function renderByIds(ids) {
|
|
652
|
+
if (_rendering) return;
|
|
653
|
+
_rendering = true;
|
|
654
|
+
layoutDepth.delete(ctx);
|
|
655
|
+
for (const id of ids) {
|
|
656
|
+
const vnode = idRegistry.get(id);
|
|
657
|
+
if (!vnode || !vnode._render) continue;
|
|
658
|
+
const oldChild = vnode._child;
|
|
659
|
+
const newChild = vnode._render(vnode.props);
|
|
660
|
+
vnode._child = newChild;
|
|
661
|
+
if (vnode._parentNode) {
|
|
662
|
+
const newNode = patchValue(
|
|
663
|
+
vnode._parentNode,
|
|
664
|
+
vnode._refNode ?? null,
|
|
665
|
+
oldChild,
|
|
666
|
+
newChild,
|
|
667
|
+
ctx
|
|
668
|
+
);
|
|
669
|
+
if (newNode && newNode !== vnode._refNode) {
|
|
670
|
+
vnode._refNode = newNode;
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
_rendering = false;
|
|
675
|
+
flushDirtyBatch();
|
|
676
|
+
}
|
|
677
|
+
function flushDirtyBatch() {
|
|
678
|
+
if (_dirtyBatch.size > 0 && !_dirtyScheduled) {
|
|
679
|
+
_dirtyScheduled = true;
|
|
680
|
+
queueMicrotask(() => {
|
|
681
|
+
_dirtyScheduled = false;
|
|
682
|
+
const batch = [..._dirtyBatch];
|
|
683
|
+
_dirtyBatch.clear();
|
|
684
|
+
if (batch.length > 0) renderByIds(batch);
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function getSelfId(uiObj) {
|
|
689
|
+
return uiObj?._selfId ?? ctx.ui?._selfId;
|
|
690
|
+
}
|
|
622
691
|
const app = {
|
|
623
692
|
get ctx() {
|
|
624
693
|
return ctx;
|
|
@@ -636,43 +705,72 @@ function createApp() {
|
|
|
636
705
|
if (!el) throw new Error(`mount target not found: ${rootSelector}`);
|
|
637
706
|
container = el;
|
|
638
707
|
container.innerHTML = "";
|
|
639
|
-
let _dirty = false;
|
|
640
|
-
let _rendering = false;
|
|
641
|
-
const doRender = () => {
|
|
642
|
-
if (_rendering || !container || !rootComponent || !oldVNode) return;
|
|
643
|
-
_rendering = true;
|
|
644
|
-
layoutDepth.delete(ctx);
|
|
645
|
-
const newVNode = wrapComponent(rootComponent, ctx);
|
|
646
|
-
const oldNode = container.firstChild;
|
|
647
|
-
if (oldNode) {
|
|
648
|
-
patchValue(container, oldNode, oldVNode, newVNode, ctx);
|
|
649
|
-
}
|
|
650
|
-
oldVNode = newVNode;
|
|
651
|
-
_rendering = false;
|
|
652
|
-
};
|
|
653
|
-
const scheduleRender = () => {
|
|
654
|
-
if (_dirty || _rendering) return;
|
|
655
|
-
_dirty = true;
|
|
656
|
-
queueMicrotask(() => {
|
|
657
|
-
if (!_dirty) return;
|
|
658
|
-
_dirty = false;
|
|
659
|
-
doRender();
|
|
660
|
-
});
|
|
661
|
-
};
|
|
662
708
|
ctx.ui = {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
709
|
+
_selfId: "_wf_root",
|
|
710
|
+
/** 同步刷新(无参 = 当前组件,传参 = 指定组件列表) */
|
|
711
|
+
render: function(ids) {
|
|
712
|
+
if (!ids || ids.length === 0) {
|
|
713
|
+
const selfId = getSelfId(this);
|
|
714
|
+
if (selfId) ids = [selfId];
|
|
715
|
+
else return;
|
|
716
|
+
}
|
|
717
|
+
renderByIds(ids);
|
|
718
|
+
},
|
|
719
|
+
/** 异步刷新(微任务批处理,无参 = 当前组件) */
|
|
720
|
+
dirty: function(ids) {
|
|
721
|
+
if (_rendering) return;
|
|
722
|
+
if (!ids || ids.length === 0) {
|
|
723
|
+
const selfId = getSelfId(this);
|
|
724
|
+
if (selfId) ids = [selfId];
|
|
725
|
+
else return;
|
|
726
|
+
}
|
|
727
|
+
for (const id of ids) {
|
|
728
|
+
if (id) _dirtyBatch.add(id);
|
|
729
|
+
}
|
|
730
|
+
if (!_dirtyScheduled) {
|
|
731
|
+
_dirtyScheduled = true;
|
|
732
|
+
queueMicrotask(() => {
|
|
733
|
+
_dirtyScheduled = false;
|
|
734
|
+
const batch = [..._dirtyBatch];
|
|
735
|
+
_dirtyBatch.clear();
|
|
736
|
+
if (batch.length > 0) renderByIds(batch);
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
},
|
|
740
|
+
/** 创建响应式状态容器:$.x = val 自动触发 dirty() */
|
|
741
|
+
$: function() {
|
|
742
|
+
const selfId = getSelfId(this);
|
|
743
|
+
return createReactiveState(() => {
|
|
744
|
+
if (selfId) ctx.ui.dirty([selfId]);
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
/** 注册组件实例的自定义 ID(用于跨组件精准刷新) */
|
|
748
|
+
selfId: function(name) {
|
|
749
|
+
if (typeof name !== "string" || !name) {
|
|
750
|
+
throw new Error(`[weifuwu] selfId requires a non-empty string, got ${typeof name}`);
|
|
751
|
+
}
|
|
752
|
+
if (idRegistry.has(name)) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
`[weifuwu] Duplicate component ID: "${name}". Each component must have a unique custom ID.`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
const vnode = this._selfVNode;
|
|
758
|
+
if (!vnode) return;
|
|
759
|
+
vnode._customId = name;
|
|
760
|
+
idRegistry.set(name, vnode);
|
|
761
|
+
}
|
|
669
762
|
};
|
|
670
763
|
_rendering = true;
|
|
671
764
|
oldVNode = wrapComponent(RootComponent, ctx);
|
|
765
|
+
oldVNode._id = "_wf_root";
|
|
766
|
+
oldVNode._parentNode = container;
|
|
767
|
+
oldVNode._refNode = null;
|
|
768
|
+
idRegistry.set("_wf_root", oldVNode);
|
|
672
769
|
const node = render(oldVNode, ctx);
|
|
673
770
|
if (node instanceof Node) container.appendChild(node);
|
|
771
|
+
oldVNode._refNode = container.firstChild;
|
|
674
772
|
_rendering = false;
|
|
675
|
-
|
|
773
|
+
flushDirtyBatch();
|
|
676
774
|
},
|
|
677
775
|
destroy() {
|
|
678
776
|
if (container) container.innerHTML = "";
|
package/dist/client/render.d.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { VNode } from './vnode.ts';
|
|
14
14
|
import type { WfuiContext } from './types.ts';
|
|
15
|
+
export declare const idRegistry: Map<string, VNode>;
|
|
15
16
|
export declare function render(input: any, ctx: WfuiContext): Node;
|
|
16
17
|
export declare function patchValue(parent: Node, oldNode: Node | null, oldInput: any, newInput: any, ctx: WfuiContext): Node | null;
|
|
17
18
|
export declare function mountVNode(container: Element, vnode: VNode, ctx: WfuiContext): void;
|
package/dist/client/types.d.ts
CHANGED
|
@@ -6,12 +6,18 @@ export interface WfuiContext {
|
|
|
6
6
|
[key: string]: unknown;
|
|
7
7
|
/** UI 框架能力(由 createApp.mount 注入) */
|
|
8
8
|
ui: {
|
|
9
|
-
/**
|
|
10
|
-
render: () => void;
|
|
11
|
-
/**
|
|
12
|
-
dirty: () => void;
|
|
13
|
-
/** 创建响应式状态容器:$.x = val 自动触发 dirty()
|
|
9
|
+
/** 触发组件重渲染(同步,无参 = 当前组件) */
|
|
10
|
+
render: (ids?: string[]) => void;
|
|
11
|
+
/** 异步触发组件重渲染(微任务批处理,无参 = 当前组件) */
|
|
12
|
+
dirty: (ids?: string[]) => void;
|
|
13
|
+
/** 创建响应式状态容器:$.x = val 自动触发 dirty() */
|
|
14
14
|
$: () => Record<string, any>;
|
|
15
|
+
/** 注册组件实例的自定义语义 ID,同名冲突抛错 */
|
|
16
|
+
selfId: (name: string) => void;
|
|
17
|
+
/** 当前组件实例 ID(仅供内部使用,通过 ctx 扩展注入) */
|
|
18
|
+
_selfId?: string;
|
|
19
|
+
/** 当前组件 VNode 引用(仅供内部使用,通过 ctx 扩展注入) */
|
|
20
|
+
_selfVNode?: any;
|
|
15
21
|
};
|
|
16
22
|
/** 路由(由 router 中间件注入) */
|
|
17
23
|
route?: {
|
package/dist/client/vnode.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface VNode {
|
|
|
19
19
|
_portalEl?: HTMLDivElement | undefined;
|
|
20
20
|
/** 两阶段组件的 render 函数(mount 返回的函数) */
|
|
21
21
|
_render?: (props: any) => VNode | null;
|
|
22
|
+
/** 组件实例 ID(如 '_wf_0') */
|
|
23
|
+
_id?: string;
|
|
24
|
+
/** 组件输出的 DOM 父节点 */
|
|
25
|
+
_parentNode?: Node;
|
|
26
|
+
/** 组件输出的第一个 DOM 节点 */
|
|
27
|
+
_refNode?: Node | null;
|
|
22
28
|
}
|
|
23
29
|
export type Component<P = {}> = (initProps: P, ctx: WfuiContext) => ((props: P) => VNode | null) | null;
|
|
24
30
|
export declare const Fragment: unique symbol;
|