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