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 有 3 个作用域级别,如下所示:
作用域类型如下:
让我们通过以下示例回顾每种作用域类型的作用:
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
值,结果应如下所示:
类型 | 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 可以在几毫秒内创建 10k 个实例,新的 Elysia 实例甚至比函数回调具有更好的类型推断性能。
默认情况下,Elysia 将注册任何插件并处理类型定义。
某些插件可能被多次使用以提供类型推断,导致初始值或路由的重复。
Elysia 通过使用 name 和 可选种子 来区分实例,从而避免这种情况,以帮助 Elysia 识别实例重复:
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 将跳过插件的注册。
如果未提供 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
Seed 可以是任何东西,从字符串到复杂对象或类。
如果提供的值是类,Elysia 将尝试使用 .toString
方法生成校验和。
当您将带有 state/decorators 的插件应用到实例时,该实例将获得类型安全性。
但如果您未将插件应用到另一个实例,它将无法推断类型。
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 找到服务以添加类型安全性。
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)
GET
Guard 允许我们一次性将钩子和模式应用到多个路由。
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 接受与内联钩子相同的参数,唯一区别是你可以将钩子应用到作用域内的多个路由。
这意味着上面的代码被翻译为:
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 提供 3 个参数来使用带有前缀的分组。
使用与 guard 相同的 API 应用于第二个参数,而不是将 group 和 guard 嵌套在一起。
考虑以下示例:
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:
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')
Guard 允许我们一次性将 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
机制的限制,它仅限于 1 个父实例。
要应用到父实例,我们需要将作用域 提升 到父实例,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('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')
})
})