weifuwu 0.76.0 → 0.77.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
@@ -30,10 +30,10 @@ npm install weifuwu # 一个依赖,完整应用栈
30
30
 
31
31
  > ⚠️ **注意:前后端都有 `ctx.ui`,但用途完全不同**
32
32
  > - **后端** `ctx.ui`(SSR/编译):`ctx.ui.html`(HTML 模板)、`ctx.ui.js`(TSX→JS 动态编译)、`ctx.ui.css`(CSS 编译)、`ctx.ui.ssr`(组件 SSR)、`ctx.ui.ssrData`(数据序列化)
33
- > - **前端** `ctx.ui`(渲染引擎,26 方法):
34
- > - 渲染:`$()`(响应式状态)/ `render()` / `dirty()` / `selfId()`(跨组件刷新)
35
- > - 状态:`useControlled()`(受控/非受控)/ `useStableRef()`(稳定 ref
36
- > - 弹层:`usePopup()`(组合器)/ `useDialog()`(对话框)/ `usePopupPosition()`(定位)
33
+ > - **前端** `ctx.ui`(渲染引擎,20+ hooks):
34
+ > - 渲染:`render()`(唯一触发——render-only)/ `selfId()`(跨组件精准刷新)
35
+ > - 状态:`useControlled()`(受控/非受控)/ `useStableRef()`(稳定 ref)/ `useExternal()`(共享状态订阅)
36
+ > - 弹层:`usePopup()`(统一能力层——锚定浮层 + 会话级模态)/ `usePopupPosition()`(定位)
37
37
  > - 事件:`useInView()` / `useScrollPosition()` / `useMedia()` / `useBreakpoint()` / `useGlobalKey()` / `useDrag()` / `useDragDrop()` / `useHoverCapable()` / `useLongPress()` / `useVisualViewport()`
38
38
  > - 动画:`useAnimationEnd()`(完成回调)/ `usePresence()`(显隐状态机)/ `useTween()`(数值补间)/ `useReducedMotion()`(偏好感知)
39
39
  > - AI/数据:`useChat()`(AI 会话)/ `useAsync()`(异步取数)
@@ -89,13 +89,13 @@ npm install weifuwu # 一个依赖,完整应用栈
89
89
 
90
90
  **零运行时依赖** — 前端无 npm 运行时依赖(自研 VDOM,不引入 Virtual DOM 库、rxjs、immer 等)。后端仅依赖 `esbuild`(TSX→JS 编译)+ `graphql` + `ws`(语言/协议本身)——**数据库客户端(PostgreSQL/Redis 协议)、GraphQL schema 工具全部自研**。esbuild 作为运行时依赖随 `npm install weifuwu` 自动安装,`ctx.ui.js()` 开箱即用。
91
91
 
92
- **两阶段组件模型** — 组件 = `(initProps, ctx) => (props) => VNode`。外层函数只执行一次(mount),内层函数每次状态/props 变化时执行(render)。无 class、无 `this`、无 Hook——**位置即语义**:外层天生只跑一次,没有 hooks 规则、没有依赖数组、没有闭包陷阱(详解见[核心概念](#核心概念))。
92
+ **两阶段组件模型** — 组件 = `async (initProps, ctx) => (props) => Promise<VNode>`。外层工厂只执行一次(mount,可 await 数据),内层 renderFn 每次状态/props 变化时执行(强制异步)。无 class、无 `this`、无 Hook——**位置即语义**:外层天生只跑一次,没有 hooks 规则、没有依赖数组、没有闭包陷阱(详解见[核心概念](#核心概念))。
93
93
 
94
- **Proxy 驱动渲染** — `ctx.ui.$()` 返回深度 Proxy,`$.x = val` 自动触发当前组件的 VDOM patch;也支持手动 `ctx.ui.render()` 精确控制渲染时机。**组件库手动优先、业务层自动优先**——同一框架内按角色选模式(详见[组件库](docs/components.md))。
94
+ **render-only 确定性渲染**渲染唯一触发 `ctx.ui.render()`(闭包绑定组件),状态是普通对象(`let` + `render()`);跨组件共享用 `createStore` + `ctx.ui.useExternal()`。行为可静态推导,无隐式触发(详见[组件库](docs/components.md))。
95
95
 
96
96
  **中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` / `ctx.limit` / `ctx.email` / `ctx.queue` / `ctx.ai` / `ctx.msg` 等),Handler/组件从 `ctx` 读取。
97
97
 
98
- **async 工厂组件** — `async (ctx) => (initProps, ctx) => (props) => VNode`:工厂层声明数据(`await ctx.data.get`)、mount 初始化状态(`$`)、render 输出视图。异步只在工厂边界,mount/render 保持同步;数据经闭包注入,写数据像写同步代码。三条纪律见[核心概念 · async 组件](#核心概念)。
98
+ **async 工厂组件** — `async (initProps, ctx) => (props) => Promise<VNode>`(weifuwu **唯一组件形态**——同步组件已不支持):工厂层声明数据(`await ctx.data.get`)、mount 初始化状态(`let` + `render()`)、render 输出视图。异步在工厂边界与 renderFn,数据经闭包注入,写数据像写同步代码。三条纪律见[核心概念 · async 组件](#核心概念)。
99
99
 
100
100
  **SPA/SSR/Hydration 统一透明** — 同一份路由定义(`UIRouter`)一个组件三场景自动适配:后端 `ssrPage(router, { url })` 匹配即自动 SSR(完整 HTML + `__DATA__`),客户端 `uiServe(router, { root, hydrate: true })` 按 URL 同源匹配并收养服务端 HTML(不重建、无闪跳)。`ctx.data.get` 一个 API:SSR 预取 / hydration 命中(不重复请求)/ SPA 触发 fetch。服务端直接用 `.tsx`(`weifuwu/dev` Node loader),前后端同一 JSX 运行时。
101
101
 
@@ -133,7 +133,7 @@ const app = new UIRouter()
133
133
  // async 组件(原生):await 数据 → 返回视图(外层初始化,内层渲染)
134
134
  const Home = async (_init, ctx) => {
135
135
  const msg = await ctx.data.get('/api/hello') // 数据管道:一个 API 三场景
136
- return (props) => <h1>{msg.msg}</h1>
136
+ return async (props) => <h1>{msg.msg}</h1>
137
137
  }
138
138
 
139
139
  app.get('/', async () => <Home />) // handler = 异步组件
@@ -275,25 +275,24 @@ cd apps/agent-platform && npm run seed && npm run dev
275
275
  import { UIRouter, uiServe, h } from 'weifuwu/ui-dom'
276
276
  import { Card, Button, Badge } from 'weifuwu/components'
277
277
 
278
- // 组件 = (initProps, ctx) => (props) => VNode
279
- const Counter = (_props, ctx) => {
280
- const $ = ctx.ui.$()
281
- $.count = 0 // mount 初始化
278
+ // 组件 = async (initProps, ctx) => (props) => Promise<VNode>(render-only:改状态后 ctx.ui.render())
279
+ const Counter = async (_init, ctx) => {
280
+ let count = 0 // mount 初始化
282
281
 
283
282
  return () =>
284
283
  h(Card, { variant: 'default', padding: 'lg' },
285
284
  h('h2', { style: { textAlign: 'center', margin: 0 } }, '⚡ Weifuwu'),
286
285
  h('div', { style: { fontSize: '4rem', fontWeight: 600, textAlign: 'center' } },
287
- String($.count)),
286
+ String(count)),
288
287
  h('div', { style: { textAlign: 'center', marginTop: '1rem' } },
289
288
  h(Badge, {
290
- variant: $.count % 2 === 0 ? 'success' : 'warning'
291
- }, $.count % 2 === 0 ? '偶数' : '奇数')),
289
+ variant: count % 2 === 0 ? 'success' : 'warning'
290
+ }, count % 2 === 0 ? '偶数' : '奇数')),
292
291
  h('hr', { style: { margin: '1rem 0', border: 'none', borderTop: '1px solid #eee' } }),
293
292
  h('div', { style: { display: 'flex', gap: '0.5rem', justifyContent: 'center' } },
294
- h(Button, { variant: 'secondary', onClick: () => $.count-- }, '➖ 减 1'),
295
- h(Button, { variant: 'danger', onClick: () => $.count = 0 }, '↺ 重置'),
296
- h(Button, { variant: 'primary', onClick: () => $.count++ }, '➕ 加 1'),
293
+ h(Button, { variant: 'secondary', onClick: () => { count--; ctx.ui.render() } }, '➖ 减 1'),
294
+ h(Button, { variant: 'danger', onClick: () => { count = 0; ctx.ui.render() } }, '↺ 重置'),
295
+ h(Button, { variant: 'primary', onClick: () => { count++; ctx.ui.render() } }, '➕ 加 1'),
297
296
  ),
298
297
  )
299
298
  }
@@ -356,7 +355,7 @@ cd apps/agent-platform && npm run seed && npm run dev
356
355
  | `weifuwu/ui-dom` | **i18n** | 国际化中间件(运行时切换语言) | — |
357
356
  | `weifuwu/ui-dom` | **ssrPage / serializeData** | 服务端渲染:SSR HTML + `__DATA__` 序列化(`ctx.params` 两端同源) | — |
358
357
  | `weifuwu/ui-dom` | **useChat / AiChat 原语** | AI 会话(流式/工具调用/HITL) | — |
359
- | `weifuwu/ui-dom` | **事件原语** | `usePopup` / `useDialog` / `usePresence` / `useInView` / `useScrollPosition` / `useGlobalKey` / `useDrag` / `useDragDrop` / `useAnimationEnd` / `useTween` / `useReducedMotion`(浏览器事件/动画统一入口,见 [docs/mobile.md](docs/mobile.md)) | — |
358
+ | `weifuwu/ui-dom` | **事件原语** | `usePopup`(统一弹窗能力层)/ `usePresence` / `useInView` / `useScrollPosition` / `useGlobalKey` / `useDrag` / `useDragDrop` / `useAnimationEnd` / `useTween` / `useReducedMotion`(浏览器事件/动画统一入口,见 [docs/mobile.md](docs/mobile.md)) | — |
360
359
  | `weifuwu/components` | **113 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/ui-dom |
361
360
  | `weifuwu/layout` | **CSS 布局** | 58 个布局原语 + 136 个工具类 + 167 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
362
361
 
@@ -381,7 +380,7 @@ cd apps/agent-platform && npm run seed && npm run dev
381
380
  | AI 对话 / Agent / HITL 审批 | `ai()` → `ctx.ai` + `ctx.ui.useChat()` + `AiChat` | [docs/saas.md](docs/saas.md) |
382
381
  | GraphQL / WebSocket | `app.graphql(handler)` · `app.ws(path, handler)` | [docs/realtime.md](docs/realtime.md) |
383
382
  | 前端 UI 组件 | `weifuwu/components`(113 个:Button/Table/Modal/AiChat/...) | [docs/components.md](docs/components.md) |
384
- | 布局/主题/暗色 | `weifuwu/layout`(57 原语 + 136 工具类 + 167 Token) | [docs/layout.md](docs/layout.md) |
383
+ | 布局/主题/暗色 | `weifuwu/layout`(58 原语 + 136 工具类 + 167 Token) | [docs/layout.md](docs/layout.md) |
385
384
  | 样式定制(零自定义 CSS) | `--wf-*` 变量覆盖 + 组件定制钩子 | [docs/styling.md](docs/styling.md) |
386
385
  | 移动端适配(tap/长按/键盘/弹层) | `usePopup` / `useHoverCapable` / `useLongPress` / `useVisualViewport` | [docs/mobile.md](docs/mobile.md) |
387
386
  | 前后端类型安全中间件 | `createMiddleware`(声明注入即类型化) | [docs/server.md](docs/server.md) |
@@ -390,27 +389,25 @@ cd apps/agent-platform && npm run seed && npm run dev
390
389
 
391
390
  ## 核心概念
392
391
 
393
- ### 三层形态(路由 / 同步组件 / async 组件)
392
+ ### 三层形态(路由 / 组件 / async 组件工厂)
394
393
 
395
394
  | 层 | 签名 | 异步 | 生命周期 |
396
395
  |----|------|------|---------|
397
396
  | **UIHandler**(路由) | `async (location, ctx) => VNode` | ✅ 整体 | 每次路由变化执行 |
398
- | **Component**(同步组件) | `(initProps, ctx) => (props) => VNode` | 同步 | mount 一次 + render 每次 |
399
- | **AsyncComponent**(async 组件) | `async (initProps, ctx) => (props) => VNode` | ✅ 只工厂 | 工厂按实例(diff 传递 `_asyncDef`,补全不重跑) |
397
+ | **Component**(唯一形态) | `async (initProps, ctx) => (props) => Promise<VNode>` | 工厂 + renderFn | mount 一次 + render 每次;同步组件已不支持(类型强制 Promise) |
400
398
 
401
- 异步只出现在两个边界——路由 handler(整页)和 async 组件工厂(数据声明);async 组件与同步组件**同签名**(唯一差别是 `async` 关键字)。渲染器按「返回值 instanceof Promise」统一判别:主路径 `buildVNode` async 预构建(await 全部工厂,兄弟并行)→ 落地零占位;动态挂载兑底占位 + 局部补全;骨架屏 `uiServe({ loading })` + `handle.ready`。
399
+ 异步只在两个边界——路由 handler(整页)和组件工厂(数据声明)+ renderFn(强制异步)。渲染器按「返回值 instanceof Promise」统一判别:主路径 `buildVNode` async 预构建(await 全部工厂,兄弟并行)→ 原子落地(无占位、无补全回调);运行时首次挂载的 async 组件同样在 buildVNode 阶段 await;骨架屏 `uiServe({ loading })` + `handle.ready`。
402
400
  ### 两阶段组件(新手必读:为什么是两层)
403
401
 
404
- 组件 = `(initProps, ctx) => (props) => VNode`——**外层 = 初始化(只执行一次),内层 = 渲染(每次状态/props 变化时执行)**。类比:外层是对象的构造函数,内层是它的 render 方法。
402
+ 组件 = `async (initProps, ctx) => (props) => Promise<VNode>`——**外层 = 初始化(只执行一次,可 await 数据),内层 = 渲染(每次状态/props 变化时执行,强制异步)**。类比:外层是对象的构造函数,内层是它的 render 方法。
405
403
 
406
404
  ```tsx
407
- const Counter = (_init, ctx) => {
408
- // 外层(mount):只跑一次——初始化状态、订阅、定时器
409
- const $ = ctx.ui.$()
410
- $.count = 0
411
- return (props) =>
412
- // 内层(render):每次变化执行——读状态输出视图
413
- <button onClick={() => $.count++}>{$.count}</button>
405
+ const Counter = async (_init, ctx) => {
406
+ // 外层(mount):只跑一次——初始化状态、订阅、定时器(可 await 数据)
407
+ let count = 0
408
+ return async (props) =>
409
+ // 内层(render):每次变化执行(强制异步)——读状态输出视图(render-only:改状态后显式 render()
410
+ <button onClick={() => { count++; ctx.ui.render() }}>{count}</button>
414
411
  }
415
412
  ```
416
413
 
@@ -441,7 +438,7 @@ const Counter = (_init, ctx) => {
441
438
  |------|------|------|
442
439
  | 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
443
440
  | 读取 | handler 读取 ctx | 组件读取 ctx |
444
- | 渲染 | 返回 Response | `ctx.ui.render()` / `ctx.ui.dirty()` / `$.x = val` 触发局部 VDOM patch |
441
+ | 渲染 | 返回 Response | `ctx.ui.render()` 触发局部 VDOM patch(render-only);共享状态 `createStore` + `useExternal` |
445
442
 
446
443
  ### async 组件(三条纪律)
447
444
 
@@ -450,12 +447,11 @@ async 组件让"拿数据渲染页面"像写同步代码——签名与同步组
450
447
  ```tsx
451
448
  const UserProfile = async (_init, ctx) => {
452
449
  const user = await ctx.data.get(`/api/user/${ctx.params.id}`) // ① 工厂层:声明数据(三场景自动)
453
- const $ = ctx.ui.$()
454
- $.liked = false // ② mount:客户端状态
455
- return (props) =>
450
+ let liked = false // ② mount:客户端状态(render-only)
451
+ return async (props) =>
456
452
  h('div', {},
457
453
  h('p', {}, user.name), // 服务端状态(闭包,SSR 进 HTML)
458
- h('button', { onClick: () => $.liked = !$.liked }, $.liked ? '❤️' : '🤍'))
454
+ h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }, liked ? '❤️' : '🤍'))
459
455
  }
460
456
  ```
461
457
 
@@ -464,14 +460,13 @@ const UserProfile = async (_init, ctx) => {
464
460
  | 纪律 | 反例 | 正确 |
465
461
  |---|---|---|
466
462
  | ① 数据 key 必须含维度 | `ctx.data.get('/api/user')`——`/users/1 → /users/2` 导航命中旧缓存 | `ctx.data.get(\`/api/user/${ctx.params.id}\`)` |
467
- | ② 会变的数据放 `$` | `const count = data.count`——点击永不更新 | `$.count = data.count`(初始值 seed 自服务端数据) |
468
- | ③ 初始状态必须确定性 | `$.w = window.innerWidth`——SSR/hydration mismatch | 用服务端数据 seed,交互后再测 |
463
+ | ② 会变的数据放组件状态 | `const count = data.count`——点击永不更新 | `let count = data.count` + 交互后 `ctx.ui.render()`(初始值 seed 自服务端数据) |
464
+ | ③ 初始状态必须确定性 | `let w = window.innerWidth`——SSR/hydration mismatch | 用服务端数据 seed,交互后再测 |
469
465
 
470
466
  **常见坑**:
471
467
  - 工厂按**实例**执行(N 处实例 = N 次工厂调用)——数据必须走 `ctx.data`(自带缓存 + 并发合并,重复执行零成本);禁止副作用/昂贵操作裸写工厂
472
468
  - 闭包数据是页面加载时的**快照**——路由参数变化靠工厂重跑刷新(key 变 → 缓存 miss → 重新取数)
473
- - **个性化数据不进 `ctx.data`**——SSR 会把工厂取数结果序列化给所有客户端,会话/用户相关数据留在 `$` + fetch
474
- - **占位显示**:async 组件未 resolve 时渲染 `Placeholder`——无边界显示 null;`<Suspense fallback={...}>` 边界内占位处显示 fallback(可选,子树内任意深度 async 组件共享)
469
+ - **个性化数据不进 `ctx.data`**——SSR 会把工厂取数结果序列化给所有客户端,会话/用户相关数据留在客户端 `let` + fetch + `render()`
475
470
 
476
471
  ### 渲染策略:SPA 还是 SSR?
477
472
 
@@ -2,7 +2,7 @@
2
2
  * weifuwu/components — DatePicker
3
3
  *
4
4
  * 四合一日期选择器,支持 mode: date | datetime | time | range
5
- * 使用 createPortal + position:fixed 定位弹出层。
5
+ * 统一 usePopup:focus 触发 + mask 遮罩 + 自由定位(left 对齐 + width 跟随 trigger)。
6
6
  *
7
7
  * 状态管理:闭包变量 + ctx.ui.render()
8
8
  */
@@ -25,7 +25,7 @@ export interface TourProps {
25
25
  }
26
26
  /**
27
27
  * Tour — 新手引导(步骤式)。
28
- * 目标 rect 定位(usePopupPosition 跟随 scroll/resize)+ Portal 遮罩 + 步骤气泡。
28
+ * 统一 usePopup:mask 遮罩 + portal 出口;position 回调更新目标 rect(scroll 跟随)。
29
29
  *
30
30
  * 状态纪律:
31
31
  * - 步骤索引闭包 let + render()(手动模式——避免 $ 内置类型问题)