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.
- package/README.md +352 -600
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,45 +1,33 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
**全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) =>
|
|
3
|
+
**全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => VNode` + 纯 CSS 布局系统**
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
一个包,零上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架;布局提供纯 CSS 原语 + 主题 Token。
|
|
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
|
-
| **client** |
|
|
26
|
-
| **layout** |
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
//
|
|
69
|
-
app.get('/
|
|
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
|
-
//
|
|
75
|
-
app.
|
|
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
|
|
87
|
-
<
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
//
|
|
144
|
-
app.
|
|
145
|
-
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
146
|
-
app.all(path, ...handlers)
|
|
97
|
+
// 中间件
|
|
98
|
+
app.use(cors())
|
|
147
99
|
|
|
148
|
-
//
|
|
149
|
-
app.
|
|
150
|
-
|
|
100
|
+
// 路由
|
|
101
|
+
app.get('/api/users', async (req: Request, ctx: Context) => {
|
|
102
|
+
return Response.json(users)
|
|
103
|
+
})
|
|
151
104
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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.
|
|
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
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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` | `
|
|
183
|
-
| `hostname` | `'0.0.0.0'` | 监听地址 |
|
|
184
|
-
| `
|
|
185
|
-
| `
|
|
186
|
-
| `shutdown` | `true` |
|
|
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
|
|
146
|
+
### cors —— CORS 中间件
|
|
189
147
|
|
|
190
148
|
```ts
|
|
149
|
+
app.use(cors())
|
|
150
|
+
|
|
191
151
|
app.use(cors({
|
|
192
|
-
origin: ['https://example.com'],
|
|
193
|
-
|
|
152
|
+
origin: ['https://app.example.com'],
|
|
153
|
+
methods: ['GET', 'POST'],
|
|
194
154
|
}))
|
|
195
155
|
```
|
|
196
156
|
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
```
|
|
166
|
+
import { serveStatic } from 'weifuwu'
|
|
202
167
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
168
|
+
// 单一路径
|
|
169
|
+
app.get('/static/*', serveStatic('./public'))
|
|
170
|
+
|
|
171
|
+
// 多目录
|
|
172
|
+
app.get('/uploads/*', serveStatic('./uploads'))
|
|
173
|
+
```
|
|
207
174
|
|
|
208
|
-
### postgres
|
|
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
|
-
|
|
215
|
-
|
|
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
|
-
| `
|
|
222
|
-
|
|
223
|
-
依赖:需要 `postgres` npm 包。实现 `close(): Promise<void>`。
|
|
189
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
190
|
+
|------|------|--------|------|
|
|
191
|
+
| `url` | `string` | `DATABASE_URL` 环境变量 | 连接字符串 |
|
|
192
|
+
| `max` | `number` | `10` | 连接池大小 |
|
|
224
193
|
|
|
225
|
-
### redis
|
|
194
|
+
### redis —— Redis
|
|
226
195
|
|
|
227
196
|
```ts
|
|
197
|
+
import { redis } from 'weifuwu'
|
|
198
|
+
|
|
228
199
|
app.use(redis())
|
|
229
|
-
|
|
230
|
-
//
|
|
231
|
-
|
|
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
|
-
|
|
208
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
209
|
+
|------|------|--------|------|
|
|
210
|
+
| `url` | `string` | `REDIS_URL` 环境变量 | 连接字符串 |
|
|
235
211
|
|
|
236
|
-
### ui
|
|
212
|
+
### ui —— SSR + SPA 渲染
|
|
237
213
|
|
|
238
214
|
```ts
|
|
239
215
|
app.use(ui())
|
|
240
216
|
|
|
241
217
|
// SSR 页面
|
|
242
|
-
app.get('/
|
|
243
|
-
|
|
244
|
-
<
|
|
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)` |
|
|
259
|
-
| `ctx.ui.css(entryPath)` | 编译 CSS
|
|
234
|
+
| `ctx.ui.js(entryPath)` | 编译 TSX → JS bundle |
|
|
235
|
+
| `ctx.ui.css(entryPath)` | 编译 CSS(PostCSS + Tailwind) |
|
|
260
236
|
|
|
261
|
-
### graphql
|
|
237
|
+
### graphql —— GraphQL
|
|
262
238
|
|
|
263
239
|
```ts
|
|
264
|
-
app.graphql(
|
|
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
|
-
|
|
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
|
-
###
|
|
320
|
-
|
|
321
|
-
`ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象层面的写入自动触发渲染,无需手动调用。
|
|
295
|
+
### 组件
|
|
322
296
|
|
|
323
297
|
```tsx
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
-
|
|
303
|
+
// 使用
|
|
304
|
+
<Greeting name="world" />
|
|
341
305
|
```
|
|
342
306
|
|
|
343
|
-
|
|
344
|
-
|------|------|
|
|
345
|
-
| `ctx.ui.$` | 深度 Proxy 对象,所有写入自动触发渲染 |
|
|
346
|
-
| `ctx.ui.dirty()` | ⚠ 极少需要 — 仅当绕过 Proxy 直接操作底层对象时 |
|
|
307
|
+
### 状态 —— 深度 Proxy
|
|
347
308
|
|
|
348
|
-
|
|
309
|
+
`ctx.ui.$` 是**深度 Proxy**:任何属性/数组/对象写入自动触发渲染,无需手动调用。
|
|
349
310
|
|
|
350
311
|
```tsx
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
| `
|
|
359
|
-
| `
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
335
|
+
使用原生 JS 控制流:
|
|
366
336
|
|
|
367
337
|
```tsx
|
|
368
338
|
// 条件
|
|
369
|
-
{
|
|
370
|
-
{
|
|
339
|
+
{cond ? <A /> : <B />}
|
|
340
|
+
{cond && <A />}
|
|
371
341
|
|
|
372
342
|
// 列表
|
|
373
|
-
{
|
|
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
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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="
|
|
452
|
-
<
|
|
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
|
-
|
|
|
460
|
-
|
|
461
|
-
| `path` |
|
|
462
|
-
| `
|
|
463
|
-
| `
|
|
464
|
-
| `
|
|
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
|
-
|
|
424
|
+
**ws —— WebSocket 客户端**
|
|
506
425
|
|
|
507
426
|
```tsx
|
|
508
|
-
app.use(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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
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
|
-
|
|
436
|
+
**api —— HTTP 客户端**
|
|
530
437
|
|
|
531
438
|
```tsx
|
|
532
439
|
app.use(api({ baseURL: '/api' }))
|
|
533
440
|
|
|
534
|
-
//
|
|
535
|
-
await ctx.api
|
|
536
|
-
await ctx.api
|
|
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
|
-
|
|
|
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
|
-
| `
|
|
553
|
-
| `
|
|
554
|
-
| `
|
|
555
|
-
| `
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
616
|
-
|
|
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
|
-
| `
|
|
626
|
-
| `
|
|
627
|
-
| `
|
|
628
|
-
| `
|
|
629
|
-
| `
|
|
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
|
-
|
|
477
|
+
### ErrorBoundary
|
|
636
478
|
|
|
637
479
|
```tsx
|
|
638
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
708
|
-
|
|
709
|
-
| `
|
|
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
|
|
728
|
-
|
|
729
|
-
```bash
|
|
730
|
-
npm install weifuwu # 已包含
|
|
731
|
-
```
|
|
501
|
+
纯 CSS 布局原语 + 主题 Token。不绑定任何 JS 框架。
|
|
732
502
|
|
|
733
503
|
```ts
|
|
734
|
-
// 服务端编译
|
|
735
|
-
app.get('/
|
|
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="/
|
|
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
|
|
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` |
|
|
762
|
-
| | `wf-fixed` |
|
|
763
|
-
| | `wf-auto` |
|
|
764
|
-
| | `wf-shrink` | 可收缩 | `min-width: 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` |
|
|
556
|
+
| | `wf-sticky` | 粘性定位 | `position: sticky` |
|
|
770
557
|
| **容器** | `wf-surface` | 基础面 | `border-radius + box-shadow + bg` |
|
|
771
|
-
| | `wf-grid` |
|
|
558
|
+
| | `wf-grid` | 二维网格 | `display: grid + --wf-cols` |
|
|
772
559
|
| | `wf-container` | 宽度约束 | `max-width + margin: auto` |
|
|
773
|
-
| | `wf-scroll` |
|
|
774
|
-
| | `wf-clip` |
|
|
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` |
|
|
786
|
-
| 语义色 | `--wf-color-success
|
|
787
|
-
| 中性色 | `--wf-color-text
|
|
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`
|
|
790
|
-
| 字重 | `--wf-font-weight-normal`
|
|
791
|
-
| 行高 | `--wf-line-height-tight
|
|
792
|
-
| 字距 | `--wf-letter-spacing
|
|
793
|
-
| 间距 | `--wf-space-xs`
|
|
794
|
-
| 间隔 | `--wf-gap-xs`
|
|
795
|
-
| 圆角 | `--wf-radius-sm`
|
|
796
|
-
| 阴影 | `--wf-shadow-sm`
|
|
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
|
|
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`
|
|
595
|
+
切换 `html` 的 `data-theme` 属性即可自动切换全部主题色:
|
|
807
596
|
|
|
808
597
|
```ts
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
引入
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
612
|
+
| 变量 | 用途 | 默认值 |
|
|
613
|
+
|------|------|--------|
|
|
614
|
+
| `DATABASE_URL` | PostgreSQL 连接字符串 | — |
|
|
615
|
+
| `REDIS_URL` | Redis 连接字符串 | — |
|
|
846
616
|
|
|
847
|
-
|
|
617
|
+
---
|
|
848
618
|
|
|
849
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
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
|
-
|
|
891
|
-
|
|
892
|
-
# http://localhost:3000
|
|
893
|
-
```
|
|
663
|
+
# 构建
|
|
664
|
+
npm run build
|
|
894
665
|
|
|
895
|
-
|
|
666
|
+
# 类型检查
|
|
667
|
+
npm run typecheck
|
|
896
668
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
## 项目结构
|
|
669
|
+
# 测试
|
|
670
|
+
npm test
|
|
900
671
|
|
|
901
|
-
|
|
902
|
-
|
|
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
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
npm
|
|
930
|
-
|
|
676
|
+
## 设计原则
|
|
677
|
+
|
|
678
|
+
- **后端为工具箱** —— 提供 HTTP 路由、数据库、中间件原语,不捆绑业务模块
|
|
679
|
+
- **全栈单包** —— `npm install weifuwu` = 后端 + 前端 + 布局
|
|
680
|
+
- **Web 标准优先** —— 所有 handler 使用 `(req: Request, ctx: Context) => Response`
|
|
681
|
+
- **零外部依赖** —— 前端和布局没有任何 npm 运行时依赖
|
|
682
|
+
- **LLM 友好** —— 模块总览表 + 一致格式 + 清晰依赖链
|