weifuwu 0.33.10 → 0.33.11

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