weifuwu 0.83.0 → 0.85.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.
Files changed (73) hide show
  1. package/README.md +56 -51
  2. package/dist/components/ChatInput/ChatInput.d.ts +2 -2
  3. package/dist/components/Confirm/Confirm.d.ts +2 -2
  4. package/dist/components/Notification/Notification.d.ts +0 -2
  5. package/dist/components/Toast/Toast.d.ts +0 -2
  6. package/dist/components/index.d.ts +3 -3
  7. package/dist/components/index.js +12 -12
  8. package/dist/components/style.css +17 -0
  9. package/dist/index.js +2408 -341
  10. package/dist/ui-dom/Confirm.d.ts +1 -1
  11. package/dist/ui-dom/Notification.d.ts +1 -1
  12. package/dist/ui-dom/Toast.d.ts +1 -1
  13. package/dist/ui-dom/hooks/index.d.ts +0 -1
  14. package/dist/ui-dom/hooks/popup.d.ts +2 -0
  15. package/dist/ui-dom/hooks/stable.d.ts +2 -6
  16. package/dist/ui-dom/index.d.ts +24 -19
  17. package/dist/ui-dom/index.js +40 -22
  18. package/dist/ui-dom/jsx-runtime.js +1 -1
  19. package/dist/ui-dom/middleware/ws.d.ts +8 -0
  20. package/dist/ui-dom/popup-tracker.d.ts +1 -1
  21. package/dist/ui-dom/testing.d.ts +3 -9
  22. package/dist/ui-dom/testing.js +7 -1
  23. package/dist/ui-dom/types.d.ts +7 -37
  24. package/dist/ui-dom/vdom3/README.d.ts +6 -3
  25. package/dist/ui-dom/vdom3/app.d.ts +20 -0
  26. package/dist/ui-dom/vdom3/audit.d.ts +13 -0
  27. package/dist/ui-dom/vdom3/build.d.ts +7 -3
  28. package/dist/ui-dom/vdom3/commands.d.ts +13 -3
  29. package/dist/ui-dom/vdom3/comp-index.d.ts +18 -0
  30. package/dist/ui-dom/vdom3/delegate.d.ts +48 -0
  31. package/dist/ui-dom/vdom3/events.d.ts +8 -2
  32. package/dist/ui-dom/vdom3/index.d.ts +5 -3
  33. package/dist/ui-dom/vdom3/jsx.d.ts +26 -4
  34. package/dist/ui-dom/vdom3/record.d.ts +3 -0
  35. package/dist/ui-dom/vdom3/render.d.ts +19 -6
  36. package/dist/ui-dom/vdom3/replay.d.ts +11 -8
  37. package/dist/ui-dom/vdom3/root.d.ts +3 -7
  38. package/dist/ui-dom/vdom3/types.d.ts +128 -70
  39. package/dist/ui-dom/vdom3/ui.d.ts +3 -30
  40. package/dist/ui-dom/vdom3.js +12 -10
  41. package/dist/ui-dom/vnode.d.ts +9 -89
  42. package/docs/compat-three-library.md +1 -1
  43. package/docs/components-map.md +1 -1
  44. package/docs/frontend-middleware.md +59 -53
  45. package/docs/frontend-ui-dom.md +115 -132
  46. package/docs/frontend.md +28 -24
  47. package/docs/realtime.md +21 -21
  48. package/package.json +1 -1
  49. package/dist/test/client/setup.d.ts +0 -12
  50. package/dist/ui-dom/context.d.ts +0 -32
  51. package/dist/ui-dom/hooks/test/hooks.test.d.ts +0 -1
  52. package/dist/ui-dom/middleware/serve.d.ts +0 -27
  53. package/dist/ui-dom/router.d.ts +0 -73
  54. package/dist/ui-dom/vdom2/audit.d.ts +0 -28
  55. package/dist/ui-dom/vdom2/build.d.ts +0 -53
  56. package/dist/ui-dom/vdom2/ctx.d.ts +0 -32
  57. package/dist/ui-dom/vdom2/dom-trace.d.ts +0 -4
  58. package/dist/ui-dom/vdom2/events.d.ts +0 -65
  59. package/dist/ui-dom/vdom2/hydrate.d.ts +0 -21
  60. package/dist/ui-dom/vdom2/index.d.ts +0 -13
  61. package/dist/ui-dom/vdom2/kind.d.ts +0 -44
  62. package/dist/ui-dom/vdom2/lifecycle.d.ts +0 -54
  63. package/dist/ui-dom/vdom2/mount.d.ts +0 -77
  64. package/dist/ui-dom/vdom2/patch.d.ts +0 -116
  65. package/dist/ui-dom/vdom2/registry.d.ts +0 -27
  66. package/dist/ui-dom/vdom2/render.d.ts +0 -21
  67. package/dist/ui-dom/vdom2/route.d.ts +0 -29
  68. package/dist/ui-dom/vdom2/ssr.d.ts +0 -32
  69. package/dist/ui-dom/vdom2/trace.d.ts +0 -62
  70. package/dist/ui-dom/vdom2/transform.d.ts +0 -76
  71. package/dist/ui-dom/vdom2/transitions.d.ts +0 -24
  72. package/dist/ui-dom/vdom2/x2html.d.ts +0 -26
  73. package/dist/ui-dom/vdom3/compat.d.ts +0 -20
@@ -1,45 +1,46 @@
1
1
  # 前端中间件与工具(weifuwu/ui-dom)
2
2
 
3
- > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**——中间件(api/auth/ws/i18n)位于 `weifuwu/ui-dom`,前端运行时唯一入口为 `weifuwu/ui-dom`(UIRouter + uiServe),见 [frontend-ui-dom.md](frontend-ui-dom.md)。
3
+ > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**——中间件(api/auth/ws/i18n)位于 `weifuwu/ui-dom`,前端运行时唯一入口为 `weifuwu/ui-dom`(vdom3 事件流引擎:createRouter + createRoot),见 [frontend-ui-dom.md](frontend-ui-dom.md)。
4
4
 
5
5
  > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
6
6
 
7
- ## UIRouter — 前端路由
7
+ ## createRouter — 前端路由(vdom3)
8
8
 
9
9
  ```tsx
10
- import { UIRouter, uiServe, h } from 'weifuwu/ui-dom'
11
- import type { UIHandler } from 'weifuwu/ui-dom'
12
-
13
- const app = new UIRouter()
14
- app.get('/', () => h(Home, {}))
15
- app.get('/users', () => h(UserList, {}))
16
- app.get('/users/:id', (loc, ctx) => h(UserDetail, { id: ctx.params.id }))
17
- app.notFound(() => h(NotFound, {}))
18
- app.use(toast()) // 中间件(ctx 注入)——app.use(mw)
19
-
20
- uiServe(app, { root: '#root' }) // 客户端落地(hydrate: true 收养 SSR HTML)
10
+ import { createRouter, h } from 'weifuwu/ui-dom'
11
+
12
+ let ctx: any = {}
13
+ ctx = v3Toast()(ctx) // 中间件面展开(ctx 注入——对齐后端 app.use)
14
+
15
+ const handle = createRouter(
16
+ [
17
+ { path: '/', render: () => h(Home, {}) },
18
+ { path: '/users', render: () => h(UserList, {}) },
19
+ { path: '/users/:id', render: (params) => h(UserDetail, { id: params.id }) },
20
+ { path: '*', render: () => h(NotFound, {}) },
21
+ ],
22
+ document.querySelector('#root')!,
23
+ { ctx },
24
+ )
21
25
  ```
22
26
 
23
- ### 嵌套布局(中间件两阶段)
27
+ ### 嵌套布局(RouteDef.layout——跨路由工厂不重跑)
24
28
 
25
29
  ```tsx
26
- const DashboardLayout: UIMiddleware = async (_loc, ctx, children) => {
30
+ const DashboardLayout = async (page: any) => {
27
31
  let open = true
28
- return async (loc, c) => {
29
- const child = await children(loc, c) // 子路由/嵌套路由内容
30
- return h('div', { class: 'wf-row' },
31
- h('aside', {}, '导航菜单'),
32
- h('main', {}, child),
33
- )
34
- }
32
+ return h('div', { class: 'wf-row' },
33
+ h('aside', {}, '导航菜单'),
34
+ h('main', {}, page), // 子路由内容(layout 包装——工厂复用——状态保持)
35
+ )
35
36
  }
36
- app.use(DashboardLayout)
37
+ // { path: '/users', layout: DashboardLayout, render: () => h(UserList, {}) }
37
38
  ```
38
39
 
39
40
  ### 编程式导航
40
41
 
41
42
  ```tsx
42
- // 在任意组件/页面中(uiServe 注入)
43
+ // 在任意组件/页面中(ctx.app——createRouter 注入)
43
44
  ctx.app?.navigate('/users/123?tab=profile')
44
45
  ```
45
46
 
@@ -51,28 +52,28 @@ ctx.app?.navigate('/users/123?tab=profile')
51
52
  | `ctx.route.query` | `Record<string, string>` | 查询参数 |
52
53
  | `ctx.app.navigate(path)` | `(string) => void` | 编程式导航 |
53
54
 
54
- | UIRouter API | 签名 | 说明 |
55
- |--------------|------|------|
56
- | `get(path, handler)` | `(string, UIHandler)` | 页面路由(handler = async 组件:`async (location, ctx) => vnode`) |
57
- | `use(prefix, sub)` / `use(mw)` | `(string, UIRouter) \| (mw)` | 子路由树挂载 / 中间件(ctx 注入) |
58
- | `notFound(handler)` | `(UIHandler)` | 404 页面 |
59
- | `mode` | `'history' \| 'hash'` | 路由模式(默认 history) |
55
+ | RouteDef | 签名 | 说明 |
56
+ |---------|------|------|
57
+ | `path` | `string` | 路径(`:id` 参数 / `*` 通配) |
58
+ | `render(params)` | `(params) => VNode` | 页面(params 注入——`ctx.route.params` 同源) |
59
+ | `layout(page)` | `(VNode) => VNode` | 嵌套布局(跨路由工厂不重跑——状态保持) |
60
60
 
61
61
  | UIHandler | 类型 | 说明 |
62
62
  |-----------|------|------|
63
63
  | 签名 | `(location, ctx) => Promise<VNode> \| VNode` | 页面 = 异步组件(`ctx.data.get` 三场景;async 组件无需包装) |
64
- | 返回值 | `VNode \| null` | 数据结构(落地由 uiServe/ssrPage 决定) |
64
+ | 返回值 | `VNode \| null` | 数据结构(落地由 createRouter 事件流渲染决定) |
65
65
 
66
66
  ---
67
67
 
68
68
  ## api — HTTP 客户端中间件
69
69
 
70
70
  ```tsx
71
- import { UIRouter, api, uiServe } from 'weifuwu/ui-dom'
71
+ import { createRouter, h, api } from 'weifuwu/ui-dom'
72
72
 
73
- const app = new UIRouter()
74
- app.use(api({ baseURL: '/api' }))
75
- uiServe(app, { root: '#root' })
73
+ let ctx: any = {}
74
+ const root = document.querySelector('#root')!
75
+ ctx = api({ baseURL: '/api' })(ctx)
76
+ createRouter([], root, { ctx })
76
77
 
77
78
  // 在组件中使用
78
79
  async function loadUsers(ctx: WfuiContext) {
@@ -124,11 +125,12 @@ try {
124
125
  ## auth — 认证中间件
125
126
 
126
127
  ```tsx
127
- import { UIRouter, auth, uiServe } from 'weifuwu/ui-dom'
128
+ import { createRouter, h, auth } from 'weifuwu/ui-dom'
128
129
 
129
- const app = new UIRouter()
130
- app.use(auth())
131
- uiServe(app, { root: '#root' })
130
+ let ctx: any = {}
131
+ const root = document.querySelector('#root')!
132
+ ctx = auth()(ctx)
133
+ createRouter([], root, { ctx })
132
134
 
133
135
  // 在组件中
134
136
  async function Profile(_props: {}, ctx: WfuiContext) {
@@ -176,11 +178,12 @@ await ctx.auth?.refresh() // → boolean
176
178
  ## ws — WebSocket 客户端中间件
177
179
 
178
180
  ```tsx
179
- import { UIRouter, ws, uiServe } from 'weifuwu/ui-dom'
181
+ import { createRouter, h, ws } from 'weifuwu/ui-dom'
180
182
 
181
- const app = new UIRouter()
182
- app.use(ws({ url: '/ws' }))
183
- uiServe(app, { root: '#root' })
183
+ let ctx: any = {}
184
+ const root = document.querySelector('#root')!
185
+ ctx = ws({ url: '/ws' })(ctx)
186
+ createRouter([], root, { ctx })
184
187
 
185
188
  // 发送消息
186
189
  ctx.ws?.send({ type: 'chat', body: 'hello' })
@@ -216,9 +219,10 @@ unsubscribe?.()
216
219
  ## i18n — 国际化中间件
217
220
 
218
221
  ```tsx
219
- import { UIRouter, i18n, uiServe } from 'weifuwu/ui-dom'
222
+ import { createRouter, h, i18n } from 'weifuwu/ui-dom'
220
223
 
221
- const app = new UIRouter()
224
+ let ctx: any = {}
225
+ const root = document.querySelector('#root')!
222
226
  app.use(i18n({
223
227
  locale: 'zh-CN',
224
228
  messages: {
@@ -301,12 +305,13 @@ import { ErrorBoundary } from 'weifuwu/ui-dom'
301
305
  **① 命令式 `ctx.confirm()`(推荐,操作前询问)**
302
306
 
303
307
  ```tsx
304
- import { UIRouter, uiServe } from 'weifuwu/ui-dom'
308
+ import { createRouter, h } from 'weifuwu/ui-dom'
305
309
  import { confirm } from 'weifuwu/components'
306
310
 
307
- const app = new UIRouter()
308
- app.use(confirm())
309
- uiServe(app, { root: '#root' })
311
+ let ctx: any = {}
312
+ const root = document.querySelector('#root')!
313
+ ctx = confirm()(ctx)
314
+ createRouter([], root, { ctx })
310
315
 
311
316
  // 任意代码中(组件事件、async 逻辑)
312
317
  async function handleDelete(ctx: WfuiContext) {
@@ -357,12 +362,13 @@ import { Confirm } from 'weifuwu/components'
357
362
  `ctx.toast()` 是 `<Toast>` 组件的全局命令式封装:任意代码中一行调用,自动消失、自动清理,无需宿主状态。
358
363
 
359
364
  ```tsx
360
- import { UIRouter, uiServe } from 'weifuwu/ui-dom'
365
+ import { createRouter, h } from 'weifuwu/ui-dom'
361
366
  import { toast } from 'weifuwu/components'
362
367
 
363
- const app = new UIRouter()
364
- app.use(toast({ position: 'top-right', duration: 3000, max: 3 }))
365
- uiServe(app, { root: '#root' })
368
+ let ctx: any = {}
369
+ const root = document.querySelector('#root')!
370
+ ctx = toast({ position: 'top-right', duration: 3000, max: 3 })(ctx)
371
+ createRouter([], root, { ctx })
366
372
 
367
373
  // 任意代码中(组件事件、api 拦截器、WS 回调、定时器)
368
374
  ctx.toast?.('保存成功', 'success')
@@ -1,168 +1,151 @@
1
- # ui-dom — 前端运行时(UIRouter + uiServe + SSR/hydration)
1
+ # ui-dom — 前端运行时(vdom3 精准事件流引擎)
2
2
 
3
- > **weifuwu 前端唯一运行时**(`weifuwu/ui-dom`,随 npm 包发布):
4
- > **UIRouter(纯路由 + ctx 注入链)+ uiServe(渲染运行时)+ ssrPage/hydration**,
5
- > 复用 `weifuwu/components`(VNode 契约唯一来源,组件零修改)。
6
- > 已取代 `weifuwu/client`——前端运行时唯一入口(createApp/router 旧 API 已删除)。
7
- >
8
- > 概念对齐:**req = window.location**,**res = VNode**(数据结构),
9
- > **uiServe = VDOM**(落地机制),**params/query 在 ctx**(对齐后端 `ctx.params`)。
3
+ > **createRouter(路由)+ createRoot(挂载)+ 精准事件流(渲染本体)**,
4
+ > 组件 = 两阶段异步组件(`async (initProps, ctx) => (props) => VNode`),
5
+ > **params 在 ctx**(对齐后端 `ctx.params`),渲染全链路可观测/可回放/可断言。
10
6
 
11
- ## 为什么是 ui-dom(开发者价值)
7
+ ## 架构:vdom3 事件流引擎
12
8
 
13
- ### 心智模型统一:一套 API 贯穿两端,无需「两套前端」
14
-
15
- ```
16
- 后端: Request → [Middleware → ctx.field] → Handler → Response
17
- 前端: window.location → [UIMiddleware → ctx.field] → Handler → VNode
18
9
  ```
19
-
20
- - **req/res 契约同构**:`UIRequest = window.location`(原生对象,不包装)对齐后端 `Request`;`UIResponse = VNode` 对齐 `Response`。后端开发者迁移到前端,中间件思维直接复用(`app.use(mw)` 与 `router.use(mw)` 累积类型同构)。
21
- - **SSR 不是另一套代码**:同一份 `router.ts`(两端共享),`ssrPage(router, { url })` 服务端直接跑,`uiServe(router, { root, hydrate: true })` 客户端收养。`ctx.data.get(key)` 一个 API 三场景自动适配(SSR 真 fetch → 序列化进 `__DATA__` → hydration 种子命中 → SPA 未命中触发 fetcher)——**开发者不需要理解「首屏/水合/客户端渲染」的区别**。
22
-
23
- ### 两阶段组件模型:从 hooks 心智负担中解脱
24
-
25
- ```tsx
26
- const Counter = async (initProps, ctx) => {
27
- let count = initProps.initial ?? 0 // mount:只执行一次
28
- return async (props) =>
29
- h('button', { onClick: () => { count += props.step; ctx.ui.render() } }, count) // render
30
- }
10
+ 用户操作(ctx.render())
11
+ → 决策层事件(jsx/vdom):comp:render → vnode:patch(reuse/rebuild 决策)
12
+ → 执行层事件(dom):node:create/insert/remove/move、prop:update、
13
+ text:update、event:bind/unbind、ref:cleanup
14
+ → DOM(精准状态变化——事件数 = 实际变化数)
31
15
  ```
32
16
 
33
- - **没有 hooks 规则、没有依赖数组、没有闭包陷阱**。外层 = 初始化(一次),内层 = 渲染(每次变化)。
34
- - **render-only 确定性渲染**:渲染只发生在 `ctx.ui.render()` 调用处——改状态后显式 `render()`,行为可静态推导。无 `$` Proxy、无隐式触发;跨组件共享用 `createStore` + `ctx.ui.useExternal()`。
35
-
36
- ### 框架即纪律:浏览器环境抽象把常见坑变成编译期/审计期错误
37
-
38
- - **`ctx.browser` 唯一入口**:复制/查询/滚动/存储/主题/定时器/媒体查询/事件监听全部经 BrowserEnv(组件侧 46 处迁移清零 + 测试侧 ~300 处对齐,SSR shim 三态同构)。开发者不需要记「SSR 里 `window.innerWidth` 会崩」这类环境知识——**框架替他们承担**。
39
- - **诚实裁剪**:不支持的能力明确抛 `ProtocolError('unsupported')`——确定性失败,绝不静默降级。
40
-
41
- ### 零依赖 + 确定性:可审计、可测试、可读的运行时
42
-
43
- - **零 npm 运行时依赖**(对比 React + react-dom + react-router + 状态库 + SSR 工具 5+ 依赖)。
44
- - **自研 VDOM/diff**(keyed children、style diff、CSS 变量、Portal、hydration 游标收养)——每个算法都有对应测试与纪律条目(真实事故沉淀)。**读源码即可完全理解框架行为**。
45
- - **VDOM 对开发者透明(占位法 + 单一规则源)**:写 JSX → 看 DOM 即真相——`data-wf-key`(数组项身份,元素/组件一致)、`data-wf-id`(组件实例身份)、`<!--wf-hole: xxx-->`(条件渲染 false 的占位注释)直接在 DOM 可见;非法输入(对象/数字 type)→ 诊断占位 + warn,不崩溃不静默;`?vdom_debug=1` 开启 patch trace + 结构 audit。**转化路径唯一清晰、无 magic、可推导**(规则表为内部开发契约)
46
- - **零构建步骤**:`weifuwu/dev` loader + `ctx.ui.js/css` 动态编译,服务端直接跑 `.tsx`,改组件刷新即生效。
47
-
48
- ### 弹层/浮层体系:最难的 UI 类别变成复用的原语
49
-
50
- `ctx.ui.usePopup()` 一个组合器覆盖:portal 渲染、fixed 定位 + 视口夹紧、锚点变化自动重算、外部点击/Escape 关闭、el-null 微任务重试、ref 稳定化。**开发者不需要再写任何弹层脚手架**——这是传统前端最常重复造轮子的地方。弹窗纪律(portal 必须、z-index token、exit 动画成对)由审计测试强制。
17
+ - **统一命名**:对象 + 动作 + 参数(`{ entity, action, target, payload }`——键 `entity:action`)——location(`route:change`)/ jsx(`comp:render`/`props:update`)/ vdom(`vnode:patch`)/ dom(`node:insert` 等)四层同构
18
+ - **DOM = fold(事件流)**:初始 DOM + 事件序列 = 任意时刻 DOM——可记录(`stream.events()`)、回放(`replay`)、断言(`expectEventSequence`/`eventsOf`)
19
+ - **不变量:无事件流不渲染**——所有状态变化都通过事件流(DOM 操作/组件生命周期/索引/事件注册/错误/挂起/内部决策/文本操作/副作用)——静态审计 + 运行时对照审计(`__WF_VDOM_AUDIT='1'`)守护
20
+ - **render-only**:渲染唯一触发 `ctx.render()`(组件级——只重跑该组件 renderFn + patch 其输出,兄弟零执行);`ctx.ui.render()` 为兼容面(同义)
21
+ - **组件级精准更新**:`ctx.render()` 只刷新自身——组件 O(1) 索引定位——props 未变的子组件剪枝——事件数 = 实际变化数
51
22
 
52
- ### 对贡献者的价值:纪律 = 事故沉淀
23
+ ### 事件代理(所有监听统一注册表)
53
24
 
54
- AGENTS.md 每条纪律对应真实事故(JSONViewer selfId 错位、AutoComplete 焦点丢失、TreeSelect 0 rect、Kanban draggable 空字符串解析 false)——新贡献者站在「踩过坑」的肩膀上。分层清晰(UIRouter 纯路由 → uiServe 装配点 → ctx.browser 环境边界 → components 消费层),改动边界由架构约束,协议层改动 TDD 先行 + 真实库验证(CS-04/CS-05)。
25
+ 元素事件/全局监听/动画钩子全部收敛到**代理注册表**(`delegate.ts`):
26
+ - 元素事件(onClick 等 props)→ 挂载点监听(每挂载点每事件一次——O(1))+ 按 data-v3-id 祖先链分发(handler 更新 = Map 覆盖——**零重绑零噪音**)
27
+ - 全局监听(keydown/mousedown/pointer/popstate/scroll/选区/剪贴板)→ `addGlobalListener`(同事件聚合——EventTarget 每事件一次)
28
+ - 动画钩子(animationend)→ `bindElementListener`(once 自动解绑)
29
+ - 不冒泡事件(error/load)→ 捕获监听;`e.currentTarget` 还原为绑定元素(与元素级语义一致)
55
30
 
56
- | 维度 | 价值 |
57
- |------|------|
58
- | **上手** | 两阶段组件 + 改状态后 render()——无 hooks/依赖数组心智负担 |
59
- | **后端互迁** | req/res/中间件契约同构,SSR 透明,一份 router 两端共享 |
60
- | **可靠性** | 确定性失败、诚实裁剪、环境边界、测试侧同构 |
61
- | **效率** | 弹层/数据管道/事件原语全覆盖——不重复造轮子 |
62
- | **可维护** | 零依赖可审计、纪律沉淀事故教训、构建可选 |
63
-
64
- > 一句话:ui-dom 不是「又一个 React」——它是把 weifuwu 后端中间件生态的确定性、可测试性和纪律性,原样移植到了前端。开发者得到的是一个行为可预测、失败可预期、源码可读懂的运行时。
65
-
66
- ## 快速开始
31
+ ## 快速开始(SPA)
67
32
 
68
33
  ```tsx
69
- import { UIRouter, uiServe, h } from './src/ui-dom/index.ts'
70
- import { toast } from './src/ui-dom/Toast.ts'
71
- import { Button } from './src/components/index.ts'
72
-
73
- const app = new UIRouter()
74
-
75
- app.use(toast()) // ctx 注入链(对齐后端 app.use——注入 ctx.toast)
34
+ import { createRouter, h } from 'weifuwu/ui-dom'
35
+ import { Button } from 'weifuwu/components'
36
+
37
+ // 两阶段异步组件:外层工厂(mount 一次——状态/数据),内层 renderFn(每次渲染)
38
+ const Counter = async (_init, ctx) => {
39
+ let count = 0
40
+ return async () =>
41
+ h('div', {},
42
+ h('button', {
43
+ onClick: () => { count++; ctx.render() }, // render-only:显式触发
44
+ }, `count:${count}`),
45
+ )
46
+ }
76
47
 
77
- // handler = 异步组件:async (location, ctx) => VNode(render-only——改状态后 ctx.ui.render())
78
- app.get('/', async (location, ctx) => {
79
- const info = await ctx.data.get('/api/info', async () => ({ title: '首页' }))
80
- let clicks = 0
81
- return h('div', {},
82
- h('h2', {}, info.title),
83
- h(Button, { onClick: () => { clicks++; ctx.ui.render() } }, `点击 ${clicks} 次`),
84
- h(Button, { variant: 'secondary', onClick: () => ctx.toast?.('提示', 'success') }, '弹 toast'),
85
- )
86
- })
48
+ createRouter(
49
+ [{ path: '/', render: () => h(Counter, {}) }],
50
+ document.querySelector('#root') as HTMLElement,
51
+ )
52
+ ```
87
53
 
88
- app.get('/users/:id', (location, ctx) => h('div', {}, `用户 ${ctx.params.id}`))
89
- app.notFound(() => h('div', {}, '404'))
54
+ - 页面组件 ctx:`ctx.render()`(页面级刷新)/ `ctx.route`(`{ path, params }`——动态参数)/ `ctx.ui`(24 hooks)
55
+ - 中间件面注入:`createRouter(routes, root, { ctx })`——`ctx` 为中间件展开结果(`api/auth/toast/confirm/i18n` 等——与后端 `app.use` 同理念):
90
56
 
91
- uiServe(app, { root: '#root' }) // 装配点:路由已注册 → serve 监听 URL → 渲染
57
+ ```ts
58
+ let ctx: any = {}
59
+ ctx = await api({ baseURL: '', token: ... })(ctx)
60
+ ctx = auth({ ... })(ctx)
61
+ ctx = v3Toast()(ctx)
62
+ createRouter([...], root, { ctx })
92
63
  ```
93
64
 
94
- ## 核心概念
95
-
96
- ### req / res / 落地分离
65
+ ## 无路由挂载(createRoot)
97
66
 
98
- | 概念 | 含义 |
99
- |------|------|
100
- | req | `window.location`(浏览器原生,不包装) |
101
- | res | `VNode`(handler 返回的数据结构) |
102
- | uiServe | VDOM 落地(renderValue/patchValue diff·patch DOM) |
103
- | ssrPage | 服务端落地(renderSsr → HTML + `__DATA__`) |
67
+ 组件树直接挂载(弹窗宿主、微前端片段、非路由页面):
104
68
 
105
- handler 只产 VNode,落地由 serve 决定——SSR 与 SPA 是两种落地,同一 router 定义共享。
69
+ ```ts
70
+ import { createRoot, h } from 'weifuwu/ui-dom'
71
+ const handle = createRoot(h(App, {}), document.getElementById('root')!, { ctx })
72
+ await handle.ready // 首帧完成
73
+ ```
106
74
 
107
- ### UIRouter 三态 use(对齐后端 app.use)
75
+ ## ctx.ui(组件库消费面——24 hooks)
108
76
 
109
- | 形态 | 签名 | 用途 |
110
- |------|------|------|
111
- | ctx 注入 | `use(mw: AppMiddleware)` | `(ctx) => ctx`——注入 ctx.toast/confirm 等(类型累积 `UIRouter<C & O>`) |
112
- | 渲染中间件 | `use(mw: UIMiddleware)` | `(location, ctx, children) => (location, ctx) => vnode`——layout/SSR 包装 children |
113
- | 子路由 | `use(prefix, sub)` | 独立路由树(sub 的中间件/notFound/嵌套均生效) |
77
+ `useExternal`(共享状态)/ `useControlled`/`useControlledInput`(受控原语)/ `useOpen`/`usePopup`(弹层组合器——portal 定位/Escape/外部点击)/ `usePopupPosition` / `useTween` / `useInView` / `useScrollPosition` / `useGlobalKey` / `useDrag`/`useDragDrop` / `useMedia`/`useBreakpoint` / `useReducedMotion` / `useStableRef` / `useAnimationEnd` / `usePresence`(退场状态机)/ `useLongPress` / `useHoverCapable` / `useVisualViewport` / `useAsync` / `useChat`(AI 会话——消息/工具调用/审批)。
114
78
 
115
- 运行时按 `arg.length` 区分(1 参 = ctx 注入;3 参 = 渲染)。
79
+ 组件库(weifuwu/components)零引擎耦合:只消费 `ctx.ui` 契约——vdom3 与未来引擎可无缝替换。
116
80
 
117
- ### components 复用(零修改)
81
+ ## 事件流可观测(调试/测试)
118
82
 
119
- VNode 契约以 ui-dom 为唯一来源(Fragment/Portal symbol 由 ui-dom 自持)——components 产的 VNode
120
- 直接被 ui-dom 渲染器识别。渲染算法(render/diff/createUi 原语)在 ui-dom 内自主实现,
121
- **registry/popup-tracker 局部实例**(serve 每实例隔离)。
83
+ ```ts
84
+ import { stream, evKey } from 'weifuwu/ui-dom'
85
+ import { eventsOf, expectEventSequence } from 'weifuwu/ui-dom'
122
86
 
123
- 命令式工厂(toast/confirm/notification)位于 ui-dom(`src/ui-dom/Toast.ts` 等):
124
- components 消费端 import `weifuwu/ui-dom`(构建外部化,共享同一模块实例)。
87
+ const events = stream.events()
88
+ eventsOf(events, 'node:create').length // 创建了几个节点
89
+ // 决策层事件解释"为什么"(reuse/rebuild),执行层事件是"做了什么"——完全可断言
90
+ ```
125
91
 
126
- ### render-only 渲染(唯一触发:ctx.ui.render())
92
+ **完整事件覆盖**(任何问题都在事件流可定位):
93
+ - **错误**:`error:throw/caught`(工厂/renderFn/挂载/更新失败——phase 定位)+ **挂起超时**(renderFn/buildVNode 永不 resolve → 3s 检测——静默失败不再静默)
94
+ - **内部决策**:`internal:queue/notfound/skip`(busy 排队/组件未定位)
95
+ - **事件流自身**:`stream:overflow`(溢出可审计)/ `stream:watermark`(水位预警)/ `size()`/`capacity()`/`overflowCount()`/`subscribe(filter, fn)`(按层过滤订阅)
96
+ - **文本操作**:`text:input`(输入/组合)/ `text:select`(选区)/ `text:copy/cut/paste`(剪贴板摘要)
97
+ - **组件副作用**:`ref:mount` + `effect:animate/lock/unlock/focus/scroll`(动画/滚动锁/焦点 trap/滚动)
127
98
 
128
- | 原语 | 触发 | 重渲染范围 |
129
- |------|------|-----------|
130
- | `ctx.ui.render()`(无参) | 主动调用 | 当前组件(闭包绑定,无 this 陷阱) |
131
- | `ctx.ui.render(['id'])` | 主动调用 | 指定组件(selfId 注册的语义 ID) |
132
- | `ctx.ui.useExternal(store)` | store 变更自动 | **仅订阅组件**(unmount 自动退订) |
99
+ 测试纪律(jsdom):`renderVNode`(VNode 层)/ `mountComponent`(同实例 re-render)/ `expectEventSequence`(精确事件序列)——渲染 = 事件序列断言。
133
100
 
134
- - 状态是普通对象(`let` / `createStore`)——改状态后显式 `render()`,无赋值自动渲染
135
- - 跨组件共享:`createStore` + `useExternal`(替代已删除的 `$` Proxy / `dirty`)
136
- - hooks(useMedia/useInView/usePopup 等)事件驱动重渲染——与"赋值自动"本质不同
101
+ ### 多应用加载(app 节点)
137
102
 
138
- ## SSR + hydration(端到端)
103
+ ```ts
104
+ import { registerApp, App } from 'weifuwu/ui-dom'
139
105
 
140
- ```tsx
141
- // 服务端(Node 无 DOM):ssrPage → 完整 HTML + __DATA__
142
- import { ssrPage } from './src/ui-dom/ssr.ts'
143
- import { app } from './router.ts' // 路由定义两端共享
106
+ // 注册子应用(工厂——可 await 初始化——appId 复用不重跑)
107
+ registerApp('mini-todo', (props, ctx) => h(MiniTodo, {}))
108
+ // 父树中嵌入(不隔离设计——共享流/全局 id——天然归属唯一)
109
+ h(App, { appId: 'mini-todo', props: { title } })
110
+ ```
111
+ - 边界事件(同流):`app:mount` / `app:update`(props keys)/ `app:unmount` / `app:error`(unknown-app)——payload 带 appId 可区分
112
+ - 子应用独立状态(工厂闭包)——渲染/事件与父同流(全链路可观测——调试一条流)
113
+
114
+ ## SSR(事件流形态——DOM = fold 不变量)
115
+
116
+ vdom3 SSR 不是 HTML 字符串 + 游标收养,而是**事件流序列化**:
117
+
118
+ ```ts
119
+ // 服务端:构建 → 事件流 → HTML + 序列化事件
120
+ import { renderToEvents, eventsToHtml, serializeEvents } from 'weifuwu/ui-dom'
121
+ const events = await renderToEvents(vnode) // 事件流(node:create/insert/...)
122
+ const html = eventsToHtml(events) // 首帧 HTML
123
+ const data = serializeEvents(events) // 序列化事件(__DATA__)
124
+ // 客户端:replay(events, root) —— 零 DOM 猜测,与服务端 HTML 同构
125
+ import { deserializeEvents, replay } from 'weifuwu/ui-dom'
126
+ replay(deserializeEvents(data), root)
127
+ ```
144
128
 
145
- const { page } = await ssrPage(app, { url: '/users/42' })
129
+ `ctx.data.get(key, fetcher)` 三场景自动适配(SSR 真 fetch → 序列化进 `__DATA__` → 客户端种子命中 → SPA 未命中触发 fetcher)。
146
130
 
147
- // 客户端:uiServe hydrate(收养服务端 HTML + __DATA__ 种子命中不重取数)
148
- uiServe(app, { root: '#root', hydrate: true })
149
- ```
131
+ ## 组件纪律速查(事故沉淀——详见 AGENTS.md)
150
132
 
151
- - SSR 时 `ctx.data.get` 写入 dataStore → `__DATA__` 脚本;hydrate 从种子同步命中
152
- - renderSsr:组件两阶段 + async 组件 await(数据 per-request)/ Portal 内联 / 事件剥离
153
- - SSR 确定性:ui 19 原语 shim 全 no-op(不启动监听/会话);渲染期非确定性由开发者规避
133
+ - **render-only**:状态是普通对象(`let` + 显式 `render()`)——禁止隐式触发
134
+ - **mount/render 分工**:只依赖稳定引用的回调(ctx/挂载闭包)→ mount 定义(零重绑);依赖最新 props → render 内定义(重绑是正确性要求)
135
+ - **ref 稳定**:带清理的 ref 定义在 mount 作用域(内联 ref 每次渲染新函数 → ref(null) 反复触发)
136
+ - **浮层 portal**:`ctx.ui.usePopup` + `popup.portal()`(#__wf_portal)——禁止 absolute 相对父容器
137
+ - **浏览器环境**:组件用 `ctx.browser`(禁裸 window/document)——SSR 安全三态
138
+ - **事件 handler**:代理 Map 覆盖——handler 更新零重绑零事件(稳定引用仅为性能建议——结构共享收益前提)
139
+ - **props 不可变(红线——Chat 空 bubble 事故)**:**禁止原地修改 props 引用的对象**(`m.content += text`)——vdom3 剪枝按 props 浅比较判断"未变"→ 复用旧输出 → 渲染不更新。**状态更新必须新建对象**(`$.msgs[i] = { ...m, content: ... }`——引用变 → 剪枝不命中 → 重渲染)。排查"渲染没更新"时看 `__wf_tail` 的 `comp:build` 事件 `reason: 'reuse-skip'`(= 契约:props 引用未变)
154
140
 
155
- ## ctx 注入
141
+ ## 与后端共享
156
142
 
157
- | 字段 | 说明 |
158
- |------|------|
159
- | `ctx.params` / `ctx.query` | 路由参数 / URL query(顶层,对齐后端) |
160
- | `ctx.data.get/set/has` | 数据管道(缓存 + in-flight 合并 + `__DATA__` 种子) |
161
- | `ctx.ui.*` | 原语(`render`/`useExternal`/`usePopup`/`useChat`/`useInView`…) |
162
- | `ctx.browser.*` | 环境抽象(window/document 唯一入口,SSR shim 同构) |
163
- | `ctx.toast` / `ctx.confirm` / `ctx.notification` | 命令式注入(`app.use(toast())` 等) |
143
+ - 中间件理念同构:后端 `app.use`(ctx.sql/redis/api/...)↔ 前端 `createRouter(routes, root, { ctx })`(ctx.api/auth/...)
144
+ - 路由参数:`ctx.route.params`(对齐后端 `ctx.params`)
145
+ - `weifuwu/dev`(Node loader):服务端直接跑 `.tsx`——与 `ctx.ui.js` 前端动态编译对称,两端同一 JSX 运行时
164
146
 
165
- ## demo
147
+ ## 参考
166
148
 
167
- `apps/layouts-demo`:UIRouter + uiServe + components 的布局蓝本壳(hash 路由切换 8 种布局模式)——
168
- `apps/components-demo`:全部 115 组件 + ui-dom 交互演示(含 Toast/Confirm 注入、SSR/hydrate)。
149
+ - 组件库蓝本:`apps/components-demo`(115 组件 + createRouter 装配)
150
+ - 完整应用:`apps/agent-platform`(多租户 AI 平台——认证守卫/嵌套布局/AI 会话)
151
+ - 布局蓝本:`apps/layouts-demo`(8 种布局模式)
package/docs/frontend.md CHANGED
@@ -5,7 +5,7 @@
5
5
  > createRoot + 事件流)。vdom2(本文档)为当前生产引擎(冻结中——bug 修复在 vdom3)。
6
6
  > 迁移路线图见仓库 `design/vdom3-migration-plan.md`。
7
7
 
8
- > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——本页 import 均用 `weifuwu/ui-dom`。前端运行时唯一入口是 **`weifuwu/ui-dom`**(UIRouter 纯路由 + uiServe 渲染运行时 + SSR/hydration),权威参考见 **[docs/frontend-ui-dom.md](frontend-ui-dom.md)**。
8
+ > ⚠️ **`weifuwu/client` 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——本页 import 均用 `weifuwu/ui-dom`。前端运行时唯一入口是 **`weifuwu/ui-dom`**(vdom3 精准事件流引擎:createRouter + createRoot——渲染全链路 `entity:action` 事件流),权威参考见 **[docs/frontend-ui-dom.md](frontend-ui-dom.md)**。
9
9
 
10
10
  > 以下为完整 API 参考,按需查阅。新手建议先阅读 README 的「核心概念」和「快速开始」。
11
11
 
@@ -25,31 +25,34 @@ esbuild.build({
25
25
 
26
26
  ---
27
27
 
28
- ## 应用引导(UIRouter + uiServe)
28
+ ## 应用引导(vdom3 事件流引擎——createRouter)
29
29
 
30
30
  ```tsx
31
- import { UIRouter, uiServe } from 'weifuwu/ui-dom'
32
-
33
- const app = new UIRouter()
34
-
35
- // 注册中间件(ctx 注入)
36
- app.use(middleware1)
37
- app.use(middleware2)
38
-
39
- // 页面路由(handler = 异步组件)
40
- app.get('/', () => h(Home, {}))
41
-
42
- // 落地:客户端渲染(hydrate: true 收养 SSR HTML)
43
- const handle = uiServe(app, { root: '#root' })
31
+ import { createRouter } from 'weifuwu/ui-dom'
32
+
33
+ // 中间件面展开(对齐后端 app.use 链——ctx 注入)
34
+ let ctx: any = {}
35
+ ctx = await api({ baseURL: '' })(ctx)
36
+ ctx = await auth({ ... })(ctx)
37
+ ctx = v3Toast()(ctx)
38
+
39
+ // 路由(RouteDef[])+ 落地(事件流渲染)
40
+ const handle = createRouter(
41
+ [
42
+ { path: '/', render: () => h(Home, {}) },
43
+ { path: '/users/:id', render: (params) => h(UserPage, { id: params.id }) },
44
+ ],
45
+ document.querySelector('#root')!,
46
+ { ctx },
47
+ )
44
48
  ```
45
49
 
46
50
  | API | 说明 |
47
51
  |------|------|
48
- | `new UIRouter()` | 创建路由实例 |
49
- | `app.use(mw)` | 注册中间件(AppMiddleware / UIMiddleware) |
50
- | `app.get(path, handler)` / `use(prefix, sub)` / `notFound(handler)` | 页面路由 / 子路由树 / 404 |
51
- | `uiServe(app, { root, hydrate })` | 客户端落地(返回 handle,`handle.close()` 卸载) |
52
- | `app.ctx` | 当前 WfuiContext |
52
+ | `createRouter(routes, root, { ctx })` | vdom3 路由:RouteDef[](`{ path, render, layout? }`)+ 中间件面 ctx 注入 |
53
+ | `handle.navigate(path)` / `refresh()` / `close()` | 导航 / 重渲染当前页 / 卸载 |
54
+ | `ctx.route` | 当前路由 `{ path, params }`(对齐后端 ctx.params) |
55
+ | `createRoot(vnode, root, { ctx })` | 无路由组件树挂载(`handle.ready` 首帧完成) |
53
56
 
54
57
  ---
55
58
 
@@ -94,10 +97,11 @@ const Home: Component<{}, ApiInjected & RouteInjected> = async (_init, ctx) => {
94
97
  }
95
98
  // 未声明的注入字段编译期报错——注入从"文档约定"变成"类型保证"
96
99
 
97
- const app = new UIRouter<{}>()
98
- .use(api()) // 注入 ctx.api
99
- .use(toast()) // 注入 ctx.toast
100
- uiServe(app, { root: '#root' }) // 类型累积完整(UIRouter<C & O>)
100
+ // 中间件面展开 + createRouter(options.ctx 注入——类型由中间件函数累积)
101
+ let ctx: any = {}
102
+ ctx = api()(ctx) // 注入 ctx.api
103
+ ctx = v3Toast()(ctx) // 注入 ctx.toast
104
+ createRouter(routes, root, { ctx })
101
105
  ```
102
106
 
103
107
  > 各中间件的注入接口:`api()` → `ApiInjected`、`auth()` → `AuthInjected`、`ws()` → `WsInjected`、`i18n()` → `I18nInjected`、`router()` → `RouteInjected`(均可从 `weifuwu/ui-dom` 导入)。