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 +36 -41
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/Tour/Tour.d.ts +1 -1
- package/dist/components/index.js +12 -12
- package/dist/components/style.css +17 -0
- package/dist/index.js +1215 -1224
- package/dist/scheduler/index.d.ts +7 -2
- package/dist/ui-dom/hooks/index.d.ts +0 -1
- package/dist/ui-dom/hooks/popup.d.ts +1 -10
- package/dist/ui-dom/hooks/stable.d.ts +1 -1
- package/dist/ui-dom/hooks/types.d.ts +0 -1
- package/dist/ui-dom/index.d.ts +1 -1
- package/dist/ui-dom/index.js +10 -10
- package/dist/ui-dom/jsx-runtime.js +1 -1
- package/dist/ui-dom/testing.js +1 -1
- package/dist/ui-dom/types.d.ts +29 -41
- package/dist/ui-dom/vdom/build.d.ts +10 -3
- package/dist/ui-dom/vdom/diff.d.ts +6 -7
- package/dist/ui-dom/vdom/index.d.ts +1 -1
- package/dist/ui-dom/vdom/mount.d.ts +14 -5
- package/dist/ui-dom/vdom/render.d.ts +2 -0
- package/dist/ui-dom/vdom/serve.d.ts +1 -1
- package/dist/ui-dom/vnode.d.ts +15 -10
- package/docs/components.md +4 -4
- package/docs/custom-components.md +78 -54
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +21 -18
- package/docs/frontend.md +243 -168
- package/docs/mobile.md +2 -2
- package/docs/realtime.md +8 -3
- package/package.json +1 -1
- package/dist/ui-dom/focus-trap.d.ts +0 -4
- package/dist/ui-dom/scroll-lock.d.ts +0 -5
- package/dist/ui-dom/vdom/scheduler.d.ts +0 -13
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`(渲染引擎,
|
|
34
|
-
> -
|
|
35
|
-
> - 状态:`useControlled()`(受控/非受控)/ `useStableRef()`(稳定 ref
|
|
36
|
-
> - 弹层:`usePopup()
|
|
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
|
|
92
|
+
**两阶段组件模型** — 组件 = `async (initProps, ctx) => (props) => Promise<VNode>`。外层工厂只执行一次(mount,可 await 数据),内层 renderFn 每次状态/props 变化时执行(强制异步)。无 class、无 `this`、无 Hook——**位置即语义**:外层天生只跑一次,没有 hooks 规则、没有依赖数组、没有闭包陷阱(详解见[核心概念](#核心概念))。
|
|
93
93
|
|
|
94
|
-
**
|
|
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 (
|
|
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 = (
|
|
280
|
-
|
|
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(
|
|
286
|
+
String(count)),
|
|
288
287
|
h('div', { style: { textAlign: 'center', marginTop: '1rem' } },
|
|
289
288
|
h(Badge, {
|
|
290
|
-
variant:
|
|
291
|
-
},
|
|
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: () =>
|
|
295
|
-
h(Button, { variant: 'danger', onClick: () =>
|
|
296
|
-
h(Button, { variant: 'primary', onClick: () =>
|
|
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
|
|
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`(
|
|
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
|
-
### 三层形态(路由 /
|
|
392
|
+
### 三层形态(路由 / 组件 / async 组件工厂)
|
|
394
393
|
|
|
395
394
|
| 层 | 签名 | 异步 | 生命周期 |
|
|
396
395
|
|----|------|------|---------|
|
|
397
396
|
| **UIHandler**(路由) | `async (location, ctx) => VNode` | ✅ 整体 | 每次路由变化执行 |
|
|
398
|
-
| **Component
|
|
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
|
-
|
|
399
|
+
异步只在两个边界——路由 handler(整页)和组件工厂(数据声明)+ renderFn(强制异步)。渲染器按「返回值 instanceof Promise」统一判别:主路径 `buildVNode` async 预构建(await 全部工厂,兄弟并行)→ 原子落地(无占位、无补全回调);运行时首次挂载的 async 组件同样在 buildVNode 阶段 await;骨架屏 `uiServe({ loading })` + `handle.ready`。
|
|
402
400
|
### 两阶段组件(新手必读:为什么是两层)
|
|
403
401
|
|
|
404
|
-
组件 = `(initProps, ctx) => (props) => VNode
|
|
402
|
+
组件 = `async (initProps, ctx) => (props) => Promise<VNode>`——**外层 = 初始化(只执行一次,可 await 数据),内层 = 渲染(每次状态/props 变化时执行,强制异步)**。类比:外层是对象的构造函数,内层是它的 render 方法。
|
|
405
403
|
|
|
406
404
|
```tsx
|
|
407
|
-
const Counter = (_init, ctx) => {
|
|
408
|
-
// 外层(mount
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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()`
|
|
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
|
-
|
|
454
|
-
|
|
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: () =>
|
|
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
|
-
| ②
|
|
468
|
-
| ③ 初始状态必须确定性 |
|
|
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
|
|
474
|
-
- **占位显示**:async 组件未 resolve 时渲染 `Placeholder`——无边界显示 null;`<Suspense fallback={...}>` 边界内占位处显示 fallback(可选,子树内任意深度 async 组件共享)
|
|
469
|
+
- **个性化数据不进 `ctx.data`**——SSR 会把工厂取数结果序列化给所有客户端,会话/用户相关数据留在客户端 `let` + fetch + `render()`
|
|
475
470
|
|
|
476
471
|
### 渲染策略:SPA 还是 SSR?
|
|
477
472
|
|
|
@@ -25,7 +25,7 @@ export interface TourProps {
|
|
|
25
25
|
}
|
|
26
26
|
/**
|
|
27
27
|
* Tour — 新手引导(步骤式)。
|
|
28
|
-
*
|
|
28
|
+
* 统一 usePopup:mask 遮罩 + portal 出口;position 回调更新目标 rect(scroll 跟随)。
|
|
29
29
|
*
|
|
30
30
|
* 状态纪律:
|
|
31
31
|
* - 步骤索引闭包 let + render()(手动模式——避免 $ 内置类型问题)
|