Elysia
2,454,631 reqs/sExpress
113,117
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
本指南适用于希望了解 Express 与 Elysia 之间差异的 Express 用户,包括语法,以及如何通过示例将应用程序从 Express 迁移到 Elysia。
Express 是一个流行的 Node.js 网络框架,广泛用于构建 Web 应用程序和 API。因其简单性和灵活性而闻名。
Elysia 是一个人性化的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计时强调人体工学和开发者友好,专注于 健全的类型安全 和性能。
由于本机 Bun 实现和静态代码分析,Elysia 在性能上相比 Express 有显著提高。
113,117
Measured in requests/second. Result from TechEmpower Benchmark Round 22 (2023-10-17) in PlainText
Express 和 Elysia 有类似的路由语法,使用 app.get()
和 app.post()
方法来定义路由,并且有类似的路径参数语法。
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
app.post('/id/:id', (req, res) => {
res.status(201).send(req.params.id)
})
app.listen(3000)
Express 使用
req
和res
作为请求和响应对象
import { Elysia } from 'elysia'
const app = new Elysia()
.get('/', 'Hello World')
.post(
'/id/:id',
({ status, params: { id } }) => {
return status(201, id)
}
)
.listen(3000)
Elysia 使用单一的
context
直接返回响应
在风格指南上存在一些细微的差异,Elysia 推荐使用方法链和对象解构。
如果您不需要使用上下文,Elysia 还支持内联值作为响应。
两者在访问输入参数(如 headers
、query
、params
和 body
)时有类似的属性。
import express from 'express'
const app = express()
app.use(express.json())
app.post('/user', (req, res) => {
const limit = req.query.limit
const name = req.body.name
const auth = req.headers.authorization
res.json({ limit, name, auth })
})
Express 需要
express.json()
中间件来解析 JSON 主体
import { Elysia } from 'elysia'
const app = new Elysia()
.post('/user', (ctx) => {
const limit = ctx.query.limit
const name = ctx.body.name
const auth = ctx.headers.authorization
return { limit, name, auth }
})
Elysia 默认解析 JSON、URL 编码数据和表单数据
Express 使用专门的 express.Router()
声明子路由,而 Elysia 将每个实例视为可以插件式组合的组件。
import express from 'express'
const subRouter = express.Router()
subRouter.get('/user', (req, res) => {
res.send('Hello User')
})
const app = express()
app.use('/api', subRouter)
Express 使用
express.Router()
创建子路由
import { Elysia } from 'elysia'
const subRouter = new Elysia({ prefix: '/api' })
.get('/user', 'Hello User')
const app = new Elysia()
.use(subRouter)
Elysia 将每个实例视为一个组件
Elysia 内置支持请求验证,具有健全的类型安全,而 Express 不提供内置验证,需要根据每个验证库手动声明类型。
import express from 'express'
import { z } from 'zod'
const app = express()
app.use(express.json())
const paramSchema = z.object({
id: z.coerce.number()
})
const bodySchema = z.object({
name: z.string()
})
app.patch('/user/:id', (req, res) => {
const params = paramSchema.safeParse(req.params)
if (!params.success)
return res.status(422).json(result.error)
const body = bodySchema.safeParse(req.body)
if (!body.success)
return res.status(422).json(result.error)
res.json({
params: params.id.data,
body: body.data
})
})
Express 需要外部验证库如
zod
或joi
来验证请求主体
import { Elysia, t } from 'elysia'
const app = new Elysia()
.patch('/user/:id', ({ params, body }) => ({
params,
body
}),
{
params: t.Object({
id: t.Number()
}),
body: t.Object({
name: t.String()
})
})
Elysia 使用 TypeBox 进行验证,并自动强制转换类型
Express 使用外部库 multer
处理文件上传,而 Elysia 内置支持文件和表单数据,并使用声明式 API 进行 MIME 类型验证。
import express from 'express'
import multer from 'multer'
import { fileTypeFromFile } from 'file-type'
import path from 'path'
const app = express()
const upload = multer({ dest: 'uploads/' })
app.post('/upload', upload.single('image'), async (req, res) => {
const file = req.file
if (!file)
return res
.status(422)
.send('没有上传文件')
const type = await fileTypeFromFile(file.path)
if (!type || !type.mime.startsWith('image/'))
return res
.status(422)
.send('文件不是有效的图像')
const filePath = path.resolve(file.path)
res.sendFile(filePath)
})
Express 需要
express.json()
中间件来解析 JSON 主体
import { Elysia, t } from 'elysia'
const app = new Elysia()
.post('/upload', ({ body }) => body.file, {
body: t.Object({
file: t.File({
type: 'image'
})
})
})
Elysia 用声明式方法处理文件和 MIME 类型验证
由于 multer 不验证 MIME 类型,您需要使用 file-type 或类似库手动验证 MIME 类型。
Elysia 验证文件上传,并使用 file-type 自动验证 MIME 类型。
Express 中间件使用单一的基于队列的顺序,而 Elysia 提供使用 基于事件 的生命周期进行更精细的控制。
Elysia 的生命周期事件可以如下所示。
点击图片放大
尽管 Express 对请求管道有单一流的处理顺序,Elysia 可以拦截请求管道中的每个事件。
import express from 'express'
const app = express()
// 全局中间件
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`)
next()
})
app.get(
'/protected',
// 路由特定中间件
(req, res, next) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('未授权')
next()
},
(req, res) => {
res.send('受保护的路由')
}
)
Express 使用单个基于队列的顺序来处理中间件,按顺序执行
import { Elysia } from 'elysia'
const app = new Elysia()
// 全局中间件
.onRequest('/user', ({ method, path }) => {
console.log(`${method} ${path}`)
})
// 路由特定中间件
.get('/protected', () => 'protected', {
beforeHandle({ status, headers }) {
if (!headers.authorizaton)
return status(401)
}
})
Elysia 为请求管道中的每个点使用特定的事件拦截器
虽然 Hono 具有调用下一个中间件的 next
函数,但 Elysia 并没有。
Elysia 被设计为具有健全的类型安全。
例如,您可以使用 derive 和 resolve 以 类型安全 的方式自定义上下文,而 Express 不支持这种方式。
import express from 'express'
import type { Request, Response } from 'express'
const app = express()
const getVersion = (req: Request, res: Response, next: Function) => {
// @ts-ignore
req.version = 2
next()
}
app.get('/version', getVersion, (req, res) => {
res.send(req.version)Property 'version' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
const authenticate = (req: Request, res: Response, next: Function) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('未授权')
// @ts-ignore
req.token = token.split(' ')[1]
next()
}
app.get('/token', getVersion, authenticate, (req, res) => {
req.versionProperty 'version' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.
res.send(req.token)Property 'token' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
Express 使用单个基于队列的顺序来处理中间件,按顺序执行
import { Elysia } from 'elysia'
const app = new Elysia()
.decorate('version', 2)
.get('/version', ({ version }) => version)
.resolve(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
return {
token: authorization.split(' ')[1]
}
})
.get('/token', ({ token, version }) => {
version
return token
})
Elysia 为请求管道中的每个点使用特定的事件拦截器
虽然 Express 可以使用 declare module
扩展 Request
接口,但它是全局可用的,并且没有健全的类型安全,也不保证该属性在所有请求处理程序中都是可用的。
declare module 'express' {
interface Request {
version: number
token: string
}
}
这对于使上面的 Express 示例正常工作是必需的,但并不提供健全的类型安全
Express 使用函数返回插件来定义可重用的路由特定中间件,而 Elysia 使用 macro 定义自定义钩子。
import express from 'express'
import type { Request, Response } from 'express'
const app = express()
const role = (role: 'user' | 'admin') =>
(req: Request, res: Response, next: Function) => {
const user = findUser(req.headers.authorization)
if (user.role !== role)
return res.status(401).send('未授权')
// @ts-ignore
req.user = user
next()
}
app.get('/token', role('admin'), (req, res) => {
res.send(req.user)Property 'user' does not exist on type 'Request<ParamsDictionary, any, any, ParsedQs, Record<string, any>>'.})
Express 使用函数回调接受中间件的自定义参数
import { Elysia } from 'elysia'
const app = new Elysia()
.macro({
role: (role: 'user' | 'admin') => ({
resolve({ status, headers: { authorization } }) {
const user = findUser(authorization)
if(user.role !== role)
return status(401)
return {
user
}
}
})
})
.get('/token', ({ user }) => user, {
role: 'admin'
})
Elysia 使用宏将自定义参数传递给自定义中间件
Express 使用单一错误处理程序处理所有路由,而 Elysia 提供了更精细的错误处理控制。
import express from 'express'
const app = express()
class CustomError extends Error {
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
}
// 全局错误处理程序
app.use((error, req, res, next) => {
if(error instanceof CustomError) {
res.status(500).json({
message: '发生了错误!',
error
})
}
})
// 路由特定错误处理程序
app.get('/error', (req, res) => {
throw new CustomError('哦,啊')
})
Express 使用中间件处理错误,所有路由共享一个错误处理程序
import { Elysia } from 'elysia'
class CustomError extends Error {
// 可选:自定义 HTTP 状态码
status = 500
constructor(message: string) {
super(message)
this.name = 'CustomError'
}
// 可选:发送到客户端的内容
toResponse() {
return {
message: "如果您看到这个,我们的开发人员忘记处理此错误",
error: this
}
}
}
const app = new Elysia()
// 可选:注册自定义错误类
.error({
CUSTOM: CustomError,
})
// 全局错误处理程序
.onError(({ error, code }) => {
if(code === 'CUSTOM')
return {
message: '发生了错误!',
error
}
})
.get('/error', () => {
throw new CustomError('哦,啊')
}, {
// 可选:路由特定错误处理程序
error({ error }) {
return {
message: '仅适用于此路由!',
error
}
}
})
Elysia 提供更精细的错误处理控制和作用域机制
虽然 Express 使用中间件提供错误处理,但 Elysia 提供:
toResponse
用于将错误映射到响应错误代码对于日志记录和调试非常有用,并且在区分扩展相同类的不同错误类型时至关重要。
Express 中间件是全局注册的,而 Elysia 通过显式作用域机制和代码顺序控制插件的副作用。
import express from 'express'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
const subRouter = express.Router()
subRouter.use((req, res, next) => {
const token = req.headers.authorization
if (!token)
return res.status(401).send('未授权')
next()
})
app.use(subRouter)
// 从子路由产生副作用
app.get('/side-effect', (req, res) => {
res.send('嗨')
})
Express 不会处理中间件的副作用,需要添加前缀以分开副作用
import { Elysia } from 'elysia'
const subRouter = new Elysia()
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
// 不会有来自子路由的副作用
.get('/side-effect', () => '嗨')
Elysia 将副作用封装到插件中
默认情况下,Elysia 将生命事件和上下文封装到所使用的实例中,因此插件的副作用不会影响父实例,除非明确说明。
import { Elysia } from 'elysia'
const subRouter = new Elysia()
.onBeforeHandle(({ status, headers: { authorization } }) => {
if(!authorization?.startsWith('Bearer '))
return status(401)
})
// 作用域限定于父实例,但不再超过此范围
.as('scoped')
const app = new Elysia()
.get('/', 'Hello World')
.use(subRouter)
// 现在有来自子路由的副作用
.get('/side-effect', () => '嗨')
Elysia 提供三种类型的作用域机制:
虽然 Express 可以通过添加前缀来限制中间件副作用,但这并不是真正的封装。副作用仍然存在,但被分隔到以所述前缀开头的任何路由,使得开发人员需要记住哪个前缀具有副作用。
这使得您可以执行以下操作:
这可能导致调试时出现噩梦场景,因为 Express 不提供真正的封装。
Express 使用外部库 cookie-parser
解析 cookies,而 Elysia 内置支持 cookie,并使用基于信号的方法处理 cookies。
import express from 'express'
import cookieParser from 'cookie-parser'
const app = express()
app.use(cookieParser('secret'))
app.get('/', function (req, res) {
req.cookies.name
req.signedCookies.name
res.cookie('name', 'value', {
signed: true,
maxAge: 1000 * 60 * 60 * 24
})
})
Express 使用
cookie-parser
解析 Cookies
import { Elysia } from 'elysia'
const app = new Elysia({
cookie: {
secret: 'secret'
}
})
.get('/', ({ cookie: { name } }) => {
// 签名验证自动处理
name.value
// 整个 cookie 签名自动处理
name.value = 'value'
name.maxAge = 1000 * 60 * 60 * 24
})
Elysia 使用基于信号的方法处理 Cookies
Express 需要单独的配置来支持 OpenAPI、验证和类型安全,而 Elysia 使用架构作为 单一真实来源 内置支持 OpenAPI。
import express from 'express'
import swaggerUi from 'swagger-ui-express'
const app = express()
app.use(express.json())
app.post('/users', (req, res) => {
// TODO: 验证请求主体
res.status(201).json(req.body)
})
const swaggerSpec = {
openapi: '3.0.0',
info: {
title: '我的 API',
version: '1.0.0'
},
paths: {
'/users': {
post: {
summary: '创建用户',
requestBody: {
content: {
'application/json': {
schema: {
type: 'object',
properties: {
name: {
type: 'string',
description: '仅为名'
},
age: { type: 'integer' }
},
required: ['name', 'age']
}
}
}
},
responses: {
'201': {
description: '用户已创建'
}
}
}
}
}
}
app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec))
Express 需要单独的配置来支持 OpenAPI、验证和类型安全
import { Elysia, t } from 'elysia'
import { swagger } from '@elysiajs/swagger'
const app = new Elysia()
.use(swagger())
.model({
user: t.Object({
name: t.String(),
age: t.Number()
})
})
.post('/users', ({ body }) => body, {
body: 'user[]',
response: {
201: 'user[]'
},
detail: {
summary: '创建用户'
}
})
Elysia 将架构用作单一真实来源
Elysia 会根据您提供的架构生成 OpenAPI 规范,并根据该架构验证请求和响应,并自动推断类型。
Elysia 还将在 model
中注册的架构附加到 OpenAPI 规范,允许您在 Swagger 或 Scalar UI 的专用部分中引用该模型。
Express 使用单个 supertest
库测试应用程序,而 Elysia 构建在 Web 标准 API 之上,允许与任何测试库一起使用。
import express from 'express'
import request from 'supertest'
import { describe, it, expect } from 'vitest'
const app = express()
app.get('/', (req, res) => {
res.send('Hello World')
})
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const res = await request(app).get('/')
expect(res.status).toBe(200)
expect(res.text).toBe('Hello World')
})
})
Express 使用
supertest
库测试应用程序
import { Elysia } from 'elysia'
import { describe, it, expect } from 'vitest'
const app = new Elysia()
.get('/', 'Hello World')
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const res = await app.handle(
new Request('http://localhost')
)
expect(res.status).toBe(200)
expect(await res.text()).toBe('Hello World')
})
})
Elysia 使用 Web 标准 API 处理请求和响应
此外,Elysia 还提供了一个称为 Eden 的助手库,用于端到端的类型安全,允许我们进行自动补全和完全类型安全的测试。
import { Elysia } from 'elysia'
import { treaty } from '@elysiajs/eden'
import { describe, expect, it } from 'bun:test'
const app = new Elysia().get('/hello', 'Hello World')
const api = treaty(app)
describe('GET /', () => {
it('应该返回 Hello World', async () => {
const { data, error, status } = await api.hello.get()
expect(status).toBe(200)
expect(data).toBe('Hello World')
})
})
Elysia 内置支持 端到端类型安全,无需代码生成,Express 不提供此功能。
import { Elysia, t } from 'elysia'
import { treaty } from '@elysiajs/eden'
const app = new Elysia()
.post('/mirror', ({ body }) => body, {
body: t.Object({
message: t.String()
})
})
const api = treaty(app)
const { data, error } = await api.mirror.post({
message: 'Hello World'
})
if(error)
throw error
console.log(data)
如果端到端类型安全对您来说很重要,那么 Elysia 是正确的选择。
Elysia 提供了更人性化和开发者友好的体验,专注于性能、类型安全和简易性,而 Express 是一个流行的 Node.js 网络框架,但在性能和简易性方面存在一些局限性。
如果您在寻找一个易于使用、具有出色开发者体验且基于 Web 标准 API 的框架,Elysia 是您正确的选择。
另外,如果您来自其他框架,可以查看: