weifuwu 0.37.0 → 0.38.1

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 CHANGED
@@ -1,288 +1,665 @@
1
1
  # weifuwu
2
2
 
3
- **全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => VNode` + 纯 CSS 布局系统**
3
+ **全栈框架 — 后端 HTTP 路由 + 前端 VDOM 框架 + 纯 CSS 布局系统**
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
- 一个包,零上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架;布局提供纯 CSS 原语 + 主题 Token
9
+ 一个包 = 后端 (`weifuwu`) + 前端 (`weifuwu/client`) + 组件库 (`weifuwu/components`) + 布局系统 (`weifuwu/layout`)
10
10
 
11
11
  ---
12
12
 
13
13
  ## 模块总览
14
14
 
15
- | 模块 | 导入路径 | 用途 | 依赖 |
16
- |------|---------|------|------|
17
- | **Router** | `weifuwu` | HTTP 路由 + 中间件链 + WebSocket + GraphQL | — |
18
- | **serve** | `weifuwu` | HTTP 服务器 | `Router` |
19
- | **cors** | `weifuwu` | CORS 跨域中间件 | `Router` |
20
- | **serveStatic** | `weifuwu` | 静态文件服务 | `Router` |
21
- | **postgres** | `weifuwu` | PostgreSQL 客户端 → `ctx.sql` | `Router` |
22
- | **redis** | `weifuwu` | Redis 客户端 → `ctx.redis` | `Router` |
23
- | **ui** | `weifuwu` | SSR 渲染 + 动态 JS/CSS 编译 → `ctx.ui` | `Router` |
24
- | **graphql** | `weifuwu` | GraphQL 端点 | `Router` |
25
- | **client** | `weifuwu/client` | 前端 VDOM + Proxy 框架 + i18n + ErrorBoundary | — |
26
- | **components** | `weifuwu/components` | 34 HTML 原语组件(Button/Table/Modal/...) | `client` |
27
- | **layout** | `weifuwu/layout` | CSS 布局原语 + 主题 Token | — |
15
+ | 导入路径 | 模块 | 用途 | 依赖 |
16
+ |---------|------|------|------|
17
+ | `weifuwu` | **Router** | Trie 路由 + 中间件链 + WebSocket + GraphQL | — |
18
+ | `weifuwu` | **serve** | HTTP 服务器 | Router |
19
+ | `weifuwu` | **cors** | CORS 跨域中间件 | Router |
20
+ | `weifuwu` | **serveStatic** | 静态文件服务(ETag/304/目录索引) | Router |
21
+ | `weifuwu` | **postgres** | PostgreSQL 连接池 → `ctx.sql` | Router, DATABASE_URL |
22
+ | `weifuwu` | **redis** | Redis 客户端 → `ctx.redis` | Router, REDIS_URL |
23
+ | `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
24
+ | `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
25
+ | `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
26
+ | `weifuwu` | **response** | HTTP 响应辅助函数(ok/badRequest/...) | |
27
+ | `weifuwu` | **parseBody** | JSON 请求体安全解析 | — |
28
+ | `weifuwu/client` | **createApp** | 应用引导 + VDOM 渲染引擎 | — |
29
+ | `weifuwu/client` | **router / RouteView** | 前端路由(history/hash 模式) | createApp |
30
+ | `weifuwu/client` | **api / auth / ws** | HTTP 客户端 / 认证 / WebSocket 中间件 | createApp |
31
+ | `weifuwu/client` | **i18n** | 国际化中间件(运行时切换语言) | createApp |
32
+ | `weifuwu/client` | **ErrorBoundary** | 错误边界组件 | createApp |
33
+ | `weifuwu/client` | **confirm** | Promise 化确认对话框 | createApp |
34
+ | `weifuwu/client** | **lockScroll/trapFocus** | 滚动锁定 / 焦点陷阱工具 | — |
35
+ | `weifuwu/components` | **37 个组件** | Button/Table/Modal/Toast/... | weifuwu/client |
36
+ | `weifuwu/layout` | **CSS 布局** | 35 个布局原语 + 72 个主题 Token | — |
28
37
 
29
38
  ---
30
39
 
31
- ## 核心理念
32
-
33
- 前后端共享同一模式:**中间件向 `ctx` 注入字段,handler/组件从 `ctx` 读取。**
34
-
35
- ```
36
- 后端: 前端:
37
- Request → Middleware → Handler createApp() → Middleware → Component
38
- │ │
39
- ▼ ▼
40
- ctx.sql ctx.ws
41
- ctx.redis ctx.route
42
- ctx.ui ctx.api / ctx.auth
43
- ctx.i18n
44
- ```
45
-
46
- ---
47
-
48
- ## 快速开始 —— 全栈应用
40
+ ## 快速开始
49
41
 
50
42
  ```ts
51
- import { serve, Router, ui } from 'weifuwu'
43
+ // server.ts
44
+ import { serve, Router, ui, cors, serveStatic } from 'weifuwu'
52
45
 
53
46
  const app = new Router()
47
+ app.use(cors())
54
48
  app.use(ui())
55
49
 
56
- // 前端 TSX → JS bundle(动态编译,零构建步骤)
57
- app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
58
-
59
- // CSS(PostCSS + Tailwind 编译)
60
- app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
61
-
62
50
  // SPA 入口
63
- app.get('/', async (req, ctx) => ctx.ui.html`
64
- <!DOCTYPE html>
65
- <html>
66
- <head><link rel="stylesheet" href="/style.css"></head>
67
- <body><div id="root"></div><script src="/app.js"></script></body>
68
- </html>
51
+ app.get('/', (req, ctx) => ctx.ui.html`
52
+ <!doctype html><html><body>
53
+ <div id="root"></div>
54
+ <script src="/app.js"></script>
55
+ </body></html>
69
56
  `)
70
57
 
58
+ // 动态编译前端 TSX(零构建步骤)
59
+ app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx'))
60
+ app.get('/style.css', (req, ctx) => ctx.ui.css('./src/style.css'))
61
+
62
+ // API
63
+ app.get('/api/hello', () => Response.json({ msg: 'world' }))
64
+
71
65
  serve(app, { port: 3000 })
72
66
  ```
73
67
 
74
68
  ```tsx
75
- // src/main.tsx —— 前端
76
- import { createApp, router, RouteView, i18n } from 'weifuwu/client'
77
- import type { WfuiContext, RouteDef } from 'weifuwu/client'
69
+ // src/main.tsx
70
+ import { createApp, router, RouteView } from 'weifuwu/client'
78
71
 
79
- function Home(_props: {}, ctx: WfuiContext) {
80
- return <h1>Hello weifuwu</h1>
81
- }
72
+ function Home() { return <h1>Hello weifuwu</h1> }
82
73
 
83
74
  createApp()
84
- .use(router({ routes: [{ path: '/', component: Home }], mode: 'history' }))
85
- .mount('#root', () => <Home />)
75
+ .use(router({ routes: [{ path: '/', component: Home }] }))
76
+ .mount('#root', () => <RouteView />)
86
77
  ```
87
78
 
88
79
  ---
89
80
 
90
- ## 后端
81
+ # 后端 API (`weifuwu`)
82
+
83
+ ## Router
91
84
 
92
- ### Router
85
+ Trie 路由,支持 URL 参数、通配符、中间件链、WebSocket、GraphQL。
93
86
 
94
87
  ```ts
95
88
  import { Router } from 'weifuwu'
96
89
 
97
90
  const app = new Router()
91
+ ```
98
92
 
99
- // 中间件
100
- app.use(cors())
93
+ ### 路由方法
101
94
 
102
- // 路由
103
- app.get('/api/users', async (req: Request, ctx: Context) => {
104
- return Response.json(users)
95
+ 每个路由方法接受 `path, ...middlewares[], handler`:
96
+
97
+ ```ts
98
+ app.get('/users', handler)
99
+ app.post('/users', handler)
100
+ app.put('/users/:id', handler)
101
+ app.patch('/users/:id', handler) // PATCH
102
+ app.delete('/users/:id', handler) // DELETE
103
+ app.head('/users', handler) // HEAD
104
+ app.options('/users', handler) // OPTIONS
105
+ app.all('/users', handler) // 所有 HTTP 方法
106
+ ```
107
+
108
+ | 参数 | 类型 | 说明 |
109
+ |------|------|------|
110
+ | `path` | `string` | 路由路径,支持 `:param` 和 `*` 通配符 |
111
+ | `...middlewares` | `Middleware[]` | 路由级中间件(可选) |
112
+ | `handler` | `Handler` 或 `Router` | 处理器或子路由器 |
113
+
114
+ ### 路由级中间件
115
+
116
+ ```ts
117
+ declare module 'weifuwu' { interface Context { auth: { userId: string } } }
118
+
119
+ const requireAuth: Middleware = (req, ctx, next) => {
120
+ if (!req.headers.get('authorization')) return new Response('Unauthorized', { status: 401 })
121
+ ;(ctx as any).auth = { userId: '123' }
122
+ return next(req, ctx)
123
+ }
124
+
125
+ app.get('/users/:id', requireAuth, (req, ctx) => {
126
+ return Response.json({ id: ctx.params.id, auth: ctx.auth })
105
127
  })
128
+ ```
129
+
130
+ ### 参数与查询
106
131
 
107
- app.post('/api/users', async (req: Request, ctx: Context) => {
108
- const body = await req.json()
109
- return Response.json({ id: 1, ...body }, { status: 201 })
132
+ ```ts
133
+ app.get('/posts/:category/:slug', (req, ctx) => {
134
+ ctx.params.category // URL 参数
135
+ ctx.params.slug // URL 参数
136
+ ctx.query.page // 查询参数 ?page=1
137
+ return Response.json(ctx.params)
110
138
  })
139
+ ```
140
+
141
+ ### 通配符
111
142
 
112
- // URL 参数
113
- app.get('/users/:id', async (req: Request, ctx: Context) => {
114
- const id = ctx.params.id
115
- return Response.json({ id, name: 'User ' + id })
143
+ ```ts
144
+ app.get('/files/*', (req, ctx) => {
145
+ ctx.params['*'] // 剩余路径 "a/b/c.txt"
146
+ return Response.json({ path: ctx.params['*'] })
116
147
  })
117
148
  ```
118
149
 
119
- | 方法 | 路由 | 说明 |
120
- |------|------|------|
121
- | `app.get(path, handler)` | 任意 | GET 请求 |
122
- | `app.post(path, handler)` | 任意 | POST 请求 |
123
- | `app.put(path, handler)` | 任意 | PUT 请求 |
124
- | `app.patch(path, handler)` | 任意 | PATCH 请求 |
125
- | `app.delete(path, handler)` | 任意 | DELETE 请求 |
126
- | `app.use(middleware)` | 全路由 | 中间件 |
127
- | `app.ws(path, handler)` | 任意 | WebSocket |
128
- | `app.graphql(options)` | 自动 | GraphQL 端点 |
129
- | `app.onError(handler)` | 全局 | 错误处理 |
150
+ ### 子路由挂载
130
151
 
131
- ### serve —— HTTP 服务器
152
+ ```ts
153
+ const users = new Router()
154
+ users.get('/', listUsers)
155
+ users.get('/:id', getUser)
156
+
157
+ app.mount('/api/users', users)
158
+ // → GET /api/users, GET /api/users/:id
159
+ ```
160
+
161
+ ### 插件模式
162
+
163
+ ```ts
164
+ app.plugin(app => {
165
+ app.get('/health', () => Response.json({ ok: true }))
166
+ app.use(cors())
167
+ })
168
+ ```
169
+
170
+ ### 类型安全中间件工厂
171
+
172
+ ```ts
173
+ import { createMiddleware } from 'weifuwu'
174
+ declare module 'weifuwu' { interface Context { greeting: string } }
175
+
176
+ const greet = createMiddleware({
177
+ injects: ['greeting'], // 注入的 ctx 字段
178
+ depends: ['sql'], // 前置依赖(可选)
179
+ setup: async (ctx) => ({
180
+ greeting: 'Hello ' + ctx.params.name
181
+ }),
182
+ })
183
+
184
+ app.use(greet)
185
+ app.get('/hello/:name', (req, ctx) => Response.json({ msg: ctx.greeting }))
186
+ ```
187
+
188
+ `createMiddleware` 自动生成 `__meta` 元数据用于运行时依赖检查。
189
+
190
+ ### 查看路由表
191
+
192
+ ```ts
193
+ console.log(app.routes())
194
+ // → [
195
+ // "GET /users",
196
+ // "POST /users",
197
+ // "WS /chat",
198
+ // "MIDDLEWARE [2 global]"
199
+ // ]
200
+ ```
201
+
202
+ ### 路由方法速查
203
+
204
+ | 方法 | 说明 |
205
+ |------|------|
206
+ | `app.get(path, ...mws, handler)` | GET |
207
+ | `app.post(path, ...mws, handler)` | POST |
208
+ | `app.put(path, ...mws, handler)` | PUT |
209
+ | `app.patch(path, ...mws, handler)` | PATCH |
210
+ | `app.delete(path, ...mws, handler)` | DELETE |
211
+ | `app.head(path, ...mws, handler)` | HEAD |
212
+ | `app.options(path, ...mws, handler)` | OPTIONS |
213
+ | `app.all(path, ...mws, handler)` | 任意方法 |
214
+ | `app.use(mw)` | 全局中间件 |
215
+ | `app.ws(path, ...mws, handler)` | WebSocket |
216
+ | `app.graphql(path?, handler)` | GraphQL |
217
+ | `app.mount(path, subRouter)` | 挂载子路由 |
218
+ | `app.plugin(fn)` | 插件扩展现有 Router |
219
+ | `app.onError(handler)` | 全局错误处理 |
220
+ | `app.wsHub(hub)` | 注入自定义 Hub(多进程 WebSocket) |
221
+ | `app.onClose(closeable)` | 注册关闭回调 |
222
+ | `app.routes()` | 打印路由表 |
223
+ | `app.close()` | 关闭所有注册的 Closeable 资源 |
224
+
225
+ ---
226
+
227
+ ## serve — HTTP 服务器
132
228
 
133
229
  ```ts
134
230
  import { serve, Router } from 'weifuwu'
135
231
 
136
- const router = new Router()
137
- serve(router, { port: 3000 })
232
+ const app = new Router()
233
+ const server = serve(app, { port: 3000 })
234
+
235
+ // 等待就绪
236
+ await server.ready
237
+ console.log(server.port) // 实际端口
238
+
239
+ // 停止
240
+ await server.close()
241
+ // 或
242
+ await server.stop(2000) // 超时毫秒
138
243
  ```
139
244
 
140
- | 参数 | 类型 | 默认值 | 说明 |
245
+ | 选项 | 类型 | 默认值 | 说明 |
141
246
  |------|------|--------|------|
142
- | `port` | `number` | `0`(随机端口) | 监听端口 |
247
+ | `port` | `number` | `0`(随机) | 监听端口 |
143
248
  | `hostname` | `string` | `'0.0.0.0'` | 监听地址 |
144
- | `timeout` | `number` | `30000` | 连接超时(ms)|
145
- | `maxBodySize` | `number` | `10MB` | 请求体上限 |
249
+ | `signal` | `AbortSignal` | | 通过信号停止 |
250
+ | `maxBodySize` | `number` | `10MB` | 请求体上限(0=无限) |
251
+ | `timeout` | `number` | `30000` | Socket 超时(ms) |
252
+ | `keepAliveTimeout` | `number` | `5000` | Keep-Alive 超时 |
253
+ | `headersTimeout` | `number` | `6000` | 请求头超时 |
146
254
  | `shutdown` | `boolean` | `true` | 自动注册 SIGTERM/SIGINT |
147
255
 
148
- ### cors —— CORS 中间件
256
+ | 属性/方法 | 类型/签名 | 说明 |
257
+ |-----------|----------|------|
258
+ | `server.port` | `number` | 实际监听端口(未就绪时 0) |
259
+ | `server.hostname` | `string` | 监听地址 |
260
+ | `server.ready` | `Promise<void>` | 服务器就绪 |
261
+ | `server.close(timeoutMs?)` | `() => Promise<void>` | 优雅关闭 |
262
+ | `server.stop(timeoutMs?)` | `() => Promise<void>` | `close` 别名 |
263
+
264
+ `sig-server` 自动注册 SIGTERM/SIGINT → `server.closeAllConnections()` → `router.close()` → `process.exit(0)`。
265
+
266
+ ---
267
+
268
+ ## cors — CORS 中间件
149
269
 
150
270
  ```ts
271
+ import { cors } from 'weifuwu'
272
+
273
+ // 默认:允许所有来源
151
274
  app.use(cors())
152
275
 
276
+ // 自定义
153
277
  app.use(cors({
154
- origin: ['https://app.example.com'],
278
+ origin: 'https://app.example.com',
155
279
  methods: ['GET', 'POST'],
280
+ allowedHeaders: ['Content-Type', 'Authorization'],
281
+ credentials: true,
282
+ exposedHeaders: ['X-Total-Count'],
283
+ maxAge: 86400,
156
284
  }))
157
285
  ```
158
286
 
159
- | 参数 | 默认值 |
160
- |------|--------|
161
- | `origin` | `*` |
162
- | `methods` | `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
163
- | `allowedHeaders` | `Content-Type, Authorization` |
287
+ | 选项 | 类型 | 默认值 | 说明 |
288
+ |------|------|--------|------|
289
+ | `origin` | `string \| string[] \| (origin) => string` | `'*'` | `credentials: true` 时自动回显请求 origin |
290
+ | `methods` | `string[]` | `GET,HEAD,PUT,PATCH,POST,DELETE` | 允许的方法 |
291
+ | `allowedHeaders` | `string[]` | `Content-Type, Authorization` | 允许的请求头 |
292
+ | `exposedHeaders` | `string[]` | — | 暴露的响应头 |
293
+ | `credentials` | `boolean` | — | 是否允许凭据 |
294
+ | `maxAge` | `number` | — | 预检缓存秒数 |
295
+
296
+ ---
164
297
 
165
- ### serveStatic —— 静态文件
298
+ ## serveStatic 静态文件服务
166
299
 
167
300
  ```ts
168
301
  import { serveStatic } from 'weifuwu'
169
302
 
170
- // 单一路径
171
- app.get('/static/*', serveStatic('./public'))
303
+ // 作为全局中间件(未匹配到文件时走下一个中间件)
304
+ app.use(serveStatic('./public'))
172
305
 
173
- // 多目录
174
- app.get('/uploads/*', serveStatic('./uploads'))
306
+ // 或挂载到特定路径
307
+ app.get('/assets/*', serveStatic('./assets', {
308
+ index: 'index.html',
309
+ maxAge: 31536000,
310
+ immutable: true,
311
+ }))
175
312
  ```
176
313
 
177
- ### postgres —— PostgreSQL
314
+ | 选项 | 类型 | 默认值 | 说明 |
315
+ |------|------|--------|------|
316
+ | `index` | `string` | `'index.html'` | 目录索引文件名 |
317
+ | `maxAge` | `number` | `0` | Cache-Control max-age(秒) |
318
+ | `immutable` | `boolean` | — | 添加 `immutable` 指令(需 maxAge) |
319
+
320
+ 特性:
321
+ - ETag/304 缓存协商(`if-none-match` + `if-modified-since`)
322
+ - MIME 类型自动检测(支持 30+ 扩展名)
323
+ - 目录遍历保护(`..` / symlink 逃逸 → 403)
324
+ - 目录自动跳转到 index 文件
325
+
326
+ ---
327
+
328
+ ## postgres — PostgreSQL 客户端
178
329
 
179
330
  ```ts
180
- import { postgres } from 'weifuwu'
331
+ import { postgres, MIGRATIONS_TABLE } from 'weifuwu'
181
332
 
333
+ // 注入 ctx.sql — postgres.js 客户端
182
334
  app.use(postgres())
183
335
 
184
- // 然后在 handler 中使用 ctx.sql
336
+ // 使用 ctx.sql
185
337
  app.get('/users', async (req, ctx) => {
186
- const users = await ctx.sql`SELECT * FROM users`
338
+ const users = await ctx.sql`SELECT * FROM users WHERE active = ${true}`
187
339
  return Response.json(users)
188
340
  })
341
+
342
+ // 事务
343
+ app.post('/transfer', async (req, ctx) => {
344
+ const result = await ctx.sql.begin(async sql => {
345
+ await sql`UPDATE accounts SET balance = balance - 100 WHERE id = 1`
346
+ await sql`UPDATE accounts SET balance = balance + 100 WHERE id = 2`
347
+ })
348
+ return Response.json({ ok: true })
349
+ })
189
350
  ```
190
351
 
191
352
  | 选项 | 类型 | 默认值 | 说明 |
192
353
  |------|------|--------|------|
193
354
  | `url` | `string` | `DATABASE_URL` 环境变量 | 连接字符串 |
194
355
  | `max` | `number` | `10` | 连接池大小 |
356
+ | `idleTimeout` | `number` | `30` | 空闲连接超时(秒)|
357
+ | `maxLifetime` | `number` | `3600` | 连接最大生存时间(秒)|
195
358
 
196
- ### redis —— Redis
359
+ | ctx 注入 | 类型 | 说明 |
360
+ |----------|------|------|
361
+ | `ctx.sql` | `postgres.Sql` | postgres.js 客户端(模板标签)|
362
+ | `ctx.sql.close()` | `() => Promise<void>` | 关闭连接池 |
363
+
364
+ ```ts
365
+ // 关闭
366
+ const pg = postgres()
367
+ app.use(pg)
368
+ // 关闭时框架自动调用 pg.close()
369
+ ```
370
+
371
+ ---
372
+
373
+ ## redis — Redis 客户端
197
374
 
198
375
  ```ts
199
376
  import { redis } from 'weifuwu'
200
377
 
201
378
  app.use(redis())
202
379
 
203
- // 使用 ctx.redis
204
- app.get('/cache', async (req, ctx) => {
205
- const cached = await ctx.redis.get('key')
206
- return Response.json({ cached })
380
+ app.get('/cache/:key', async (req, ctx) => {
381
+ const val = await ctx.redis.get(ctx.params.key)
382
+ if (!val) return Response.json({ miss: true })
383
+ return Response.json({ value: val })
384
+ })
385
+
386
+ app.post('/cache/:key', async (req, ctx) => {
387
+ const { value } = await req.json()
388
+ await ctx.redis.set(ctx.params.key, JSON.stringify(value), 'EX', 3600)
389
+ return Response.json({ ok: true })
207
390
  })
208
391
  ```
209
392
 
210
393
  | 选项 | 类型 | 默认值 | 说明 |
211
394
  |------|------|--------|------|
212
395
  | `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
396
+ | `options` | `RedisOptions` | — | ioredis 配置选项 |
213
397
 
214
- ### ui —— SSR + SPA 渲染
398
+ | ctx 注入 | 类型 | 说明 |
399
+ |----------|------|------|
400
+ | `ctx.redis` | `ioredis.Redis` | ioredis 实例 |
401
+ | `ctx.redis.close()` | `() => Promise<void>` | 关闭连接 |
402
+
403
+ 支持全部 ioredis API:`get`, `set`, `del`, `hget`, `hset`, `lpush`, `publish` 等。
404
+
405
+ ---
406
+
407
+ ## ui — SSR 渲染 + JS/CSS 编译
215
408
 
216
409
  ```ts
410
+ import { ui } from 'weifuwu'
411
+
217
412
  app.use(ui())
413
+ ```
414
+
415
+ | ctx 注入 | 签名 | 说明 |
416
+ |----------|------|------|
417
+ | `ctx.ui.html` | `` (strings, ...values) => Response `` | HTML 模板 (转义防 XSS) |
418
+ | `ctx.ui.html.unsafe(str)` | `(string) => string` | 插入原始 HTML |
419
+ | `ctx.ui.js(entryPath)` | `(string) => Promise<Response>` | esbuild 编译 TSX → JS bundle |
420
+ | `ctx.ui.css(entryPath)` | `(string) => Promise<Response>` | 读取/编译 CSS(自动 PostCSS + Tailwind) |
421
+
422
+ ### ctx.ui.html — HTML 模板
218
423
 
219
- // SSR 页面
220
- app.get('/page', async (req, ctx) => ctx.ui.html`
221
- <h1>${title}</h1>
222
- <p>${body}</p>
424
+ 模板插值自动转义(`& < > "` → 实体),防 XSS:
425
+
426
+ ```ts
427
+ app.get('/page', (req, ctx) => ctx.ui.html`
428
+ <h1>${title}</h1> <!-- 自动转义 -->
429
+ <div>${ctx.ui.html.unsafe(richHtml)}</div> <!-- 不转义 -->
223
430
  `)
431
+ ```
224
432
 
225
- // 动态 JS 编译(esbuild,零构建步骤)
226
- app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
433
+ ### ctx.ui.js — 编译 TSX → JS
227
434
 
228
- // CSS 编译(PostCSS + Tailwind)
229
- app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
435
+ ```ts
436
+ app.get('/app.js', (req, ctx) => ctx.ui.js('./src/main.tsx'))
230
437
  ```
231
438
 
232
- | 方法 | 用途 |
233
- |------|------|
234
- | `ctx.ui.html\`...\`` | 渲染 HTML 模板(转义变量防 XSS) |
235
- | `ctx.ui.html.unsafe(str)` | 插入原始 HTML |
236
- | `ctx.ui.js(entryPath)` | 编译 TSX → JS bundle |
237
- | `ctx.ui.css(entryPath)` | 编译 CSS(PostCSS + Tailwind) |
439
+ 使用 esbuild 编译:
440
+ - `bundle: true`, `format: 'esm'`, `platform: 'browser'`
441
+ - `jsx: 'automatic'`, `jsxImportSource: 'weifuwu/client'`
442
+ - mtime 缓存验证(开发时编辑文件后自动失效)
238
443
 
239
- ### graphql —— GraphQL
444
+ ### ctx.ui.css CSS 编译
240
445
 
241
446
  ```ts
242
- app.graphql({
243
- schema: `type Query { hello: String }`,
244
- resolvers: { Query: { hello: () => 'world' } },
447
+ app.get('/style.css', (req, ctx) => ctx.ui.css('./src/style.css'))
448
+ ```
449
+
450
+ - 纯 CSS 文件直接返回
451
+ - 检测到 `postcss` + `@tailwindcss/postcss` 时自动编译 Tailwind CSS
452
+ - 带 mtime 缓存验证
453
+
454
+ ---
455
+
456
+ ## graphql — GraphQL 端点
457
+
458
+ ```ts
459
+ import type { GraphQLHandler } from 'weifuwu'
460
+
461
+ const handler: GraphQLHandler = async (req, ctx) => ({
462
+ schema: `
463
+ type Query {
464
+ hello: String
465
+ users: [User]
466
+ }
467
+ type User { id: ID, name: String }
468
+ `,
469
+ resolvers: {
470
+ Query: {
471
+ hello: () => 'world',
472
+ users: () => [{ id: 1, name: 'Alice' }],
473
+ },
474
+ },
475
+ rootValue: {},
476
+ context: (req, ctx) => ({ user: ctx.user }),
245
477
  graphiql: true,
478
+ maxDepth: 10,
479
+ timeout: 30000,
480
+ })
481
+
482
+ // 挂载到 /
483
+ app.graphql(handler)
484
+
485
+ // 或挂载到自定义路径
486
+ app.graphql('/graphql', handler)
487
+ ```
488
+
489
+ | 选项 | 类型 | 默认值 | 说明 |
490
+ |------|------|--------|------|
491
+ | `schema` | `string \| GraphQLSchema` | — | SDL 字符串或 Schema 对象 |
492
+ | `resolvers` | `any` | — | 解析器(schema 为字符串时必填)|
493
+ | `rootValue` | `any` | — | 根值 |
494
+ | `context` | `(req, ctx) => object` | — | 上下文工厂 |
495
+ | `graphiql` | `boolean` | — | 启用 GraphiQL IDE |
496
+ | `maxDepth` | `number` | `10` | 查询深度限制(0=关闭)|
497
+ | `timeout` | `number` | `30000` | 执行超时(ms,0=关闭)|
498
+
499
+ GET 请求支持 query 参数查询;POST 支持 JSON body。启用 `graphiql: true` 时,GET 无 `?query=` 参数返回 GraphiQL IDE 页面。
500
+
501
+ ---
502
+
503
+ ## WebSocket
504
+
505
+ ```ts
506
+ app.ws('/chat/:room', {
507
+ open(ws, ctx) {
508
+ ws.send(`欢迎加入 ${ctx.params.room} 房间`)
509
+ ctx.hub?.join(ctx.params.room, ws)
510
+ },
511
+ message(ws, ctx, data) {
512
+ // data: string | Buffer
513
+ ctx.hub?.send(ctx.params.room, `用户: ${data}`)
514
+ },
515
+ close(ws, ctx) {
516
+ ctx.hub?.leave(ws)
517
+ },
518
+ error(ws, ctx, error) {
519
+ console.error('WS error:', error)
520
+ },
246
521
  })
247
522
  ```
248
523
 
249
- ### WebSocket
524
+ | 回调 | 参数 | 说明 |
525
+ |------|------|------|
526
+ | `open(ws, ctx)` | `WebSocket`, `Context` | 连接建立 |
527
+ | `message(ws, ctx, data)` | `WebSocket`, `Context`, `string \| Buffer` | 收到消息 |
528
+ | `close(ws, ctx)` | `WebSocket`, `Context` | 连接关闭 |
529
+ | `error(ws, ctx, error)` | `WebSocket`, `Context`, `Error` | 错误 |
530
+
531
+ ### Hub — WebSocket 房间
250
532
 
251
533
  ```ts
252
- app.ws('/ws', {
253
- open(ws, ctx) { ws.send('connected') },
254
- message(ws, ctx, data) { /* data: string | Buffer */ },
255
- close(ws, ctx) { /* cleanup */ },
256
- error(ws, ctx, err) { /* log */ },
534
+ // 注入 hub → ctx.hub
535
+ app.ws('/chat/:room', {
536
+ open(ws, ctx) { ctx.hub.join(ctx.params.room, ws) },
537
+ message(ws, ctx, data) { ctx.hub.send(ctx.params.room, String(data)) },
538
+ close(ws, ctx) { ctx.hub.leave(ws) },
257
539
  })
540
+
541
+ // 自定义 Hub(Redis 后端)
542
+ import type { Hub } from 'weifuwu'
543
+ const redisHub: Hub = { ... }
544
+ app.wsHub(redisHub)
258
545
  ```
259
546
 
260
- ### 错误处理
547
+ | Hub 方法 | 说明 |
548
+ |----------|------|
549
+ | `join(key, ws)` | WebSocket 加入房间 |
550
+ | `leave(ws)` | WebSocket 离开所有房间 |
551
+ | `send(key, message)` | 向房间广播消息 |
552
+ | `close()` | 关闭 Hub |
553
+
554
+ WebSocket 原生 `ws.send()` 发送,`ws.on('message', cb)` WebSocket 接收。
555
+
556
+ ---
557
+
558
+ ## HttpError — HTTP 错误
261
559
 
262
560
  ```ts
263
- app.onError((err, req, ctx) => {
264
- if (err instanceof HttpError) {
265
- return new Response(err.message, { status: err.status })
266
- }
267
- console.error(err)
268
- return new Response('Internal Server Error', { status: 500 })
561
+ import { HttpError } from 'weifuwu'
562
+
563
+ app.get('/secure', () => {
564
+ if (!condition) throw new HttpError('Forbidden', 403)
565
+ // serve() 自动捕获并返回对应状态码
566
+ })
567
+ ```
568
+
569
+ | API | 说明 |
570
+ |-----|------|
571
+ | `new HttpError(msg, status)` | 创建 HTTP 错误,name = 'HttpError' |
572
+ | `DEFAULT_MAX_BODY` | `10 * 1024 * 1024` (10MB) |
573
+
574
+ ---
575
+
576
+ ## 响应辅助函数
577
+
578
+ 消除 `Response.json(...)` 重复模式:
579
+
580
+ ```ts
581
+ import { ok, created, noContent, badRequest, unauthorized, forbidden, notFound, conflict, unprocessable, tooManyRequests, serverError, redirect } from 'weifuwu'
582
+
583
+ app.get('/users/:id', async (req, ctx) => {
584
+ const user = await findUser(ctx.params.id)
585
+ if (!user) return notFound('用户不存在')
586
+ return ok(user)
587
+ })
588
+
589
+ app.post('/users', async (req, ctx) => {
590
+ const body = await parseBody(req)
591
+ const user = await createUser(body)
592
+ return created(user)
269
593
  })
270
594
  ```
271
595
 
272
- | 类/常量 | 说明 |
273
- |---------|------|
274
- | `HttpError` | HTTP 错误 `new HttpError(msg, status)` |
275
- | `DEFAULT_MAX_BODY` | 默认请求体上限 10MB |
596
+ | 函数 | 状态码 | Content-Type |
597
+ |------|--------|-------------|
598
+ | `ok(data, init?)` | 200 | `application/json` |
599
+ | `created(data, init?)` | 201 | `application/json` |
600
+ | `noContent(init?)` | 204 | — |
601
+ | `badRequest(msg?)` | 400 | `application/json` |
602
+ | `unauthorized(msg?)` | 401 | `application/json` |
603
+ | `forbidden(msg?)` | 403 | `application/json` |
604
+ | `notFound(msg?)` | 404 | `application/json` |
605
+ | `conflict(msg?)` | 409 | `application/json` |
606
+ | `unprocessable(msg?)` | 422 | `application/json` |
607
+ | `tooManyRequests(msg?)` | 429 | `application/json` |
608
+ | `serverError(msg?)` | 500 | `application/json` |
609
+ | `redirect(url, status?)` | 302 (默认) | — |
610
+
611
+ ---
612
+
613
+ ## parseBody — 请求体解析
614
+
615
+ ```ts
616
+ import { parseBody } from 'weifuwu'
617
+
618
+ app.post('/users', async (req, ctx) => {
619
+ const body = await parseBody<{ name: string; email: string }>(req)
620
+ // JSON 解析失败自动 throw HttpError(400)
621
+ return ok(body)
622
+ })
623
+ ```
624
+
625
+ | 行为 | 说明 |
626
+ |------|------|
627
+ | JSON 格式正确 | 返回解析后的数据 |
628
+ | JSON 格式错误 | `throw new HttpError('Invalid JSON body', 400)` |
629
+ | GET/HEAD 请求 | 返回 `{}` |
630
+
631
+ ---
632
+
633
+ ## 后端类型
276
634
 
277
- ### 后端类型
635
+ ```ts
636
+ import type { Context, Handler, Middleware, ErrorHandler, User, Closeable } from 'weifuwu'
637
+ import type { HttpError } from 'weifuwu'
638
+ import type { ServeOptions, Server } from 'weifuwu'
639
+ import type { Hub, WebSocketHandler } from 'weifuwu'
640
+ import type { WebSocket } from 'weifuwu'
641
+ import type { CORSOptions } from 'weifuwu'
642
+ import type { ServeStaticOptions } from 'weifuwu'
643
+ import type { PostgresOptions, PostgresClient, PostgresInjected } from 'weifuwu'
644
+ import type { RedisOptions, RedisClient, RedisInjected } from 'weifuwu'
645
+ import type { GraphQLOptions, GraphQLHandler } from 'weifuwu'
646
+ ```
278
647
 
279
- `Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
648
+ | 类型 | 签名 | 说明 |
649
+ |------|------|------|
650
+ | `Context` | `interface` | `{ params, query, mountPath, user, loaderData?, env?, [key]: unknown }` |
651
+ | `Handler<T>` | `(req: Request, ctx: T) => Response \| Promise<Response>` | 请求处理器 |
652
+ | `Middleware<In, Out>` | `(req, ctx: In, next) => Response` | 中间件,含 `__meta` |
653
+ | `ErrorHandler<T>` | `(error, req, ctx: T) => Response` | 错误处理器 |
654
+ | `Closeable` | `interface` | `{ close(): Promise<void> }` |
655
+ | `User` | `interface` | `{ id, role?, tenant?, [key]: unknown }` |
656
+ | `HttpError` | `class` | `extends Error`,含 `status` 属性 |
280
657
 
281
658
  ---
282
659
 
283
- ## 前端 (`weifuwu/client`)
660
+ # 前端 API (`weifuwu/client`)
284
661
 
285
- 零外部依赖。组件模型:**纯函数 `(props, ctx) => VNode`**。
662
+ 零外部 npm 运行时依赖。组件模型:纯函数 `(props, ctx) => VNode`。
286
663
 
287
664
  构建配置(esbuild):
288
665
 
@@ -294,11 +671,46 @@ esbuild.build({
294
671
  })
295
672
  ```
296
673
 
297
- ### 组件
674
+ ---
675
+
676
+ ## createApp — 应用引导
298
677
 
299
678
  ```tsx
679
+ import { createApp } from 'weifuwu/client'
680
+
681
+ const app = createApp()
682
+
683
+ // 注册中间件
684
+ app.use(middleware1)
685
+ app.use(middleware2)
686
+
687
+ // 挂载到 DOM
688
+ app.mount('#root', RootComponent)
689
+
690
+ // 获取当前 ctx
691
+ console.log(app.ctx)
692
+
693
+ // 销毁
694
+ app.destroy()
695
+ ```
696
+
697
+ | 方法 | 说明 |
698
+ |------|------|
699
+ | `createApp()` | 创建应用实例 |
700
+ | `app.use(mw)` | 注册 AppMiddleware |
701
+ | `app.mount(selector, RootComponent)` | 挂载到 DOM |
702
+ | `app.destroy()` | 卸载应用 |
703
+ | `app.ctx` | 当前 WfuiContext |
704
+
705
+ ---
706
+
707
+ ## 组件模型
708
+
709
+ ```tsx
710
+ import type { Component, WfuiContext } from 'weifuwu/client'
711
+
300
712
  // 组件 = 纯函数 (props, ctx) => VNode
301
- function Greeting(props: { name: string }, _ctx: WfuiContext) {
713
+ const Greeting: Component<{ name: string }> = (props, ctx) => {
302
714
  return <div>Hello, {props.name}!</div>
303
715
  }
304
716
 
@@ -306,14 +718,61 @@ function Greeting(props: { name: string }, _ctx: WfuiContext) {
306
718
  <Greeting name="world" />
307
719
  ```
308
720
 
309
- ### 状态 —— 深度 Proxy
721
+ | 规则 | 说明 |
722
+ |------|------|
723
+ | 组件签名 | `(props: P, ctx: WfuiContext) => VNode \| null` |
724
+ | 无 class | 无 `this`,无实例方法 |
725
+ | 无 hook | 无 `useState` / `useEffect` / `useMemo` |
726
+ | 无生命周期 | 替代方案:`ref` 回调管理 mount/unmount |
727
+
728
+ ### JSX 工厂
729
+
730
+ ```tsx
731
+ // 由 esbuild 自动调用(jsxImportSource: 'weifuwu/client')
732
+ import { h, jsx, jsxs, jsxDEV, Fragment } from 'weifuwu/client'
733
+
734
+ // h 支持 variadic children
735
+ h('div', { class: 'x' }, child1, child2)
736
+
737
+ // Fragment
738
+ <><div>A</div><div>B</div></>
739
+ ```
740
+
741
+ | 导出 | 用途 |
742
+ |------|------|
743
+ | `h(type, props, ...children)` | hyperscript |
744
+ | `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
745
+ | `Fragment` | 片段 |
746
+
747
+ ### VNode 结构
748
+
749
+ ```ts
750
+ interface VNode {
751
+ type: string | Component | typeof Fragment
752
+ props: Record<string, any>
753
+ key?: string
754
+ el?: Node // 对应 DOM(框架内部)
755
+ _$?: Record<string, any> // 组件状态(框架内部)
756
+ _child?: any // 子 VNode 缓存
757
+ _cleanup?: (() => void) // ref 清理函数
758
+ }
759
+ ```
760
+
761
+ ---
762
+
763
+ ## 状态管理 — ctx.ui.$
764
+
765
+ `ctx.ui.$` 是**组件级深度 Proxy**:
310
766
 
311
- `ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象写入自动触发渲染,无需手动调用。
767
+ - `$.x = val` 自动标记 dirty → 下个微任务批量 VDOM patch
768
+ - `$.arr.push(val)` → Proxy 拦截数组突变 → 自动渲染
769
+ - `$.arr[0].x = y` → 深度拦截嵌套对象 → 自动渲染
770
+ - 每个组件实例有独立 Proxy(`vnode._$`),同名变量不冲突
312
771
 
313
772
  ```tsx
314
773
  function Counter(_props: {}, ctx: WfuiContext) {
315
774
  const $ = ctx.ui.$
316
- if (!ctx.ui.ready) $.count = 0
775
+ if (!ctx.ui.ready) $.count = 0 // 初始化(仅首次执行)
317
776
 
318
777
  return (
319
778
  <div>
@@ -325,14 +784,17 @@ function Counter(_props: {}, ctx: WfuiContext) {
325
784
  ```
326
785
 
327
786
  | API | 说明 |
328
- |------|------|
329
- | `ctx.ui.$` | 深度 Proxy,所有写入自动触发渲染 |
330
- | `$.x = val` | 顶层属性赋值 自动渲染 |
331
- | `$.items.push(val)` | 数组突变 自动渲染 |
332
- | `$.items[0].x = val` | 对象属性突变 → 自动渲染 |
333
- | `ctx.ui.dirty()` | 仅当绕过 Proxy 直接操作底层对象时使用 |
787
+ |-----|------|
788
+ | `ctx.ui.$` | 组件级深度 Proxy |
789
+ | `ctx.ui.ready` | `boolean`,首次执行后为 `true` |
790
+ | `ctx.ui.dirty()` | 手动标记脏状态(仅绕过 Proxy 时使用) |
791
+ | `ctx.ui.render()` | 手动触发渲染(框架内部使用,页面代码不用) |
792
+
793
+ **重要规则**:render 函数内部不写 `$`。`$` 的写入只在事件回调(onClick/onInput)、ref 回调、或 `if (!ctx.ui.ready)` 初始化块中进行。
794
+
795
+ ---
334
796
 
335
- ### 条件与列表
797
+ ## 条件与列表
336
798
 
337
799
  使用原生 JS 控制流:
338
800
 
@@ -341,23 +803,25 @@ function Counter(_props: {}, ctx: WfuiContext) {
341
803
  {cond ? <A /> : <B />}
342
804
  {cond && <A />}
343
805
 
344
- // 列表
345
- {items.map(item => <div key={item.id}>{item.name}</div>)}
806
+ // 列表 — 必须指定 key
807
+ {items.map(item => (
808
+ <div key={item.id}>{item.name}</div>
809
+ ))}
346
810
  ```
347
811
 
348
- ### 生命周期 —— ref 回调
812
+ ---
813
+
814
+ ## 生命周期 — ref 回调
349
815
 
350
- `ref` 回调在 mount 时触发,接收 DOM 元素。返回的清理函数在 unmount 时由框架保证调用。
816
+ `ref` 回调在 mount 时触发,接收 DOM 元素。返回的清理函数在 unmount 时由框架保证调用:
351
817
 
352
818
  ```tsx
353
- function MyComponent(_props: {}, ctx: WfuiContext) {
819
+ function Timer(_props: {}, ctx: WfuiContext) {
354
820
  return (
355
821
  <div ref={el => {
356
- // mount: el DOM 元素
357
- el.addEventListener('scroll', handler)
358
- // 返回清理函数,unmount 时框架保证调用
359
- return () => el.removeEventListener('scroll', handler)
360
- }} />
822
+ const timer = setInterval(() => console.log('tick'), 1000)
823
+ return () => { clearInterval(timer); console.log('cleanup') }
824
+ }}>Timer</div>
361
825
  )
362
826
  }
363
827
  ```
@@ -365,44 +829,40 @@ function MyComponent(_props: {}, ctx: WfuiContext) {
365
829
  | 场景 | 写法 |
366
830
  |------|------|
367
831
  | 事件监听 | `ref={el => { el.addEventListener(...); return () => el.removeEventListener(...) }}` |
368
- | 定时器 | `ref={el => { const t = setInterval(f, 1000); return () => clearInterval(t) }}` |
832
+ | 定时器 | `ref={el => { const t = setInterval(...); return () => clearInterval(t) }}` |
369
833
  | 第三方库 | `ref={el => { const c = new Chart(el); return () => c.destroy() }}` |
370
- | 仅 mount | `ref={el => { init(el) }}` |
834
+ | 仅 mount | `ref={el => { init(el) }}`(无返回) |
371
835
 
372
- ### 应用 —— createApp
373
-
374
- ```tsx
375
- import { createApp } from 'weifuwu/client'
376
-
377
- const app = createApp()
378
- app.use(middleware1)
379
- app.use(middleware2)
380
- app.mount('#root', RootComponent)
381
- app.destroy()
382
- ```
836
+ ---
383
837
 
384
- ### 路由 —— router + RouteView
838
+ ## router + RouteView — 前端路由
385
839
 
386
840
  ```tsx
387
- import { router, RouteView } from 'weifuwu/client'
841
+ import { createApp, router, RouteView } from 'weifuwu/client'
842
+ import type { RouteDef, WfuiContext } from 'weifuwu/client'
843
+
844
+ const routes: RouteDef[] = [
845
+ { path: '/', component: Home },
846
+ { path: '/users', component: UserList },
847
+ { path: '/users/:id', component: UserDetail },
848
+ ]
388
849
 
389
850
  createApp()
390
851
  .use(router({
391
- routes: [
392
- { path: '/', component: Home },
393
- { path: '/users', component: UserList },
394
- { path: '/users/:id', component: UserDetail },
395
- ],
396
- notFound: NotFound,
852
+ routes,
397
853
  mode: 'history', // 或 'hash'
854
+ notFound: NotFoundPage,
398
855
  }))
399
- .mount('#root', AppShell)
856
+ .mount('#root', () => <RouteView />)
857
+ ```
400
858
 
401
- // 嵌套布局
859
+ ### 嵌套布局
860
+
861
+ ```tsx
402
862
  const routes = [
403
863
  {
404
864
  path: '/dashboard',
405
- layout: DashboardLayout, // 持久布局
865
+ layout: DashboardLayout, // 持久布局(包含 RouteView)
406
866
  children: [
407
867
  { path: '/overview', component: Overview },
408
868
  { path: '/settings', component: Settings },
@@ -410,374 +870,633 @@ const routes = [
410
870
  },
411
871
  ]
412
872
 
413
- // 在 layout 中放置 RouteView 渲染子路由
414
873
  function DashboardLayout(_props: {}, ctx: WfuiContext) {
415
874
  return (
416
- <div class="wf-split">
417
- <aside>sidebar</aside>
418
- <main><RouteView /></main>
875
+ <div style="display:flex">
876
+ <aside>导航菜单</aside>
877
+ <main><RouteView /></main> {/* 渲染子路由 */}
419
878
  </div>
420
879
  )
421
880
  }
422
881
  ```
423
882
 
424
- | API | 说明 |
425
- |------|------|
426
- | `ctx.route.path` | 当前路由路径 |
427
- | `ctx.route.params` | URL 参数(如 `:id`)|
428
- | `ctx.route.query` | 查询参数对象 |
429
- | `ctx.app.navigate(path)` | 编程式导航 |
430
-
431
- ### 中间件
432
-
433
- **ws —— WebSocket 客户端**
883
+ ### 编程式导航
434
884
 
435
885
  ```tsx
436
- app.use(ws())
886
+ // 在任意组件中
887
+ ctx.app?.navigate('/users/123?tab=profile')
888
+ ```
437
889
 
438
- // 发送消息
439
- ctx.ws?.send({ type: 'chat', body: 'hello' })
890
+ | ctx 注入 | 类型 | 说明 |
891
+ |----------|------|------|
892
+ | `ctx.route.path` | `string` | 当前路由路径 |
893
+ | `ctx.route.params` | `Record<string, string>` | URL 参数 |
894
+ | `ctx.route.query` | `Record<string, string>` | 查询参数 |
895
+ | `ctx.app.navigate(path)` | `(string) => void` | 编程式导航 |
896
+
897
+ | RouterOptions | 类型 | 默认值 | 说明 |
898
+ |---------------|------|--------|------|
899
+ | `routes` | `RouteDef[]` | — | 路由定义 |
900
+ | `mode` | `'history' \| 'hash'` | `'history'` | 路由模式 |
901
+ | `notFound` | `Component` | — | 404 页面 |
902
+
903
+ | RouteDef | 类型 | 说明 |
904
+ |----------|------|------|
905
+ | `path` | `string` | 路径(支持 `:param`) |
906
+ | `component` | `Component` | 页面组件 |
907
+ | `layout` | `Component` | 布局组件(内含 `<RouteView />`) |
908
+ | `children` | `RouteDef[]` | 子路由 |
909
+ | `auth` | `boolean` | 是否需要认证(配合 auth 中间件) |
910
+ | `title` | `string` | 页面标题(自动设置 `document.title`) |
440
911
 
441
- // 接收消息
442
- ctx.ws?.onMessage((msg) => { console.log(msg) })
443
- ```
912
+ ---
444
913
 
445
- **api —— HTTP 客户端**
914
+ ## api HTTP 客户端中间件
446
915
 
447
916
  ```tsx
448
- app.use(api({ baseURL: '/api' }))
917
+ import { createApp, api } from 'weifuwu/client'
449
918
 
450
- // 自动携带 Authorization header
451
- const user = await ctx.api?.get('/users/1')
452
- const res = await ctx.api?.post('/users', { name: 'Alice' })
919
+ createApp()
920
+ .use(api({ baseURL: '/api' }))
921
+ .mount('#root', App)
922
+
923
+ // 在组件中使用
924
+ async function loadUsers(ctx: WfuiContext) {
925
+ const users = await ctx.api?.get<User[]>('/users')
926
+ const user = await ctx.api?.get<User>('/users/1')
927
+ const created = await ctx.api?.post<User>('/users', { name: 'Alice' })
928
+ await ctx.api?.put('/users/1', { name: 'Bob' })
929
+ await ctx.api?.patch('/users/1', { name: 'Bob' })
930
+ await ctx.api?.delete('/users/1')
931
+ }
453
932
  ```
454
933
 
455
- | 方法 | 说明 |
456
- |------|------|
457
- | `ctx.api.get(url, opts?)` | GET |
458
- | `ctx.api.post(url, body?, opts?)` | POST |
459
- | `ctx.api.put(url, body?, opts?)` | PUT |
460
- | `ctx.api.patch(url, body?, opts?)` | PATCH |
461
- | `ctx.api.delete(url, opts?)` | DELETE |
934
+ | 选项 | 类型 | 默认值 | 说明 |
935
+ |------|------|--------|------|
936
+ | `baseURL` | `string` | `''` | API 基础路径 |
937
+ | `headers` | `Record<string, string>` | `{ 'Content-Type': 'application/json' }` | 默认请求头 |
938
+ | `onRequest` | `(req) => { url, init }` | | 请求拦截器 |
939
+ | `onResponse` | `(res) => Promise<T>` | | 响应拦截器 |
940
+ | `timeout` | `number` | `0`(无超时) | 请求超时(ms)|
941
+
942
+ | ctx.api 方法 | 签名 | 说明 |
943
+ |-------------|------|------|
944
+ | `api.get(url, opts?)` | `<T>(string, ApiRequestOptions?) => Promise<T>` | GET |
945
+ | `api.post(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | POST |
946
+ | `api.put(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | PUT |
947
+ | `api.patch(url, body?, opts?)` | `<T>(string, unknown?, ApiRequestOptions?) => Promise<T>` | PATCH |
948
+ | `api.delete(url, opts?)` | `<T>(string, ApiRequestOptions?) => Promise<T>` | DELETE |
949
+
950
+ ```ts
951
+ // 错误处理
952
+ try {
953
+ await ctx.api!.get('/users')
954
+ } catch (e) {
955
+ if (e instanceof ApiError) {
956
+ console.log(e.status, e.body) // e.g. 404, 'Not Found'
957
+ }
958
+ }
959
+ ```
960
+
961
+ `ApiError`:`{ status: number, body: string }`,继承 `Error`。
962
+
963
+ | ApiRequestOptions | 类型 | 说明 |
964
+ |-------------------|------|------|
965
+ | `headers` | `Record<string, string>` | 本次请求自定义请求头 |
966
+ | `signal` | `AbortSignal` | 取消请求 |
967
+
968
+ ---
462
969
 
463
- **auth —— 认证状态管理**
970
+ ## auth 认证中间件
464
971
 
465
972
  ```tsx
466
- app.use(auth())
973
+ import { createApp, auth } from 'weifuwu/client'
974
+
975
+ createApp()
976
+ .use(auth())
977
+ .mount('#root', App)
978
+
979
+ // 在组件中
980
+ function Profile(_props: {}, ctx: WfuiContext) {
981
+ if (!ctx.auth?.isLoggedIn) return <p>请登录</p>
982
+ return <p>欢迎, {ctx.auth?.user?.name}</p>
983
+ }
467
984
 
468
985
  // 登录
469
- ctx.auth?.login(token, user)
986
+ ctx.auth?.login(token, { id: 1, name: 'Alice' }, refreshToken)
470
987
 
471
988
  // 登出
472
989
  ctx.auth?.logout()
473
990
 
474
- // 状态
475
- if (ctx.auth?.isLoggedIn) { ... }
991
+ // 更新用户信息
992
+ ctx.auth?.setUser({ id: 1, name: 'Bob' })
993
+
994
+ // 刷新 token
995
+ await ctx.auth?.refresh() // → boolean
476
996
  ```
477
997
 
478
- | API | 说明 |
479
- |------|------|
480
- | `ctx.auth.token` | JWT token |
481
- | `ctx.auth.user` | 用户对象 |
482
- | `ctx.auth.isLoggedIn` | 是否已登录 |
483
- | `ctx.auth.login(token, user, refreshToken?)` | 登录 |
484
- | `ctx.auth.logout()` | 登出 |
998
+ | 选项 | 类型 | 默认值 | 说明 |
999
+ |------|------|--------|------|
1000
+ | `storage` | `Storage` | `localStorage` | 存储方式 |
1001
+ | `tokenKey` | `string` | `'weifuwu_token'` | Token 存储 key |
1002
+ | `userKey` | `string` | `'weifuwu_user'` | 用户信息存储 key |
1003
+ | `refreshTokenKey` | `string` | `'weifuwu_refresh'` | Refresh token 存储 key |
1004
+ | `refreshEndpoint` | `string` | `'/api/auth/refresh'` | 刷新端点 |
1005
+
1006
+ | ctx.auth | 类型 | 说明 |
1007
+ |----------|------|------|
1008
+ | `.token` | `string \| null` | JWT token |
1009
+ | `.user` | `any` | 用户对象 |
1010
+ | `.isLoggedIn` | `boolean` | 是否已登录(基于 token 存在) |
1011
+ | `.login(token, user, refreshToken?)` | `void` | 登录 |
1012
+ | `.logout()` | `void` | 登出(清除存储) |
1013
+ | `.setUser(user)` | `void` | 更新用户信息 |
1014
+ | `.refresh()` | `Promise<boolean>` | 刷新 token(自动检测过期) |
1015
+
1016
+ 启动时自动检测 token 是否过期(JWT `exp` 提前 30 秒),过期则自动调用 `refresh()`。
1017
+
1018
+ ---
485
1019
 
486
- ### ErrorBoundary
1020
+ ## ws — WebSocket 客户端中间件
487
1021
 
488
1022
  ```tsx
489
- import { ErrorBoundary } from 'weifuwu/client'
1023
+ import { createApp, ws } from 'weifuwu/client'
490
1024
 
491
- <ErrorBoundary fallback={<p>出错了</p>}>
492
- <UserProfile />
493
- </ErrorBoundary>
1025
+ createApp()
1026
+ .use(ws({ url: '/ws' }))
1027
+ .mount('#root', App)
1028
+
1029
+ // 发送消息
1030
+ ctx.ws?.send({ type: 'chat', body: 'hello' })
1031
+
1032
+ // 接收消息 — 返回 unsubscribe 函数
1033
+ const unsubscribe = ctx.ws?.onMessage((msg) => {
1034
+ console.log('收到:', msg)
1035
+ })
1036
+
1037
+ // 清理
1038
+ unsubscribe?.()
494
1039
  ```
495
1040
 
496
- ### 工具
1041
+ | 选项 | 类型 | 默认值 | 说明 |
1042
+ |------|------|--------|------|
1043
+ | `url` | `string` | `'/ws'` | WebSocket 连接地址 |
1044
+ | `reconnectInterval` | `number` | `3000` | 重连间隔(ms) |
1045
+ | `maxReconnect` | `number` | `10` | 最大重连次数 |
1046
+ | `pingInterval` | `number` | `30000` | 心跳发送间隔 |
1047
+ | `pingTimeout` | `number` | `10000` | 心跳超时断开 |
1048
+
1049
+ | ctx.ws | 类型 | 说明 |
1050
+ |--------|------|------|
1051
+ | `.send(msg)` | `(unknown) => void` | 发送 JSON 消息 |
1052
+ | `.onMessage(fn)` | `(fn) => () => void` | 订阅消息,返回 unsubscribe |
1053
+ | `.isConnected` | `boolean` | 连接状态 |
1054
+ | `.close()` | `() => void` | 断开连接 |
497
1055
 
498
- | 函数 | 用途 |
499
- |------|------|
500
- | `extendCtx(ctx, fields)` | 创建新 ctx,继承原 ctx 的 getter |
1056
+ 自动重连(指数退避)、心跳保活、JSON 序列化/反序列化。
501
1057
 
502
- ### 国际化 — `ctx.i18n`
1058
+ ---
503
1059
 
504
- `i18n()` 中间件注入 `ctx.i18n`,支持运行时语言切换:
1060
+ ## i18n 国际化中间件
505
1061
 
506
- ```ts
1062
+ ```tsx
507
1063
  import { createApp, i18n } from 'weifuwu/client'
508
1064
 
509
1065
  createApp()
510
1066
  .use(i18n({
511
1067
  locale: 'zh-CN',
512
- messages: { 'users.title': '用户管理' },
1068
+ messages: {
1069
+ 'title': '仪表盘',
1070
+ 'welcome': '欢迎, {name}',
1071
+ },
513
1072
  }))
514
- .mount('#root', () => <App />)
1073
+ .mount('#root', App)
515
1074
 
516
- // 页面中使用
517
- ctx.i18n?.t('users.title') // → '用户管理'
1075
+ // 组件中使用
1076
+ <h1>{ctx.i18n?.t('title')}</h1>
1077
+ <p>{ctx.i18n?.t('welcome')}</p>
518
1078
 
519
- // 运行时切换
520
- ctx.i18n?.setLocale('en-US') // → 自动触发重渲染
1079
+ // 运行时切换语言
1080
+ ctx.i18n?.setLocale('en-US')
1081
+ // → 自动触发全应用重渲染
521
1082
  ```
522
1083
 
523
- 内置语言包:`zh-CN`(默认)、`en-US`。组件文案(Button `加载中...`、FileUpload `点击或拖拽上传文件`)随语言自动切换。组件通过 `ctx.i18n?.components?.ComponentName.field` 读取,支持 `props.locale` 局部覆盖。
1084
+ | I18nOptions | 类型 | 默认值 | 说明 |
1085
+ |-------------|------|--------|------|
1086
+ | `locale` | `string` | `'zh-CN'` | 初始语言 |
1087
+ | `messages` | `Record<string, string>` | `{}` | 翻译键值对 |
1088
+ | `components` | `Record<string, Record<string, string>>` | `{}` | 组件文案覆盖 |
1089
+
1090
+ | ctx.i18n | 类型 | 说明 |
1091
+ |----------|------|------|
1092
+ | `.t(key, fallback?)` | `(string, string?) => string` | 翻译 |
1093
+ | `.locale` | `string` | 当前语言 |
1094
+ | `.setLocale(lang)` | `(string) => void` | 切换语言(触发重渲染) |
1095
+ | `.components` | `Record<string, Record<string, string>>` | 组件文案映射 |
1096
+
1097
+ 内置语言包:
524
1098
 
525
1099
  ```ts
526
- import { i18n, zhCN, enUS } from 'weifuwu/client'
1100
+ import { zhCN, enUS } from 'weifuwu/client'
527
1101
  ```
528
1102
 
529
- ### 前端类型
1103
+ - `zh-CN`:默认中文
1104
+ - `en-US`:英文
1105
+
1106
+ 组件文案(Button 的 `加载中...`、FileUpload 的 `点击或拖拽上传文件` 等)随语言自动切换。组件内部通过 `ctx.i18n?.components?.ComponentName.field` 读取。
530
1107
 
531
- `VNode`, `VNodeType`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `ApiError`, `AuthClient`, `AuthOptions`, `ErrorBoundaryProps`, `I18nOptions`, `I18nState`, `LocalePackage`
1108
+ 组件支持 `props.locale` 局部覆盖语言。
532
1109
 
533
1110
  ---
534
1111
 
535
- ## 布局 & 主题 (`weifuwu/layout`)
1112
+ ## ErrorBoundary 错误边界
536
1113
 
537
- 纯 CSS 布局原语 + 主题 Token。不绑定任何 JS 框架。
1114
+ ```tsx
1115
+ import { ErrorBoundary } from 'weifuwu/client'
538
1116
 
539
- ```ts
540
- // 服务端编译
541
- app.get('/layout.css', async (req, ctx) => ctx.ui.css('./node_modules/weifuwu/dist/layout/weifuwu-layout.css'))
1117
+ <ErrorBoundary fallback={<p>出错了,请刷新页面</p>}>
1118
+ <UserProfile />
1119
+ </ErrorBoundary>
1120
+
1121
+ // fallback 也可以是一个接收 error 的函数
1122
+ <ErrorBoundary fallback={({ error }) => (
1123
+ <div>
1124
+ <p>出错了: {String(error)}</p>
1125
+ <button onClick={() => ctx.ui.$.error = null}>重试</button>
1126
+ </div>
1127
+ )}>
1128
+ <UserProfile />
1129
+ </ErrorBoundary>
542
1130
  ```
543
1131
 
544
- ```html
545
- <!-- 或直接引入 -->
546
- <link rel="stylesheet" href="/layout.css">
1132
+ | ErrorBoundaryProps | 类型 | 默认值 | 说明 |
1133
+ |--------------------|------|--------|------|
1134
+ | `fallback` | `VNode \| ((props: { error }) => VNode) \| null` | `null` | 错误时渲染的内容 |
1135
+ | `children` | `any` | — | 子组件 |
1136
+
1137
+ 捕获子组件 render 时的错误 → `$.error` → 渲染 fallback。清除 `$.error` 即可重试。
1138
+
1139
+ ---
1140
+
1141
+ ## confirm — 确认对话框
1142
+
1143
+ ```tsx
1144
+ import { createApp, confirm } from 'weifuwu/client'
1145
+
1146
+ createApp()
1147
+ .use(confirm())
1148
+ .mount('#root', App)
1149
+
1150
+ // 在组件中使用
1151
+ const $ = ctx.ui.$
1152
+ async function handleDelete() {
1153
+ const ok = await ctx.confirm?.('确定删除这条记录?', {
1154
+ title: '确认删除',
1155
+ confirmText: '删除',
1156
+ cancelText: '取消',
1157
+ variant: 'danger', // 'primary' | 'danger'
1158
+ })
1159
+ if (ok) {
1160
+ $.deleted = true
1161
+ }
1162
+ }
547
1163
  ```
548
1164
 
549
- ### 使用示例
1165
+ | ConfirmOptions | 类型 | 默认值 | 说明 |
1166
+ |----------------|------|--------|------|
1167
+ | `title` | `string` | `'确认操作'` | 对话框标题 |
1168
+ | `confirmText` | `string` | `'确定'` | 确认按钮文字 |
1169
+ | `cancelText` | `string` | `'取消'` | 取消按钮文字 |
1170
+ | `variant` | `'primary' \| 'danger'` | `'primary'` | 按钮样式变体 |
550
1171
 
551
- ```html
552
- <div class="wf-stack" style="--wf-gap: 24px">
553
- <div class="wf-split">
554
- <h2 style="color: var(--wf-color-text)">仪表盘</h2>
555
- <button style="background: var(--wf-color-primary); color: #fff; border-radius: var(--wf-radius)">+ 新建</button>
556
- </div>
557
- <div class="wf-row" style="--wf-gap: 16px">
558
- <div class="wf-fill wf-surface wf-stack" style="padding: 20px; background: var(--wf-color-bg); --wf-gap: 4px">
559
- <span style="color: var(--wf-color-text-secondary)">总用户</span>
560
- <span style="font-size: var(--wf-font-size-4xl); font-weight: var(--wf-font-weight-bold); color: var(--wf-color-text)">1,234</span>
561
- </div>
562
- </div>
563
- </div>
564
- ```
565
-
566
- ### 33 个布局原语
567
-
568
- | 类别 | 原语 | 含义 | CSS 实现 |
569
- |------|------|------|---------|
570
- | **排列** | `wf-stack` | 纵向堆叠 | `flex-direction: column + gap` |
571
- | | `wf-stack-reverse` | 反向堆叠 | `flex-direction: column-reverse` |
572
- | | `wf-row` | 横向排列 | `flex + flex-wrap + gap` |
573
- | | `wf-row-reverse` | 反向排列 | `flex-direction: row-reverse` |
574
- | | `wf-nowrap` | 不换行 | `flex-wrap: nowrap` |
575
- | | `wf-cluster` | 换行簇 | `flex-wrap: wrap + justify-content: center` |
576
- | **分布** | `wf-split` | 两端展开 | `justify-content: space-between` |
577
- | | `wf-center` | 居中 | `flex + center both axes` |
578
- | | `wf-right` | 靠右 | `justify-content: flex-end` |
579
- | | `wf-around` | 环绕 | `justify-content: space-around` |
580
- | | `wf-evenly` | 均匀 | `justify-content: space-evenly` |
581
- | **对齐** | `wf-top` | 顶部 | `align-items: flex-start` |
582
- | | `wf-bottom` | 底部 | `align-items: flex-end` |
583
- | | `wf-stretch` | 拉伸 | `align-items: stretch` |
584
- | **弹性** | `wf-fill` | 撑满剩余空间 | `flex: 1 + min-width: 0` |
585
- | | `wf-fixed` | 固定不伸缩 | `flex: none` |
586
- | | `wf-auto` | 按内容撑满 | `flex: auto` |
587
- | | `wf-shrink` | 可收缩 | `min-width: 0 + min-height: 0` |
588
- | **Z轴** | `wf-cover` | 全屏覆盖 | `position: fixed + inset: 0` |
589
- | | `wf-pop` | 浮动层 | `position: absolute` |
590
- | | `wf-anchor` | 锚点容器 | `position: relative` |
591
- | | `wf-layer` | 层级控制 | `position: relative + z-index` |
592
- | | `wf-sticky` | 粘性定位 | `position: sticky` |
593
- | **容器** | `wf-surface` | 基础面 | `border-radius + box-shadow + bg` |
594
- | | `wf-grid` | 二维网格 | `display: grid + --wf-cols` |
595
- | | `wf-container` | 宽度约束 | `max-width + margin: auto` |
596
- | | `wf-scroll` | 可滚动 | `overflow: auto` |
597
- | | `wf-clip` | 溢出裁剪 | `overflow: hidden` |
598
- | **显隐** | `wf-hidden` | 隐藏 | `display: none` |
599
- | | `wf-block` | 块级 | `display: block` |
600
- | | `wf-inline` | 行内 | `display: inline` |
601
- | | `wf-inline-block` | 行内块 | `display: inline-block` |
602
- | | `wf-contents` | 容器抹除 | `display: contents` |
603
-
604
- ### 72 个主题 Token
605
-
606
- | 类别 | Token 示例 | 值/层级 |
607
- |------|-----------|---------|
608
- | 品牌色 | `--wf-color-primary`, `--wf-color-primary-bg` | 品牌色 + Hover + 背景 |
609
- | 语义色 | `--wf-color-success/warning/error/info` | 各带 `-bg` 背景变体 |
610
- | 中性色 | `--wf-color-text/text-secondary/text-tertiary/text-disabled` | 4 级文字色 |
611
- | | `--wf-color-bg/bg-secondary/bg-tertiary` | 3 级背景色 |
612
- | | `--wf-color-border/border-light/border-dark` | 3 级边框色 |
613
- | 字体 | `--wf-font-sans`, `--wf-font-mono` | 字体族 |
614
- | 字号 | `--wf-font-size-xs/sm/base/lg/xl/2xl/3xl/4xl/5xl` | 9 级字号 |
615
- | 字重 | `--wf-font-weight-normal/medium/semibold/bold` | 4 级字重 |
616
- | 行高 | `--wf-line-height-tight/normal/relaxed` | 3 级行高 |
617
- | 字距 | `--wf-letter-spacing/wide/wider` | 3 级字符间距 |
618
- | 间距 | `--wf-space-xs/sm/md/lg/xl/2xl` | 8 级 margin/padding |
619
- | 间隔 | `--wf-gap-xs/sm/md/lg/xl/2xl` | 6 级 flex/grid gap |
620
- | 圆角 | `--wf-radius-sm/md/lg/xl` | 5 级 border-radius |
621
- | 阴影 | `--wf-shadow-sm/md/lg` | 4 级 box-shadow |
622
- | 边框 | `--wf-border-width` | 边框宽度 |
623
- | 聚焦 | `--wf-focus-ring` | 聚焦环(box-shadow)|
624
- | 动效 | `--wf-transition-duration/timing` | 过渡时长 + 曲线 |
625
- | 表单 | `--wf-accent-color`, `--wf-caret-color` | 控件主题色 + 光标色 |
626
- | 透明 | `--wf-opacity-disabled`, `--wf-opacity-overlay` | 禁用态 + 遮罩透明度 |
627
- | 层级 | `--wf-pop-z`, `--wf-cover-z` | z-index 层 |
1172
+ - 直接 DOM 渲染(不经过 VDOM)
1173
+ - 返回 `Promise<boolean>`
1174
+ - ESC / 点击遮罩 → resolve(false)
1175
+ - 自动锁定背景滚动
628
1176
 
629
- ### 暗色模式
1177
+ ---
630
1178
 
631
- 切换 `html` `data-theme` 属性即可自动切换全部主题色:
1179
+ ## ScrollLock / FocusTrap
632
1180
 
633
- ```ts
634
- document.documentElement.setAttribute('data-theme', 'dark')
635
- // 全部引用 var(--wf-*) 的元素自动变色
1181
+ ```tsx
1182
+ import { lockScroll, unlockScroll } from 'weifuwu/client'
1183
+ import { trapFocus } from 'weifuwu/client'
1184
+
1185
+ // 锁定/解锁滚动(支持嵌套计数)
1186
+ lockScroll()
1187
+ unlockScroll()
1188
+
1189
+ // 焦点陷阱 — 返回 cleanup 函数
1190
+ const cleanup = trapFocus(containerElement)
1191
+ cleanup() // 恢复之前的焦点
1192
+ ```
1193
+
1194
+ | API | 说明 |
1195
+ |-----|------|
1196
+ | `lockScroll()` | 锁定 body 滚动(iOS 兼容) |
1197
+ | `unlockScroll()` | 解锁滚动,恢复滚动位置 |
1198
+ | `trapFocus(el)` | Tab/Shift+Tab 在容器内循环,返回 cleanup |
1199
+
1200
+ ---
1201
+
1202
+ ## extendCtx — 上下文扩展
1203
+
1204
+ ```tsx
1205
+ import { extendCtx } from 'weifuwu/client'
1206
+
1207
+ // 在 AppMiddleware 中创建新 ctx,原 ctx getter 通过原型链继承
1208
+ function myMw(ctx: WfuiContext): WfuiContext {
1209
+ return extendCtx(ctx, { myField: 'value' })
1210
+ }
636
1211
  ```
637
1212
 
638
- ### 基础元素默认样式
1213
+ `extendCtx` 使用 `Object.create(ctx)` 保持原型链,再用 `Object.assign` 添加新字段。保证 getter 不被快照化。
639
1214
 
640
- 引入 weifuwu/layout 后,以下 HTML 元素自动绑定主题 Token:
1215
+ ---
1216
+
1217
+ ## 前端类型
1218
+
1219
+ ```tsx
1220
+ import type { VNode, VNodeType, Component, WfuiContext, AppMiddleware, RouteDef } from 'weifuwu/client'
1221
+ import type { ApiClient, ApiOptions, ApiRequestOptions, ApiError } from 'weifuwu/client'
1222
+ import type { AuthClient, AuthOptions } from 'weifuwu/client'
1223
+ import type { ErrorBoundaryProps } from 'weifuwu/client'
1224
+ import type { I18nOptions, I18nState, LocalePackage } from 'weifuwu/client'
1225
+ import type { ConfirmOptions, ConfirmState } from 'weifuwu/client'
1226
+ import type { RouterOptions } from 'weifuwu/client'
1227
+ ```
641
1228
 
642
- `body`, `h1`~`h6`, `p`, `a`, `label`, `small`, `input`, `textarea`, `select`, `button`, `table`, `th`, `td`, `hr`, `pre`, `code`
1229
+ | 类型 | 说明 |
1230
+ |------|------|
1231
+ | `VNode` | `{ type, props, key?, el?, _$?, _child?, _cleanup? }` |
1232
+ | `VNodeType` | `string \| Component \| typeof Fragment` |
1233
+ | `Component<P>` | `(props: P, ctx: WfuiContext) => VNode \| null` |
1234
+ | `WfuiContext` | `{ ui, route?, app?, ws?, api?, auth?, i18n?, confirm?, [key]: unknown }` |
1235
+ | `AppMiddleware` | `(ctx: WfuiContext) => WfuiContext` |
1236
+ | `RouteDef` | `{ path, component?, layout?, children?, auth?, title? }` |
1237
+ | `ApiClient` | `{ get, post, put, patch, delete }` |
1238
+ | `ApiError` | `class { status, body } extends Error` |
1239
+ | `AuthClient` | `{ token, user, isLoggedIn, login, logout, setUser, refresh }` |
1240
+ | `I18nOptions` | `{ locale?, messages?, components? }` |
1241
+ | `I18nState` | `{ locale, t, setLocale, components }` |
1242
+ | `ErrorBoundaryProps` | `{ fallback?, children? }` |
1243
+ | `ConfirmOptions` | `{ title?, confirmText?, cancelText?, variant? }` |
643
1244
 
644
1245
  ---
645
1246
 
646
- ## 组件库 `weifuwu/components`
1247
+ # 组件库 (`weifuwu/components`)
647
1248
 
648
- 29**HTML 原语**,覆盖 90% 的 SaaS 页面 HTML 需求。每个组件是 `(props, ctx) => VNode` 纯函数,引用 `weifuwu/layout` CSS 变量做主题。
1249
+ 37 个 HTML 原语组件。每个是 `(props, ctx) => VNode` 纯函数,引用 `--wf-*` CSS 变量做主题。
649
1250
 
650
1251
  ```ts
651
1252
  import { Button, Input, Table, Modal, Toast } from 'weifuwu/components'
652
1253
  import 'weifuwu/components/style.css'
653
1254
  ```
654
1255
 
655
- ### 模块总览
1256
+ ## 组件列表
1257
+
1258
+ ### 表单核心
1259
+
1260
+ | 组件 | 导入名 | 关键 Props | 说明 |
1261
+ |-----|--------|-----------|------|
1262
+ | Button | `Button` | `variant`, `size`, `loading`, `disabled`, `block`, `type` | 按钮 |
1263
+ | Input | `Input` | `variant`, `size`, `placeholder`, `disabled`, `error`, `prefix`, `suffix` | 输入框 |
1264
+ | Textarea | `Textarea` | `rows`, `resize`, `maxLength`, `error` | 文本域 |
1265
+ | Select | `Select` | `options: SelectOption[]`, `placeholder`, `searchable` | 下拉选择 |
1266
+
1267
+ ### 表单选择
1268
+
1269
+ | 组件 | 导入名 | 关键 Props | 说明 |
1270
+ |-----|--------|-----------|------|
1271
+ | Checkbox | `Checkbox` | `checked`, `label`, `indeterminate` | 复选框 |
1272
+ | Switch | `Switch` | `checked`, `size` | 开关 |
1273
+ | RadioGroup | `RadioGroup` | `options: RadioOption[]`, `value`, `name` | 单选组 |
1274
+ | Slider | `Slider` | `min`, `max`, `step`, `value`, `range` | 滑块 |
1275
+
1276
+ ### 表单增强
1277
+
1278
+ | 组件 | 导入名 | 关键 Props | 说明 |
1279
+ |-----|--------|-----------|------|
1280
+ | Form | `Form` | `onSubmit`, `validation` | 表单容器 |
1281
+ | Field | `Field` | `label`, `error`, `required`, `help` | 字段包装 |
1282
+ | FileUpload | `FileUpload` | `accept`, `multiple`, `maxSize`, `onFiles` | 文件上传 |
1283
+ | SearchInput | `SearchInput` | `value`, `placeholder`, `onSearch`, `loading` | 搜索框 |
1284
+ | ProgressBar | `ProgressBar` | `value`, `max`, `variant`, `size`, `label` | 进度条 |
1285
+
1286
+ ### 数据展示
1287
+
1288
+ | 组件 | 导入名 | 关键 Props | 说明 |
1289
+ |-----|--------|-----------|------|
1290
+ | Table | `Table` | `columns: TableColumn[]`, `data`, `loading`, `sortable`, `selectable` | 表格 |
1291
+ | Card | `Card` | `title`, `extra`, `shadow`, `padding` | 卡片 |
1292
+ | Badge | `Badge` | `variant: BadgeVariant`, `count`, `dot`, `max` | 徽标 |
1293
+ | Tag | `Tag` | `variant`, `closable`, `onClose` | 标签 |
1294
+ | Avatar | `Avatar` | `src`, `name`, `size`, `shape` | 头像 |
1295
+ | StatCard | `StatCard` | `title`, `value`, `trend`, `icon`, `variant` | 统计卡片 |
1296
+ | PageHeader | `PageHeader` | `title`, `subtitle`, `actions`, `onBack`, `breadcrumb` | 页面标题 |
1297
+ | Img | `Img` | `src`, `alt`, `fallback`, `lazy`, `fit` | 图片(含 fallback) |
1298
+
1299
+ ### 数据反馈
1300
+
1301
+ | 组件 | 导入名 | 关键 Props | 说明 |
1302
+ |-----|--------|-----------|------|
1303
+ | Modal | `Modal` | `open`, `title`, `onClose`, `width`, `footer`, `closable` | 模态框 |
1304
+ | Drawer | `Drawer` | `open`, `title`, `onClose`, `position: DrawerPosition`, `width` | 抽屉 |
1305
+ | Tooltip | `Tooltip` | `content`, `position: TooltipPosition`, `trigger` | 工具提示 |
1306
+ | Popover | `Popover` | `content`, `position: PopoverPosition`, `trigger` | 弹出层 |
1307
+ | Toast | `Toast` | `items: ToastItem[]`, `position`, `max` | 消息提示 |
1308
+ | Alert | `Alert` | `variant: AlertVariant`, `title`, `closable`, `icon` | 警告提示 |
1309
+ | Loading | `Loading` | `size`, `text`, `fullscreen` | 加载中 |
1310
+ | EmptyState | `EmptyState` | `title`, `description`, `action`, `icon` | 空状态 |
1311
+ | Skeleton | `Skeleton` | `variant: SkeletonVariant`, `rows`, `width`, `height` | 骨架屏 |
1312
+
1313
+ ### 导航组件
1314
+
1315
+ | 组件 | 导入名 | 关键 Props | 说明 |
1316
+ |-----|--------|-----------|------|
1317
+ | Breadcrumb | `Breadcrumb` | `items: BreadcrumbItem[]` | 面包屑 |
1318
+ | Tabs | `Tabs` | `items: TabItem[]`, `activeKey`, `onChange`, `type` | 标签页 |
1319
+ | Dropdown | `Dropdown` | `items: DropdownItem[]`, `trigger`, `placement` | 下拉菜单 |
1320
+ | Pagination | `Pagination` | `total`, `page`, `pageSize`, `onChange` | 分页 |
1321
+ | Steps | `Steps` | `items: StepItem[]`, `current`, `direction`, `size` | 步骤条 |
1322
+ | Accordion | `Accordion` | `items: AccordionItem[]`, `multiple`, `defaultActive` | 手风琴 |
1323
+
1324
+ ### 布局
1325
+
1326
+ | 组件 | 导入名 | 关键 Props | 说明 |
1327
+ |-----|--------|-----------|------|
1328
+ | Divider | `Divider` | `orientation`, `plain` | 分割线(水平/垂直/带文字) |
656
1329
 
657
- | 类别 | 组件 | 用途 |
658
- |------|------|------|
659
- | **表单核心** | `Button` `Input` `Textarea` `Select` | 4 个最常用的表单元素 |
660
- | **表单核心** | `InputNumber` | 数字输入,带自定义步进按钮 (`showStepper`) |
661
- | **表单选择** | `Checkbox` `Switch` `RadioGroup` `Slider` | 选择类输入 |
662
- | **表单增强** | `Form` `Field` `FileUpload` `SearchInput` `ProgressBar` | 文件上传、搜索、进度 |
663
- | **数据展示** | `Table` `Card` `Badge` `Tag` `Avatar` `StatCard` `PageHeader` | 数据展示与页面标题 |
664
- | **数据反馈** | `Modal` `Drawer` `Tooltip` `Toast` `Alert` `Loading` `EmptyState` | 弹窗、抽屉、提示 |
665
- | **导航组件** | `Breadcrumb` `Tabs` `Dropdown` `Pagination` `Steps` `Accordion` | 面包屑、标签页、分页 |
666
- | **布局** | `Divider` | 分割线 (水平/垂直/带文字) |
667
-
668
- ### 状态管理说明
1330
+ ---
669
1331
 
670
- `ctx.ui.$` 是**组件级**状态——每个组件实例有独立的 Proxy(基于 `vnode._$`),同名变量不会冲突:
1332
+ # 布局系统 (`weifuwu/layout`)
671
1333
 
672
- ```tsx
673
- // 组件 A
674
- const $ = ctx.ui.$
675
- $.open = true // 只影响组件 A
1334
+ 纯 CSS 布局原语 + 72 个主题 Token。不绑定任何 JS 框架。
676
1335
 
677
- // 组件 B(在同一页面)
678
- const $ = ctx.ui.$
679
- $.open = false // 只影响组件 B,不影响 A
1336
+ ```html
1337
+ <link rel="stylesheet" href="/node_modules/weifuwu/dist/layout/weifuwu-layout.css">
680
1338
  ```
681
1339
 
682
- 跨组件共享状态请使用 `ctx` 直接挂载属性(延续中间件模式):
1340
+ 或通过 `ctx.ui.css` 服务:
683
1341
 
684
1342
  ```ts
685
- ctx.theme = 'dark' // 所有组件可读
686
- ctx.toast?.success('成功') // 如已注入 toast 中间件
1343
+ app.get('/layout.css', (req, ctx) => ctx.ui.css('./node_modules/weifuwu/dist/layout/weifuwu-layout.css'))
687
1344
  ```
688
1345
 
689
- ### 页面模板 — `docs/pages/`
1346
+ ## 35 个布局原语
690
1347
 
691
- | 模板 | 文件 | 用途 |
1348
+ | 类别 | 原语 | 效果 |
692
1349
  |------|------|------|
693
- | **列表页** | `docs/pages/list-page.md` | 搜索 + 表格 + 分页 + 加载/空/错误状态 |
694
- | **表单页** | `docs/pages/form-page.md` | 表单 + 字段 + 校验 + 提交 |
695
- | **详情页** | `docs/pages/detail-page.md` | 信息展示 + Tabs + 操作 |
696
- | **设置页** | `docs/pages/settings-page.md` | 分组设置 + 独立保存 |
697
- | **仪表盘** | `docs/pages/dashboard-page.md` | KPI 卡片 + 图表 + 列表 |
698
- | **认证页** | `docs/pages/auth-page.md` | 居中卡片 + 表单 + 错误提示 |
699
- | **应用壳** | `docs/pages/app-layout.md` | 侧边栏 + 导航菜单 + 认证守卫 |
700
-
701
- 每个模板标注了「改这里」——复制代码后改 API 路径、字段定义、操作按钮、导航项即可使用。
702
-
703
- ---
704
-
705
- ## 环境变量
706
-
707
- | 变量 | 用途 | 默认值 |
708
- |------|------|--------|
709
- | `DATABASE_URL` | PostgreSQL 连接字符串 | — |
710
- | `REDIS_URL` | Redis 连接字符串 | — |
711
-
712
- ---
713
-
714
- ## 项目结构
715
-
716
- ```
717
- src/
718
- ├── index.ts # 统一导出
719
- ├── types.ts # 后端类型
720
- ├── request.ts # 请求解析
721
- ├── response.ts # 响应工具
722
- ├── core/
723
- │ ├── router.ts # HTTP 路由
724
- │ ├── serve.ts # HTTP 服务器
725
- │ └── ws.ts # WebSocket
726
- ├── middleware/
727
- │ ├── cors.ts
728
- │ └── static.ts
729
- ├── postgres/
730
- ├── redis/
731
- ├── graphql.ts
732
- ├── ui/ # SSR + JS/CSS 编译
733
- ├── client/ # 前端 VDOM 框架
734
- │ ├── index.ts
735
- │ ├── vnode.ts
736
- │ ├── app.ts
737
- │ ├── render.ts
738
- │ ├── router.ts
739
- │ ├── types.ts
740
- │ ├── error-boundary.ts
741
- │ └── middleware/
742
- │ ├── api.ts
743
- │ ├── auth.ts
744
- │ └── ws.ts
745
- ├── components/ # 29 个 HTML 原语组件
746
- │ ├── index.ts
747
- │ ├── Button/ # Button.ts + .css + .test.ts
748
- │ ├── Input/
749
- │ ├── ...
750
- │ └── PageHeader/
751
- └── layout/ # 纯 CSS 布局 + 主题
752
- ├── weifuwu-layout.css
753
- ├── _tokens.css
754
- ├── _dark.css
755
- ├── _base.css
756
- └── _*.css # 33 个原语
757
- ```
758
-
759
- ---
760
-
761
- ## 开发
1350
+ | **排列** | `wf-stack` | 纵向 flex + gap |
1351
+ | | `wf-stack-reverse` | 纵向反向 |
1352
+ | | `wf-row` | 横向 flex + wrap + gap |
1353
+ | | `wf-row-reverse` | 横向反向 |
1354
+ | | `wf-nowrap` | flex-wrap: nowrap |
1355
+ | | `wf-cluster` | 换行居中簇 |
1356
+ | **分布** | `wf-split` | justify-content: space-between |
1357
+ | | `wf-center` | 双轴居中 |
1358
+ | | `wf-right` | justify-content: flex-end |
1359
+ | | `wf-around` | space-around |
1360
+ | | `wf-evenly` | space-evenly |
1361
+ | **对齐** | `wf-top` | align-items: flex-start |
1362
+ | | `wf-bottom` | align-items: flex-end |
1363
+ | | `wf-stretch` | align-items: stretch |
1364
+ | **弹性** | `wf-fill` | flex: 1 + min-width: 0 |
1365
+ | | `wf-fixed` | flex: none |
1366
+ | | `wf-auto` | flex: auto |
1367
+ | | `wf-shrink` | min-width/height: 0 |
1368
+ | **Z轴** | `wf-cover` | position: fixed + inset: 0 |
1369
+ | | `wf-pop` | position: absolute |
1370
+ | | `wf-anchor` | position: relative |
1371
+ | | `wf-layer` | position: relative + z-index |
1372
+ | | `wf-sticky` | position: sticky |
1373
+ | **容器** | `wf-surface` | 基础面(border-radius + shadow + bg) |
1374
+ | | `wf-grid` | display: grid + --wf-cols |
1375
+ | | `wf-container` | max-width + margin: auto |
1376
+ | | `wf-scroll` | overflow: auto |
1377
+ | | `wf-clip` | overflow: hidden |
1378
+ | **显隐** | `wf-hidden` | display: none |
1379
+ | | `wf-block` | display: block |
1380
+ | | `wf-inline` | display: inline |
1381
+ | | `wf-inline-block` | display: inline-block |
1382
+ | | `wf-contents` | display: contents |
1383
+
1384
+ ## 72 个主题 Token
1385
+
1386
+ ```css
1387
+ /* 品牌色 */
1388
+ --wf-color-primary / --wf-color-primary-hover / --wf-color-primary-bg
1389
+ --wf-color-secondary / --wf-color-secondary-bg
1390
+
1391
+ /* 语义色 */
1392
+ --wf-color-success / --wf-color-success-bg
1393
+ --wf-color-warning / --wf-color-warning-bg
1394
+ --wf-color-error / --wf-color-error-bg
1395
+ --wf-color-info / --wf-color-info-bg
1396
+
1397
+ /* 文字色 */
1398
+ --wf-color-text / --wf-color-text-secondary / --wf-color-text-tertiary / --wf-color-text-disabled
1399
+
1400
+ /* 背景色 */
1401
+ --wf-color-bg / --wf-color-bg-secondary / --wf-color-bg-tertiary
1402
+
1403
+ /* 边框色 */
1404
+ --wf-color-border / --wf-color-border-light / --wf-color-border-dark
1405
+
1406
+ /* 字体 */
1407
+ --wf-font-sans / --wf-font-mono
1408
+
1409
+ /* 字号: xs sm base lg xl 2xl 3xl 4xl 5xl */
1410
+ --wf-font-size-*
1411
+
1412
+ /* 字重: normal medium semibold bold */
1413
+ --wf-font-weight-*
1414
+
1415
+ /* 行高: tight normal relaxed */
1416
+ --wf-line-height-*
1417
+
1418
+ /* 字距: normal wide wider */
1419
+ --wf-letter-spacing-*
1420
+
1421
+ /* 间距: xs sm md lg xl 2xl */
1422
+ --wf-space-*
1423
+
1424
+ /* 间隔: xs sm md lg xl 2xl */
1425
+ --wf-gap-*
1426
+
1427
+ /* 圆角: sm md lg xl */
1428
+ --wf-radius-*
1429
+
1430
+ /* 阴影: sm md lg */
1431
+ --wf-shadow-*
1432
+
1433
+ /* 其他 */
1434
+ --wf-border-width / --wf-focus-ring
1435
+ --wf-transition-duration / --wf-transition-timing
1436
+ --wf-accent-color / --wf-caret-color
1437
+ --wf-opacity-disabled / --wf-opacity-overlay
1438
+ --wf-pop-z / --wf-cover-z
1439
+ ```
762
1440
 
763
- ```bash
764
- # 构建
765
- npm run build
1441
+ ### 暗色模式
1442
+
1443
+ ```ts
1444
+ document.documentElement.setAttribute('data-theme', 'dark')
1445
+ // 所有 var(--wf-*) 自动切换
1446
+ ```
766
1447
 
767
- # 类型检查
768
- npm run typecheck
1448
+ ---
1449
+
1450
+ # 核心概念
769
1451
 
770
- # 测试
771
- npm test
1452
+ ## 中间件模式(前后端一致)
772
1453
 
773
- # 发布
774
- node scripts/release.mjs <version>
1454
+ ```
1455
+ 后端: app.use(cors())
1456
+ app.use(postgres())
1457
+ app.get('/users', (req, ctx) => { ctx.sql`SELECT *` })
1458
+ // ctx 已注入 ctx.sql
1459
+
1460
+ 前端: createApp()
1461
+ .use(api({ baseURL: '/api' }))
1462
+ .use(auth())
1463
+ .mount('#root', App)
1464
+ // ctx 已注入 ctx.api, ctx.auth
775
1465
  ```
776
1466
 
777
- ## 设计原则
1467
+ ## 状态管理
1468
+
1469
+ | 模式 | 后端 | 前端 |
1470
+ |------|------|------|
1471
+ | 注入 | 中间件注入 ctx.field | 中间件注入 ctx.field |
1472
+ | 读取 | handler 读取 ctx | 组件读取 ctx |
1473
+ | 渲染 | 返回 Response | Proxy 自动触发 VDOM patch |
1474
+
1475
+ ## Closeable 接口
1476
+
1477
+ 所有有状态模块(postgres、redis)实现 `close(): Promise<void>`,serve 关闭时自动调用。
1478
+
1479
+ ---
778
1480
 
779
- - **后端为工具箱** —— 提供 HTTP 路由、数据库、中间件原语,不捆绑业务模块
780
- - **全栈单包** —— `npm install weifuwu` = 后端 + 前端 + 布局
781
- - **Web 标准优先** —— 所有 handler 使用 `(req: Request, ctx: Context) => Response`
782
- - **零外部依赖** —— 前端和布局没有任何 npm 运行时依赖
783
- - **LLM 友好** —— 模块总览表 + 一致格式 + 清晰依赖链
1481
+ # 环境变量
1482
+
1483
+ | 变量 | 用途 | 模块 |
1484
+ |------|------|------|
1485
+ | `DATABASE_URL` | PostgreSQL 连接字符串 | `postgres()` |
1486
+ | `REDIS_URL` | Redis 连接字符串 | `redis()` |
1487
+
1488
+ ---
1489
+
1490
+ # 开发命令
1491
+
1492
+ ```bash
1493
+ npm run build # 构建 dist/
1494
+ npm run typecheck # TypeScript 类型检查
1495
+ npm test # 运行 node --test
1496
+ node scripts/release.mjs <version> # 发布
1497
+ ```
1498
+
1499
+ ```bash
1500
+ # 测试前启动依赖服务
1501
+ docker compose up -d
1502
+ ```