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