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/docs/examples.md
CHANGED
|
@@ -5,12 +5,11 @@
|
|
|
5
5
|
## 登录表单
|
|
6
6
|
|
|
7
7
|
```tsx
|
|
8
|
-
const LoginPage = (_init, ctx) => {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
$.submitting = false
|
|
8
|
+
const LoginPage = async (_init, ctx) => {
|
|
9
|
+
let errors: Record<string, string> = {}
|
|
10
|
+
let submitting = false
|
|
12
11
|
|
|
13
|
-
return (props) =>
|
|
12
|
+
return async (props) =>
|
|
14
13
|
h('div', { class: 'wf-stack', style: { maxWidth: 400, margin: '40px auto' } },
|
|
15
14
|
h(Card, { padding: 'lg' },
|
|
16
15
|
h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
@@ -21,17 +20,19 @@ const LoginPage = (_init, ctx) => {
|
|
|
21
20
|
password: [{ required: true, minLength: 6, message: '密码至少6位' }],
|
|
22
21
|
},
|
|
23
22
|
onSubmit: async (values) => {
|
|
24
|
-
|
|
23
|
+
submitting = true
|
|
24
|
+
ctx.ui.render()
|
|
25
25
|
await ctx.api?.post('/login', values) // api 客户端由中间件注入 ctx.api
|
|
26
|
-
|
|
26
|
+
submitting = false
|
|
27
|
+
ctx.ui.render()
|
|
27
28
|
},
|
|
28
|
-
onError: (
|
|
29
|
+
onError: (errs) => { errors = errs; ctx.ui.render() },
|
|
29
30
|
}, [
|
|
30
|
-
h(Field, { label: '邮箱', error:
|
|
31
|
+
h(Field, { label: '邮箱', error: errors.email },
|
|
31
32
|
h(Input, { name: 'email', type: 'email', placeholder: 'name@example.com' })),
|
|
32
|
-
h(Field, { label: '密码', error:
|
|
33
|
+
h(Field, { label: '密码', error: errors.password },
|
|
33
34
|
h(Input, { name: 'password', type: 'password' })),
|
|
34
|
-
h(Button, { type: 'submit', loading:
|
|
35
|
+
h(Button, { type: 'submit', loading: submitting, block: true }, '登录'),
|
|
35
36
|
])
|
|
36
37
|
)
|
|
37
38
|
)
|
|
@@ -42,20 +43,19 @@ const LoginPage = (_init, ctx) => {
|
|
|
42
43
|
## 数据列表 + 搜索
|
|
43
44
|
|
|
44
45
|
```tsx
|
|
45
|
-
const UserList = (_init, ctx) => {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
$.sortOrder = 'asc'
|
|
46
|
+
const UserList = async (_init, ctx) => {
|
|
47
|
+
let keyword = ''
|
|
48
|
+
let sortKey = 'name'
|
|
49
|
+
let sortOrder = 'asc'
|
|
50
50
|
const users = [
|
|
51
51
|
{ id: 1, name: '张三', email: 'zhang@example.com', role: '管理员' },
|
|
52
52
|
{ id: 2, name: '李四', email: 'li@example.com', role: '编辑' },
|
|
53
53
|
]
|
|
54
54
|
|
|
55
|
-
return (props) => {
|
|
56
|
-
// 派生数据必须在 render 内计算(每次 render 读最新
|
|
55
|
+
return async (props) => {
|
|
56
|
+
// 派生数据必须在 render 内计算(每次 render 读最新 keyword)
|
|
57
57
|
const filtered = users.filter(u =>
|
|
58
|
-
|
|
58
|
+
!keyword || u.name.includes(keyword) || u.email.includes(keyword)
|
|
59
59
|
)
|
|
60
60
|
|
|
61
61
|
return h('div', { class: 'wf-stack', style: { gap: 'var(--wf-space-md)' } },
|
|
@@ -85,39 +85,33 @@ const UserList = (_init, ctx) => {
|
|
|
85
85
|
## 消息提示
|
|
86
86
|
|
|
87
87
|
```tsx
|
|
88
|
-
//
|
|
89
|
-
|
|
88
|
+
// 官方推荐:命令式中间件(app.use(toast()) → ctx.toast('消息', 'success'))
|
|
89
|
+
app.use(toast())
|
|
90
|
+
// 任意组件:ctx.toast?.('操作成功', 'success')
|
|
90
91
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
$.toasts = $.toasts ?? []
|
|
95
|
-
const id = String(++toastId)
|
|
96
|
-
$.toasts = [...$.toasts, { id, type, message }]
|
|
92
|
+
// 自管理列表(render-only:let + render())
|
|
93
|
+
let toasts: { id: string; type: string; message: string }[] = []
|
|
94
|
+
let toastId = 0
|
|
97
95
|
|
|
98
|
-
|
|
96
|
+
function showToast(ctx: WfuiContext, type: string, message: string) {
|
|
97
|
+
toasts = [...toasts, { id: String(++toastId), type, message }]
|
|
98
|
+
ctx.ui.render()
|
|
99
99
|
if (type !== 'error') {
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
$.toasts = $.toasts.filter((t: any) => t.id !== id)
|
|
102
|
-
}, 3000)
|
|
100
|
+
setTimeout(() => { toasts = toasts.filter((t: any) => t.id !== String(toastId)); ctx.ui.render() }, 3000)
|
|
103
101
|
}
|
|
104
102
|
}
|
|
105
103
|
|
|
106
104
|
// 页面中使用
|
|
107
|
-
const App = (_init, ctx) => {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
return (props) =>
|
|
105
|
+
const App = async (_init, ctx) => {
|
|
106
|
+
toasts = []
|
|
107
|
+
return async (props) =>
|
|
112
108
|
h('div', {}, [
|
|
113
|
-
h(Button, {
|
|
114
|
-
onClick: () => showToast(ctx, 'success', '操作成功'),
|
|
115
|
-
}, '显示提示'),
|
|
109
|
+
h(Button, { onClick: () => showToast(ctx, 'success', '操作成功') }, '显示提示'),
|
|
116
110
|
h(Toast, {
|
|
117
|
-
toasts
|
|
111
|
+
toasts,
|
|
118
112
|
position: 'top-right',
|
|
119
113
|
max: 3,
|
|
120
|
-
onRemove: (id) => {
|
|
114
|
+
onRemove: (id) => { toasts = toasts.filter((t: any) => t.id !== id); ctx.ui.render() },
|
|
121
115
|
}),
|
|
122
116
|
])
|
|
123
117
|
}
|
|
@@ -24,8 +24,7 @@ uiServe(app, { root: '#root' }) // 客户端落地(hydrate: true 收养 SS
|
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
26
|
const DashboardLayout: UIMiddleware = async (_loc, ctx, children) => {
|
|
27
|
-
|
|
28
|
-
$.open = true
|
|
27
|
+
let open = true
|
|
29
28
|
return async (loc, c) => {
|
|
30
29
|
const child = await children(loc, c) // 子路由/嵌套路由内容
|
|
31
30
|
return h('div', { class: 'wf-row' },
|
|
@@ -132,8 +131,8 @@ app.use(auth())
|
|
|
132
131
|
uiServe(app, { root: '#root' })
|
|
133
132
|
|
|
134
133
|
// 在组件中
|
|
135
|
-
function Profile(_props: {}, ctx: WfuiContext) {
|
|
136
|
-
return (props) => {
|
|
134
|
+
async function Profile(_props: {}, ctx: WfuiContext) {
|
|
135
|
+
return async (props) => {
|
|
137
136
|
if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
|
|
138
137
|
return <p>欢迎, {ctx.auth?.user?.name}</p>
|
|
139
138
|
}
|
package/docs/frontend-ui-dom.md
CHANGED
|
@@ -23,16 +23,15 @@
|
|
|
23
23
|
### 两阶段组件模型:从 hooks 心智负担中解脱
|
|
24
24
|
|
|
25
25
|
```tsx
|
|
26
|
-
const Counter = (initProps, ctx) => {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
h('button', { onClick: () => $.count += props.step }, $.count) // render
|
|
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
|
|
31
30
|
}
|
|
32
31
|
```
|
|
33
32
|
|
|
34
|
-
- **没有 hooks 规则、没有依赖数组、没有闭包陷阱**。外层 = 初始化(一次),内层 =
|
|
35
|
-
-
|
|
33
|
+
- **没有 hooks 规则、没有依赖数组、没有闭包陷阱**。外层 = 初始化(一次),内层 = 渲染(每次变化)。
|
|
34
|
+
- **render-only 确定性渲染**(design/render-only-plan.md):渲染只发生在 `ctx.ui.render()` 调用处——改状态后显式 `render()`,行为可静态推导。无 `$` Proxy、无隐式触发;跨组件共享用 `createStore` + `ctx.ui.useExternal()`。
|
|
36
35
|
|
|
37
36
|
### 框架即纪律:浏览器环境抽象把常见坑变成编译期/审计期错误
|
|
38
37
|
|
|
@@ -55,7 +54,7 @@ AGENTS.md 每条纪律对应真实事故(JSONViewer selfId 错位、AutoComple
|
|
|
55
54
|
|
|
56
55
|
| 维度 | 价值 |
|
|
57
56
|
|------|------|
|
|
58
|
-
| **上手** | 两阶段组件 +
|
|
57
|
+
| **上手** | 两阶段组件 + 改状态后 render()——无 hooks/依赖数组心智负担 |
|
|
59
58
|
| **后端互迁** | req/res/中间件契约同构,SSR 透明,一份 router 两端共享 |
|
|
60
59
|
| **可靠性** | 确定性失败、诚实裁剪、环境边界、测试侧同构 |
|
|
61
60
|
| **效率** | 弹层/数据管道/事件原语全覆盖——不重复造轮子 |
|
|
@@ -74,14 +73,13 @@ const app = new UIRouter()
|
|
|
74
73
|
|
|
75
74
|
app.use(toast()) // ctx 注入链(对齐后端 app.use——注入 ctx.toast)
|
|
76
75
|
|
|
77
|
-
// handler = 异步组件:async (location, ctx) => VNode
|
|
76
|
+
// handler = 异步组件:async (location, ctx) => VNode(render-only——改状态后 ctx.ui.render())
|
|
78
77
|
app.get('/', async (location, ctx) => {
|
|
79
78
|
const info = await ctx.data.get('/api/info', async () => ({ title: '首页' }))
|
|
80
|
-
|
|
81
|
-
$.clicks = $.clicks ?? 0
|
|
79
|
+
let clicks = 0
|
|
82
80
|
return h('div', {},
|
|
83
81
|
h('h2', {}, info.title),
|
|
84
|
-
h(Button, { onClick: () =>
|
|
82
|
+
h(Button, { onClick: () => { clicks++; ctx.ui.render() } }, `点击 ${clicks} 次`),
|
|
85
83
|
h(Button, { variant: 'secondary', onClick: () => ctx.toast?.('提示', 'success') }, '弹 toast'),
|
|
86
84
|
)
|
|
87
85
|
})
|
|
@@ -119,17 +117,22 @@ handler 只产 VNode,落地由 serve 决定——SSR 与 SPA 是两种落地
|
|
|
119
117
|
|
|
120
118
|
VNode 契约以 ui-dom 为唯一来源(Fragment/Portal symbol 由 ui-dom 自持)——components 产的 VNode
|
|
121
119
|
直接被 ui-dom 渲染器识别。渲染算法(render/diff/createUi 原语)在 ui-dom 内自主实现,
|
|
122
|
-
**registry/popup-tracker
|
|
120
|
+
**registry/popup-tracker 局部实例**(serve 每实例隔离)。
|
|
123
121
|
|
|
124
122
|
命令式工厂(toast/confirm/notification)位于 ui-dom(`src/ui-dom/Toast.ts` 等):
|
|
125
123
|
components 消费端 import `weifuwu/ui-dom`(构建外部化,共享同一模块实例)。
|
|
126
124
|
|
|
127
|
-
###
|
|
125
|
+
### render-only 渲染(唯一触发:ctx.ui.render())
|
|
128
126
|
|
|
129
|
-
|
|
|
127
|
+
| 原语 | 触发 | 重渲染范围 |
|
|
130
128
|
|------|------|-----------|
|
|
131
|
-
|
|
|
132
|
-
|
|
|
129
|
+
| `ctx.ui.render()`(无参) | 主动调用 | 当前组件(闭包绑定,无 this 陷阱) |
|
|
130
|
+
| `ctx.ui.render(['id'])` | 主动调用 | 指定组件(selfId 注册的语义 ID) |
|
|
131
|
+
| `ctx.ui.useExternal(store)` | store 变更自动 | **仅订阅组件**(unmount 自动退订) |
|
|
132
|
+
|
|
133
|
+
- 状态是普通对象(`let` / `createStore`)——改状态后显式 `render()`,无赋值自动渲染
|
|
134
|
+
- 跨组件共享:`createStore` + `useExternal`(替代已删除的 `$` Proxy / `dirty`)
|
|
135
|
+
- hooks(useMedia/useInView/usePopup 等)事件驱动重渲染——与"赋值自动"本质不同
|
|
133
136
|
|
|
134
137
|
## SSR + hydration(端到端)
|
|
135
138
|
|
|
@@ -154,7 +157,7 @@ uiServe(app, { root: '#root', hydrate: true })
|
|
|
154
157
|
|------|------|
|
|
155
158
|
| `ctx.params` / `ctx.query` | 路由参数 / URL query(顶层,对齐后端) |
|
|
156
159
|
| `ctx.data.get/set/has` | 数据管道(缓存 + in-flight 合并 + `__DATA__` 种子) |
|
|
157
|
-
| `ctx.ui.*` |
|
|
160
|
+
| `ctx.ui.*` | 原语(`render`/`useExternal`/`usePopup`/`useChat`/`useInView`…) |
|
|
158
161
|
| `ctx.browser.*` | 环境抽象(window/document 唯一入口,SSR shim 同构) |
|
|
159
162
|
| `ctx.toast` / `ctx.confirm` / `ctx.notification` | 命令式注入(`app.use(toast())` 等) |
|
|
160
163
|
|