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.
- package/README.md +14 -14
- package/dist/ai/types.d.ts +1 -1
- package/dist/components/Button/Button.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js +2 -123
- package/dist/ui/index.d.ts +1 -1
- package/dist/ui-dom/ai.d.ts +1 -1
- package/dist/ui-dom/browser.d.ts +1 -1
- package/dist/ui-dom/diff.d.ts +1 -1
- package/dist/ui-dom/focus-trap.d.ts +1 -1
- package/dist/ui-dom/hydration.d.ts +1 -1
- package/dist/ui-dom/index.d.ts +1 -1
- package/dist/ui-dom/index.js +10 -8
- package/dist/ui-dom/middleware/api.d.ts +3 -3
- package/dist/ui-dom/middleware/auth.d.ts +1 -1
- package/dist/ui-dom/motion.d.ts +1 -1
- package/dist/ui-dom/popup.d.ts +1 -1
- package/dist/ui-dom/registry.d.ts +1 -13
- package/dist/ui-dom/render.d.ts +1 -1
- package/dist/ui-dom/scroll-lock.d.ts +1 -1
- package/dist/ui-dom/ssr.d.ts +1 -0
- package/dist/ui-dom/types.d.ts +6 -6
- package/dist/ui-dom/ui.d.ts +1 -1
- package/dist/ui-dom/vnode.d.ts +4 -20
- package/dist/user/token.d.ts +1 -1
- package/docs/compat-three-library.md +1 -1
- package/docs/components-map.md +1 -1
- package/docs/custom-components.md +5 -6
- package/docs/data.md +16 -33
- package/docs/environment.md +1 -1
- package/docs/frontend-middleware.md +65 -79
- package/docs/frontend-ui-dom.md +4 -2
- package/docs/frontend.md +35 -38
- package/docs/realtime.md +23 -28
- package/docs/saas.md +8 -6
- package/package.json +1 -1
- package/dist/ui/ssr-page.d.ts +0 -41
- package/dist/ui-dom/route-match.d.ts +0 -22
|
@@ -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
|
|
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 子容器清理(指定实例) */
|
package/dist/ui-dom/render.d.ts
CHANGED
package/dist/ui-dom/ssr.d.ts
CHANGED
package/dist/ui-dom/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* weifuwu/
|
|
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 =
|
|
546
|
+
get: <T = any>(url: string, options?: {
|
|
547
547
|
headers?: Record<string, string>;
|
|
548
548
|
signal?: AbortSignal;
|
|
549
549
|
}) => Promise<T>;
|
|
550
|
-
post: <T =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
562
|
+
delete: <T = any>(url: string, options?: {
|
|
563
563
|
headers?: Record<string, string>;
|
|
564
564
|
signal?: AbortSignal;
|
|
565
565
|
}) => Promise<T>;
|
package/dist/ui-dom/ui.d.ts
CHANGED
package/dist/ui-dom/vnode.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* weifuwu/
|
|
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/
|
|
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
|
|
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;
|
package/dist/user/token.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 会话令牌(零依赖,node:crypto)
|
|
3
3
|
*
|
|
4
4
|
* access token:HMAC-SHA256 签名的 JWT 形态(base64url),
|
|
5
|
-
* 与 weifuwu/
|
|
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 | `
|
|
108
|
+
| App 引导 | `UIRouter + uiServe` | 🔧 | ui-dom 框架内置(weifuwu/client 已并入 ui-dom) |
|
|
109
109
|
| ConfigProvider | `--wf-*` token + ctx 注入 | 🔧 | client 框架内置 |
|
|
110
110
|
|
|
111
111
|
---
|
package/docs/components-map.md
CHANGED
|
@@ -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 由
|
|
196
|
+
> 框架级 App/ConfigProvider/Teleport/Overlay 由 UIRouter+uiServe/createPortal/--wf-* token 内置)。
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# 自定义组件开发指南
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
> ⚠️ **weifuwu/client
|
|
5
|
-
> 用 weifuwu/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
-
// ③
|
|
30
|
+
// ③ 事务(中间件实例 pg.transaction——不在 ctx.sql 接口)
|
|
31
|
+
const pg = postgres()
|
|
32
|
+
app.use(pg)
|
|
31
33
|
app.post('/transfer', async (req, ctx) => {
|
|
32
|
-
await
|
|
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
|
-
###
|
|
110
|
+
### 类型层(查询泛型)
|
|
109
111
|
|
|
110
112
|
```ts
|
|
111
|
-
//
|
|
113
|
+
// 查询结果泛型(编译期类型,无需手写 interface + 断言)
|
|
112
114
|
interface Deck { id: number; title: string; deck_json: { slides: unknown[] } }
|
|
113
|
-
const decks = await ctx.sql
|
|
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.
|
|
130
|
-
| `ctx.sql.
|
|
131
|
-
| `ctx.sql.
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
135
|
-
| `ctx.sql.
|
|
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`/网络抖动后服务自愈,命令不命中死连接。
|
package/docs/environment.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| 变量 | 用途 | 模块 | 默认 |
|
|
8
8
|
|------|------|------|------|
|
|
9
9
|
| `DATABASE_URL` | PostgreSQL 连接字符串 | `postgres()` | —(必填) |
|
|
10
|
-
| `REDIS_URL` | Redis 连接字符串 | `redis()` | `
|
|
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`
|
|
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
|
-
##
|
|
7
|
+
## UIRouter — 前端路由
|
|
8
8
|
|
|
9
9
|
```tsx
|
|
10
|
-
import {
|
|
11
|
-
import type {
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
children
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
|
67
|
-
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
70
|
-
| `notFound` | `
|
|
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
|
-
|
|
|
73
|
-
|
|
74
|
-
|
|
|
75
|
-
|
|
|
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 {
|
|
72
|
+
import { UIRouter, api, uiServe } from 'weifuwu/ui-dom'
|
|
87
73
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 {
|
|
128
|
+
import { UIRouter, auth, uiServe } from 'weifuwu/ui-dom'
|
|
143
129
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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 {
|
|
180
|
+
import { UIRouter, ws, uiServe } from 'weifuwu/ui-dom'
|
|
195
181
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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 {
|
|
220
|
+
import { UIRouter, i18n, uiServe } from 'weifuwu/ui-dom'
|
|
235
221
|
|
|
236
|
-
|
|
237
|
-
|
|
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/
|
|
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/
|
|
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 {
|
|
305
|
+
import { UIRouter, uiServe } from 'weifuwu/ui-dom'
|
|
320
306
|
import { confirm } from 'weifuwu/components'
|
|
321
307
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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 {
|
|
361
|
+
import { UIRouter, uiServe } from 'weifuwu/ui-dom'
|
|
376
362
|
import { toast } from 'weifuwu/components'
|
|
377
363
|
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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/
|
|
404
|
-
import { trapFocus } from 'weifuwu/
|
|
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/
|
|
412
|
+
import { extendCtx } from 'weifuwu/ui-dom'
|
|
427
413
|
|
|
428
414
|
// 在 AppMiddleware 中创建新 ctx,原 ctx getter 通过原型链继承
|
|
429
415
|
function myMw(ctx: WfuiContext): WfuiContext {
|
package/docs/frontend-ui-dom.md
CHANGED
|
@@ -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
|
|
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
|