Skip to content
Our Sponsors
Open in Anthropic

title: 生命周期 - ElysiaJS head: - - meta - property: 'og:title' content: 生命周期 - ElysiaJS

- - meta
  - name: 'description'
    content: 生命周期事件是 Elysia 处理每个阶段的概念,"生命周期"或"钩子"是一个事件监听器,用于拦截和监听这些循环发生的事件。钩子允许您转换在数据管道中运行的数据。通过钩子,您可以最大限度地定制 Elysia。

- - meta
  - property: 'og:description'
    content: 生命周期事件是 Elysia 处理每个阶段的概念,"生命周期"或"钩子"是一个事件监听器,用于拦截和监听这些循环发生的事件。钩子允许您转换在数据管道中运行的数据。通过钩子,您可以最大限度地定制 Elysia。

生命周期

生命周期事件允许您在预定义的节点拦截重要事件,从而根据需要自定义服务器的行为。

Elysia 的生命周期如下图所示。 Elysia Life Cycle Graph

点击图片放大

以下是 Elysia 中可用的请求生命周期事件:

为什么

假设我们想返回一些 HTML。

通常,我们会将 "Content-Type" 头设置为 "text/html",以便浏览器可以渲染它。

但为每个路由手动设置是一项繁琐的工作。

相反,如果框架能检测到响应是 HTML 并自动为您设置头信息呢?这就是生命周期概念的由来。

钩子

每个拦截 生命周期事件 的函数都称为 "钩子"

(因为函数 "钩住" 了生命周期事件)

钩子可以分为两类:

  1. 本地钩子: 在特定路由上执行
  2. 拦截器钩子: 在钩子注册后于每个路由上执行

TIP

钩子将接受与处理器相同的 Context;您可以将其想象为在特定点添加一个路由处理器。

本地钩子

本地钩子在特定路由上执行。

要使用本地钩子,您可以将钩子内联到路由处理器中:

typescript
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
/hitext/plain; charset=utf8

拦截器钩子

将钩子注册到当前实例中之后的所有处理器。

要添加拦截器钩子,您可以使用 .on 后跟一个驼峰式命名生命周期事件:

typescript
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
/nonetext/plain; charset=utf8
/text/html; charset=utf8
/hitext/html; charset=utf8

来自其他插件的事件也会应用于该路由,因此代码顺序很重要。

代码顺序

事件仅在注册后应用于路由。

如果您将 onError 放在插件之前,插件将不会继承 onError 事件。

typescript
import { Elysia } from 'elysia'

new Elysia()
 	.onBeforeHandle(() => {
        console.log('1')
    })
	.get('/', () => 'hi')
    .onBeforeHandle(() => {
        console.log('2')
    })
    .listen(3000)

控制台应记录以下内容:

bash
1

注意,它没有记录 2,因为事件是在路由之后注册的,所以它没有应用于该路由。

这也适用于插件。

typescript
import { Elysia } from 'elysia'

new Elysia()
	.onBeforeHandle(() => {
		console.log('1')
	})
	.use(someRouter)
	.onBeforeHandle(() => {
		console.log('2')
	})
	.listen(3000)

在本例中,只会记录 1,因为事件是在插件之后注册的。

除了 onRequest 之外,所有事件都遵循相同的规则。 因为 onRequest 在请求时发生,它不知道要应用于哪个路由,所以它是一个全局事件

请求

每个新请求接收后执行的第一个生命周期事件。

由于 onRequest 旨在仅提供最关键的上下文以减少开销,建议在以下场景中使用:

  • 缓存
  • 速率限制器 / IP/地区锁定
  • 分析
  • 提供自定义头,例如 CORS

示例

以下是对某个 IP 地址强制执行速率限制的伪代码。

typescript
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 返回一个值,它将被用作响应,并且生命周期的其余部分将被跳过。

预上下文

Context 的 onRequest 类型为 PreContext,它是 Context 的最小表示,具有以下属性: request: Request

  • set: Set
  • store
  • decorators

Context 不提供 derived 值,因为 derive 是基于 onTransform 事件的。

解析

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 解析器。

示例

以下是基于自定义头检索值的示例代码。

typescript
import { Elysia } from 'elysia'

new Elysia().onParse(({ request, contentType }) => {
    if (contentType === 'application/custom-type') return request.text()
})

返回值将被分配给 Context.body。如果没有,Elysia 将继续从 onParse 栈中迭代额外的解析器函数,直到 body 被分配或所有解析器都执行完毕。

上下文

onParse Context 扩展自 Context,并具有以下附加属性:

  • contentType: 请求的 Content-Type 头

所有上下文都基于普通上下文,可以像路由处理器中的普通上下文一样使用。

解析器

默认情况下,Elysia 会尝试提前确定 body 解析函数,并选择最合适的函数以加快处理速度。

Elysia 能够通过读取 body 来确定 body 函数。

看这个例子:

typescript
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 解析器,并减少编译时的开销。

显式解析器

但是,在某些情况下,如果 Elysia 未能选择正确的 body 解析函数,我们可以通过指定 type 来明确告诉 Elysia 使用某个函数。

typescript
import { Elysia } from 'elysia'

new Elysia().post('/', ({ body }) => body, {
    // application/json 的简写
    parse: 'json'
})

这允许我们控制 Elysia 选择 body 解析函数的行为,以适应复杂场景中的需求。

type 可以是以下之一:

typescript
type ContentType = |
    // 'text/plain' 的简写
    | 'text'
    // 'application/json' 的简写
    | 'json'
    // 'multipart/form-data' 的简写
    | 'formdata'
    // 'application/x-www-form-urlencoded' 的简写
    | 'urlencoded'
    // 完全跳过 body 解析
    | 'none'
    | 'text/plain'
    | 'application/json'
    | 'multipart/form-data'
    | 'application/x-www-form-urlencoded'

跳过 Body 解析

当您需要将第三方库与 HTTP 处理器(如 trpcorpc)集成时,它可能会抛出 Body is already used 错误。

这是因为 Web Standard Request 只能解析一次。

Elysia 和第三方库都有自己的 body 解析器,因此您可以通过指定 parse: 'none' 来跳过 Elysia 端的 body 解析。

typescript
import { Elysia } from 'elysia'

new Elysia()
	.post(
		'/',
		({ request }) => library.handle(request),
		{
			parse: 'none'
		}
	)

自定义解析器

您可以使用 parser 注册一个自定义解析器:

typescript
import { Elysia } from 'elysia'

new Elysia()
    .parser('custom', ({ request, contentType }) => {
        if (contentType === 'application/elysia') return request.text()
    })
    .post('/', ({ body }) => body, {
        parse: ['custom', 'json']
    })

转换

验证 过程之前执行,用于改变上下文以符合验证或附加新值。

建议在以下情况下使用转换:

  • 改变现有上下文以符合验证。
  • derive 基于 onTransform,并支持提供类型。

示例

以下是一个使用转换将 params 变为数值的示例。

typescript
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)

派生

在验证之前直接向上下文附加新值。它存储在与 transform 相同的栈中。

与在服务器启动前分配值的 statedecorate 不同。derive 在每个请求发生时分配一个属性。这允许我们将一段信息提取到一个属性中。

typescript
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 可以访问 headersquerybody 等 Request 属性,而 storedecorate 则不能。

statedecorate 不同。由 derive 分配的属性是唯一的,不与其他请求共享。

TIP

在大多数情况下,您可能希望使用 resolve 代替 derive。

Resolve 与 derive 类似,但在验证后执行。这使得 resolve 更安全,因为我们可以验证传入数据,然后再用它来派生新属性。

队列

derivetransform 存储在同一个队列中。

typescript
import { Elysia } from 'elysia'

new Elysia()
    .onTransform(() => {
        console.log(1)
    })
    .derive(() => {
        console.log(2)

        return {}
    })

控制台应记录以下内容:

bash
1
2

处理前

在验证之后和主路由处理器之前执行。

旨在提供自定义验证,以满足在运行主处理器之前的特定要求。

如果返回一个值,路由处理器将被跳过。

建议在以下情况下使用处理前钩子:

  • 访问限制检查:授权、用户登录
  • 对数据结构的自定义请求要求

示例

以下是一个使用处理前钩子检查用户登录的示例。

typescript
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)

响应应如下所示:

是否已登录响应
未授权
Hi

守卫

当我们需要将相同的处理前钩子应用于多个路由时,我们可以使用 guard 将相同的处理前钩子应用于多个路由。

typescript
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)

解析

在验证之后向上下文附加新值。它存储在与 beforeHandle 相同的栈中。

Resolve 语法与 derive 相同,以下是从 Authorization 插件检索 bearer 头的示例。

typescript
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)

使用 resolveonBeforeHandle 存储在同一个队列中。

typescript
import { Elysia } from 'elysia'

new Elysia()
    .onBeforeHandle(() => {
        console.log(1)
    })
    .resolve(() => {
        console.log(2)

        return {}
    })
    .onBeforeHandle(() => {
        console.log(3)
    })

控制台应记录以下内容:

bash
1
2
3

derive 相同,由 resolve 分配的属性是唯一的,不与其他请求共享。

守卫解析

由于 resolve 在本地钩子中不可用,建议使用 guard 来封装 resolve 事件。

typescript
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)

处理后

在主处理器之后执行,用于将 处理前钩子路由处理器 的返回值映射为适当的响应。

建议在以下情况下使用处理后钩子:

  • 将请求转换为新值,例如压缩、事件流
  • 根据响应值添加自定义头,例如 Content-Type

示例

以下是一个使用处理后钩子向响应头添加 HTML 内容类型的示例。

typescript
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
/hitext/plain; charset=utf8

返回值

如果返回一个值,处理后钩子将使用该返回值作为新的响应值,除非该值是 undefined

上面的例子可以重写如下:

typescript
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 的迭代 不会 被跳过。

上下文

onAfterHandle 上下文扩展自 Context,并具有 response 附加属性,该属性是要返回给客户端的响应。

onAfterHandle 上下文基于普通上下文,可以像路由处理器中的普通上下文一样使用。

映射响应

"afterHandle" 之后立即执行,旨在提供自定义响应映射。

建议在以下情况下使用转换:

  • 压缩
  • 将值映射为 Web 标准 Response

示例

以下是一个使用 mapResponse 提供响应压缩的示例。

typescript
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)

parsebeforeHandle 一样,在返回一个值后,mapResponse 的下一次迭代将被跳过。

Elysia 将自动处理来自 mapResponseset.headers 的合并过程。我们无需担心手动将 set.headers 附加到 Response。

错误时(错误处理)

专为错误处理而设计。当在任何生命周期中抛出错误时,它将被执行。

建议在以下情况下使用错误时钩子:

  • 提供自定义错误消息
  • 故障安全处理、错误处理或重试请求
  • 日志记录和分析

示例

Elysia 捕获处理器中抛出的所有错误,对错误代码进行分类,并将它们传递给 onError 中间件。

typescript
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 必须在我们想要应用它的处理器之前调用。

自定义 404 消息

例如,返回自定义 404 消息:

typescript
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)

上下文

onError Context 扩展自 Context,并具有以下附加属性:

  • error: 抛出的值
  • code: 错误代码

错误代码

Elysia 错误代码包括:

  • NOT_FOUND
  • PARSE
  • VALIDATION
  • INTERNAL_SERVER_ERROR
  • INVALID_COOKIE_SIGNATURE
  • INVALID_FILE_TYPE
  • UNKNOWN
  • number (基于 HTTP 状态码)

默认情况下,抛出的错误代码是 UNKNOWN

TIP

如果没有返回错误响应,则错误将使用 error.name 返回。

本地错误

与其他生命周期一样,我们使用 guard 将错误提供一个作用域:

typescript
import { Elysia } from 'elysia'

new Elysia()
    .get('/', () => 'Hello', {
        beforeHandle({ set, request: { headers }, error }) {
            if (!isSignIn(headers)) throw error(401)
        },
        error() {
            return 'Handled'
        }
    })
    .listen(3000)

响应后

在响应发送给客户端后执行。

建议在以下情况下使用 响应后 钩子:

  • 清理响应
  • 日志记录和分析

示例

以下是一个使用响应后钩子记录响应时间的示例。

typescript
import { Elysia } from 'elysia'

new Elysia()
    .onAfterResponse(() => {
        console.log('Response', performance.now())
    })
    .listen(3000)

控制台应记录以下内容:

bash
Response 0.0000
Response 0.0001
Response 0.0002

响应

映射响应 类似,afterResponse 也接受 responseValue 值。

typescript
import { Elysia } from 'elysia'

new Elysia()
	.onAfterResponse(({ responseValue }) => {
		console.log(responseValue)
	})
	.get('/', () => 'Hello')
	.listen(3000)

onAfterResponse 中的 response 不是 Web 标准的 Response,而是从处理器返回的值。

要获取从处理器返回的头和状态,我们可以从上下文中访问 set

typescript
import { Elysia } from 'elysia'

new Elysia()
	.onAfterResponse(({ set }) => {
		console.log(set.status, set.headers)
	})
	.get('/', () => 'Hello')
	.listen(3000)