Skip to content
我们的赞助商

插件

插件是一种将功能解耦为更小部分的设计模式。为我们的 Web 服务器创建可重用的组件。

要创建插件,就是创建一个单独的实例。

typescript
import { 
Elysia
} from 'elysia'
const
plugin
= new
Elysia
()
.
decorate
('plugin', 'hi')
.
get
('/plugin', ({
plugin
}) =>
plugin
)
const
app
= new
Elysia
()
.
use
(
plugin
)
.
get
('/', ({
plugin
}) =>
plugin
)
.
listen
(3000)

我们可以通过将实例传递给 Elysia.use 来使用插件。

localhost

GET

插件将继承插件实例的所有属性,如 statedecorate,但 不会继承插件生命周期,因为它默认是 隔离的

Elysia 还会自动处理类型推断。

插件

每个 Elysia 实例都可以作为一个插件。

我们将逻辑解耦到一个单独的 Elysia 实例中,并在多个实例中重用它。

要创建插件,只需在单独的文件中定义一个实例:

typescript
// plugin.ts
import { 
Elysia
} from 'elysia'
export const
plugin
= new
Elysia
()
.
get
('/plugin', () => 'hi')

然后我们将实例导入到主文件中:

typescript
import { Elysia } from 'elysia'
import { plugin } from './plugin'

const app = new Elysia()
    .use(plugin) 
    .listen(3000)

作用域

Elysia 生命周期方法 仅封装在其自身实例中

这意味着如果你创建一个新实例,它不会与其他实例共享生命周期方法。

ts
import { Elysia } from 'elysia'

const profile = new Elysia()
	.onBeforeHandle(({ cookie }) => {
		throwIfNotSignIn(cookie)
	})
	.get('/profile', () => 'Hi there!')

const app = new Elysia()
	.use(profile)
	// ⚠️ 这将没有登录检查
	.patch('/rename', ({ body }) => updateProfile(body))

在这个例子中,isSignIn 检查仅适用于 profile,而不适用于 app

localhost

GET

尝试在 URL 栏中更改路径为 /rename 并查看结果


Elysia 默认隔离生命周期,除非明确指定。这类似于 JavaScript 中的 export,你需要导出函数才能在模块外部使用它。

要将生命周期 “导出” 到其他实例,必须指定作用域。

ts
import { Elysia } from 'elysia'

const profile = new Elysia()
	.onBeforeHandle(
		{ as: 'global' }, 
		({ cookie }) => {
			throwIfNotSignIn(cookie)
		}
	)
	.get('/profile', () => 'Hi there!')

const app = new Elysia()
	.use(profile)
	// 这有登录检查
	.patch('/rename', ({ body }) => updateProfile(body))
localhost

GET

将生命周期转换为 "global" 将导出生命周期到 每个实例

作用域级别

Elysia 有 3 个作用域级别,如下所示:

作用域类型如下:

  1. local (默认) - 仅适用于当前实例及其后代
  2. scoped - 适用于父实例、当前实例及其后代
  3. global - 适用于应用该插件的所有实例(所有父实例、当前实例及其后代)

让我们通过以下示例回顾每种作用域类型的作用:

typescript
import { Elysia } from 'elysia'


const child = new Elysia()
    .get('/child', 'hi')

const current = new Elysia()
	// ? 根据下表提供的值
    .onBeforeHandle({ as: 'local' }, () => { 
        console.log('hi')
    })
    .use(child)
    .get('/current', 'hi')

const parent = new Elysia()
    .use(current)
    .get('/parent', 'hi')

const main = new Elysia()
    .use(parent)
    .get('/main', 'hi')

通过更改 type 值,结果应如下所示:

类型childcurrentparentmain
local
scoped
global

后代

默认情况下,插件将 仅将钩子应用到自身及其后代

如果钩子在插件中注册,继承该插件的实例将 不会 继承钩子和模式。

typescript
import { Elysia } from 'elysia'

const plugin = new Elysia()
    .onBeforeHandle(() => {
        console.log('hi')
    })
    .get('/child', 'log hi')

const main = new Elysia()
    .use(plugin)
    .get('/parent', 'not log hi')

要全局应用钩子,我们需要将钩子指定为 global。

typescript
import { Elysia } from 'elysia'

const plugin = new Elysia()
    .onBeforeHandle(() => {
        return 'hi'
    })
    .get('/child', 'child')
    .as('scoped')

const main = new Elysia()
    .use(plugin)
    .get('/parent', 'parent')
localhost

GET

配置

为了使插件更有用,推荐允许通过配置进行自定义。

你可以创建一个接受参数的函数,这些参数可能会改变插件的行为,使其更具可重用性。

typescript
import { Elysia } from 'elysia'

const version = (version = 1) => new Elysia()
        .get('/version', version)

const app = new Elysia()
    .use(version(1))
    .listen(3000)

函数回调

推荐定义一个新的插件实例,而不是使用函数回调。

函数回调允许我们访问主实例的现有属性。例如,检查特定路由或存储是否存在,但正确处理封装和作用域会更难。

要定义函数回调,请创建一个接受 Elysia 作为参数的函数。

typescript
import { 
Elysia
} from 'elysia'
const
plugin
= (
app
:
Elysia
) =>
app
.
state
('counter', 0)
.
get
('/plugin', () => 'Hi')
const
app
= new
Elysia
()
.
use
(
plugin
)
.
get
('/counter', ({
store
: {
counter
} }) =>
counter
)
.
listen
(3000)
localhost

GET

一旦传递给 Elysia.use,函数回调的行为就像普通插件一样,只是属性直接分配给主实例。

TIP

您不必担心函数回调和创建实例之间的性能差异。

Elysia 可以在几毫秒内创建 10k 个实例,新的 Elysia 实例甚至比函数回调具有更好的类型推断性能。

插件去重

默认情况下,Elysia 将注册任何插件并处理类型定义。

某些插件可能被多次使用以提供类型推断,导致初始值或路由的重复。

Elysia 通过使用 name可选种子 来区分实例,从而避免这种情况,以帮助 Elysia 识别实例重复:

typescript
import { Elysia } from 'elysia'

const plugin = <T extends string>(config: { prefix: T }) =>
    new Elysia({
        name: 'my-plugin', 
        seed: config, 
    })
    .get(`${config.prefix}/hi`, () => 'Hi')

const app = new Elysia()
    .use(
        plugin({
            prefix: '/v2'
        })
    )
    .listen(3000)
localhost

GET

Elysia 将使用 nameseed 创建校验和,以识别实例是否之前已注册,如果是,则 Elysia 将跳过插件的注册。

如果未提供 seed,Elysia 将仅使用 name 来区分实例。这意味着即使您多次注册插件,它也只注册一次。

typescript
import { Elysia } from 'elysia'

const plugin = new Elysia({ name: 'plugin' })

const app = new Elysia()
    .use(plugin)
    .use(plugin)
    .use(plugin)
    .use(plugin)
    .listen(3000)

这允许 Elysia 通过重用已注册的插件而不是反复处理插件来提高性能。

TIP

Seed 可以是任何东西,从字符串到复杂对象或类。

如果提供的值是类,Elysia 将尝试使用 .toString 方法生成校验和。

服务定位器

当您将带有 state/decorators 的插件应用到实例时,该实例将获得类型安全性。

但如果您未将插件应用到另一个实例,它将无法推断类型。

typescript
import { 
Elysia
} from 'elysia'
const
child
= new
Elysia
()
// ❌ 'a' is missing .
get
('/', ({ a }) =>
a
)
Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
const
main
= new
Elysia
()
.
decorate
('a', 'a')
.
use
(
child
)

Elysia 引入 Service Locator 模式来对抗这种情况。

Elysia 将查找插件校验和并获取值或注册一个新值。从插件推断类型。

因此,我们必须提供插件引用,以便 Elysia 找到服务以添加类型安全性。

typescript
import { 
Elysia
} from 'elysia'
const
setup
= new
Elysia
({
name
: 'setup' })
.
decorate
('a', 'a')
// Without 'setup', type will be missing const
error
= new
Elysia
()
.
get
('/', ({ a }) =>
a
)
Property 'a' does not exist on type '{ body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends number | keyof StatusMap, const T = Code extends 100 | ... 59 more ... | 511 ? { ...; }[Code] : Code>(co...'.
// With `setup`, type will be inferred const
child
= new
Elysia
()
.
use
(
setup
)
.
get
('/', ({
a
}) =>
a
)
const
main
= new
Elysia
()
.
use
(
child
)
localhost

GET

防护

Guard 允许我们一次性将钩子和模式应用到多个路由。

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
guard
(
{
body
:
t
.
Object
({
username
:
t
.
String
(),
password
:
t
.
String
()
}) }, (
app
) =>
app
.
post
('/sign-up', ({
body
}) =>
signUp
(
body
))
.
post
('/sign-in', ({
body
}) =>
signIn
(
body
), {
beforeHandle
:
isUserExists
}) ) .
get
('/', 'hi')
.
listen
(3000)

这段代码将验证应用于 '/sign-in' 和 '/sign-up' 的 body,而不是逐一内联模式,但不应用于 '/'。

我们可以将路由验证总结如下:

路径是否有验证
/sign-up
/sign-in
/

Guard 接受与内联钩子相同的参数,唯一区别是你可以将钩子应用到作用域内的多个路由。

这意味着上面的代码被翻译为:

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
post
('/sign-up', ({
body
}) =>
signUp
(
body
), {
body
:
t
.
Object
({
username
:
t
.
String
(),
password
:
t
.
String
()
}) }) .
post
('/sign-in', ({
body
}) =>
body
, {
beforeHandle
:
isUserExists
,
body
:
t
.
Object
({
username
:
t
.
String
(),
password
:
t
.
String
()
}) }) .
get
('/', () => 'hi')
.
listen
(3000)

分组 Guard

我们可以通过向 group 提供 3 个参数来使用带有前缀的分组。

  1. 前缀 - 路由前缀
  2. Guard - 模式
  3. 作用域 - Elysia 应用回调

使用与 guard 相同的 API 应用于第二个参数,而不是将 group 和 guard 嵌套在一起。

考虑以下示例:

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
group
('/v1', (
app
) =>
app
.
guard
(
{
body
:
t
.
Literal
('Rikuhachima Aru')
}, (
app
) =>
app
.
post
('/student', ({
body
}) =>
body
)
) ) .
listen
(3000)

从嵌套的分组 guard 开始,我们可以通过将 guard 作用域提供给 group 的第二个参数来合并 group 和 guard:

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
group
(
'/v1', (
app
) =>
app
.
guard
(
{
body
:
t
.
Literal
('Rikuhachima Aru')
}, (
app
) =>
app
.
post
('/student', ({
body
}) =>
body
)
) ) .
listen
(3000)

结果语法如下:

typescript
import { 
Elysia
,
t
} from 'elysia'
new
Elysia
()
.
group
(
'/v1', {
body
:
t
.
Literal
('Rikuhachima Aru')
}, (
app
) =>
app
.
post
('/student', ({
body
}) =>
body
)
) .
listen
(3000)
localhost

POST

作用域转换 高级概念

要将钩子应用到父实例,可以使用以下之一:

  1. 内联 as 仅应用于单个钩子
  2. guard as 应用于 guard 中的所有钩子
  3. 实例 as 应用于实例中的所有钩子

内联

每个事件监听器都会接受 as 参数来指定钩子的作用域。

typescript
import { 
Elysia
} from 'elysia'
const
plugin
= new
Elysia
()
.
derive
({
as
: 'scoped' }, () => {
return {
hi
: 'ok' }
}) .
get
('/child', ({
hi
}) =>
hi
)
const
main
= new
Elysia
()
.
use
(
plugin
)
// ✅ Hi 现在可用 .
get
('/parent', ({
hi
}) =>
hi
)

但是,这种方法仅适用于单个钩子,对于多个钩子可能不合适。

Guard as

每个事件监听器都会接受 as 参数来指定钩子的作用域。

typescript
import { Elysia, t } from 'elysia'

const plugin = new Elysia()
	.guard({
		as: 'scoped', 
		response: t.String(),
		beforeHandle() {
			console.log('ok')
		}
	})
    .get('/child', 'ok')

const main = new Elysia()
    .use(plugin)
    .get('/parent', 'hello')

Guard 允许我们一次性将 schemahook 应用到多个路由,同时指定作用域。

但是,它不支持 deriveresolve 方法。

实例 as

as 将读取当前实例的所有钩子和模式作用域,并修改。

typescript
import { 
Elysia
} from 'elysia'
const
plugin
= new
Elysia
()
.
derive
(() => {
return {
hi
: 'ok' }
}) .
get
('/child', ({
hi
}) =>
hi
)
.
as
('scoped')
const
main
= new
Elysia
()
.
use
(
plugin
)
// ✅ Hi 现在可用 .
get
('/parent', ({
hi
}) =>
hi
)

有时我们想将插件重新应用到父实例,但由于 scoped 机制的限制,它仅限于 1 个父实例。

要应用到父实例,我们需要将作用域 提升 到父实例,as 是实现这一点的完美方法。

这意味着如果你有 local 作用域,并想将其应用到父实例,你可以使用 as('scoped') 来提升它。

typescript
import { 
Elysia
,
t
} from 'elysia'
const
plugin
= new
Elysia
()
.
guard
({
response
:
t
.
String
()
}) .
onBeforeHandle
(() => {
console
.
log
('called') })
.
get
('/ok', () => 'ok')
.
get
('/not-ok', () => 1)
Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'. Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'. Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'.
.
as
('scoped')
const
instance
= new
Elysia
()
.
use
(
plugin
)
.
get
('/no-ok-parent', () => 2)
Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {} & {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'. Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'. Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'.
.
as
('scoped')
const
parent
= new
Elysia
()
.
use
(
instance
)
// 现在错误,因为 `scoped` 被提升到父实例 .
get
('/ok', () => 3)
Argument of type '() => number' is not assignable to parameter of type 'InlineHandler<NoInfer<IntersectIfObjectSchema<{ body: unknown; headers: unknown; query: unknown; params: {}; cookie: unknown; response: { 200: string; }; }, {} & {}>>, { decorator: {}; store: {}; derive: {}; resolve: {}; } & { ...; }, {}>'. Type '() => number' is not assignable to type '(context: { body: unknown; query: Record<string, string>; params: {}; headers: Record<string, string | undefined>; cookie: Record<string, Cookie<unknown>>; server: Server | null; ... 6 more ...; status: <const Code extends 200 | "OK", T extends Code extends 200 ? { ...; }[Code] : Code extends "Continue" | ... 59 mor...'. Type 'number' is not assignable to type 'Response | MaybePromise<string | ElysiaCustomStatusResponse<200, string, 200>>'.

懒加载

模块默认是急切加载的。

Elysia 将确保所有模块在服务器启动前注册。

但是,一些模块可能计算密集或阻塞,导致服务器启动缓慢。

为了解决这个问题,Elysia 允许您提供一个异步插件,它不会阻塞服务器启动。

延迟模块

延迟模块是一个异步插件,可以在服务器启动后注册。

typescript
// plugin.ts
import { Elysia, file } from 'elysia'
import { loadAllFiles } from './files'

export const loadStatic = async (app: Elysia) => {
    const files = await loadAllFiles()

    files.forEach((asset) => app
        .get(asset, file(file))
    )

    return app
}

在主文件中:

typescript
import { Elysia } from 'elysia'
import { loadStatic } from './plugin'

const app = new Elysia()
    .use(loadStatic)

懒加载模块

与异步插件相同,懒加载模块将在服务器启动后注册。

懒加载模块可以是同步或异步函数,只要使用 import 导入模块,它就会被懒加载。

typescript
import { Elysia } from 'elysia'

const app = new Elysia()
    .use(import('./plugin'))

当模块计算密集和/或阻塞时,推荐使用模块懒加载。

要确保模块在服务器启动前注册,我们可以使用 await 于延迟模块。

测试

在测试环境中,我们可以使用 await app.modules 来等待延迟和懒加载模块。

typescript
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'

describe('Modules', () => {
    it('inline async', async () => {
        const app = new Elysia()
              .use(async (app) =>
                  app.get('/async', () => 'async')
              )

        await app.modules

        const res = await app
            .handle(new Request('http://localhost/async'))
            .then((r) => r.text())

        expect(res).toBe('async')
    })
})