@tyno/tyno 2.1.3
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-zh.md +572 -0
- package/README.md +555 -0
- package/example/app.ts +173 -0
- package/example/public/index.html +1 -0
- package/example/public/test.json +1 -0
- package/package.json +63 -0
- package/scripts/build.mjs +97 -0
- package/scripts/rename-cjs.mjs +23 -0
- package/src/application.ts +304 -0
- package/src/cache/drivers/file.ts +79 -0
- package/src/cache/drivers/memory.ts +72 -0
- package/src/cache/drivers/redis.ts +72 -0
- package/src/cache/index.ts +5 -0
- package/src/cache/manager.ts +106 -0
- package/src/cache/types.ts +24 -0
- package/src/cache-facade.ts +64 -0
- package/src/compose.ts +139 -0
- package/src/context.ts +5 -0
- package/src/errors/app-error.ts +37 -0
- package/src/errors/http-error.ts +34 -0
- package/src/errors/index.ts +4 -0
- package/src/errors/runtime-error.ts +19 -0
- package/src/facade/index.ts +3 -0
- package/src/index.ts +29 -0
- package/src/middlewares/compress.ts +101 -0
- package/src/middlewares/cors.ts +57 -0
- package/src/middlewares/error-page.ts +89 -0
- package/src/middlewares/index.ts +9 -0
- package/src/middlewares/request-id.ts +47 -0
- package/src/middlewares/static.ts +138 -0
- package/src/mime.ts +38 -0
- package/src/request/body-parser.ts +61 -0
- package/src/request/index.ts +273 -0
- package/src/request/multipart-parser.ts +360 -0
- package/src/request-global.ts +31 -0
- package/src/response/sse.ts +54 -0
- package/src/response.ts +177 -0
- package/src/router/index.ts +290 -0
- package/src/router/node.ts +15 -0
- package/src/router/parse-path.ts +18 -0
- package/src/types.ts +109 -0
- package/test/functional.test.ts +614 -0
- package/tsconfig.build.json +13 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.json +21 -0
package/README-zh.md
ADDED
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
# tyno
|
|
2
|
+
|
|
3
|
+
零外部依赖的轻量级 Node.js HTTP 框架,TypeScript 编写,原生 `http` 模块。融合 Koa 洋葱模型与 ThinkPHP/Laravel 请求取值风格。
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { Tyno } from '@tyno/tyno'
|
|
7
|
+
|
|
8
|
+
const app = new Tyno()
|
|
9
|
+
app.use((req) => `Hello ${req.query('name') || 'World'}`)
|
|
10
|
+
app.listen(3000)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Tyno } from '@tyno/tyno'
|
|
15
|
+
import { Router } from '@tyno/tyno/router'
|
|
16
|
+
|
|
17
|
+
const app = new Tyno()
|
|
18
|
+
const r = new Router()
|
|
19
|
+
r.get('/users/:id', (req) => ({ id: req.params.id }))
|
|
20
|
+
app.use(r.routes()).listen(3000)
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 特性
|
|
24
|
+
|
|
25
|
+
- **洋葱模型中间件**:返回值自动归一化,异步压缩
|
|
26
|
+
- **前缀树路由**:静态/参数 `:id`/正则 `:id(\d+)`/通配符 `*`,HEAD,分组 `group()`,`fallback()`
|
|
27
|
+
- **请求取值**:`query()` / `post()` / `param()` / `input()`
|
|
28
|
+
- **文件上传**:流式 multipart 状态机解析,buffer/磁盘双模
|
|
29
|
+
- **Response 构建**:静态工厂 `json/text/empty/redirect/image`,链式 `set/type/setStatus/setCookie/attachment`
|
|
30
|
+
- **全局门面**:`req/res`(Request/Response 别名)/ `request`(读入站)/ `Cache`(缓存)
|
|
31
|
+
- **错误体系**:`HttpError` / `RuntimeError` + 错误中间件 + Symbol 标记
|
|
32
|
+
- **事件系统**:`request` / `response` / `response:sent` / `error` / `ready`
|
|
33
|
+
- **缓存**:内存/文件/Redis,`get/set/has/delete/clear/remember`
|
|
34
|
+
- **内置中间件**:CORS、异步压缩、Range 静态文件、请求 ID
|
|
35
|
+
- **测试**:`app.inject()` 无需启动服务器
|
|
36
|
+
|
|
37
|
+
## 模块结构
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// 主入口
|
|
41
|
+
import { Tyno, Response, res, Request, req } from '@tyno/tyno'
|
|
42
|
+
|
|
43
|
+
// 门面
|
|
44
|
+
import { request, Cache } from '@tyno/tyno/facade'
|
|
45
|
+
|
|
46
|
+
// 中间件
|
|
47
|
+
import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'
|
|
48
|
+
|
|
49
|
+
// 路由
|
|
50
|
+
import { Router } from '@tyno/tyno/router'
|
|
51
|
+
|
|
52
|
+
// 错误
|
|
53
|
+
import { HttpError, NotFound, RuntimeError } from '@tyno/tyno/errors'
|
|
54
|
+
|
|
55
|
+
// 缓存
|
|
56
|
+
import { CacheManager, MemoryDriver } from '@tyno/tyno/cache'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 核心模块
|
|
62
|
+
|
|
63
|
+
### 中间件
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
// 洋葱模型
|
|
67
|
+
app.use(async (req, next) => {
|
|
68
|
+
const start = Date.now()
|
|
69
|
+
const res = await next()
|
|
70
|
+
console.log(`${req.method} ${req.path} ${res.status} ${Date.now() - start}ms`)
|
|
71
|
+
return res
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
// 终端中间件(不接收 next)
|
|
75
|
+
app.use((req) => ({ hello: 'world' }))
|
|
76
|
+
|
|
77
|
+
// 错误中间件(3 个参数或 Symbol 标记)
|
|
78
|
+
app.use(async (err, req, next) => {
|
|
79
|
+
return Response.json({ error: err.message }, err.status || 500)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// 批量注册(数组),依次执行
|
|
83
|
+
app.use([logger, auth, compress])
|
|
84
|
+
app.middleware([cors, requestId]) // middleware 是 use 的别名
|
|
85
|
+
|
|
86
|
+
// Router 同样支持
|
|
87
|
+
const r = new Router()
|
|
88
|
+
r.middleware([auth, adminCheck])
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**返回值自动归一整表**:
|
|
92
|
+
|
|
93
|
+
| 返回类型 | 自动转换 | Content-Type | 状态码 |
|
|
94
|
+
|---------|---------|-------------|--------|
|
|
95
|
+
| `Response` 实例 | 原样使用 | 已有 | 已有 |
|
|
96
|
+
| `string` | `Response.text(str)` | `text/plain; charset=utf-8` | 200 |
|
|
97
|
+
| `Buffer` | `Response.text(buf)` | `text/plain; charset=utf-8` | 200 |
|
|
98
|
+
| `object`(普通对象) | `Response.json(obj)` | `application/json; charset=utf-8` | 200 |
|
|
99
|
+
| `number` / `boolean` | `Response.text(String(v))` | `text/plain; charset=utf-8` | 200 |
|
|
100
|
+
| `ReadableStream`(有 `pipe`) | 流式输出 | 无(透传) | 200 |
|
|
101
|
+
| `AsyncIterable` / `Generator` | SSE 流式输出 | `text/event-stream` | 200 |
|
|
102
|
+
| `null` / `undefined` | `Response.empty()` | 无 | 204 |
|
|
103
|
+
| `Image Buffer`(手动) | `Response.image(buf, 'png')` | `image/png` | 200 |
|
|
104
|
+
|
|
105
|
+
> 返回图片需显式调用 `Response.image()`,框架不会自动区分 Buffer 是文本还是图片。
|
|
106
|
+
|
|
107
|
+
### Request
|
|
108
|
+
|
|
109
|
+
Proxy 封装原生 `IncomingMessage`,内置属性只读,自定义属性写入 `DATA_KEY`。
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// 基本信息
|
|
113
|
+
req.method // GET / POST
|
|
114
|
+
req.path // 不含 query string
|
|
115
|
+
req.url // 含 query string
|
|
116
|
+
req.ip // 优先 X-Forwarded-For
|
|
117
|
+
req.protocol // http / https
|
|
118
|
+
req.secure // 是否 HTTPS
|
|
119
|
+
req.fullUrl // protocol://host/url
|
|
120
|
+
req.httpVersion // 1.1 / 2.0
|
|
121
|
+
req.host // 含端口
|
|
122
|
+
req.hostname // 不含端口
|
|
123
|
+
req.userAgent // User-Agent
|
|
124
|
+
req.isAjax() // X-Requested-With 判断
|
|
125
|
+
req.wantsJSON() // Accept 头判断
|
|
126
|
+
|
|
127
|
+
// Query
|
|
128
|
+
req.query() // → { id: '1', name: 'Alice' }
|
|
129
|
+
req.query('id') // → '1'
|
|
130
|
+
req.query('x', 'def') // → 'def' 带默认值
|
|
131
|
+
req.get('id') // 别名
|
|
132
|
+
req.getQuery('id') // 别名
|
|
133
|
+
|
|
134
|
+
// Body(惰性解析,结果缓存)
|
|
135
|
+
await req.body() // JSON / urlencoded / text 自动识别
|
|
136
|
+
await req.post('title') // 取 POST 字段
|
|
137
|
+
await req.param('id') // GET 优先,fallback POST
|
|
138
|
+
await req.input('id', 0, parseInt) // 带 filter
|
|
139
|
+
|
|
140
|
+
// Header / Cookie
|
|
141
|
+
req.header('authorization') // 大小写不敏感
|
|
142
|
+
req.cookies // 含 URL 解码
|
|
143
|
+
req.getCookie('sessionId')
|
|
144
|
+
|
|
145
|
+
// 文件上传(流式 multipart)
|
|
146
|
+
await req.files() // → { fields, files }
|
|
147
|
+
await req.file('avatar') // → 单个 UploadedFile | null
|
|
148
|
+
// UploadedFile: { fieldname, filename, mimetype, filepath, size, buffer? }
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**Body 配置**:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const app = new Tyno({
|
|
155
|
+
body: {
|
|
156
|
+
limit: '2mb', // JSON/urlencoded/text 大小限制
|
|
157
|
+
uploadLimit: '20mb', // 文件上传总大小限制
|
|
158
|
+
uploadBufferLimit: '512kb',// 文件内存阈值,超过流式写磁盘
|
|
159
|
+
uploadDir: '/tmp',
|
|
160
|
+
keepExtensions: true
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Response
|
|
166
|
+
|
|
167
|
+
实例构造 + 静态工厂 + 静态门面(预设 header/cookie),三位一体。
|
|
168
|
+
|
|
169
|
+
**别名**:`Response`(类名)= `response` = `res`
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
import { Response, res } from '@tyno/tyno'
|
|
173
|
+
|
|
174
|
+
// —— 静态工厂 ——
|
|
175
|
+
Response.json({ ok: true }) // 200 application/json; charset=utf-8
|
|
176
|
+
Response.json(data, 201) // 自定义状态码
|
|
177
|
+
Response.text('hello') // 200 text/plain; charset=utf-8
|
|
178
|
+
Response.empty() // 204 无 Content-Type
|
|
179
|
+
Response.empty(201) // 201
|
|
180
|
+
Response.redirect('/new') // 302 Location: /new
|
|
181
|
+
Response.redirect('/new', 301) // 301
|
|
182
|
+
Response.image(buf) // 200 image/png
|
|
183
|
+
Response.image(buf, 'jpeg') // 200 image/jpeg
|
|
184
|
+
Response.image(buf, 'image/webp') // 200 image/webp
|
|
185
|
+
Response.image(buf, 'svg', 201) // 201 image/svg+xml
|
|
186
|
+
|
|
187
|
+
// —— 实例构造 ——
|
|
188
|
+
new Response(200, { 'X-Custom': 'yes' }, 'body')
|
|
189
|
+
new Response(404, {}, 'Not Found')
|
|
190
|
+
|
|
191
|
+
// —— 链式修改 ——
|
|
192
|
+
Response.json({ ok: true })
|
|
193
|
+
.set('X-Request-Id', 'abc')
|
|
194
|
+
.type('json')
|
|
195
|
+
.setStatus(201)
|
|
196
|
+
.setCookie('token', 'xxx', { httpOnly: true, maxAge: 3600, sameSite: 'Lax' })
|
|
197
|
+
.clearCookie('old_session')
|
|
198
|
+
.attachment('report.txt') // Content-Disposition: attachment
|
|
199
|
+
|
|
200
|
+
// —— 静态门面(预设,final 优先) ——
|
|
201
|
+
app.use(async (req, next) => {
|
|
202
|
+
Response.header('X-Powered-By', 'tyno')
|
|
203
|
+
Response.setCookie('track', 'abc', { httpOnly: true })
|
|
204
|
+
return next()
|
|
205
|
+
})
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Response 输出类型完整参考**:
|
|
209
|
+
|
|
210
|
+
| 工厂方法 | Content-Type | 默认状态码 | body 类型 |
|
|
211
|
+
|---------|-------------|-----------|----------|
|
|
212
|
+
| `json(data, status?)` | `application/json; charset=utf-8` | 200 | `string` (JSON) |
|
|
213
|
+
| `text(data, status?)` | `text/plain; charset=utf-8` | 200 | `string` |
|
|
214
|
+
| `empty(status?)` | 无 | 204 | `null` |
|
|
215
|
+
| `redirect(url, status?)` | 无 (`Location` 头) | 302 | `null` |
|
|
216
|
+
| `image(data, type?, status?)` | `image/*` | 200 | `Buffer \| string` |
|
|
217
|
+
| `new Response(s, h, b)` | 手动指定 | 手动指定 | `ResponseBody` |
|
|
218
|
+
|
|
219
|
+
| `ResponseBody` 类型 | 输出行为 |
|
|
220
|
+
|-------------------|---------|
|
|
221
|
+
| `string` | `nodeRes.end(body)` |
|
|
222
|
+
| `Buffer` | `nodeRes.end(body)` |
|
|
223
|
+
| `NodeJS.ReadableStream` | `body.pipe(nodeRes)` |
|
|
224
|
+
| `AsyncIterable` | SSE 流式逐块写入 |
|
|
225
|
+
| `null` | `nodeRes.end()` |
|
|
226
|
+
|
|
227
|
+
### 路由
|
|
228
|
+
|
|
229
|
+
前缀树实现,支持参数约束、通配符、路由分组和 fallback。
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { Router } from '@tyno/tyno/router'
|
|
233
|
+
|
|
234
|
+
const r = new Router({ prefix: '/api' })
|
|
235
|
+
|
|
236
|
+
// 路由级中间件(支持单个或数组,use / middleware 等价)
|
|
237
|
+
r.use(async (req, next) => {
|
|
238
|
+
req.user = { id: 1 }
|
|
239
|
+
return next()
|
|
240
|
+
})
|
|
241
|
+
r.middleware([auth, adminCheck]) // 数组批量注册
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
// 方法注册
|
|
245
|
+
r.get('/users/:id(\\d+)', (req) => ({ id: req.params.id }))
|
|
246
|
+
r.head('/health', () => Response.empty(200))
|
|
247
|
+
r.post('/users', async (req) => ({ created: await req.body() }))
|
|
248
|
+
r.put('/users/:id', async (req) => Response.empty(204))
|
|
249
|
+
r.delete('/users/:id', () => Response.empty(204))
|
|
250
|
+
r.patch('/users/:id', async (req) => 'ok')
|
|
251
|
+
r.all('/any', (req) => `${req.method} matched`)
|
|
252
|
+
r.get('/*', (req) => ({ wild: req.params['*'] }))
|
|
253
|
+
|
|
254
|
+
// 路由分组
|
|
255
|
+
r.group('/admin', (admin) => {
|
|
256
|
+
admin.get('/dashboard', () => 'Admin Panel')
|
|
257
|
+
admin.get('/users', () => 'User List')
|
|
258
|
+
// 等价于 /api/admin/dashboard、/api/admin/users
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
// fallback:所有路由都不匹配时调用
|
|
262
|
+
r.fallback((req) => Response.json({ error: 'Not Found' }, 404))
|
|
263
|
+
|
|
264
|
+
app.use(r.routes())
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**路径模式**:
|
|
268
|
+
|
|
269
|
+
| 模式 | 含义 | 匹配 |
|
|
270
|
+
|------|------|------|
|
|
271
|
+
| `users` | 静态段 | `/api/users` |
|
|
272
|
+
| `:id` | 参数段 | `/api/123` |
|
|
273
|
+
| `:id(\d+)` | 正则约束 | `/api/123` |
|
|
274
|
+
| `*` | 通配符 | `/api/a/b/c` |
|
|
275
|
+
|
|
276
|
+
优先级:**静态 > 参数 > 通配符**。支持 `get/head/post/put/delete/patch/all`。
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
## 错误 & 事件
|
|
281
|
+
|
|
282
|
+
### 错误处理
|
|
283
|
+
|
|
284
|
+
```ts
|
|
285
|
+
import { HttpError, NotFound, RuntimeError, isRuntimeError } from '@tyno/tyno/errors'
|
|
286
|
+
|
|
287
|
+
// HttpError:4xx 默认 expose,5xx 默认隐藏
|
|
288
|
+
throw new NotFound('资源不存在')
|
|
289
|
+
throw new HttpError(401, '请先登录')
|
|
290
|
+
|
|
291
|
+
// RuntimeError:默认 500 不暴露,带 cause 链
|
|
292
|
+
throw new RuntimeError('数据库连接失败', {
|
|
293
|
+
code: 'DB_FAIL',
|
|
294
|
+
cause: new Error('ECONNREFUSED')
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
**快捷类**:`BadRequest`、`Unauthorized`、`Forbidden`、`NotFound`、`Conflict`、`PayloadTooLarge`、`TooManyRequests`、`InternalServerError`。
|
|
299
|
+
|
|
300
|
+
**错误中间件**(参数 >= 3 或 `IS_ERROR_MIDDLEWARE` 标记,按注册顺序串联):
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
import { asErrorMiddleware } from '@tyno/tyno'
|
|
304
|
+
|
|
305
|
+
app.use(async (err, req, next) => {
|
|
306
|
+
if (isRuntimeError(err)) {
|
|
307
|
+
return Response.json({ error: '服务不可用', code: err.code }, 500)
|
|
308
|
+
}
|
|
309
|
+
return Response.json({ error: err.message }, err.status || 500)
|
|
310
|
+
})
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### 事件系统
|
|
314
|
+
|
|
315
|
+
Application 继承 EventEmitter,完整生命周期事件:
|
|
316
|
+
|
|
317
|
+
| 事件 | 触发时机 | 参数 |
|
|
318
|
+
|------|---------|------|
|
|
319
|
+
| `request` | 请求到达,compose 之前,`AsyncLocalStorage` 就绪 | `(req)` |
|
|
320
|
+
| `response` | compose 完成,响应发送**前** | `(req, res)` |
|
|
321
|
+
| `response:sent` | 响应发送**后** | `(req, res)` |
|
|
322
|
+
| `error` | 异常发生(有监听器时触发) | `(err, req)` |
|
|
323
|
+
| `ready` | 服务启动成功 | `()` |
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
import { Tyno } from '@tyno/tyno'
|
|
327
|
+
const app = new Tyno({ debug: true })
|
|
328
|
+
|
|
329
|
+
app.on('request', (req) => {
|
|
330
|
+
console.log(`→ ${req.method} ${req.path}`)
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
app.on('response', (req, res) => {
|
|
334
|
+
// 响应发送前:可记录日志
|
|
335
|
+
console.log(`← ${req.path} ${res.status}`)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
app.on('response:sent', (req, res) => {
|
|
339
|
+
// 响应已发送:可记录耗时、清理资源
|
|
340
|
+
console.log(`✓ ${req.path} ${res.status} done`)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
app.on('error', (err, req) => {
|
|
344
|
+
// 需注册监听器才触发,遵循 Node.js EventEmitter 语义
|
|
345
|
+
logger.error({ err, path: req.path })
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
app.on('ready', () => {
|
|
349
|
+
console.log('Server ready')
|
|
350
|
+
})
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## 内置中间件
|
|
356
|
+
|
|
357
|
+
```ts
|
|
358
|
+
import { cors, compress, serveStatic, requestId } from '@tyno/tyno/middleware'
|
|
359
|
+
|
|
360
|
+
// CORS
|
|
361
|
+
app.use(cors())
|
|
362
|
+
app.use(cors({ origin: ['https://a.com'], credentials: true, maxAge: 86400 }))
|
|
363
|
+
|
|
364
|
+
// 异步压缩(gzip/deflate),支持流式响应,跳过图片/视频
|
|
365
|
+
app.use(compress())
|
|
366
|
+
app.use(compress({ threshold: 2048, level: 6 }))
|
|
367
|
+
|
|
368
|
+
// 静态文件:ETag/304、Range/206、路径穿越防护
|
|
369
|
+
app.use(serveStatic('./public', { prefix: '/static', maxAge: 3600 }))
|
|
370
|
+
|
|
371
|
+
// 请求 ID:自动注入 req.requestId,写入响应头
|
|
372
|
+
app.use(requestId())
|
|
373
|
+
app.use(requestId({ readFromHeader: false }))
|
|
374
|
+
|
|
375
|
+
// 开发错误页
|
|
376
|
+
new Tyno({ debug: true }) // 自动附加,显示堆栈和 cause,支持 JSON/HTML
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
---
|
|
380
|
+
|
|
381
|
+
## 缓存
|
|
382
|
+
|
|
383
|
+
内存 / 文件 / Redis 三种驱动,统一 `get/set/has/delete/clear/remember` API。
|
|
384
|
+
|
|
385
|
+
**别名**:`Cache`(首字母大写)= `cache`(小写)
|
|
386
|
+
|
|
387
|
+
```ts
|
|
388
|
+
import { Cache } from '@tyno/tyno/facade'
|
|
389
|
+
|
|
390
|
+
const app = new Tyno({ cache: { driver: 'memory', prefix: 'my:', ttl: 3600 } })
|
|
391
|
+
|
|
392
|
+
// 通过 app
|
|
393
|
+
await app.cache().set('user:1', { name: 'Alice' }, 60)
|
|
394
|
+
const user = await app.cache().get('user:1')
|
|
395
|
+
|
|
396
|
+
// remember:不存在时调用 factory
|
|
397
|
+
const config = await app.cache().remember('config', 3600, loadConfig)
|
|
398
|
+
|
|
399
|
+
// 全局门面(中间件内)
|
|
400
|
+
app.use(async () => {
|
|
401
|
+
await Cache.set('key', 'value')
|
|
402
|
+
return Response.json({ ok: true })
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
**文件驱动**:`{ driver: 'file', file: { path: './storage/cache' } }`
|
|
407
|
+
|
|
408
|
+
**Redis 驱动**:需 `npm install redis`,`{ driver: 'redis', redis: { host, port } }`
|
|
409
|
+
|
|
410
|
+
---
|
|
411
|
+
|
|
412
|
+
## 其他
|
|
413
|
+
|
|
414
|
+
### 初始化器
|
|
415
|
+
|
|
416
|
+
```ts
|
|
417
|
+
app.initialize(async (app) => { await db.connect() })
|
|
418
|
+
app.listen(4567) // 自动先执行
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### HTTPS
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
app.listen({ port: 443, tls: { key, cert } })
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 测试
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
const res = await app.inject({ method: 'GET', url: '/?name=alice' })
|
|
431
|
+
res.status // 200
|
|
432
|
+
res.json() // { hello: 'alice' }
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
### SSE
|
|
436
|
+
|
|
437
|
+
```ts
|
|
438
|
+
async function* gen() { yield 'chunk1'; yield 'chunk2' }
|
|
439
|
+
return gen()
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
---
|
|
443
|
+
|
|
444
|
+
## API 速览
|
|
445
|
+
|
|
446
|
+
### Application
|
|
447
|
+
|
|
448
|
+
| 方法 | 说明 |
|
|
449
|
+
|------|------|
|
|
450
|
+
| `new Tyno({ debug?, body?, cache? })` | 创建应用 |
|
|
451
|
+
| `use(fn\|[...fn])` / `middleware(fn\|[...fn])` | 注册中间件,支持数组 |
|
|
452
|
+
| `initialize(fn)` | 注册初始化器 |
|
|
453
|
+
| `listen(port\|opts)` | 启动 HTTP/HTTPS |
|
|
454
|
+
| `close()` | 优雅关闭 |
|
|
455
|
+
| `inject(opts)` | 测试请求 |
|
|
456
|
+
| `cache()` | 缓存实例 |
|
|
457
|
+
| `on/emit/once` | EventEmitter |
|
|
458
|
+
|
|
459
|
+
### Router
|
|
460
|
+
|
|
461
|
+
| 方法 | 说明 |
|
|
462
|
+
|------|------|
|
|
463
|
+
| `new Router({ prefix? })` | 创建路由 |
|
|
464
|
+
| `use(fn\|[...fn])` / `middleware(fn\|[...fn])` | 路由级中间件,支持数组 |
|
|
465
|
+
| `get/head/post/put/delete/patch/all(path, ...h)` | 注册路由 |
|
|
466
|
+
| `group(prefix, fn)` | 路由分组 |
|
|
467
|
+
| `fallback(handler)` | 未匹配时的回退处理 |
|
|
468
|
+
| `routes()` | 返回中间件 |
|
|
469
|
+
|
|
470
|
+
### Request
|
|
471
|
+
|
|
472
|
+
| 别名 | 导出来源 |
|
|
473
|
+
|------|---------|
|
|
474
|
+
| `Request`(类名)= `req` | `tyno` |
|
|
475
|
+
| `request`(全局门面) | `tyno/facade` |
|
|
476
|
+
|
|
477
|
+
| 方法 | 说明 |
|
|
478
|
+
|------|------|
|
|
479
|
+
| `query()/query(k)/get(k)` | Query 参数 |
|
|
480
|
+
| `body()/post/param/input` | Body 取值 |
|
|
481
|
+
| `files()/file(name)` | 文件上传 |
|
|
482
|
+
| `header/cookies/getCookie` | 请求头 |
|
|
483
|
+
|
|
484
|
+
### Response
|
|
485
|
+
|
|
486
|
+
| 别名 | 导出来源 |
|
|
487
|
+
|------|---------|
|
|
488
|
+
| `Response`(类名)= `response` = `res` | `tyno` |
|
|
489
|
+
|
|
490
|
+
| 静态工厂 | Content-Type | 状态码 |
|
|
491
|
+
|---------|-------------|--------|
|
|
492
|
+
| `json(data, status?)` | `application/json` | 200 |
|
|
493
|
+
| `text(data, status?)` | `text/plain` | 200 |
|
|
494
|
+
| `empty(status?)` | — | 204 |
|
|
495
|
+
| `redirect(url, status?)` | `Location` 头 | 302 |
|
|
496
|
+
| `image(data, type?, status?)` | `image/*` | 200 |
|
|
497
|
+
|
|
498
|
+
| 实例/静态方法 | 说明 |
|
|
499
|
+
|-------------|------|
|
|
500
|
+
| `set/setHeader/type/setStatus` | 链式修改 |
|
|
501
|
+
| `setCookie/clearCookie/attachment` | Cookie + 下载 |
|
|
502
|
+
| `get(name)` | 读取响应头 |
|
|
503
|
+
| `static header(name, value)` | 预设响应头(门面) |
|
|
504
|
+
| `static setCookie(name, value, options?)` | 预设 Cookie(门面) |
|
|
505
|
+
|
|
506
|
+
### 错误类
|
|
507
|
+
|
|
508
|
+
| 类 | 说明 |
|
|
509
|
+
|----|------|
|
|
510
|
+
| `AppError` | 基类 |
|
|
511
|
+
| `HttpError(status, msg?, props?)` | HTTP 错误 |
|
|
512
|
+
| `RuntimeError(msg, opts?)` | 运行时错误 |
|
|
513
|
+
| `NotFound/Forbidden/...` | 快捷子类 |
|
|
514
|
+
|
|
515
|
+
### 全局门面
|
|
516
|
+
|
|
517
|
+
| 导出 | 来源 | 说明 |
|
|
518
|
+
|------|------|------|
|
|
519
|
+
| `req` | `tyno` | Request 类别名 |
|
|
520
|
+
| `res` | `tyno` | Response 类别名 |
|
|
521
|
+
| `request` | `tyno/facade` | 读取当前请求 |
|
|
522
|
+
| `Cache / cache` | `tyno/facade` | 缓存操作 |
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
## 项目结构
|
|
527
|
+
|
|
528
|
+
```
|
|
529
|
+
tyno/
|
|
530
|
+
├── src/
|
|
531
|
+
│ ├── index.ts # 主入口
|
|
532
|
+
│ ├── application.ts # Application (EventEmitter)
|
|
533
|
+
│ ├── compose.ts # 洋葱模型 + 错误处理
|
|
534
|
+
│ ├── context.ts # AsyncLocalStorage
|
|
535
|
+
│ ├── types.ts # 类型定义
|
|
536
|
+
│ ├── response.ts # Response 类
|
|
537
|
+
│ ├── response/sse.ts # SSEStream
|
|
538
|
+
│ ├── request/
|
|
539
|
+
│ │ ├── index.ts # Request 类
|
|
540
|
+
│ │ ├── body-parser.ts # BodyParser
|
|
541
|
+
│ │ └── multipart-parser.ts # 流式 multipart
|
|
542
|
+
│ ├── router/
|
|
543
|
+
│ │ ├── index.ts # Router + group + fallback
|
|
544
|
+
│ │ ├── node.ts # TrieNode
|
|
545
|
+
│ │ └── parse-path.ts # 路径解析
|
|
546
|
+
│ ├── errors/
|
|
547
|
+
│ │ ├── app-error.ts # AppError
|
|
548
|
+
│ │ ├── http-error.ts # HttpError
|
|
549
|
+
│ │ └── runtime-error.ts # RuntimeError
|
|
550
|
+
│ ├── middlewares/
|
|
551
|
+
│ │ ├── index.ts # 子路径导出
|
|
552
|
+
│ │ ├── cors.ts # CORS
|
|
553
|
+
│ │ ├── compress.ts # 异步压缩
|
|
554
|
+
│ │ ├── static.ts # 静态文件 + Range
|
|
555
|
+
│ │ ├── request-id.ts # 请求 ID
|
|
556
|
+
│ │ └── error-page.ts # 开发错误页
|
|
557
|
+
│ ├── cache/
|
|
558
|
+
│ │ ├── index.ts / manager.ts / types.ts
|
|
559
|
+
│ │ └── drivers/ (memory / file / redis)
|
|
560
|
+
│ ├── facade/ # 门面子路径导出
|
|
561
|
+
│ ├── request-global.ts # 全局 request
|
|
562
|
+
│ ├── cache-facade.ts # 全局 cache
|
|
563
|
+
│ └── mime.ts
|
|
564
|
+
├── example/app.ts
|
|
565
|
+
├── test/functional.test.ts
|
|
566
|
+
├── package.json
|
|
567
|
+
└── tsconfig.json
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
## License
|
|
571
|
+
|
|
572
|
+
MIT
|