Elysia 0.6 - 这场游戏

以传奇动漫《No Game No Life》的开场为名,由 Konomi Suzuki 作曲的「这场游戏」。
这场游戏通过重新设计的插件模型、动态模式、声明式自定义错误来推动开发者体验、通过 'onResponse' 收集更多指标、可自定义的宽松和严格路径映射、TypeBox 0.30 以及 WinterCG 框架互操作,将中等规模项目到大规模应用的边界推向极限。
(我们仍在等待 No Game No Life 第二季)
新插件模型
这场游戏引入了插件注册的新语法,并在内部提出了新的插件模型。
以前,你可以通过为 Elysia 实例定义回调函数来注册插件,比如这样:
const plugin = (app: Elysia) => app.get('/', () => 'hello')
使用新插件,你现在可以将 Elysia 实例转换为插件:
const plugin = new Elysia()
.get('/', () => 'hello')
这允许任何 Elysia 实例,甚至现有的实例在整个应用中使用,消除了任何可能的额外回调和制表符间距。
这在处理嵌套组时显著改善了开发者体验
// < 0.6
const group = (app: Elysia) => app
.group('/v1', (app) => app
.get('/hello', () => 'Hello World')
)
// >= 0.6
const group = new Elysia({ prefix: '/v1' })
.get('/hello', () => 'Hello World')
我们鼓励你使用新的 Elysia 插件实例模型,因为我们可以利用插件校验和以及未来可能的新功能。
然而,我们不会弃用回调函数方法,因为在某些情况下函数模型很有用,比如:
- 内联函数
- 需要主实例信息的插件(例如访问 OpenAPI 模式)
借助这个新插件模型,我们希望你能让你的代码库更容易维护。
插件校验和
默认情况下,Elysia 插件使用函数回调来注册插件。
这意味着,如果你为类型声明注册插件,它会为了提供类型支持而重复自身,导致生产环境中插件重复使用。
这就是为什么引入插件校验和,以去重为类型声明注册的插件。
要启用插件校验和,你需要使用新插件模型,并提供 name
属性来告诉 Elysia 防止插件被去重
const plugin = new Elysia({
name: 'plugin'
})
这允许 Elysia 根据名称识别插件并进行去重。
任何重复的名称只会注册一次,但即使插件被去重,注册后也会提供类型安全。
如果你的插件需要配置,你可以将配置提供到 seed 属性中,以生成校验和用于去重插件。
const plugin = (config) = new Elysia({
name: 'plugin',
seed: config
})
名称和种子将用于生成校验和以去重注册,从而带来更好的性能提升。
此更新还修复了插件生命周期的去重,当 Elysia 不确定插件是本地事件还是全局事件时,意外内联生命周期的问题。
一如既往,对于比“Hello World”更大的应用,这意味着性能提升。
Mount 和 WinterCG 合规性
WinterCG 是由 Cloudflare、Deno、Vercel Edge Runtime、Netlify Function 等支持的 web 互操作运行时标准。
WinterCG 是一个标准,允许 web 服务器在运行时之间互操作,使用 Web 标准定义如 Fetch、Request 和 Response。
因此,Elysia 部分遵循 WinterCG 合规性,因为我们针对 Bun 进行了优化,但也公开支持其他运行时(如果可能)。
这允许任何符合 WinterCG 的框架和代码在理论上一起运行,Hono 通过引入 .mount 方法证明了这一点,该方法允许在单一代码库中运行多个框架,包括 Remix、Elysia、Itty Router 和 Hono 本身,在一个简单的函数中。
因此,我们为 Elysia 实现了相同的逻辑,通过引入 .mount
方法来运行任何符合 WinterCG 的框架或代码。
要使用 .mount
,只需传递一个 fetch
函数:
const app = new Elysia()
.get('/', () => 'Hello from Elysia')
.mount('/hono', hono.fetch)
fetch 函数是一个接受 Web 标准 Request 并返回 Web 标准 Response 的函数,其定义为:
// Web 标准 Request-like 对象
// Web 标准 Response
type fetch = (request: RequestLike) => Response
默认情况下,此声明由以下使用:
- Bun
- Deno
- Vercel Edge Runtime
- Cloudflare Worker
- Netlify Edge Function
- Remix Function Handler
这意味着你可以将上述所有代码与 Elysia 互操作运行在单一服务器上,或者在单一部署中重用现有的函数,无需设置反向代理来处理多个服务器。
如果框架还支持 .mount 函数,你可以无限深度嵌套支持它的框架。
const elysia = new Elysia()
.get('/Hello from Elysia inside Hono inside Elysia')
const hono = new Hono()
.get('/', (c) => c.text('Hello from Hono!'))
.mount('/elysia', elysia.fetch)
const main = new Elysia()
.get('/', () => 'Hello from Elysia')
.mount('/hono', hono.fetch)
.listen(3000)
你甚至可以在服务器中重用多个现有的 Elysia 项目。
import A from 'project-a/elysia'
import B from 'project-b/elysia'
import C from 'project-c/elysia'
new Elysia()
.mount(A)
.mount(B)
.mount(C)
如果传递给 mount 的实例是 Elysia 实例,它将自动解析为 use
,默认提供类型安全和 Eden 支持。
这使互操作框架和运行时的可能性成为现实。
改进的启动时间
启动时间是无服务器环境中一个重要的指标,Elysia 在此方面表现出色,但我们已将其推向更远。
默认情况下,Elysia 为每个路由自动生成 OpenAPI 模式,并在未使用时将其存储在内部。
在本版本中,Elysia 推迟了编译,并移至 @elysiajs/swagger
,从而使 Elysia 的启动时间更快。
借助各种微优化和新插件模型的可能,启动时间现在快达 35%。
动态模式
Elysia 引入了静态代码分析和提前编译来推动性能边界。
静态代码分析允许 Elysia 阅读你的代码,然后生成最优化的代码版本,从而将性能推向极限。
即使 Elysia 符合 WinterCG,像 Cloudflare Worker 这样的环境不支持函数组合。
这意味着提前编译不可能,因此我们创建了动态模式,使用 JIT 编译而不是 AoT,从而使 Elysia 也能在 Cloudflare Worker 中运行。
要启用动态模式,将 aot
设置为 false。
new Elysia({
aot: false
})
动态模式在 Cloudflare Worker 中默认启用。
值得注意的是,启用动态模式将禁用一些功能,如动态注入代码,例如 t.Numeric
,它会自动将字符串解析为数字。
提前编译可以读取、检测和优化你的代码,以启动时间换取额外性能提升,而动态模式使用 JIT 编译,使启动时间更快达 6 倍。
但需要注意的是,Elysia 的默认启动时间已经足够快。
Elysia 能够在仅 78ms 内注册 10,000 个路由,平均 0.0079 ms/路由。
话虽如此,我们留给你自己决定。
声明式自定义错误
此更新添加了对处理自定义错误类型支持的支持。
class CustomError extends Error {
constructor(public message: string) {
super(message)
}
}
new Elysia()
.addError({
MyError: CustomError
})
.onError(({ code, error }) => {
switch(code) {
// 带有自动补全
case 'MyError':
// 带有类型缩小
// Error 被类型化为 CustomError
return error
}
})
这允许我们使用类型缩小处理自定义错误,并为错误代码提供自动补全,从而缩小正确类型,完全声明式类型安全。
这实现了我们主要理念之一,即专注于开发者体验,尤其是类型。
Elysia 类型系统很复杂,但我们尽量避免用户需要编写自定义类型或传递自定义泛型,使所有代码看起来就像 JavaScript。
它就是这样工作,所有代码看起来就像 JavaScript。
TypeBox 0.30
TypeBox 是驱动 Elysia 严格类型系统 Elysia.t 的核心库。
在此更新中,我们将 TypeBox 从 0.28 更新到 0.30,使更精细的类型系统几乎成为严格类型语言。
这些更新引入了新功能和许多有趣的变化,例如 Iterator 类型、减少包大小、TypeScript 代码生成。
并支持实用类型,如:
t.Awaited
t.Uppercase
t.Capitlized
严格路径
我们收到了许多处理宽松路径的请求。
默认情况下,Elysia 严格处理路径,这意味着如果你需要支持带或不带可选 /
的路径,它不会被解析,你必须重复路径名两次。
new Elysia()
.group('/v1', (app) => app
// 处理 /v1
.get('', handle)
// 处理 /v1/
.get('/', handle)
)
因此,许多人要求 /v1/
也应该解析 /v1
。
借助此更新,我们默认添加了对宽松路径匹配的支持,自动启用此功能。
new Elysia()
.group('/v1', (app) => app
// 处理 /v1 和 /v1/
.get('/', handle)
)
要禁用 loosePath 映射,你可以将 strictPath
设置为 true 以使用之前的行為:
new Elysia({
strictPath: false
})
我们希望这将澄清有关路径匹配及其预期行为的任何疑问
onResponse
此更新引入了一个名为 onResponse
的新生命周期钩子。
这是由 elysia#67 提出的提案
以前 Elysia 生命周期的工作如图所示。
对于任何指标、数据收集或日志目的,你可以使用 onAfterHandle
来运行函数收集指标,但是,当处理程序遇到错误时(无论是路由错误还是提供的自定义错误),此生命周期不会执行。
这就是为什么我们引入 onResponse
来处理所有 Response 情况。
你可以使用 onRequest
和 onResponse
一起来测量性能指标或任何所需的日志。
引用
然而,onAfterHandle 函数仅在成功响应时触发。例如,如果路由未找到、body 无效或抛出错误,它不会触发。如何监听成功和非成功请求?这也是我建议 onResponse 的原因。
根据图,我建议如下:
值得注意的改进:
- 在 Elysia 类型系统中添加了用于添加自定义错误消息的 error 字段
- 支持带有动态模式(和 ENV)的 Cloudflare Worker
- AfterHandle 现在自动映射值
- 使用 bun build 针对 Bun 环境,提高整体性能 5-10%
- 使用插件注册时去重内联生命周期
- 支持设置
prefix
- 递归路径类型
- 略微提高了类型检查速度
- 递归模式碰撞导致无限类型
变更:
- 将 registerSchemaPath 移至 @elysiajs/swagger
- [内部] 在上下文中添加 qi (queryIndex)
破坏性变更:
- [内部] 移除 Elysia Symbol
- [内部] 重构
getSchemaValidator
、getResponseSchemaValidator
为命名参数 - [内部] 将
registerSchemaPath
移至@elysiajs/swagger
后记
我们刚刚度过一周年里程碑,对 Elysia 和 Bun 在这一年中的改进感到非常兴奋!
使用 Bun 推动 JavaScript 的性能边界,以及使用 Elysia 推动开发者体验,我们很高兴与你和我们的社区保持联系。
每个更新都使 Elysia 更加稳定,并逐步提供更好的开发者体验,而不会降低性能和功能。
我们很高兴看到我们的开源开发者社区通过他们的项目将 Elysia 带入生活,比如。
- Elysia Vite Plugin SSR 允许我们使用 Elysia 作为服务器进行 Vite 服务器端渲染。
- Elysia Connect 使 Connect 的插件与 Elysia 兼容
以及更多选择 Elysia 作为下一个大项目的开发者。
凭借我们的承诺,我们最近还引入了 Mobius,这是一个开源 TypeScript 库,用于将 GraphQL 解析为 TypeScript 类型,而无需依赖代码生成,完全使用 TypeScript 模板字面量类型,是第一个实现端到端类型安全而无需依赖代码生成的框架。
我们非常感谢你对 Elysia 的压倒性持续支持,并希望在下一个发布中与你一起推动边界。
当这个全新的世界为我的名字欢呼
我绝不会把它留给命运
当我看到机会,我会铺平道路
我叫将死
这是突破的时候
所以我会重写故事并最终改变所有规则
我们是独行侠
我们不会屈服,直到赢得这场游戏
虽然我不知道明天会发生什么
我会下注并打出我的牌来赢得这场游戏
与其他人不同,我会尽我所能,我绝不会输
放弃这个机会将是致命的罪过,所以让我们全力以赴
我把所有的命运押上,让 游戏开始