weifuwu 0.33.10 → 0.34.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 CHANGED
@@ -1,119 +1,270 @@
1
1
  # weifuwu
2
2
 
3
- **Web framework + reactive frontend** — `(req, ctx) => Response` + `(props, ctx) => JSX`
3
+ **全栈框架 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => JSX`**
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
- One package. Backend (`weifuwu`) + frontend (`weifuwu/client`). Minimal, composable, no magic.
9
+ 一个包,无上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架。
10
10
 
11
11
  ---
12
12
 
13
- ## Core Concept: `ctx`
13
+ ## 模块总览
14
+
15
+ | 模块 | 导出 | 用途 | 依赖 |
16
+ |------|------|------|------|
17
+ | **Router** | `Router` | HTTP 路由 + 中间件链 + WebSocket + GraphQL | — |
18
+ | **serve** | `serve` | HTTP 服务器 | `Router` |
19
+ | **cors** | `cors` | CORS 跨域中间件 | `Router` |
20
+ | **serveStatic** | `serveStatic` | 静态文件服务 | `Router` |
21
+ | **postgres** | `postgres` | PostgreSQL 客户端 → `ctx.sql` | `Router` |
22
+ | **redis** | `redis` | Redis 客户端 → `ctx.redis` | `Router` |
23
+ | **ui** | `ui` | SSR 渲染 + 动态 JS 编译 → `ctx.ui.html/css/js` | `Router` |
24
+ | **graphql** | `router.graphql()` | GraphQL 端点 | `Router` |
25
+ | **client** | — | 前端 VDOM + Proxy 框架 | — |
26
+
27
+ **前端 `weifuwu/client` 模块总览:**
28
+
29
+ | 类别 | 导出 | 用途 |
30
+ |------|------|------|
31
+ | **渲染引擎** | `VNode`, `Component` | 虚拟 DOM 节点与组件类型 | — |
32
+ | **JSX 运行时** | `jsx`/`jsxs`/`jsxDEV`, `Fragment` | TSX 编译目标 | — |
33
+ | **应用** | `createApp` | 中间件链 + 挂载 | — |
34
+ | **路由** | `router`, `RouteView` | 嵌套布局路由 | — |
35
+ | **中间件** | `ws`, `api`, `auth` | WebSocket / HTTP 客户端 / 认证状态 | — |
36
+ | **工具** | `extendCtx` | ctx 扩展 | — |
37
+ | **类型** | `WfuiContext`, `AppMiddleware`, `RouteDef`, `VNodeType`, `Component`, `ApiClient`, `AuthClient` | — | — |
14
38
 
15
- Backend and frontend share the same pattern: middleware injects fields into `ctx`, handlers/components read from `ctx`.
39
+ ---
40
+
41
+ ## 核心理念:`ctx`
42
+
43
+ 前后端共享同一模式:**中间件向 `ctx` 注入字段,handler/组件从 `ctx` 读取。**
16
44
 
17
45
  ```
18
- Backend: Frontend:
19
- Request → Middleware → Handler createApp() → Middleware → Component
20
-
21
-
22
- ctx.sql ctx.ws
23
- ctx.redis ctx.route
24
- ctx.ui ctx.app.navigate
46
+ 后端: 前端:
47
+ Request → Middleware → Handler createApp() → Middleware → Component
48
+
49
+
50
+ ctx.sql ctx.ws
51
+ ctx.redis ctx.route
52
+ ctx.ui ctx.api / ctx.auth
25
53
  ```
26
54
 
27
- **Backend:**
55
+ ---
56
+
57
+ ## 快速开始 — 全栈应用
58
+
28
59
  ```ts
29
- app.use(postgres()) // ctx.sql
30
- app.use(redis()) // ctx.redis
31
- app.use(ui()) // → ctx.ui.html / ctx.ui.js / ctx.ui.css
60
+ // server.ts — 同一个 npm 包
61
+ import { serve, Router, cors, ui } from 'weifuwu'
62
+
63
+ const app = new Router()
64
+ app.use(cors())
65
+ app.use(ui())
66
+
67
+ // REST API
68
+ app.get('/api/posts', async (req, ctx) => {
69
+ const posts = [{ id: 1, title: 'Hello' }]
70
+ return Response.json(posts)
71
+ })
72
+
73
+ // WebSocket
74
+ app.ws('/ws', {
75
+ open(ws) { ws.send(JSON.stringify({ type: 'system', body: 'connected' })) },
76
+ message(ws, ctx, data) {
77
+ const msg = JSON.parse(data.toString())
78
+ ws.send(JSON.stringify({ type: 'echo', body: msg.body }))
79
+ },
80
+ })
81
+
82
+ // SPA 入口 — 动态编译前端(零构建步骤)
83
+ app.get('/', async (req, ctx) => ctx.ui.html`
84
+ <!DOCTYPE html>
85
+ <html><body><div id="root"></div>
86
+ <script src="/static/app.js"></script></body></html>
87
+ `)
88
+ app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
89
+
90
+ serve(app, { port: 3000 })
32
91
  ```
33
92
 
34
- **Frontend:**
35
93
  ```tsx
36
- app.use(ws()) // → ctx.ws.send / onMessage / isConnected
37
- app.use(router({ routes })) // ctx.route.path / params / query
94
+ // src/main.tsx 前端
95
+ import { createApp, router, RouteView, ws, api, auth } from 'weifuwu/client'
96
+ import type { WfuiContext, RouteDef } from 'weifuwu/client'
97
+
98
+ // 组件 = (props, ctx) => VNode
99
+ function Home(_props: {}, ctx: WfuiContext) {
100
+ const $ = ctx.ui.$
101
+ if (!ctx.ui.ready) { $.items = [{ id: 1, text: 'hello' }] }
102
+ return <div>{$.items.map((i: any) => <div key={i.id}>{i.text}</div>)}</div>
103
+ }
104
+
105
+ const app = createApp()
106
+ app.use(api({ baseURL: '' }))
107
+ app.use(auth())
108
+ app.use(ws())
109
+ app.use(router({
110
+ routes: [
111
+ { path: '/', component: Home },
112
+ {
113
+ path: '/dashboard',
114
+ layout: DashboardLayout,
115
+ ],
116
+ },
117
+ ],
118
+ mode: 'hash',
119
+ scrollRestoration: true,
120
+ }))
121
+ app.mount('#root', AppShell)
122
+
123
+ function AppShell(_props: {}, ctx: WfuiContext) {
124
+ return (
125
+ <div>
126
+ <nav>{/* ... */}</nav>
127
+ <main><RouteView /></main>
128
+ </div>
129
+ )
130
+ }
38
131
  ```
39
132
 
40
133
  ---
41
134
 
42
- ## Backend
135
+ ## 后端
43
136
 
44
- ### Exports
137
+ ### Router
45
138
 
46
- | Export | Type | Purpose |
47
- |--------|------|---------|
48
- | `Router` | class | HTTP router + middleware chain + `.ws()` + `.graphql()` |
49
- | `serve` | function | HTTP server |
50
- | `cors` | middleware | CORS headers |
51
- | `postgres` | middleware | PostgreSQL client → `ctx.sql` |
52
- | `redis` | middleware | Redis client → `ctx.redis` |
53
- | `serveStatic` | middleware | Static file serving |
54
- | `ui` | middleware | SSR/SPA rendering → `ctx.ui.html/css/js` |
55
- | `HttpError` | class | HTTP error with status code |
56
- | `DEFAULT_MAX_BODY` | constant | Default 10MB body limit |
57
- | `MIGRATIONS_TABLE` | constant | Postgres migrations table name |
139
+ ```ts
140
+ const app = new Router()
58
141
 
59
- ### Types
142
+ // HTTP 方法
143
+ app.get(path, ...handlers)
144
+ app.post / put / delete / patch / head / options(path, ...handlers)
145
+ app.all(path, ...handlers)
60
146
 
61
- `Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
147
+ // WebSocket / GraphQL
148
+ app.ws(path, handler)
149
+ app.graphql('/graphql', handler)
62
150
 
63
- ### Quick Start
151
+ // 中间件
152
+ app.use(middleware)
153
+ app.mount(prefix, subRouter)
154
+ app.onError(handler)
64
155
 
65
- ```ts
66
- import { serve, Router, cors, serveStatic, ui } from 'weifuwu'
156
+ // 调试
157
+ app.routes() // 列出所有路由
158
+ ```
67
159
 
68
- const app = new Router()
69
- app.use(cors())
70
- app.use(serveStatic('./public'))
71
- app.use(ui())
160
+ | 方法 | 参数 | 说明 |
161
+ |------|------|------|
162
+ | `get/post/put/delete/patch/head/options` | `(path, ...handlers)` | 注册 HTTP 路由 |
163
+ | `all` | `(path, ...handlers)` | 匹配所有方法 |
164
+ | `ws` | `(path, handler)` | WebSocket 端点 |
165
+ | `graphql` | `('/path', handler)` | GraphQL 端点 |
166
+ | `use` | `(middleware)` | 全局中间件 |
167
+ | `mount` | `(prefix, subRouter)` | 子路由挂载 |
168
+ | `onError` | `(handler)` | 全局错误处理 |
169
+ | `routes` | `()` | 返回路由列表数组 |
72
170
 
73
- // API routes
74
- app.get('/api/hello', async (req, ctx) => {
75
- return Response.json({ message: 'hello' })
76
- })
171
+ ### serve — HTTP 服务器
77
172
 
78
- // WebSocket
79
- app.ws('/ws', {
80
- open(ws) { ws.send('connected') },
81
- message(ws, ctx, data) { ws.send(data.toString()) },
82
- })
173
+ ```ts
174
+ const srv = serve(app, { port: 3000 })
175
+ await srv.stop() // 程序化停止
176
+ // Ctrl+C / SIGTERM 自动关闭所有连接后退出
177
+ ```
83
178
 
84
- // GraphQL
85
- app.graphql(async (req, ctx) => ({
86
- schema: `type Query { hello: String }`,
87
- resolvers: { Query: { hello: () => 'world' } },
88
- graphiql: true,
179
+ | 选项 | 默认 | 说明 |
180
+ |------|------|------|
181
+ | `port` | `0` | 监听端口 |
182
+ | `hostname` | `'0.0.0.0'` | 监听地址 |
183
+ | `maxBodySize` | `10MB` | 请求体上限 |
184
+ | `timeout` | `30000` | Socket 超时 (ms) |
185
+ | `shutdown` | `true` | 是否注册 SIGTERM/SIGINT 处理 |
186
+
187
+ ### cors — CORS 中间件
188
+
189
+ ```ts
190
+ app.use(cors({
191
+ origin: ['https://example.com'],
192
+ credentials: true,
89
193
  }))
194
+ ```
195
+
196
+ ### serveStatic — 静态文件
197
+
198
+ ```ts
199
+ app.use(serveStatic('./public', { prefix: '/assets' }))
200
+ ```
201
+
202
+ | 选项 | 默认 | 说明 |
203
+ |------|------|------|
204
+ | `prefix` | `''` | URL 前缀 |
205
+ | `index` | `'index.html'` | 默认首页 |
206
+
207
+ ### postgres — PostgreSQL
90
208
 
91
- // SSR page
209
+ ```ts
210
+ app.use(postgres())
211
+ // → ctx.sql`SELECT * FROM users`
212
+
213
+ app.use(postgres({ url: 'postgres://user:pass@host:5432/db' }))
214
+ // 默认读取 DATABASE_URL 环境变量
215
+ ```
216
+
217
+ | 选项 | 默认 | 说明 |
218
+ |------|------|------|
219
+ | `url` | `DATABASE_URL` | 连接字符串 |
220
+ | `migrations` | `'./migrations'` | 迁移文件目录 |
221
+
222
+ 依赖:需要 `postgres` npm 包。实现 `close(): Promise<void>`。
223
+
224
+ ### redis — Redis
225
+
226
+ ```ts
227
+ app.use(redis())
228
+ // → ctx.redis.get('key')
229
+ // → ctx.redis.set('key', 'value')
230
+ // 默认读取 REDIS_URL 环境变量
231
+ ```
232
+
233
+ 依赖:需要 `ioredis` npm 包。实现 `close(): Promise<void>`。
234
+
235
+ ### ui — SSR + SPA 渲染
236
+
237
+ ```ts
238
+ app.use(ui())
239
+
240
+ // SSR 页面
92
241
  app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
93
242
  <!DOCTYPE html>
94
243
  <html><body><h1>${post.title}</h1></body></html>
95
244
  `)
96
245
 
97
- // Dynamic JS compilation (no build step)
246
+ // 动态 JS 编译(esbuild,零构建步骤)
98
247
  app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
99
248
 
100
- serve(app, { port: 3000 })
249
+ // CSS 编译(PostCSS + Tailwind)
250
+ app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
101
251
  ```
102
252
 
103
- ### Router
253
+ | 方法 | 用途 |
254
+ |------|------|
255
+ | `ctx.ui.html\`...\`` | 渲染 HTML 模板(转义变量防 XSS) |
256
+ | `ctx.ui.html.unsafe(str)` | 插入原始 HTML |
257
+ | `ctx.ui.js(entryPath)` | 动态编译 TSX → JS bundle |
258
+ | `ctx.ui.css(entryPath)` | 编译 CSS (PostCSS + Tailwind) |
259
+
260
+ ### graphql — GraphQL
104
261
 
105
262
  ```ts
106
- const app = new Router()
107
- app.get(path, ...handlers)
108
- app.post / put / delete / patch / head / options(path, ...handlers)
109
- app.all(path, ...handlers)
110
- app.ws(path, handler) // WebSocket
111
- app.graphql(handler) // GraphQL at /
112
- app.graphql('/graphql', handler) // GraphQL at /graphql
113
- app.use(middleware) // Global middleware
114
- app.mount(prefix, subRouter) // Sub-router
115
- app.onError(handler) // Error handler
116
- app.routes() // Debug: list all routes
263
+ app.graphql(async (req, ctx) => ({
264
+ schema: `type Query { hello: String }`,
265
+ resolvers: { Query: { hello: () => 'world' } },
266
+ graphiql: true,
267
+ }))
117
268
  ```
118
269
 
119
270
  ### WebSocket
@@ -127,143 +278,503 @@ app.ws('/ws', {
127
278
  })
128
279
  ```
129
280
 
130
- ### GraphQL
281
+ ### 错误处理
131
282
 
132
283
  ```ts
133
- // At root
134
- app.graphql(async (req, ctx) => ({
135
- schema: `type Query { hello: String }`,
136
- resolvers: { Query: { hello: () => 'world' } },
137
- graphiql: true,
138
- }))
284
+ app.onError((err, req, ctx) => {
285
+ if (err instanceof HttpError) {
286
+ return new Response(err.message, { status: err.status })
287
+ }
288
+ console.error(err)
289
+ return new Response('Internal Server Error', { status: 500 })
290
+ })
291
+ ```
139
292
 
140
- // Or at a custom path
141
- app.graphql('/graphql', handler)
293
+ | 类/常量 | 说明 |
294
+ |---------|------|
295
+ | `HttpError` | HTTP 错误 `new HttpError(msg, status)` |
296
+ | `DEFAULT_MAX_BODY` | 默认请求体上限 10MB |
297
+ | `MIGRATIONS_TABLE` | Postgres 迁移表名 |
298
+
299
+ ### 后端类型
300
+
301
+ `Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
302
+
303
+ ---
304
+
305
+ ## 前端 (`weifuwu/client`)
306
+
307
+ **2750 行源码,28 个运行时导出 + 19 个类型,零外部依赖。**
308
+
309
+ 构建配置(esbuild):
310
+ ```js
311
+ esbuild.build({
312
+ jsx: 'automatic',
313
+ jsxImportSource: 'weifuwu/client',
314
+ bundle: true,
315
+ })
142
316
  ```
143
317
 
144
- ### Graceful Shutdown
318
+ ### 状态管理
145
319
 
146
- ```ts
147
- const srv = serve(app)
148
- // Ctrl+C / SIGTERM immediately closes all connections and exits
149
- await srv.stop() // Programmatic stop
320
+ ```tsx
321
+ const $ = ctx.ui.$ // 获取状态对象
322
+
323
+ $.count = 0 // 赋值 → 自动渲染
324
+ $.items = [...$.items, newItem] // 不可变更新
325
+ $.items.push(newItem); ctx.ui.dirty() // 可变更新需显式 dirty
326
+
327
+ // 派生值 — 每次 render 重新计算
328
+ const doubled = $.count * 2
329
+ const completed = $.todos.filter(t => t.done)
150
330
  ```
151
331
 
152
- ---
332
+ | API | 说明 |
333
+ |------|------|
334
+ | `ctx.ui.$` | 状态代理对象,赋值自动触发渲染 |
335
+ | `ctx.ui.dirty()` | 显式标记脏状态(嵌套突变后用) |
336
+ | `ctx.ui.render()` | 同步立即渲染 |
337
+
338
+ **原理**:`ctx.ui.$` 是 Proxy 对象,任何属性赋值(`$.x = val`)自动调用 `queueMicrotask(render)`。\
339
+ 多次赋值在同一个微任务内合并为一次渲染。
153
340
 
154
- ## Frontend (`weifuwu/client`)
341
+ ### JSX 运行时
155
342
 
156
- 16 runtime exports, zero dependencies, zero virtual DOM.
343
+ ```tsx
344
+ // 自动由 esbuild 调用(无需手动导入)
345
+ <div class="foo">hello</div>
346
+ <Fragment>...</Fragment>
347
+ ```
157
348
 
158
- | Export | Type | Purpose |
159
- |--------|------|---------|
160
- | `signal`, `computed`, `effect`, `batch` | function | Reactive state system |
161
- | `jsx`/`jsxs`/`jsxDEV` | function | JSX compilation target |
162
- | `Fragment` | component | `<></>` |
163
- | `Show` | component | Conditional rendering |
164
- | `For` | component | Keyed list rendering |
165
- | `onMount`, `onCleanup` | function | Lifecycle hooks |
166
- | `createApp` | function | App instance with middleware chain |
167
- | `router` | middleware | Hash/history router → `ctx.route` |
168
- | `RouteView` | component | Route outlet |
169
- | `ws` | middleware | WebSocket client → `ctx.ws` |
349
+ | 导出 | 说明 |
350
+ |------|------|
351
+ | `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
352
+ | `Fragment` | `<></>` 片段组件 |
170
353
 
171
- Types: `Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`
354
+ **Signal 属性自动绑定:** `<input value={signalVal} />` — 信号变化时只更新对应 DOM 属性。
172
355
 
173
- ### Quick Start
356
+ ### 条件与列表
357
+
358
+ 使用原生 JS 表达式,不需要框架 API:
174
359
 
175
360
  ```tsx
176
- import { signal, Show, For, createApp, ws, router, RouteView } from 'weifuwu/client'
177
- import type { WfuiContext, RouteDef } from 'weifuwu/client'
361
+ // 条件
362
+ {$.loading && <Loading />}
363
+ {$.error ? <Error msg={$.error} /> : <Content />}
364
+
365
+ // 列表
366
+ {$.items.map(item => (
367
+ <div key={item.id}>{item.name}</div>
368
+ ))}
369
+ ```
370
+
371
+ | 模式 | 说明 |
372
+ |------|------|
373
+ | `{cond && <A/>}` | 条件渲染(cond 为 true 时渲染 A) |
374
+ | `{cond ? <A/> : <B/>}` | 二选一 |
375
+ | `{arr.map(x => <div key={x.id}/>)}` | 列表渲染,必须加 `key` |
376
+
377
+ ### 生命周期
378
+
379
+ 使用 `ref` 回调替代生命周期钩子:
380
+
381
+ ```tsx
382
+ <div ref={el => {
383
+ if (el) {
384
+ // 挂载:el 是 DOM 元素
385
+ fetchData()
386
+ el.addEventListener('scroll', handler)
387
+ }
388
+ if (!el) {
389
+ // 卸载:el 为 null
390
+ cleanup()
391
+ }
392
+ }} />
393
+ ```
394
+
395
+ `ref` 在挂载时传入 DOM 元素,卸载时传入 `null`。一个回调覆盖 mount/unmount 两个场景。
396
+
397
+ ### 应用
398
+
399
+ ```tsx
400
+ const app = createApp()
401
+ app.use(middleware1)
402
+ app.use(middleware2)
403
+ await app.mount('#root', AppShell)
404
+ ```
405
+
406
+ | 方法 | 说明 |
407
+ |------|------|
408
+ | `use(mw)` | 注册中间件,返回 `this` 支持链式 |
409
+ | `mount(selector, RootComponent)` | 挂载到 DOM |
410
+ | `hydrate(selector, Component, props?)` | 在 SSR 内容上附加组件 |
411
+ | `ctx` | 当前上下文 |
412
+
413
+ ### 路由
178
414
 
415
+ ```tsx
416
+ // 路由定义
179
417
  const routes: RouteDef[] = [
180
418
  { path: '/', component: HomePage },
181
- { path: '/hello/:name', component: HelloPage },
419
+ {
420
+ path: '/dashboard',
421
+ layout: DashboardLayout, // 嵌套布局
422
+ children: [
423
+ { path: '/overview', component: Overview }, // 子路由
424
+ { path: '/settings', component: Settings },
425
+ ],
426
+ },
427
+ { path: '/user/:id', component: UserPage, title: '用户' },
182
428
  ]
183
429
 
184
- const app = createApp()
185
- app.use(ws())
186
- app.use(router({ routes, mode: 'hash' }))
187
- app.mount('#root', AppShell)
430
+ // 注册路由中间件
431
+ app.use(router({
432
+ routes,
433
+ notFound: NotFound,
434
+ mode: 'hash', // 'hash' | 'history'
435
+ scrollRestoration: true,
436
+ }))
188
437
 
189
- function AppShell(_props: {}, ctx: WfuiContext) {
438
+ // 路由出口 根层级和嵌套层级用同一个组件
439
+ function AppShell() {
440
+ return <main><RouteView /></main> // 根出口
441
+ }
442
+ function DashboardLayout() {
190
443
  return (
191
- <div>
192
- <nav>...</nav>
193
- <main><RouteView /></main>
444
+ <div class="flex">
445
+ <Sidebar />
446
+ <main><RouteView /></main> // 嵌套出口(同一组件)
194
447
  </div>
195
448
  )
196
449
  }
197
450
  ```
198
451
 
199
- ### Signal
452
+ | RouteDef 字段 | 类型 | 说明 |
453
+ |---------------|------|------|
454
+ | `path` | `string` | 路由路径,支持 `/:param` |
455
+ | `component` | `Component` | 路由组件 |
456
+ | `layout` | `Component` | 嵌套布局(渲染 `<RouteView />` 显示子路由)|
457
+ | `children` | `RouteDef[]` | 子路由(与 layout 配合使用)|
458
+ | `auth` | `boolean` | 是否需要登录 |
459
+ | `title` | `string` | 页面标题(自动设置 `document.title`)|
460
+ | `loader` | `(ctx) => Promise<data>` | 数据预取 → `ctx.route.data` |
461
+ | `transition` | `string` | 页面切换过渡动画 CSS class 前缀 |
462
+
463
+ | RouterOptions | 默认 | 说明 |
464
+ |---------------|------|------|
465
+ | `mode` | `'hash'` | 路由模式 |
466
+ | `notFound` | — | 404 组件 |
467
+ | `scrollRestoration` | `true` | 历史模式时恢复滚动位置 |
468
+ | `transition` | — | 全局过渡动画 |
469
+
470
+ **`ctx.route` 注入:**
200
471
 
201
472
  ```tsx
202
- const count = signal(0)
203
- const doubled = computed(() => count.value * 2)
204
- effect(() => console.log('count:', count.value))
205
- batch(() => { a.value = 1; b.value = 2 })
473
+ ctx.route.path // '/user/42'
474
+ ctx.route.params // { id: '42' }
475
+ ctx.route.query // { tab: 'profile' }
476
+ ctx.route.component // 当前路由组件
477
+ ctx.route.data // loader 返回的数据
478
+ ctx.route.loading // loader 是否加载中
479
+ ctx.app.navigate('/path')
206
480
  ```
207
481
 
208
- ### Control Flow
482
+ ### 代码分割
209
483
 
210
484
  ```tsx
211
- <Show when={isLoggedIn} fallback={<Login />}>
212
- <Dashboard />
213
- </Show>
485
+ const AdminPage = lazy(() => import('./pages/AdminPage'), {
486
+ fallback: () => <div>加载中...</div>,
487
+ })
214
488
 
215
- <For each={items} keyBy="id">
216
- {(item) => <div>{item.name}</div>}
217
- </For>
489
+ const routes = [
490
+ { path: '/admin', component: AdminPage },
491
+ ]
218
492
  ```
219
493
 
220
- ### WebSocket (`ctx.ws`)
494
+ esbuild `splitting: true` + `outdir`。
495
+
496
+ ### 中间件
497
+
498
+ #### ws — WebSocket 客户端
221
499
 
222
500
  ```tsx
223
501
  app.use(ws({ url: '/ws' }))
224
502
 
225
- // In component:
226
- onMount(() => {
227
- const unsub = ctx.ws.onMessage((data) => { ... })
228
- onCleanup(() => unsub())
503
+ // 组件中:
504
+ const unsub = ctx.ws?.onMessage((data) => { ... })
505
+ // 清理在 ref 中处理
506
+ ctx.ws?.send({ type: 'chat', body: 'hello' })
507
+ {ctx.ws?.isConnected && <span>🟢 已连接</span>}
508
+ ```
509
+
510
+ | `ctx.ws` | 类型 | 说明 |
511
+ |----------|------|------|
512
+ | `send` | `(data: unknown) => void` | 发送消息 |
513
+ | `onMessage` | `(handler) => dispose()` | 注册消息监听 |
514
+ | `isConnected` | `Signal<boolean>` | 连接状态信号 |
515
+
516
+ | 选项 | 默认 | 说明 |
517
+ |------|------|------|
518
+ | `url` | `'/ws'` | WebSocket 地址 |
519
+ | `reconnectInterval` | `3000` | 重连间隔 (ms) |
520
+ | `maxReconnect` | `10` | 最大重连次数 |
521
+
522
+ #### api — HTTP 客户端
523
+
524
+ ```tsx
525
+ app.use(api({ baseURL: '/api' }))
526
+
527
+ // 组件中:
528
+ await ctx.api.get<User[]>('/users')
529
+ await ctx.api.post<User>('/users', body)
530
+ await ctx.api.put<User>('/users/1', body)
531
+ await ctx.api.patch<User>('/users/1', body)
532
+ await ctx.api.delete('/users/1')
533
+ ```
534
+
535
+ | `ctx.api` | 签名 | 说明 |
536
+ |-----------|------|------|
537
+ | `get` | `<T>(url, opts?) => Promise<T>` | GET 请求 |
538
+ | `post` | `<T>(url, body?, opts?) => Promise<T>` | POST 请求 |
539
+ | `put` | `<T>(url, body?, opts?) => Promise<T>` | PUT 请求 |
540
+ | `patch` | `<T>(url, body?, opts?) => Promise<T>` | PATCH 请求 |
541
+ | `delete` | `<T>(url, opts?) => Promise<T>` | DELETE 请求 |
542
+
543
+ | 选项 | 说明 |
544
+ |------|------|
545
+ | `baseURL` | API 基础路径 |
546
+ | `headers` | 默认请求头 |
547
+ | `onRequest` | 请求拦截器 `({url, init}) => {url, init}` |
548
+ | `onResponse` | 响应拦截器 `(res) => Promise<T>` |
549
+
550
+ 错误类型:`ApiError` — 包含 `status` 和 `body`。
551
+
552
+ #### auth — 认证状态管理
553
+
554
+ ```tsx
555
+ app.use(auth())
556
+
557
+ // 组件中:
558
+ {ctx.auth?.isLoggedIn ? (
559
+ <div>
560
+ <span>{ctx.auth?.user?.name}</span>
561
+ <button onClick={() => ctx.auth?.logout()}>退出</button>
562
+ </div>
563
+ ) : (
564
+ <Login />
565
+ )}
566
+
567
+ // 登录
568
+ ctx.auth.login('jwt-token', { id: 1, name: 'Alice' })
569
+ // 退出
570
+ ctx.auth.logout()
571
+ ```
572
+
573
+ | `ctx.auth` | 类型 | 说明 |
574
+ |-----------|------|------|
575
+ | `token` | `Signal<string \| null>` | 当前 token |
576
+ | `user` | `Signal<AuthUser \| null>` | 当前用户 |
577
+ | `isLoggedIn` | `Signal<boolean>` | 是否已登录(computed)|
578
+ | `login` | `(token, user) => void` | 存储 token + 用户到 localStorage |
579
+ | `logout` | `() => void` | 清除 token + 用户 |
580
+ | `setUser` | `(user) => void` | 更新用户信息 |
581
+ | `authorizationHeader` | `Signal<string \| null>` | `'Bearer xxx'` 或 `null` |
582
+
583
+ | 选项 | 默认 | 说明 |
584
+ |------|------|------|
585
+ | `storage` | `localStorage` | 存储方式 |
586
+ | `tokenKey` | `'weifuwu_token'` | token 存储 key |
587
+ | `userKey` | `'weifuwu_user'` | 用户信息存储 key |
588
+
589
+ ### 工具
590
+
591
+ #### useForm — 表单管理
592
+
593
+ ```tsx
594
+ const form = useForm({
595
+ initial: { name: '', email: '' },
596
+ validate: {
597
+ name: (v) => !v ? '请输入姓名' : null,
598
+ email: [
599
+ (v) => !v ? '请输入邮箱' : null,
600
+ (v) => !v.includes('@') ? '邮箱格式错误' : null,
601
+ ],
602
+ },
603
+ onSubmit: async (values) => {
604
+ await ctx.api.post('/users', values)
605
+ },
229
606
  })
230
- ctx.ws.send({ type: 'chat', body: 'hello' })
231
- <Show when={ctx.ws.isConnected}>🟢 已连接</Show>
607
+
608
+ // JSX:
609
+ <form onSubmit={form.handleSubmit}>
610
+ <input {...form.field('name')} />
611
+ <span>{form.errors.value.name}</span>
612
+ <button disabled={form.submitting}>提交</button>
613
+ </form>
614
+ ```
615
+
616
+ | 返回值 | 类型 | 说明 |
617
+ |--------|------|------|
618
+ | `values` | `Signal<T>` | 表单值 |
619
+ | `errors` | `Signal<Partial<Record<keyof T, string\|null>>>` | 验证错误 |
620
+ | `submitting` | `Signal<boolean>` | 提交状态 |
621
+ | `touched` | `Signal<Partial<Record<keyof T, boolean>>>` | 触碰字段 |
622
+ | `handleSubmit` | `(e: Event) => void` | 提交处理(绑定到 `<form>`)|
623
+ | `field` | `(name) => { value, onInput, error }` | 字段绑定对象 |
624
+ | `setValue` | `(name, value) => void` | 设字段值 |
625
+ | `reset` | `() => void` | 重置表单 |
626
+ | `validateAll` | `() => boolean` | 触发全部验证 |
627
+
628
+ #### createResource — 异步数据
629
+
630
+ ```tsx
631
+ const [data, { loading, error, refetch }] = createResource(
632
+ () => fetch('/api/posts').then(r => r.json()),
633
+ { initialValue: [] }
634
+ )
635
+
636
+ // JSX:
637
+ <Show when={loading}><p>加载中...</p></Show>
638
+ <Show when={error}><p>错误: {error.value?.message}</p></Show>
639
+ <Show when={computed(() => !loading.value && !error.value)}>
640
+ <For each={data}>{(item) => <div>{item.title}</div>}</For>
641
+ </Show>
232
642
  ```
233
643
 
234
- ### Router (`ctx.route`)
644
+ | 返回值 | 类型 | 说明 |
645
+ |--------|------|------|
646
+ | `data` (元组第一项) | `Signal<T \| undefined>` | 数据信号 |
647
+ | `loading` | `Signal<boolean>` | 加载状态 |
648
+ | `error` | `Signal<Error \| undefined>` | 错误信号 |
649
+ | `refetch` | `() => void` | 手动重新加载 |
650
+
651
+ #### ErrorBoundary — 错误捕获
235
652
 
236
653
  ```tsx
237
- app.use(router({ routes, notFound: NotFound, mode: 'hash' }))
654
+ <ErrorBoundary
655
+ fallback={(e) => <p>出错了: {e.message}</p>}
656
+ onError={(e) => console.error(e)}
657
+ >
658
+ {() => <Dashboard />} {/* 必须用 thunk */}
659
+ </ErrorBoundary>
660
+ ```
238
661
 
239
- ctx.route.path // '/hello/world'
240
- ctx.route.params // { name: 'world' }
241
- ctx.route.query // { tab: 'intro' }
242
- ctx.app.navigate('/hello/world')
662
+ #### createPortal — 渲染到指定位置
663
+
664
+ ```tsx
665
+ <Show when={showModal}>
666
+ {createPortal(<Modal />, document.body)}
667
+ </Show>
243
668
  ```
244
669
 
245
- ### Lifecycle
670
+ #### wrap — 封装三方库为组件
246
671
 
247
672
  ```tsx
248
- onMount(() => {
249
- init()
250
- return () => cleanup() // Auto-cleanup on unmount
673
+ const Chart = wrap('div', (el, props: { data: any }, ctx) => {
674
+ const chart = echarts.init(el)
675
+ chart.setOption(props.data)
676
+ effect(() => chart.setOption(props.data))
677
+ return () => chart.dispose() // 卸载时自动清理
251
678
  })
252
- onCleanup(() => clearInterval(id))
679
+
680
+ // 使用:
681
+ <Chart data={salesData} />
682
+ ```
683
+
684
+ #### createContext / extendCtx — 上下文扩展
685
+
686
+ ```tsx
687
+ // 类型安全的 provide/inject
688
+ const ThemeCtx = createContext<string>('theme')
689
+ ThemeCtx.provide(ctx, 'dark')
690
+ const theme = ThemeCtx.inject(ctx) // 'dark' | null
691
+
692
+ // 中间件注入
693
+ function myMiddleware(): AppMiddleware {
694
+ return (ctx) => extendCtx(ctx, { myField: 'hello' })
695
+ }
253
696
  ```
254
697
 
255
- ### From React
698
+ ### React 对照表
256
699
 
257
700
  | React | weifuwu/client |
258
701
  |-------|----------------|
259
- | `useState(0)` | `signal(0)` |
260
- | `useMemo(() => a*2, [a])` | `computed(() => a.value * 2)` |
261
- | `useEffect(() => f, [])` | `onMount(f)` |
262
- | `useEffect(() => f, [dep])` | `effect(f)` |
263
- | `{cond && <X/>}` | `<Show when={cond}><X/></Show>` |
264
- | `{items.map(i => <X/>)}` | `<For each={items}>{(i) => <X/>}</For>` |
265
- | `useNavigate()` | `ctx.app.navigate()` |
266
- | `useParams()` | `ctx.route.params` |
702
+ | `useState(0)` | `$.count = 0` |
703
+ | `useMemo(() => a*2, [a])` | `const doubled = a * 2`(render 时计算) |
704
+ | `useEffect(() => f, [])` | `if (!ctx.ui.ready) { f() }` |
705
+ | `{cond && <X/>}` | 相同 |
706
+ | `{arr.map(i => <X/>)}` | 相同,加 `key` |
707
+ | `Suspense` | `{$.loading && <Loading/>}` |
708
+ | `useNavigate()` | `ctx.app?.navigate()` |
709
+ | `useParams()` | `ctx.route?.params` |
710
+ | `axios.get()` | `ctx.api?.get()` |
711
+
712
+ ### 前端类型
713
+
714
+ `VNode`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `AuthClient`
715
+
716
+ ---
717
+
718
+ ## 全栈模式
719
+
720
+ ### 认证流程
721
+
722
+ ```ts
723
+ // 后端
724
+ app.post('/api/login', async (req, ctx) => {
725
+ const { email } = await req.json()
726
+ return Response.json({
727
+ token: 'jwt_' + Math.random().toString(36),
728
+ user: { id: 1, name: email.split('@')[0], email },
729
+ })
730
+ })
731
+
732
+ // 前端
733
+ app.use(api({ baseURL: '' }))
734
+ app.use(auth())
735
+
736
+ // 登录
737
+ const res = await ctx.api.post('/api/login', { email, password })
738
+ ctx.auth.login(res.token, res.user)
739
+ ```
740
+
741
+ ### 异步数据 + SSR
742
+
743
+ ```ts
744
+ // 后端 — 同路径既支持 SSR 也支持 API
745
+ app.get('/api/posts', async (req, ctx) => {
746
+ return Response.json(posts)
747
+ })
748
+
749
+ // 前端 — 客户端获取
750
+ const [posts, { loading }] = createResource(
751
+ () => ctx.api.get('/api/posts')
752
+ )
753
+ ```
754
+
755
+ ### 嵌套布局 + 代码分割
756
+
757
+ ```tsx
758
+ const routes = [
759
+ {
760
+ path: '/dashboard',
761
+ layout: DashboardLayout, // 侧边栏等 UI 保持挂载
762
+ children: [
763
+ { path: '/overview', component: lazy(() => import('./Overview')) },
764
+ { path: '/settings', component: lazy(() => import('./Settings')) },
765
+ ],
766
+ },
767
+ ]
768
+ ```
769
+
770
+ ---
771
+
772
+ ## 环境变量
773
+
774
+ | 变量 | 默认 | 说明 |
775
+ |------|------|------|
776
+ | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | Postgres 连接字符串 |
777
+ | `REDIS_URL` | `redis://localhost:6379` | Redis 连接字符串 |
267
778
 
268
779
  ---
269
780
 
@@ -275,43 +786,39 @@ node server.ts
275
786
  # http://localhost:3000
276
787
  ```
277
788
 
278
- ---
279
-
280
- ## Environment Variables
281
-
282
- | Variable | Default | Used by |
283
- |----------|---------|---------|
284
- | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
285
- | `REDIS_URL` | `redis://localhost:6379` | `redis()` |
789
+ Demo 包含:嵌套布局、ctx.ui.$ 待办列表、手动表单、async fetch 数据请求、api + auth 认证、WebSocket 实时通信。
286
790
 
287
791
  ---
288
792
 
289
- ## Project Structure
793
+ ## 项目结构
290
794
 
291
795
  ```
292
796
  src/
293
- ├── index.ts Entry, exports
294
- ├── types.ts Context, Handler, Middleware
295
- ├── core/ Router, serve, WebSocket upgrade
797
+ ├── index.ts 入口,导出所有后端模块
798
+ ├── types.ts Context, Handler, Middleware 等类型
799
+ ├── core/ Router, serve, WebSocket upgrade
296
800
  ├── middleware/ cors, serveStatic
297
- ├── postgres/ PostgreSQL client
298
- ├── redis/ Redis client
299
- ├── ui/ ctx.ui.html/js/css
300
- ├── graphql.ts GraphQL + router.graphql()
301
- ├── client/ Frontend framework
302
- │ ├── index.ts 16 runtime exports
303
- │ ├── signal.ts signal/computed/effect/batch
304
- │ ├── jsx-runtime.ts JSX DOM + Show/For/Fragment/Portal
305
- │ ├── app.ts createApp
306
- │ ├── router.ts Router + RouteView
307
- │ ├── types.ts WfuiContext + types
308
- │ └── middleware/ws.ts WebSocket client
309
- ├── test/ 47 backend + 77 frontend tests
310
- apps/demo/ Full-stack demo
801
+ ├── postgres/ PostgreSQL 客户端
802
+ ├── redis/ Redis 客户端
803
+ ├── ui/ SSR 渲染 + 动态编译
804
+ ├── graphql.ts GraphQL
805
+ ├── client/
806
+ │ ├── index.ts 前端导出入口
807
+ │ ├── vnode.ts VNode 类型 + JSX 工厂
808
+ │ ├── render.ts VDOM 渲染器(render + patchValue)
809
+ │ ├── router.ts 路由中间件 + RouteView
810
+ │ ├── app.ts createApp 应用实例
811
+ │ ├── types.ts 前端类型
812
+ │ └── middleware/
813
+ ├── ws.ts WebSocket 客户端
814
+ │ ├── api.ts HTTP 客户端
815
+ │ └── auth.ts 认证状态管理
816
+ ├── test/ 测试
817
+ apps/demo/ 全栈 demo
311
818
  ```
312
819
 
313
820
  ```bash
314
821
  npm run build # esbuild → dist/
315
822
  npm run typecheck # tsc --noEmit
316
- npm test # Run all tests
823
+ npm test # 运行所有测试
317
824
  ```