weifuwu 0.35.1 → 0.36.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,44 +1,33 @@
1
1
  # weifuwu
2
2
 
3
- **全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => JSX`**
3
+ **全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => VNode` + 纯 CSS 布局系统**
4
4
 
5
5
  ```bash
6
6
  npm install weifuwu
7
7
  ```
8
8
 
9
- 一个包,无上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架。
9
+ 一个包,零上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架;布局提供纯 CSS 原语 + 主题 Token。
10
10
 
11
11
  ---
12
12
 
13
13
  ## 模块总览
14
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` | — | — |
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 框架 | — |
26
+ | **layout** | `weifuwu/layout` | 纯 CSS 布局原语 + 主题 Token | — |
38
27
 
39
28
  ---
40
29
 
41
- ## 核心理念:`ctx`
30
+ ## 核心理念
42
31
 
43
32
  前后端共享同一模式:**中间件向 `ctx` 注入字段,handler/组件从 `ctx` 读取。**
44
33
 
@@ -54,80 +43,44 @@ npm install weifuwu
54
43
 
55
44
  ---
56
45
 
57
- ## 快速开始 全栈应用
46
+ ## 快速开始 —— 全栈应用
58
47
 
59
48
  ```ts
60
- // server.ts 同一个 npm
61
- import { serve, Router, cors, ui } from 'weifuwu'
49
+ import { serve, Router, ui } from 'weifuwu'
62
50
 
63
51
  const app = new Router()
64
- app.use(cors())
65
52
  app.use(ui())
66
53
 
67
- // REST API
68
- app.get('/api/posts', async (req, ctx) => {
69
- const posts = [{ id: 1, title: 'Hello' }]
70
- return Response.json(posts)
71
- })
54
+ // 前端 TSX → JS bundle(动态编译,零构建步骤)
55
+ app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
72
56
 
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
- })
57
+ // CSS(PostCSS + Tailwind 编译)
58
+ app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
81
59
 
82
- // SPA 入口 — 动态编译前端(零构建步骤)
60
+ // SPA 入口
83
61
  app.get('/', async (req, ctx) => ctx.ui.html`
84
62
  <!DOCTYPE html>
85
- <html><body><div id="root"></div>
86
- <script src="/static/app.js"></script></body></html>
63
+ <html>
64
+ <head><link rel="stylesheet" href="/style.css"></head>
65
+ <body><div id="root"></div><script src="/app.js"></script></body>
66
+ </html>
87
67
  `)
88
- app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
89
68
 
90
69
  serve(app, { port: 3000 })
91
70
  ```
92
71
 
93
72
  ```tsx
94
- // src/main.tsx 前端
95
- import { createApp, router, RouteView, ws, api, auth } from 'weifuwu/client'
73
+ // src/main.tsx —— 前端
74
+ import { createApp, router, RouteView } from 'weifuwu/client'
96
75
  import type { WfuiContext, RouteDef } from 'weifuwu/client'
97
76
 
98
- // 组件 = (props, ctx) => VNode
99
77
  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>
78
+ return <h1>Hello weifuwu</h1>
103
79
  }
104
80
 
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
- }
81
+ createApp()
82
+ .use(router({ routes: [{ path: '/', component: Home }], mode: 'history' }))
83
+ .mount('#root', () => <Home />)
131
84
  ```
132
85
 
133
86
  ---
@@ -137,110 +90,134 @@ function AppShell(_props: {}, ctx: WfuiContext) {
137
90
  ### Router
138
91
 
139
92
  ```ts
93
+ import { Router } from 'weifuwu'
94
+
140
95
  const app = new Router()
141
96
 
142
- // HTTP 方法
143
- app.get(path, ...handlers)
144
- app.post / put / delete / patch / head / options(path, ...handlers)
145
- app.all(path, ...handlers)
97
+ // 中间件
98
+ app.use(cors())
146
99
 
147
- // WebSocket / GraphQL
148
- app.ws(path, handler)
149
- app.graphql('/graphql', handler)
100
+ // 路由
101
+ app.get('/api/users', async (req: Request, ctx: Context) => {
102
+ return Response.json(users)
103
+ })
150
104
 
151
- // 中间件
152
- app.use(middleware)
153
- app.mount(prefix, subRouter)
154
- app.onError(handler)
105
+ app.post('/api/users', async (req: Request, ctx: Context) => {
106
+ const body = await req.json()
107
+ return Response.json({ id: 1, ...body }, { status: 201 })
108
+ })
155
109
 
156
- // 调试
157
- app.routes() // 列出所有路由
110
+ // URL 参数
111
+ app.get('/users/:id', async (req: Request, ctx: Context) => {
112
+ const id = ctx.params.id
113
+ return Response.json({ id, name: 'User ' + id })
114
+ })
158
115
  ```
159
116
 
160
- | 方法 | 参数 | 说明 |
117
+ | 方法 | 路由 | 说明 |
161
118
  |------|------|------|
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` | `()` | 返回路由列表数组 |
170
-
171
- ### serve — HTTP 服务器
119
+ | `app.get(path, handler)` | 任意 | GET 请求 |
120
+ | `app.post(path, handler)` | 任意 | POST 请求 |
121
+ | `app.put(path, handler)` | 任意 | PUT 请求 |
122
+ | `app.patch(path, handler)` | 任意 | PATCH 请求 |
123
+ | `app.delete(path, handler)` | 任意 | DELETE 请求 |
124
+ | `app.use(middleware)` | 全路由 | 中间件 |
125
+ | `app.ws(path, handler)` | 任意 | WebSocket |
126
+ | `app.graphql(options)` | 自动 | GraphQL 端点 |
127
+ | `app.onError(handler)` | 全局 | 错误处理 |
128
+
129
+ ### serve —— HTTP 服务器
172
130
 
173
131
  ```ts
174
- const srv = serve(app, { port: 3000 })
175
- await srv.stop() // 程序化停止
176
- // Ctrl+C / SIGTERM — 自动关闭所有连接后退出
132
+ import { serve, Router } from 'weifuwu'
133
+
134
+ const router = new Router()
135
+ serve(router, { port: 3000 })
177
136
  ```
178
137
 
179
- | 选项 | 默认 | 说明 |
180
- |------|------|------|
181
- | `port` | `0` | 监听端口 |
182
- | `hostname` | `'0.0.0.0'` | 监听地址 |
183
- | `maxBodySize` | `10MB` | 请求体上限 |
184
- | `timeout` | `30000` | Socket 超时 (ms) |
185
- | `shutdown` | `true` | 是否注册 SIGTERM/SIGINT 处理 |
138
+ | 参数 | 类型 | 默认值 | 说明 |
139
+ |------|------|--------|------|
140
+ | `port` | `number` | `0`(随机端口) | 监听端口 |
141
+ | `hostname` | `string` | `'0.0.0.0'` | 监听地址 |
142
+ | `timeout` | `number` | `30000` | 连接超时(ms)|
143
+ | `maxBodySize` | `number` | `10MB` | 请求体上限 |
144
+ | `shutdown` | `boolean` | `true` | 自动注册 SIGTERM/SIGINT |
186
145
 
187
- ### cors CORS 中间件
146
+ ### cors —— CORS 中间件
188
147
 
189
148
  ```ts
149
+ app.use(cors())
150
+
190
151
  app.use(cors({
191
- origin: ['https://example.com'],
192
- credentials: true,
152
+ origin: ['https://app.example.com'],
153
+ methods: ['GET', 'POST'],
193
154
  }))
194
155
  ```
195
156
 
196
- ### serveStatic 静态文件
157
+ | 参数 | 默认值 |
158
+ |------|--------|
159
+ | `origin` | `*` |
160
+ | `methods` | `GET, POST, PUT, PATCH, DELETE, OPTIONS` |
161
+ | `allowedHeaders` | `Content-Type, Authorization` |
162
+
163
+ ### serveStatic —— 静态文件
197
164
 
198
165
  ```ts
199
- app.use(serveStatic('./public', { prefix: '/assets' }))
200
- ```
166
+ import { serveStatic } from 'weifuwu'
201
167
 
202
- | 选项 | 默认 | 说明 |
203
- |------|------|------|
204
- | `prefix` | `''` | URL 前缀 |
205
- | `index` | `'index.html'` | 默认首页 |
168
+ // 单一路径
169
+ app.get('/static/*', serveStatic('./public'))
170
+
171
+ // 多目录
172
+ app.get('/uploads/*', serveStatic('./uploads'))
173
+ ```
206
174
 
207
- ### postgres PostgreSQL
175
+ ### postgres —— PostgreSQL
208
176
 
209
177
  ```ts
178
+ import { postgres } from 'weifuwu'
179
+
210
180
  app.use(postgres())
211
- // → ctx.sql`SELECT * FROM users`
212
181
 
213
- app.use(postgres({ url: 'postgres://user:pass@host:5432/db' }))
214
- // 默认读取 DATABASE_URL 环境变量
182
+ // 然后在 handler 中使用 ctx.sql
183
+ app.get('/users', async (req, ctx) => {
184
+ const users = await ctx.sql`SELECT * FROM users`
185
+ return Response.json(users)
186
+ })
215
187
  ```
216
188
 
217
- | 选项 | 默认 | 说明 |
218
- |------|------|------|
219
- | `url` | `DATABASE_URL` | 连接字符串 |
220
- | `migrations` | `'./migrations'` | 迁移文件目录 |
189
+ | 选项 | 类型 | 默认值 | 说明 |
190
+ |------|------|--------|------|
191
+ | `url` | `string` | `DATABASE_URL` 环境变量 | 连接字符串 |
192
+ | `max` | `number` | `10` | 连接池大小 |
221
193
 
222
- 依赖:需要 `postgres` npm 包。实现 `close(): Promise<void>`。
223
-
224
- ### redis — Redis
194
+ ### redis —— Redis
225
195
 
226
196
  ```ts
197
+ import { redis } from 'weifuwu'
198
+
227
199
  app.use(redis())
228
- // → ctx.redis.get('key')
229
- // ctx.redis.set('key', 'value')
230
- // 默认读取 REDIS_URL 环境变量
200
+
201
+ // 使用 ctx.redis
202
+ app.get('/cache', async (req, ctx) => {
203
+ const cached = await ctx.redis.get('key')
204
+ return Response.json({ cached })
205
+ })
231
206
  ```
232
207
 
233
- 依赖:需要 `ioredis` npm 包。实现 `close(): Promise<void>`。
208
+ | 选项 | 类型 | 默认值 | 说明 |
209
+ |------|------|--------|------|
210
+ | `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
234
211
 
235
- ### ui SSR + SPA 渲染
212
+ ### ui —— SSR + SPA 渲染
236
213
 
237
214
  ```ts
238
215
  app.use(ui())
239
216
 
240
217
  // SSR 页面
241
- app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
242
- <!DOCTYPE html>
243
- <html><body><h1>${post.title}</h1></body></html>
218
+ app.get('/page', async (req, ctx) => ctx.ui.html`
219
+ <h1>${title}</h1>
220
+ <p>${body}</p>
244
221
  `)
245
222
 
246
223
  // 动态 JS 编译(esbuild,零构建步骤)
@@ -254,17 +231,17 @@ app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
254
231
  |------|------|
255
232
  | `ctx.ui.html\`...\`` | 渲染 HTML 模板(转义变量防 XSS) |
256
233
  | `ctx.ui.html.unsafe(str)` | 插入原始 HTML |
257
- | `ctx.ui.js(entryPath)` | 动态编译 TSX → JS bundle |
258
- | `ctx.ui.css(entryPath)` | 编译 CSS (PostCSS + Tailwind) |
234
+ | `ctx.ui.js(entryPath)` | 编译 TSX → JS bundle |
235
+ | `ctx.ui.css(entryPath)` | 编译 CSSPostCSS + Tailwind |
259
236
 
260
- ### graphql GraphQL
237
+ ### graphql —— GraphQL
261
238
 
262
239
  ```ts
263
- app.graphql(async (req, ctx) => ({
240
+ app.graphql({
264
241
  schema: `type Query { hello: String }`,
265
242
  resolvers: { Query: { hello: () => 'world' } },
266
243
  graphiql: true,
267
- }))
244
+ })
268
245
  ```
269
246
 
270
247
  ### WebSocket
@@ -294,7 +271,6 @@ app.onError((err, req, ctx) => {
294
271
  |---------|------|
295
272
  | `HttpError` | HTTP 错误 `new HttpError(msg, status)` |
296
273
  | `DEFAULT_MAX_BODY` | 默认请求体上限 10MB |
297
- | `MIGRATIONS_TABLE` | Postgres 迁移表名 |
298
274
 
299
275
  ### 后端类型
300
276
 
@@ -304,9 +280,10 @@ app.onError((err, req, ctx) => {
304
280
 
305
281
  ## 前端 (`weifuwu/client`)
306
282
 
307
- **2750 行源码,28 个运行时导出 + 19 个类型,零外部依赖。**
283
+ 零外部依赖。组件模型:**纯函数 `(props, ctx) => VNode`**。
308
284
 
309
285
  构建配置(esbuild):
286
+
310
287
  ```js
311
288
  esbuild.build({
312
289
  jsx: 'automatic',
@@ -315,484 +292,327 @@ esbuild.build({
315
292
  })
316
293
  ```
317
294
 
318
- ### 状态管理 — 深度 Proxy
319
-
320
- `ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象层面的写入自动触发渲染,无需手动调用。
295
+ ### 组件
321
296
 
322
297
  ```tsx
323
- const $ = ctx.ui.$
324
-
325
- // 顶层属性赋值
326
- $.count = 0 // → 自动渲染
327
- $.user = { name: 'alice' } // → 新值自动深度包装
328
-
329
- // 数组突变
330
- $.items.push(newItem) // → 自动渲染
331
- $.items.pop() // → 自动渲染
332
- $.items.splice(i, 1) // → 自动渲染
333
-
334
- // 对象属性突变(数组内部也支持)
335
- $.items[0].done = true // → 自动渲染
336
- $.msgs[idx].content += event.text // → 自动渲染
298
+ // 组件 = 纯函数 (props, ctx) => VNode
299
+ function Greeting(props: { name: string }, _ctx: WfuiContext) {
300
+ return <div>Hello, {props.name}!</div>
301
+ }
337
302
 
338
- // 不可变更新(同样支持)
339
- $.items = [...$.items, newItem]
303
+ // 使用
304
+ <Greeting name="world" />
340
305
  ```
341
306
 
342
- | API | 说明 |
343
- |------|------|
344
- | `ctx.ui.$` | 深度 Proxy 对象,所有写入自动触发渲染 |
345
- | `ctx.ui.dirty()` | ⚠ 极少需要 — 仅当绕过 Proxy 直接操作底层对象时 |
307
+ ### 状态 —— 深度 Proxy
346
308
 
347
- ### JSX 运行时
309
+ `ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象写入自动触发渲染,无需手动调用。
348
310
 
349
311
  ```tsx
350
- // 自动由 esbuild 调用(无需手动导入)
351
- <div class="foo">hello</div>
352
- <Fragment>...</Fragment>
312
+ function Counter(_props: {}, ctx: WfuiContext) {
313
+ const $ = ctx.ui.$
314
+ if (!ctx.ui.ready) $.count = 0
315
+
316
+ return (
317
+ <div>
318
+ <span>{$.count}</span>
319
+ <button onClick={() => $.count++}>+</button>
320
+ </div>
321
+ )
322
+ }
353
323
  ```
354
324
 
355
- | 导出 | 说明 |
325
+ | API | 说明 |
356
326
  |------|------|
357
- | `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
358
- | `Fragment` | `<></>` 片段组件 |
359
-
360
- **Signal 属性自动绑定:** `<input value={signalVal} />` 信号变化时只更新对应 DOM 属性。
327
+ | `ctx.ui.$` | 深度 Proxy,所有写入自动触发渲染 |
328
+ | `$.x = val` | 顶层属性赋值 自动渲染 |
329
+ | `$.items.push(val)` | 数组突变 → 自动渲染 |
330
+ | `$.items[0].x = val` | 对象属性突变 自动渲染 |
331
+ | `ctx.ui.dirty()` | 仅当绕过 Proxy 直接操作底层对象时使用 |
361
332
 
362
333
  ### 条件与列表
363
334
 
364
- 使用原生 JS 表达式,不需要框架 API:
335
+ 使用原生 JS 控制流:
365
336
 
366
337
  ```tsx
367
338
  // 条件
368
- {$.loading && <Loading />}
369
- {$.error ? <Error msg={$.error} /> : <Content />}
339
+ {cond ? <A /> : <B />}
340
+ {cond && <A />}
370
341
 
371
342
  // 列表
372
- {$.items.map(item => (
373
- <div key={item.id}>{item.name}</div>
374
- ))}
343
+ {items.map(item => <div key={item.id}>{item.name}</div>)}
375
344
  ```
376
345
 
377
- | 模式 | 说明 |
378
- |------|------|
379
- | `{cond && <A/>}` | 条件渲染(cond 为 true 时渲染 A) |
380
- | `{cond ? <A/> : <B/>}` | 二选一 |
381
- | `{arr.map(x => <div key={x.id}/>)}` | 列表渲染,必须加 `key` |
382
-
383
- ### 生命周期
384
-
385
- 使用 `ref` 回调替代生命周期钩子:
346
+ ### 生命周期 —— ref 回调
386
347
 
387
348
  ```tsx
388
- <div ref={el => {
389
- if (el) {
390
- // 挂载:el 是 DOM 元素
391
- fetchData()
392
- el.addEventListener('scroll', handler)
349
+ function MyComponent(_props: {}, ctx: WfuiContext) {
350
+ function onRef(el: HTMLElement | null) {
351
+ if (el) {
352
+ // mount: 加事件监听等
353
+ el.addEventListener('scroll', handler)
354
+ } else {
355
+ // unmount: 清理
356
+ }
393
357
  }
394
- if (!el) {
395
- // 卸载:el 为 null
396
- cleanup()
397
- }
398
- }} />
399
- ```
400
358
 
401
- `ref` 在挂载时传入 DOM 元素,卸载时传入 `null`。一个回调覆盖 mount/unmount 两个场景。
359
+ return <div ref={onRef} />
360
+ }
361
+ ```
402
362
 
403
- ### 应用
363
+ ### 应用 —— createApp
404
364
 
405
365
  ```tsx
366
+ import { createApp } from 'weifuwu/client'
367
+
406
368
  const app = createApp()
407
369
  app.use(middleware1)
408
370
  app.use(middleware2)
409
- await app.mount('#root', AppShell)
371
+ app.mount('#root', RootComponent)
372
+ app.destroy()
410
373
  ```
411
374
 
412
- | 方法 | 说明 |
413
- |------|------|
414
- | `use(mw)` | 注册中间件,返回 `this` 支持链式 |
415
- | `mount(selector, RootComponent)` | 挂载到 DOM |
416
- | `hydrate(selector, Component, props?)` | 在 SSR 内容上附加组件 |
417
- | `ctx` | 当前上下文 |
418
-
419
- ### 路由
375
+ ### 路由 —— router + RouteView
420
376
 
421
377
  ```tsx
422
- // 路由定义
423
- const routes: RouteDef[] = [
424
- { path: '/', component: HomePage },
378
+ import { router, RouteView } from 'weifuwu/client'
379
+
380
+ createApp()
381
+ .use(router({
382
+ routes: [
383
+ { path: '/', component: Home },
384
+ { path: '/users', component: UserList },
385
+ { path: '/users/:id', component: UserDetail },
386
+ ],
387
+ notFound: NotFound,
388
+ mode: 'history', // 或 'hash'
389
+ }))
390
+ .mount('#root', AppShell)
391
+
392
+ // 嵌套布局
393
+ const routes = [
425
394
  {
426
395
  path: '/dashboard',
427
- layout: DashboardLayout, // 嵌套布局
396
+ layout: DashboardLayout, // 持久布局
428
397
  children: [
429
- { path: '/overview', component: Overview }, // 子路由
398
+ { path: '/overview', component: Overview },
430
399
  { path: '/settings', component: Settings },
431
400
  ],
432
401
  },
433
- { path: '/user/:id', component: UserPage, title: '用户' },
434
402
  ]
435
403
 
436
- // 注册路由中间件
437
- app.use(router({
438
- routes,
439
- notFound: NotFound,
440
- mode: 'hash', // 'hash' | 'history'
441
- scrollRestoration: true,
442
- }))
443
-
444
- // 路由出口 — 根层级和嵌套层级用同一个组件
445
- function AppShell() {
446
- return <main><RouteView /></main> // 根出口
447
- }
448
- function DashboardLayout() {
404
+ // 在 layout 中放置 RouteView 渲染子路由
405
+ function DashboardLayout(_props: {}, ctx: WfuiContext) {
449
406
  return (
450
- <div class="flex">
451
- <Sidebar />
452
- <main><RouteView /></main> // 嵌套出口(同一组件)
407
+ <div class="wf-split">
408
+ <aside>sidebar</aside>
409
+ <main><RouteView /></main>
453
410
  </div>
454
411
  )
455
412
  }
456
413
  ```
457
414
 
458
- | RouteDef 字段 | 类型 | 说明 |
459
- |---------------|------|------|
460
- | `path` | `string` | 路由路径,支持 `/:param` |
461
- | `component` | `Component` | 路由组件 |
462
- | `layout` | `Component` | 嵌套布局(渲染 `<RouteView />` 显示子路由)|
463
- | `children` | `RouteDef[]` | 子路由(与 layout 配合使用)|
464
- | `auth` | `boolean` | 是否需要登录 |
465
- | `title` | `string` | 页面标题(自动设置 `document.title`)|
466
- | `loader` | `(ctx) => Promise<data>` | 数据预取 → `ctx.route.data` |
467
- | `transition` | `string` | 页面切换过渡动画 CSS class 前缀 |
468
-
469
- | RouterOptions | 默认 | 说明 |
470
- |---------------|------|------|
471
- | `mode` | `'hash'` | 路由模式 |
472
- | `notFound` | — | 404 组件 |
473
- | `scrollRestoration` | `true` | 历史模式时恢复滚动位置 |
474
- | `transition` | — | 全局过渡动画 |
475
-
476
- **`ctx.route` 注入:**
477
-
478
- ```tsx
479
- ctx.route.path // '/user/42'
480
- ctx.route.params // { id: '42' }
481
- ctx.route.query // { tab: 'profile' }
482
- ctx.route.component // 当前路由组件
483
- ctx.route.data // loader 返回的数据
484
- ctx.route.loading // loader 是否加载中
485
- ctx.app.navigate('/path')
486
- ```
487
-
488
- ### 代码分割
489
-
490
- ```tsx
491
- const AdminPage = lazy(() => import('./pages/AdminPage'), {
492
- fallback: () => <div>加载中...</div>,
493
- })
494
-
495
- const routes = [
496
- { path: '/admin', component: AdminPage },
497
- ]
498
- ```
499
-
500
- 需 esbuild `splitting: true` + `outdir`。
415
+ | API | 说明 |
416
+ |------|------|
417
+ | `ctx.route.path` | 当前路由路径 |
418
+ | `ctx.route.params` | URL 参数(如 `:id`)|
419
+ | `ctx.route.query` | 查询参数对象 |
420
+ | `ctx.app.navigate(path)` | 编程式导航 |
501
421
 
502
422
  ### 中间件
503
423
 
504
- #### ws WebSocket 客户端
424
+ **ws —— WebSocket 客户端**
505
425
 
506
426
  ```tsx
507
- app.use(ws({ url: '/ws' }))
427
+ app.use(ws())
508
428
 
509
- // 组件中:
510
- const unsub = ctx.ws?.onMessage((data) => { ... })
511
- // 清理在 ref 中处理
429
+ // 发送消息
512
430
  ctx.ws?.send({ type: 'chat', body: 'hello' })
513
- {ctx.ws?.isConnected && <span>🟢 已连接</span>}
514
- ```
515
-
516
- | `ctx.ws` | 类型 | 说明 |
517
- |----------|------|------|
518
- | `send` | `(data: unknown) => void` | 发送消息 |
519
- | `onMessage` | `(handler) => dispose()` | 注册消息监听 |
520
- | `isConnected` | `Signal<boolean>` | 连接状态信号 |
521
431
 
522
- | 选项 | 默认 | 说明 |
523
- |------|------|------|
524
- | `url` | `'/ws'` | WebSocket 地址 |
525
- | `reconnectInterval` | `3000` | 重连间隔 (ms) |
526
- | `maxReconnect` | `10` | 最大重连次数 |
432
+ // 接收消息
433
+ ctx.ws?.onMessage((msg) => { console.log(msg) })
434
+ ```
527
435
 
528
- #### api HTTP 客户端
436
+ **api —— HTTP 客户端**
529
437
 
530
438
  ```tsx
531
439
  app.use(api({ baseURL: '/api' }))
532
440
 
533
- // 组件中:
534
- await ctx.api.get<User[]>('/users')
535
- await ctx.api.post<User>('/users', body)
536
- await ctx.api.put<User>('/users/1', body)
537
- await ctx.api.patch<User>('/users/1', body)
538
- await ctx.api.delete('/users/1')
441
+ // 自动携带 Authorization header
442
+ const user = await ctx.api?.get('/users/1')
443
+ const res = await ctx.api?.post('/users', { name: 'Alice' })
539
444
  ```
540
445
 
541
- | `ctx.api` | 签名 | 说明 |
542
- |-----------|------|------|
543
- | `get` | `<T>(url, opts?) => Promise<T>` | GET 请求 |
544
- | `post` | `<T>(url, body?, opts?) => Promise<T>` | POST 请求 |
545
- | `put` | `<T>(url, body?, opts?) => Promise<T>` | PUT 请求 |
546
- | `patch` | `<T>(url, body?, opts?) => Promise<T>` | PATCH 请求 |
547
- | `delete` | `<T>(url, opts?) => Promise<T>` | DELETE 请求 |
548
-
549
- | 选项 | 说明 |
446
+ | 方法 | 说明 |
550
447
  |------|------|
551
- | `baseURL` | API 基础路径 |
552
- | `headers` | 默认请求头 |
553
- | `onRequest` | 请求拦截器 `({url, init}) => {url, init}` |
554
- | `onResponse` | 响应拦截器 `(res) => Promise<T>` |
555
-
556
- 错误类型:`ApiError` — 包含 `status` 和 `body`。
448
+ | `ctx.api.get(url, opts?)` | GET |
449
+ | `ctx.api.post(url, body?, opts?)` | POST |
450
+ | `ctx.api.put(url, body?, opts?)` | PUT |
451
+ | `ctx.api.patch(url, body?, opts?)` | PATCH |
452
+ | `ctx.api.delete(url, opts?)` | DELETE |
557
453
 
558
- #### auth 认证状态管理
454
+ **auth —— 认证状态管理**
559
455
 
560
456
  ```tsx
561
457
  app.use(auth())
562
458
 
563
- // 组件中:
564
- {ctx.auth?.isLoggedIn ? (
565
- <div>
566
- <span>{ctx.auth?.user?.name}</span>
567
- <button onClick={() => ctx.auth?.logout()}>退出</button>
568
- </div>
569
- ) : (
570
- <Login />
571
- )}
572
-
573
459
  // 登录
574
- ctx.auth.login('jwt-token', { id: 1, name: 'Alice' })
575
- // 退出
576
- ctx.auth.logout()
577
- ```
578
-
579
- | `ctx.auth` | 类型 | 说明 |
580
- |-----------|------|------|
581
- | `token` | `Signal<string \| null>` | 当前 token |
582
- | `user` | `Signal<AuthUser \| null>` | 当前用户 |
583
- | `isLoggedIn` | `Signal<boolean>` | 是否已登录(computed)|
584
- | `login` | `(token, user) => void` | 存储 token + 用户到 localStorage |
585
- | `logout` | `() => void` | 清除 token + 用户 |
586
- | `setUser` | `(user) => void` | 更新用户信息 |
587
- | `authorizationHeader` | `Signal<string \| null>` | `'Bearer xxx'` 或 `null` |
588
-
589
- | 选项 | 默认 | 说明 |
590
- |------|------|------|
591
- | `storage` | `localStorage` | 存储方式 |
592
- | `tokenKey` | `'weifuwu_token'` | token 存储 key |
593
- | `userKey` | `'weifuwu_user'` | 用户信息存储 key |
460
+ ctx.auth?.login(token, user)
594
461
 
595
- ### 工具
462
+ // 登出
463
+ ctx.auth?.logout()
596
464
 
597
- #### useForm — 表单管理
598
-
599
- ```tsx
600
- const form = useForm({
601
- initial: { name: '', email: '' },
602
- validate: {
603
- name: (v) => !v ? '请输入姓名' : null,
604
- email: [
605
- (v) => !v ? '请输入邮箱' : null,
606
- (v) => !v.includes('@') ? '邮箱格式错误' : null,
607
- ],
608
- },
609
- onSubmit: async (values) => {
610
- await ctx.api.post('/users', values)
611
- },
612
- })
613
-
614
- // JSX:
615
- <form onSubmit={form.handleSubmit}>
616
- <input {...form.field('name')} />
617
- <span>{form.errors.value.name}</span>
618
- <button disabled={form.submitting}>提交</button>
619
- </form>
465
+ // 状态
466
+ if (ctx.auth?.isLoggedIn) { ... }
620
467
  ```
621
468
 
622
- | 返回值 | 类型 | 说明 |
623
- |--------|------|------|
624
- | `values` | `Signal<T>` | 表单值 |
625
- | `errors` | `Signal<Partial<Record<keyof T, string\|null>>>` | 验证错误 |
626
- | `submitting` | `Signal<boolean>` | 提交状态 |
627
- | `touched` | `Signal<Partial<Record<keyof T, boolean>>>` | 触碰字段 |
628
- | `handleSubmit` | `(e: Event) => void` | 提交处理(绑定到 `<form>`)|
629
- | `field` | `(name) => { value, onInput, error }` | 字段绑定对象 |
630
- | `setValue` | `(name, value) => void` | 设字段值 |
631
- | `reset` | `() => void` | 重置表单 |
632
- | `validateAll` | `() => boolean` | 触发全部验证 |
469
+ | API | 说明 |
470
+ |------|------|
471
+ | `ctx.auth.token` | JWT token |
472
+ | `ctx.auth.user` | 用户对象 |
473
+ | `ctx.auth.isLoggedIn` | 是否已登录 |
474
+ | `ctx.auth.login(token, user, refreshToken?)` | 登录 |
475
+ | `ctx.auth.logout()` | 登出 |
633
476
 
634
- #### createResource — 异步数据
477
+ ### ErrorBoundary
635
478
 
636
479
  ```tsx
637
- const [data, { loading, error, refetch }] = createResource(
638
- () => fetch('/api/posts').then(r => r.json()),
639
- { initialValue: [] }
640
- )
641
-
642
- // JSX:
643
- <Show when={loading}><p>加载中...</p></Show>
644
- <Show when={error}><p>错误: {error.value?.message}</p></Show>
645
- <Show when={computed(() => !loading.value && !error.value)}>
646
- <For each={data}>{(item) => <div>{item.title}</div>}</For>
647
- </Show>
648
- ```
649
-
650
- | 返回值 | 类型 | 说明 |
651
- |--------|------|------|
652
- | `data` (元组第一项) | `Signal<T \| undefined>` | 数据信号 |
653
- | `loading` | `Signal<boolean>` | 加载状态 |
654
- | `error` | `Signal<Error \| undefined>` | 错误信号 |
655
- | `refetch` | `() => void` | 手动重新加载 |
656
-
657
- #### ErrorBoundary — 错误捕获
480
+ import { ErrorBoundary } from 'weifuwu/client'
658
481
 
659
- ```tsx
660
- <ErrorBoundary
661
- fallback={(e) => <p>出错了: {e.message}</p>}
662
- onError={(e) => console.error(e)}
663
- >
664
- {() => <Dashboard />} {/* 必须用 thunk */}
482
+ <ErrorBoundary fallback={<p>出错了</p>}>
483
+ <UserProfile />
665
484
  </ErrorBoundary>
666
485
  ```
667
486
 
668
- #### createPortal — 渲染到指定位置
669
-
670
- ```tsx
671
- <Show when={showModal}>
672
- {createPortal(<Modal />, document.body)}
673
- </Show>
674
- ```
675
-
676
- #### wrap — 封装三方库为组件
677
-
678
- ```tsx
679
- const Chart = wrap('div', (el, props: { data: any }, ctx) => {
680
- const chart = echarts.init(el)
681
- chart.setOption(props.data)
682
- effect(() => chart.setOption(props.data))
683
- return () => chart.dispose() // 卸载时自动清理
684
- })
685
-
686
- // 使用:
687
- <Chart data={salesData} />
688
- ```
689
-
690
- #### createContext / extendCtx — 上下文扩展
691
-
692
- ```tsx
693
- // 类型安全的 provide/inject
694
- const ThemeCtx = createContext<string>('theme')
695
- ThemeCtx.provide(ctx, 'dark')
696
- const theme = ThemeCtx.inject(ctx) // 'dark' | null
697
-
698
- // 中间件注入
699
- function myMiddleware(): AppMiddleware {
700
- return (ctx) => extendCtx(ctx, { myField: 'hello' })
701
- }
702
- ```
703
-
704
- ### React 对照表
487
+ ### 工具
705
488
 
706
- | React | weifuwu/client |
707
- |-------|----------------|
708
- | `useState(0)` | `$.count = 0` |
709
- | `useMemo(() => a*2, [a])` | `const doubled = a * 2`(render 时计算) |
710
- | `useEffect(() => f, [])` | `if (!ctx.ui.ready) { f() }` |
711
- | `{cond && <X/>}` | 相同 |
712
- | `{arr.map(i => <X/>)}` | 相同,加 `key` |
713
- | `Suspense` | `{$.loading && <Loading/>}` |
714
- | `useNavigate()` | `ctx.app?.navigate()` |
715
- | `useParams()` | `ctx.route?.params` |
716
- | `axios.get()` | `ctx.api?.get()` |
489
+ | 函数 | 用途 |
490
+ |------|------|
491
+ | `extendCtx(ctx, fields)` | 创建新 ctx,继承原 ctx 的 getter |
717
492
 
718
493
  ### 前端类型
719
494
 
720
- `VNode`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `AuthClient`
495
+ `VNode`, `VNodeType`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `ApiError`, `AuthClient`, `AuthOptions`, `ErrorBoundaryProps`
721
496
 
722
497
  ---
723
498
 
724
- ## 全栈模式
499
+ ## 布局 & 主题 (`weifuwu/layout`)
725
500
 
726
- ### 认证流程
501
+ CSS 布局原语 + 主题 Token。不绑定任何 JS 框架。
727
502
 
728
503
  ```ts
729
- // 后端
730
- app.post('/api/login', async (req, ctx) => {
731
- const { email } = await req.json()
732
- return Response.json({
733
- token: 'jwt_' + Math.random().toString(36),
734
- user: { id: 1, name: email.split('@')[0], email },
735
- })
736
- })
737
-
738
- // 前端
739
- app.use(api({ baseURL: '' }))
740
- app.use(auth())
741
-
742
- // 登录
743
- const res = await ctx.api.post('/api/login', { email, password })
744
- ctx.auth.login(res.token, res.user)
504
+ // 服务端编译
505
+ app.get('/layout.css', async (req, ctx) => ctx.ui.css('./node_modules/weifuwu/dist/layout/weifuwu-layout.css'))
745
506
  ```
746
507
 
747
- ### 异步数据 + SSR
748
-
749
- ```ts
750
- // 后端 — 同路径既支持 SSR 也支持 API
751
- app.get('/api/posts', async (req, ctx) => {
752
- return Response.json(posts)
753
- })
754
-
755
- // 前端 — 客户端获取
756
- const [posts, { loading }] = createResource(
757
- () => ctx.api.get('/api/posts')
758
- )
508
+ ```html
509
+ <!-- 或直接引入 -->
510
+ <link rel="stylesheet" href="/layout.css">
759
511
  ```
760
512
 
761
- ### 嵌套布局 + 代码分割
513
+ ### 使用示例
762
514
 
763
- ```tsx
764
- const routes = [
765
- {
766
- path: '/dashboard',
767
- layout: DashboardLayout, // 侧边栏等 UI 保持挂载
768
- children: [
769
- { path: '/overview', component: lazy(() => import('./Overview')) },
770
- { path: '/settings', component: lazy(() => import('./Settings')) },
771
- ],
772
- },
773
- ]
515
+ ```html
516
+ <div class="wf-stack" style="--wf-gap: 24px">
517
+ <div class="wf-split">
518
+ <h2 style="color: var(--wf-color-text)">仪表盘</h2>
519
+ <button style="background: var(--wf-color-primary); color: #fff; border-radius: var(--wf-radius)">+ 新建</button>
520
+ </div>
521
+ <div class="wf-row" style="--wf-gap: 16px">
522
+ <div class="wf-fill wf-surface wf-stack" style="padding: 20px; background: var(--wf-color-bg); --wf-gap: 4px">
523
+ <span style="color: var(--wf-color-text-secondary)">总用户</span>
524
+ <span style="font-size: var(--wf-font-size-4xl); font-weight: var(--wf-font-weight-bold); color: var(--wf-color-text)">1,234</span>
525
+ </div>
526
+ </div>
527
+ </div>
528
+ ```
529
+
530
+ ### 33 个布局原语
531
+
532
+ | 类别 | 原语 | 含义 | CSS 实现 |
533
+ |------|------|------|---------|
534
+ | **排列** | `wf-stack` | 纵向堆叠 | `flex-direction: column + gap` |
535
+ | | `wf-stack-reverse` | 反向堆叠 | `flex-direction: column-reverse` |
536
+ | | `wf-row` | 横向排列 | `flex + flex-wrap + gap` |
537
+ | | `wf-row-reverse` | 反向排列 | `flex-direction: row-reverse` |
538
+ | | `wf-nowrap` | 不换行 | `flex-wrap: nowrap` |
539
+ | | `wf-cluster` | 换行簇 | `flex-wrap: wrap + justify-content: center` |
540
+ | **分布** | `wf-split` | 两端展开 | `justify-content: space-between` |
541
+ | | `wf-center` | 居中 | `flex + center both axes` |
542
+ | | `wf-right` | 靠右 | `justify-content: flex-end` |
543
+ | | `wf-around` | 环绕 | `justify-content: space-around` |
544
+ | | `wf-evenly` | 均匀 | `justify-content: space-evenly` |
545
+ | **对齐** | `wf-top` | 顶部 | `align-items: flex-start` |
546
+ | | `wf-bottom` | 底部 | `align-items: flex-end` |
547
+ | | `wf-stretch` | 拉伸 | `align-items: stretch` |
548
+ | **弹性** | `wf-fill` | 撑满剩余空间 | `flex: 1 + min-width: 0` |
549
+ | | `wf-fixed` | 固定不伸缩 | `flex: none` |
550
+ | | `wf-auto` | 按内容撑满 | `flex: auto` |
551
+ | | `wf-shrink` | 可收缩 | `min-width: 0 + min-height: 0` |
552
+ | **Z轴** | `wf-cover` | 全屏覆盖 | `position: fixed + inset: 0` |
553
+ | | `wf-pop` | 浮动层 | `position: absolute` |
554
+ | | `wf-anchor` | 锚点容器 | `position: relative` |
555
+ | | `wf-layer` | 层级控制 | `position: relative + z-index` |
556
+ | | `wf-sticky` | 粘性定位 | `position: sticky` |
557
+ | **容器** | `wf-surface` | 基础面 | `border-radius + box-shadow + bg` |
558
+ | | `wf-grid` | 二维网格 | `display: grid + --wf-cols` |
559
+ | | `wf-container` | 宽度约束 | `max-width + margin: auto` |
560
+ | | `wf-scroll` | 可滚动 | `overflow: auto` |
561
+ | | `wf-clip` | 溢出裁剪 | `overflow: hidden` |
562
+ | **显隐** | `wf-hidden` | 隐藏 | `display: none` |
563
+ | | `wf-block` | 块级 | `display: block` |
564
+ | | `wf-inline` | 行内 | `display: inline` |
565
+ | | `wf-inline-block` | 行内块 | `display: inline-block` |
566
+ | | `wf-contents` | 容器抹除 | `display: contents` |
567
+
568
+ ### 72 个主题 Token
569
+
570
+ | 类别 | Token 示例 | 值/层级 |
571
+ |------|-----------|---------|
572
+ | 品牌色 | `--wf-color-primary`, `--wf-color-primary-bg` | 品牌色 + Hover + 背景 |
573
+ | 语义色 | `--wf-color-success/warning/error/info` | 各带 `-bg` 背景变体 |
574
+ | 中性色 | `--wf-color-text/text-secondary/text-tertiary/text-disabled` | 4 级文字色 |
575
+ | | `--wf-color-bg/bg-secondary/bg-tertiary` | 3 级背景色 |
576
+ | | `--wf-color-border/border-light/border-dark` | 3 级边框色 |
577
+ | 字体 | `--wf-font-sans`, `--wf-font-mono` | 字体族 |
578
+ | 字号 | `--wf-font-size-xs/sm/base/lg/xl/2xl/3xl/4xl/5xl` | 9 级字号 |
579
+ | 字重 | `--wf-font-weight-normal/medium/semibold/bold` | 4 级字重 |
580
+ | 行高 | `--wf-line-height-tight/normal/relaxed` | 3 级行高 |
581
+ | 字距 | `--wf-letter-spacing/wide/wider` | 3 级字符间距 |
582
+ | 间距 | `--wf-space-xs/sm/md/lg/xl/2xl` | 8 级 margin/padding |
583
+ | 间隔 | `--wf-gap-xs/sm/md/lg/xl/2xl` | 6 级 flex/grid gap |
584
+ | 圆角 | `--wf-radius-sm/md/lg/xl` | 5 级 border-radius |
585
+ | 阴影 | `--wf-shadow-sm/md/lg` | 4 级 box-shadow |
586
+ | 边框 | `--wf-border-width` | 边框宽度 |
587
+ | 聚焦 | `--wf-focus-ring` | 聚焦环(box-shadow)|
588
+ | 动效 | `--wf-transition-duration/timing` | 过渡时长 + 曲线 |
589
+ | 表单 | `--wf-accent-color`, `--wf-caret-color` | 控件主题色 + 光标色 |
590
+ | 透明 | `--wf-opacity-disabled`, `--wf-opacity-overlay` | 禁用态 + 遮罩透明度 |
591
+ | 层级 | `--wf-pop-z`, `--wf-cover-z` | z-index 层 |
592
+
593
+ ### 暗色模式
594
+
595
+ 切换 `html` 的 `data-theme` 属性即可自动切换全部主题色:
596
+
597
+ ```ts
598
+ document.documentElement.setAttribute('data-theme', 'dark')
599
+ // → 全部引用 var(--wf-*) 的元素自动变色
774
600
  ```
775
601
 
776
- ---
602
+ ### 基础元素默认样式
777
603
 
778
- ## 环境变量
604
+ 引入 weifuwu/layout 后,以下 HTML 元素自动绑定主题 Token:
779
605
 
780
- | 变量 | 默认 | 说明 |
781
- |------|------|------|
782
- | `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | Postgres 连接字符串 |
783
- | `REDIS_URL` | `redis://localhost:6379` | Redis 连接字符串 |
606
+ `body`, `h1`~`h6`, `p`, `a`, `label`, `small`, `input`, `textarea`, `select`, `button`, `table`, `th`, `td`, `hr`, `pre`, `code`
784
607
 
785
608
  ---
786
609
 
787
- ## Demo
788
-
789
- ```bash
790
- cd apps/demo
791
- node server.ts
792
- # http://localhost:3000
793
- ```
610
+ ## 环境变量
794
611
 
795
- Demo 包含:嵌套布局、ctx.ui.$ 待办列表、手动表单、async fetch 数据请求、api + auth 认证、WebSocket 实时通信。
612
+ | 变量 | 用途 | 默认值 |
613
+ |------|------|--------|
614
+ | `DATABASE_URL` | PostgreSQL 连接字符串 | — |
615
+ | `REDIS_URL` | Redis 连接字符串 | — |
796
616
 
797
617
  ---
798
618
 
@@ -800,31 +620,63 @@ Demo 包含:嵌套布局、ctx.ui.$ 待办列表、手动表单、async fetch
800
620
 
801
621
  ```
802
622
  src/
803
- ├── index.ts 入口,导出所有后端模块
804
- ├── types.ts Context, Handler, Middleware 等类型
805
- ├── core/ Router, serve, WebSocket upgrade
806
- ├── middleware/ cors, serveStatic
807
- ├── postgres/ PostgreSQL 客户端
808
- ├── redis/ Redis 客户端
809
- ├── ui/ SSR 渲染 + 动态编译
810
- ├── graphql.ts GraphQL
811
- ├── client/
812
- │ ├── index.ts 前端导出入口
813
- ├── vnode.ts VNode 类型 + JSX 工厂
814
- ├── render.ts VDOM 渲染器(render + patchValue)
815
- ├── router.ts 路由中间件 + RouteView
816
- ├── app.ts createApp 应用实例
817
- ├── types.ts 前端类型
623
+ ├── index.ts # 统一导出
624
+ ├── types.ts # 后端类型
625
+ ├── request.ts # 请求解析
626
+ ├── response.ts # 响应工具
627
+ ├── core/
628
+ ├── router.ts # HTTP 路由
629
+ ├── serve.ts # HTTP 服务器
630
+ │ └── ws.ts # WebSocket
631
+ ├── middleware/
632
+ │ ├── cors.ts
633
+ └── static.ts
634
+ ├── postgres/
635
+ ├── redis/
636
+ ├── graphql.ts
637
+ ├── ui/ # SSR + JS/CSS 编译
638
+ ├── client/ # 前端 VDOM 框架
639
+ │ ├── index.ts
640
+ │ ├── vnode.ts
641
+ │ ├── app.ts
642
+ │ ├── render.ts
643
+ │ ├── router.ts
644
+ │ ├── types.ts
645
+ │ ├── error-boundary.ts
818
646
  │ └── middleware/
819
- │ ├── ws.ts WebSocket 客户端
820
- │ ├── api.ts HTTP 客户端
821
- │ └── auth.ts 认证状态管理
822
- ├── test/ 测试
823
- apps/demo/ 全栈 demo
647
+ │ ├── api.ts
648
+ │ ├── auth.ts
649
+ │ └── ws.ts
650
+ └── layout/ # 纯 CSS 布局 + 主题
651
+ ├── weifuwu-layout.css
652
+ ├── _tokens.css
653
+ ├── _dark.css
654
+ ├── _base.css
655
+ └── _*.css # 33 个原语
824
656
  ```
825
657
 
658
+ ---
659
+
660
+ ## 开发
661
+
826
662
  ```bash
827
- npm run build # esbuild → dist/
828
- npm run typecheck # tsc --noEmit
829
- npm test # 运行所有测试
663
+ # 构建
664
+ npm run build
665
+
666
+ # 类型检查
667
+ npm run typecheck
668
+
669
+ # 测试
670
+ npm test
671
+
672
+ # 发布
673
+ node scripts/release.mjs <version>
830
674
  ```
675
+
676
+ ## 设计原则
677
+
678
+ - **后端为工具箱** —— 提供 HTTP 路由、数据库、中间件原语,不捆绑业务模块
679
+ - **全栈单包** —— `npm install weifuwu` = 后端 + 前端 + 布局
680
+ - **Web 标准优先** —— 所有 handler 使用 `(req: Request, ctx: Context) => Response`
681
+ - **零外部依赖** —— 前端和布局没有任何 npm 运行时依赖
682
+ - **LLM 友好** —— 模块总览表 + 一致格式 + 清晰依赖链