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