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