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 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` 自动触发 VDOM patch。无需手动调用 `useState`/`useEffect`。
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()` 触发 VDOM patch |
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 测量、动画触发、第三方库在事件中同步读取 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
- **但实际上,绝大多数情况下你不需要 `dirty()`。** 深度 Proxy 已经拦截了所有常见的变更新为方式(深层属性赋值、数组 push/splice、delete 等)。先赋值给 `$` 永远是更清晰的做法。
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
- // ✅ 推荐:ctx.ui.$() + $.x = val — 自动、批量、无脑
909
+ // 自动:$.x = val — 微任务批量,绑定当前组件
917
910
  const $ = ctx.ui.$()
918
911
  $.count++
919
- $.name = 'hello' // 微任务合并,只渲染一次
912
+ $.name = 'hello' // 多次赋值合并为一次渲染
920
913
 
921
- // ✅ 简单场景:闭包变量 + ctx.ui.render() — 轻量同步
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
- // ⚠️ 罕见:ctx.ui.dirty() — 绕过 Proxy 后手动标记
920
+ // 异步:ctx.ui.dirty() — 微任务批量,同 render() 作用域
921
+ ctx.ui.dirty()
922
+ ctx.ui.dirty(['stats']) // 批处理合并
927
923
  ```
928
924
 
929
925
  **性能说明**:
930
- - `$.x = val` 和 `dirty()` 都是微任务批量合并:同一 tick 内 N 次赋值 → 1 次渲染
931
- - `render()` 每次调用都触发一次完整 diff/patch,频繁调用可能影响性能
926
+ - `$.x = val` 和 `dirty()` 都是微任务批量合并
927
+ - `render()` 每次调用都触发一次完整 diff/patch
928
+ - 三个入口同一套 scope 机制,不想要的渲染不触发
932
929
 
933
- ### 实践建议:日常开发 vs 组件分享
930
+ ### 实践建议
934
931
 
935
- **日常组件内**:优先用 `$.x = val`,无脑、自动、批量。
932
+ **组件库**(可分享组件)推荐手动模式:
936
933
 
937
- **制作可分享组件**(组件库、npm 包、跨项目复用)时,推荐用 `ctx.ui.dirty()` 或 `ctx.ui.render()` 精确控制刷新时机:
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
- // 可分享的 Toast 组件:主动控制渲染,避免消费方上下文干扰
941
- const Toast = (_init, ctx) => {
942
- let items: ToastItem[] = []
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
- - 暴露 `add/remove` 等命令式 API 时,`render()` / `dirty()` 让刷新时机**显式、可预测**
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
 
@@ -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.dirty() 标记脏状态,下个微任务批量渲染
9
- * ctx.ui.$() 创建响应式状态容器($.x = val 自动 dirty)
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';
@@ -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
- el.appendChild(renderValue(child, ctx));
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, ctx);
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
- ctx.ui = ctx.ui ?? {};
240
- if (oldV._render) newV._render = oldV._render;
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, ctx);
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 rendered = false;
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
- render: doRender,
665
- /** 标记脏状态,下个微任务批量渲染 */
666
- dirty: scheduleRender,
667
- /** 创建响应式状态容器:$.x = val 自动触发 dirty()(仅事件/timer 中生效) */
668
- $: () => createReactiveState(scheduleRender)
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
- rendered = true;
773
+ flushDirtyBatch();
676
774
  },
677
775
  destroy() {
678
776
  if (container) container.innerHTML = "";
@@ -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;
@@ -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?: {
@@ -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;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.50.0",
4
+ "version": "0.51.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {