Skip to content
我们的赞助商
博客

Elysia 0.7 - Stellar Stellar

夜晚满天星辰的野生和山脉景观

以我们永不放弃的精神命名,我们喜爱的虚拟 YouTuber,Suicopath 星街彗星,以及她那璀璨的声音:「Stellar Stellar」来自她的首张专辑:「Still Still Stellar」

曾经被遗忘,她真的是在黑暗中真正闪耀的星星。

Stellar Stellar 为 Elysia 带来了许多激动人心的更新,帮助 Elysia 巩固基础,并轻松处理复杂性,包括:

  • 完全重写的类型系统,高达 13 倍更快的类型推断。
  • “Trace” 用于声明式遥测和更好的性能审计。
  • 响应式 Cookie 模型和 Cookie 验证,以简化 Cookie 处理。
  • TypeBox 0.31 以及自定义解码器支持。
  • 重写的 Web Socket 以提供更好的支持。
  • 定义重映射,以及声明式前后缀以防止名称冲突。
  • 基于文本的状态码

重写的类型系统

Elysia 关于开发者体验的核心功能。

类型是 Elysia 的最重要方面之一,因为它允许我们实现许多惊人的功能,如统一类型、同期业务逻辑、类型、文档和前端。

我们希望您在 Elysia 中获得出色的体验,专注于您的业务逻辑部分,让 Elysia 处理其余部分,无论是指南统一类型的类型推断,还是用于与后端同步类型的 Eden 连接器。

为了实现这一点,我们努力创建了一个统一类型系统来同步所有类型,但随着功能的增长,我们发现由于几年前 TypeScript 经验的不足,我们的类型推断可能不够快。

凭借我们在处理复杂类型系统、各种优化以及许多项目(如 Mobius)中所积累的经验。我们再次挑战自己加速类型系统,这是 Elysia 的第二次类型重写。

我们从头删除并重写每个 Elysia 类型,使 Elysia 类型快上数量级。

以下是 0.6 和 0.7 在简单 Elysia.get 代码上的比较:

Elysia 0.6Elysia 0.7

凭借我们新获得的经验,以及 TypeScript 的新特性如 const 泛型,我们能够简化大量代码,将类型代码库减少超过一千行。

这使我们能够进一步优化类型系统,使其更快、更稳定。

Elysia 0.6 和 0.7 在复杂项目中的比较,该项目有 300 个路由和 3,500 行类型声明

使用 Perfetto 和 TypeScript CLI 在大规模复杂应用上生成追踪,我们测量到高达 13 倍的推断速度。

如果您想知道我们是否会破坏 0.6 的类型推断,我们确实有类型级别的单元测试来确保大多数情况下没有破坏性变更。

我们希望这项改进能帮助您获得更快的类型推断,如更快的自动补全,以及 IDE 加载时间接近即时,从而使您的开发更快、更流畅。

Trace

性能是 Elysia 的另一个重要方面。

我们不希望只是为了基准测试而快,我们希望您在真实世界场景中拥有真正快速的服务器,而不仅仅是基准测试。

有许多因素可能减慢您的应用,并且很难识别其中一个,这就是为什么我们引入 "Trace"

Trace 允许我们深入生命周期事件并识别应用的性能瓶颈。

Trace 使用示例

这个示例代码允许我们深入所有 beforeHandle 事件,并逐一提取执行时间,然后设置 Server-Timing API 来检查性能瓶颈。

这不仅限于 beforeHandle,甚至 handler 本身也可以追踪。命名约定基于您已经熟悉的生命周期事件。

这个 API 允许我们轻松审计 Elysia 服务器的性能瓶颈,并与您选择的报告工具集成。

默认情况下,Trace 使用 AoT 编译和动态代码注入来有条件地报告,即使您实际使用它,也会自动应用,这意味着没有任何性能影响。

我们将 Cookie 插件合并到 Elysia 核心中。

与 Trace 类似,响应式 Cookie 使用 AoT 编译和动态代码注入来有条件地注入 Cookie 使用代码,如果您不使用它,则不会产生性能影响。

响应式 Cookie 采用更现代的方法,如 signal,来使用人体工程学 API 处理 Cookie。

响应式 Cookie 使用示例

没有 getCookiesetCookie,一切只是一个 Cookie 对象。

当您想使用 Cookie 时,您只需提取名称并获取/设置其值,例如:

typescript
app.get('/', ({ cookie: { name } }) => {
    // Get
    name.value

    // Set
    name.value = "New Value"
})

然后 Cookie 将自动将值与标头和 Cookie jar 同步,使 cookie 对象成为处理 Cookie 的单一事实来源。

Cookie Jar 是响应式的,这意味着如果您不设置 Cookie 的新值,则不会发送 Set-Cookie 标头,以保持相同的 Cookie 值并减少性能瓶颈。

随着 Cookie 合并到 Elysia 核心,我们引入了新的 Cookie Schema 来验证 Cookie 值。

这在您需要严格验证 Cookie 会话或希望处理 Cookie 时具有严格类型或类型推断时非常有用。

typescript
app.get('/', ({ cookie: { name } }) => {
    // Set
    name.value = {
        id: 617,
        name: 'Summoning 101'
    }
}, {
    cookie: t.Cookie({
        value: t.Object({
            id: t.Numeric(),
            name: t.String()
        })
    })
})

Elysia 会自动为您编码和解码 Cookie 值,因此如果您想在 Cookie 中存储 JSON,如解码的 JWT 值,或者只是想确保值是数字字符串,您可以轻松实现。

最后,随着 Cookie Schema 和 t.Cookie 类型的引入。我们能够创建统一类型来自动处理签名/验证 Cookie 签名。

Cookie 签名是附加到 Cookie 值上的加密哈希,使用密钥和 Cookie 内容生成,以通过添加签名增强 Cookie 的安全性。

这确保 Cookie 值未被恶意行为者修改,有助于验证 Cookie 数据的真实性和完整性。

在 Elysia 中处理 Cookie 签名只需提供 secretsign 属性即可:

typescript
new Elysia({
    cookie: {
        secret: 'Fischl von Luftschloss Narfidort'
    }
})
    .get('/', ({ cookie: { profile } }) => {
        profile.value = {
            id: 617,
            name: 'Summoning 101'
        }
    }, {
        cookie: t.Cookie({
            profile: t.Object({
                id: t.Numeric(),
                name: t.String()
            })
        }, {
            sign: ['profile']
        })
    })

通过提供 Cookie 密钥和 sign 属性来指示哪些 Cookie 应该有签名验证。

Elysia 然后自动签名和取消签名 Cookie 值,消除了手动 sign / unsign 函数的需求。

Elysia 自动处理 Cookie 的密钥轮换,因此如果您需要迁移到新的 Cookie 密钥,您只需追加密钥,Elysia 将使用第一个值签名新 Cookie,同时尝试使用其余密钥取消签名以匹配。

typescript
new Elysia({
    cookie: {
        secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort']
    }
})

响应式 Cookie API 是声明式的且直截了当,它提供的一些人体工程学特性有一些神奇之处,我们非常期待您尝试它。

TypeBox 0.31

随着 0.7 的发布,我们更新到 TypeBox 0.31,为 Elysia 带来更多功能。

这带来了激动人心的新功能,如原生支持 TypeBox 的 Decode

以前,像 Numeric 这样的自定义类型需要动态代码注入来将数字字符串转换为数字,但使用 TypeBox 的 decode,我们可以定义自定义函数来自动编码和解码类型的值。

这使我们能够简化类型为:

typescript
Numeric: (property?: NumericOptions<number>) =>
    Type.Transform(Type.Union([Type.String(), Type.Number(property)]))
        .Decode((value) => {
            const number = +value
            if (isNaN(number)) return value

            return number
        })
        .Encode((value) => value) as any as TNumber,

不再依赖广泛的检查和代码注入,而是通过 TypeBox 中的 Decode 函数简化。

我们已将所有需要动态代码注入的类型重写为使用 Transform,以便更容易维护代码。

不仅限于此,使用 t.Transform,您现在也可以定义自定义类型,使用自定义函数手动编码和解码,从而编写比以往更具表现力的代码。

我们迫不及待地想看到您将通过 t.Transform 的引入带来什么。

新类型

随着 Transform 的引入,我们添加了像 t.ObjectString 这样的新类型,以自动解码请求中的对象值。

这在您需要使用 multipart/formdata 处理文件上传但不支持对象时非常有用。您现在可以使用 t.ObjectString() 来告诉 Elysia 该字段是字符串化的 JSON,从而 Elysia 可以自动解码它。

typescript
new Elysia({
    cookie: {
        secret: 'Fischl von Luftschloss Narfidort'
    }
})
    .post('/', ({ body: { data: { name } } }) => name, {
        body: t.Object({
            image: t.File(),
            data: t.ObjectString({
                name: t.String()
            })
        })
    })

我们希望这将简化 multipart 中的 JSON 需求。

重写的 Web Socket

除了完全重写的类型系统,我们还完全重写了 Web Socket。

以前,我们发现 Web Socket 有 3 个主要问题:

  1. Schema 未严格验证
  2. 类型推断缓慢
  3. 每个插件都需要 .use(ws())

通过这次新更新,解决了上述所有问题,同时提高了 Web Socket 的性能。

  1. 现在,Elysia 的 Web Socket 严格验证,并且类型自动同步。
  2. 我们移除了每个插件中使用 WebSocket 的 .use(ws()) 需求。

我们为已经快速的 Web Socket 带来了性能改进。

以前,Elysia Web Socket 需要为每个传入请求处理路由,以统一数据和上下文,但在新模型中,Web Socket 现在可以为其路由推断数据,而不依赖路由器。

这将性能提升到接近 Bun 原生 Web Socket 的水平。

感谢 Bogeychan 提供了 Elysia Web Socket 的测试用例,帮助我们自信地重写 Web Socket。

定义重映射

Bogeychan#83 中提出

总结来说,Elysia 允许我们用任何我们想要的值装饰和存储请求,但是某些插件可能与我们拥有的值有重复名称,有时插件有名称冲突但我们无法重命名属性。

重映射

顾名思义,这允许我们将现有的 statedecoratemodelderive 重映射为我们喜欢的任何东西,以防止名称冲突,或者只是想重命名属性。

通过提供一个函数作为第一个参数,回调将接受当前值,允许我们将值重映射为我们喜欢的任何东西。

typescript
new Elysia()
    .state({
        a: "a",
        b: "b"
    })
    // 排除 b state
    .state(({ b, ...rest }) => rest)

这在处理具有重复名称的插件时非常有用,允许您重映射插件的名称:

typescript
new Elysia()
    .use(
        plugin
            .decorate(({ logger, ...rest }) => ({
                pluginLogger: logger,
                ...rest
            }))
    )

重映射函数可与 statedecoratemodelderive 一起使用,帮助您定义正确的属性名称并防止名称冲突。

前后缀

为了提供更流畅的体验,某些插件可能有很多属性值,一一重映射可能会很繁琐。

Affix 函数,由 prefixsuffix 组成,允许我们重映射实例的所有属性,防止插件的名称冲突。

typescript
const setup = new Elysia({ name: 'setup' })
    .decorate({
        argon: 'a',
        boron: 'b',
        carbon: 'c'
    })

const app = new Elysia()
    .use(
        setup
            .prefix('decorator', 'setup')
    )
    .get('/', ({ setupCarbon }) => setupCarbon)

允许我们轻松批量重映射插件的属性,防止插件的名称冲突。

默认情况下,affix 将自动处理运行时和类型级代码,将属性重映射为驼峰命名约定。

在某些情况下,您也可以重映射插件的 all 属性:

typescript
const app = new Elysia()
    .use(
        setup
            .prefix('all', 'setup')
    )
    .get('/', ({ setupCarbon }) => setupCarbon)

我们希望重映射和前后缀将为您提供强大的 API,以轻松处理多个复杂插件。

真正的封装作用域

随着 Elysia 0.7 的引入,Elysia 现在可以将作用域实例视为另一个实例,从而真正封装实例。

新作用域模型甚至可以防止像 onRequest 这样的事件在主实例上解析,这以前是不可能的。

typescript
const plugin = new Elysia({ scoped: true, prefix: '/hello' })
    .onRequest(() => {
        console.log('In Scoped')
    })
    .get('/', () => 'hello')

const app = new Elysia()
    .use(plugin)
    // 'In Scoped' 不会记录
    .get('/', () => 'Hello World')

此外,作用域现在在运行时和类型级别都真正被限定,这在没有之前提到的类型重写的情况下是不可能的。

从维护者的角度来看,这很令人兴奋,因为以前几乎不可能真正封装实例的作用域,但使用 mount 和 WinterCG 兼容性,我们终于能够真正封装插件实例,同时为主实例属性如 statedecorate 提供软链接。

基于文本的状态码

有超过 64 个标准 HTTP 状态码需要记住,我承认有时我们也会忘记想使用的状态码。

这就是为什么我们以基于文本的形式提供 64 个 HTTP 状态码,并带有自动补全。

使用基于文本状态码的示例

文本将自动解析为状态码,如预期的那样。

当您输入时,IDE 应该自动弹出文本自动补全,无论它是 NeoVim 还是 VSCode,因为这是内置的 TypeScript 功能。

基于文本状态码显示自动补全

这是一个小型人体工程学功能,帮助您开发服务器而无需在 IDE 和 MDN 之间切换来搜索正确的状态码。

显著改进

改进:

  • onRequest 现在可以是异步的
  • Context 添加到 onError
  • 生命周期钩子现在接受数组函数
  • 静态代码分析现在支持剩余参数
  • 将动态路由分解为单个管道而不是内联到静态路由,以减少内存使用
  • t.Filet.Files 设置为 File 而不是 Blob
  • 跳过类实例合并
  • 处理 UnknownContextPassToFunction
  • #157 WebSocket - 添加单元测试并修复示例和 API,由 @bogeychan
  • #179 添加 GitHub Action 来运行 bun test,由 @arthurfiorette

破坏性变更:

  • 移除 ws 插件,迁移到核心
  • addError 重命名为 error

变更:

  • 使用单个 findDynamicRoute 而不是内联到静态映射
  • 移除 mergician
  • 由于 TypeScript 的问题移除数组路由
  • 重写 Type.ElysiaMeta 以使用 TypeBox.Transform

Bug 修复:

  • 默认严格验证响应
  • t.Numeric 在 headers / query / params 上不起作用
  • t.Optional(t.Object({ [name]: t.Numeric })) 导致错误
  • 在转换 Numeric 之前添加 null 检查
  • 将 store 继承到实例插件
  • 处理类重叠
  • #187 InternalServerError 消息修复为 "INTERNAL_SERVER_ERROR" 而不是 "NOT_FOUND",由 @bogeychan
  • #167 mapEarlyResponse 与 after handle 上的 aot

结语

自最新发布以来,我们在 GitHub 上获得了超过 2,000 个星标!

回顾过去,我们的进步超出了我们当时的想象。

推动 TypeScript 和开发者体验的边界,甚至达到了我们感觉真正深刻的地步。

每一次发布,我们都逐渐接近我们很久以前描绘的未来。

一个我们可以自由创建任何我们想要的东西,并拥有惊人开发者体验的未来。

我们由衷感谢 TypeScript 和 Bun 的可爱社区对我们的喜爱。

看到 Elysia 被像以下这样的优秀开发者带入生活,真是令人兴奋:

以及更多选择 Elysia 作为下一个项目的开发者。

我们的目标很简单,带来一个永恒的天堂,在那里您可以追求您的梦想,每个人都能幸福生活。

感谢您对 Elysia 的爱和压倒性的支持,我们希望有一天我们能将未来描绘成追求梦想的现实。

愿所有美好都被祝福

伸出那只手,仿佛要触及某人

我和你一样,没什么特别的

没错,我将唱出夜晚的歌谣

Stellar Stellar

在世界的中央,宇宙之中

音乐今晚永不、永不停止

没错,我一直渴望成为

不是等待的灰姑娘

而是为她而来的王子

因为我是一颗星星,这就是原因

Stellar Stellar

Elysia:人性化框架