weifuwu 0.33.9 → 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 +654 -483
- package/dist/client/app.d.ts +1 -3
- package/dist/client/form.d.ts +111 -0
- package/dist/client/index.d.ts +78 -148
- package/dist/client/index.js +432 -790
- package/dist/client/jsx-runtime.d.ts +11 -1
- package/dist/client/jsx-runtime.js +432 -790
- package/dist/client/lazy.d.ts +50 -0
- package/dist/client/middleware/api.d.ts +71 -23
- package/dist/client/middleware/auth.d.ts +63 -20
- package/dist/client/middleware/ws.d.ts +2 -2
- package/dist/client/resource.d.ts +50 -0
- package/dist/client/router.d.ts +41 -7
- package/dist/client/signal.d.ts +0 -36
- package/dist/client/types.d.ts +35 -31
- package/dist/core/router.d.ts +21 -11
- package/dist/core/ws.d.ts +9 -12
- package/dist/graphql.d.ts +2 -1
- package/dist/index.d.ts +2 -35
- package/dist/index.js +367 -3586
- package/dist/types.d.ts +0 -12
- package/package.json +2 -1
- package/dist/ai/agent.d.ts +0 -127
- package/dist/ai/index.d.ts +0 -26
- package/dist/ai/types.d.ts +0 -59
- package/dist/base/index.d.ts +0 -59
- package/dist/base/module.d.ts +0 -42
- package/dist/base/types.d.ts +0 -116
- package/dist/client/components/Chat.d.ts +0 -19
- package/dist/client/components/Link.d.ts +0 -27
- package/dist/client/components/LoginForm.d.ts +0 -16
- package/dist/client/components/Transition.d.ts +0 -42
- package/dist/client/lib/css.d.ts +0 -33
- package/dist/client/lib/dev.d.ts +0 -28
- package/dist/client/lib/form.d.ts +0 -56
- package/dist/client/lib/model.d.ts +0 -36
- package/dist/client/lib/resource.d.ts +0 -56
- package/dist/cms/index.d.ts +0 -67
- package/dist/cms/module.d.ts +0 -38
- package/dist/cms/types.d.ts +0 -96
- package/dist/core/logger.d.ts +0 -16
- package/dist/core/trace.d.ts +0 -95
- package/dist/hub.d.ts +0 -36
- package/dist/kb/index.d.ts +0 -57
- package/dist/kb/module.d.ts +0 -37
- package/dist/kb/types.d.ts +0 -83
- package/dist/messager/index.d.ts +0 -72
- package/dist/messager/module.d.ts +0 -46
- package/dist/messager/types.d.ts +0 -99
- package/dist/middleware/compress.d.ts +0 -20
- package/dist/middleware/helmet.d.ts +0 -33
- package/dist/middleware/rate-limit.d.ts +0 -44
- package/dist/middleware/sandbox.d.ts +0 -52
- package/dist/middleware/upload.d.ts +0 -55
- package/dist/queue/cron.d.ts +0 -9
- package/dist/queue/index.d.ts +0 -12
- package/dist/queue/types.d.ts +0 -57
- package/dist/user/index.d.ts +0 -61
- package/dist/user/module.d.ts +0 -64
- package/dist/user/types.d.ts +0 -98
package/README.md
CHANGED
|
@@ -1,651 +1,822 @@
|
|
|
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
|
+
---
|
|
16
42
|
|
|
17
|
-
|
|
18
|
-
后端: 前端:
|
|
19
|
-
Request → 中间件链 → Handler createApp() → 中间件链 → 组件
|
|
20
|
-
│ │
|
|
21
|
-
▼ ▼
|
|
22
|
-
ctx.sql ctx.api
|
|
23
|
-
ctx.user ctx.socket
|
|
24
|
-
ctx.kb ctx.route
|
|
25
|
-
ctx.agent ctx.user
|
|
26
|
-
ctx.redis ctx.app.navigate
|
|
27
|
-
ctx.messager ctx.provide/inject
|
|
28
|
-
ctx.cms
|
|
29
|
-
ctx.base
|
|
30
|
-
ctx.queue
|
|
31
|
-
ctx.ui
|
|
32
|
-
```
|
|
43
|
+
## 核心理念:`ctx`
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
```ts
|
|
36
|
-
// 中间件注入 → handler 直接使用
|
|
37
|
-
app.get('/api/chat', async (req, ctx) => {
|
|
38
|
-
const user = ctx.user // 当前用户(auto-resolve from JWT)
|
|
39
|
-
const result = await ctx.kb.search(query) // RAG 知识库
|
|
40
|
-
return ctx.agent.chatStreamResponse({ messages }) // AI 流式响应
|
|
41
|
-
})
|
|
42
|
-
```
|
|
45
|
+
前后端共享同一模式:**中间件向 `ctx` 注入字段,handler/组件从 `ctx` 读取。**
|
|
43
46
|
|
|
44
|
-
**前端每个组件通过第二个参数接收 ctx:**
|
|
45
|
-
```tsx
|
|
46
|
-
function ChatPage(_props: {}, ctx: WfuiContext) {
|
|
47
|
-
ctx.api.get('/api/posts') // HTTP 客户端
|
|
48
|
-
ctx.socket.send(data) // WebSocket
|
|
49
|
-
ctx.route.path // 当前路由
|
|
50
|
-
ctx.app.navigate('/about') // 页面导航
|
|
51
|
-
}
|
|
52
47
|
```
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
| Module | Import | Injects into `ctx` | Depends on | Purpose |
|
|
63
|
-
|--------|--------|-------------------|------------|---------|
|
|
64
|
-
| `postgres()` | `weifuwu` | `ctx.sql` | `DATABASE_URL` | PostgreSQL client |
|
|
65
|
-
| `redis()` | `weifuwu` | `ctx.redis` | `REDIS_URL` | Redis client |
|
|
66
|
-
| `user()` | `weifuwu` | `ctx.user`, `ctx.userModule` | `postgres()`, `JWT_SECRET` | Auth, JWT, roles |
|
|
67
|
-
| `messager()` | `weifuwu` | `ctx.messager` | `postgres()`, `user()` | IM + AI conversation |
|
|
68
|
-
| `kb()` | `weifuwu` | `ctx.kb` | `postgres()`, `DASHSCOPE_API_KEY` | RAG knowledge base |
|
|
69
|
-
| `agent()` | `weifuwu` | `ctx.agent` | — | LLM chat, tools, streaming |
|
|
70
|
-
| `cms()` | `weifuwu` | `ctx.cms` | `postgres()`, `user()` | Blog, docs, changelog |
|
|
71
|
-
| `base()` | `weifuwu` | `ctx.base` | `postgres()`, `user()` | Dynamic data engine |
|
|
72
|
-
| `queue()` | `weifuwu` | `ctx.queue` | `REDIS_URL` | Job queue + cron |
|
|
73
|
-
| `ui()` | `weifuwu` | `ctx.ui.html/js/css` | — | SSR/SPA rendering |
|
|
74
|
-
|
|
75
|
-
### Backend Middleware
|
|
76
|
-
|
|
77
|
-
| Middleware | Injects into `ctx` | Purpose |
|
|
78
|
-
|-----------|-------------------|---------|
|
|
79
|
-
| `cors()` | — | CORS headers |
|
|
80
|
-
| `helmet()` | — | Security headers |
|
|
81
|
-
| `compress()` | — | gzip / brotli / deflate |
|
|
82
|
-
| `rateLimit()` | — | Sliding-window rate limiter |
|
|
83
|
-
| `logger()` | — | Request logging |
|
|
84
|
-
| `upload()` | `ctx.upload` | Multipart file upload |
|
|
85
|
-
| `serveStatic()` | — | Static files |
|
|
86
|
-
| `sandbox()` | — | Filesystem isolation |
|
|
87
|
-
|
|
88
|
-
### Frontend (`weifuwu/client`) — 每个中间件向 ctx 注入什么
|
|
89
|
-
|
|
90
|
-
| Import | Type | Injects into `ctx` | Purpose |
|
|
91
|
-
|--------|------|-------------------|---------|
|
|
92
|
-
| `signal()` | function | — | Reactive state container |
|
|
93
|
-
| `computed()` | function | — | Derived signals |
|
|
94
|
-
| `effect()` | function | — | Auto-tracked side effects |
|
|
95
|
-
| `batch()` | function | — | Batch multiple signal writes |
|
|
96
|
-
| `untrack()` | function | — | Read signal without subscribing |
|
|
97
|
-
| `onMount()` | function | — | Component mount callback |
|
|
98
|
-
| `onCleanup()` | function | — | Component unmount callback |
|
|
99
|
-
| `api()` | **middleware** | `ctx.api.get/post/...` | HTTP client |
|
|
100
|
-
| `auth()` | **middleware** | `ctx.user/login/logout` | Auth state management |
|
|
101
|
-
| `socket()` | **middleware** | `ctx.socket.send/onMessage/...` | WebSocket client |
|
|
102
|
-
| `router()` | **middleware** | `ctx.route.path/params/query`, `ctx.app.navigate` | Hash/history router |
|
|
103
|
-
| `createApp()` | function | — | App instance |
|
|
104
|
-
| `<RouteView>` | component | — | Route outlet |
|
|
105
|
-
| `<Show>` | component | — | Conditional rendering |
|
|
106
|
-
| `<For>` | component | — | Keyed list rendering |
|
|
107
|
-
| `<Transition>` | component | — | Animated enter/leave |
|
|
108
|
-
| `<ErrorBoundary>` | component | — | Catch render errors |
|
|
109
|
-
| `<Link>` | component | — | SPA navigation link |
|
|
110
|
-
| `<LoginForm>` | component | — | Login/register form |
|
|
111
|
-
| `<Chat>` | component | — | Real-time messaging |
|
|
112
|
-
| `useForm()` | function | — | Form state management |
|
|
113
|
-
| `useModel()` | function | — | Two-way form binding |
|
|
114
|
-
| `reactiveArray()` | function | — | Reactive array with mut methods |
|
|
115
|
-
| `createResource()` | function | — | Async data (loading/error/data) |
|
|
116
|
-
| `createStyles()` | function | — | Scoped CSS |
|
|
117
|
-
| `createContext()` | function | — | Type-safe provide/inject |
|
|
118
|
-
| `createPortal()` | function | — | Render outside parent DOM |
|
|
119
|
-
| `wrap()` | function | — | Third-party lib integration |
|
|
120
|
-
| `enableDevtools()` | function | — | Dev warnings + browser inspector |
|
|
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
|
|
55
|
+
```
|
|
121
56
|
|
|
122
57
|
---
|
|
123
58
|
|
|
124
|
-
##
|
|
125
|
-
|
|
126
|
-
### 完整全栈示例 — ctx 贯穿前后端
|
|
59
|
+
## 快速开始 — 全栈应用
|
|
127
60
|
|
|
128
|
-
**后端 `server.ts`:**
|
|
129
61
|
```ts
|
|
130
|
-
|
|
131
|
-
import {
|
|
62
|
+
// server.ts — 同一个 npm 包
|
|
63
|
+
import { serve, Router, cors, ui } from 'weifuwu'
|
|
132
64
|
|
|
133
65
|
const app = new Router()
|
|
134
66
|
app.use(cors())
|
|
135
|
-
app.use(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
app.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
142
|
-
})) // → ctx.agent
|
|
143
|
-
app.use(messager()) // → ctx.messager
|
|
144
|
-
app.use(ui()) // → ctx.ui
|
|
145
|
-
|
|
146
|
-
// 业务 API — ctx 中所有能力可直接使用
|
|
147
|
-
app.post('/api/chat', async (req, ctx) => {
|
|
148
|
-
const { messages } = await req.json()
|
|
149
|
-
return ctx.agent.chatStreamResponse({ messages })
|
|
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)
|
|
150
73
|
})
|
|
151
74
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
},
|
|
155
82
|
})
|
|
156
83
|
|
|
157
|
-
//
|
|
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
|
+
`)
|
|
158
90
|
app.get('/static/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
159
91
|
|
|
160
92
|
serve(app, { port: 3000 })
|
|
161
93
|
```
|
|
162
94
|
|
|
163
|
-
**前端 `src/main.tsx` — ctx 驱动组件:**
|
|
164
95
|
```tsx
|
|
165
|
-
|
|
166
|
-
import
|
|
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)
|
|
167
124
|
|
|
168
125
|
function AppShell(_props: {}, ctx: WfuiContext) {
|
|
169
|
-
if (!ctx.isAuthenticated) return <LoginForm />
|
|
170
126
|
return (
|
|
171
127
|
<div>
|
|
172
|
-
<nav
|
|
128
|
+
<nav>{/* ... */}</nav>
|
|
173
129
|
<main><RouteView /></main>
|
|
174
130
|
</div>
|
|
175
131
|
)
|
|
176
132
|
}
|
|
177
|
-
|
|
178
|
-
const app = createApp()
|
|
179
|
-
app.use(api()) // → ctx.api.get/post/...
|
|
180
|
-
app.use(auth()) // → ctx.user/login/logout
|
|
181
|
-
app.use(socket()) // → ctx.socket.send/onMessage
|
|
182
|
-
app.use(router({ routes })) // → ctx.route.path/params/query
|
|
183
|
-
app.mount('#root', AppShell)
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
**`tsconfig.json`:**
|
|
187
|
-
```json
|
|
188
|
-
{ "jsx": "react-jsx", "jsxImportSource": "weifuwu/client" }
|
|
189
133
|
```
|
|
190
134
|
|
|
191
135
|
---
|
|
192
136
|
|
|
193
|
-
##
|
|
137
|
+
## 后端
|
|
194
138
|
|
|
195
|
-
|
|
196
|
-
|----------|---------|---------|
|
|
197
|
-
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | `postgres()` |
|
|
198
|
-
| `REDIS_URL` | `redis://localhost:6379` | `redis()`, `queue()` |
|
|
199
|
-
| `JWT_SECRET` | — | `user()` |
|
|
200
|
-
| `DASHSCOPE_API_KEY` | — | `kb()` (embedding) |
|
|
201
|
-
| `DEEPSEEK_API_KEY` / `OPENAI_API_KEY` | — | `agent()` (LLM) |
|
|
202
|
-
| `DEEPSEEK_MODEL` | `deepseek-v4-flash` | `agent()` |
|
|
139
|
+
### Router
|
|
203
140
|
|
|
204
|
-
|
|
141
|
+
```ts
|
|
142
|
+
const app = new Router()
|
|
143
|
+
|
|
144
|
+
// HTTP 方法
|
|
145
|
+
app.get(path, ...handlers)
|
|
146
|
+
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
147
|
+
app.all(path, ...handlers)
|
|
205
148
|
|
|
206
|
-
|
|
149
|
+
// WebSocket / GraphQL
|
|
150
|
+
app.ws(path, handler)
|
|
151
|
+
app.graphql('/graphql', handler)
|
|
207
152
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```
|
|
216
|
-
| Method | Description |
|
|
217
|
-
|--------|-------------|
|
|
218
|
-
| `ctx.sql\`...\`` | Tagged template SQL |
|
|
219
|
-
| `ctx.sql.begin(fn)` | Transaction |
|
|
220
|
-
| `sql.close()` | Close pool |
|
|
221
|
-
|
|
222
|
-
### redis
|
|
223
|
-
```ts
|
|
224
|
-
import { redis } from 'weifuwu'
|
|
225
|
-
const r = redis()
|
|
226
|
-
app.use(r) // → ctx.redis
|
|
227
|
-
await ctx.redis.set('key', 'value')
|
|
228
|
-
await ctx.redis.get('key')
|
|
229
|
-
redis.close()
|
|
153
|
+
// 中间件
|
|
154
|
+
app.use(middleware)
|
|
155
|
+
app.mount(prefix, subRouter)
|
|
156
|
+
app.onError(handler)
|
|
157
|
+
|
|
158
|
+
// 调试
|
|
159
|
+
app.routes() // 列出所有路由
|
|
230
160
|
```
|
|
231
|
-
Reads `REDIS_URL` env (default: `redis://localhost:6379`).
|
|
232
161
|
|
|
233
|
-
|
|
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` | `()` | 返回路由列表数组 |
|
|
172
|
+
|
|
173
|
+
### serve — HTTP 服务器
|
|
174
|
+
|
|
234
175
|
```ts
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
176
|
+
const srv = serve(app, { port: 3000 })
|
|
177
|
+
await srv.stop() // 程序化停止
|
|
178
|
+
// Ctrl+C / SIGTERM — 自动关闭所有连接后退出
|
|
179
|
+
```
|
|
239
180
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
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,
|
|
195
|
+
}))
|
|
245
196
|
```
|
|
246
|
-
| `ctx.userModule.*` | Returns | Description |
|
|
247
|
-
|--------------------|---------|-------------|
|
|
248
|
-
| `register(input)` | `{ user, token }` | Register |
|
|
249
|
-
| `login(email, pw)` | `{ user, token } \| null` | Login |
|
|
250
|
-
| `getUserById(id)` | `UserRecord \| null` | Get by ID |
|
|
251
|
-
| `updateUser(id, input)` | `UserRecord \| null` | Update |
|
|
252
|
-
| `changePassword(id, oldPw, newPw)` | `boolean` | Change password |
|
|
253
|
-
| `deleteUser(id)` | `boolean` | Soft delete |
|
|
254
|
-
| `listUsers(inactive?)` | `UserRecord[]` | List users |
|
|
255
|
-
| `verifyToken(token)` | `TokenPayload \| null` | Verify JWT |
|
|
256
197
|
|
|
257
|
-
|
|
198
|
+
### serveStatic — 静态文件
|
|
258
199
|
|
|
259
|
-
### messager
|
|
260
200
|
```ts
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
| `
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
| `createDirectConversation(userId)` | `Conversation` | Create/reuse DM |
|
|
272
|
-
| `createGroupConversation(title, userIds)` | `Conversation` | Create group |
|
|
273
|
-
| `editMessage(msgId, body)` | `Message \| null` | Edit (24h) |
|
|
274
|
-
| `deleteMessage(msgId)` | `boolean` | Soft delete |
|
|
275
|
-
| `markRead(convId)` | `void` | Mark as read |
|
|
276
|
-
|
|
277
|
-
### kb — Knowledge Base
|
|
201
|
+
app.use(serveStatic('./public', { prefix: '/assets' }))
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
| 选项 | 默认 | 说明 |
|
|
205
|
+
|------|------|------|
|
|
206
|
+
| `prefix` | `''` | URL 前缀 |
|
|
207
|
+
| `index` | `'index.html'` | 默认首页 |
|
|
208
|
+
|
|
209
|
+
### postgres — PostgreSQL
|
|
210
|
+
|
|
278
211
|
```ts
|
|
279
|
-
import { kb } from 'weifuwu'
|
|
280
212
|
app.use(postgres())
|
|
281
|
-
|
|
213
|
+
// → ctx.sql`SELECT * FROM users`
|
|
214
|
+
|
|
215
|
+
app.use(postgres({ url: 'postgres://user:pass@host:5432/db' }))
|
|
216
|
+
// 默认读取 DATABASE_URL 环境变量
|
|
282
217
|
```
|
|
283
|
-
| `ctx.kb.*` | Returns | Description |
|
|
284
|
-
|------------|---------|-------------|
|
|
285
|
-
| `importText(title, text)` | `{ document, chunks }` | Import → chunk → embed |
|
|
286
|
-
| `search(query, opts?)` | `SearchResult[]` | Semantic search |
|
|
287
|
-
| `list()` | `Document[]` | List documents |
|
|
288
|
-
| `get(id)` | `Document \| null` | Get document |
|
|
289
|
-
| `delete(id)` | `boolean` | Delete + cascade chunks |
|
|
290
218
|
|
|
291
|
-
|
|
219
|
+
| 选项 | 默认 | 说明 |
|
|
220
|
+
|------|------|------|
|
|
221
|
+
| `url` | `DATABASE_URL` | 连接字符串 |
|
|
222
|
+
| `migrations` | `'./migrations'` | 迁移文件目录 |
|
|
223
|
+
|
|
224
|
+
依赖:需要 `postgres` npm 包。实现 `close(): Promise<void>`。
|
|
225
|
+
|
|
226
|
+
### redis — Redis
|
|
227
|
+
|
|
292
228
|
```ts
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
app.use(agent({
|
|
299
|
-
model: openai('deepseek-v4-flash', { baseURL: 'https://api.deepseek.com/v1' }),
|
|
300
|
-
system: 'You are a helpful assistant.',
|
|
301
|
-
knowledge: { search: async (q, ctx) => ctx.kb.search(q) },
|
|
302
|
-
tools: {
|
|
303
|
-
getWeather: tool({
|
|
304
|
-
description: 'Get weather for a city',
|
|
305
|
-
parameters: z.object({ city: z.string() }),
|
|
306
|
-
execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
|
|
307
|
-
}),
|
|
308
|
-
},
|
|
309
|
-
maxSteps: 5,
|
|
310
|
-
})) // → ctx.agent
|
|
229
|
+
app.use(redis())
|
|
230
|
+
// → ctx.redis.get('key')
|
|
231
|
+
// → ctx.redis.set('key', 'value')
|
|
232
|
+
// 默认读取 REDIS_URL 环境变量
|
|
311
233
|
```
|
|
312
|
-
| `ctx.agent.*` | Description |
|
|
313
|
-
|--------------|-------------|
|
|
314
|
-
| `chat(prompt, opts?)` | Non-streaming, returns text |
|
|
315
|
-
| `chatStreamResponse({ messages })` | SSE stream (useChat compatible) |
|
|
316
234
|
|
|
317
|
-
|
|
235
|
+
依赖:需要 `ioredis` npm 包。实现 `close(): Promise<void>`。
|
|
236
|
+
|
|
237
|
+
### ui — SSR + SPA 渲染
|
|
238
|
+
|
|
318
239
|
```ts
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
app.
|
|
240
|
+
app.use(ui())
|
|
241
|
+
|
|
242
|
+
// SSR 页面
|
|
243
|
+
app.get('/blog/:slug', async (req, ctx) => ctx.ui.html`
|
|
244
|
+
<!DOCTYPE html>
|
|
245
|
+
<html><body><h1>${post.title}</h1></body></html>
|
|
246
|
+
`)
|
|
247
|
+
|
|
248
|
+
// 动态 JS 编译(esbuild,零构建步骤)
|
|
249
|
+
app.get('/app.js', async (req, ctx) => ctx.ui.js('./src/main.tsx'))
|
|
250
|
+
|
|
251
|
+
// CSS 编译(PostCSS + Tailwind)
|
|
252
|
+
app.get('/style.css', async (req, ctx) => ctx.ui.css('./src/style.css'))
|
|
323
253
|
```
|
|
324
|
-
| `ctx.cms.*` | Returns | Description |
|
|
325
|
-
|-------------|---------|-------------|
|
|
326
|
-
| `create(input)` | `Content` | Create (admin) |
|
|
327
|
-
| `get(slug)` | `Content \| null` | Get by slug |
|
|
328
|
-
| `list(opts?)` | `Content[]` | List with filters |
|
|
329
|
-
| `publish(id)` / `unpublish(id)` | `Content \| null` | Toggle status |
|
|
330
254
|
|
|
331
|
-
|
|
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
|
|
263
|
+
|
|
332
264
|
```ts
|
|
333
|
-
app.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
265
|
+
app.graphql(async (req, ctx) => ({
|
|
266
|
+
schema: `type Query { hello: String }`,
|
|
267
|
+
resolvers: { Query: { hello: () => 'world' } },
|
|
268
|
+
graphiql: true,
|
|
269
|
+
}))
|
|
337
270
|
```
|
|
338
271
|
|
|
339
|
-
###
|
|
272
|
+
### WebSocket
|
|
273
|
+
|
|
340
274
|
```ts
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
275
|
+
app.ws('/ws', {
|
|
276
|
+
open(ws, ctx) { ws.send('connected') },
|
|
277
|
+
message(ws, ctx, data) { /* data: string | Buffer */ },
|
|
278
|
+
close(ws, ctx) { /* cleanup */ },
|
|
279
|
+
error(ws, ctx, err) { /* log */ },
|
|
280
|
+
})
|
|
347
281
|
```
|
|
348
282
|
|
|
349
|
-
###
|
|
283
|
+
### 错误处理
|
|
284
|
+
|
|
350
285
|
```ts
|
|
351
|
-
app.
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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
|
+
})
|
|
358
293
|
```
|
|
359
294
|
|
|
360
|
-
|
|
295
|
+
| 类/常量 | 说明 |
|
|
296
|
+
|---------|------|
|
|
297
|
+
| `HttpError` | HTTP 错误 `new HttpError(msg, status)` |
|
|
298
|
+
| `DEFAULT_MAX_BODY` | 默认请求体上限 10MB |
|
|
299
|
+
| `MIGRATIONS_TABLE` | Postgres 迁移表名 |
|
|
361
300
|
|
|
362
|
-
|
|
301
|
+
### 后端类型
|
|
363
302
|
|
|
364
|
-
|
|
303
|
+
`Context`, `Handler`, `Middleware`, `ErrorHandler`, `WebSocket`, `WebSocketHandler`, `ServeOptions`, `Server`, `CORSOptions`, `ServeStaticOptions`, `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `RedisOptions`, `RedisClient`, `RedisInjected`, `GraphQLOptions`, `GraphQLHandler`
|
|
365
304
|
|
|
366
|
-
|
|
305
|
+
---
|
|
367
306
|
|
|
368
|
-
|
|
369
|
-
// Signal — reactive state
|
|
370
|
-
const count = signal(0)
|
|
371
|
-
const doubled = computed(() => count.value * 2)
|
|
372
|
-
effect(() => console.log('count:', count.value))
|
|
307
|
+
## 前端 (`weifuwu/client`)
|
|
373
308
|
|
|
374
|
-
|
|
375
|
-
batch(() => { a.value = 1; b.value = 2 })
|
|
309
|
+
**2750 行源码,28 个运行时导出 + 19 个类型,零外部依赖。**
|
|
376
310
|
|
|
377
|
-
|
|
378
|
-
|
|
311
|
+
构建配置(esbuild):
|
|
312
|
+
```js
|
|
313
|
+
esbuild.build({
|
|
314
|
+
jsx: 'automatic',
|
|
315
|
+
jsxImportSource: 'weifuwu/client',
|
|
316
|
+
bundle: true,
|
|
317
|
+
})
|
|
318
|
+
```
|
|
379
319
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
320
|
+
### 信号系统
|
|
321
|
+
|
|
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) // 类型判断
|
|
383
329
|
```
|
|
384
330
|
|
|
385
|
-
|
|
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 |
|
|
339
|
+
|
|
340
|
+
### JSX 运行时
|
|
386
341
|
|
|
387
342
|
```tsx
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
items.remove(1) // [1, 3]
|
|
392
|
-
items.clear() // []
|
|
393
|
-
items.sort()
|
|
394
|
-
items.reverse()
|
|
343
|
+
// 自动由 esbuild 调用(无需手动导入)
|
|
344
|
+
<div class="foo">hello</div>
|
|
345
|
+
<Fragment>...</Fragment>
|
|
395
346
|
```
|
|
396
347
|
|
|
397
|
-
|
|
348
|
+
| 导出 | 说明 |
|
|
349
|
+
|------|------|
|
|
350
|
+
| `jsx` / `jsxs` / `jsxDEV` | JSX 编译目标 |
|
|
351
|
+
| `Fragment` | `<></>` 片段组件 |
|
|
352
|
+
|
|
353
|
+
**Signal 属性自动绑定:** `<input value={signalVal} />` — 信号变化时只更新对应 DOM 属性。
|
|
354
|
+
|
|
355
|
+
### 控制流
|
|
398
356
|
|
|
399
357
|
```tsx
|
|
400
|
-
|
|
401
|
-
<Show when={isLoggedIn} fallback={<LoginPage />}>
|
|
358
|
+
<Show when={isLoggedIn} fallback={<Login />}>
|
|
402
359
|
<Dashboard />
|
|
403
360
|
</Show>
|
|
404
361
|
|
|
405
|
-
|
|
406
|
-
<
|
|
407
|
-
{(todo) => <TodoItem todo={todo} />}
|
|
362
|
+
<For each={items} keyBy="id">
|
|
363
|
+
{(item) => <div>{item.name}</div>}
|
|
408
364
|
</For>
|
|
365
|
+
```
|
|
409
366
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
367
|
+
| 组件 | 属性 | 说明 |
|
|
368
|
+
|------|------|------|
|
|
369
|
+
| `Show` | `when: boolean \| Signal<boolean>`, `fallback?`, `children?` | 条件渲染,Signal 响应式切换 |
|
|
370
|
+
| `For` | `each: T[] \| Signal<T[]>`, `children: (item, index) => Node`, `keyBy?` | 列表渲染,支持 keyed 复用 |
|
|
414
371
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
372
|
+
### 生命周期
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
onMount(() => {
|
|
376
|
+
init()
|
|
377
|
+
return () => cleanup() // 自动清理
|
|
378
|
+
})
|
|
379
|
+
onCleanup(() => clearInterval(id))
|
|
419
380
|
```
|
|
420
381
|
|
|
421
|
-
|
|
382
|
+
| 函数 | 说明 |
|
|
383
|
+
|------|------|
|
|
384
|
+
| `onMount(fn)` | 组件挂载后执行,返回函数在卸载时自动清理 |
|
|
385
|
+
| `onCleanup(fn)` | 组件卸载时执行 |
|
|
386
|
+
|
|
387
|
+
### 应用
|
|
422
388
|
|
|
423
389
|
```tsx
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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` | 当前上下文 |
|
|
432
402
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
403
|
+
### 路由
|
|
404
|
+
|
|
405
|
+
```tsx
|
|
406
|
+
// 路由定义
|
|
407
|
+
const routes: RouteDef[] = [
|
|
408
|
+
{ path: '/', component: HomePage },
|
|
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: '用户' },
|
|
418
|
+
]
|
|
419
|
+
|
|
420
|
+
// 注册路由中间件
|
|
421
|
+
app.use(router({
|
|
422
|
+
routes,
|
|
423
|
+
notFound: NotFound,
|
|
424
|
+
mode: 'hash', // 'hash' | 'history'
|
|
425
|
+
scrollRestoration: true,
|
|
426
|
+
}))
|
|
427
|
+
|
|
428
|
+
// 路由出口 — 根层级和嵌套层级用同一个组件
|
|
429
|
+
function AppShell() {
|
|
430
|
+
return <main><RouteView /></main> // 根出口
|
|
431
|
+
}
|
|
432
|
+
function DashboardLayout() {
|
|
433
|
+
return (
|
|
434
|
+
<div class="flex">
|
|
435
|
+
<Sidebar />
|
|
436
|
+
<main><RouteView /></main> // 嵌套出口(同一组件)
|
|
437
|
+
</div>
|
|
438
|
+
)
|
|
439
|
+
}
|
|
438
440
|
```
|
|
439
441
|
|
|
440
|
-
|
|
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` 注入:**
|
|
441
461
|
|
|
442
462
|
```tsx
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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')
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### 代码分割
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
const AdminPage = lazy(() => import('./pages/AdminPage'), {
|
|
476
|
+
fallback: () => <div>加载中...</div>,
|
|
477
|
+
})
|
|
447
478
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
479
|
+
const routes = [
|
|
480
|
+
{ path: '/admin', component: AdminPage },
|
|
481
|
+
]
|
|
451
482
|
```
|
|
452
483
|
|
|
453
|
-
|
|
484
|
+
需 esbuild `splitting: true` + `outdir`。
|
|
485
|
+
|
|
486
|
+
### 中间件
|
|
487
|
+
|
|
488
|
+
#### ws — WebSocket 客户端
|
|
454
489
|
|
|
455
490
|
```tsx
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
491
|
+
app.use(ws({ url: '/ws' }))
|
|
492
|
+
|
|
493
|
+
// 组件中:
|
|
494
|
+
onMount(() => {
|
|
495
|
+
const unsub = ctx.ws.onMessage((data) => { ... })
|
|
496
|
+
onCleanup(() => unsub())
|
|
459
497
|
})
|
|
460
|
-
|
|
498
|
+
ctx.ws.send({ type: 'chat', body: 'hello' })
|
|
499
|
+
<Show when={ctx.ws.isConnected}>🟢 已连接</Show>
|
|
461
500
|
```
|
|
462
501
|
|
|
463
|
-
|
|
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 客户端
|
|
464
515
|
|
|
465
516
|
```tsx
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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')
|
|
469
525
|
```
|
|
470
526
|
|
|
471
|
-
|
|
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 — 认证状态管理
|
|
472
545
|
|
|
473
546
|
```tsx
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
547
|
+
app.use(auth())
|
|
548
|
+
|
|
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()
|
|
559
|
+
```
|
|
560
|
+
|
|
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 — 表单管理
|
|
580
|
+
|
|
581
|
+
```tsx
|
|
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
|
+
},
|
|
479
594
|
})
|
|
480
|
-
|
|
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>
|
|
481
602
|
```
|
|
482
603
|
|
|
483
|
-
|
|
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 — 异步数据
|
|
484
617
|
|
|
485
618
|
```tsx
|
|
486
|
-
|
|
619
|
+
const [data, { loading, error, refetch }] = createResource(
|
|
620
|
+
() => fetch('/api/posts').then(r => r.json()),
|
|
621
|
+
{ initialValue: [] }
|
|
622
|
+
)
|
|
487
623
|
|
|
488
|
-
|
|
489
|
-
<
|
|
490
|
-
<
|
|
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>
|
|
491
630
|
```
|
|
492
631
|
|
|
493
|
-
|
|
632
|
+
| 返回值 | 类型 | 说明 |
|
|
633
|
+
|--------|------|------|
|
|
634
|
+
| `data` (元组第一项) | `Signal<T \| undefined>` | 数据信号 |
|
|
635
|
+
| `loading` | `Signal<boolean>` | 加载状态 |
|
|
636
|
+
| `error` | `Signal<Error \| undefined>` | 错误信号 |
|
|
637
|
+
| `refetch` | `() => void` | 手动重新加载 |
|
|
638
|
+
|
|
639
|
+
#### ErrorBoundary — 错误捕获
|
|
494
640
|
|
|
495
641
|
```tsx
|
|
496
|
-
|
|
497
|
-
{
|
|
498
|
-
{
|
|
499
|
-
|
|
500
|
-
}
|
|
501
|
-
|
|
642
|
+
<ErrorBoundary
|
|
643
|
+
fallback={(e) => <p>出错了: {e.message}</p>}
|
|
644
|
+
onError={(e) => console.error(e)}
|
|
645
|
+
>
|
|
646
|
+
{() => <Dashboard />} {/* 必须用 thunk */}
|
|
647
|
+
</ErrorBoundary>
|
|
648
|
+
```
|
|
502
649
|
|
|
503
|
-
|
|
504
|
-
// ctx.route.path / .params / .query / .data / .loading
|
|
650
|
+
#### createPortal — 渲染到指定位置
|
|
505
651
|
|
|
506
|
-
|
|
507
|
-
|
|
652
|
+
```tsx
|
|
653
|
+
<Show when={showModal}>
|
|
654
|
+
{createPortal(<Modal />, document.body)}
|
|
655
|
+
</Show>
|
|
508
656
|
```
|
|
509
657
|
|
|
510
|
-
|
|
658
|
+
#### wrap — 封装三方库为组件
|
|
511
659
|
|
|
512
|
-
```
|
|
513
|
-
|
|
514
|
-
|
|
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
|
+
})
|
|
515
667
|
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
// __wefu__.warnings() → 切换开发警告
|
|
668
|
+
// 使用:
|
|
669
|
+
<Chart data={salesData} />
|
|
519
670
|
```
|
|
520
671
|
|
|
521
|
-
|
|
672
|
+
#### createContext / extendCtx — 上下文扩展
|
|
522
673
|
|
|
523
|
-
|
|
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 对照表
|
|
524
687
|
|
|
525
688
|
| React | weifuwu/client |
|
|
526
689
|
|-------|----------------|
|
|
527
690
|
| `useState(0)` | `signal(0)` |
|
|
528
|
-
| `useMemo(() => a
|
|
529
|
-
| `useEffect(() =>
|
|
530
|
-
| `
|
|
531
|
-
| `{
|
|
532
|
-
| `
|
|
533
|
-
|
|
|
534
|
-
| `
|
|
691
|
+
| `useMemo(() => a*2, [a])` | `computed(() => a.value * 2)` |
|
|
692
|
+
| `useEffect(() => f, [])` | `onMount(f)` |
|
|
693
|
+
| `useEffect(() => f, [dep])` | `effect(f)` |
|
|
694
|
+
| `{cond && <X/>}` | `<Show when={cond}><X/></Show>` |
|
|
695
|
+
| `{arr.map(i => <X/>)}` | `<For each={arr}>{(i) => <X/>}</For>` |
|
|
696
|
+
| `Suspense` | `<Show when={!loading}>` |
|
|
697
|
+
| `ErrorBoundary` | `<ErrorBoundary>` |
|
|
698
|
+
| `createPortal` | `createPortal` |
|
|
699
|
+
| `useNavigate()` | `ctx.app.navigate()` |
|
|
535
700
|
| `useParams()` | `ctx.route.params` |
|
|
536
|
-
| `
|
|
537
|
-
|
|
|
701
|
+
| `useFormik()` | `useForm()` |
|
|
702
|
+
| `axios.get()` | `ctx.api.get()` |
|
|
703
|
+
| `useSWR()` | `createResource()` |
|
|
704
|
+
| `React.lazy()` | `lazy()` |
|
|
705
|
+
| `useContext()` | `createContext()` |
|
|
706
|
+
|
|
707
|
+
### 前端类型
|
|
538
708
|
|
|
539
|
-
|
|
540
|
-
- No hooks rules — no `use` prefix, no rules-of-hooks
|
|
541
|
-
- No virtual DOM — JSX creates real DOM directly
|
|
542
|
-
- No dependency arrays — `effect()` auto-tracks
|
|
543
|
-
- No state management library — signal is the state management
|
|
544
|
-
- No Context.Provider component — just `ctx.provide/inject`
|
|
709
|
+
`Signal`, `Component`, `WfuiContext`, `AppMiddleware`, `RouteDef`, `ApiClient`, `ApiOptions`, `ApiRequestOptions`, `AuthClient`, `AuthUser`, `AuthOptions`, `ResourceOptions`, `ResourceState`, `FormOptions`, `FormReturn`, `FormFieldBindings`, `FormValidators`, `LazyComponentOptions`, `LazyStatus`
|
|
545
710
|
|
|
546
711
|
---
|
|
547
712
|
|
|
548
|
-
##
|
|
713
|
+
## 全栈模式
|
|
549
714
|
|
|
550
|
-
|
|
551
|
-
|--------|---------|
|
|
552
|
-
| `requireRole('admin')` | Middleware factory: check `ctx.user.role` |
|
|
553
|
-
| `createHub()` | WebSocket pub/sub |
|
|
554
|
-
| `HttpError` | `throw new HttpError(msg, 404)` |
|
|
555
|
-
| `trace()` | Request tracing |
|
|
715
|
+
### 认证流程
|
|
556
716
|
|
|
557
|
-
|
|
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
|
+
})
|
|
558
726
|
|
|
559
|
-
|
|
727
|
+
// 前端
|
|
728
|
+
app.use(api({ baseURL: '' }))
|
|
729
|
+
app.use(auth())
|
|
560
730
|
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
app.post / put / delete / patch / head / options(path, ...handlers)
|
|
565
|
-
app.all(path, ...handlers)
|
|
566
|
-
app.ws(path, ...handler) // WebSocket
|
|
567
|
-
app.use(middleware) // Global middleware
|
|
568
|
-
app.mount(prefix, router) // Sub-router
|
|
569
|
-
app.plugin(fn) // Plugin
|
|
570
|
-
app.onError(handler) // Error handler
|
|
571
|
-
app.routes() // Debug: list all routes
|
|
731
|
+
// 登录
|
|
732
|
+
const res = await ctx.api.post('/api/login', { email, password })
|
|
733
|
+
ctx.auth.login(res.token, res.user)
|
|
572
734
|
```
|
|
573
735
|
|
|
574
|
-
|
|
736
|
+
### 异步数据 + SSR
|
|
575
737
|
|
|
576
|
-
|
|
738
|
+
```ts
|
|
739
|
+
// 后端 — 同路径既支持 SSR 也支持 API
|
|
740
|
+
app.get('/api/posts', async (req, ctx) => {
|
|
741
|
+
return Response.json(posts)
|
|
742
|
+
})
|
|
577
743
|
|
|
744
|
+
// 前端 — 客户端获取
|
|
745
|
+
const [posts, { loading }] = createResource(
|
|
746
|
+
() => ctx.api.get('/api/posts')
|
|
747
|
+
)
|
|
578
748
|
```
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
├── graphql.ts ← GraphQL
|
|
594
|
-
├── hub.ts ← WebSocket hub
|
|
595
|
-
├── ui/ ← ctx.ui.html / ctx.ui.js / ctx.ui.css
|
|
596
|
-
├── client/ ← Frontend framework
|
|
597
|
-
│ ├── index.ts ← Exports all client APIs (33 exports)
|
|
598
|
-
│ ├── signal.ts ← Signal / effect / computed / batch / untrack
|
|
599
|
-
│ ├── jsx-runtime.ts ← JSX → DOM + types + Show/For/wrap/ErrorBoundary/Portal/Transition
|
|
600
|
-
│ ├── app.ts ← createApp / hydrate / middleware chain
|
|
601
|
-
│ ├── router.ts ← Route matching / RouteView / loader / transition
|
|
602
|
-
│ ├── types.ts ← WfuiContext / RouteDef / createContext
|
|
603
|
-
│ ├── lib/
|
|
604
|
-
│ │ ├── form.ts ← useForm (validated forms)
|
|
605
|
-
│ │ ├── model.ts ← useModel (two-way binding)
|
|
606
|
-
│ │ ├── resource.ts ← createResource (async data)
|
|
607
|
-
│ │ ├── css.ts ← createStyles (scoped CSS)
|
|
608
|
-
│ │ └── dev.ts ← enableDevtools (dev warnings)
|
|
609
|
-
│ ├── middleware/
|
|
610
|
-
│ │ ├── api.ts ← HTTP client → ctx.api
|
|
611
|
-
│ │ ├── auth.ts ← Auth → ctx.user/login/logout
|
|
612
|
-
│ │ └── ws.ts ← WebSocket → ctx.socket
|
|
613
|
-
│ └── components/
|
|
614
|
-
│ ├── LoginForm.tsx ← Login/register
|
|
615
|
-
│ ├── Chat.tsx ← Real-time chat
|
|
616
|
-
│ ├── Link.tsx ← SPA navigation
|
|
617
|
-
│ └── Transition.tsx ← Animated enter/leave
|
|
618
|
-
└── test/ ← 66 unit tests + 10 benchmarks
|
|
619
|
-
|
|
620
|
-
apps/demo/ ← Full-stack demo
|
|
621
|
-
├── server.ts ← weifuwu server (user, kb, agent, messager 未演示)
|
|
622
|
-
├── src/main.tsx ← SPA + SSR hydrate demo pages
|
|
623
|
-
├── public/ ← style.css (Tailwind + transition classes)
|
|
624
|
-
└── tsconfig.json
|
|
625
|
-
|
|
626
|
-
docker-compose.yml ← postgres (pgvector) + redis
|
|
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
|
+
]
|
|
627
763
|
```
|
|
628
764
|
|
|
629
765
|
---
|
|
630
766
|
|
|
631
|
-
##
|
|
767
|
+
## 环境变量
|
|
768
|
+
|
|
769
|
+
| 变量 | 默认 | 说明 |
|
|
770
|
+
|------|------|------|
|
|
771
|
+
| `DATABASE_URL` | `postgres://root:123456@localhost:5432/demo` | Postgres 连接字符串 |
|
|
772
|
+
| `REDIS_URL` | `redis://localhost:6379` | Redis 连接字符串 |
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
## Demo
|
|
632
777
|
|
|
633
778
|
```bash
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
npm test # Run all tests
|
|
779
|
+
cd apps/demo
|
|
780
|
+
node server.ts
|
|
781
|
+
# http://localhost:3000
|
|
638
782
|
```
|
|
639
783
|
|
|
784
|
+
Demo 包含:嵌套布局、signal 待办列表、useForm 表单、createResource 数据请求、api + auth 认证、WebSocket 实时通信。
|
|
785
|
+
|
|
640
786
|
---
|
|
641
787
|
|
|
642
|
-
##
|
|
788
|
+
## 项目结构
|
|
643
789
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
790
|
+
```
|
|
791
|
+
src/
|
|
792
|
+
├── index.ts 入口,导出所有后端模块
|
|
793
|
+
├── types.ts Context, Handler, Middleware 等类型
|
|
794
|
+
├── core/ Router, serve, WebSocket upgrade
|
|
795
|
+
├── middleware/ cors, serveStatic
|
|
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
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
```bash
|
|
819
|
+
npm run build # esbuild → dist/
|
|
820
|
+
npm run typecheck # tsc --noEmit
|
|
821
|
+
npm test # 运行所有测试
|
|
822
|
+
```
|