weifuwu 0.76.0 → 0.78.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 +38 -41
- package/dist/components/DatePicker/DatePicker.d.ts +1 -1
- package/dist/components/List/List.d.ts +3 -0
- 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 +1272 -1228
- 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 +31 -41
- package/dist/ui-dom/vdom/audit.d.ts +20 -0
- package/dist/ui-dom/vdom/build.d.ts +10 -3
- package/dist/ui-dom/vdom/diff.d.ts +7 -8
- 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 +4 -0
- package/dist/ui-dom/vdom/serve.d.ts +1 -1
- package/dist/ui-dom/vdom/transform.d.ts +32 -0
- package/dist/ui-dom/vnode.d.ts +18 -10
- package/docs/components.md +5 -5
- package/docs/custom-components.md +86 -54
- package/docs/examples.md +35 -41
- package/docs/frontend-middleware.md +3 -4
- package/docs/frontend-ui-dom.md +22 -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,15 @@ 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
|
+
|
|
96
|
+
**VDOM 输出透明(写 JSX,看 DOM 即真相)** — VDOM 对用户输入零 magic:条件渲染的 false 在 DOM 里是诊断占位注释(`<!--wf-hole: false-->`),数组项 key 与组件实例 id 直接落 DOM(`data-wf-key` / `data-wf-id`)——devtools 看到的 DOM 就是引擎决策的可读输出;非法输入占位 + warn,不崩溃不静默。转化契约唯一清晰(design/vdom-transform-rules.md):用户写什么,vnode 就是什么,DOM 就长什么样。
|
|
95
97
|
|
|
96
98
|
**中间件注入一切** — 后端和前端共用同一理念:中间件向 `ctx` 注入能力(`ctx.sql` / `ctx.redis` / `ctx.api` / `ctx.auth` / `ctx.i18n` / `ctx.limit` / `ctx.email` / `ctx.queue` / `ctx.ai` / `ctx.msg` 等),Handler/组件从 `ctx` 读取。
|
|
97
99
|
|
|
98
|
-
**async 工厂组件** — `async (
|
|
100
|
+
**async 工厂组件** — `async (initProps, ctx) => (props) => Promise<VNode>`(weifuwu **唯一组件形态**——同步组件已不支持):工厂层声明数据(`await ctx.data.get`)、mount 初始化状态(`let` + `render()`)、render 输出视图。异步在工厂边界与 renderFn,数据经闭包注入,写数据像写同步代码。三条纪律见[核心概念 · async 组件](#核心概念)。
|
|
99
101
|
|
|
100
102
|
**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
103
|
|
|
@@ -133,7 +135,7 @@ const app = new UIRouter()
|
|
|
133
135
|
// async 组件(原生):await 数据 → 返回视图(外层初始化,内层渲染)
|
|
134
136
|
const Home = async (_init, ctx) => {
|
|
135
137
|
const msg = await ctx.data.get('/api/hello') // 数据管道:一个 API 三场景
|
|
136
|
-
return (props) => <h1>{msg.msg}</h1>
|
|
138
|
+
return async (props) => <h1>{msg.msg}</h1>
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
app.get('/', async () => <Home />) // handler = 异步组件
|
|
@@ -275,25 +277,24 @@ cd apps/agent-platform && npm run seed && npm run dev
|
|
|
275
277
|
import { UIRouter, uiServe, h } from 'weifuwu/ui-dom'
|
|
276
278
|
import { Card, Button, Badge } from 'weifuwu/components'
|
|
277
279
|
|
|
278
|
-
// 组件 = (initProps, ctx) => (props) => VNode
|
|
279
|
-
const Counter = (
|
|
280
|
-
|
|
281
|
-
$.count = 0 // mount 初始化
|
|
280
|
+
// 组件 = async (initProps, ctx) => (props) => Promise<VNode>(render-only:改状态后 ctx.ui.render())
|
|
281
|
+
const Counter = async (_init, ctx) => {
|
|
282
|
+
let count = 0 // mount 初始化
|
|
282
283
|
|
|
283
284
|
return () =>
|
|
284
285
|
h(Card, { variant: 'default', padding: 'lg' },
|
|
285
286
|
h('h2', { style: { textAlign: 'center', margin: 0 } }, '⚡ Weifuwu'),
|
|
286
287
|
h('div', { style: { fontSize: '4rem', fontWeight: 600, textAlign: 'center' } },
|
|
287
|
-
String(
|
|
288
|
+
String(count)),
|
|
288
289
|
h('div', { style: { textAlign: 'center', marginTop: '1rem' } },
|
|
289
290
|
h(Badge, {
|
|
290
|
-
variant:
|
|
291
|
-
},
|
|
291
|
+
variant: count % 2 === 0 ? 'success' : 'warning'
|
|
292
|
+
}, count % 2 === 0 ? '偶数' : '奇数')),
|
|
292
293
|
h('hr', { style: { margin: '1rem 0', border: 'none', borderTop: '1px solid #eee' } }),
|
|
293
294
|
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: () =>
|
|
295
|
+
h(Button, { variant: 'secondary', onClick: () => { count--; ctx.ui.render() } }, '➖ 减 1'),
|
|
296
|
+
h(Button, { variant: 'danger', onClick: () => { count = 0; ctx.ui.render() } }, '↺ 重置'),
|
|
297
|
+
h(Button, { variant: 'primary', onClick: () => { count++; ctx.ui.render() } }, '➕ 加 1'),
|
|
297
298
|
),
|
|
298
299
|
)
|
|
299
300
|
}
|
|
@@ -356,7 +357,7 @@ cd apps/agent-platform && npm run seed && npm run dev
|
|
|
356
357
|
| `weifuwu/ui-dom` | **i18n** | 国际化中间件(运行时切换语言) | — |
|
|
357
358
|
| `weifuwu/ui-dom` | **ssrPage / serializeData** | 服务端渲染:SSR HTML + `__DATA__` 序列化(`ctx.params` 两端同源) | — |
|
|
358
359
|
| `weifuwu/ui-dom` | **useChat / AiChat 原语** | AI 会话(流式/工具调用/HITL) | — |
|
|
359
|
-
| `weifuwu/ui-dom` | **事件原语** | `usePopup
|
|
360
|
+
| `weifuwu/ui-dom` | **事件原语** | `usePopup`(统一弹窗能力层)/ `usePresence` / `useInView` / `useScrollPosition` / `useGlobalKey` / `useDrag` / `useDragDrop` / `useAnimationEnd` / `useTween` / `useReducedMotion`(浏览器事件/动画统一入口,见 [docs/mobile.md](docs/mobile.md)) | — |
|
|
360
361
|
| `weifuwu/components` | **113 个组件** | Button/Table/Modal/Confirm/Toast/... + `confirm()` / `toast()` 命令式中间件 | weifuwu/ui-dom |
|
|
361
362
|
| `weifuwu/layout` | **CSS 布局** | 58 个布局原语 + 136 个工具类 + 167 个主题 Token(也支持 `weifuwu/layout/style.css`) | — |
|
|
362
363
|
|
|
@@ -381,7 +382,7 @@ cd apps/agent-platform && npm run seed && npm run dev
|
|
|
381
382
|
| AI 对话 / Agent / HITL 审批 | `ai()` → `ctx.ai` + `ctx.ui.useChat()` + `AiChat` | [docs/saas.md](docs/saas.md) |
|
|
382
383
|
| GraphQL / WebSocket | `app.graphql(handler)` · `app.ws(path, handler)` | [docs/realtime.md](docs/realtime.md) |
|
|
383
384
|
| 前端 UI 组件 | `weifuwu/components`(113 个:Button/Table/Modal/AiChat/...) | [docs/components.md](docs/components.md) |
|
|
384
|
-
| 布局/主题/暗色 | `weifuwu/layout`(
|
|
385
|
+
| 布局/主题/暗色 | `weifuwu/layout`(58 原语 + 136 工具类 + 167 Token) | [docs/layout.md](docs/layout.md) |
|
|
385
386
|
| 样式定制(零自定义 CSS) | `--wf-*` 变量覆盖 + 组件定制钩子 | [docs/styling.md](docs/styling.md) |
|
|
386
387
|
| 移动端适配(tap/长按/键盘/弹层) | `usePopup` / `useHoverCapable` / `useLongPress` / `useVisualViewport` | [docs/mobile.md](docs/mobile.md) |
|
|
387
388
|
| 前后端类型安全中间件 | `createMiddleware`(声明注入即类型化) | [docs/server.md](docs/server.md) |
|
|
@@ -390,27 +391,25 @@ cd apps/agent-platform && npm run seed && npm run dev
|
|
|
390
391
|
|
|
391
392
|
## 核心概念
|
|
392
393
|
|
|
393
|
-
### 三层形态(路由 /
|
|
394
|
+
### 三层形态(路由 / 组件 / async 组件工厂)
|
|
394
395
|
|
|
395
396
|
| 层 | 签名 | 异步 | 生命周期 |
|
|
396
397
|
|----|------|------|---------|
|
|
397
398
|
| **UIHandler**(路由) | `async (location, ctx) => VNode` | ✅ 整体 | 每次路由变化执行 |
|
|
398
|
-
| **Component
|
|
399
|
-
| **AsyncComponent**(async 组件) | `async (initProps, ctx) => (props) => VNode` | ✅ 只工厂 | 工厂按实例(diff 传递 `_asyncDef`,补全不重跑) |
|
|
399
|
+
| **Component**(唯一形态) | `async (initProps, ctx) => (props) => Promise<VNode>` | ✅ 工厂 + renderFn | mount 一次 + render 每次;同步组件已不支持(类型强制 Promise) |
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
异步只在两个边界——路由 handler(整页)和组件工厂(数据声明)+ renderFn(强制异步)。渲染器按「返回值 instanceof Promise」统一判别:主路径 `buildVNode` async 预构建(await 全部工厂,兄弟并行)→ 原子落地(无**中间态**占位、无补全回调——注意:数组内 false/null 的静态诊断占位 `<!--wf-hole-->` 是另一回事,见「VDOM 输出透明」);运行时首次挂载的 async 组件同样在 buildVNode 阶段 await;骨架屏 `uiServe({ loading })` + `handle.ready`。
|
|
402
402
|
### 两阶段组件(新手必读:为什么是两层)
|
|
403
403
|
|
|
404
|
-
组件 = `(initProps, ctx) => (props) => VNode
|
|
404
|
+
组件 = `async (initProps, ctx) => (props) => Promise<VNode>`——**外层 = 初始化(只执行一次,可 await 数据),内层 = 渲染(每次状态/props 变化时执行,强制异步)**。类比:外层是对象的构造函数,内层是它的 render 方法。
|
|
405
405
|
|
|
406
406
|
```tsx
|
|
407
|
-
const Counter = (_init, ctx) => {
|
|
408
|
-
// 外层(mount
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
<button onClick={() => $.count++}>{$.count}</button>
|
|
407
|
+
const Counter = async (_init, ctx) => {
|
|
408
|
+
// 外层(mount):只跑一次——初始化状态、订阅、定时器(可 await 数据)
|
|
409
|
+
let count = 0
|
|
410
|
+
return async (props) =>
|
|
411
|
+
// 内层(render):每次变化执行(强制异步)——读状态输出视图(render-only:改状态后显式 render())
|
|
412
|
+
<button onClick={() => { count++; ctx.ui.render() }}>{count}</button>
|
|
414
413
|
}
|
|
415
414
|
```
|
|
416
415
|
|
|
@@ -441,7 +440,7 @@ const Counter = (_init, ctx) => {
|
|
|
441
440
|
|------|------|------|
|
|
442
441
|
| 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
|
|
443
442
|
| 读取 | handler 读取 ctx | 组件读取 ctx |
|
|
444
|
-
| 渲染 | 返回 Response | `ctx.ui.render()`
|
|
443
|
+
| 渲染 | 返回 Response | `ctx.ui.render()` 触发局部 VDOM patch(render-only);共享状态 `createStore` + `useExternal` |
|
|
445
444
|
|
|
446
445
|
### async 组件(三条纪律)
|
|
447
446
|
|
|
@@ -450,12 +449,11 @@ async 组件让"拿数据渲染页面"像写同步代码——签名与同步组
|
|
|
450
449
|
```tsx
|
|
451
450
|
const UserProfile = async (_init, ctx) => {
|
|
452
451
|
const user = await ctx.data.get(`/api/user/${ctx.params.id}`) // ① 工厂层:声明数据(三场景自动)
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
return (props) =>
|
|
452
|
+
let liked = false // ② mount:客户端状态(render-only)
|
|
453
|
+
return async (props) =>
|
|
456
454
|
h('div', {},
|
|
457
455
|
h('p', {}, user.name), // 服务端状态(闭包,SSR 进 HTML)
|
|
458
|
-
h('button', { onClick: () =>
|
|
456
|
+
h('button', { onClick: () => { liked = !liked; ctx.ui.render() } }, liked ? '❤️' : '🤍'))
|
|
459
457
|
}
|
|
460
458
|
```
|
|
461
459
|
|
|
@@ -464,14 +462,13 @@ const UserProfile = async (_init, ctx) => {
|
|
|
464
462
|
| 纪律 | 反例 | 正确 |
|
|
465
463
|
|---|---|---|
|
|
466
464
|
| ① 数据 key 必须含维度 | `ctx.data.get('/api/user')`——`/users/1 → /users/2` 导航命中旧缓存 | `ctx.data.get(\`/api/user/${ctx.params.id}\`)` |
|
|
467
|
-
| ②
|
|
468
|
-
| ③ 初始状态必须确定性 |
|
|
465
|
+
| ② 会变的数据放组件状态 | `const count = data.count`——点击永不更新 | `let count = data.count` + 交互后 `ctx.ui.render()`(初始值 seed 自服务端数据) |
|
|
466
|
+
| ③ 初始状态必须确定性 | `let w = window.innerWidth`——SSR/hydration mismatch | 用服务端数据 seed,交互后再测 |
|
|
469
467
|
|
|
470
468
|
**常见坑**:
|
|
471
469
|
- 工厂按**实例**执行(N 处实例 = N 次工厂调用)——数据必须走 `ctx.data`(自带缓存 + 并发合并,重复执行零成本);禁止副作用/昂贵操作裸写工厂
|
|
472
470
|
- 闭包数据是页面加载时的**快照**——路由参数变化靠工厂重跑刷新(key 变 → 缓存 miss → 重新取数)
|
|
473
|
-
- **个性化数据不进 `ctx.data`**——SSR
|
|
474
|
-
- **占位显示**:async 组件未 resolve 时渲染 `Placeholder`——无边界显示 null;`<Suspense fallback={...}>` 边界内占位处显示 fallback(可选,子树内任意深度 async 组件共享)
|
|
471
|
+
- **个性化数据不进 `ctx.data`**——SSR 会把工厂取数结果序列化给所有客户端,会话/用户相关数据留在客户端 `let` + fetch + `render()`
|
|
475
472
|
|
|
476
473
|
### 渲染策略:SPA 还是 SSR?
|
|
477
474
|
|
|
@@ -8,6 +8,9 @@ import type { Component } from '../../ui-dom/vnode.ts';
|
|
|
8
8
|
export interface ListProps<T = any> {
|
|
9
9
|
items: T[];
|
|
10
10
|
renderItem: (item: T, index: number) => any;
|
|
11
|
+
/** 自定义项 key(可选,默认数组下标)——renderItem 渲染有内部状态的组件且列表动态增删/重排时传
|
|
12
|
+
* 身份跟随内容的 key(如项 id),否则默认下标 = 位置身份,增删后项状态会继承错位(规则表 §3) */
|
|
13
|
+
keyBy?: (item: T, index: number) => string | number;
|
|
11
14
|
divided?: boolean;
|
|
12
15
|
header?: any;
|
|
13
16
|
footer?: any;
|
|
@@ -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()(手动模式——避免 $ 内置类型问题)
|