localhost
GET
插件是一种将功能解耦成更小部分的模式。为我们的 Web 服务器创建可重用的组件。
要创建一个插件,就是创建一个单独的实例。
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 来使用插件。
GET
插件将继承插件实例的所有属性,比如 state
、decorate
,但不会继承插件生命周期,因为它默认是隔离的。
Elysia 也会自动处理类型推断。
每一个 Elysia 实例都可以成为一个插件。
我们将逻辑拆分成一个单独的 Elysia 实例,并在多个实例中重用它。
创建插件,只需在单独的文件中定义一个实例:
// plugin.ts
import { Elysia } from 'elysia'
export const plugin = new Elysia()
.get('/plugin', () => 'hi')
然后在主文件中导入该实例:
import { Elysia } from 'elysia'
import { plugin } from './plugin'
const app = new Elysia()
.use(plugin)
.listen(3000)
Elysia 的生命周期方法只对其自身实例封装。
这意味着如果你创建一个新的实例,它不会与其他实例共享生命周期方法。
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
。
GET
尝试在 URL 输入框中切换到 /rename 查看结果
Elysia 默认隔离生命周期,除非显式声明。这类似于JavaScript中的export,你需要导出函数才能让其在模块外部可用。
要将生命周期**“导出”**到其他实例,必须指定作用域。
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))
GET
将生命周期作用域设置为**"global"将把生命周期导出至所有实例**。
Elysia 有三种作用域级别:
作用域类型如下:
通过以下示例查看每种作用域类型的行为:
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
值的不同,结果应如下所示:
type | child | current | parent | main |
---|---|---|---|---|
local | ✅ | ✅ | ❌ | ❌ |
scoped | ✅ | ✅ | ✅ | ❌ |
global | ✅ | ✅ | ✅ | ✅ |
默认情况下,插件只会将钩子应用于自身及其子实例。
如果钩子注册在一个插件中,继承该插件的实例不会继承该钩子和模式。
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。
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')
GET
为了让插件更有用,建议通过配置支持自定义。
你可以创建一个接受参数的函数,这些参数能够影响插件行为,从而提升复用性。
import { Elysia } from 'elysia'
const version = (version = 1) => new Elysia()
.get('/version', version)
const app = new Elysia()
.use(version(1))
.listen(3000)
推荐定义一个新的插件实例,而非使用功能回调。
函数式回调允许我们访问主实例已有属性,例如检测某些路由或状态是否存在,但正确处理封装和作用域较困难。
函数式回调是指创建一个接受 Elysia 实例作为参数的函数。
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)
GET
一旦传递给 Elysia.use
,函数式回调的行为类似于普通插件,只不过其属性直接赋值给主实例。
TIP
你无需担心函数回调与新建实例间的性能差异。
Elysia 能在几毫秒内创建 1 万个实例,且新实例的类型推断性能优于函数回调。
默认情况下,Elysia 会注册任意插件并处理类型定义。
某些插件可能因需要多次使用以实现类型推断,导致初始化值或路由重复设置。
Elysia 通过使用 name 和 可选种子(seed) 来区分实例,避免重复注册:
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)
GET
Elysia 使用 name 和 seed 生成校验和用以判断实例是否已注册,若已注册则跳过插件注册。
若未提供种子,Elysia 只判断 name。这意味着即使多次注册插件,也只会注册一次。
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
种子可以是任何类型,从字符串到复杂对象或类。
若种子为类,Elysia 会尝试调用其 .toString
方法生成校验和。
当你将带状态或装饰器的插件应用于实例时,该实例将获得类型安全。
但若不将插件应用于另一个实例,则无法推断类型。
import { Elysia } from 'elysia'
const child = new Elysia()
// ❌ 缺少 'a'
.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 引入了 服务定位器 模式。
Elysia 会寻找插件的校验和以获取值,或者注册新的值,并根据插件推断类型。
因此,我们必须提供插件引用,以便 Elysia 找到服务,从而添加类型安全。
import { Elysia } from 'elysia'
const setup = new Elysia({ name: 'setup' })
.decorate('a', 'a')
// 没有使用 'setup',类型会缺失
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...'.
// 使用 `setup`,类型可以推断
const child = new Elysia()
.use(setup)
.get('/', ({ a }) => a)
const main = new Elysia()
.use(child)
GET
防护允许我们将钩子和模式一次性应用于多个路由。
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 | ✅ |
/ | ❌ |
防护接受与内联钩子相同的参数,唯一不同的是你可以在作用域内将钩子应用于多个路由。
这意味着上述代码可等同于:
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)
通过提供三个参数给 group 可以使用前缀:
防护应用的 API 和内联用法相同,只是作用于第二个参数,而不是将分组和防护嵌套。
示例:
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)
防护和分组合并写法:
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)
等价于如下语法:
import { Elysia, t } from 'elysia'
new Elysia()
.group(
'/v1',
{
body: t.Literal('Rikuhachima Aru')
},
(app) => app.post('/student', ({ body }) => body)
)
.listen(3000)
POST
若要将钩子应用于父实例,可以使用以下方式之一:
每个事件监听器都接受 as
参数,用以指定钩子的作用域。
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)
但此方法只适用于单个钩子,且可能不适合多个钩子。
每个事件监听器接受 as
参数,指定钩子作用域。
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')
防护允许我们一次性将 schema
和 hook
应用于多个路由,并且可以指定作用域。
但它不支持 derive
和 resolve
方法。
as
会读取当前实例所有钩子和模式的作用域,并对其进行修改。
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
机制仅限一个父级。
要将其应用到父实例,我们需要提升作用域到父实例,而 as
是实现此目的的绝佳方式。
这意味着如果你有一个 local
范围,想要应用到父实例,你可以用 as('scoped')
来提升它。
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 允许你提供异步插件,以免阻塞服务器启动。
延迟模块是一个异步插件,服务器启动后才注册。
// 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
}
在主文件中:
import { Elysia } from 'elysia'
import { loadStatic } from './plugin'
const app = new Elysia()
.use(loadStatic)
与异步插件类似,延迟加载模块会在服务器启动后注册。
延迟加载模块可以是同步或异步函数,只要通过 import
使用,模块就会延迟加载。
import { Elysia } from 'elysia'
const app = new Elysia()
.use(import('./plugin'))
对计算量大和/或阻塞的模块,推荐使用延迟加载。
若需确保模块在服务器启动前加载,请 await
延迟模块。
测试环境中,我们可以通过 await app.modules
等待延迟加载和懒加载模块完成。
import { describe, expect, it } from 'bun:test'
import { Elysia } from 'elysia'
describe('模块', () => {
it('内联异步', 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')
})
})