weifuwu 0.74.0 → 0.75.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.
@@ -4,19 +4,13 @@
4
4
  * 复制自 client/registry.ts(算法相同),但**无模块级单例**——uiServe 创建实例
5
5
  * 注入 ctx.__registry,render/diff/ui 经 getRegistry(ctx) 读取,与 createApp 零交叉。
6
6
  */
7
- import type { VNode, Component, AsyncComponent } from './vnode.ts';
8
- import type { WfuiContext } from './types.ts';
7
+ import type { VNode } from './vnode.ts';
9
8
  type UnmountHook = (id: string) => void;
10
- interface FactoryEntry {
11
- promise: Promise<Component<any, any>>;
12
- resolved?: Component<any, any>;
13
- }
14
9
  /** 注册表实例状态 */
15
10
  export interface Registry {
16
11
  idCounter: number;
17
12
  idRegistry: Map<string, VNode>;
18
13
  unmountHooks: UnmountHook[];
19
- asyncFactoryCache: WeakMap<AsyncComponent<any, any>, FactoryEntry>;
20
14
  }
21
15
  /** 创建局部注册表(uiServe 每实例一个——组件 id/dirty/卸载钩子与 createApp 隔离) */
22
16
  export declare function createRegistry(): Registry;
@@ -26,12 +20,6 @@ export declare function getRegistry(ctx: any): Registry;
26
20
  export declare function nextComponentIdFor(reg: Registry): string;
27
21
  /** 指定实例注册卸载钩子(组件从 idRegistry 注销时触发) */
28
22
  export declare function onComponentUnmountFor(reg: Registry, hook: UnmountHook): () => void;
29
- /** 启动 async 工厂(幂等,缓存) */
30
- export declare function startAsyncFactory(reg: Registry, Comp: AsyncComponent, ctx: WfuiContext): FactoryEntry;
31
- /** async 模式:await 工厂定义 */
32
- export declare function resolveAsyncFactory(reg: Registry, Comp: AsyncComponent, ctx: WfuiContext): Promise<Component>;
33
- /** sync 模式:工厂已解析 → 定义;未解析 → undefined */
34
- export declare function resolveAsyncFactorySync(reg: Registry, Comp: AsyncComponent): Component | undefined;
35
23
  /** 包裹 ref 回调调用——用户 ref 逻辑抛错时不中断渲染/卸载管线 */
36
24
  export declare function safeCallRef(ref: Function, arg: any, phase: 'mount' | 'cleanup', name?: string): void;
37
25
  /** 通知 ref 清理 + Portal 子容器清理(指定实例) */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * weifuwu/client 渲染器 — VNode → DOM + patchValue diff
2
+ * weifuwu/ui-dom 渲染器 — VNode → DOM + patchValue diff
3
3
  *
4
4
  * render(vnode, ctx) → 首次渲染,返回 DOM
5
5
  * patchValue(el, old, new, ctx) → 增量更新
@@ -1,5 +1,5 @@
1
1
  /**
2
- * weifuwu/client — ScrollLock
2
+ * weifuwu/ui-dom — ScrollLock
3
3
  */
4
4
  export declare function lockScroll(): void;
5
5
  export declare function unlockScroll(): void;
@@ -53,4 +53,5 @@ export declare function ssrPage(router: UIRouter, opts: {
53
53
  title?: string;
54
54
  lang?: string;
55
55
  rootId?: string;
56
+ styles?: string[];
56
57
  }): Promise<SsrPageResult>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * weifuwu/client 类型定义
2
+ * weifuwu/ui-dom 类型定义
3
3
  */
4
4
  import type { UseChatHandle, UseChatOptions } from './use-chat.ts';
5
5
  import type { VNode } from './vnode.ts';
@@ -543,23 +543,23 @@ export interface WfuiContext {
543
543
  };
544
544
  /** API 客户端(由 api 中间件注入);options 为 ApiRequestOptions 形状(headers/signal) */
545
545
  api?: {
546
- get: <T = unknown>(url: string, options?: {
546
+ get: <T = any>(url: string, options?: {
547
547
  headers?: Record<string, string>;
548
548
  signal?: AbortSignal;
549
549
  }) => Promise<T>;
550
- post: <T = unknown>(url: string, body?: unknown, options?: {
550
+ post: <T = any>(url: string, body?: unknown, options?: {
551
551
  headers?: Record<string, string>;
552
552
  signal?: AbortSignal;
553
553
  }) => Promise<T>;
554
- put: <T = unknown>(url: string, body?: unknown, options?: {
554
+ put: <T = any>(url: string, body?: unknown, options?: {
555
555
  headers?: Record<string, string>;
556
556
  signal?: AbortSignal;
557
557
  }) => Promise<T>;
558
- patch: <T = unknown>(url: string, body?: unknown, options?: {
558
+ patch: <T = any>(url: string, body?: unknown, options?: {
559
559
  headers?: Record<string, string>;
560
560
  signal?: AbortSignal;
561
561
  }) => Promise<T>;
562
- delete: <T = unknown>(url: string, options?: {
562
+ delete: <T = any>(url: string, options?: {
563
563
  headers?: Record<string, string>;
564
564
  signal?: AbortSignal;
565
565
  }) => Promise<T>;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * weifuwu/client ctx.ui 工厂 — createApp 注入的 UI 能力
2
+ * weifuwu/ui-dom ctx.ui 工厂 — createApp 注入的 UI 能力
3
3
  *
4
4
  * 从 app.ts 拆出(P2 结构拆分)。createUi(deps) 返回完整 ui 对象:
5
5
  * render / dirty / $ / useChat / useMedia / useBreakpoint / usePopupPosition / selfId
@@ -1,10 +1,10 @@
1
1
  /**
2
- * weifuwu/client VNode — 虚拟 DOM 节点
2
+ * weifuwu/ui-dom VNode — 虚拟 DOM 节点
3
3
  *
4
4
  * VNode 是纯 JS 对象,不依赖 DOM。组件返回 VNode。
5
5
  *
6
6
  * h/jsx 由 esbuild JSX 编译调用:
7
- * --jsxImportSource=weifuwu/client
7
+ * --jsxImportSource=weifuwu/ui-dom
8
8
  */
9
9
  import type { WfuiContext } from './types.ts';
10
10
  export type VNodeType = string | Component<any, any> | AsyncComponent | typeof Fragment | typeof Portal;
@@ -52,25 +52,9 @@ export type Component<P = {}, C extends object = {}> = (initProps: P, ctx: WfuiC
52
52
  *
53
53
  * 渲染器统一判别「返回值 instanceof Promise」:
54
54
  * 客户端:占位 → resolve 后整树重渲染(vnode 级 _asyncDef 按实例缓存)
55
- * SSR:直接 await(无占位)
56
- *
57
- * asyncComponent() 是兼容包装(工厂签名 (ctx),WeakMap 全局一次——代码分割场景)。
58
- */
59
- export type AsyncComponent<C extends object = {}, P = {}> = (initProps: P, ctx: WfuiContext & C) => Promise<Component<P, C> | null>;
60
- /**
61
- * 兼容包装:async 工厂(旧签名 (ctx),WeakMap 全局一次——代码分割/昂贵一次性资源)。
62
- * 统一为原生 async 组件签名 (initProps, ctx) => Promise<Component>,渲染器原生处理。
63
- *
64
- * ```tsx
65
- * const UserProfile = asyncComponent(async (ctx) => {
66
- * const { default: def } = await import('./view.tsx')
67
- * return def
68
- * })
69
- * ```
55
+ * SSR/hydration:直接 await(无占位)
70
56
  */
71
- export declare function asyncComponent<C extends object = {}, P = {}>(factory: (ctx: WfuiContext & C) => Promise<Component<P, C>>): AsyncComponent<C, P>;
72
- /** 判定一个组件类型是否为 async 工厂(asyncComponent 包装过) */
73
- export declare function isAsyncComponent(type: any): type is AsyncComponent;
57
+ export type AsyncComponent<C extends object = {}, P = {}> = (initProps: P, ctx: WfuiContext & C) => Promise<((props: P) => VNode | null) | null>;
74
58
  export declare const Fragment: unique symbol;
75
59
  /** Portal — 将子 VNode 渲染到 document.body 下的独立容器 */
76
60
  export declare const Portal: unique symbol;
@@ -2,7 +2,7 @@
2
2
  * 会话令牌(零依赖,node:crypto)
3
3
  *
4
4
  * access token:HMAC-SHA256 签名的 JWT 形态(base64url),
5
- * 与 weifuwu/client 的 auth() 兼容(客户端解码 payload 检查 exp)。
5
+ * 与 weifuwu/ui-dom 的 auth() 兼容(客户端解码 payload 检查 exp)。
6
6
  * refresh token:不透明随机串(256-bit),DB 只存 SHA-256 哈希(可撤销)。
7
7
  */
8
8
  /** 签发 JWT access token(HS256) */
@@ -105,7 +105,7 @@
105
105
  ### 框架级
106
106
  | antd | weifuwu | 状态 | 备注 |
107
107
  |------|---------|:----:|------|
108
- | App | `createApp` | 🔧 | client 框架内置 |
108
+ | App 引导 | `UIRouter + uiServe` | 🔧 | ui-dom 框架内置(weifuwu/client 已并入 ui-dom) |
109
109
  | ConfigProvider | `--wf-*` token + ctx 注入 | 🔧 | client 框架内置 |
110
110
 
111
111
  ---
@@ -193,4 +193,4 @@
193
193
  | **StatCard ⬆️** | 增强 | antd Statistic.Countdown | countdown 模式:剩余 HH:MM:SS + 结束回调(1s tick + 清理纪律) |
194
194
 
195
195
  > 三库 208 项(antd 84 / EP 74 / shadcn 50)→ 100% 覆盖(业务组件全对应;
196
- > 框架级 App/ConfigProvider/Teleport/Overlay 由 createApp/createPortal/--wf-* token 内置)。
196
+ > 框架级 App/ConfigProvider/Teleport/Overlay 由 UIRouter+uiServe/createPortal/--wf-* token 内置)。
@@ -1,8 +1,8 @@
1
1
  # 自定义组件开发指南
2
2
 
3
3
 
4
- > ⚠️ **weifuwu/client 已删除**——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
5
- > 用 weifuwu/client 写自己的组件——与内置组件同权:同渲染引擎、同弹层原语、同类型安全。
4
+ > ⚠️ **weifuwu/client 已并入 `weifuwu/ui-dom`**(`src/client/` 已删除)——前端运行时唯一入口为 `weifuwu/ui-dom`,见 [frontend-ui-dom.md](frontend-ui-dom.md)。
5
+ > 用 weifuwu/ui-dom 写自己的组件——与内置组件同权:同渲染引擎、同弹层原语、同类型安全。
6
6
  > 前置:[前端概念](frontend.md)(两阶段模型/ctx.ui)+ [组件列表](components.md)。
7
7
 
8
8
  ---
@@ -10,7 +10,7 @@
10
10
  ## 0. 最小骨架
11
11
 
12
12
  ```tsx
13
- import { h, type Component } from 'weifuwu/client'
13
+ import { h, type Component } from 'weifuwu/ui-dom'
14
14
 
15
15
  // Component<P, C>:P = props(JSX 自动推断),C = ctx 注入依赖(默认 {})
16
16
  const Badge: Component<{ text: string; color?: string }> = () =>
@@ -76,7 +76,7 @@ const MyPopover: Component<{ content: string }> = (_init, ctx) => {
76
76
  全屏对话框(焦点 trap + 滚动锁 + 退场动画)不在 usePopup 范围——用 **`ctx.ui.useDialog`** 组合器(Modal/Drawer 同款:退场状态机 + 滚动锁 + 焦点 trap + animationend 卸载):
77
77
 
78
78
  ```tsx
79
- import { createPortal } from 'weifuwu/client'
79
+ import { createPortal } from 'weifuwu/ui-dom'
80
80
 
81
81
  const MyDialog: Component<{ open: boolean; onClose: () => void }> = (_init, ctx) => {
82
82
  const dialog = ctx.ui.useDialog({ name: 'MyDialog' }) // mount 创建
@@ -98,7 +98,7 @@ const MyDialog: Component<{ open: boolean; onClose: () => void }> = (_init, ctx)
98
98
  ```
99
99
 
100
100
  > `dialog.rootRef` 挂到 portal 根(lockScroll + animationend 退场监听);`panelRef` 挂到面板(trapFocus)。
101
- > 低层原语 `trapFocus`/`lockScroll`/`animateOut` 仍从 `weifuwu/client` 导出(特殊场景组装用)。
101
+ > 低层原语 `trapFocus`/`lockScroll`/`animateOut` 仍从 `weifuwu/ui-dom` 导出(特殊场景组装用)。
102
102
 
103
103
  ## 4. AI 组件
104
104
 
@@ -138,7 +138,6 @@ const UserCard = async (initProps, ctx) => {
138
138
  - 渲染器按「返回值是 Promise」判别:客户端未 resolve → 占位(`Placeholder`),resolve 后整树补全;SSR 直接 await(无占位)
139
139
  - 工厂按实例执行;**数据必须走 ctx.data**(缓存+并发合并,重复执行零成本);禁止副作用裸写工厂
140
140
  - 占位显示:无边界 → null;`<Suspense fallback={...}>` → 占位处显示 fallback(可选)
141
- - 代码分割/昂贵一次性资源:`asyncComponent(async (ctx) => { const { default: def } = await import('./view'); return def })`(WeakMap 全局一次,兼容保留)
142
141
  - **个性化数据不进 ctx.data**(SSR 会序列化给所有客户端)——留在客户端 `$` + fetch
143
142
 
144
143
  ## 6. 类型纪律(编译期防线)
package/docs/data.md CHANGED
@@ -27,9 +27,11 @@ app.post('/decks', async (req, ctx) => {
27
27
  // 读回来自动是对象:rows[0].deck_json === { slides: [...] }(不是字符串)
28
28
  })
29
29
 
30
- // ③ 事务(postgres.js 兼容 begin)
30
+ // ③ 事务(中间件实例 pg.transaction——不在 ctx.sql 接口)
31
+ const pg = postgres()
32
+ app.use(pg)
31
33
  app.post('/transfer', async (req, ctx) => {
32
- await ctx.sql.begin(async sql => {
34
+ await pg.transaction(async sql => {
33
35
  await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
34
36
  await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
35
37
  })
@@ -105,20 +107,12 @@ const q = queue({ redis })
105
107
  | timestamp / date / interval | `string`(无时区语义——转 Date 按本地时区解析即时区魔法,诚实裁剪不转) |
106
108
  | NULL | `null` |
107
109
 
108
- ### 类型层(查询泛型 + schema 写前校验)
110
+ ### 类型层(查询泛型)
109
111
 
110
112
  ```ts
111
- // ① 查询结果泛型(编译期类型,无需手写 interface + 断言)
113
+ // 查询结果泛型(编译期类型,无需手写 interface + 断言)
112
114
  interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
113
- const decks = await ctx.sql.query<Deck>('SELECT id, title, deck_json FROM decks')
114
-
115
- // ② schema 注册 → insert 写前校验(脏数据源头拦截)
116
- ctx.sql.register('decks', {
117
- title: { type: 'text', required: true },
118
- status: { type: 'enum', values: ['outline', 'ready'] },
119
- deck_json: { type: 'jsonb' },
120
- })
121
- await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → ValidationError
115
+ const decks = await ctx.sql`SELECT id, title, deck_json FROM decks` as Deck[]
122
116
  ```
123
117
 
124
118
  ### 方法面
@@ -126,17 +120,13 @@ await ctx.sql.insert('decks', { title: 'x', status: 'INVALID' }) // → Validati
126
120
  | 方法 | 说明 |
127
121
  |------|------|
128
122
  | `ctx.sql\`...\`` | tagged template → 参数化查询(插值=参数,表名需硬编码) |
129
- | `ctx.sql.query<T>(sql, params?)` | 参数化查询 + 泛型 |
130
- | `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名) |
131
- | `ctx.sql.begin(fn)` | 事务(回调收到 tagged template sql) |
132
- | `ctx.sql.transaction(fn)` | 事务(回调收到 `{ query }`) |
133
- | `ctx.sql.register(table, schema)` | 注册表结构(写前校验) |
134
- | `ctx.sql.insert(table, row)` | schema 校验 + 参数化插入 |
135
- | `ctx.sql.insertMany(table, rows[], { batchSize? })` | **批量插入**:多行 VALUES 单次往返(默认 500/批;所有行键必须一致) |
136
- | `ctx.sql.update(table, set, where, { returning? })` | **参数化 UPDATE**:SET/WHERE 全部参数化,返回 `affectedRows` |
137
- | `ctx.sql.delete(table, where)` | **参数化 DELETE**:WHERE 必填(防全表误删),返回 `affectedRows` |
138
- | `ctx.sql\`...\` 内嵌片段` | 条件 SQL 片段(嵌套过滤,参数自动重编号) |
139
- | `ctx.sql.close()` | 关闭连接池 |
123
+ | `ctx.sql.unsafe(sql, params?)` | 原生 SQL(DDL / 动态表名;`$1` 占位符) |
124
+ | `ctx.sql.query` | **Query Language**:`sql.query.from('users').where({...}).run()`(AST 双后端) |
125
+ | `ctx.sql.raw\`...\`` | 逃生舱片段(`NOW() - interval '7 days'`——真库透传/内存裁剪) |
126
+ | `pg.transaction(fn)` | 事务(中间件实例;回调收到 callable sql,postgres.js 兼容 begin 语义) |
127
+ | `pg.migrate()` / `markMigrated` / `isMigrated` | 幂等迁移(`_weifuwu_migrations` 表) |
128
+ | `pg.poolStats()` | 连接池摘要(active/idle/waiting/max) |
129
+ | `ctx.sql.close()` / `pg.close()` | 关闭连接池 |
140
130
 
141
131
  ### 影响行数(affectedRows)
142
132
 
@@ -147,14 +137,6 @@ const r = await ctx.sql`UPDATE messages SET read = true WHERE id = ${id}`
147
137
  if (r.affectedRows === 0) return new Response('not found', { status: 404 })
148
138
  ```
149
139
 
150
- ```ts
151
- // 批量插入:100 行 1 次往返
152
- await ctx.sql.insertMany('agent_logs', logs, { batchSize: 500 })
153
- // 语义化更新/删除:WHERE 全参数化 + 返回影响行数
154
- await ctx.sql.update('users', { role: 'admin' }, { id: userId })
155
- await ctx.sql.delete('messages', { id: msgId })
156
- ```
157
-
158
140
  ### 条件片段(嵌套过滤)
159
141
 
160
142
  ```ts
@@ -281,10 +263,11 @@ await ctx.redis.set('user', 1) // 实际写入 'api:user'
281
263
 
282
264
  | 选项 | 类型 | 默认值 | 说明 |
283
265
  |------|------|--------|------|
284
- | `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
266
+ | `url` | `string` | `REDIS_URL` 环境变量(两者都缺 → 构造抛错,禁止静默回退 localhost) | 连接字符串 |
285
267
  | `poolSize` | `number` | `5` | 连接池大小 |
286
268
  | `keyPrefix` | `string` | `''` | 所有 key 自动加前缀(多应用隔离) |
287
269
  | `commandTimeoutMs` | `number` | `0` | 命令超时(阻塞命令 resolve(null);防挂起。0=禁用) |
270
+ | `onCommand` | `(command, args, durationMs, traceId?) => void` | — | 命令观测钩子;第 4 参数为请求级 traceId(`x-trace-id` 头经 ALS 传播) |
288
271
  | `socketTimeoutMs` | `number` | `0` | socket 响应超时(僵尸连接自愈:pending 有命令且超时无数据 → 主动断开重连。0=禁用) |
289
272
 
290
273
  > **连接健康**:断线自动剔除死连接并重建(池不萎缩);`CLIENT KILL`/网络抖动后服务自愈,命令不命中死连接。
@@ -7,7 +7,7 @@
7
7
  | 变量 | 用途 | 模块 | 默认 |
8
8
  |------|------|------|------|
9
9
  | `DATABASE_URL` | PostgreSQL 连接字符串 | `postgres()` | —(必填) |
10
- | `REDIS_URL` | Redis 连接字符串 | `redis()` | `redis://localhost:6379` |
10
+ | `REDIS_URL` | Redis 连接字符串 | `redis()` | —(必填;缺 env 且未传 `url` 构造抛错,不静默回退 localhost) |
11
11
  | `AUTH_SECRET` | userSystem HMAC 签名密钥(≥16 字符) | `userSystem()` | 可传 `options.secret` |
12
12
  | `DEEPSEEK_API_KEY` | LLM 对话 provider API key | `ai()` | — |
13
13
  | `DEEPSEEK_BASE_URL` | LLM 对话 provider 端点 | `ai()` | `https://api.deepseek.com/v1` |
@@ -1,93 +1,79 @@
1
1
  # 前端中间件与工具(weifuwu/ui-dom)
2
2
 
3
- > ⚠️ **`weifuwu/client` 已删除**——中间件(api/auth/ws/i18n)现位于 `weifuwu/ui-dom`,新 API 见 [frontend-ui-dom.md](frontend-ui-dom.md)。
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)。
4
4
 
5
5
  > 本页为 weifuwu 官方文档拆分页 · [返回 README](../README.md)
6
6
 
7
- ## router + RouteView — 前端路由
7
+ ## UIRouter — 前端路由
8
8
 
9
9
  ```tsx
10
- import { createApp, router, RouteView } from 'weifuwu/client'
11
- import type { RouteDef, WfuiContext } from 'weifuwu/client'
12
-
13
- const routes: RouteDef[] = [
14
- { path: '/', component: Home },
15
- { path: '/users', component: UserList },
16
- { path: '/users/:id', component: UserDetail },
17
- ]
18
-
19
- createApp()
20
- .use(router({
21
- routes,
22
- mode: 'history', // 或 'hash'
23
- notFound: NotFoundPage,
24
- }))
25
- .mount('#root', () => () => <RouteView />) // 根组件也要两阶段:外层返回 render 函数
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)
26
21
  ```
27
22
 
28
- ### 嵌套布局
23
+ ### 嵌套布局(中间件两阶段)
29
24
 
30
25
  ```tsx
31
- const routes = [
32
- {
33
- path: '/dashboard',
34
- layout: DashboardLayout, // 持久布局(包含 RouteView)
35
- children: [
36
- { path: '/overview', component: Overview },
37
- { path: '/settings', component: Settings },
38
- ],
39
- },
40
- ]
41
-
42
- function DashboardLayout(_props: {}, ctx: WfuiContext) {
43
- return (props) => (
44
- <div style="display:flex">
45
- <aside>导航菜单</aside>
46
- <main><RouteView /></main> {/* 渲染子路由 */}
47
- </div>
48
- )
26
+ const DashboardLayout: UIMiddleware = async (_loc, ctx, children) => {
27
+ const $ = ctx.ui.$()
28
+ $.open = true
29
+ return async (loc, c) => {
30
+ const child = await children(loc, c) // 子路由/嵌套路由内容
31
+ return h('div', { class: 'wf-row' },
32
+ h('aside', {}, '导航菜单'),
33
+ h('main', {}, child),
34
+ )
35
+ }
49
36
  }
37
+ app.use(DashboardLayout)
50
38
  ```
51
39
 
52
40
  ### 编程式导航
53
41
 
54
42
  ```tsx
55
- // 在任意组件中
43
+ // 在任意组件/页面中(uiServe 注入)
56
44
  ctx.app?.navigate('/users/123?tab=profile')
57
45
  ```
58
46
 
59
47
  | ctx 注入 | 类型 | 说明 |
60
48
  |----------|------|------|
49
+ | `ctx.params` / `ctx.query` | `Record<string, string>` | 路由参数 / URL query(顶层注入) |
61
50
  | `ctx.route.path` | `string` | 当前路由路径 |
62
51
  | `ctx.route.params` | `Record<string, string>` | URL 参数 |
63
52
  | `ctx.route.query` | `Record<string, string>` | 查询参数 |
64
53
  | `ctx.app.navigate(path)` | `(string) => void` | 编程式导航 |
65
54
 
66
- | RouterOptions | 类型 | 默认值 | 说明 |
67
- |---------------|------|--------|------|
68
- | `routes` | `RouteDef[]` | — | 路由定义 |
69
- | `mode` | `'history' \| 'hash'` | `'history'` | 路由模式 |
70
- | `notFound` | `Component` | — | 404 页面 |
55
+ | UIRouter API | 签名 | 说明 |
56
+ |--------------|------|------|
57
+ | `get(path, handler)` | `(string, UIHandler)` | 页面路由(handler = async 组件:`async (location, ctx) => vnode`) |
58
+ | `use(prefix, sub)` / `use(mw)` | `(string, UIRouter) \| (mw)` | 子路由树挂载 / 中间件(ctx 注入) |
59
+ | `notFound(handler)` | `(UIHandler)` | 404 页面 |
60
+ | `mode` | `'history' \| 'hash'` | 路由模式(默认 history) |
71
61
 
72
- | RouteDef | 类型 | 说明 |
73
- |----------|------|------|
74
- | `path` | `string` | 路径(支持 `:param`) |
75
- | `component` | `Component` | 页面组件 |
76
- | `layout` | `Component` | 布局组件(内含 `<RouteView />`) |
77
- | `children` | `RouteDef[]` | 子路由 |
78
- | `auth` | `boolean` | 是否需要认证(配合 auth 中间件) |
79
- | `title` | `string` | 页面标题(自动设置 `document.title`) |
62
+ | UIHandler | 类型 | 说明 |
63
+ |-----------|------|------|
64
+ | 签名 | `(location, ctx) => Promise<VNode> \| VNode` | 页面 = 异步组件(`ctx.data.get` 三场景;async 组件无需包装) |
65
+ | 返回值 | `VNode \| null` | 数据结构(落地由 uiServe/ssrPage 决定) |
80
66
 
81
67
  ---
82
68
 
83
69
  ## api — HTTP 客户端中间件
84
70
 
85
71
  ```tsx
86
- import { createApp, api } from 'weifuwu/client'
72
+ import { UIRouter, api, uiServe } from 'weifuwu/ui-dom'
87
73
 
88
- createApp()
89
- .use(api({ baseURL: '/api' }))
90
- .mount('#root', App)
74
+ const app = new UIRouter()
75
+ app.use(api({ baseURL: '/api' }))
76
+ uiServe(app, { root: '#root' })
91
77
 
92
78
  // 在组件中使用
93
79
  async function loadUsers(ctx: WfuiContext) {
@@ -139,11 +125,11 @@ try {
139
125
  ## auth — 认证中间件
140
126
 
141
127
  ```tsx
142
- import { createApp, auth } from 'weifuwu/client'
128
+ import { UIRouter, auth, uiServe } from 'weifuwu/ui-dom'
143
129
 
144
- createApp()
145
- .use(auth())
146
- .mount('#root', App)
130
+ const app = new UIRouter()
131
+ app.use(auth())
132
+ uiServe(app, { root: '#root' })
147
133
 
148
134
  // 在组件中
149
135
  function Profile(_props: {}, ctx: WfuiContext) {
@@ -191,11 +177,11 @@ await ctx.auth?.refresh() // → boolean
191
177
  ## ws — WebSocket 客户端中间件
192
178
 
193
179
  ```tsx
194
- import { createApp, ws } from 'weifuwu/client'
180
+ import { UIRouter, ws, uiServe } from 'weifuwu/ui-dom'
195
181
 
196
- createApp()
197
- .use(ws({ url: '/ws' }))
198
- .mount('#root', App)
182
+ const app = new UIRouter()
183
+ app.use(ws({ url: '/ws' }))
184
+ uiServe(app, { root: '#root' })
199
185
 
200
186
  // 发送消息
201
187
  ctx.ws?.send({ type: 'chat', body: 'hello' })
@@ -231,10 +217,10 @@ unsubscribe?.()
231
217
  ## i18n — 国际化中间件
232
218
 
233
219
  ```tsx
234
- import { createApp, i18n } from 'weifuwu/client'
220
+ import { UIRouter, i18n, uiServe } from 'weifuwu/ui-dom'
235
221
 
236
- createApp()
237
- .use(i18n({
222
+ const app = new UIRouter()
223
+ app.use(i18n({
238
224
  locale: 'zh-CN',
239
225
  messages: {
240
226
  'title': '仪表盘',
@@ -268,7 +254,7 @@ ctx.i18n?.setLocale('en-US')
268
254
  内置语言包:
269
255
 
270
256
  ```ts
271
- import { zhCN, enUS } from 'weifuwu/client'
257
+ import { zhCN, enUS } from 'weifuwu/ui-dom'
272
258
  ```
273
259
 
274
260
  - `zh-CN`:默认中文
@@ -283,7 +269,7 @@ import { zhCN, enUS } from 'weifuwu/client'
283
269
  ## ErrorBoundary — 错误边界
284
270
 
285
271
  ```tsx
286
- import { ErrorBoundary } from 'weifuwu/client'
272
+ import { ErrorBoundary } from 'weifuwu/ui-dom'
287
273
 
288
274
  <ErrorBoundary fallback={<p>出错了,请刷新页面</p>}>
289
275
  <UserProfile />
@@ -316,12 +302,12 @@ import { ErrorBoundary } from 'weifuwu/client'
316
302
  **① 命令式 `ctx.confirm()`(推荐,操作前询问)**
317
303
 
318
304
  ```tsx
319
- import { createApp } from 'weifuwu/client'
305
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
320
306
  import { confirm } from 'weifuwu/components'
321
307
 
322
- createApp()
323
- .use(confirm())
324
- .mount('#root', App)
308
+ const app = new UIRouter()
309
+ app.use(confirm())
310
+ uiServe(app, { root: '#root' })
325
311
 
326
312
  // 任意代码中(组件事件、async 逻辑)
327
313
  async function handleDelete(ctx: WfuiContext) {
@@ -372,12 +358,12 @@ import { Confirm } from 'weifuwu/components'
372
358
  `ctx.toast()` 是 `<Toast>` 组件的全局命令式封装:任意代码中一行调用,自动消失、自动清理,无需宿主状态。
373
359
 
374
360
  ```tsx
375
- import { createApp } from 'weifuwu/client'
361
+ import { UIRouter, uiServe } from 'weifuwu/ui-dom'
376
362
  import { toast } from 'weifuwu/components'
377
363
 
378
- createApp()
379
- .use(toast({ position: 'top-right', duration: 3000, max: 3 }))
380
- .mount('#root', App)
364
+ const app = new UIRouter()
365
+ app.use(toast({ position: 'top-right', duration: 3000, max: 3 }))
366
+ uiServe(app, { root: '#root' })
381
367
 
382
368
  // 任意代码中(组件事件、api 拦截器、WS 回调、定时器)
383
369
  ctx.toast?.('保存成功', 'success')
@@ -400,8 +386,8 @@ ctx.toast?.('普通消息') // 默认 type = 'info'
400
386
  ## ScrollLock / FocusTrap
401
387
 
402
388
  ```tsx
403
- import { lockScroll, unlockScroll } from 'weifuwu/client'
404
- import { trapFocus } from 'weifuwu/client'
389
+ import { lockScroll, unlockScroll } from 'weifuwu/ui-dom'
390
+ import { trapFocus } from 'weifuwu/ui-dom'
405
391
 
406
392
  // 锁定/解锁滚动(支持嵌套计数)
407
393
  lockScroll()
@@ -423,7 +409,7 @@ cleanup() // 恢复之前的焦点
423
409
  ## extendCtx — 上下文扩展
424
410
 
425
411
  ```tsx
426
- import { extendCtx } from 'weifuwu/client'
412
+ import { extendCtx } from 'weifuwu/ui-dom'
427
413
 
428
414
  // 在 AppMiddleware 中创建新 ctx,原 ctx getter 通过原型链继承
429
415
  function myMw(ctx: WfuiContext): WfuiContext {
@@ -3,7 +3,7 @@
3
3
  > **weifuwu 前端唯一运行时**(`weifuwu/ui-dom`,随 npm 包发布):
4
4
  > **UIRouter(纯路由 + ctx 注入链)+ uiServe(渲染运行时)+ ssrPage/hydration**,
5
5
  > 复用 `weifuwu/components`(VNode 契约唯一来源,组件零修改)。
6
- > 已取代 `weifuwu/client`(createApp/router 已删除)。
6
+ > 已取代 `weifuwu/client`——前端运行时唯一入口(createApp/router 旧 API 已删除)。
7
7
  >
8
8
  > 概念对齐:**req = window.location**,**res = VNode**(数据结构),
9
9
  > **uiServe = VDOM**(落地机制),**params/query 在 ctx**(对齐后端 `ctx.params`)。
@@ -161,5 +161,7 @@ uiServe(app, { root: '#root', hydrate: true })
161
161
  ## demo
162
162
 
163
163
  `apps/ui-router-demo`:UIRouter + uiServe + components(Button/Input/Tag/Dropdown)
164
- + toast 注入 + 嵌套路由 + SSR/hydrate 端到端。
164
+ + toast 注入 + 嵌套路由 + SSR/hydrate 端到端(原生 async 组件页 `/async` 验证
165
+ 占位补全 + `__DATA__` 三场景)。server 用 weifuwu serve + `ui()` 中间件
166
+ (`ctx.ui.js` 编译前端 / `ctx.ui.css` 组件样式 / `/*` → ssrPage)。
165
167
  启动:`node apps/ui-router-demo/server.ts` → http://localhost:3100