生命周期
生命周期事件允许您在预定义点拦截重要事件,从而根据需要自定义服务器的行为。
Elysia 的生命周期可以如以下图所示。
点击图像放大
以下是 Elysia 中可用的请求生命周期事件:
为什么
假设我们想要返回一些 HTML。
通常,我们会将 "Content-Type" 标头设置为 "text/html",以便浏览器可以渲染它。
但为每个路由手动设置一个是很繁琐的。
相反,如果框架能够检测响应是 HTML 并自动为您设置标头呢?这正是生命周期理念的用武之地。
钩子
每个拦截 生命周期事件 的函数作为 "hook"。
(因为函数 "hooks" 进入生命周期事件)
钩子可以分为 2 种类型:
TIP
钩子将接受与处理器相同的 Context;您可以想象在特定点添加路由处理器。
本地钩子
本地钩子在特定路由上执行。
要使用本地钩子,您可以内联钩子到路由处理器中:
import { Elysia } from 'elysia'
import { isHtml } from '@elysiajs/html'
new Elysia()
.get('/', () => '<h1>Hello World</h1>', {
afterHandle({ responseValue, set }) {
if (isHtml(responseValue))
set.headers['Content-Type'] = 'text/html; charset=utf8'
}
})
.get('/hi', () => '<h1>Hello World</h1>')
.listen(3000)
响应应列出如下:
路径 | Content-Type |
---|---|
/ | text/html; charset=utf8 |
/hi | text/plain; charset=utf8 |
拦截器钩子
将钩子注册到当前实例之后的每个处理器中。
要添加拦截器钩子,您可以使用 .on
后跟 camelCase 的生命周期事件:
import { Elysia } from 'elysia'
import { isHtml } from '@elysiajs/html'
new Elysia()
.get('/none', () => '<h1>Hello World</h1>')
.onAfterHandle(({ responseValue, set }) => {
if (isHtml(responseValue))
set.headers['Content-Type'] = 'text/html; charset=utf8'
})
.get('/', () => '<h1>Hello World</h1>')
.get('/hi', () => '<h1>Hello World</h1>')
.listen(3000)
响应应列出如下:
路径 | Content-Type |
---|---|
/none | text/plain; charset=utf8 |
/ | text/html; charset=utf8 |
/hi | text/html; charset=utf8 |
其他插件的事件也会应用于路由,因此代码顺序很重要。
代码顺序
事件仅适用于注册之后的路由。
如果您将 onError
放在插件之前,插件将不会继承 onError
事件。
import { Elysia } from 'elysia'
new Elysia()
.onBeforeHandle(() => {
console.log('1')
})
.get('/', () => 'hi')
.onBeforeHandle(() => {
console.log('2')
})
.listen(3000)
控制台应记录以下内容:
1
注意,它没有记录 2,因为事件在路由之后注册,因此不适用于该路由。
这也适用于插件。
import { Elysia } from 'elysia'
new Elysia()
.onBeforeHandle(() => {
console.log('1')
})
.use(someRouter)
.onBeforeHandle(() => {
console.log('2')
})
.listen(3000)
在这个示例中,只有 1 会被记录,因为事件在插件之后注册。
每个事件都遵循相同规则,除了 onRequest
。 因为 onRequest 发生在请求上,它不知道应用于哪个路由,因此它是一个全局事件
Request
每个新请求收到的第一个执行的生命周期事件。
由于 onRequest
旨在仅提供最关键的上下文以减少开销,建议在以下场景中使用:
- 缓存
- 速率限制器 / IP/Region 锁定
- 分析
- 提供自定义标头,例如 CORS
示例
以下是针对特定 IP 地址强制执行速率限制的伪代码。
import { Elysia } from 'elysia'
new Elysia()
.use(rateLimiter)
.onRequest(({ rateLimiter, ip, set, status }) => {
if (rateLimiter.check(ip)) return status(420, 'Enhance your calm')
})
.get('/', () => 'hi')
.listen(3000)
如果从 onRequest
返回一个值,它将被用作响应,并跳过生命周期的其余部分。
Pre Context
Context 的 onRequest
被键入为 PreContext
,它是 Context
的最小表示,具有以下属性: request: Request
- set:
Set
- store
- decorators
Context 不提供 derived
值,因为 derive 基于 onTransform
事件。
Parse
Parse 等同于 Express 中的 body parser。
一个用于解析 body 的函数,返回值将附加到 Context.body
,否则 Elysia 将继续迭代由 onParse
分配的附加解析器函数,直到 body 被分配或所有解析器都已执行。
默认情况下,Elysia 将使用以下 content-type 解析 body:
text/plain
application/json
multipart/form-data
application/x-www-form-urlencoded
建议使用 onParse
事件来提供 Elysia 未提供的自定义 body 解析器。
示例
以下是基于自定义标头检索值的示例代码。
import { Elysia } from 'elysia'
new Elysia().onParse(({ request, contentType }) => {
if (contentType === 'application/custom-type') return request.text()
})
返回值将被分配到 Context.body
。否则,Elysia 将继续迭代来自 onParse 堆栈的附加解析器函数,直到 body 被分配或所有解析器都已执行。
Context
onParse
Context 从 Context
扩展,并具有以下附加属性:
- contentType: 请求的 Content-Type 标头
所有上下文基于正常上下文,可以像路由处理器中的正常上下文一样使用。
Parser
默认情况下,Elysia 将尝试提前确定 body 解析函数,并选择最合适的函数以加速进程。
Elysia 可以通过读取 body
来确定 body 函数。
看看这个示例:
import { Elysia, t } from 'elysia'
new Elysia().post('/', ({ body }) => body, {
body: t.Object({
username: t.String(),
password: t.String()
})
})
Elysia 读取 body 模式并发现类型完全是对象,因此 body 很可能是 JSON。Elysia 然后提前选择 JSON body 解析器函数并尝试解析 body。
以下是 Elysia 用于选择 body 解析器类型的标准
application/json
:body 键入为t.Object
multipart/form-data
:body 键入为t.Object
,并且深度为 1 级,具有t.File
application/x-www-form-urlencoded
:body 键入为t.URLEncoded
text/plain
:其他原始类型
这允许 Elysia 提前优化 body 解析器,并在编译时减少开销。
Explicit Parser
然而,在某些场景中,如果 Elysia 未能选择正确的 body 解析器函数,我们可以通过指定 type
明确告诉 Elysia 使用某个函数。
import { Elysia } from 'elysia'
new Elysia().post('/', ({ body }) => body, {
// Short form of application/json
parse: 'json'
})
这允许我们在复杂场景中控制 Elysia 选择 body 解析器函数的行为,以满足我们的需求。
type
可以是以下之一:
type ContentType = |
// Shorthand for 'text/plain'
| 'text'
// Shorthand for 'application/json'
| 'json'
// Shorthand for 'multipart/form-data'
| 'formdata'
// Shorthand for 'application/x-www-form-urlencoded'
| 'urlencoded'
// Skip body parsing entirely
| 'none'
| 'text/plain'
| 'application/json'
| 'multipart/form-data'
| 'application/x-www-form-urlencoded'
Skip Body Parsing
当您需要集成像 trpc
、orpc
这样的第三方库与 HTTP 处理器,并且它抛出 Body is already used
时。
这是因为 Web Standard Request 只能解析一次。
Elysia 和第三方库都有自己的 body 解析器,因此您可以通过指定 parse: 'none'
在 Elysia 端跳过 body 解析
import { Elysia } from 'elysia'
new Elysia()
.post(
'/',
({ request }) => library.handle(request),
{
parse: 'none'
}
)
Custom Parser
您可以使用 parser
注册自定义解析器:
import { Elysia } from 'elysia'
new Elysia()
.parser('custom', ({ request, contentType }) => {
if (contentType === 'application/elysia') return request.text()
})
.post('/', ({ body }) => body, {
parse: ['custom', 'json']
})
Transform
在 Validation 进程之前执行,旨在变异上下文以符合验证或附加新值。
建议将 transform 用于以下情况:
- 变异现有上下文以符合验证。
derive
基于onTransform
,并支持提供类型。
示例
以下是使用 transform 将 params 变异为数值值的示例。
import { Elysia, t } from 'elysia'
new Elysia()
.get('/id/:id', ({ params: { id } }) => id, {
params: t.Object({
id: t.Number()
}),
transform({ params }) {
const id = +params.id
if (!Number.isNaN(id)) params.id = id
}
})
.listen(3000)
Derive
直接在验证之前将新值附加到上下文。它与 transform 存储在相同的堆栈中。
与在服务器启动前分配值的 state 和 decorate 不同。derive 在每个请求发生时分配属性。这允许我们将一段信息提取到属性中。
import { Elysia } from 'elysia'
new Elysia()
.derive(({ headers }) => {
const auth = headers['Authorization']
return {
bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null
}
})
.get('/', ({ bearer }) => bearer)
因为 derive 在新请求开始时分配一次,derive 可以访问像 headers、query、body 这样的 Request 属性,而 store 和 decorate 不能。
与 state 和 decorate 不同。由 derive 分配的属性是唯一的,不与其他请求共享。
Queue
derive
和 transform
存储在相同的队列中。
import { Elysia } from 'elysia'
new Elysia()
.onTransform(() => {
console.log(1)
})
.derive(() => {
console.log(2)
return {}
})
控制台应记录以下内容:
1
2
Before Handle
在验证之后和主路由处理器之前执行。
旨在提供自定义验证,以在运行主处理器之前满足特定要求。
如果返回一个值,路由处理器将被跳过。
建议在以下情况下使用 Before Handle:
- 受限访问检查:授权、用户登录
- 数据结构之上的自定义请求要求
示例
以下是使用 before handle 检查用户登录的示例。
import { Elysia } from 'elysia'
import { validateSession } from './user'
new Elysia()
.get('/', () => 'hi', {
beforeHandle({ set, cookie: { session }, status }) {
if (!validateSession(session.value)) return status(401)
}
})
.listen(3000)
响应应列出如下:
是否登录 | 响应 |
---|---|
❌ | Unauthorized |
✅ | Hi |
Guard
当我们需要将相同的 before handle 应用于多个路由时,我们可以使用 guard
将相同的 before handle 应用于多个路由。
import { Elysia } from 'elysia'
import { signUp, signIn, validateSession, isUserExists } from './user'
new Elysia()
.guard(
{
beforeHandle({ set, cookie: { session }, status }) {
if (!validateSession(session.value)) return status(401)
}
},
(app) =>
app
.get('/user/:id', ({ body }) => signUp(body))
.post('/profile', ({ body }) => signIn(body), {
beforeHandle: isUserExists
})
)
.get('/', () => 'hi')
.listen(3000)
Resolve
在验证之后将新值附加到上下文。它与 beforeHandle 存储在相同的堆栈中。
Resolve 语法与 derive 相同,以下是从 Authorization 插件检索 bearer 标头的示例。
import { Elysia, t } from 'elysia'
new Elysia()
.guard(
{
headers: t.Object({
authorization: t.TemplateLiteral('Bearer ${string}')
})
},
(app) =>
app
.resolve(({ headers: { authorization } }) => {
return {
bearer: authorization.split(' ')[1]
}
})
.get('/', ({ bearer }) => bearer)
)
.listen(3000)
使用 resolve
和 onBeforeHandle
存储在相同的队列中。
import { Elysia } from 'elysia'
new Elysia()
.onBeforeHandle(() => {
console.log(1)
})
.resolve(() => {
console.log(2)
return {}
})
.onBeforeHandle(() => {
console.log(3)
})
控制台应记录以下内容:
1
2
3
与 derive 相同,由 resolve 分配的属性是唯一的,不与其他请求共享。
Guard resolve
由于 resolve 不可用于本地钩子,建议使用 guard 来封装 resolve 事件。
import { Elysia } from 'elysia'
import { isSignIn, findUserById } from './user'
new Elysia()
.guard(
{
beforeHandle: isSignIn
},
(app) =>
app
.resolve(({ cookie: { session } }) => ({
userId: findUserById(session.value)
}))
.get('/profile', ({ userId }) => userId)
)
.listen(3000)
After Handle
在主处理器之后执行,用于将 before handle 和 route handler 的返回值映射为适当的响应。
建议在以下情况下使用 After Handle:
- 将请求转换为新值,例如压缩、事件流
- 基于响应值添加自定义标头,例如 Content-Type
示例
以下是使用 after handle 为响应标头添加 HTML 内容类型的示例。
import { Elysia } from 'elysia'
import { isHtml } from '@elysiajs/html'
new Elysia()
.get('/', () => '<h1>Hello World</h1>', {
afterHandle({ response, set }) {
if (isHtml(response))
set.headers['content-type'] = 'text/html; charset=utf8'
}
})
.get('/hi', () => '<h1>Hello World</h1>')
.listen(3000)
响应应列出如下:
路径 | Content-Type |
---|---|
/ | text/html; charset=utf8 |
/hi | text/plain; charset=utf8 |
Returned Value
如果 After Handle 返回一个值,除非值为 undefined,否则将使用返回值作为新响应值
上面的示例可以重写为以下内容:
import { Elysia } from 'elysia'
import { isHtml } from '@elysiajs/html'
new Elysia()
.get('/', () => '<h1>Hello World</h1>', {
afterHandle({ response, set }) {
if (isHtml(response)) {
set.headers['content-type'] = 'text/html; charset=utf8'
return new Response(response)
}
}
})
.get('/hi', () => '<h1>Hello World</h1>')
.listen(3000)
与 beforeHandle 不同,在从 afterHandle 返回值之后,afterHandle 的迭代 不会 被跳过。
Context
onAfterHandle
上下文从 Context
扩展,并具有 response
的附加属性,这是要返回给客户端的响应。
onAfterHandle
上下文基于正常上下文,可以像路由处理器中的正常上下文一样使用。
Map Response
在 "afterHandle" 之后立即执行,旨在提供自定义响应映射。
建议将 transform 用于以下情况:
- 压缩
- 将值映射为 Web Standard Response
示例
以下是使用 mapResponse 提供响应压缩的示例。
import { Elysia } from 'elysia'
const encoder = new TextEncoder()
new Elysia()
.mapResponse(({ responseValue, set }) => {
const isJson = typeof response === 'object'
const text = isJson
? JSON.stringify(responseValue)
: (responseValue?.toString() ?? '')
set.headers['Content-Encoding'] = 'gzip'
return new Response(Bun.gzipSync(encoder.encode(text)), {
headers: {
'Content-Type': `${
isJson ? 'application/json' : 'text/plain'
}; charset=utf-8`
}
})
})
.get('/text', () => 'mapResponse')
.get('/json', () => ({ map: 'response' }))
.listen(3000)
与 parse 和 beforeHandle 类似,在返回值之后,mapResponse 的下一次迭代将被跳过。
Elysia 将自动处理来自 mapResponse 的 set.headers 合并过程。我们无需手动将 set.headers 附加到 Response。
On Error (Error Handling)
专为错误处理设计。它将在任何生命周期中抛出错误时执行。
建议在以下情况下使用 on Error:
- 提供自定义错误消息
- 故障安全处理、错误处理器或重试请求
- 日志和分析
示例
Elysia 捕获处理器中抛出的所有错误,将错误代码分类,并将它们管道传输到 onError
中间件。
import { Elysia } from 'elysia'
new Elysia()
.onError(({ error }) => {
return new Response(error.toString())
})
.get('/', () => {
throw new Error('Server is during maintenance')
return 'unreachable'
})
使用 onError
,我们可以捕获并将错误转换为自定义错误消息。
TIP
重要的是,onError
必须在我们想要应用它的处理器之前调用。
Custom 404 message
例如,返回自定义 404 消息:
import { Elysia, NotFoundError } from 'elysia'
new Elysia()
.onError(({ code, status, set }) => {
if (code === 'NOT_FOUND') return status(404, 'Not Found :(')
})
.post('/', () => {
throw new NotFoundError()
})
.listen(3000)
Context
onError
Context 从 Context
扩展,并具有以下附加属性:
- error:抛出的值
- code:Error Code
Error Code
Elysia 错误代码包括:
- NOT_FOUND
- PARSE
- VALIDATION
- INTERNAL_SERVER_ERROR
- INVALID_COOKIE_SIGNATURE
- INVALID_FILE_TYPE
- UNKNOWN
- number (基于 HTTP Status)
默认情况下,抛出的错误代码是 UNKNOWN
。
TIP
如果没有返回错误响应,则将使用 error.name
返回错误。
Local Error
与其它生命周期相同,我们使用 guard 将错误提供到 scope 中:
import { Elysia } from 'elysia'
new Elysia()
.get('/', () => 'Hello', {
beforeHandle({ set, request: { headers }, error }) {
if (!isSignIn(headers)) throw error(401)
},
error() {
return 'Handled'
}
})
.listen(3000)
After Response
在响应发送给客户端之后执行。
建议在以下情况下使用 After Response:
- 清理响应
- 日志和分析
示例
以下是使用 response handle 检查用户登录的示例。
import { Elysia } from 'elysia'
new Elysia()
.onAfterResponse(() => {
console.log('Response', performance.now())
})
.listen(3000)
控制台应记录以下内容:
Response 0.0000
Response 0.0001
Response 0.0002
Response
类似于 Map Response,afterResponse
也接受 responseValue
值。
import { Elysia } from 'elysia'
new Elysia()
.onAfterResponse(({ responseValue }) => {
console.log(responseValue)
})
.get('/', () => 'Hello')
.listen(3000)
onAfterResponse
中的 response
不是 Web-Standard 的 Response
,而是从处理器返回的值。
要获取从处理器返回的标头和状态,我们可以从上下文中访问 set
。
import { Elysia } from 'elysia'
new Elysia()
.onAfterResponse(({ set }) => {
console.log(set.status, set.headers)
})
.get('/', () => 'Hello')
.listen(3000)