weifuwu 0.33.10 → 0.34.0
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 +685 -178
- package/dist/client/app.d.ts +11 -27
- package/dist/client/index.d.ts +26 -136
- package/dist/client/index.js +665 -630
- package/dist/client/middleware/api.d.ts +82 -0
- package/dist/client/middleware/auth.d.ts +24 -0
- package/dist/client/middleware/ws.d.ts +4 -10
- package/dist/client/render.d.ts +17 -0
- package/dist/client/router.d.ts +5 -21
- package/dist/client/types.d.ts +52 -88
- package/dist/client/vnode.d.ts +38 -0
- package/dist/core/router.d.ts +22 -4
- package/dist/index.d.ts +3 -0
- package/dist/index.js +218 -56
- package/dist/middleware/static.d.ts +8 -3
- package/dist/postgres/client.d.ts +1 -1
- package/dist/postgres/index.d.ts +0 -1
- package/dist/postgres/types.d.ts +5 -1
- package/dist/redis/client.d.ts +1 -1
- package/dist/request.d.ts +23 -0
- package/dist/response.d.ts +46 -0
- package/dist/types.d.ts +29 -0
- package/package.json +2 -3
- package/dist/client/jsx-runtime.d.ts +0 -481
- package/dist/client/jsx-runtime.js +0 -743
- package/dist/client/signal.d.ts +0 -65
- package/dist/postgres/module.d.ts +0 -12
package/README.md
CHANGED
|
@@ -1,119 +1,270 @@
|
|
|
1
1
|
# weifuwu
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**全栈框架 — 后端 `(req, ctx) => Response` + 前端 `(props, ctx) => JSX`**
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install weifuwu
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
一个包,无上游依赖。后端提供 HTTP 路由、数据库、中间件;前端提供 VDOM + Proxy 驱动的前端框架。
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
-
##
|
|
13
|
+
## 模块总览
|
|
14
|
+
|
|
15
|
+
| 模块 | 导出 | 用途 | 依赖 |
|
|
16
|
+
|------|------|------|------|
|
|
17
|
+
| **Router** | `Router` | HTTP 路由 + 中间件链 + WebSocket + GraphQL | — |
|
|
18
|
+
| **serve** | `serve` | HTTP 服务器 | `Router` |
|
|
19
|
+
| **cors** | `cors` | CORS 跨域中间件 | `Router` |
|
|
20
|
+
| **serveStatic** | `serveStatic` | 静态文件服务 | `Router` |
|
|
21
|
+
| **postgres** | `postgres` | PostgreSQL 客户端 → `ctx.sql` | `Router` |
|
|
22
|
+
| **redis** | `redis` | Redis 客户端 → `ctx.redis` | `Router` |
|
|
23
|
+
| **ui** | `ui` | SSR 渲染 + 动态 JS 编译 → `ctx.ui.html/css/js` | `Router` |
|
|
24
|
+
| **graphql** | `router.graphql()` | GraphQL 端点 | `Router` |
|
|
25
|
+
| **client** | — | 前端 VDOM + Proxy 框架 | — |
|
|
26
|
+
|
|
27
|
+
**前端 `weifuwu/client` 模块总览:**
|
|
28
|
+
|
|
29
|
+
| 类别 | 导出 | 用途 |
|
|
30
|
+
|------|------|------|
|
|
31
|
+
| **渲染引擎** | `VNode`, `Component` | 虚拟 DOM 节点与组件类型 | — |
|
|
32
|
+
| **JSX 运行时** | `jsx`/`jsxs`/`jsxDEV`, `Fragment` | TSX 编译目标 | — |
|
|
33
|
+
| **应用** | `createApp` | 中间件链 + 挂载 | — |
|
|
34
|
+
| **路由** | `router`, `RouteView` | 嵌套布局路由 | — |
|
|
35
|
+
| **中间件** | `ws`, `api`, `auth` | WebSocket / HTTP 客户端 / 认证状态 | — |
|
|
36
|
+
| **工具** | `extendCtx` | ctx 扩展 | — |
|
|
37
|
+
| **类型** | `WfuiContext`, `AppMiddleware`, `RouteDef`, `VNodeType`, `Component`, `ApiClient`, `AuthClient` | — | — |
|
|
14
38
|
|
|
15
|
-
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## 核心理念:`ctx`
|
|
42
|
+
|
|
43
|
+
前后端共享同一模式:**中间件向 `ctx` 注入字段,handler/组件从 `ctx` 读取。**
|
|
16
44
|
|
|
17
45
|
```
|
|
18
|
-
|
|
19
|
-
Request → Middleware → Handler
|
|
20
|
-
│
|
|
21
|
-
▼
|
|
22
|
-
ctx.sql
|
|
23
|
-
ctx.redis
|
|
24
|
-
ctx.ui
|
|
46
|
+
后端: 前端:
|
|
47
|
+
Request → Middleware → Handler createApp() → Middleware → Component
|
|
48
|
+
│ │
|
|
49
|
+
▼ ▼
|
|
50
|
+
ctx.sql ctx.ws
|
|
51
|
+
ctx.redis ctx.route
|
|
52
|
+
ctx.ui ctx.api / ctx.auth
|
|
25
53
|
```
|
|
26
54
|
|
|
27
|
-
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 快速开始 — 全栈应用
|
|
58
|
+
|
|
28
59
|
```ts
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
60
|
+
// server.ts — 同一个 npm 包
|
|
61
|
+
import { serve, Router, cors, ui } from 'weifuwu'
|
|
62
|
+
|
|
63
|
+
const app = new Router()
|
|
64
|
+
app.use(cors())
|
|
65
|
+
app.use(ui())
|
|
66
|
+
|
|
67
|
+
// REST API
|
|
68
|
+
app.get('/api/posts', async (req, ctx) => {
|
|
69
|
+
const posts = [{ id: 1, title: 'Hello' }]
|
|
70
|
+
return Response.json(posts)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// WebSocket
|
|
74
|
+
app.ws('/ws', {
|
|
75
|
+
open(ws) { ws.send(JSON.stringify({ type: 'system', body: 'connected' })) },
|
|
76
|
+
message(ws, ctx, data) {
|
|
77
|
+
const msg = JSON.parse(data.toString())
|
|
78
|
+
ws.send(JSON.stringify({ type: 'echo', body: msg.body }))
|
|
79
|
+
},
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// SPA 入口 — 动态编译前端(零构建步骤)
|
|
83
|
+
app.get('/', async (req, ctx) => ctx.ui.html`
|
|
84
|
+
<!DOCTYPE html>
|
|
85
|
+
<html><body><div id="root"></div>
|
|
86
|
+
<script src="/static/app.js"></script></body></html>
|
|
87
|
+
`)
|
|
88
|
+
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
89
|
+
|
|
90
|
+
serve(app, { port: 3000 })
|
|
32
91
|
```
|
|
33
92
|
|
|
34
|
-
**Frontend:**
|
|
35
93
|
```tsx
|
|
36
|
-
|
|
37
|
-
|
|
94
|
+
// src/main.tsx — 前端
|
|
95
|
+
import { createApp, router, RouteView, ws, api, auth } from 'weifuwu/client'
|
|
96
|
+
import type { WfuiContext, RouteDef } from 'weifuwu/client'
|
|
97
|
+
|
|
98
|
+
// 组件 = (props, ctx) => VNode
|
|
99
|
+
function Home(_props: {}, ctx: WfuiContext) {
|
|
100
|
+
const $ = ctx.ui.$
|
|
101
|
+
if (!ctx.ui.ready) { $.items = [{ id: 1, text: 'hello' }] }
|
|
102
|
+
return <div>{$.items.map((i: any) => <div key={i.id}>{i.text}</div>)}</div>
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const app = createApp()
|
|
106
|
+
app.use(api({ baseURL: '' }))
|
|
107
|
+
app.use(auth())
|
|
108
|
+
app.use(ws())
|
|
109
|
+
app.use(router({
|
|
110
|
+
routes: [
|
|
111
|
+
{ path: '/', component: Home },
|
|
112
|
+
{
|
|
113
|
+
path: '/dashboard',
|
|
114
|
+
layout: DashboardLayout,
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
mode: 'hash',
|
|
119
|
+
scrollRestoration: true,
|
|
120
|
+
}))
|
|
121
|
+
app.mount('#root', AppShell)
|
|
122
|
+
|
|
123
|
+
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
124
|
+
return (
|
|
125
|
+
<div>
|
|
126
|
+
<nav>{/* ... */}</nav>
|
|
127
|
+
<main><RouteView /></main>
|
|
128
|
+
</div>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
38
131
|
```
|
|
39
132
|
|
|
40
133
|
---
|
|
41
134
|
|
|
42
|
-
##
|
|
135
|
+
## 后端
|
|
43
136
|
|
|
44
|
-
###
|
|
137
|
+
### Router
|
|
45
138
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
| `Router` | class | HTTP router + middleware chain + `.ws()` + `.graphql()` |
|
|
49
|
-
| `serve` | function | HTTP server |
|
|
50
|
-
| `cors` | middleware | CORS headers |
|
|
51
|
-
| `postgres` | middleware | PostgreSQL client → `ctx.sql` |
|
|
52
|
-
| `redis` | middleware | Redis client → `ctx.redis` |
|
|
53
|
-
| `serveStatic` | middleware | Static file serving |
|
|
54
|
-
| `ui` | middleware | SSR/SPA rendering → `ctx.ui.html/css/js` |
|
|
55
|
-
| `HttpError` | class | HTTP error with status code |
|
|
56
|
-
| `DEFAULT_MAX_BODY` | constant | Default 10MB body limit |
|
|
57
|
-
| `MIGRATIONS_TABLE` | constant | Postgres migrations table name |
|
|
139
|
+
```ts
|
|
140
|
+
const app = new Router()
|
|
58
141
|
|
|
59
|
-
|
|
142
|
+
// HTTP 方法
|
|
143
|
+
app.get(path, ...handlers)
|
|
144
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
145
|
+
app.all(path, ...handlers)
|
|
60
146
|
|
|
61
|
-
|
|
147
|
+
// WebSocket / GraphQL
|
|
148
|
+
app.ws(path, handler)
|
|
149
|
+
app.graphql('/graphql', handler)
|
|
62
150
|
|
|
63
|
-
|
|
151
|
+
// 中间件
|
|
152
|
+
app.use(middleware)
|
|
153
|
+
app.mount(prefix, subRouter)
|
|
154
|
+
app.onError(handler)
|
|
64
155
|
|
|
65
|
-
|
|
66
|
-
|
|
156
|
+
// 调试
|
|
157
|
+
app.routes() // 列出所有路由
|
|
158
|
+
```
|
|
67
159
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
160
|
+
| 方法 | 参数 | 说明 |
|
|
161
|
+
|------|------|------|
|
|
162
|
+
| `get/post/put/delete/patch/head/options` | `(path, ...handlers)` | 注册 HTTP 路由 |
|
|
163
|
+
| `all` | `(path, ...handlers)` | 匹配所有方法 |
|
|
164
|
+
| `ws` | `(path, handler)` | WebSocket 端点 |
|
|
165
|
+
| `graphql` | `('/path', handler)` | GraphQL 端点 |
|
|
166
|
+
| `use` | `(middleware)` | 全局中间件 |
|
|
167
|
+
| `mount` | `(prefix, subRouter)` | 子路由挂载 |
|
|
168
|
+
| `onError` | `(handler)` | 全局错误处理 |
|
|
169
|
+
| `routes` | `()` | 返回路由列表数组 |
|
|
72
170
|
|
|
73
|
-
|
|
74
|
-
app.get('/api/hello', async (req, ctx) => {
|
|
75
|
-
return Response.json({ message: 'hello' })
|
|
76
|
-
})
|
|
171
|
+
### serve — HTTP 服务器
|
|
77
172
|
|
|
78
|
-
|
|
79
|
-
app
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
173
|
+
```ts
|
|
174
|
+
const srv = serve(app, { port: 3000 })
|
|
175
|
+
await srv.stop() // 程序化停止
|
|
176
|
+
// Ctrl+C / SIGTERM — 自动关闭所有连接后退出
|
|
177
|
+
```
|
|
83
178
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
179
|
+
| 选项 | 默认 | 说明 |
|
|
180
|
+
|------|------|------|
|
|
181
|
+
| `port` | `0` | 监听端口 |
|
|
182
|
+
| `hostname` | `'0.0.0.0'` | 监听地址 |
|
|
183
|
+
| `maxBodySize` | `10MB` | 请求体上限 |
|
|
184
|
+
| `timeout` | `30000` | Socket 超时 (ms) |
|
|
185
|
+
| `shutdown` | `true` | 是否注册 SIGTERM/SIGINT 处理 |
|
|
186
|
+
|
|
187
|
+
### cors — CORS 中间件
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
app.use(cors({
|
|
191
|
+
origin: ['https://example.com'],
|
|
192
|
+
credentials: true,
|
|
89
193
|
}))
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### serveStatic — 静态文件
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
app.use(serveStatic('./public', { prefix: '/assets' }))
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
| 选项 | 默认 | 说明 |
|
|
203
|
+
|------|------|------|
|
|
204
|
+
| `prefix` | `''` | URL 前缀 |
|
|
205
|
+
| `index` | `'index.html'` | 默认首页 |
|
|
206
|
+
|
|
207
|
+
### postgres — PostgreSQL
|
|
90
208
|
|
|
91
|
-
|
|
209
|
+
```ts
|
|
210
|
+
app.use(postgres())
|
|
211
|
+
// → ctx.sql`SELECT * FROM users`
|
|
212
|
+
|
|
213
|
+
app.use(postgres({ url: 'postgres://user:pass@host:5432/db' }))
|
|
214
|
+
// 默认读取 DATABASE_URL 环境变量
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
| 选项 | 默认 | 说明 |
|
|
218
|
+
|------|------|------|
|
|
219
|
+
| `url` | `DATABASE_URL` | 连接字符串 |
|
|
220
|
+
| `migrations` | `'./migrations'` | 迁移文件目录 |
|
|
221
|
+
|
|
222
|
+
依赖:需要 `postgres` npm 包。实现 `close(): Promise<void>`。
|
|
223
|
+
|
|
224
|
+
### redis — Redis
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
app.use(redis())
|
|
228
|
+
// → ctx.redis.get('key')
|
|
229
|
+
// → ctx.redis.set('key', 'value')
|
|
230
|
+
// 默认读取 REDIS_URL 环境变量
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
依赖:需要 `ioredis` npm 包。实现 `close(): Promise<void>`。
|
|
234
|
+
|
|
235
|
+
### ui — SSR + SPA 渲染
|
|
236
|
+
|
|
237
|
+
```ts
|
|
238
|
+
app.use(ui())
|
|
239
|
+
|
|
240
|
+
// SSR 页面
|
|
92
241
|
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
93
242
|
<!DOCTYPE html>
|
|
94
243
|
<html><body><h1>${post.title}</h1></body></html>
|
|
95
244
|
`)
|
|
96
245
|
|
|
97
|
-
//
|
|
246
|
+
// 动态 JS 编译(esbuild,零构建步骤)
|
|
98
247
|
app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
99
248
|
|
|
100
|
-
|
|
249
|
+
// CSS 编译(PostCSS + Tailwind)
|
|
250
|
+
app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
101
251
|
```
|
|
102
252
|
|
|
103
|
-
|
|
253
|
+
| 方法 | 用途 |
|
|
254
|
+
|------|------|
|
|
255
|
+
| `ctx.ui.html\`...\`` | 渲染 HTML 模板(转义变量防 XSS) |
|
|
256
|
+
| `ctx.ui.html.unsafe(str)` | 插入原始 HTML |
|
|
257
|
+
| `ctx.ui.js(entryPath)` | 动态编译 TSX → JS bundle |
|
|
258
|
+
| `ctx.ui.css(entryPath)` | 编译 CSS (PostCSS + Tailwind) |
|
|
259
|
+
|
|
260
|
+
### graphql — GraphQL
|
|
104
261
|
|
|
105
262
|
```ts
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
app.graphql(handler) // GraphQL at /
|
|
112
|
-
app.graphql('/graphql', handler) // GraphQL at /graphql
|
|
113
|
-
app.use(middleware) // Global middleware
|
|
114
|
-
app.mount(prefix, subRouter) // Sub-router
|
|
115
|
-
app.onError(handler) // Error handler
|
|
116
|
-
app.routes() // Debug: list all routes
|
|
263
|
+
app.graphql(async (req, ctx) => ({
|
|
264
|
+
schema: `type Query { hello: String }`,
|
|
265
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
266
|
+
graphiql: true,
|
|
267
|
+
}))
|
|
117
268
|
```
|
|
118
269
|
|
|
119
270
|
### WebSocket
|
|
@@ -127,143 +278,503 @@ app.ws('/ws', {
|
|
|
127
278
|
})
|
|
128
279
|
```
|
|
129
280
|
|
|
130
|
-
###
|
|
281
|
+
### 错误处理
|
|
131
282
|
|
|
132
283
|
```ts
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
})
|
|
284
|
+
app.onError((err, req, ctx) => {
|
|
285
|
+
if (err instanceof HttpError) {
|
|
286
|
+
return new Response(err.message, { status: err.status })
|
|
287
|
+
}
|
|
288
|
+
console.error(err)
|
|
289
|
+
return new Response('Internal Server Error', { status: 500 })
|
|
290
|
+
})
|
|
291
|
+
```
|
|
139
292
|
|
|
140
|
-
|
|
141
|
-
|
|
293
|
+
| 类/常量 | 说明 |
|
|
294
|
+
|---------|------|
|
|
295
|
+
| `HttpError` | HTTP 错误 `new HttpError(msg, status)` |
|
|
296
|
+
| `DEFAULT_MAX_BODY` | 默认请求体上限 10MB |
|
|
297
|
+
| `MIGRATIONS_TABLE` | Postgres 迁移表名 |
|
|
298
|
+
|
|
299
|
+
### 后端类型
|
|
300
|
+
|
|
301
|
+
`Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## 前端 (`weifuwu/client`)
|
|
306
|
+
|
|
307
|
+
**2750 行源码,28 个运行时导出 + 19 个类型,零外部依赖。**
|
|
308
|
+
|
|
309
|
+
构建配置(esbuild):
|
|
310
|
+
```js
|
|
311
|
+
esbuild.build({
|
|
312
|
+
jsx: 'automatic',
|
|
313
|
+
jsxImportSource: 'weifuwu/client',
|
|
314
|
+
bundle: true,
|
|
315
|
+
})
|
|
142
316
|
```
|
|
143
317
|
|
|
144
|
-
###
|
|
318
|
+
### 状态管理
|
|
145
319
|
|
|
146
|
-
```
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
320
|
+
```tsx
|
|
321
|
+
const $ = ctx.ui.$ // 获取状态对象
|
|
322
|
+
|
|
323
|
+
$.count = 0 // 赋值 → 自动渲染
|
|
324
|
+
$.items = [...$.items, newItem] // 不可变更新
|
|
325
|
+
$.items.push(newItem); ctx.ui.dirty() // 可变更新需显式 dirty
|
|
326
|
+
|
|
327
|
+
// 派生值 — 每次 render 重新计算
|
|
328
|
+
const doubled = $.count * 2
|
|
329
|
+
const completed = $.todos.filter(t => t.done)
|
|
150
330
|
```
|
|
151
331
|
|
|
152
|
-
|
|
332
|
+
| API | 说明 |
|
|
333
|
+
|------|------|
|
|
334
|
+
| `ctx.ui.$` | 状态代理对象,赋值自动触发渲染 |
|
|
335
|
+
| `ctx.ui.dirty()` | 显式标记脏状态(嵌套突变后用) |
|
|
336
|
+
| `ctx.ui.render()` | 同步立即渲染 |
|
|
337
|
+
|
|
338
|
+
**原理**:`ctx.ui.$` 是 Proxy 对象,任何属性赋值(`$.x = val`)自动调用 `queueMicrotask(render)`。\
|
|
339
|
+
多次赋值在同一个微任务内合并为一次渲染。
|
|
153
340
|
|
|
154
|
-
|
|
341
|
+
### JSX 运行时
|
|
155
342
|
|
|
156
|
-
|
|
343
|
+
```tsx
|
|
344
|
+
// 自动由 esbuild 调用(无需手动导入)
|
|
345
|
+
<div class="foo">hello</div>
|
|
346
|
+
<Fragment>...</Fragment>
|
|
347
|
+
```
|
|
157
348
|
|
|
158
|
-
|
|
|
159
|
-
|
|
160
|
-
| `
|
|
161
|
-
| `
|
|
162
|
-
| `Fragment` | component | `<></>` |
|
|
163
|
-
| `Show` | component | Conditional rendering |
|
|
164
|
-
| `For` | component | Keyed list rendering |
|
|
165
|
-
| `onMount`, `onCleanup` | function | Lifecycle hooks |
|
|
166
|
-
| `createApp` | function | App instance with middleware chain |
|
|
167
|
-
| `router` | middleware | Hash/history router → `ctx.route` |
|
|
168
|
-
| `RouteView` | component | Route outlet |
|
|
169
|
-
| `ws` | middleware | WebSocket client → `ctx.ws` |
|
|
349
|
+
| 导出 | 说明 |
|
|
350
|
+
|------|------|
|
|
351
|
+
| `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
|
|
352
|
+
| `Fragment` | `<></>` 片段组件 |
|
|
170
353
|
|
|
171
|
-
|
|
354
|
+
**Signal 属性自动绑定:** `<input value={signalVal} />` — 信号变化时只更新对应 DOM 属性。
|
|
172
355
|
|
|
173
|
-
###
|
|
356
|
+
### 条件与列表
|
|
357
|
+
|
|
358
|
+
使用原生 JS 表达式,不需要框架 API:
|
|
174
359
|
|
|
175
360
|
```tsx
|
|
176
|
-
|
|
177
|
-
|
|
361
|
+
// 条件
|
|
362
|
+
{$.loading && <Loading />}
|
|
363
|
+
{$.error ? <Error msg={$.error} /> : <Content />}
|
|
364
|
+
|
|
365
|
+
// 列表
|
|
366
|
+
{$.items.map(item => (
|
|
367
|
+
<div key={item.id}>{item.name}</div>
|
|
368
|
+
))}
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
| 模式 | 说明 |
|
|
372
|
+
|------|------|
|
|
373
|
+
| `{cond && <A/>}` | 条件渲染(cond 为 true 时渲染 A) |
|
|
374
|
+
| `{cond ? <A/> : <B/>}` | 二选一 |
|
|
375
|
+
| `{arr.map(x => <div key={x.id}/>)}` | 列表渲染,必须加 `key` |
|
|
376
|
+
|
|
377
|
+
### 生命周期
|
|
378
|
+
|
|
379
|
+
使用 `ref` 回调替代生命周期钩子:
|
|
380
|
+
|
|
381
|
+
```tsx
|
|
382
|
+
<div ref={el => {
|
|
383
|
+
if (el) {
|
|
384
|
+
// 挂载:el 是 DOM 元素
|
|
385
|
+
fetchData()
|
|
386
|
+
el.addEventListener('scroll', handler)
|
|
387
|
+
}
|
|
388
|
+
if (!el) {
|
|
389
|
+
// 卸载:el 为 null
|
|
390
|
+
cleanup()
|
|
391
|
+
}
|
|
392
|
+
}} />
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
`ref` 在挂载时传入 DOM 元素,卸载时传入 `null`。一个回调覆盖 mount/unmount 两个场景。
|
|
396
|
+
|
|
397
|
+
### 应用
|
|
398
|
+
|
|
399
|
+
```tsx
|
|
400
|
+
const app = createApp()
|
|
401
|
+
app.use(middleware1)
|
|
402
|
+
app.use(middleware2)
|
|
403
|
+
await app.mount('#root', AppShell)
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
| 方法 | 说明 |
|
|
407
|
+
|------|------|
|
|
408
|
+
| `use(mw)` | 注册中间件,返回 `this` 支持链式 |
|
|
409
|
+
| `mount(selector, RootComponent)` | 挂载到 DOM |
|
|
410
|
+
| `hydrate(selector, Component, props?)` | 在 SSR 内容上附加组件 |
|
|
411
|
+
| `ctx` | 当前上下文 |
|
|
412
|
+
|
|
413
|
+
### 路由
|
|
178
414
|
|
|
415
|
+
```tsx
|
|
416
|
+
// 路由定义
|
|
179
417
|
const routes: RouteDef[] = [
|
|
180
418
|
{ path: '/', component: HomePage },
|
|
181
|
-
{
|
|
419
|
+
{
|
|
420
|
+
path: '/dashboard',
|
|
421
|
+
layout: DashboardLayout, // 嵌套布局
|
|
422
|
+
children: [
|
|
423
|
+
{ path: '/overview', component: Overview }, // 子路由
|
|
424
|
+
{ path: '/settings', component: Settings },
|
|
425
|
+
],
|
|
426
|
+
},
|
|
427
|
+
{ path: '/user/:id', component: UserPage, title: '用户' },
|
|
182
428
|
]
|
|
183
429
|
|
|
184
|
-
|
|
185
|
-
app.use(
|
|
186
|
-
|
|
187
|
-
|
|
430
|
+
// 注册路由中间件
|
|
431
|
+
app.use(router({
|
|
432
|
+
routes,
|
|
433
|
+
notFound: NotFound,
|
|
434
|
+
mode: 'hash', // 'hash' | 'history'
|
|
435
|
+
scrollRestoration: true,
|
|
436
|
+
}))
|
|
188
437
|
|
|
189
|
-
|
|
438
|
+
// 路由出口 — 根层级和嵌套层级用同一个组件
|
|
439
|
+
function AppShell() {
|
|
440
|
+
return <main><RouteView /></main> // 根出口
|
|
441
|
+
}
|
|
442
|
+
function DashboardLayout() {
|
|
190
443
|
return (
|
|
191
|
-
<div>
|
|
192
|
-
<
|
|
193
|
-
<main><RouteView /></main>
|
|
444
|
+
<div class="flex">
|
|
445
|
+
<Sidebar />
|
|
446
|
+
<main><RouteView /></main> // 嵌套出口(同一组件)
|
|
194
447
|
</div>
|
|
195
448
|
)
|
|
196
449
|
}
|
|
197
450
|
```
|
|
198
451
|
|
|
199
|
-
|
|
452
|
+
| RouteDef 字段 | 类型 | 说明 |
|
|
453
|
+
|---------------|------|------|
|
|
454
|
+
| `path` | `string` | 路由路径,支持 `/:param` |
|
|
455
|
+
| `component` | `Component` | 路由组件 |
|
|
456
|
+
| `layout` | `Component` | 嵌套布局(渲染 `<RouteView />` 显示子路由)|
|
|
457
|
+
| `children` | `RouteDef[]` | 子路由(与 layout 配合使用)|
|
|
458
|
+
| `auth` | `boolean` | 是否需要登录 |
|
|
459
|
+
| `title` | `string` | 页面标题(自动设置 `document.title`)|
|
|
460
|
+
| `loader` | `(ctx) => Promise<data>` | 数据预取 → `ctx.route.data` |
|
|
461
|
+
| `transition` | `string` | 页面切换过渡动画 CSS class 前缀 |
|
|
462
|
+
|
|
463
|
+
| RouterOptions | 默认 | 说明 |
|
|
464
|
+
|---------------|------|------|
|
|
465
|
+
| `mode` | `'hash'` | 路由模式 |
|
|
466
|
+
| `notFound` | — | 404 组件 |
|
|
467
|
+
| `scrollRestoration` | `true` | 历史模式时恢复滚动位置 |
|
|
468
|
+
| `transition` | — | 全局过渡动画 |
|
|
469
|
+
|
|
470
|
+
**`ctx.route` 注入:**
|
|
200
471
|
|
|
201
472
|
```tsx
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
473
|
+
ctx.route.path // '/user/42'
|
|
474
|
+
ctx.route.params // { id: '42' }
|
|
475
|
+
ctx.route.query // { tab: 'profile' }
|
|
476
|
+
ctx.route.component // 当前路由组件
|
|
477
|
+
ctx.route.data // loader 返回的数据
|
|
478
|
+
ctx.route.loading // loader 是否加载中
|
|
479
|
+
ctx.app.navigate('/path')
|
|
206
480
|
```
|
|
207
481
|
|
|
208
|
-
###
|
|
482
|
+
### 代码分割
|
|
209
483
|
|
|
210
484
|
```tsx
|
|
211
|
-
|
|
212
|
-
<
|
|
213
|
-
|
|
485
|
+
const AdminPage = lazy(() => import('./pages/AdminPage'), {
|
|
486
|
+
fallback: () => <div>加载中...</div>,
|
|
487
|
+
})
|
|
214
488
|
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
489
|
+
const routes = [
|
|
490
|
+
{ path: '/admin', component: AdminPage },
|
|
491
|
+
]
|
|
218
492
|
```
|
|
219
493
|
|
|
220
|
-
|
|
494
|
+
需 esbuild `splitting: true` + `outdir`。
|
|
495
|
+
|
|
496
|
+
### 中间件
|
|
497
|
+
|
|
498
|
+
#### ws — WebSocket 客户端
|
|
221
499
|
|
|
222
500
|
```tsx
|
|
223
501
|
app.use(ws({ url: '/ws' }))
|
|
224
502
|
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
503
|
+
// 组件中:
|
|
504
|
+
const unsub = ctx.ws?.onMessage((data) => { ... })
|
|
505
|
+
// 清理在 ref 中处理
|
|
506
|
+
ctx.ws?.send({ type: 'chat', body: 'hello' })
|
|
507
|
+
{ctx.ws?.isConnected && <span>🟢 已连接</span>}
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
| `ctx.ws` | 类型 | 说明 |
|
|
511
|
+
|----------|------|------|
|
|
512
|
+
| `send` | `(data: unknown) => void` | 发送消息 |
|
|
513
|
+
| `onMessage` | `(handler) => dispose()` | 注册消息监听 |
|
|
514
|
+
| `isConnected` | `Signal<boolean>` | 连接状态信号 |
|
|
515
|
+
|
|
516
|
+
| 选项 | 默认 | 说明 |
|
|
517
|
+
|------|------|------|
|
|
518
|
+
| `url` | `'/ws'` | WebSocket 地址 |
|
|
519
|
+
| `reconnectInterval` | `3000` | 重连间隔 (ms) |
|
|
520
|
+
| `maxReconnect` | `10` | 最大重连次数 |
|
|
521
|
+
|
|
522
|
+
#### api — HTTP 客户端
|
|
523
|
+
|
|
524
|
+
```tsx
|
|
525
|
+
app.use(api({ baseURL: '/api' }))
|
|
526
|
+
|
|
527
|
+
// 组件中:
|
|
528
|
+
await ctx.api.get<User[]>('/users')
|
|
529
|
+
await ctx.api.post<User>('/users', body)
|
|
530
|
+
await ctx.api.put<User>('/users/1', body)
|
|
531
|
+
await ctx.api.patch<User>('/users/1', body)
|
|
532
|
+
await ctx.api.delete('/users/1')
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
| `ctx.api` | 签名 | 说明 |
|
|
536
|
+
|-----------|------|------|
|
|
537
|
+
| `get` | `<T>(url, opts?) => Promise<T>` | GET 请求 |
|
|
538
|
+
| `post` | `<T>(url, body?, opts?) => Promise<T>` | POST 请求 |
|
|
539
|
+
| `put` | `<T>(url, body?, opts?) => Promise<T>` | PUT 请求 |
|
|
540
|
+
| `patch` | `<T>(url, body?, opts?) => Promise<T>` | PATCH 请求 |
|
|
541
|
+
| `delete` | `<T>(url, opts?) => Promise<T>` | DELETE 请求 |
|
|
542
|
+
|
|
543
|
+
| 选项 | 说明 |
|
|
544
|
+
|------|------|
|
|
545
|
+
| `baseURL` | API 基础路径 |
|
|
546
|
+
| `headers` | 默认请求头 |
|
|
547
|
+
| `onRequest` | 请求拦截器 `({url, init}) => {url, init}` |
|
|
548
|
+
| `onResponse` | 响应拦截器 `(res) => Promise<T>` |
|
|
549
|
+
|
|
550
|
+
错误类型:`ApiError` — 包含 `status` 和 `body`。
|
|
551
|
+
|
|
552
|
+
#### auth — 认证状态管理
|
|
553
|
+
|
|
554
|
+
```tsx
|
|
555
|
+
app.use(auth())
|
|
556
|
+
|
|
557
|
+
// 组件中:
|
|
558
|
+
{ctx.auth?.isLoggedIn ? (
|
|
559
|
+
<div>
|
|
560
|
+
<span>{ctx.auth?.user?.name}</span>
|
|
561
|
+
<button onClick={() => ctx.auth?.logout()}>退出</button>
|
|
562
|
+
</div>
|
|
563
|
+
) : (
|
|
564
|
+
<Login />
|
|
565
|
+
)}
|
|
566
|
+
|
|
567
|
+
// 登录
|
|
568
|
+
ctx.auth.login('jwt-token', { id: 1, name: 'Alice' })
|
|
569
|
+
// 退出
|
|
570
|
+
ctx.auth.logout()
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
| `ctx.auth` | 类型 | 说明 |
|
|
574
|
+
|-----------|------|------|
|
|
575
|
+
| `token` | `Signal<string \| null>` | 当前 token |
|
|
576
|
+
| `user` | `Signal<AuthUser \| null>` | 当前用户 |
|
|
577
|
+
| `isLoggedIn` | `Signal<boolean>` | 是否已登录(computed)|
|
|
578
|
+
| `login` | `(token, user) => void` | 存储 token + 用户到 localStorage |
|
|
579
|
+
| `logout` | `() => void` | 清除 token + 用户 |
|
|
580
|
+
| `setUser` | `(user) => void` | 更新用户信息 |
|
|
581
|
+
| `authorizationHeader` | `Signal<string \| null>` | `'Bearer xxx'` 或 `null` |
|
|
582
|
+
|
|
583
|
+
| 选项 | 默认 | 说明 |
|
|
584
|
+
|------|------|------|
|
|
585
|
+
| `storage` | `localStorage` | 存储方式 |
|
|
586
|
+
| `tokenKey` | `'weifuwu_token'` | token 存储 key |
|
|
587
|
+
| `userKey` | `'weifuwu_user'` | 用户信息存储 key |
|
|
588
|
+
|
|
589
|
+
### 工具
|
|
590
|
+
|
|
591
|
+
#### useForm — 表单管理
|
|
592
|
+
|
|
593
|
+
```tsx
|
|
594
|
+
const form = useForm({
|
|
595
|
+
initial: { name: '', email: '' },
|
|
596
|
+
validate: {
|
|
597
|
+
name: (v) => !v ? '请输入姓名' : null,
|
|
598
|
+
email: [
|
|
599
|
+
(v) => !v ? '请输入邮箱' : null,
|
|
600
|
+
(v) => !v.includes('@') ? '邮箱格式错误' : null,
|
|
601
|
+
],
|
|
602
|
+
},
|
|
603
|
+
onSubmit: async (values) => {
|
|
604
|
+
await ctx.api.post('/users', values)
|
|
605
|
+
},
|
|
229
606
|
})
|
|
230
|
-
|
|
231
|
-
|
|
607
|
+
|
|
608
|
+
// JSX:
|
|
609
|
+
<form onSubmit={form.handleSubmit}>
|
|
610
|
+
<input {...form.field('name')} />
|
|
611
|
+
<span>{form.errors.value.name}</span>
|
|
612
|
+
<button disabled={form.submitting}>提交</button>
|
|
613
|
+
</form>
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
| 返回值 | 类型 | 说明 |
|
|
617
|
+
|--------|------|------|
|
|
618
|
+
| `values` | `Signal<T>` | 表单值 |
|
|
619
|
+
| `errors` | `Signal<Partial<Record<keyof T, string\|null>>>` | 验证错误 |
|
|
620
|
+
| `submitting` | `Signal<boolean>` | 提交状态 |
|
|
621
|
+
| `touched` | `Signal<Partial<Record<keyof T, boolean>>>` | 触碰字段 |
|
|
622
|
+
| `handleSubmit` | `(e: Event) => void` | 提交处理(绑定到 `<form>`)|
|
|
623
|
+
| `field` | `(name) => { value, onInput, error }` | 字段绑定对象 |
|
|
624
|
+
| `setValue` | `(name, value) => void` | 设字段值 |
|
|
625
|
+
| `reset` | `() => void` | 重置表单 |
|
|
626
|
+
| `validateAll` | `() => boolean` | 触发全部验证 |
|
|
627
|
+
|
|
628
|
+
#### createResource — 异步数据
|
|
629
|
+
|
|
630
|
+
```tsx
|
|
631
|
+
const [data, { loading, error, refetch }] = createResource(
|
|
632
|
+
() => fetch('/api/posts').then(r => r.json()),
|
|
633
|
+
{ initialValue: [] }
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
// JSX:
|
|
637
|
+
<Show when={loading}><p>加载中...</p></Show>
|
|
638
|
+
<Show when={error}><p>错误: {error.value?.message}</p></Show>
|
|
639
|
+
<Show when={computed(() => !loading.value && !error.value)}>
|
|
640
|
+
<For each={data}>{(item) => <div>{item.title}</div>}</For>
|
|
641
|
+
</Show>
|
|
232
642
|
```
|
|
233
643
|
|
|
234
|
-
|
|
644
|
+
| 返回值 | 类型 | 说明 |
|
|
645
|
+
|--------|------|------|
|
|
646
|
+
| `data` (元组第一项) | `Signal<T \| undefined>` | 数据信号 |
|
|
647
|
+
| `loading` | `Signal<boolean>` | 加载状态 |
|
|
648
|
+
| `error` | `Signal<Error \| undefined>` | 错误信号 |
|
|
649
|
+
| `refetch` | `() => void` | 手动重新加载 |
|
|
650
|
+
|
|
651
|
+
#### ErrorBoundary — 错误捕获
|
|
235
652
|
|
|
236
653
|
```tsx
|
|
237
|
-
|
|
654
|
+
<ErrorBoundary
|
|
655
|
+
fallback={(e) => <p>出错了: {e.message}</p>}
|
|
656
|
+
onError={(e) => console.error(e)}
|
|
657
|
+
>
|
|
658
|
+
{() => <Dashboard />} {/* 必须用 thunk */}
|
|
659
|
+
</ErrorBoundary>
|
|
660
|
+
```
|
|
238
661
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
662
|
+
#### createPortal — 渲染到指定位置
|
|
663
|
+
|
|
664
|
+
```tsx
|
|
665
|
+
<Show when={showModal}>
|
|
666
|
+
{createPortal(<Modal />, document.body)}
|
|
667
|
+
</Show>
|
|
243
668
|
```
|
|
244
669
|
|
|
245
|
-
|
|
670
|
+
#### wrap — 封装三方库为组件
|
|
246
671
|
|
|
247
672
|
```tsx
|
|
248
|
-
|
|
249
|
-
init()
|
|
250
|
-
|
|
673
|
+
const Chart = wrap('div', (el, props: { data: any }, ctx) => {
|
|
674
|
+
const chart = echarts.init(el)
|
|
675
|
+
chart.setOption(props.data)
|
|
676
|
+
effect(() => chart.setOption(props.data))
|
|
677
|
+
return () => chart.dispose() // 卸载时自动清理
|
|
251
678
|
})
|
|
252
|
-
|
|
679
|
+
|
|
680
|
+
// 使用:
|
|
681
|
+
<Chart data={salesData} />
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
#### createContext / extendCtx — 上下文扩展
|
|
685
|
+
|
|
686
|
+
```tsx
|
|
687
|
+
// 类型安全的 provide/inject
|
|
688
|
+
const ThemeCtx = createContext<string>('theme')
|
|
689
|
+
ThemeCtx.provide(ctx, 'dark')
|
|
690
|
+
const theme = ThemeCtx.inject(ctx) // 'dark' | null
|
|
691
|
+
|
|
692
|
+
// 中间件注入
|
|
693
|
+
function myMiddleware(): AppMiddleware {
|
|
694
|
+
return (ctx) => extendCtx(ctx, { myField: 'hello' })
|
|
695
|
+
}
|
|
253
696
|
```
|
|
254
697
|
|
|
255
|
-
###
|
|
698
|
+
### React 对照表
|
|
256
699
|
|
|
257
700
|
| React | weifuwu/client |
|
|
258
701
|
|-------|----------------|
|
|
259
|
-
| `useState(0)` |
|
|
260
|
-
| `useMemo(() => a*2, [a])` | `
|
|
261
|
-
| `useEffect(() => f, [])` | `
|
|
262
|
-
| `
|
|
263
|
-
| `{
|
|
264
|
-
| `
|
|
265
|
-
| `useNavigate()` | `ctx.app
|
|
266
|
-
| `useParams()` | `ctx.route
|
|
702
|
+
| `useState(0)` | `$.count = 0` |
|
|
703
|
+
| `useMemo(() => a*2, [a])` | `const doubled = a * 2`(render 时计算) |
|
|
704
|
+
| `useEffect(() => f, [])` | `if (!ctx.ui.ready) { f() }` |
|
|
705
|
+
| `{cond && <X/>}` | 相同 |
|
|
706
|
+
| `{arr.map(i => <X/>)}` | 相同,加 `key` |
|
|
707
|
+
| `Suspense` | `{$.loading && <Loading/>}` |
|
|
708
|
+
| `useNavigate()` | `ctx.app?.navigate()` |
|
|
709
|
+
| `useParams()` | `ctx.route?.params` |
|
|
710
|
+
| `axios.get()` | `ctx.api?.get()` |
|
|
711
|
+
|
|
712
|
+
### 前端类型
|
|
713
|
+
|
|
714
|
+
`VNode`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `AuthClient`
|
|
715
|
+
|
|
716
|
+
---
|
|
717
|
+
|
|
718
|
+
## 全栈模式
|
|
719
|
+
|
|
720
|
+
### 认证流程
|
|
721
|
+
|
|
722
|
+
```ts
|
|
723
|
+
// 后端
|
|
724
|
+
app.post('/api/login', async (req, ctx) => {
|
|
725
|
+
const { email } = await req.json()
|
|
726
|
+
return Response.json({
|
|
727
|
+
token: 'jwt_' + Math.random().toString(36),
|
|
728
|
+
user: { id: 1, name: email.split('@')[0], email },
|
|
729
|
+
})
|
|
730
|
+
})
|
|
731
|
+
|
|
732
|
+
// 前端
|
|
733
|
+
app.use(api({ baseURL: '' }))
|
|
734
|
+
app.use(auth())
|
|
735
|
+
|
|
736
|
+
// 登录
|
|
737
|
+
const res = await ctx.api.post('/api/login', { email, password })
|
|
738
|
+
ctx.auth.login(res.token, res.user)
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### 异步数据 + SSR
|
|
742
|
+
|
|
743
|
+
```ts
|
|
744
|
+
// 后端 — 同路径既支持 SSR 也支持 API
|
|
745
|
+
app.get('/api/posts', async (req, ctx) => {
|
|
746
|
+
return Response.json(posts)
|
|
747
|
+
})
|
|
748
|
+
|
|
749
|
+
// 前端 — 客户端获取
|
|
750
|
+
const [posts, { loading }] = createResource(
|
|
751
|
+
() => ctx.api.get('/api/posts')
|
|
752
|
+
)
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
### 嵌套布局 + 代码分割
|
|
756
|
+
|
|
757
|
+
```tsx
|
|
758
|
+
const routes = [
|
|
759
|
+
{
|
|
760
|
+
path: '/dashboard',
|
|
761
|
+
layout: DashboardLayout, // 侧边栏等 UI 保持挂载
|
|
762
|
+
children: [
|
|
763
|
+
{ path: '/overview', component: lazy(() => import('./Overview')) },
|
|
764
|
+
{ path: '/settings', component: lazy(() => import('./Settings')) },
|
|
765
|
+
],
|
|
766
|
+
},
|
|
767
|
+
]
|
|
768
|
+
```
|
|
769
|
+
|
|
770
|
+
---
|
|
771
|
+
|
|
772
|
+
## 环境变量
|
|
773
|
+
|
|
774
|
+
| 变量 | 默认 | 说明 |
|
|
775
|
+
|------|------|------|
|
|
776
|
+
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | Postgres 连接字符串 |
|
|
777
|
+
| `REDIS_URL` | `redis://localhost:6379` | Redis 连接字符串 |
|
|
267
778
|
|
|
268
779
|
---
|
|
269
780
|
|
|
@@ -275,43 +786,39 @@ node server.ts
|
|
|
275
786
|
# http://localhost:3000
|
|
276
787
|
```
|
|
277
788
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
## Environment Variables
|
|
281
|
-
|
|
282
|
-
| Variable | Default | Used by |
|
|
283
|
-
|----------|---------|---------|
|
|
284
|
-
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
285
|
-
| `REDIS_URL` | `redis://localhost:6379` | `redis()` |
|
|
789
|
+
Demo 包含:嵌套布局、ctx.ui.$ 待办列表、手动表单、async fetch 数据请求、api + auth 认证、WebSocket 实时通信。
|
|
286
790
|
|
|
287
791
|
---
|
|
288
792
|
|
|
289
|
-
##
|
|
793
|
+
## 项目结构
|
|
290
794
|
|
|
291
795
|
```
|
|
292
796
|
src/
|
|
293
|
-
├── index.ts
|
|
294
|
-
├── types.ts
|
|
295
|
-
├── core/
|
|
797
|
+
├── index.ts 入口,导出所有后端模块
|
|
798
|
+
├── types.ts Context, Handler, Middleware 等类型
|
|
799
|
+
├── core/ Router, serve, WebSocket upgrade
|
|
296
800
|
├── middleware/ cors, serveStatic
|
|
297
|
-
├── postgres/
|
|
298
|
-
├── redis/
|
|
299
|
-
├── ui/
|
|
300
|
-
├── graphql.ts
|
|
301
|
-
├── client/
|
|
302
|
-
│ ├── index.ts
|
|
303
|
-
│ ├──
|
|
304
|
-
│ ├──
|
|
305
|
-
│ ├──
|
|
306
|
-
│ ├──
|
|
307
|
-
│ ├── types.ts
|
|
308
|
-
│ └── middleware/
|
|
309
|
-
├──
|
|
310
|
-
|
|
801
|
+
├── postgres/ PostgreSQL 客户端
|
|
802
|
+
├── redis/ Redis 客户端
|
|
803
|
+
├── ui/ SSR 渲染 + 动态编译
|
|
804
|
+
├── graphql.ts GraphQL
|
|
805
|
+
├── client/
|
|
806
|
+
│ ├── index.ts 前端导出入口
|
|
807
|
+
│ ├── vnode.ts VNode 类型 + JSX 工厂
|
|
808
|
+
│ ├── render.ts VDOM 渲染器(render + patchValue)
|
|
809
|
+
│ ├── router.ts 路由中间件 + RouteView
|
|
810
|
+
│ ├── app.ts createApp 应用实例
|
|
811
|
+
│ ├── types.ts 前端类型
|
|
812
|
+
│ └── middleware/
|
|
813
|
+
│ ├── ws.ts WebSocket 客户端
|
|
814
|
+
│ ├── api.ts HTTP 客户端
|
|
815
|
+
│ └── auth.ts 认证状态管理
|
|
816
|
+
├── test/ 测试
|
|
817
|
+
apps/demo/ 全栈 demo
|
|
311
818
|
```
|
|
312
819
|
|
|
313
820
|
```bash
|
|
314
821
|
npm run build # esbuild → dist/
|
|
315
822
|
npm run typecheck # tsc --noEmit
|
|
316
|
-
npm test #
|
|
823
|
+
npm test # 运行所有测试
|
|
317
824
|
```
|