--- url: 'https://elysiajs.com/plugins/graphql-apollo.md' --- # GraphQL Apollo 插件 {#graphql-apollo-plugin} ¥GraphQL Apollo Plugin 用于使用 GraphQL Apollo 的 [elysia](https://github.com/elysiajs/elysia) 插件。 ¥Plugin for [elysia](https://github.com/elysiajs/elysia) for using GraphQL Apollo. 使用以下工具安装: ¥Install with: ```bash bun add graphql @elysiajs/apollo @apollo/server ``` 然后使用它: ¥Then use it: ```typescript import { Elysia } from 'elysia' import { apollo, gql } from '@elysiajs/apollo' const app = new Elysia() .use( apollo({ typeDefs: gql` type Book { title: String author: String } type Query { books: [Book] } `, resolvers: { Query: { books: () => { return [ { title: 'Elysia', author: 'saltyAom' } ] } } } }) ) .listen(3000) ``` 访问 `/graphql` 应该会显示 Apollo GraphQL Playground 的运行情况。 ¥Accessing `/graphql` should show Apollo GraphQL playground work with. ## 上下文 {#context} ¥Context 由于 Elysia 基于 Web 标准请求和响应,这与 Express 使用的 Node 的 `HttpRequest` 和 `HttpResponse` 不同,导致 `req, res` 在上下文中未定义。 ¥Because Elysia is based on Web Standard Request and Response which is different from Node's `HttpRequest` and `HttpResponse` that Express uses, results in `req, res` being undefined in context. 因此,Elysia 将两者都替换为类似 `context` 的路由参数。 ¥Because of this, Elysia replaces both with `context` like route parameters. ```typescript const app = new Elysia() .use( apollo({ typeDefs, resolvers, context: async ({ request }) => { const authorization = request.headers.get('Authorization') return { authorization } } }) ) .listen(3000) ``` ## 配置 {#config} ¥Config 此插件扩展了 Apollo 的 [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options)(即 `ApolloServer`'s' 构造函数参数)。 ¥This plugin extends Apollo's [ServerRegistration](https://www.apollographql.com/docs/apollo-server/api/apollo-server/#options) (which is `ApolloServer`'s' constructor parameter). 以下是使用 Elysia 配置 Apollo 服务器的扩展参数。 ¥Below are the extended parameters for configuring Apollo Server with Elysia. ### path {#path} @default `"/graphql"` 公开 Apollo 服务器的路径。 ¥Path to expose Apollo Server. ### enablePlayground {#enableplayground} @default `process.env.ENV !== 'production'` 确定 Apollo 是否应提供 Apollo Playground。 ¥Determine whether should Apollo should provide Apollo Playground. --- --- url: 'https://elysiajs.com/plugins/cors.md' --- # CORS 插件 {#cors-plugin} ¥CORS Plugin 此插件增加了自定义 [跨域资源共享](https://web.nodejs.cn/en-US/docs/Web/HTTP/CORS) 行为的支持。 ¥This plugin adds support for customizing [Cross-Origin Resource Sharing](https://web.nodejs.cn/en-US/docs/Web/HTTP/CORS) behavior. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/cors ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' new Elysia().use(cors()).listen(3000) ``` 这将设置 Elysia 接受来自任何来源的请求。 ¥This will set Elysia to accept requests from any origin. ## 配置 {#config} ¥Config 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### origin {#origin} @default `true` 指示响应是否可以与来自给定来源的请求代码共享。 ¥Indicates whether the response can be shared with the requesting code from the given origins. 值可以是以下之一: ¥Value can be one of the following: * string - 将直接分配给 [Access-Control-Allow-Origin](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 标头的来源名称。 * boolean - 如果设置为 true,[Access-Control-Allow-Origin](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin) 将设置为 `*`(任何来源) * RegExp - 匹配请求 URL 的模式,如果匹配则允许。 * 函数 - 允许资源共享的自定义逻辑,如果返回 `true`,则允许共享。 * 预期类型: ```typescript cors(context: Context) => boolean | void ``` * Array\ - 按顺序遍历上述所有情况,如果任何值是 `true`,则允许。 *** ### methods {#methods} @default `*` 允许跨域请求的方法。 ¥Allowed methods for cross-origin requests. 分配 [Access-Control-Allow-Methods](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) 标头。 ¥Assign [Access-Control-Allow-Methods](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) header. 值可以是以下之一: ¥Value can be one of the following: * 未定义 | 空 | '' - 忽略所有方法。 * * * 允许所有方法。 * string - 预期为单个方法或逗号分隔的字符串 * (例如:`'GET, PUT, POST'`) * string\[] - 允许多个 HTTP 方法。 * 例如:`['GET', 'PUT', 'POST']` *** ### allowedHeaders {#allowedheaders} @default `*` 允许传入请求的标头。 ¥Allowed headers for an incoming request. 分配 [Access-Control-Allow-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) 标头。 ¥Assign [Access-Control-Allow-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) header. 值可以是以下之一: ¥Value can be one of the following: * string - 预期为单个标头或逗号分隔的字符串 * 例如:`'Content-Type, Authorization'`. * string\[] - 允许多个 HTTP 标头。 * 例如:`['Content-Type', 'Authorization']` *** ### exposeHeaders {#exposeheaders} @default `*` 响应指定标头的 CORS。 ¥Response CORS with specified headers. 分配 [Access-Control-Expose-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) 标头。 ¥Assign [Access-Control-Expose-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) header. 值可以是以下之一: ¥Value can be one of the following: * string - 预期为单个标头或逗号分隔的字符串。 * 例如:`'Content-Type, X-Powered-By'`. * string\[] - 允许多个 HTTP 标头。 * 例如:`['Content-Type', 'X-Powered-By']` *** ### credentials {#credentials} @default `true` Access-Control-Allow-Credentials 响应头告知浏览器,当请求的凭据模式 [Request.credentials](https://web.nodejs.cn/en-US/docs/Web/API/Request/credentials) 为 `include` 时,是否将响应暴露给前端 JavaScript 代码。 ¥The Access-Control-Allow-Credentials response header tells browsers whether to expose the response to the frontend JavaScript code when the request's credentials mode [Request.credentials](https://web.nodejs.cn/en-US/docs/Web/API/Request/credentials) is `include`. 当请求的凭证模式 [Request.credentials](https://web.nodejs.cn/en-US/docs/Web/API/Request/credentials) 为 `include` 时,仅当 Access-Control-Allow-Credentials 值为 true 时,浏览器才会将响应公开给前端 JavaScript 代码。 ¥When a request's credentials mode [Request.credentials](https://web.nodejs.cn/en-US/docs/Web/API/Request/credentials) is `include`, browsers will only expose the response to the frontend JavaScript code if the Access-Control-Allow-Credentials value is true. 凭证可以是 Cookie、授权标头或 TLS 客户端证书。 ¥Credentials are cookies, authorization headers, or TLS client certificates. 分配 [Access-Control-Allow-Credentials](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) 标头。 ¥Assign [Access-Control-Allow-Credentials](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials) header. *** ### maxAge {#maxage} @default `5` 指示 [预检请求](https://web.nodejs.cn/en-US/docs/Glossary/Preflight_request) 的结果(即 [Access-Control-Allow-Methods](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) 和 [Access-Control-Allow-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) 标头中包含的信息)可以缓存多长时间。 ¥Indicates how long the results of a [preflight request](https://web.nodejs.cn/en-US/docs/Glossary/Preflight_request) (that is the information contained in the [Access-Control-Allow-Methods](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Methods) and [Access-Control-Allow-Headers](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Headers) headers) can be cached. 分配 [Access-Control-Max-Age](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) 标头。 ¥Assign [Access-Control-Max-Age](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age) header. *** ### preflight {#preflight} 预检请求是发送的请求,用于检查 CORS 协议是否被理解,以及服务器是否知道使用特定的方法和标头。 ¥The preflight request is a request sent to check if the CORS protocol is understood and if a server is aware of using specific methods and headers. 响应带有 3 个 HTTP 请求标头的 OPTIONS 请求: ¥Response with **OPTIONS** request with 3 HTTP request headers: * **访问控制请求方法** * **访问控制请求标头** * **来源** 此配置指示服务器是否应响应预检请求。 ¥This config indicates if the server should respond to preflight requests. ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. ## 允许通过顶层域名进行 CORS {#allow-cors-by-top-level-domain} ¥Allow CORS by top-level domain ```typescript twoslash import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' const app = new Elysia() .use( cors({ origin: /.*\.saltyaom\.com$/ }) ) .get('/', () => 'Hi') .listen(3000) ``` 这将允许使用 `saltyaom.com` 从顶层域名发出请求。 ¥This will allow requests from top-level domains with `saltyaom.com` --- --- url: 'https://elysiajs.com/plugins/cron.md' --- # Cron 插件 {#cron-plugin} ¥Cron Plugin 此插件增加了在 Elysia 服务器中运行 cronjobs 的支持。 ¥This plugin adds support for running cronjobs in the Elysia server. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/cron ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { cron } from '@elysiajs/cron' new Elysia() .use( cron({ name: 'heartbeat', pattern: '*/10 * * * * *', run() { console.log('Heartbeat') } }) ) .listen(3000) ``` 上述代码将每 10 秒记录一次 `heartbeat`。 ¥The above code will log `heartbeat` every 10 seconds. ## cron {#cron} 为 Elysia 服务器创建一个 cronjob。 ¥Create a cronjob for the Elysia server. 类型: ¥type: ``` cron(config: CronConfig, callback: (Instance['store']) => void): this ``` `CronConfig` 接受以下指定的参数: ¥`CronConfig` accepts the parameters specified below: ### name {#name} 要注册到 `store` 的作业名称。 ¥Job name to register to `store`. 这将使用指定的名称将 cron 实例注册到 `store`,以便在后续流程中引用,例如停止作业。 ¥This will register the cron instance to `store` with a specified name, which can be used to reference in later processes eg. stop the job. ### pattern {#pattern} 按照 [cron 语法](https://en.wikipedia.org/wiki/Cron) 指定的方式运行作业的时间如下: ¥Time to run the job as specified by [cron syntax](https://en.wikipedia.org/wiki/Cron) specified as below: ``` ┌────────────── second (optional) │ ┌──────────── minute │ │ ┌────────── hour │ │ │ ┌──────── day of the month │ │ │ │ ┌────── month │ │ │ │ │ ┌──── day of week │ │ │ │ │ │ * * * * * * ``` 这可以通过 [Crontab Guru](https://crontab.guru/) 等工具生成。 ¥This can be generated by tools like [Crontab Guru](https://crontab.guru/) *** 此插件使用 [cronner](https://github.com/hexagon/croner) 将 cron 方法扩展至 Elysia。 ¥This plugin extends the cron method to Elysia using [cronner](https://github.com/hexagon/croner). 以下是 cronner 接受的配置。 ¥Below are the configs accepted by cronner. ### timezone {#timezone} 时区,采用欧洲/斯德哥尔摩格式 ¥Time zone in Europe/Stockholm format ### startAt {#startat} 作业的计划启动时间 ¥Schedule start time for the job ### stopAt {#stopat} 作业的计划停止时间 ¥Schedule stop time for the job ### maxRuns {#maxruns} 最大执行次数 ¥Maximum number of executions ### catch {#catch} 即使触发的函数抛出未处理的错误,也继续执行。 ¥Continue execution even if an unhandled error is thrown by a triggered function. ### interval {#interval} 两次执行之间的最小间隔,以秒为单位。 ¥The minimum interval between executions, in seconds. ## 模式 {#pattern-1} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. ## 停止 cronjob {#stop-cronjob} ¥Stop cronjob 你可以通过访问注册到 `store` 的 cronjob 名称来手动停止 cronjob。 ¥You can stop cronjob manually by accessing the cronjob name registered to `store`. ```typescript import { Elysia } from 'elysia' import { cron } from '@elysiajs/cron' const app = new Elysia() .use( cron({ name: 'heartbeat', pattern: '*/1 * * * * *', run() { console.log('Heartbeat') } }) ) .get( '/stop', ({ store: { cron: { heartbeat } } }) => { heartbeat.stop() return 'Stop heartbeat' } ) .listen(3000) ``` ## 预定义模式 {#predefined-patterns} ¥Predefined patterns 你可以使用 `@elysiajs/cron/schedule` 中的预定义模式 ¥You can use predefined patterns from `@elysiajs/cron/schedule` ```typescript import { Elysia } from 'elysia' import { cron, Patterns } from '@elysiajs/cron' const app = new Elysia() .use( cron({ name: 'heartbeat', pattern: Patterns.everySecond(), run() { console.log('Heartbeat') } }) ) .get( '/stop', ({ store: { cron: { heartbeat } } }) => { heartbeat.stop() return 'Stop heartbeat' } ) .listen(3000) ``` ### 函数 {#functions} ¥Functions | 函数 | 描述 | | ---------------------------------------- | ------------------ | | `.everySeconds(2)` | 每 2 秒运行一次任务 | | `.everyMinutes(5)` | 每 5 分钟运行一次任务 | | `.everyHours(3)` | 每 3 小时运行一次任务 | | `.everyHoursAt(3, 15)` | 每 3 小时 15 分钟运行一次任务 | | `.everyDayAt('04:19')` | 每天 04:19 运行任务 | | `.everyWeekOn(Patterns.MONDAY, '19:30')` | 每周一 19:30 运行任务 | | `.everyWeekdayAt('17:00')` | 每周一至周五 17:00 运行任务 | | `.everyWeekendAt('11:00')` | 周六和周日 11:00 运行任务 | ### 常量的函数别名 {#function-aliases-to-constants} ¥Function aliases to constants | 函数 | 常量 | | ----------------- | ---------------------------------------- | | `.everySecond()` | EVERY\_SECOND | | `.everyMinute()` | EVERY\_MINUTE | | `.hourly()` | EVERY\_HOUR | | `.daily()` | EVERY\_DAY\_AT\_MIDNIGHT | | `.everyWeekday()` | EVERY\_WEEKDAY | | `.everyWeekend()` | EVERY\_WEEKEND | | `.weekly()` | EVERY\_WEEK | | `.monthly()` | EVERY\_1ST\_DAY\_OF\_MONTH\_AT\_MIDNIGHT | | `.everyQuarter()` | EVERY\_QUARTER | | `.yearly()` | EVERY\_YEAR | ### 常量 {#constants} ¥Constants | 常量 | 模式 | | ---------------------------------------- | -------------------- | | `.EVERY_SECOND` | `* * * * * *` | | `.EVERY_5_SECONDS` | `*/5 * * * * *` | | `.EVERY_10_SECONDS` | `*/10 * * * * *` | | `.EVERY_30_SECONDS` | `*/30 * * * * *` | | `.EVERY_MINUTE` | `*/1 * * * *` | | `.EVERY_5_MINUTES` | `0 */5 * * * *` | | `.EVERY_10_MINUTES` | `0 */10 * * * *` | | `.EVERY_30_MINUTES` | `0 */30 * * * *` | | `.EVERY_HOUR` | `0 0-23/1 * * *` | | `.EVERY_2_HOURS` | `0 0-23/2 * * *` | | `.EVERY_3_HOURS` | `0 0-23/3 * * *` | | `.EVERY_4_HOURS` | `0 0-23/4 * * *` | | `.EVERY_5_HOURS` | `0 0-23/5 * * *` | | `.EVERY_6_HOURS` | `0 0-23/6 * * *` | | `.EVERY_7_HOURS` | `0 0-23/7 * * *` | | `.EVERY_8_HOURS` | `0 0-23/8 * * *` | | `.EVERY_9_HOURS` | `0 0-23/9 * * *` | | `.EVERY_10_HOURS` | `0 0-23/10 * * *` | | `.EVERY_11_HOURS` | `0 0-23/11 * * *` | | `.EVERY_12_HOURS` | `0 0-23/12 * * *` | | `.EVERY_DAY_AT_1AM` | `0 01 * * *` | | `.EVERY_DAY_AT_2AM` | `0 02 * * *` | | `.EVERY_DAY_AT_3AM` | `0 03 * * *` | | `.EVERY_DAY_AT_4AM` | `0 04 * * *` | | `.EVERY_DAY_AT_5AM` | `0 05 * * *` | | `.EVERY_DAY_AT_6AM` | `0 06 * * *` | | `.EVERY_DAY_AT_7AM` | `0 07 * * *` | | `.EVERY_DAY_AT_8AM` | `0 08 * * *` | | `.EVERY_DAY_AT_9AM` | `0 09 * * *` | | `.EVERY_DAY_AT_10AM` | `0 10 * * *` | | `.EVERY_DAY_AT_11AM` | `0 11 * * *` | | `.EVERY_DAY_AT_NOON` | `0 12 * * *` | | `.EVERY_DAY_AT_1PM` | `0 13 * * *` | | `.EVERY_DAY_AT_2PM` | `0 14 * * *` | | `.EVERY_DAY_AT_3PM` | `0 15 * * *` | | `.EVERY_DAY_AT_4PM` | `0 16 * * *` | | `.EVERY_DAY_AT_5PM` | `0 17 * * *` | | `.EVERY_DAY_AT_6PM` | `0 18 * * *` | | `.EVERY_DAY_AT_7PM` | `0 19 * * *` | | `.EVERY_DAY_AT_8PM` | `0 20 * * *` | | `.EVERY_DAY_AT_9PM` | `0 21 * * *` | | `.EVERY_DAY_AT_10PM` | `0 22 * * *` | | `.EVERY_DAY_AT_11PM` | `0 23 * * *` | | `.EVERY_DAY_AT_MIDNIGHT` | `0 0 * * *` | | `.EVERY_WEEK` | `0 0 * * 0` | | `.EVERY_WEEKDAY` | `0 0 * * 1-5` | | `.EVERY_WEEKEND` | `0 0 * * 6,0` | | `.EVERY_1ST_DAY_OF_MONTH_AT_MIDNIGHT` | `0 0 1 * *` | | `.EVERY_1ST_DAY_OF_MONTH_AT_NOON` | `0 12 1 * *` | | `.EVERY_2ND_HOUR` | `0 */2 * * *` | | `.EVERY_2ND_HOUR_FROM_1AM_THROUGH_11PM` | `0 1-23/2 * * *` | | `.EVERY_2ND_MONTH` | `0 0 1 */2 *` | | `.EVERY_QUARTER` | `0 0 1 */3 *` | | `.EVERY_6_MONTHS` | `0 0 1 */6 *` | | `.EVERY_YEAR` | `0 0 1 1 *` | | `.EVERY_30_MINUTES_BETWEEN_9AM_AND_5PM` | `0 */30 9-17 * * *` | | `.EVERY_30_MINUTES_BETWEEN_9AM_AND_6PM` | `0 */30 9-18 * * *` | | `.EVERY_30_MINUTES_BETWEEN_10AM_AND_7PM` | `0 */30 10-19 * * *` | --- --- url: 'https://elysiajs.com/eden/installation.md' --- # Eden 安装 {#eden-installation} ¥Eden Installation 首先在前端安装 Eden: ¥Start by installing Eden on your frontend: ```bash bun add @elysiajs/eden bun add -d elysia ``` ::: tip 提示 Eden 需要 Elysia 来推断工具类型。 ¥Eden needs Elysia to infer utilities type. 确保在服务器上安装与版本匹配的 Elysia。 ¥Make sure to install Elysia with the version matching on the server. ::: 首先,导出你现有的 Elysia 服务器类型: ¥First, export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] ``` 然后在客户端使用 Elysia API: ¥Then consume the Elysia API on client side: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] // @filename: index.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const client = treaty('localhost:3000') // [!code ++] // response: Hi Elysia const { data: index } = await client.get() // response: 1895 const { data: id } = await client.id({ id: 1895 }).get() // response: { id: 1895, name: 'Skadi' } const { data: nendoroid } = await client.mirror.post({ id: 1895, name: 'Skadi' }) // @noErrors client. // ^| ``` ## 疑难解答 {#gotcha} ¥Gotcha 有时,Eden 可能无法正确从 Elysia 推断类型,以下是修复 Eden 类型推断的最常见解决方法。 ¥Sometimes, Eden may not infer types from Elysia correctly, the following are the most common workarounds to fix Eden type inference. ### 类型严格 {#type-strict} ¥Type Strict 确保在 tsconfig.json 中启用严格模式 ¥Make sure to enable strict mode in **tsconfig.json** ```json { "compilerOptions": { "strict": true // [!code ++] } } ``` ### Elysia 版本不匹配 {#unmatch-elysia-version} ¥Unmatch Elysia version Eden 依赖于 Elysia 类来导入 Elysia 实例并正确推断类型。 ¥Eden depends on Elysia class to import Elysia instance and infer types correctly. 确保客户端和服务器都具有匹配的 Elysia 版本。 ¥Make sure that both client and server have the matching Elysia version. 你可以使用 [`npm why`](https://npm.nodejs.cn/cli/v10/commands/npm-explain) 命令进行检查: ¥You can check it with [`npm why`](https://npm.nodejs.cn/cli/v10/commands/npm-explain) command: ```bash npm why elysia ``` 输出应该只在顶层包含一个 Elysia 版本: ¥And output should contain only one elysia version on top-level: ``` elysia@1.1.12 node_modules/elysia elysia@"1.1.25" from the root project peer elysia@">= 1.1.0" from @elysiajs/html@1.1.0 node_modules/@elysiajs/html dev @elysiajs/html@"1.1.1" from the root project peer elysia@">= 1.1.0" from @elysiajs/opentelemetry@1.1.2 node_modules/@elysiajs/opentelemetry dev @elysiajs/opentelemetry@"1.1.7" from the root project peer elysia@">= 1.1.0" from @elysiajs/swagger@1.1.0 node_modules/@elysiajs/swagger dev @elysiajs/swagger@"1.1.6" from the root project peer elysia@">= 1.1.0" from @elysiajs/eden@1.1.2 node_modules/@elysiajs/eden dev @elysiajs/eden@"1.1.3" from the root project ``` ### TypeScript 版本 {#typescript-version} ¥TypeScript version Elysia 使用 TypeScript 的新功能和语法,以最高效的方式推断类型。Const Generic 和 Template Literal 等功能被广泛使用。 ¥Elysia uses newer features and syntax of TypeScript to infer types in the most performant way. Features like Const Generic and Template Literal are heavily used. 如果你的客户端 TypeScript 版本 >= 5.0,请确保其最低版本为 TypeScript ¥Make sure your client has a **minimum TypeScript version if >= 5.0** ### 方法链 {#method-chaining} ¥Method Chaining 要使 Eden 正常工作,Elysia 必须使用方法链。 ¥To make Eden work, Elysia must use **method chaining** Elysia 的类型系统非常复杂,方法通常会为实例引入新的类型。 ¥Elysia's type system is complex, methods usually introduce a new type to the instance. 使用方法链将有助于节省新的类型引用。 ¥Using method chaining will help save that new type reference. 例如: ¥For example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) .listen(3000) ``` 使用此代码,state 现在返回一个新的 ElysiaInstance 类型,将 build 引入到 store 中以替换当前的类型。 ¥Using this, **state** now returns a new **ElysiaInstance** type, introducing **build** into store replacing the current one. 如果没有方法链,Elysia 在引入新类型时不会保存,从而导致无法进行类型推断。 ¥Without method chaining, Elysia doesn't save the new type when introduced, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` ### 类型定义 {#type-definitions} ¥Type Definitions 如果你正在使用 Bun 的特定功能(例如 `Bun.file` 或类似的 API)并从处理程序返回它,则可能还需要将 Bun 类型定义安装到客户端。 ¥If you are using a Bun specific feature, like `Bun.file` or similar API and return it from a handler, you may need to install Bun type definitions to the client as well. ```bash bun add -d @types/bun ``` ### 路径别名 (monorepo) {#path-alias-monorepo} ¥Path alias (monorepo) 如果你在 monorepo 中使用路径别名,请确保前端能够解析与后端相同的路径。 ¥If you are using path alias in your monorepo, make sure that frontend is able to resolve the path as same as backend. ::: tip 提示 在 monorepo 中设置路径别名有点棘手,你可以 fork 我们的示例模板:[Kozeki 模板](https://github.com/SaltyAom/kozeki-template) 并根据你的需求进行修改。 ¥Setting up path alias in monorepo is a bit tricky, you can fork our example template: [Kozeki Template](https://github.com/SaltyAom/kozeki-template) and modify it to your needs. ::: 例如,如果你在 tsconfig.json 中为后端指定了以下路径别名: ¥For example, if you have the following path alias for your backend in **tsconfig.json**: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["./src/*"] } } } ``` 你的后端代码如下所示: ¥And your backend code is like this: ```typescript import { Elysia } from 'elysia' import { a, b } from '@/controllers' const app = new Elysia() .use(a) .use(b) .listen(3000) export type app = typeof app ``` 你必须确保你的前端代码能够解析相同的路径别名。否则,类型推断将被解析为 any。 ¥You **must** make sure that your frontend code is able to resolve the same path alias. Otherwise, type inference will be resolved as any. ```typescript import { treaty } from '@elysiajs/eden' import type { app } from '@/index' const client = treaty('localhost:3000') // This should be able to resolve the same module both frontend and backend, and not `any` import { a, b } from '@/controllers' // [!code ++] ``` 为了解决这个问题,你必须确保路径别名在前端和后端都解析为同一个文件。 ¥To fix this, you must make sure that path alias is resolved to the same file in both frontend and backend. 因此,你必须将 tsconfig.json 中的路径别名更改为: ¥So, you must change the path alias in **tsconfig.json** to: ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@/*": ["../apps/backend/src/*"] } } } ``` 如果配置正确,你应该能够在前端和后端解析相同的模块。 ¥If configured correctly, you should be able to resolve the same module in both frontend and backend. ```typescript // This should be able to resolve the same module both frontend and backend, and not `any` import { a, b } from '@/controllers' ``` #### 命名空间 {#namespace} ¥Namespace 我们建议为 monorepo 中的每个模块添加命名空间前缀,以避免可能发生的任何混淆和冲突。 ¥We recommended adding a **namespace** prefix for each module in your monorepo to avoid any confusion and conflict that may happen. ```json { "compilerOptions": { "baseUrl": ".", "paths": { "@frontend/*": ["./apps/frontend/src/*"], "@backend/*": ["./apps/backend/src/*"] } } } ``` 然后,你可以像这样导入模块: ¥Then, you can import the module like this: ```typescript // Should work in both frontend and backend and not return `any` import { a, b } from '@backend/controllers' ``` 我们建议创建一个单独的 tsconfig.json 文件,将 `baseUrl` 定义为代码库的根目录,根据模块位置提供路径,并为每个继承了根目录 tsconfig.json 文件(该目录具有路径别名)的模块创建一个 tsconfig.json 文件。 ¥We recommend creating a **single tsconfig.json** that defines a `baseUrl` as the root of your repo, provide a path according to the module location, and create a **tsconfig.json** for each module that inherits the root **tsconfig.json** which has the path alias. 你可以在此 [路径别名示例代码库](https://github.com/SaltyAom/elysia-monorepo-path-alias) 或 [Kozeki 模板](https://github.com/SaltyAom/kozeki-template) 中找到一个可用的示例。 ¥You may find a working example of in this [path alias example repo](https://github.com/SaltyAom/elysia-monorepo-path-alias) or [Kozeki Template](https://github.com/SaltyAom/kozeki-template). --- --- url: 'https://elysiajs.com/eden/treaty/websocket.md' --- # WebSocket {#websocket} Eden Treaty 使用 `subscribe` 方法支持 WebSocket。 ¥Eden Treaty supports WebSocket using `subscribe` method. ```typescript twoslash import { Elysia, t } from "elysia"; import { treaty } from "@elysiajs/eden"; const app = new Elysia() .ws("/chat", { body: t.String(), response: t.String(), message(ws, message) { ws.send(message); }, }) .listen(3000); const api = treaty("localhost:3000"); const chat = api.chat.subscribe(); chat.subscribe((message) => { console.log("got", message); }); chat.on("open", () => { chat.send("hello from client"); }); ``` .subscribe 接受与 `get` 和 `head` 相同的参数。 ¥**.subscribe** accepts the same parameter as `get` and `head`. ## 响应 {#response} ¥Response Eden.subscribe 返回 EdenWS,它以相同的语法扩展了 [WebSocket](https://web.nodejs.cn/en-US/docs/Web/API/WebSocket/WebSocket) 的结果。 ¥**Eden.subscribe** returns **EdenWS** which extends the [WebSocket](https://web.nodejs.cn/en-US/docs/Web/API/WebSocket/WebSocket) results in identical syntax. 如果需要更多控制,可以访问 EdenWebSocket.raw 来与原生 WebSocket API 交互。 ¥If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. --- --- url: 'https://elysiajs.com/eden/treaty/unit-test.md' --- # 单元测试 {#unit-test} ¥Unit Test 根据 [Eden Treaty 配置](/eden/treaty/config.html#urlorinstance) 和 [单元测试](/patterns/unit-test),我们可以将 Elysia 实例直接传递给 Eden Treaty,以便直接与 Elysia 服务器交互,而无需发送网络请求。 ¥According to [Eden Treaty config](/eden/treaty/config.html#urlorinstance) and [Unit Test](/patterns/unit-test), we may pass an Elysia instance to Eden Treaty directly to interact with Elysia server directly without sending a network request. 我们可以使用此模式创建一个单元测试,同时包含端到端类型安全和类型级测试。 ¥We may use this pattern to create a unit test with end-to-end type safety and type-level test all at once. ```typescript twoslash // test/index.test.ts import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia().get('/hello', 'hi') const api = treaty(app) describe('Elysia', () => { it('returns a response', async () => { const { data } = await api.hello.get() expect(data).toBe('hi') // ^? }) }) ``` ## 类型安全测试 {#type-safety-test} ¥Type safety test 要执行类型安全测试,只需在测试文件夹上运行 tsc 即可。 ¥To perform a type safety test, simply run **tsc** on test folders. ```bash tsc --noEmit test/**/*.ts ``` 这对于确保客户端和服务器的类型完整性非常有用,尤其是在迁移期间。 ¥This is useful to ensure type integrity for both client and server, especially during migrations. --- --- url: 'https://elysiajs.com/eden/treaty/parameters.md' --- # 参数 {#parameters} ¥Parameters 我们最终需要将有效负载发送到服务器。 ¥We need to send a payload to server eventually. 为了处理这种情况,Eden Treaty 的方法接受两个参数来将数据发送到服务器。 ¥To handle this, Eden Treaty's methods accept 2 parameters to send data to server. 两个参数都是类型安全的,并将由 TypeScript 自动引导: ¥Both parameters are type safe and will be guided by TypeScript automatically: 1. body 2. 附加参数 * query * headers * fetch ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', ({ body }) => body, { body: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') // ✅ works api.user.post({ name: 'Elysia' }) // ✅ also works api.user.post({ name: 'Elysia' }, { // This is optional as not specified in schema headers: { authorization: 'Bearer 12345' }, query: { id: 2 } }) ``` 除非该方法不接受 body,否则 body 将被省略,只保留一个参数。 ¥Unless if the method doesn't accept body, then body will be omitted and left with single parameter only. 如果方法 "GET" 或 "HEAD": ¥If the method **"GET"** or **"HEAD"**: 1. 附加参数 * query * headers * fetch ```typescript import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hello', () => 'hi') .listen(3000) const api = treaty('localhost:3000') // ✅ works api.hello.get({ // This is optional as not specified in schema headers: { hello: 'world' } }) ``` ## 空体 {#empty-body} ¥Empty body 如果 body 是可选的或不需要,但查询或标头是必需的,你可以将 body 作为 `null` 或 `undefined` 传递。 ¥If body is optional or not need but query or headers is required, you may pass the body as `null` or `undefined` instead. ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', () => 'hi', { query: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') api.user.post(null, { query: { name: 'Ely' } }) ``` ## 获取参数 {#fetch-parameters} ¥Fetch parameters Eden Treaty 是一个获取封装器,我们可以通过将任何有效的 [获取](https://web.nodejs.cn/en-US/docs/Web/API/Fetch_API/Using_Fetch) 参数传递给 `$fetch` 来向 Eden 添加它: ¥Eden Treaty is a fetch wrapper, we may add any valid [Fetch](https://web.nodejs.cn/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hello', () => 'hi') .listen(3000) const api = treaty('localhost:3000') const controller = new AbortController() const cancelRequest = setTimeout(() => { controller.abort() }, 5000) await api.hello.get({ fetch: { signal: controller.signal } }) clearTimeout(cancelRequest) ``` ## 文件上传 {#file-upload} ¥File Upload 我们可以将以下任一内容传递给附加文件: ¥We may either pass one of the following to attach file(s): * **文件** * **File\[]** * **FileList** * **Blob** 附加文件会导致 content-type 变为 multipart/form-data。 ¥Attaching a file will results **content-type** to be **multipart/form-data** 假设我们的服务器如下: ¥Suppose we have the server as the following: ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), image: t.Files() }) }) .listen(3000) export const api = treaty('localhost:3000') const images = document.getElementById('images') as HTMLInputElement const { data } = await api.image.post({ title: "Misono Mika", image: images.files!, }) ``` --- --- url: 'https://elysiajs.com/eden/treaty/response.md' --- # 响应 {#response} ¥Response 调用 fetch 方法后,Eden Treaty 将返回一个包含以下属性的对象 `Promise`: ¥Once the fetch method is called, Eden Treaty returns a `Promise` containing an object with the following properties: * data - 响应的返回值 (2xx) * error - 响应的返回值 (>= 3xx) * 响应 `Response` - Web 标准响应类 * 状态 `number` - HTTP 状态码 * 标头 `FetchRequestInit['headers']` - 响应头 返回后,必须提供错误处理机制,以确保响应数据值已解包,否则该值将为可空值。Elysia 提供 `error()` 辅助函数来处理错误,Eden 将提供错误值的类型缩减功能。 ¥Once returned, you must provide error handling to ensure that the response data value is unwrapped, otherwise the value will be nullable. Elysia provides a `error()` helper function to handle the error, and Eden will provide type narrowing for the error value. ```typescript import { Elysia, t } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', ({ body: { name }, status }) => { if(name === 'Otto') return status(400) return name }, { body: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') const submit = async (name: string) => { const { data, error } = await api.user.post({ name }) // type: string | null console.log(data) if (error) switch(error.status) { case 400: // Error type will be narrow down throw error.value default: throw error.value } // Once the error is handled, type will be unwrapped // type: string return data } ``` 默认情况下,Elysia 会自动将 `error` 和 `response` 类型推断为 TypeScript,并且 Eden 将提供自动补齐和类型缩小功能以确保准确的行为。 ¥By default, Elysia infers `error` and `response` types to TypeScript automatically, and Eden will be providing auto-completion and type narrowing for accurate behavior. ::: tip 提示 如果服务器响应的 HTTP 状态码 >= 300,则该值将始终为 `null`,而返回值将改为 `error`。 ¥If the server responds with an HTTP status >= 300, then the value will always be `null`, and `error` will have a returned value instead. 否则,响应将传递给 `data`。 ¥Otherwise, response will be passed to `data`. ::: ## Stream 响应 {#stream-response} ¥Stream response Eden 会将流响应或 [服务器发送事件](/essential/handler.html#server-sent-events-sse) 解释为 `AsyncGenerator`,从而允许我们使用 `for await` 循环来使用该流。 ¥Eden will interpret a stream response or [Server-Sent Events](/essential/handler.html#server-sent-events-sse) as `AsyncGenerator` allowing us to use `for await` loop to consume the stream. ::: code-group ```typescript twoslash [Stream] import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/ok', function* () { yield 1 yield 2 yield 3 }) const { data, error } = await treaty(app).ok.get() if (error) throw error for await (const chunk of data) console.log(chunk) // ^? ``` ```typescript twoslash [Server-Sent Events] import { Elysia, sse } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/ok', function* () { yield sse({ event: 'message', data: 1 }) yield sse({ event: 'message', data: 2 }) yield sse({ event: 'end' }) }) const { data, error } = await treaty(app).ok.get() if (error) throw error for await (const chunk of data) console.log(chunk) // ^? // ``` ::: ## 工具类型 {#utility-type} ¥Utility type Eden Treaty 提供了实用类型 `Treaty.Data` 和 `Treaty.Error`,用于从响应中提取 `data` 和 `error` 类型。 ¥Eden Treaty provides a utility type `Treaty.Data` and `Treaty.Error` to extract the `data` and `error` type from the response. ```typescript twoslash import { Elysia, t } from 'elysia' import { treaty, Treaty } from '@elysiajs/eden' const app = new Elysia() .post('/user', ({ body: { name }, status }) => { if(name === 'Otto') return status(400) return name }, { body: t.Object({ name: t.String() }) }) .listen(3000) const api = treaty('localhost:3000') type UserData = Treaty.Data // ^? // Alternatively you can also pass a response const response = await api.user.post({ name: 'Saltyaom' }) type UserDataFromResponse = Treaty.Data // ^? type UserError = Treaty.Error // ^? // ``` --- --- url: 'https://elysiajs.com/eden/treaty/legacy.md' --- # Eden 条约遗留问题 {#eden-treaty-legacy} ¥Eden Treaty Legacy ::: tip 注意 这是 Eden Treaty 1 或 (edenTreaty) 的文档 ¥This is a documentation for Eden Treaty 1 or (edenTreaty) 对于新项目,我们建议从 Eden Treaty 2(条约)开始。 ¥For a new project, we recommended starting with Eden Treaty 2 (treaty) instead. ::: Eden Treaty 是一个 Elysia 服务器的对象表示。 ¥Eden Treaty is an object-like representation of an Elysia server. 提供像普通对象一样的访问器,直接从服务器获取类型,帮助我们加快速度,并确保不会出现任何问题 ¥Providing accessor like a normal object with type directly from the server, helping us to move faster, and make sure that nothing break *** 要使用 Eden Treaty,首先导出你现有的 Elysia 服务器类型: ¥To use Eden Treaty, first export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] ``` 然后导入服务器类型,并在客户端使用 Elysia API: ¥Then import the server type, and consume the Elysia API on client: ```typescript // client.ts import { edenTreaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const app = edenTreaty('http://localhost:') // response type: 'Hi Elysia' const { data: pong, error } = app.get() // response type: 1895 const { data: id, error } = app.id['1895'].get() // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) ``` ::: tip 提示 Eden Treaty 完全类型安全,并支持自动补齐功能。 ¥Eden Treaty is fully type-safe with auto-completion support. ::: ## 结构 {#anatomy} ¥Anatomy Eden Treaty 会将所有现有路径转换为类似对象的表示形式,具体如下: ¥Eden Treaty will transform all existing paths to object-like representation, that can be described as: ```typescript EdenTreaty.<1>.<2>..({ ...body, $query?: {}, $fetch?: RequestInit }) ``` ### 路径 {#path} ¥Path Eden 会将 `/` 转换为 `.`,后者可以通过已注册的 `method` 进行调用,例如: ¥Eden will transform `/` into `.` which can be called with a registered `method`, for example: * /path -> .path * /nested/path -> .nested.path ### 路径参数 {#path-parameters} ¥Path parameters 路径参数将根据其在 URL 中的名称自动映射。 ¥Path parameters will be mapped automatically by their name in the URL. * /id/:id -> .id.`` * 例如: .id.hi * 例如:.id\['123'] ::: tip 提示 如果路径不支持路径参数,TypeScript 将显示错误。 ¥If a path doesn't support path parameters, TypeScript will show an error. ::: ### 查询 {#query} ¥Query 你可以使用 `$query` 将查询附加到路径: ¥You can append queries to path with `$query`: ```typescript app.get({ $query: { name: 'Eden', code: 'Gold' } }) ``` ### 获取 {#fetch} ¥Fetch Eden Treaty 是一个获取封装器,你可以通过将任何有效的 [获取](https://web.nodejs.cn/en-US/docs/Web/API/Fetch_API/Using_Fetch) 参数传递给 `$fetch` 来向 Eden 添加它: ¥Eden Treaty is a fetch wrapper, you can add any valid [Fetch](https://web.nodejs.cn/en-US/docs/Web/API/Fetch_API/Using_Fetch) parameters to Eden by passing it to `$fetch`: ```typescript app.post({ $fetch: { headers: { 'x-organization': 'MANTIS' } } }) ``` ## 错误处理 {#error-handling} ¥Error Handling Eden Treaty 将返回 `data` 和 `error` 的值,且均为完整类型。 ¥Eden Treaty will return a value of `data` and `error` as a result, both fully typed. ```typescript // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) if(error) { switch(error.status) { case 400: case 401: warnUser(error.value) break case 500: case 502: emergencyCallDev(error.value) break default: reportError(error.value) break } throw error } const { id, name } = nendoroid ``` 数据和错误都将被类型化为可空,直到你使用类型保护确认它们的状态。 ¥Both **data**, and **error** will be typed as nullable until you can confirm their statuses with a type guard. 简单来说,如果获取成功,数据将具有值,错误将为空,反之亦然。 ¥To put it simply, if fetch is successful, data will have a value and error will be null, and vice-versa. ::: tip 提示 错误信息被 `Error` 包裹,服务器返回的错误值可以通过 `Error.value` 检索。 ¥Error is wrapped with an `Error` with its value return from the server can be retrieve from `Error.value` ::: ### 基于状态的错误类型 {#error-type-based-on-status} ¥Error type based on status 如果你在 Elysia 服务器中明确指定了错误类型,Eden Treaty 和 Eden Fetch 都可以根据状态码缩小错误类型的范围。 ¥Both Eden Treaty and Eden Fetch can narrow down an error type based on status code if you explicitly provided an error type in the Elysia server. ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .model({ nendoroid: t.Object({ id: t.Number(), name: t.String() }), error: t.Object({ message: t.String() }) }) .get('/', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: 'nendoroid', response: { 200: 'nendoroid', // [!code ++] 400: 'error', // [!code ++] 401: 'error' // [!code ++] } }) .listen(3000) export type App = typeof app ``` 客户端: ¥An on the client side: ```typescript const { data: nendoroid, error } = app.mirror.post({ id: 1895, name: 'Skadi' }) if(error) { switch(error.status) { case 400: case 401: // narrow down to type 'error' described in the server warnUser(error.value) break default: // typed as unknown reportError(error.value) break } throw error } ``` ## WebSocket {#websocket} Eden 使用与普通路由相同的 API 支持 WebSocket。 ¥Eden supports WebSocket using the same API as a normal route. ```typescript // Server import { Elysia, t } from 'elysia' const app = new Elysia() .ws('/chat', { message(ws, message) { ws.send(message) }, body: t.String(), response: t.String() }) .listen(3000) type App = typeof app ``` 要开始监听实时数据,请调用 `.subscribe` 方法: ¥To start listening to real-time data, call the `.subscribe` method: ```typescript // Client import { edenTreaty } from '@elysiajs/eden' const app = edenTreaty('http://localhost:') const chat = app.chat.subscribe() chat.subscribe((message) => { console.log('got', message) }) chat.send('hello from client') ``` 我们可以使用 [schema](/integrations/cheat-sheet#schema) 在 WebSocket 上强制执行类型安全,就像普通路由一样。 ¥We can use [schema](/integrations/cheat-sheet#schema) to enforce type-safety on WebSockets, just like a normal route. *** Eden.subscribe 返回 EdenWebSocket,它扩展了 [WebSocket](https://web.nodejs.cn/en-US/docs/Web/API/WebSocket/WebSocket) 类,并实现了类型安全。语法与 WebSocket 相同 ¥**Eden.subscribe** returns **EdenWebSocket** which extends the [WebSocket](https://web.nodejs.cn/en-US/docs/Web/API/WebSocket/WebSocket) class with type-safety. The syntax is identical with the WebSocket 如果需要更多控制,可以访问 EdenWebSocket.raw 来与原生 WebSocket API 交互。 ¥If more control is need, **EdenWebSocket.raw** can be accessed to interact with the native WebSocket API. ## 文件上传 {#file-upload} ¥File Upload 你可以将以下任一参数传递给要附加文件的字段: ¥You may either pass one of the following to the field to attach file: * **文件** * **FileList** * **Blob** 附加文件会导致 content-type 变为 multipart/form-data。 ¥Attaching a file will results **content-type** to be **multipart/form-data** 假设我们的服务器如下: ¥Suppose we have the server as the following: ```typescript // server.ts import { Elysia } from 'elysia' const app = new Elysia() .post('/image', ({ body: { image, title } }) => title, { body: t.Object({ title: t.String(), image: t.Files(), }) }) .listen(3000) export type App = typeof app ``` 我们可以使用客户端,如下所示: ¥We may use the client as follows: ```typescript // client.ts import { edenTreaty } from '@elysia/eden' import type { Server } from './server' export const client = edenTreaty('http://localhost:3000') const id = (id: string) => document.getElementById(id)! as T const { data } = await client.image.post({ title: "Misono Mika", image: id('picture').files!, }) ``` --- --- url: 'https://elysiajs.com/eden/treaty/config.md' --- # 配置 {#config} ¥Config Eden Treaty 接受 2 个参数: ¥Eden Treaty accepts 2 parameters: * urlOrInstance - URL 端点或 Elysia 实例 * 选项(可选) - 自定义获取行为 ## urlOrInstance {#urlorinstance} 接受 URL 端点作为字符串或 Elysia 实例的字面值。 ¥Accept either URL endpoint as string or a literal Elysia instance. Eden 将根据类型更改行为,如下所示: ¥Eden will change the behavior based on type as follows: ### URL 端点(字符串) {#url-endpoint-string} ¥URL Endpoint (string) 如果传递了 URL 端点,Eden Treaty 将使用 `fetch` 或 `config.fetcher` 创建向 Elysia 实例的网络请求。 ¥If URL endpoint is passed, Eden Treaty will use `fetch` or `config.fetcher` to create a network request to an Elysia instance. ```typescript import { treaty } from '@elysiajs/eden' import type { App } from './server' const api = treaty('localhost:3000') ``` 你可以为 URL 端点指定或不指定协议。 ¥You may or may not specify a protocol for URL endpoint. Elysia 将自动附加端点,如下所示: ¥Elysia will append the endpoints automatically as follows: 1. 如果指定了协议,请直接使用 URL。 2. 如果 URL 是 localhost 且环境变量不是 production,请使用 http 3. 否则使用 https This also applies to Web Socket as well for determining between ws:// or wss://. *** ### Elysia 实例 {#elysia-instance} ¥Elysia Instance 如果传递了 Elysia 实例,Eden Treaty 将创建一个 `Request` 类并直接传递给 `Elysia.handle`,而无需创建网络请求。 ¥If Elysia instance is passed, Eden Treaty will create a `Request` class and pass to `Elysia.handle` directly without creating a network request. 这使我们能够直接与 Elysia 服务器交互,而无需请求开销或启动服务器。 ¥This allows us to interact with Elysia server directly without request overhead, or the need to start a server. ```typescript import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/hi', 'Hi Elysia') .listen(3000) const api = treaty(app) ``` 如果传递的是实例,则无需传递泛型,因为 Eden Treaty 可以直接从参数推断类型。 ¥If an instance is passed, generic is not needed to be passed as Eden Treaty can infer the type from a parameter directly. 建议使用此模式执行单元测试,或创建类型安全的反向代理服务器或微服务。 ¥This pattern is recommended for performing unit tests, or creating a type-safe reverse proxy server or micro-services. ## 选项 {#options} ¥Options Eden Treaty 的第二个可选参数用于自定义获取行为,接受以下参数: ¥2nd optional parameter for Eden Treaty to customize fetch behavior, accepting parameters as follows: * [fetch](#fetch) - 添加默认参数以进行获取初始化 (RequestInit) * [headers](#headers) - 定义默认标头 * [fetcher](#fetcher) - 自定义获取函数,例如 Axios,unfetch * [onRequest](#onrequest) - 在触发前拦截并修改获取请求 * [onResponse](#onresponse) - 拦截并修改获取的响应 ## 获取 {#fetch} ¥Fetch 默认参数附加到 fetch 的第二个参数,扩展了 Fetch.RequestInit 的类型。 ¥Default parameters append to 2nd parameters of fetch extends type of **Fetch.RequestInit**. ```typescript export type App = typeof app // [!code ++] import { treaty } from '@elysiajs/eden' // ---cut--- treaty('localhost:3000', { fetch: { credentials: 'include' } }) ``` 所有传递给 fetch 的参数都会传递给 fetcher,这相当于: ¥All parameters that are passed to fetch will be passed to fetcher, which is equivalent to: ```typescript fetch('http://localhost:3000', { credentials: 'include' }) ``` ## 标题 {#headers} ¥Headers 提供额外的默认获取标头,即 `options.fetch.headers` 的简写。 ¥Provide an additional default headers to fetch, a shorthand of `options.fetch.headers`. ```typescript treaty('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` 所有传递给 fetch 的参数都会传递给 fetcher,这相当于: ¥All parameters that passed to fetch, will be passed to fetcher, which is an equivalent to: ```typescript twoslash fetch('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` headers 可以接受以下参数: ¥headers may accept the following as parameters: * 对象 * 函数 ### 标题对象 {#headers-object} ¥Headers Object 如果传递了对象,则会直接传递给 fetch。 ¥If object is passed, then it will be passed to fetch directly ```typescript treaty('localhost:3000', { headers: { 'X-Custom': 'Griseo' } }) ``` ### 函数 {#function} ¥Function 你可以将 headers 指定为函数,以根据条件返回自定义 headers。 ¥You may specify headers as a function to return custom headers based on condition ```typescript treaty('localhost:3000', { headers(path, options) { if(path.startsWith('user')) return { authorization: 'Bearer 12345' } } }) ``` 你可以返回对象并将其值附加到获取标头中。 ¥You may return object to append its value to fetch headers. headers 函数接受 2 个参数: ¥headers function accepts 2 parameters: * `string` 路径 - 将发送给参数的路径 * 注意:主机名将被排除,例如: (/user/griseo) * `RequestInit` 选项:通过 fetch 的第二个参数传递的参数 ### 数组 {#array} ¥Array 如果需要多个条件,你可以将 headers 函数定义为数组。 ¥You may define a headers function as an array if multiple conditions are needed. ```typescript treaty('localhost:3000', { headers: [ (path, options) => { if(path.startsWith('user')) return { authorization: 'Bearer 12345' } } ] }) ``` 即使值已返回,Eden Treaty 也会运行所有函数。 ¥Eden Treaty will **run all functions** even if the value is already returned. ## 标题优先级 {#headers-priority} ¥Headers Priority 如果出现重复,Eden Treaty 将按以下方式优先处理请求头: ¥Eden Treaty will prioritize the order headers if duplicated as follows: 1. 内联方法 - 直接传入方法函数 2. headers - 传入 `config.headers` * 如果 `config.headers` 是数组,则优先处理后面的参数。 3. fetch - 传入 `config.fetch.headers` 例如,对于以下示例: ¥For example, for the following example: ```typescript const api = treaty('localhost:3000', { headers: { authorization: 'Bearer Aponia' } }) api.profile.get({ headers: { authorization: 'Bearer Griseo' } }) ``` 这将导致: ¥This will result in: ```typescript fetch('http://localhost:3000', { headers: { authorization: 'Bearer Griseo' } }) ``` 如果内联函数未指定标头,则结果将改为 "承载 Aponia"。 ¥If inline function doesn't specified headers, then the result will be "**Bearer Aponia**" instead. ## 获取器 {#fetcher} ¥Fetcher 提供自定义获取函数,而不是使用环境的默认获取函数。 ¥Provide a custom fetcher function instead of using an environment's default fetch. ```typescript treaty('localhost:3000', { fetcher(url, options) { return fetch(url, options) } }) ``` 如果你想使用 fetch 以外的其他客户端(例如 Axios 的 unfetch),建议替换 fetch。 ¥It's recommended to replace fetch if you want to use other client other than fetch, eg. Axios, unfetch. ## OnRequest {#onrequest} 在触发前拦截并修改获取请求。 ¥Intercept and modify fetch request before firing. 你可以返回对象并将其值附加到 RequestInit 中。 ¥You may return object to append the value to **RequestInit**. ```typescript treaty('localhost:3000', { onRequest(path, options) { if(path.startsWith('user')) return { headers: { authorization: 'Bearer 12345' } } } }) ``` 如果返回值,Eden Treaty 将对返回值和 `value.headers` 进行浅合并。 ¥If value is returned, Eden Treaty will perform a **shallow merge** for returned value and `value.headers`. onRequest 接受 2 个参数: ¥**onRequest** accepts 2 parameters: * `string` 路径 - 将发送给参数的路径 * 注意:主机名将被排除,例如: (/user/griseo) * `RequestInit` 选项:通过 fetch 的第二个参数传递的参数 ### 数组 {#array-1} ¥Array 如果需要多个条件,你可以将 onRequest 函数定义为数组。 ¥You may define an onRequest function as an array if multiples conditions are need. ```typescript treaty('localhost:3000', { onRequest: [ (path, options) => { if(path.startsWith('user')) return { headers: { authorization: 'Bearer 12345' } } } ] }) ``` 即使值已返回,Eden Treaty 也会运行所有函数。 ¥Eden Treaty will **run all functions** even if the value is already returned. ## onResponse {#onresponse} 拦截并修改获取的响应或返回新值。 ¥Intercept and modify fetch's response or return a new value. ```typescript treaty('localhost:3000', { onResponse(response) { if(response.ok) return response.json() } }) ``` onRequest 接受 1 个参数: ¥**onRequest** accepts 1 parameter: * 响应 `Response` - Web 标准响应通常从 `fetch` 返回 ### 数组 {#array-2} ¥Array 如果需要多个条件,你可以将 onResponse 函数定义为数组。 ¥You may define an onResponse function as an array if multiple conditions are need. ```typescript treaty('localhost:3000', { onResponse: [ (response) => { if(response.ok) return response.json() } ] }) ``` 与 [headers](#headers) 和 [onRequest](#onrequest) 不同,Eden Treaty 会循环遍历函数,直到找到返回值或抛出错误,返回值将用作新的响应。 ¥Unlike [headers](#headers) and [onRequest](#onrequest), Eden Treaty will loop through functions until a returned value is found or error thrown, the returned value will be use as a new response. --- --- url: 'https://elysiajs.com/eden/test.md' --- # Eden 测试 {#eden-test} ¥Eden Test 使用 Eden,我们可以创建具有端到端类型安全和自动补齐功能的集成测试。 ¥Using Eden, we can create an integration test with end-to-end type safety and auto-completion. > 使用 Eden Treaty 通过 [irvilerodrigues 在 Twitter 上](https://twitter.com/irvilerodrigues/status/1724836632300265926) 创建测试 ## 设置 {#setup} ¥Setup 我们可以使用 [Bun 测试](https://bun.sh/guides/test/watch-mode) 创建测试。 ¥We can use [Bun test](https://bun.sh/guides/test/watch-mode) to create tests. 在项目根目录下创建 test/index.test.ts 文件,并包含以下内容: ¥Create **test/index.test.ts** in the root of project directory with the following: ```typescript // test/index.test.ts import { describe, expect, it } from 'bun:test' import { edenTreaty } from '@elysiajs/eden' const app = new Elysia() .get('/', () => 'hi') .listen(3000) const api = edenTreaty('http://localhost:3000') describe('Elysia', () => { it('return a response', async () => { const { data } = await api.get() expect(data).toBe('hi') }) }) ``` 然后我们可以通过运行 bun test 进行测试。 ¥Then we can perform tests by running **bun test** ```bash bun test ``` 这使我们能够以编程方式执行集成测试,而无需手动获取,同时支持自动类型检查。 ¥This allows us to perform integration tests programmatically instead of manual fetch while supporting type checking automatically. --- --- url: 'https://elysiajs.com/eden/fetch.md' --- # Eden 获取 {#eden-fetch} ¥Eden Fetch 这是一个类似于 Eden Treaty 的替代方案。 ¥A fetch-like alternative to Eden Treaty. 借助 Eden Fetch,你可以使用 Fetch API 以类型安全的方式与 Elysia 服务器交互。 ¥With Eden Fetch, you can interact with Elysia server in a type-safe manner using Fetch API. *** 首先导出你现有的 Elysia 服务器类型: ¥First export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/hi', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app ``` 然后导入服务器类型,并在客户端使用 Elysia API: ¥Then import the server type, and consume the Elysia API on client: ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') // response type: 'Hi Elysia' const pong = await fetch('/hi', {}) // response type: 1895 const id = await fetch('/id/:id', { params: { id: '1895' } }) // response type: { id: 1895, name: 'Skadi' } const nendoroid = await fetch('/mirror', { method: 'POST', body: { id: 1895, name: 'Skadi' } }) ``` ## 错误处理 {#error-handling} ¥Error Handling 你可以像《Eden 条约》一样处理错误: ¥You can handle errors the same way as Eden Treaty: ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') // response type: { id: 1895, name: 'Skadi' } const { data: nendoroid, error } = await fetch('/mirror', { method: 'POST', body: { id: 1895, name: 'Skadi' } }) if(error) { switch(error.status) { case 400: case 401: throw error.value break case 500: case 502: throw error.value break default: throw error.value break } } const { id, name } = nendoroid ``` ## 何时应该使用 Eden Fetch 而不是 Eden Treaty {#when-should-i-use-eden-fetch-over-eden-treaty} ¥When should I use Eden Fetch over Eden Treaty 与 Elysia < 1.0 版本不同,Eden Fetch 不再比 Eden Treaty 更快。 ¥Unlike Elysia < 1.0, Eden Fetch is not faster than Eden Treaty anymore. 首选项基于你和你的团队的协议,但我们建议使用 [Eden 条约](/eden/treaty/overview)。 ¥The preference is base on you and your team agreement, however we recommend to use [Eden Treaty](/eden/treaty/overview) instead. 对于 Elysia < 1.0: ¥For Elysia < 1.0: 使用 Eden Treaty 需要大量的底层迭代才能一次性映射所有可能的类型,而 Eden Fetch 则可以延迟执行,直到你选择合适的路径。 ¥Using Eden Treaty requires a lot of down-level iteration to map all possible types in a single go, while in contrast, Eden Fetch can be lazily executed until you pick a route. 由于类型复杂且服务器路由众多,在低端开发设备上使用 Eden Treaty 会导致类型推断和自动补齐速度缓慢。 ¥With complex types and a lot of server routes, using Eden Treaty on a low-end development device can lead to slow type inference and auto-completion. 但是由于 Elysia 对许多类型和推断进行了调整和优化,Eden Treaty 在相当多的路由上表现非常出色。 ¥But as Elysia has tweaked and optimized a lot of types and inference, Eden Treaty can perform very well in the considerable amount of routes. 如果你的单个进程包含超过 500 条路由,并且你需要在单个前端代码库中使用所有路由,那么你可能需要使用 Eden Fetch,因为它的 TypeScript 性能明显优于 Eden Treaty。 ¥If your single process contains **more than 500 routes**, and you need to consume all of the routes **in a single frontend codebase**, then you might want to use Eden Fetch as it has a significantly better TypeScript performance than Eden Treaty. --- --- url: 'https://elysiajs.com/plugins/graphql-yoga.md' --- # GraphQL Yoga 插件 {#graphql-yoga-plugin} ¥GraphQL Yoga Plugin 此插件将 GraphQL yoga 与 Elysia 集成。 ¥This plugin integrates GraphQL yoga with Elysia 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/graphql-yoga ``` 然后使用它: ¥Then use it: ```typescript import { Elysia } from 'elysia' import { yoga } from '@elysiajs/graphql-yoga' const app = new Elysia() .use( yoga({ typeDefs: /* GraphQL */ ` type Query { hi: String } `, resolvers: { Query: { hi: () => 'Hello from Elysia' } } }) ) .listen(3000) ``` 在浏览器中访问 `/graphql`(GET 请求)将显示一个支持 GraphQL 的 Elysia 服务器的 GraphiQL 实例。 ¥Accessing `/graphql` in the browser (GET request) would show you a GraphiQL instance for the GraphQL-enabled Elysia server. 可选:你还可以安装自定义版本的可选对等依赖: ¥optional: you can install a custom version of optional peer dependencies as well: ```bash bun add graphql graphql-yoga ``` ## 解析器 {#resolver} ¥Resolver Elysia 使用 [Mobius](https://github.com/saltyaom/mobius) 自动从 typeDefs 字段推断类型,使你在输入解析器类型时获得完全的类型安全性和自动补齐功能。 ¥Elysia uses [Mobius](https://github.com/saltyaom/mobius) to infer type from **typeDefs** field automatically, allowing you to get full type-safety and auto-complete when typing **resolver** types. ## 上下文 {#context} ¥Context 你可以通过添加 context 为解析器函数添加自定义上下文。 ¥You can add custom context to the resolver function by adding **context** ```ts import { Elysia } from 'elysia' import { yoga } from '@elysiajs/graphql-yoga' const app = new Elysia() .use( yoga({ typeDefs: /* GraphQL */ ` type Query { hi: String } `, context: { name: 'Mobius' }, // If context is a function on this doesn't present // for some reason it won't infer context type useContext(_) {}, resolvers: { Query: { hi: async (parent, args, context) => context.name } } }) ) .listen(3000) ``` ## 配置 {#config} ¥Config 此插件扩展了 [GraphQL Yoga 的 createYoga 选项,请参阅 GraphQL Yoga 文档](https://the-guild.dev/graphql/yoga-server/docs),并将 `schema` 配置内联到 root。 ¥This plugin extends [GraphQL Yoga's createYoga options, please refer to the GraphQL Yoga documentation](https://the-guild.dev/graphql/yoga-server/docs) with inlining `schema` config to root. 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### path {#path} @default `/graphql` 用于公开 GraphQL 处理程序的端点 ¥Endpoint to expose GraphQL handler --- --- url: 'https://elysiajs.com/plugins/html.md' --- # HTML 插件 {#html-plugin} ¥HTML Plugin 允许你使用带有适当标头和支持的 [JSX](#jsx) 和 HTML。 ¥Allows you to use [JSX](#jsx) and HTML with proper headers and support. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/html ``` 然后使用它: ¥Then use it: ```tsx twoslash import React from 'react' // ---cut--- import { Elysia } from 'elysia' import { html, Html } from '@elysiajs/html' new Elysia() .use(html()) .get( '/html', () => ` Hello World

Hello World

` ) .get('/jsx', () => ( Hello World

Hello World

)) .listen(3000) ``` 此插件会自动将 `Content-Type: text/html; charset=utf8` 标头添加到响应中,添加 ``,并将其转换为 Response 对象。 ¥This plugin will automatically add `Content-Type: text/html; charset=utf8` header to the response, add ``, and convert it into a Response object. ## JSX {#jsx} Elysia HTML 基于 [@kitajs/html](https://github.com/kitajs/html),允许我们在编译时将 JSX 转换为字符串,以实现高性能。 ¥Elysia HTML is based on [@kitajs/html](https://github.com/kitajs/html) allowing us to define JSX to string in compile time to achieve high performance. 将需要使用 JSX 的文件命名为 "x" 后缀: ¥Name your file that needs to use JSX to end with affix **"x"**: * .js -> .jsx * .ts -> .tsx 要注册 TypeScript 类型,请将以下内容附加到 tsconfig.json 中: ¥To register the TypeScript type, please append the following to **tsconfig.json**: ```jsonc // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Html.createElement", "jsxFragmentFactory": "Html.Fragment" } } ``` 就是这样,现在你可以使用 JSX 作为模板引擎了: ¥That's it, now you can use JSX as your template engine: ```tsx twoslash import React from 'react' // ---cut--- import { Elysia } from 'elysia' import { html, Html } from '@elysiajs/html' // [!code ++] new Elysia() .use(html()) // [!code ++] .get('/', () => ( Hello World

Hello World

)) .listen(3000) ``` 如果出现 `Cannot find name 'Html'. Did you mean 'html'?` 错误,则必须将以下导入添加到 JSX 模板中: ¥If the error `Cannot find name 'Html'. Did you mean 'html'?` occurs, this import must be added to the JSX template: ```tsx import { Html } from '@elysiajs/html' ``` 务必使用大写字母。 ¥It is important that it is written in uppercase. ## XSS {#xss} Elysia HTML 基于 Kita HTML 插件,可在编译时检测可能的 XSS 攻击。 ¥Elysia HTML is based use of the Kita HTML plugin to detect possible XSS attacks in compile time. 你可以使用专用的 `safe` 属性来过滤用户值,以防止 XSS 漏洞。 ¥You can use a dedicated `safe` attribute to sanitize user value to prevent XSS vulnerability. ```tsx import { Elysia, t } from 'elysia' import { html, Html } from '@elysiajs/html' new Elysia() .use(html()) .post( '/', ({ body }) => ( Hello World

{body}

), { body: t.String() } ) .listen(3000) ``` 但是,在构建大型应用时,最好使用类型提醒来检测代码库中可能存在的 XSS 漏洞。 ¥However, when are building a large-scale app, it's best to have a type reminder to detect possible XSS vulnerabilities in your codebase. 要添加类型安全提醒,请安装: ¥To add a type-safe reminder, please install: ```sh bun add @kitajs/ts-html-plugin ``` 然后附加以下 tsconfig.json 文件 ¥Then appends the following **tsconfig.json** ```jsonc // tsconfig.json { "compilerOptions": { "jsx": "react", "jsxFactory": "Html.createElement", "jsxFragmentFactory": "Html.Fragment", "plugins": [{ "name": "@kitajs/ts-html-plugin" }] } } ``` ## 选项 {#options} ¥Options ### contentType {#contenttype} * 类型:`string` * 默认:`'text/html; charset=utf8'` 响应的内容类型。 ¥The content-type of the response. ### autoDetect {#autodetect} * 类型:`boolean` * 默认:`true` 是否自动检测 HTML 内容并设置内容类型。 ¥Whether to automatically detect HTML content and set the content-type. ### autoDoctype {#autodoctype} * 类型:`boolean | 'full'` * 默认:`true` 如果未找到以 `` 开头的响应,是否自动添加 ``。 ¥Whether to automatically add `` to a response starting with ``, if not found. 使用 `full` 可在未使用此插件返回的响应中自动添加文档类型 ¥Use `full` to also automatically add doctypes on responses returned without this plugin ```ts // without the plugin app.get('/', () => '') // With the plugin app.get('/', ({ html }) => html('')) ``` ### isHtml {#ishtml} * 类型:`(value: string) => boolean` * 默认:`isHtml`(导出函数) 该函数用于检测字符串是否为 HTML 格式。如果长度大于 7,则默认实现以 `<` 开头,以 `>` 结尾。 ¥The function is used to detect if a string is a html or not. Default implementation if length is greater than 7, starts with `<` and ends with `>`. 请记住,没有真正有效的 HTML 验证方法,因此默认实现只是最佳猜测。 ¥Keep in mind there's no real way to validate HTML, so the default implementation is a best guess. --- --- url: 'https://elysiajs.com/plugins/jwt.md' --- # JWT 插件 {#jwt-plugin} ¥JWT Plugin 此插件添加了在 Elysia 处理程序中使用 JWT 的支持。 ¥This plugin adds support for using JWT in Elysia handlers. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/jwt ``` 然后使用它: ¥Then use it: ::: code-group ```typescript [cookie] import { Elysia } from 'elysia' import { jwt } from '@elysiajs/jwt' const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'Fischl von Luftschloss Narfidort' }) ) .get('/sign/:name', async ({ jwt, params: { name }, cookie: { auth } }) => { const value = await jwt.sign({ name }) auth.set({ value, httpOnly: true, maxAge: 7 * 86400, path: '/profile', }) return `Sign in as ${value}` }) .get('/profile', async ({ jwt, status, cookie: { auth } }) => { const profile = await jwt.verify(auth.value) if (!profile) return status(401, 'Unauthorized') return `Hello ${profile.name}` }) .listen(3000) ``` ```typescript [headers] import { Elysia } from 'elysia' import { jwt } from '@elysiajs/jwt' const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'Fischl von Luftschloss Narfidort' }) ) .get('/sign/:name', ({ jwt, params: { name } }) => { return jwt.sign({ name }) }) .get('/profile', async ({ jwt, error, headers: { authorization } }) => { const profile = await jwt.verify(authorization) if (!profile) return status(401, 'Unauthorized') return `Hello ${profile.name}` }) .listen(3000) ``` ::: ## 配置 {#config} ¥Config 此插件扩展了 [jose](https://github.com/panva/jose) 的配置。 ¥This plugin extends config from [jose](https://github.com/panva/jose). 以下是插件接受的配​​置。 ¥Below is a config that is accepted by the plugin. ### name {#name} 将 `jwt` 函数注册为的名称。 ¥Name to register `jwt` function as. 例如,`jwt` 函数将使用自定义名称注册。 ¥For example, `jwt` function will be registered with a custom name. ```typescript app .use( jwt({ name: 'myJWTNamespace', secret: process.env.JWT_SECRETS! }) ) .get('/sign/:name', ({ myJWTNamespace, params }) => { return myJWTNamespace.sign(params) }) ``` 由于某些开发者可能需要在单个服务器中使用具有不同配置的多个 `jwt`,因此需要使用不同的名称显式注册 JWT 函数。 ¥Because some might need to use multiple `jwt` with different configs in a single server, explicitly registering the JWT function with a different name is needed. ### secret {#secret} 用于签署 JWT 有效负载的私钥。 ¥The private key to sign JWT payload with. ### schema {#schema} 对 JWT 负载进行严格类型验证。 ¥Type strict validation for JWT payload. *** 以下是从 [cookie](https://npmjs.com/package/cookie) 扩展的配置。 ¥Below is a config that extends from [cookie](https://npmjs.com/package/cookie) ### alg {#alg} @default `HS256` 用于签名 JWT 有效负载的签名算法。 ¥Signing Algorithm to sign JWT payload with. jose 的可能属性包括:HS256 HS384 HS512 PS256 PS384 PS512 RS256 RS384 RS512 ES256 ES256K ES384 ES512 EdDSA ¥Possible properties for jose are: HS256 HS384 HS512 PS256 PS384 PS512 RS256 RS384 RS512 ES256 ES256K ES384 ES512 EdDSA ### iss {#iss} 颁发者声明根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1) 标识颁发 JWT 的主体 ¥The issuer claim identifies the principal that issued the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.1) TLDR;通常是签名者的(域名)。 ¥TLDR; is usually (the domain) name of the signer. ### sub {#sub} 主体声明标识了 JWT 的主体。 ¥The subject claim identifies the principal that is the subject of the JWT. JWT 中的声明通常是根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2) 定义的关于主题的陈述。 ¥The claims in a JWT are normally statements about the subject as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.2) ### aud {#aud} 受众声明标识了 JWT 的目标接收者。 ¥The audience claim identifies the recipients that the JWT is intended for. 每个打算处理 JWT 的主体都必须根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) 在受众声明中使用一个值来标识自己。 ¥Each principal intended to process the JWT MUST identify itself with a value in the audience claim as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.3) ### jti {#jti} JWT ID 声明根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7) 为 JWT 提供唯一标识符 ¥JWT ID claim provides a unique identifier for the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.7) ### nbf {#nbf} "不在此之前" 声明标识根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5) 规定,在此时间之前不得接受 JWT 进行处理。 ¥The "not before" claim identifies the time before which the JWT must not be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.5) ### exp {#exp} 到期时间声明标识了 JWT 的到期时间或之后,根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4) 规定,JWT 不得被接受处理。 ¥The expiration time claim identifies the expiration time on or after which the JWT MUST NOT be accepted for processing as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.4) ### iat {#iat} "发布于" 声明标识 JWT 的签发时间。 ¥The "issued at" claim identifies the time at which the JWT was issued. 此声明可用于根据 [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) 确定 JWT 的生存期。 ¥This claim can be used to determine the age of the JWT as per [RFC7519](https://www.rfc-editor.org/rfc/rfc7519#section-4.1.6) ### b64 {#b64} 此 JWS 扩展标头参数根据 [RFC7797](https://www.rfc-editor.org/rfc/rfc7797) 修改了 JWS 有效负载表示和 JWS 签名输入计算。 ¥This JWS Extension Header Parameter modifies the JWS Payload representation and the JWS Signing input computation as per [RFC7797](https://www.rfc-editor.org/rfc/rfc7797). ### kid {#kid} 指示用于保护 JWS 的密钥的提示。 ¥A hint indicating which key was used to secure the JWS. 此参数允许发起者根据 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) 明确向接收者发出密钥更改信号。 ¥This parameter allows originators to explicitly signal a change of key to recipients as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.4) ### x5t {#x5t} (X.509 证书 SHA-1 指纹) 标头参数是一个 base64url 编码的 SHA-1 摘要,该摘要是对 X.509 证书 [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) 的 DER 编码进行编码,该证书或证书链与用于按照 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7) 对 JWS 进行数字签名的密钥相对应 ¥(X.509 certificate SHA-1 thumbprint) header parameter is a base64url-encoded SHA-1 digest of the DER encoding of the X.509 certificate [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.7) ### x5c {#x5c} (X.509 证书链) 标头参数包含用于按照 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6) 对 JWS 进行数字签名的密钥对应的 X.509 公钥证书或证书链 [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) ¥(X.509 certificate chain) header parameter contains the X.509 public key certificate or certificate chain [RFC5280](https://www.rfc-editor.org/rfc/rfc5280) corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.6) ### x5u {#x5u} (X.509 URL) 标头参数是一个 URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986),它指向一个 X.509 公钥证书或证书链 \[RFC5280] 资源,该证书或证书链与用于按照 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5) 对 JWS 进行数字签名的密钥相对应 ¥(X.509 URL) header parameter is a URI [RFC3986](https://www.rfc-editor.org/rfc/rfc3986) that refers to a resource for the X.509 public key certificate or certificate chain \[RFC5280] corresponding to the key used to digitally sign the JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.5) ### jwk {#jwk} "jku"(JWK 设置 URL)标头参数是一个 URI \[RFC3986],它指向一组 JSON 编码公钥的资源,其中一个公钥对应于用于对 JWS 进行数字签名的密钥。 ¥The "jku" (JWK Set URL) Header Parameter is a URI \[RFC3986] that refers to a resource for a set of JSON-encoded public keys, one of which corresponds to the key used to digitally sign the JWS. 密钥必须根据 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2) 编码为 JWK 集 \[JWK] ¥The keys MUST be encoded as a JWK Set \[JWK] as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.2) ### typ {#typ} JWS 应用使用 `typ`(类型)标头参数来声明此完整 JWS 的媒体类型 \[IANA.MediaTypes]。 ¥The `typ` (type) Header Parameter is used by JWS applications to declare the media type \[IANA.MediaTypes] of this complete JWS. 这旨在供应用使用,当根据 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) 可以包含 JWS 的应用数据结构中可能存在多种类型的对象时。 ¥This is intended for use by the application when more than one kind of object could be present in an application data structure that can contain a JWS as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) ### ctr {#ctr} JWS 应用使用 Content-Type 参数来声明受保护内容(有效负载)的媒体类型 \[IANA.MediaTypes]。 ¥Content-Type parameter is used by JWS applications to declare the media type \[IANA.MediaTypes] of the secured content (the payload). 这旨在供应用使用,当根据 [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) 可以包含 JWS 有效负载中可能存在多种类型的对象时。 ¥This is intended for use by the application when more than one kind of object could be present in the JWS Payload as per [RFC7515](https://www.rfc-editor.org/rfc/rfc7515#section-4.1.9) ## 处理程序 {#handler} ¥Handler 以下是添加到处理程序的值。 ¥Below are the value added to the handler. ### jwt.sign {#jwtsign} JWT 插件注册的与 JWT 一起使用的动态集合对象。 ¥A dynamic object of collection related to use with JWT registered by the JWT plugin. 类型: ¥Type: ```typescript sign: (payload: JWTPayloadSpec): Promise ``` `JWTPayloadSpec` 接受与 [JWT 配置](#config) 相同的值 ¥`JWTPayloadSpec` accepts the same value as [JWT config](#config) ### jwt.verify {#jwtverify} 使用提供的 JWT 配置验证有效负载 ¥Verify payload with the provided JWT config 类型: ¥Type: ```typescript verify(payload: string) => Promise ``` `JWTPayloadSpec` 接受与 [JWT 配置](#config) 相同的值 ¥`JWTPayloadSpec` accepts the same value as [JWT config](#config) ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. ## 设置 JWT 过期日期 {#set-jwt-expiration-date} ¥Set JWT expiration date 默认情况下,配置将传递给 `setCookie` 并继承其值。 ¥By default, the config is passed to `setCookie` and inherits its value. ```typescript const app = new Elysia() .use( jwt({ name: 'jwt', secret: 'kunikuzushi', exp: '7d' }) ) .get('/sign/:name', async ({ jwt, params }) => jwt.sign(params)) ``` 这将对 JWT 进行签名,其有效期为未来 7 天。 ¥This will sign JWT with an expiration date of the next 7 days. --- --- url: 'https://elysiajs.com/patterns/openapi.md' --- # OpenAPI {#openapi} Elysia 提供一流的支持,并默认遵循 OpenAPI 架构。 ¥Elysia has first-class support and follows OpenAPI schema by default. Elysia 可以使用 OpenAPI 插件自动生成 API 文档页面。 ¥Elysia can automatically generate an API documentation page by using an OpenAPI plugin. 要生成 Swagger 页面,请安装以下插件: ¥To generate the Swagger page, install the plugin: ```bash bun add @elysiajs/openapi ``` 将插件注册到服务器: ¥And register the plugin to the server: ```typescript import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' // [!code ++] new Elysia() .use(openapi()) // [!code ++] ``` 默认情况下,Elysia 使用 OpenAPI V3 模式和 [Scalar UI](http://scalar.com)。 ¥By default, Elysia uses OpenAPI V3 schema and [Scalar UI](http://scalar.com) 有关 OpenAPI 插件配置,请参阅 [OpenAPI 插件页面](/plugins/openapi)。 ¥For OpenAPI plugin configuration, see the [OpenAPI plugin page](/plugins/openapi). ## 从类型生成 OpenAPI {#openapi-from-types} ¥OpenAPI from types > 这是可选的,但我们强烈建议你使用它,以获得更好的文档体验。 默认情况下,Elysia 依赖运行时模式来生成 OpenAPI 文档。 ¥By default, Elysia relies on runtime schema to generate OpenAPI documentation. 但是,你也可以使用 OpenAPI 插件中的生成器,根据类型生成 OpenAPI 文档,如下所示: ¥However, you can also generate OpenAPI documentation from types by using a generator from OpenAPI plugin as follows: 1. 指定项目的根文件(通常为 `src/index.ts`),并导出一个实例。 2. 导入一个生成器并提供从项目根目录到类型生成器的文件路径 ```ts import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { fromTypes } from '@elysiajs/openapi/gen' // [!code ++] export const app = new Elysia() // [!code ++] .use( openapi({ references: fromTypes('src/index.ts') // [!code ++] }) ) .get('/', { test: 'hello' as const }) .post('/json', ({ body, status }) => body, { body: t.Object({ hello: t.String() }) }) .listen(3000) ``` Elysia 将尝试通过读取导出实例的类型来生成 OpenAPI 文档。 ¥Elysia will attempt to generate OpenAPI documentation by reading the type of an exported instance to generate OpenAPI documentation. 这将与运行时模式共存,并且运行时模式将优先于类型定义。 ¥This will co-exists with the runtime schema, and the runtime schema will take precedence over the type definition. ### 生产 {#production} ¥Production 在生产环境中,你可能会将 Elysia 编译为 [带 Bun 的单个可执行文件](/patterns/deploy.html) 或 [打包成单个 JavaScript 文件](https://elysia.nodejs.cn/patterns/deploy.html#compile-to-javascript)。 ¥In production environment, it's likely that you might compile Elysia to a [single executable with Bun](/patterns/deploy.html) or [bundle into a single JavaScript file](https://elysia.nodejs.cn/patterns/deploy.html#compile-to-javascript). 建议你预先生成声明文件 (.d.ts),以便为生成器提供类型声明。 ¥It's recommended that you should pre-generate the declaration file (**.d.ts**) to provide type declaration to the generator. ```ts import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { fromTypes } from '@elysiajs/openapi/gen' const app = new Elysia() .use( openapi({ references: fromTypes( process.env.NODE_ENV === 'production' // [!code ++] ? 'dist/index.d.ts' // [!code ++] : 'src/index.ts' // [!code ++] ) }) ) ``` ### 注意事项:根路径 {#caveats-root-path} ¥Caveats: Root path 由于猜测项目根目录不可靠,因此建议提供项目根目录的路径,以确保生成器能够正确运行,尤其是在使用 monorepo 时。 ¥As it's unreliable to guess to root of the project, it's recommended to provide the path to the project root to allow generator to run correctly, especially when using monorepo. ```ts import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { fromTypes } from '@elysiajs/openapi/gen' export const app = new Elysia() .use( openapi({ references: fromTypes('src/index.ts', { projectRoot: path.join('..', import.meta.dir) // [!code ++] }) }) ) .get('/', { test: 'hello' as const }) .post('/json', ({ body, status }) => body, { body: t.Object({ hello: t.String() }) }) .listen(3000) ``` ### 自定义 tsconfig.json {#custom-tsconfigjson} ¥Custom tsconfig.json 如果你有多个 `tsconfig.json` 文件,请务必指定一个正确的 `tsconfig.json` 文件用于类型生成。 ¥If you have multiple `tsconfig.json` files, it's important that you must specify a correct `tsconfig.json` file to be used for type generation. ```ts import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { fromTypes } from '@elysiajs/openapi/gen' export const app = new Elysia() .use( openapi({ references: fromTypes('src/index.ts', { // This is reference from root of the project tsconfigPath: 'tsconfig.dts.json' // [!code ++] }) }) ) .get('/', { test: 'hello' as const }) .post('/json', ({ body, status }) => body, { body: t.Object({ hello: t.String() }) }) .listen(3000) ``` ## OpenAPI 的标准 Schema {#standard-schema-with-openapi} ¥Standard Schema with OpenAPI Elysia 将尝试使用每个架构中的原生方法转换为 OpenAPI 架构。 ¥Elysia will try to use a native method from each schema to convert to OpenAPI schema. 但是,如果架构未提供原生方法,你可以通过提供 `mapJsonSchema` 来向 OpenAPI 提供自定义架构,如下所示: ¥However, if the schema doesn't provide a native method, you can provide a custom schema to OpenAPI by providing a `mapJsonSchema` as follows: \ ### Zod OpenAPI {#zod-openapi} 由于 Zod 的架构没有 `toJSONSchema` 方法,我们需要提供一个自定义映射器将 Zod 架构转换为 OpenAPI 架构。 ¥As Zod doesn't have a `toJSONSchema` method on the schema, we need to provide a custom mapper to convert Zod schema to OpenAPI schema. ::: code-group ```typescript [Zod 4] import openapi from '@elysiajs/openapi' import * as z from 'zod' openapi({ mapJsonSchema: { zod: z.toJSONSchema } }) ``` ```typescript [Zod 3] import openapi from '@elysiajs/openapi' import { zodToJsonSchema } from 'zod-to-json-schema' openapi({ mapJsonSchema: { zod: zodToJsonSchema } }) ``` ::: ### Valibot OpenAPI {#valibot-openapi} Valibot 使用单独的包 (`@valibot/to-json-schema`) 将 Valibot Schema 转换为 JSON Schema。 ¥Valibot use a separate package (`@valibot/to-json-schema`) to convert Valibot schema to JSON Schema. ```typescript import openapi from '@elysiajs/openapi' import { toJsonSchema } from '@valibot/to-json-schema' openapi({ mapJsonSchema: { valibot: toJsonSchema } }) ``` ### OpenAPI 效果 {#effect-openapi} ¥Effect OpenAPI 由于 Effect 在架构上没有 `toJSONSchema` 方法,我们需要提供一个自定义映射器,将 Effect 架构转换为 OpenAPI 架构。 ¥As Effect doesn't have a `toJSONSchema` method on the schema, we need to provide a custom mapper to convert Effect schema to OpenAPI schema. ```typescript import openapi from '@elysiajs/openapi' import { JSONSchema } from 'effect' openapi({ mapJsonSchema: { effect: JSONSchema.make } }) ``` ## 描述路线 {#describing-route} ¥Describing route 我们可以通过提供模式类型来添加路由信息。 ¥We can add route information by providing a schema type. 然而,有时仅定义类型并不能清楚地说明路由可能做什么。你可以使用 [detail](/plugins/openapi#detail) 字段明确描述路由。 ¥However, sometimes defining only a type does not make it clear what the route might do. You can use [detail](/plugins/openapi#detail) fields to explicitly describe the route. ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use(openapi()) .post( '/sign-in', ({ body }) => body, { body: t.Object( { username: t.String(), password: t.String({ minLength: 8, description: 'User password (at least 8 characters)' // [!code ++] }) }, { // [!code ++] description: 'Expected a username and password' // [!code ++] } // [!code ++] ), detail: { // [!code ++] summary: 'Sign in the user', // [!code ++] tags: ['authentication'] // [!code ++] } // [!code ++] }) ``` detail 字段遵循 OpenAPI V3 定义,默认具有自动补全和类型安全功能。 ¥The detail fields follows an OpenAPI V3 definition with auto-completion and type-safety by default. 然后将详细信息传递给 OpenAPI,并将描述放入 OpenAPI 路由。 ¥Detail is then passed to OpenAPI to put the description to OpenAPI route. ## 响应头 {#response-headers} ¥Response headers 我们可以通过使用 `withHeader` 封装模式来添加响应头: ¥We can add a response headers by wrapping a schema with `withHeader`: ```typescript import { Elysia, t } from 'elysia' import { openapi, withHeader } from '@elysiajs/openapi' // [!code ++] new Elysia() .use(openapi()) .get( '/thing', ({ body, set }) => { set.headers['x-powered-by'] = 'Elysia' return body }, { response: withHeader( // [!code ++] t.Literal('Hi'), // [!code ++] { // [!code ++] 'x-powered-by': t.Literal('Elysia') // [!code ++] } // [!code ++] ) // [!code ++] } ) ``` 请注意,`withHeader` 只是一个注解,并不强制或验证实际的响应头。你需要手动设置 headers。 ¥Note that `withHeader` is an annotation only, and does not enforce or validate the actual response headers. You need to set the headers manually. ### 隐藏路线 {#hide-route} ¥Hide route 你可以通过将 `detail.hide` 设置为 `true` 来隐藏 Swagger 页面中的路由。 ¥You can hide the route from the Swagger page by setting `detail.hide` to `true` ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use(openapi()) .post( '/sign-in', ({ body }) => body, { body: t.Object( { username: t.String(), password: t.String() }, { description: 'Expected a username and password' } ), detail: { // [!code ++] hide: true // [!code ++] } // [!code ++] } ) ``` ## 标签 {#tags} ¥Tags Elysia 可以使用 Swaggers 标签系统将端点分组。 ¥Elysia can separate the endpoints into groups by using the Swaggers tag system 首先,在 Swagger 配置对象中定义可用的标签 ¥Firstly define the available tags in the swagger config object ```typescript new Elysia().use( openapi({ documentation: { tags: [ { name: 'App', description: 'General endpoints' }, { name: 'Auth', description: 'Authentication endpoints' } ] } }) ) ``` 然后使用端点配置部分的 details 属性将该端点分配给组 ¥Then use the details property of the endpoint configuration section to assign that endpoint to the group ```typescript new Elysia() .get('/', () => 'Hello Elysia', { detail: { tags: ['App'] } }) .group('/auth', (app) => app.post( '/sign-up', ({ body }) => db.user.create({ data: body, select: { id: true, username: true } }), { detail: { tags: ['Auth'] } } ) ) ``` 将生成如下所示的 Swagger 页面 ¥Which will produce a swagger page like the following ### 标签组 {#tags-group} ¥Tags group Elysia 可以接受标签,将整个实例或一组路由添加到特定标签。 ¥Elysia may accept tags to add an entire instance or group of routes to a specific tag. ```typescript import { Elysia, t } from 'elysia' new Elysia({ tags: ['user'] }) .get('/user', 'user') .get('/admin', 'admin') ``` ## 模型 {#models} ¥Models 通过使用 [参考模型](/essential/validation.html#reference-model),Elysia 将自动处理模式生成。 ¥By using [reference model](/essential/validation.html#reference-model), Elysia will handle the schema generation automatically. 通过将模型分离为专用部分并通过引用链接。 ¥By separating models into a dedicated section and linked by reference. ```typescript new Elysia() .model({ User: t.Object({ id: t.Number(), username: t.String() }) }) .get('/user', () => ({ id: 1, username: 'saltyaom' }), { response: { 200: 'User' }, detail: { tags: ['User'] } }) ``` ## 守护 {#guard} ¥Guard 或者,Elysia 可以接受守卫,将整个实例或一组路由添加到特定的守卫。 ¥Alternatively, Elysia may accept guards to add an entire instance or group of routes to a specific guard. ```typescript import { Elysia, t } from 'elysia' new Elysia() .guard({ detail: { description: 'Require user to be logged in' } }) .get('/user', 'user') .get('/admin', 'admin') ``` ## 变更 OpenAPI 端点 {#change-openapi-endpoint} ¥Change OpenAPI Endpoint 你可以通过在插件配置中设置 [path](#path) 来更改 OpenAPI 端点。 ¥You can change the OpenAPI endpoint by setting [path](#path) in the plugin config. ```typescript twoslash import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use( openapi({ path: '/v2/openapi' }) ) .listen(3000) ``` ## 自定义 OpenAPI 信息 {#customize-openapi-info} ¥Customize OpenAPI info 我们可以通过在插件配置中设置 [documentation.info](#documentationinfo) 来自定义 OpenAPI 信息。 ¥We can customize the OpenAPI information by setting [documentation.info](#documentationinfo) in the plugin config. ```typescript twoslash import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use( openapi({ documentation: { info: { title: 'Elysia Documentation', version: '1.0.0' } } }) ) .listen(3000) ``` 这在以下情况下很有用: ¥This can be useful for * 添加标题 * 设置 API 版本 * 添加了解释 API 用途的描述 * 解释可用的标签以及每个标签的含义 ## 安全配置 {#security-configuration} ¥Security Configuration 要保护你的 API 端点,你可以在 Swagger 配置中定义安全方案。以下示例演示了如何使用 Bearer Authentication (JWT) 保护你的端点: ¥To secure your API endpoints, you can define security schemes in the Swagger configuration. The example below demonstrates how to use Bearer Authentication (JWT) to protect your endpoints: ```typescript new Elysia().use( openapi({ documentation: { components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } } } }) ) export const addressController = new Elysia({ prefix: '/address', detail: { tags: ['Address'], security: [ { bearerAuth: [] } ] } }) ``` 这将确保 `/address` 前缀下的所有端点都需要有效的 JWT 令牌才能访问。 ¥This will ensures that all endpoints under the `/address` prefix require a valid JWT token for access. --- --- url: 'https://elysiajs.com/plugins/openapi.md' --- # OpenAPI 插件 {#openapi-plugin} ¥OpenAPI Plugin 用于自动生成 API 文档页面的 [elysia](https://github.com/elysiajs/elysia) 插件。 ¥Plugin for [elysia](https://github.com/elysiajs/elysia) to auto-generate API documentation page. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/openapi ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use(openapi()) .get('/', () => 'hello') .post('/hello', () => 'OpenAPI') .listen(3000) ``` 访问 `/openapi` 将显示一个 Scalar UI,其中包含 Elysia 服务器生成的端点文档。你还可以访问 `/openapi/json` 处的原始 OpenAPI 规范。 ¥Accessing `/openapi` would show you a Scalar UI with the generated endpoint documentation from the Elysia server. You can also access the raw OpenAPI spec at `/openapi/json`. ::: tip 提示 本页为插件配置参考。 ¥This page is the plugin configuration reference. 如果你正在寻找常见模式或 OpenAPI 的高级用法,请查看 [模式:OpenAPI](/patterns/openapi)。 ¥If you're looking for a common patterns or an advanced usage of OpenAPI, check out [Patterns: OpenAPI](/patterns/openapi) ::: ## 详情 {#detail} ¥Detail `detail` 扩展了 [OpenAPI 操作对象](https://spec.openapis.org/oas/v3.0.3.html#operation-object) ¥`detail` extends the [OpenAPI Operation Object](https://spec.openapis.org/oas/v3.0.3.html#operation-object) detail 字段是一个对象,可用于描述 API 文档路由的信息。 ¥The detail field is an object that can be used to describe information about the route for API documentation. 它可能包含以下字段: ¥It may contain the following fields: ## detail.hide {#detailhide} 你可以通过将 `detail.hide` 设置为 `true` 来隐藏 Swagger 页面中的路由。 ¥You can hide the route from the Swagger page by setting `detail.hide` to `true` ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia().use(openapi()).post('/sign-in', ({ body }) => body, { body: t.Object( { username: t.String(), password: t.String() }, { description: 'Expected a username and password' } ), detail: { // [!code ++] hide: true // [!code ++] } // [!code ++] }) ``` ### detail.deprecated {#detaildeprecated} 声明此操作已弃用。用户应避免使用声明的操作。默认值为 `false`。 ¥Declares this operation to be deprecated. Consumers SHOULD refrain from usage of the declared operation. Default value is `false`. ### detail.description {#detaildescription} 操作行为的详细说明。 ¥A verbose explanation of the operation behavior. ### detail.summary {#detailsummary} 操作的简要概述。 ¥A short summary of what the operation does. ## 配置 {#config} ¥Config 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ## enabled {#enabled} @default true Enable/Disable the plugin ## 文档 {#documentation} ¥documentation OpenAPI 文档信息 ¥OpenAPI documentation information @see ## exclude {#exclude} 用于从文档中排除路径或方法的配置 ¥Configuration to exclude paths or methods from documentation ## exclude.methods {#excludemethods} 应从文档中排除的方法列表 ¥List of methods to exclude from documentation ## exclude.paths {#excludepaths} 应从文档中排除的路径列表 ¥List of paths to exclude from documentation ## exclude.staticFile {#excludestaticfile} @default true 从文档中排除静态文件路由 ¥Exclude static file routes from documentation ## exclude.tags {#excludetags} 应从文档中排除的标签列表 ¥List of tags to exclude from documentation ## mapJsonSchema {#mapjsonschema} 从标准架构到 OpenAPI 架构的自定义映射函数 ¥A custom mapping function from Standard schema to OpenAPI schema ### 示例 {#example} ¥Example ```typescript import { openapi } from '@elysiajs/openapi' import { toJsonSchema } from '@valibot/to-json-schema' openapi({ mapJsonSchema: { valibot: toJsonSchema } }) ``` ## path {#path} @default '/openapi' 用于公开 OpenAPI 文档前端的端点 ¥The endpoint to expose OpenAPI documentation frontend ## provider {#provider} @default 'scalar' OpenAPI 文档前端介于: ¥OpenAPI documentation frontend between: * [Scalar](https://github.com/scalar/scalar) * [SwaggerUI](https://github.com/openapi-api/openapi-ui) * null:禁用前端 ## references {#references} 为每个端点添加额外的 OpenAPI 参考 ¥Additional OpenAPI reference for each endpoint ## scalar {#scalar} 标量配置,请参阅 [Scalar 配置](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) ¥Scalar configuration, refers to [Scalar config](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) ## specPath {#specpath} @default '/${path}/json' 用于公开 JSON 格式 OpenAPI 规范的端点 ¥The endpoint to expose OpenAPI specification in JSON format ## swagger {#swagger} Swagger 配置,引用 [Swagger 配置](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/) ¥Swagger config, refers to [Swagger config](https://swagger.io/docs/open-source-tools/swagger-ui/usage/configuration/) 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. --- --- url: 'https://elysiajs.com/integrations/opentelemetry.md' --- # OpenTelemetry {#opentelemetry} 要开始使用 OpenTelemetry,请安装 `@elysiajs/opentelemetry` 并将插件应用于任何实例。 ¥To start using OpenTelemetry, install `@elysiajs/opentelemetry` and apply plugin to any instance. ```typescript import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia().use( opentelemetry({ spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())] }) ) ``` ![jaeger showing collected trace automatically](/blog/elysia-11/jaeger.webp) Elysia OpenTelemetry 将收集任何兼容 OpenTelemetry 标准的库的 span,并自动应用父级和子级 span。 ¥Elysia OpenTelemetry will **collect span of any library compatible with OpenTelemetry standard**, and will apply parent and child span automatically. 在上面的代码中,我们应用 `Prisma` 来跟踪每个查询的耗时。 ¥In the code above, we apply `Prisma` to trace how long each query took. 通过应用 OpenTelemetry,Elysia 将: ¥By applying OpenTelemetry, Elysia will then: * 收集遥测数据 * 将相关生命周期分组 * 测量每个函数的执行时间 * 检测 HTTP 请求和响应 * 收集错误和异常 你可以将遥测数据导出到 Jaeger、Zipkin、New Relic、Axiom 或任何其他兼容 OpenTelemetry 的后端。 ¥You may export telemetry data to Jaeger, Zipkin, New Relic, Axiom or any other OpenTelemetry compatible backend. ![axiom showing collected trace from OpenTelemetry](/blog/elysia-11/axiom.webp) 以下是将遥测数据导出到 [Axiom](https://axiom.co) 的示例 ¥Here's an example of exporting telemetry to [Axiom](https://axiom.co) ```typescript import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia().use( opentelemetry({ spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter({ url: 'https://api.axiom.co/v1/traces', // [!code ++] headers: { // [!code ++] Authorization: `Bearer ${Bun.env.AXIOM_TOKEN}`, // [!code ++] 'X-Axiom-Dataset': Bun.env.AXIOM_DATASET // [!code ++] } // [!code ++] }) ) ] }) ) ``` ## 仪器 {#instrumentations} ¥Instrumentations 许多插桩库要求 SDK 必须在导入模块之前运行。 ¥Many instrumentation libraries required that the SDK **MUST** run before importing the module. 例如,要使用 `PgInstrumentation`,必须在导入 `pg` 模块之前运行 `OpenTelemetry SDK`。 ¥For example, to use `PgInstrumentation`, the `OpenTelemetry SDK` must run before importing the `pg` module. 要在 Bun 中实现这一点,我们可以 ¥To achieve this in Bun, we can 1. 将 OpenTelemetry 设置分离到另一个文件中 2. 创建 `bunfig.toml` 以预加载 OpenTelemetry 设置文件 让我们在 `src/instrumentation.ts` 中创建一个新文件 ¥Let's create a new file in `src/instrumentation.ts` ```ts [src/instrumentation.ts] import { opentelemetry } from '@elysiajs/opentelemetry' import { PgInstrumentation } from '@opentelemetry/instrumentation-pg' export const instrumentation = opentelemetry({ instrumentations: [new PgInstrumentation()] }) ``` 然后我们可以将这个 `instrumentaiton` 插件应用到 `src/index.ts` 的主实例中。 ¥Then we can apply this `instrumentaiton` plugin into our main instance in `src/index.ts` ```ts [src/index.ts] import { Elysia } from 'elysia' import { instrumentation } from './instrumentation.ts' new Elysia().use(instrumentation).listen(3000) ``` 然后创建一个 `bunfig.toml`,内容如下: ¥Then create a `bunfig.toml` with the following: ```toml [bunfig.toml] preload = ["./src/instrumentation.ts"] ``` 这将告诉 Bun 在运行 `src/index.ts` 之前加载并设置 `instrumentation`,以便 OpenTelemetry 根据需要进行设置。 ¥This will tell Bun to load and setup `instrumentation` before running the `src/index.ts` allowing OpenTelemetry to do its setup as needed. ### 部署到生产环境 {#deploying-to-production} ¥Deploying to production 如果你使用 `bun build` 或其他打包器。 ¥If you are using `bun build` or other bundlers. 由于 OpenTelemetry 依赖于 monkey-patching `node_modules/`。为了使 make instrumentations 正常工作,我们需要将要被 instrument 的库指定为外部模块,以将其排除在打包之外。 ¥As OpenTelemetry rely on monkey-patching `node_modules/`. It's required that make instrumentations works properly, we need to specify that libraries to be instrument is an external module to exclude it from being bundled. 例如,如果你使用 `@opentelemetry/instrumentation-pg` 来检测 `pg` 库。我们需要将 `pg` 从打包包中排除,并确保它导入 `node_modules/pg`。 ¥For example, if you are using `@opentelemetry/instrumentation-pg` to instrument `pg` library. We need to exclude `pg` from being bundled and make sure that it is importing `node_modules/pg`. 为了使其正常工作,我们可以使用 `--external pg` 将 `pg` 指定为外部模块。 ¥To make this works, we may specified `pg` as an external module with `--external pg` ```bash bun build --compile --external pg --outfile server src/index.ts ``` 这告诉 bun 不要将 `pg` 打包到最终输出文件中,而是在运行时从 node\_modules 目录导入。所以,在生产服务器上,你还必须保留 node\_modules 目录。 ¥This tells bun to not `pg` bundled into the final output file, and will be imported from the **node\_modules** directory at runtime. So on a production server, you must also keeps the **node\_modules** directory. 建议在 package.json 中将生产服务器中应可用的软件包指定为依赖,并使用 `bun install --production` 仅安装生产依赖。 ¥It's recommended to specify packages that should be available in a production server as **dependencies** in **package.json** and use `bun install --production` to install only production dependencies. ```json { "dependencies": { "pg": "^8.15.6" }, "devDependencies": { "@elysiajs/opentelemetry": "^1.2.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@types/pg": "^8.11.14", "elysia": "^1.2.25" } } ``` 然后在生产服务器上运行构建命令后, ¥Then after running a build command, on a production server ```bash bun install --production ``` 如果 node\_modules 目录仍然包含开发依赖,你可以删除 node\_modules 目录并重新安装生产依赖。 ¥If the node\_modules directory still includes development dependencies, you may remove the node\_modules directory and reinstall production dependencies again. ## OpenTelemetry SDK {#opentelemetry-sdk} Elysia OpenTelemetry 仅用于将 OpenTelemetry 应用于 Elysia 服务器。 ¥Elysia OpenTelemetry is for applying OpenTelemetry to Elysia server only. 你可以正常使用 OpenTelemetry SDK,并且 span 在 Elysia 的请求 span 下运行,它将自动显示在 Elysia 跟踪中。 ¥You may use OpenTelemetry SDK normally, and the span is run under Elysia's request span, it will be automatically appear in Elysia trace. 然而,我们还提供了 `getTracer` 和 `record` 实用程序,用于从应用的任何部分收集 span。 ¥However, we also provide a `getTracer`, and `record` utility to collect span from any part of your application. ```typescript import { Elysia } from 'elysia' import { record } from '@elysiajs/opentelemetry' export const plugin = new Elysia().get('', () => { return record('database.query', () => { return db.query('SELECT * FROM users') }) }) ``` ## 记录实用程序 {#record-utility} ¥Record utility `record` 相当于 OpenTelemetry 的 `startActiveSpan`,但它可以处理自动关闭并自动捕获异常。 ¥`record` is an equivalent to OpenTelemetry's `startActiveSpan` but it will handle auto-closing and capture exception automatically. 你可以将 `record` 视为代码的标签,该标签将显示在跟踪中。 ¥You may think of `record` as a label for your code that will be shown in trace. ### 为可观察性准备代码库 {#prepare-your-codebase-for-observability} ¥Prepare your codebase for observability Elysia OpenTelemetry 将对生命周期进行分组,并将每个钩子的函数名称读取为 span 的名称。 ¥Elysia OpenTelemetry will group lifecycle and read the **function name** of each hook as the name of the span. 现在是时候命名你的函数了。 ¥It's a good time to **name your function**. 如果你的钩子处理程序是箭头函数,你可以将其重构为命名函数以便更好地理解跟踪,否则,你的跟踪跨度将被命名为 `anonymous`。 ¥If your hook handler is an arrow function, you may refactor it to named function to understand the trace better, otherwise your trace span will be named as `anonymous`. ```typescript const bad = new Elysia() // ⚠️ span name will be anonymous .derive(async ({ cookie: { session } }) => { return { user: await getProfile(session) } }) const good = new Elysia() // ✅ span name will be getProfile .derive(async function getProfile({ cookie: { session } }) { return { user: await getProfile(session) } }) ``` ## getCurrentSpan {#getcurrentspan} `getCurrentSpan` 是一个实用程序,用于在处理程序之外获取当前请求的当前跨度。 ¥`getCurrentSpan` is a utility to get the current span of the current request when you are outside of the handler. ```typescript import { getCurrentSpan } from '@elysiajs/opentelemetry' function utility() { const span = getCurrentSpan() span.setAttributes({ 'custom.attribute': 'value' }) } ``` 这在处理程序之外工作,通过从 `AsyncLocalStorage` 检索当前跨度来实现。 ¥This works outside of the handler by retriving current span from `AsyncLocalStorage` ## setAttributes {#setattributes} `setAttributes` 是一个实用程序,用于将属性设置为当前跨度。 ¥`setAttributes` is a utility to set attributes to the current span. ```typescript import { setAttributes } from '@elysiajs/opentelemetry' function utility() { setAttributes({ 'custom.attribute': 'value' }) } ``` 这是 `getCurrentSpan().setAttributes` 的语法糖 ¥This is a syntax sugar for `getCurrentSpan().setAttributes` ## 配置 {#configuration} ¥Configuration 有关配置选项和定义,请参阅 [opentelemetry 插件](/plugins/opentelemetry)。 ¥See [opentelemetry plugin](/plugins/opentelemetry) for configuration option and definition. --- --- url: 'https://elysiajs.com/plugins/opentelemetry.md' --- # OpenTelemetry {#opentelemetry} ::: tip 提示 本页是 OpenTelemetry 的配置参考,如果你希望设置和集成 OpenTelemetry,建议你查看 [与 OpenTelemetry 集成](/integrations/opentelemetry)。 ¥This page is a **config reference** for **OpenTelemetry**, if you're looking to setup and integrate with OpenTelemetry, we recommended taking a look at [Integrate with OpenTelemetry](/integrations/opentelemetry) instead. ::: 要开始使用 OpenTelemetry,请安装 `@elysiajs/opentelemetry` 并将插件应用于任何实例。 ¥To start using OpenTelemetry, install `@elysiajs/opentelemetry` and apply plugin to any instance. ```typescript twoslash import { Elysia } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-node' import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-proto' new Elysia() .use( opentelemetry({ spanProcessors: [ new BatchSpanProcessor( new OTLPTraceExporter() ) ] }) ) ``` ![jaeger showing collected trace automatically](/blog/elysia-11/jaeger.webp) Elysia OpenTelemetry 将收集任何兼容 OpenTelemetry 标准的库的 span,并自动应用父级和子级 span。 ¥Elysia OpenTelemetry is will **collect span of any library compatible OpenTelemetry standard**, and will apply parent and child span automatically. ## 用法 {#usage} ¥Usage 有关用法和实用程序,请参阅 [opentelemetry](/integrations/opentelemetry) ¥See [opentelemetry](/integrations/opentelemetry) for usage and utilities ## 配置 {#config} ¥Config 此插件扩展了 OpenTelemetry SDK 参数选项。 ¥This plugin extends OpenTelemetry SDK parameters options. 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### autoDetectResources - boolean {#autodetectresources---boolean} 使用默认资源检测器自动从环境中检测资源。 ¥Detect resources automatically from the environment using the default resource detectors. 默认:`true` ¥default: `true` ### contextManager - ContextManager {#contextmanager---contextmanager} 使用自定义上下文管理器。 ¥Use a custom context manager. 默认:`AsyncHooksContextManager` ¥default: `AsyncHooksContextManager` ### textMapPropagator - TextMapPropagator {#textmappropagator---textmappropagator} 使用自定义传播器。 ¥Use a custom propagator. 默认:`CompositePropagator` 使用 W3C 跟踪上下文和 Baggage ¥default: `CompositePropagator` using W3C Trace Context and Baggage ### metricReader - MetricReader {#metricreader---metricreader} 添加一个将传递给 MeterProvider 的 MetricReader。 ¥Add a MetricReader that will be passed to the MeterProvider. ### views - View\[] {#views---view} 要传递给 MeterProvider 的视图列表。 ¥A list of views to be passed to the MeterProvider. 接受一个 View 实例数组。此参数可用于配置直方图指标的明确存储桶大小。 ¥Accepts an array of View-instances. This parameter can be used to configure explicit bucket sizes of histogram metrics. ### instrumentations - (Instrumentation | Instrumentation\[])\[] {#instrumentations---instrumentation--instrumentation} 配置检测。 ¥Configure instrumentations. 默认情况下,`getNodeAutoInstrumentations` 处于启用状态,如果你想启用它们,可以使用元包或单独配置每个检测工具。 ¥By default `getNodeAutoInstrumentations` is enabled, if you want to enable them you can use either metapackage or configure each instrumentation individually. 默认:`getNodeAutoInstrumentations()` ¥default: `getNodeAutoInstrumentations()` ### resource - IResource {#resource---iresource} 配置资源。 ¥Configure a resource. 也可以使用 SDK 的 autoDetectResources 方法检测资源。 ¥Resources may also be detected by using the autoDetectResources method of the SDK. ### resourceDetectors - Array\ {#resourcedetectors---arraydetector--detectorsync} 配置资源检测器。默认情况下,资源检测器为 \[envDetector, processDetector, hostDetector]。注意:为了启用检测,参数 autoDetectResources 必须为 true。 ¥Configure resource detectors. By default, the resource detectors are \[envDetector, processDetector, hostDetector]. NOTE: In order to enable the detection, the parameter autoDetectResources has to be true. 如果未设置 resourceDetectors,你还可以使用环境变量 OTEL\_NODE\_RESOURCE\_DETECTORS 来启用某些检测器,或完全禁用它们: ¥If resourceDetectors was not set, you can also use the environment variable OTEL\_NODE\_RESOURCE\_DETECTORS to enable only certain detectors, or completely disable them: * env * host * os * process * serviceinstance(实验性) * all - 启用上述所有资源检测器 * none - 禁用资源检测 例如,要仅启用环境和主机检测器: ¥For example, to enable only the env, host detectors: ```bash export OTEL_NODE_RESOURCE_DETECTORS="env,host" ``` ### sampler - 采样器 {#sampler---sampler} ¥sampler - Sampler 配置自定义采样器。默认情况下,所有跟踪都将被采样。 ¥Configure a custom sampler. By default, all traces will be sampled. ### serviceName - string {#servicename---string} 待识别的命名空间。 ¥Namespace to be identify as. ### spanProcessors - SpanProcessor\[] {#spanprocessors---spanprocessor} 用于注册到跟踪器提供程序的 span 处理器数组。 ¥An array of span processors to register to the tracer provider. ### traceExporter - SpanExporter {#traceexporter---spanexporter} 配置跟踪导出器。如果配置了导出器,它将与 `BatchSpanProcessor` 一起使用。 ¥Configure a trace exporter. If an exporter is configured, it will be used with a `BatchSpanProcessor`. 如果未以编程方式配置导出器或 span 处理器,此包将自动设置使用 http/protobuf 协议的默认 otlp 导出器,并使用 BatchSpanProcessor。 ¥If an exporter OR span processor is not configured programmatically, this package will auto setup the default otlp exporter with http/protobuf protocol with a BatchSpanProcessor. ### spanLimits - SpanLimits {#spanlimits---spanlimits} 配置跟踪参数。这些参数与配置跟踪器时使用的参数相同。 ¥Configure tracing parameters. These are the same trace parameters used to configure a tracer. --- --- url: 'https://elysiajs.com/integrations/react-email.md' --- # React 电子邮件 {#react-email} ¥React Email React Email 是一个允许你使用 React 组件创建电子邮件的库。 ¥React Email is a library that allows you to use React components to create emails. 由于 Elysia 使用 Bun 作为运行时环境,我们可以直接编写一个 React Email 组件,并将 JSX 直接导入到我们的代码中来发送电子邮件。 ¥As Elysia is using Bun as runtime environment, we can directly write a React Email component and import the JSX directly to our code to send emails. ## 安装 {#installation} ¥Installation 要安装 React Email,请运行以下命令: ¥To install React Email, run the following command: ```bash bun add -d react-email bun add @react-email/components react react-dom ``` 然后将此脚本添加到 `package.json`: ¥Then add this script to `package.json`: ```json { "scripts": { "email": "email dev --dir src/emails" } } ``` 我们建议将电子邮件模板添加到 `src/emails` 目录中,因为我们可以直接导入 JSX 文件。 ¥We recommend adding email templates into the `src/emails` directory as we can directly import the JSX files. ### TypeScript {#typescript} 如果你使用 TypeScript,则可能需要将以下内容添加到你的 `tsconfig.json`: ¥If you are using TypeScript, you may need to add the following to your `tsconfig.json`: ```json { "compilerOptions": { "jsx": "react" } } ``` ## 你的第一封邮件 {#your-first-email} ¥Your first email 使用以下代码创建文件 `src/emails/otp.tsx`: ¥Create file `src/emails/otp.tsx` with the following code: ```tsx import * as React from 'react' import { Tailwind, Section, Text } from '@react-email/components' export default function OTPEmail({ otp }: { otp: number }) { return (
Verify your Email Address Use the following code to verify your email address {otp} This code is valid for 10 minutes Thank you for joining us
) } OTPEmail.PreviewProps = { otp: 123456 } ``` 你可能注意到我们正在使用 `@react-email/components` 创建电子邮件模板。 ¥You may notice that we are using `@react-email/components` to create the email template. 此库提供了一组组件,包括使用 Tailwind 进行样式设置,并与 Gmail、Outlook 等电子邮件客户端兼容。 ¥This library provides a set of components including **styling with Tailwind** that are compatible with email clients like Gmail, Outlook, etc. 我们还在 `OTPEmail` 函数中添加了 `PreviewProps`。这仅在我们的 Playground 上预览电子邮件时适用。 ¥We also added a `PreviewProps` to the `OTPEmail` function. This is only apply when previewing the email on our playground. ## 预览你的电子邮件 {#preview-your-email} ¥Preview your email 要预览你的电子邮件,请运行以下命令: ¥To preview your email, run the following command: ```bash bun email ``` 这将打开一个浏览器窗口,其中包含你的电子邮件预览。 ¥This will open a browser window with the preview of your email. ![React Email playground showing an OTP email we have just written](/recipe/react-email/email-preview.webp) ## 发送电子邮件 {#sending-email} ¥Sending email 要发送电子邮件,我们可以使用 `react-dom/server` 渲染电子邮件,然后使用首选提供程序提交: ¥To send an email, we can use `react-dom/server` to render the the email then submit using a preferred provider: ::: code-group ```tsx [Nodemailer] import { Elysia, t } from 'elysia' import * as React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import OTPEmail from './emails/otp' import nodemailer from 'nodemailer' // [!code ++] const transporter = nodemailer.createTransport({ // [!code ++] host: 'smtp.gehenna.sh', // [!code ++] port: 465, // [!code ++] auth: { // [!code ++] user: 'makoto', // [!code ++] pass: '12345678' // [!code ++] } // [!code ++] }) // [!code ++] new Elysia() .get('/otp', async ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await transporter.sendMail({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html, // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [Resend] import { Elysia, t } from 'elysia' import OTPEmail from './emails/otp' import Resend from 'resend' // [!code ++] const resend = new Resend('re_123456789') // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 await resend.emails.send({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html: , // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [AWS SES] import { Elysia, t } from 'elysia' import * as React from 'react' import { renderToStaticMarkup } from 'react-dom/server' import OTPEmail from './emails/otp' import { type SendEmailCommandInput, SES } from '@aws-sdk/client-ses' // [!code ++] import { fromEnv } from '@aws-sdk/credential-providers' // [!code ++] const ses = new SES({ // [!code ++] credentials: // [!code ++] process.env.NODE_ENV === 'production' ? fromEnv() : undefined // [!code ++] }) // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await ses.sendEmail({ // [!code ++] Source: 'ibuki@gehenna.sh', // [!code ++] Destination: { // [!code ++] ToAddresses: [body] // [!code ++] }, // [!code ++] Message: { // [!code ++] Body: { // [!code ++] Html: { // [!code ++] Charset: 'UTF-8', // [!code ++] Data: html // [!code ++] } // [!code ++] }, // [!code ++] Subject: { // [!code ++] Charset: 'UTF-8', // [!code ++] Data: 'Verify your email address' // [!code ++] } // [!code ++] } // [!code ++] } satisfies SendEmailCommandInput) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ```tsx [Sendgrid] import { Elysia, t } from 'elysia' import OTPEmail from './emails/otp' import sendgrid from "@sendgrid/mail" // [!code ++] sendgrid.setApiKey(process.env.SENDGRID_API_KEY) // [!code ++] new Elysia() .get('/otp', ({ body }) => { // Random between 100,000 and 999,999 const otp = ~~(Math.random() * (900_000 - 1)) + 100_000 const html = renderToStaticMarkup() await sendgrid.send({ // [!code ++] from: 'ibuki@gehenna.sh', // [!code ++] to: body, // [!code ++] subject: 'Verify your email address', // [!code ++] html // [!code ++] }) // [!code ++] return { success: true } }, { body: t.String({ format: 'email' }) }) .listen(3000) ``` ::: ::: tip 提示 请注意,借助 Bun,我们可以直接导入电子邮件组件。 ¥Notice that we can directly import the email component out of the box thanks to Bun ::: 你可以在 [React Email 集成](https://react.email/docs/integrations/overview) 中看到所有可用的 React Email 集成,并在 [React Email 文档](https://react.email/docs) 中了解更多关于 React Email 的信息。 ¥You may see all of the available integration with React Email in the [React Email Integration](https://react.email/docs/integrations/overview), and learn more about React Email in [React Email documentation](https://react.email/docs) --- --- url: 'https://elysiajs.com/patterns/cookie.md' --- # Cookie {#cookie} 要使用 Cookie,你可以提取 cookie 属性并直接访问其名称和值。 ¥To use Cookie, you can extract the cookie property and access its name and value directly. 没有 get/set 方法,你可以提取 cookie 名称并直接检索或更新其值。 ¥There's no get/set, you can extract the cookie name and retrieve or update its value directly. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Get name.value // Set name.value = "New Value" }) ``` 默认情况下,Reactive Cookie 可以自动编码/解码对象类型,这使我们可以将 Cookie 视为对象,而无需担心编码/解码。它真的有效。 ¥By default, Reactive Cookie can encode/decode object types automatically allowing us to treat cookies as objects without worrying about the encoding/decoding. **It just works**. ## 反应性 {#reactivity} ¥Reactivity Elysia cookie 是响应式的。这意味着当你更改 cookie 值时,cookie 将基于类似信号的方法自动更新。 ¥The Elysia cookie is reactive. This means that when you change the cookie value, the cookie will be updated automatically based on an approach like signals. Elysia Cookie 提供了处理 Cookie 的单一真实来源,它能够自动设置标头并同步 Cookie 值。 ¥A single source of truth for handling cookies is provided by Elysia cookies, which have the ability to automatically set headers and sync cookie values. 由于 Cookie 默认是依赖于代理的对象,因此提取的值永远不会是未定义的;相反,它始终是 `Cookie` 的值,可以通过调用 .value 属性获取。 ¥Since cookies are Proxy-dependent objects by default, the extract value can never be **undefined**; instead, it will always be a value of `Cookie`, which can be obtained by invoking the **.value** property. 我们可以将 Cookie 罐视为常规对象,对其进行迭代只会迭代已经存在的 Cookie 值。 ¥We can treat the cookie jar as a regular object, iteration over it will only iterate over an already-existing cookie value. ## Cookie 属性 {#cookie-attribute} ¥Cookie Attribute 要使用 Cookie 属性,你可以使用以下任一方法: ¥To use Cookie attribute, you can either use one of the following: 1. 直接设置属性 2. 使用 `set` 或 `add` 更新 Cookie 属性。 有关更多信息,请参阅 [cookie 属性配置](/patterns/cookie.html#config)。 ¥See [cookie attribute config](/patterns/cookie.html#config) for more information. ### 分配属性 {#assign-property} ¥Assign Property 你可以像任何普通对象一样获取/设置 cookie 的属性,响应式模型会自动同步 cookie 值。 ¥You can get/set the property of a cookie like any normal object, the reactivity model synchronizes the cookie value automatically. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // get name.domain // set name.domain = 'millennium.sh' name.httpOnly = true }) ``` ## set {#set} set 允许通过重置所有属性并用新值覆盖属性来一次性更新多个 cookie 属性。 ¥**set** permits updating multiple cookie properties all at once through **reset all property** and overwrite the property with a new value. ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { name.set({ domain: 'millennium.sh', httpOnly: true }) }) ``` ## add {#add} 类似 set,add 允许我们一次更新多个 Cookie 属性,但它只会覆盖定义的属性,而不是重置。 ¥Like **set**, **add** allow us to update multiple cookie properties at once, but instead, will only overwrite the property defined instead of resetting. ## remove {#remove} 要删除 Cookie,你可以使用以下任一方式: ¥To remove a cookie, you can use either: 1. name.remove 2. 删除 cookie.name ```ts import { Elysia } from 'elysia' new Elysia() .get('/', ({ cookie, cookie: { name } }) => { name.remove() delete cookie.name }) ``` ## Cookie Schema {#cookie-schema} 你可以使用 `t.Cookie` 的 cookie 模式严格验证 cookie 类型并提供 cookie 的类型推断。 ¥You can strictly validate cookie type and providing type inference for cookie by using cookie schema with `t.Cookie`. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Set name.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ name: t.Object({ id: t.Numeric(), name: t.String() }) }) }) ``` ## 可空值 Cookie {#nullable-cookie} ¥Nullable Cookie 要处理可空的 Cookie 值,你可以在要设置为可空的 Cookie 名称上使用 `t.Optional`。 ¥To handle nullable cookie value, you can use `t.Optional` on the cookie name you want to be nullable. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { name } }) => { // Set name.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ name: t.Optional( t.Object({ id: t.Numeric(), name: t.String() }) ) }) }) ``` ## Cookie 签名 {#cookie-signature} ¥Cookie Signature 通过引入 Cookie Schema 和 `t.Cookie` 类型,我们可以创建一个统一的类型来自动处理 cookie 签名/验证。 ¥With an introduction of Cookie Schema, and `t.Cookie` type, we can create a unified type for handling sign/verify cookie signature automatically. Cookie 签名是附加到 Cookie 值的加密哈希值,它使用密钥和 Cookie 的内容生成,通过向 Cookie 添加签名来增强安全性。 ¥Cookie signature is a cryptographic hash appended to a cookie's value, generated using a secret key and the content of the cookie to enhance security by adding a signature to the cookie. 这确保了 cookie 值不会被恶意行为者修改,有助于验证 cookie 数据的真实性和完整性。 ¥This make sure that the cookie value is not modified by malicious actor, helps in verifying the authenticity and integrity of the cookie data. ## 使用 Cookie 签名 {#using-cookie-signature} ¥Using Cookie Signature 通过提供 cookie 密钥和 `sign` 属性来指示哪个 cookie 应该进行签名验证。 ¥By provide a cookie secret, and `sign` property to indicate which cookie should have a signature verification. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ profile: t.Object({ id: t.Numeric(), name: t.String() }) }, { secrets: 'Fischl von Luftschloss Narfidort', sign: ['profile'] }) }) ``` Elysia 随后会自动对 Cookie 值进行签名和取消签名。 ¥Elysia then sign and unsign cookie value automatically. ## 构造函数 {#constructor-NaN} ¥Constructor 你可以使用 Elysia 构造函数设置全局 cookie `secret` 和 `sign` 值,并将其全局应用于所有路由,而无需将其内联到你需要的每个路由。 ¥You can use Elysia constructor to set global cookie `secret`, and `sign` value to apply to all route globally instead of inlining to every route you need. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ cookie: { secrets: 'Fischl von Luftschloss Narfidort', sign: ['profile'] } }) .get('/', ({ cookie: { profile } }) => { profile.value = { id: 617, name: 'Summoning 101' } }, { cookie: t.Cookie({ profile: t.Object({ id: t.Numeric(), name: t.String() }) }) }) ``` ## Cookie 轮换 {#cookie-rotation} ¥Cookie Rotation Elysia 会自动处理 Cookie 的 secret 轮换。 ¥Elysia handle Cookie's secret rotation automatically. Cookie 轮换是一种迁移技术,它使用较新的密钥对 Cookie 进行签名,同时还能够验证 Cookie 的旧签名。 ¥Cookie Rotation is a migration technique to sign a cookie with a newer secret, while also be able to verify the old signature of the cookie. ```ts import { Elysia } from 'elysia' new Elysia({ cookie: { secrets: ['Vengeance will be mine', 'Fischl von Luftschloss Narfidort'] } }) ``` ## 配置 {#config} ¥Config 以下是 Elysia 接受的 cookie 配置。 ¥Below is a cookie config accepted by Elysia. ### secret {#secret} 用于签署/取消签署 Cookie 的密钥。 ¥The secret key for signing/un-signing cookies. 如果传递的是数组,将使用密钥轮换。 ¥If an array is passed, will use Key Rotation. 密钥轮换是指将加密密钥退役,并用生成新的加密密钥来替换它。 ¥Key rotation is when an encryption key is retired and replaced by generating a new cryptographic key. *** 以下是从 [cookie](https://npmjs.com/package/cookie) 扩展的配置。 ¥Below is a config that extends from [cookie](https://npmjs.com/package/cookie) ### domain {#domain} 指定 [域名 Set-Cookie 属性](https://tools.ietf.org/html/rfc6265#section-5.2.3) 的值。 ¥Specifies the value for the [Domain Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.3). 默认情况下,未设置域,大多数客户端会认为 cookie 仅适用于当前域。 ¥By default, no domain is set, and most clients will consider the cookie to apply to only the current domain. ### encode {#encode} @default `encodeURIComponent` 指定用于编码 Cookie 值的函数。 ¥Specifies a function that will be used to encode a cookie's value. 由于 Cookie 的值具有有限的字符集(并且必须是一个简单的字符串),此函数可用于将值编码为适合 Cookie 值的字符串。 ¥Since the value of a cookie has a limited character set (and must be a simple string), this function can be used to encode a value into a string suited for a cookie's value. 默认函数是全局函数 `encodeURIComponent`,它将 JavaScript 字符串编码为 UTF-8 字节序列,然后对超出 Cookie 范围的内容进行 URL 编码。 ¥The default function is the global `encodeURIComponent`, which will encode a JavaScript string into UTF-8 byte sequences and then URL-encode any that fall outside of the cookie range. ### expires {#expires} 指定 Date 对象作为 [Set-Cookie 属性的过期时间](https://tools.ietf.org/html/rfc6265#section-5.2.1) 的值。 ¥Specifies the Date object to be the value for the [Expires Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.1). 默认情况下,未设置过期时间,大多数客户端会将其视为 "非持久性 Cookie",并在退出 Web 浏览器应用等情况下将其删除。 ¥By default, no expiration is set, and most clients will consider this a "non-persistent cookie" and will delete it on a condition like exiting a web browser application. ::: tip 提示 [cookie 存储模型规范](https://tools.ietf.org/html/rfc6265#section-5.3) 规定,如果同时设置了 `expires` 和 `maxAge`,则 `maxAge` 优先,但并非所有客户端都遵循此规定,因此如果同时设置了两者,则它们应指向相同的日期和时间。 ¥The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time. ::: ### httpOnly {#httponly} @default `false` 指定 [HttpOnly Set-Cookie 属性](https://tools.ietf.org/html/rfc6265#section-5.2.6) 的布尔值。 ¥Specifies the boolean value for the [HttpOnly Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.6). 当查询参数为真时,HttpOnly 属性已设置,否则未设置。 ¥When truthy, the HttpOnly attribute is set, otherwise, it is not. 默认情况下,未设置 HttpOnly 属性。 ¥By default, the HttpOnly attribute is not set. ::: tip 提示 设置为 true 时请小心,因为兼容的客户端不允许客户端 JavaScript 查看 `document.cookie` 中的 Cookie。 ¥be careful when setting this to true, as compliant clients will not allow client-side JavaScript to see the cookie in `document.cookie`. ::: ### maxAge {#maxage} @default `undefined` 指定数字(以秒为单位)作为 [Max-Age Set-Cookie 属性](https://tools.ietf.org/html/rfc6265#section-5.2.2) 的值。 ¥Specifies the number (in seconds) to be the value for the [Max-Age Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.2). 给定的数字将通过向下舍入转换为整数。默认情况下,未设置最大使用期限。 ¥The given number will be converted to an integer by rounding down. By default, no maximum age is set. ::: tip 提示 [cookie 存储模型规范](https://tools.ietf.org/html/rfc6265#section-5.3) 规定,如果同时设置了 `expires` 和 `maxAge`,则 `maxAge` 优先,但并非所有客户端都遵循此规定,因此如果同时设置了两者,则它们应指向相同的日期和时间。 ¥The [cookie storage model specification](https://tools.ietf.org/html/rfc6265#section-5.3) states that if both `expires` and `maxAge` are set, then `maxAge` takes precedence, but not all clients may obey this, so if both are set, they should point to the same date and time. ::: ### path {#path} 指定 [路径 Set-Cookie 属性](https://tools.ietf.org/html/rfc6265#section-5.2.4) 的值。 ¥Specifies the value for the [Path Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.4). 默认情况下,路径处理程序被视为默认路径。 ¥By default, the path handler is considered the default path. ### priority {#priority} 指定字符串作为 [优先级 Set-Cookie 属性](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1) 的值。`low` 将 Priority 属性设置为 Low。`medium` 将 Priority 属性设置为 Medium,即未设置时的默认优先级。`high` 将 Priority 属性设置为 High。 ¥Specifies the string to be the value for the [Priority Set-Cookie attribute](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1). `low` will set the Priority attribute to Low. `medium` will set the Priority attribute to Medium, the default priority when not set. `high` will set the Priority attribute to High. 有关不同优先级的更多信息,请参阅 [规范](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1)。 ¥More information about the different priority levels can be found in [the specification](https://tools.ietf.org/html/draft-west-cookie-priority-00#section-4.1). ::: tip 提示 这是一个尚未完全标准化的属性,将来可能会更改。这也意味着许多客户端可能会忽略此属性,直到他们理解它为止。 ¥This is an attribute that has not yet been fully standardized and may change in the future. This also means many clients may ignore this attribute until they understand it. ::: ### sameSite {#samesite} 指定布尔值或字符串作为 [SameSite Set-Cookie 属性](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7) 的值。`true` 将 SameSite 属性设置为 Strict,以实现严格的同站点强制执行。`false` 不会设置 SameSite 属性。`'lax'` 将 SameSite 属性设置为 Lax,以实现宽松的同站点强制执行。`'none'` 将 SameSite 属性设置为 None,以实现显式跨站点 Cookie。`'strict'` 将 SameSite 属性设置为 Strict,以实现严格的同站点强制执行。有关不同执行级别的更多信息,请参阅 [规范](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7)。 ¥Specifies the boolean or string to be the value for the [SameSite Set-Cookie attribute](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). `true` will set the SameSite attribute to Strict for strict same-site enforcement. `false` will not set the SameSite attribute. `'lax'` will set the SameSite attribute to Lax for lax same-site enforcement. `'none'` will set the SameSite attribute to None for an explicit cross-site cookie. `'strict'` will set the SameSite attribute to Strict for strict same-site enforcement. More information about the different enforcement levels can be found in [the specification](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-09#section-5.4.7). ::: tip 提示 这是一个尚未完全标准化的属性,将来可能会更改。这也意味着许多客户端可能会忽略此属性,直到他们理解它为止。 ¥This is an attribute that has not yet been fully standardized and may change in the future. This also means many clients may ignore this attribute until they understand it. ::: ### secure {#secure} 指定 [安全的 Set-Cookie 属性](https://tools.ietf.org/html/rfc6265#section-5.2.5) 的布尔值。当查询参数为真时,Secure 属性已设置,否则未设置。默认情况下,未设置 Secure 属性。 ¥Specifies the boolean value for the [Secure Set-Cookie attribute](https://tools.ietf.org/html/rfc6265#section-5.2.5). When truthy, the Secure attribute is set, otherwise, it is not. By default, the Secure attribute is not set. ::: tip 提示 将此设置为 true 时要小心,因为如果浏览器没有 HTTPS 连接,兼容的客户端将来不会将 cookie 发送回服务器。 ¥Be careful when setting this to true, as compliant clients will not send the cookie back to the server in the future if the browser does not have an HTTPS connection. ::: --- --- url: 'https://elysiajs.com/plugins/stream.md' --- # Stream 插件 {#stream-plugin} ¥Stream Plugin ::: warning 警告 此插件处于维护模式,不会添加新功能。我们建议使用 [改为使用 Generator Stream](/essential/handler#stream) ¥This plugin is in maintenance mode and will not receive new features. We recommend using the [Generator Stream instead](/essential/handler#stream) ::: 此插件添加了对流式响应或将服务器发送事件发送回客户端的支持。 ¥This plugin adds support for streaming response or sending Server-Sent Event back to the client. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/stream ``` 然后使用它: ¥Then use it: ```typescript import { Elysia } from 'elysia' import { Stream } from '@elysiajs/stream' new Elysia() .get('/', () => new Stream(async (stream) => { stream.send('hello') await stream.wait(1000) stream.send('world') stream.close() })) .listen(3000) ``` 默认情况下,`Stream` 将返回 `Response` 和 `content-type` 或 `text/event-stream; charset=utf8`。 ¥By default, `Stream` will return `Response` with `content-type` of `text/event-stream; charset=utf8`. ## 构造函数 {#constructor-NaN} ¥Constructor 以下是 `Stream` 接受的构造函数参数: ¥Below is the constructor parameter accepted by `Stream`: 1. Stream: * 自动:根据提供的值自动流式响应 * 可迭代对象 * AsyncIterable * ReadableStream * 响应 * 手动:`(stream: this) => unknown` 或 `undefined` 的回调 2. 选项:`StreamOptions` * [event](https://web.nodejs.cn/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event):用于标识所描述事件类型的字符串 * [retry](https://web.nodejs.cn/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#retry):重新连接时间(以毫秒为单位) ## 方法 {#method} ¥Method 以下是 `Stream` 提供的方法: ¥Below is the method provided by `Stream`: ### send {#send} 将数据排队到流中以发送回客户端 ¥Enqueue data to stream to send back to the client ### close {#close} 关闭流 ¥Close the stream ### wait {#wait} 返回一个以毫秒为单位解析提供的值的 Promise。 ¥Return a promise that resolves in the provided value in ms ### value {#value} `ReadableStream` 的内部值 ¥Inner value of the `ReadableStream` ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. * [OpenAI](#openai) * [获取流](#fetch-stream) * [服务器发送事件](#server-sent-event) ## OpenAI {#openai} 当参数为 `Iterable` 或 `AsyncIterable` 时,将触发自动模式,并自动将响应流式传输回客户端。 ¥Automatic mode is triggered when the parameter is either `Iterable` or `AsyncIterable` streaming the response back to the client automatically. 以下是将 ChatGPT 集成到 Elysia 的示例。 ¥Below is an example of integrating ChatGPT into Elysia. ```ts new Elysia() .get( '/ai', ({ query: { prompt } }) => new Stream( openai.chat.completions.create({ model: 'gpt-3.5-turbo', stream: true, messages: [{ role: 'user', content: prompt }] }) ) ) ``` 默认情况下,[openai](https://npmjs.com/package/openai) chatGPT 完成返回 `AsyncIterable`,因此你应该能够将 OpenAI 封装在 `Stream` 中。 ¥By default [openai](https://npmjs.com/package/openai) chatGPT completion returns `AsyncIterable` so you should be able to wrap the OpenAI in `Stream`. ## 获取流 {#fetch-stream} ¥Fetch Stream 你可以将从返回流的端点的获取传递给代理流。 ¥You can pass a fetch from an endpoint that returns the stream to proxy a stream. 这对于使用 AI 文本生成的端点非常有用,因为你可以直接代理它,例如。 [Cloudflare AI](https://developers.cloudflare.com/workers-ai/models/llm/#examples---chat-style-with-system-prompt-preferred)。 ¥This is useful for those endpoints that use AI text generation since you can proxy it directly, eg. [Cloudflare AI](https://developers.cloudflare.com/workers-ai/models/llm/#examples---chat-style-with-system-prompt-preferred). ```ts const model = '@cf/meta/llama-2-7b-chat-int8' const endpoint = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/ai/run/${model}` new Elysia() .get('/ai', ({ query: { prompt } }) => fetch(endpoint, { method: 'POST', headers: { authorization: `Bearer ${API_TOKEN}`, 'content-type': 'application/json' }, body: JSON.stringify({ messages: [ { role: 'system', content: 'You are a friendly assistant' }, { role: 'user', content: prompt } ] }) }) ) ``` ## 服务器发送事件 {#server-sent-event} ¥Server Sent Event 当参数为 `callback` 或 `undefined` 时,将触发手动模式,让你可以控制流。 ¥Manual mode is triggered when the parameter is either `callback` or `undefined`, allowing you to control the stream. ### callback-based {#callback-based} 以下是使用构造函数回调创建服务器发送事件端点的示例。 ¥Below is an example of creating a Server-Sent Event endpoint using a constructor callback ```ts new Elysia() .get('/source', () => new Stream((stream) => { const interval = setInterval(() => { stream.send('hello world') }, 500) setTimeout(() => { clearInterval(interval) stream.close() }, 3000) }) ) ``` ### value-based {#value-based} 以下是使用基于值的构造函数创建服务器发送事件端点的示例。 ¥Below is an example of creating a Server-Sent Event endpoint using a value-based ```ts new Elysia() .get('/source', () => { const stream = new Stream() const interval = setInterval(() => { stream.send('hello world') }, 500) setTimeout(() => { clearInterval(interval) stream.close() }, 3000) return stream }) ``` 基于回调和基于值的流的工作方式相同,但语法有所不同,以根据你的偏好设置。 ¥Both callback-based and value-based streams work in the same way but with different syntax for your preference. --- --- url: 'https://elysiajs.com/plugins/swagger.md' --- ::: warning 警告 Swagger 插件已弃用,不再维护。请使用 [OpenAPI 插件](/plugins/openapi) 代替。 ¥Swagger plugin is deprecated and is no longer be maintained. Please use [OpenAPI plugin](/plugins/openapi) instead. ::: # Swagger 插件 {#swagger-plugin} ¥Swagger Plugin 此插件为 Elysia 服务器生成 Swagger 端点。 ¥This plugin generates a Swagger endpoint for an Elysia server 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/swagger ``` 然后使用它: ¥Then use it: ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use(swagger()) .get('/', () => 'hi') .post('/hello', () => 'world') .listen(3000) ``` 访问 `/swagger` 将显示一个 Scalar UI,其中包含 Elysia 服务器生成的端点文档。你还可以访问 `/swagger/json` 处的原始 OpenAPI 规范。 ¥Accessing `/swagger` would show you a Scalar UI with the generated endpoint documentation from the Elysia server. You can also access the raw OpenAPI spec at `/swagger/json`. ## 配置 {#config} ¥Config 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### provider {#provider} @default `scalar` 用于文档的 UI 提供程序。默认为标量。 ¥UI Provider for documentation. Default to Scalar. ### scalar {#scalar} 用于自定义 Scalar 的配置。 ¥Configuration for customizing Scalar. 请参阅 [Scalar 配置](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) ¥Please refer to the [Scalar config](https://github.com/scalar/scalar/blob/main/documentation/configuration.md) ### swagger {#swagger} 用于自定义 Swagger 的配置。 ¥Configuration for customizing Swagger. 请参阅 [Swagger 规范](https://swagger.io/specification/v2/)。 ¥Please refer to the [Swagger specification](https://swagger.io/specification/v2/). ### excludeStaticFile {#excludestaticfile} @default `true` 确定 Swagger 是否应排除静态文件。 ¥Determine if Swagger should exclude static files. ### path {#path} @default `/swagger` 用于暴露 Swagger 的端点 ¥Endpoint to expose Swagger ### exclude {#exclude} 要从 Swagger 文档中排除的路径。 ¥Paths to exclude from Swagger documentation. 值可以是以下之一: ¥Value can be one of the following: * **string** * **RegExp** * **Array\** ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. ## 变更 Swagger 端点 {#change-swagger-endpoint} ¥Change Swagger Endpoint 你可以通过在插件配置中设置 [path](#path) 来更改 Swagger 端点。 ¥You can change the swagger endpoint by setting [path](#path) in the plugin config. ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use( swagger({ path: '/v2/swagger' }) ) .listen(3000) ``` ## 自定义 Swagger 信息 {#customize-swagger-info} ¥Customize Swagger info ```typescript import { Elysia } from 'elysia' import { swagger } from '@elysiajs/swagger' new Elysia() .use( swagger({ documentation: { info: { title: 'Elysia Documentation', version: '1.0.0' } } }) ) .listen(3000) ``` ## 使用标签 {#using-tags} ¥Using Tags Elysia 可以使用 Swaggers 标签系统将端点分组。 ¥Elysia can separate the endpoints into groups by using the Swaggers tag system 首先,在 Swagger 配置对象中定义可用的标签 ¥Firstly define the available tags in the swagger config object ```typescript app.use( swagger({ documentation: { tags: [ { name: 'App', description: 'General endpoints' }, { name: 'Auth', description: 'Authentication endpoints' } ] } }) ) ``` 然后使用端点配置部分的 details 属性将该端点分配给组 ¥Then use the details property of the endpoint configuration section to assign that endpoint to the group ```typescript app.get('/', () => 'Hello Elysia', { detail: { tags: ['App'] } }) app.group('/auth', (app) => app.post( '/sign-up', async ({ body }) => db.user.create({ data: body, select: { id: true, username: true } }), { detail: { tags: ['Auth'] } } ) ) ``` 将生成如下所示的 Swagger 页面 ¥Which will produce a swagger page like the following ## 安全配置 {#security-configuration} ¥Security Configuration 要保护你的 API 端点,你可以在 Swagger 配置中定义安全方案。以下示例演示了如何使用 Bearer Authentication (JWT) 保护你的端点: ¥To secure your API endpoints, you can define security schemes in the Swagger configuration. The example below demonstrates how to use Bearer Authentication (JWT) to protect your endpoints: ```typescript app.use( swagger({ documentation: { components: { securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', bearerFormat: 'JWT' } } } } }) ) export const addressController = new Elysia({ prefix: '/address', detail: { tags: ['Address'], security: [ { bearerAuth: [] } ] } }) ``` 此配置确保所有 `/address` 前缀下的端点都需要有效的 JWT 令牌才能访问。 ¥This configuration ensures that all endpoints under the `/address` prefix require a valid JWT token for access. --- --- url: 'https://elysiajs.com/patterns/websocket.md' --- # WebSocket {#websocket} WebSocket 是一种用于客户端和服务器之间通信的实时协议。 ¥WebSocket is a realtime protocol for communication between your client and server. 与 HTTP 不同,HTTP 中客户端需要反复向网站请求信息并等待回复,而 WebSocket 则建立了一条直接的通信线路,客户端和服务器可以直接来回发送消息,使对话更快捷、更顺畅,无需重新发送每条消息。 ¥Unlike HTTP where our client repeatedly asks the website for information and waits for a reply each time, WebSocket sets up a direct line where our client and server can send messages back and forth directly, making the conversation quicker and smoother without having to start over with each message. SocketIO 是一个流行的 WebSocket 库,但它并非唯一的库。Elysia 使用 Bun 在底层使用 [uWebSocket](https://github.com/uNetworking/uWebSockets) 并使用相同的 API。 ¥SocketIO is a popular library for WebSocket, but it is not the only one. Elysia uses [uWebSocket](https://github.com/uNetworking/uWebSockets) which Bun uses under the hood with the same API. 要使用 WebSocket,只需调用 `Elysia.ws()`: ¥To use WebSocket, simply call `Elysia.ws()`: ```typescript import { Elysia } from 'elysia' new Elysia() .ws('/ws', { message(ws, message) { ws.send(message) } }) .listen(3000) ``` ## WebSocket 消息验证: {#websocket-message-validation} ¥WebSocket message validation: 与普通路由相同,WebSocket 也接受一个 schema 对象来严格定义请求的类型并进行验证。 ¥Same as normal routes, WebSockets also accept a **schema** object to strictly type and validate requests. ```typescript import { Elysia, t } from 'elysia' const app = new Elysia() .ws('/ws', { // validate incoming message body: t.Object({ message: t.String() }), query: t.Object({ id: t.String() }), message(ws, { message }) { // Get schema from `ws.data` const { id } = ws.data.query ws.send({ id, message, time: Date.now() }) } }) .listen(3000) ``` WebSocket 模式可以验证以下内容: ¥WebSocket schema can validate the following: * message - 一条传入消息。 * query - 查询字符串或 URL 参数。 * params - 路径参数。 * header - 请求的标头。 * cookie - 请求的 cookie * response - 返回值来自处理程序 默认情况下,Elysia 会将传入的字符串化 JSON 消息解析为对象进行验证。 ¥By default Elysia will parse incoming stringified JSON message as Object for validation. ## 配置 {#configuration} ¥Configuration 你可以设置 Elysia 构造函数来设置 Web Socket 值。 ¥You can set Elysia constructor to set the Web Socket value. ```ts import { Elysia } from 'elysia' new Elysia({ websocket: { idleTimeout: 30 } }) ``` Elysia 的 WebSocket 实现扩展了 Bun 的 WebSocket 配置,更多信息请参阅 [Bun 的 WebSocket 文档](https://bun.sh/docs/api/websockets)。 ¥Elysia's WebSocket implementation extends Bun's WebSocket configuration, please refer to [Bun's WebSocket documentation](https://bun.sh/docs/api/websockets) for more information. 以下是 [Bun WebSocket](https://bun.sh/docs/api/websockets#create-a-websocket-server) 的简要配置 ¥The following are a brief configuration from [Bun WebSocket](https://bun.sh/docs/api/websockets#create-a-websocket-server) ### perMessageDeflate {#permessagedeflate} @default `false` 为支持压缩的客户端启用压缩。 ¥Enable compression for clients that support it. 默认情况下,压缩功能处于禁用状态。 ¥By default, compression is disabled. ### maxPayloadLength {#maxpayloadlength} 消息的最大大小。 ¥The maximum size of a message. ### idleTimeout {#idletimeout} @default `120` 如果连接在指定秒数内未收到消息,则会关闭。 ¥After a connection has not received a message for this many seconds, it will be closed. ### backpressureLimit {#backpressurelimit} @default `16777216` (16MB) 单个连接可缓冲的最大字节数。 ¥The maximum number of bytes that can be buffered for a single connection. ### closeOnBackpressureLimit {#closeonbackpressurelimit} @default `false` 如果达到背压限制,请关闭连接。 ¥Close the connection if the backpressure limit is reached. ## 方法 {#methods} ¥Methods 以下是 WebSocket 路由可用的新方法。 ¥Below are the new methods that are available to the WebSocket route ## ws {#ws} 创建一个 websocket 处理程序 ¥Create a websocket handler 示例: ¥Example: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .ws('/ws', { message(ws, message) { ws.send(message) } }) .listen(3000) ``` 类型: ¥Type: ```typescript .ws(endpoint: path, options: Partial>): this ``` * endpoint - 作为 websocket 处理程序公开的路径 * options - 自定义 WebSocket 处理程序行为 ## WebSocketHandler {#websockethandler} WebSocketHandler 从 [config](#configuration) 扩展了配置。 ¥WebSocketHandler extends config from [config](#configuration). 以下是 `ws` 接受的配置。 ¥Below is a config which is accepted by `ws`. ## open {#open} 用于连接新的 WebSocket 的回调函数。 ¥Callback function for new websocket connection. 类型: ¥Type: ```typescript open(ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>): this ``` ## message {#message} 用于接收 WebSocket 消息的回调函数。 ¥Callback function for incoming websocket message. 类型: ¥Type: ```typescript message( ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>, message: Message ): this ``` `Message` 类型基于 `schema.message`。默认值为 `string`。 ¥`Message` type based on `schema.message`. Default is `string`. ## close {#close} 用于关闭 websocket 连接的回调函数。 ¥Callback function for closing websocket connection. 类型: ¥Type: ```typescript close(ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>): this ``` ## drain {#drain} 用于服务器已准备好接收更多数据的回调函数。 ¥Callback function for the server is ready to accept more data. 类型: ¥Type: ```typescript drain( ws: ServerWebSocket<{ // uid for each connection id: string data: Context }>, code: number, reason: string ): this ``` ## parse {#parse} `Parse` 中间件,用于在将 HTTP 连接升级到 WebSocket 之前解析请求。 ¥`Parse` middleware to parse the request before upgrading the HTTP connection to WebSocket. ## beforeHandle {#beforehandle} `Before Handle` 中间件,用于在将 HTTP 连接升级到 WebSocket 之前执行。 ¥`Before Handle` middleware which execute before upgrading the HTTP connection to WebSocket. 理想的验证位置。 ¥Ideal place for validation. ## transform {#transform} `Transform` 中间件,用于在验证之前执行。 ¥`Transform` middleware which execute before validation. ## transformMessage {#transformmessage} 类似 `transform`,但在验证 WebSocket 消息之前执行 ¥Like `transform`, but execute before validation of WebSocket message ## header {#header} 升级到 WebSocket 连接前需要添加的附加标头。 ¥Additional headers to add before upgrade connection to WebSocket. --- --- url: 'https://elysiajs.com/integrations/ai-sdk.md' --- # 与 AI SDK 集成 {#integration-with-ai-sdk} ¥Integration with AI SDK Elysia 轻松支持响应流,让你可以与 [Vercel AI SDK](https://vercel.com/docs/ai) 无缝集成。 ¥Elysia provides a support for response streaming with ease, allowing you to integrate with [Vercel AI SDKs](https://vercel.com/docs/ai) seamlessly. ## 响应流 {#response-streaming} ¥Response Streaming Elysia 支持 `ReadableStream` 和 `Response` 的连续流传输,允许你直接从 AI SDK 返回流。 ¥Elysia support continous streaming of a `ReadableStream` and `Response` allowing you to return stream directly from the AI SDKs. ```ts import { Elysia } from 'elysia' import { streamText } from 'ai' import { openai } from '@ai-sdk/openai' new Elysia().get('/', () => { const stream = streamText({ model: openai('gpt-5'), system: 'You are Yae Miko from Genshin Impact', prompt: 'Hi! How are you doing?' }) // Just return a ReadableStream return result.textStream // [!code ++] // UI Message Stream is also supported return result.toUIMessageStream() // [!code ++] }) ``` Elysia 将自动处理流,允许你以各种方式使用它。 ¥Elysia will handle the stream automatically, allowing you to use it in various ways. ## 服务器发送事件 {#server-sent-event} ¥Server Sent Event Elysia 还支持服务器发送事件,只需将 `ReadableStream` 函数封装为 `sse` 函数即可实现流式响应。 ¥Elysia also supports Server Sent Event for streaming response by simply wrap a `ReadableStream` with `sse` function. ```ts import { Elysia, sse } from 'elysia' // [!code ++] import { streamText } from 'ai' import { openai } from '@ai-sdk/openai' new Elysia().get('/', () => { const stream = streamText({ model: openai('gpt-5'), system: 'You are Yae Miko from Genshin Impact', prompt: 'Hi! How are you doing?' }) // Each chunk will be sent as a Server Sent Event return sse(result.textStream) // [!code ++] // UI Message Stream is also supported return sse(result.toUIMessageStream()) // [!code ++] }) ``` ## 作为响应 {#as-response} ¥As Response 如果你不需要流的类型安全以便进一步使用 [Eden](/eden/overview),则可以直接将流作为响应返回。 ¥If you don't need a type-safety of the stream for further usage with [Eden](/eden/overview), you can return the stream directly as a response. ```ts import { Elysia } from 'elysia' import { ai } from 'ai' import { openai } from '@ai-sdk/openai' new Elysia().get('/', () => { const stream = streamText({ model: openai('gpt-5'), system: 'You are Yae Miko from Genshin Impact', prompt: 'Hi! How are you doing?' }) return result.toTextStreamResponse() // [!code ++] // UI Message Stream Response will use SSE return result.toUIMessageStreamResponse() // [!code ++] }) ``` ## 手动流式传输 {#manual-streaming} ¥Manual Streaming 如果你想更好地控制数据流,可以使用生成器函数手动生成数据块。 ¥If you want to have more control over the streaming, you can use a generator function to yield the chunks manually. ```ts import { Elysia, sse } from 'elysia' import { ai } from 'ai' import { openai } from '@ai-sdk/openai' new Elysia().get('/', async function* () { const stream = streamText({ model: openai('gpt-5'), system: 'You are Yae Miko from Genshin Impact', prompt: 'Hi! How are you doing?' }) for await (const data of result.textStream) // [!code ++] yield sse({ // [!code ++] data, // [!code ++] event: 'message' // [!code ++] }) // [!code ++] yield sse({ event: 'done' }) }) ``` ## 获取 {#fetch} ¥Fetch 如果 AI SDK 不支持你正在使用的模型,你仍然可以使用 `fetch` 函数向 AI SDK 发出请求并直接流式传输响应。 ¥If AI SDK doesn't support model you're using, you can still use the `fetch` function to make requests to the AI SDKs and stream the response directly. ```ts import { Elysia, fetch } from 'elysia' new Elysia().get('/', () => { return fetch('https://api.openai.com/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.OPENAI_API_KEY}` }, body: JSON.stringify({ model: 'gpt-5', stream: true, messages: [ { role: 'system', content: 'You are Yae Miko from Genshin Impact' }, { role: 'user', content: 'Hi! How are you doing?' } ] }) }) }) ``` Elysia 将自动使用流支持代理获取响应。 ¥Elysia will proxy fetch response with streaming support automatically. *** 更多信息,请参阅 [AI SDK 文档](https://ai-sdk.dev/docs/introduction) ¥For additional information, please refer to [AI SDK documentation](https://ai-sdk.dev/docs/introduction) --- --- url: 'https://elysiajs.com/integrations/astro.md' --- # 与 Astro 集成 {#integration-with-astro} ¥Integration with Astro 通过 [Astro 端点](https://astro.nodejs.cn/en/core-concepts/endpoints/),我们可以直接在 Astro 上运行 Elysia。 ¥With [Astro Endpoint](https://astro.nodejs.cn/en/core-concepts/endpoints/), we can run Elysia on Astro directly. 1. 在 astro.config.mjs 中将输出设置为服务器 ```javascript // astro.config.mjs import { defineConfig } from 'astro/config' // https://astro.build/config export default defineConfig({ output: 'server' // [!code ++] }) ``` 2. 创建 pages/\[...slugs].ts 3. 在 \[...slugs].ts 中创建或导入现有的 Elysia 服务器 4. 使用你想要公开的方法名称导出处理程序 ```typescript // pages/[...slugs].ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/api', () => 'hi') .post('/api', ({ body }) => body, { body: t.Object({ name: t.String() }) }) const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] export const GET = handle // [!code ++] export const POST = handle // [!code ++] ``` 由于符合 WinterCG 标准,Elysia 将按预期正常工作。 ¥Elysia will work normally as expected because of WinterCG compliance. 我们建议运行 [Astro 开启 Bun](https://astro.nodejs.cn/en/recipes/bun),因为 Elysia 旨在在 Bun 上运行。 ¥We recommend running [Astro on Bun](https://astro.nodejs.cn/en/recipes/bun) as Elysia is designed to be run on Bun. ::: tip 提示 由于 WinterCG 支持,你可以在 Bun 上运行 Elysia 服务器而无需运行 Astro。 ¥You can run Elysia server without running Astro on Bun thanks to WinterCG support. 然而,如果你在 Node 上运行 Astro,某些插件(如 Elysia Static)可能无法工作。 ¥However, some plugins like **Elysia Static** may not work if you are running Astro on Node. ::: 通过这种方法,你可以将前端和后端共存于一个存储库中,并通过 Eden 实现端到端的类型安全。 ¥With this approach, you can have co-location of both frontend and backend in a single repository and have End-to-end type-safety with Eden. 更多信息请参阅 [Astro 端点](https://astro.nodejs.cn/en/core-concepts/endpoints/)。 ¥Please refer to [Astro Endpoint](https://astro.nodejs.cn/en/core-concepts/endpoints/) for more information. ## 前缀 {#prefix} ¥Prefix 如果你将 Elysia 服务器放置在应用路由的根目录之外,则需要将前缀注释为 Elysia 服务器。 ¥If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. 例如,如果你将 Elysia 服务器放置在 pages/api/\[...slugs].ts 中,则需要将 Elysia 服务器的前缀注释为 /api。 ¥For example, if you place Elysia server in **pages/api/\[...slugs].ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript // pages/api/[...slugs].ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/api' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) const handle = ({ request }: { request: Request }) => app.handle(request) // [!code ++] export const GET = handle // [!code ++] export const POST = handle // [!code ++] ``` 这将确保 Elysia 路由在你放置它的任何位置都能正常工作。 ¥This will ensure that Elysia routing will work properly in any location you place it. --- --- url: 'https://elysiajs.com/integrations/drizzle.md' --- # Drizzle {#drizzle} Drizzle ORM 是一个无头 TypeScript ORM,专注于类型安全和开发者体验。 ¥Drizzle ORM is a headless TypeScript ORM with a focus on type safety and developer experience. 我们可以使用 `drizzle-typebox` 将 Drizzle 模式转换为 Elysia 验证模型。 ¥We may convert Drizzle schema to Elysia validation models using `drizzle-typebox` ### Drizzle Typebox {#drizzle-typebox} [Elysia.t](/essential/validation.html#elysia-type) 是 TypeBox 的一个分支,允许我们在 Elysia 中直接使用任何 TypeBox 类型。 ¥[Elysia.t](/essential/validation.html#elysia-type) is a fork of TypeBox, allowing us to use any TypeBox type in Elysia directly. 我们可以使用 ["drizzle-typebox"](https://npmjs.org/package/drizzle-typebox) 将 Drizzle 模式转换为 TypeBox 模式,并直接在 Elysia 的模式验证中使用它。 ¥We can convert Drizzle schema into TypeBox schema using ["drizzle-typebox"](https://npmjs.org/package/drizzle-typebox), and use it directly on Elysia's schema validation. ### 工作原理如下: {#heres-how-it-works} ¥Here's how it works: 1. 在 Drizzle 中定义数据库模式。 2. 使用 `drizzle-typebox` 将 Drizzle 模式转换为 Elysia 验证模型。 3. 使用转换后的 Elysia 验证模型来确保类型验证。 4. OpenAPI 模式由 Elysia 验证模型生成。 5. 添加 [Eden 条约](/eden/overview),为前端添加类型安全。 ``` * ——————————————— * | | | -> | Documentation | * ————————— * * ———————— * OpenAPI | | | | | drizzle- | | ——————— | * ——————————————— * | Drizzle | —————————-> | Elysia | | | -typebox | | ——————— | * ——————————————— * * ————————— * * ———————— * Eden | | | | -> | Frontend Code | | | * ——————————————— * ``` ## 安装 {#installation} ¥Installation 要安装 Drizzle,请运行以下命令: ¥To install Drizzle, run the following command: ```bash bun add drizzle-orm drizzle-typebox ``` 然后你需要固定 `@sinclair/typebox`,因为 `drizzle-typebox` 和 `Elysia` 之间可能存在版本不匹配的情况,这可能会导致两个版本之间的符号冲突。 ¥Then you need to pin `@sinclair/typebox` as there might be a mismatch version between `drizzle-typebox` and `Elysia`, this may cause conflict of Symbols between two versions. 我们建议使用以下方法将 `@sinclair/typebox` 的版本固定为 `elysia` 使用的最低版本: ¥We recommend pinning the version of `@sinclair/typebox` to the **minimum version** used by `elysia` by using: ```bash grep "@sinclair/typebox" node_modules/elysia/package.json ``` 我们可以使用 `package.json` 中的 `overrides` 字段来固定 `@sinclair/typebox` 的版本: ¥We may use `overrides` field in `package.json` to pin the version of `@sinclair/typebox`: ```json { "overrides": { "@sinclair/typebox": "0.32.4" } } ``` ## Drizzle 模式 {#drizzle-schema} ¥Drizzle schema 假设我们的代码库中有一个 `user` 表,如下所示: ¥Assuming we have a `user` table in our codebase as follows: ::: code-group ```ts [src/database/schema.ts] import { relations } from 'drizzle-orm' import { pgTable, varchar, timestamp } from 'drizzle-orm/pg-core' import { createId } from '@paralleldrive/cuid2' export const user = pgTable( 'user', { id: varchar('id') .$defaultFn(() => createId()) .primaryKey(), username: varchar('username').notNull().unique(), password: varchar('password').notNull(), email: varchar('email').notNull().unique(), salt: varchar('salt', { length: 64 }).notNull(), createdAt: timestamp('created_at').defaultNow().notNull(), } ) export const table = { user } as const export type Table = typeof table ``` ::: ## drizzle-typebox {#drizzle-typebox-1} 我们可以使用 `drizzle-typebox` 将 `user` 表转换为 TypeBox 模型: ¥We may convert the `user` table into TypeBox models by using `drizzle-typebox`: ::: code-group ```ts [src/index.ts] import { createInsertSchema } from 'drizzle-typebox' import { table } from './database/schema' const _createUser = createInsertSchema(table.user, { // Replace email with Elysia's email type email: t.String({ format: 'email' }) }) new Elysia() .post('/sign-up', ({ body }) => { // Create a new user }, { body: t.Omit( _createUser, ['id', 'salt', 'createdAt'] ) }) ``` ::: 这使我们能够在 Elysia 验证模型中重用数据库模式。 ¥This allows us to reuse the database schema in Elysia validation models ## 类型实例化可能无限 {#type-instantiation-is-possibly-infinite} ¥Type instantiation is possibly infinite 如果你收到类似“类型实例化可能无限”的错误,这是由于 `drizzle-typebox` 和 `Elysia` 之间存在循环引用。 ¥If you got an error like **Type instantiation is possibly infinite** this is because of the circular reference between `drizzle-typebox` and `Elysia`. 如果我们将 drizzle-typebox 中的类型嵌套到 Elysia 模式中,将导致类型实例化的无限循环。 ¥If we nested a type from drizzle-typebox into Elysia schema, it will cause an infinite loop of type instantiation. 为了防止这种情况,我们需要在 `drizzle-typebox` 和 `Elysia` 模式之间显式定义一个类型: ¥To prevent this, we need to **explicitly define a type between `drizzle-typebox` and `Elysia`** schema: ```ts import { t } from 'elysia' import { createInsertSchema } from 'drizzle-typebox' import { table } from './database/schema' const _createUser = createInsertSchema(table.user, { email: t.String({ format: 'email' }) }) // ✅ This works, by referencing the type from `drizzle-typebox` const createUser = t.Omit( _createUser, ['id', 'salt', 'createdAt'] ) // ❌ This will cause an infinite loop of type instantiation const createUser = t.Omit( createInsertSchema(table.user, { email: t.String({ format: 'email' }) }), ['id', 'salt', 'createdAt'] ) ``` 如果你想使用 Elysia 类型,请务必为 `drizzle-typebox` 声明一个变量并引用它。 ¥Always declare a variable for `drizzle-typebox` and reference it if you want to use Elysia type ## 实用程序 {#utility} ¥Utility 由于我们可能会使用 `t.Pick` 和 `t.Omit` 来排除或包含某些字段,因此重复此过程可能会很麻烦: ¥As we are likely going to use `t.Pick` and `t.Omit` to exclude or include certain fields, it may be cumbersome to repeat the process: 我们建议使用这些实用程序函数(按原样复制)来简化流程: ¥We recommend using these utility functions **(copy as-is)** to simplify the process: ::: code-group ```ts [src/database/utils.ts] /** * @lastModified 2025-02-04 * @see https://elysia.nodejs.cn/recipe/drizzle.html#utility */ import { Kind, type TObject } from '@sinclair/typebox' import { createInsertSchema, createSelectSchema, BuildSchema, } from 'drizzle-typebox' import { table } from './schema' import type { Table } from 'drizzle-orm' type Spread< T extends TObject | Table, Mode extends 'select' | 'insert' | undefined, > = T extends TObject ? { [K in keyof Fields]: Fields[K] } : T extends Table ? Mode extends 'select' ? BuildSchema< 'select', T['_']['columns'], undefined >['properties'] : Mode extends 'insert' ? BuildSchema< 'insert', T['_']['columns'], undefined >['properties'] : {} : {} /** * Spread a Drizzle schema into a plain object */ export const spread = < T extends TObject | Table, Mode extends 'select' | 'insert' | undefined, >( schema: T, mode?: Mode, ): Spread => { const newSchema: Record = {} let table switch (mode) { case 'insert': case 'select': if (Kind in schema) { table = schema break } table = mode === 'insert' ? createInsertSchema(schema) : createSelectSchema(schema) break default: if (!(Kind in schema)) throw new Error('Expect a schema') table = schema } for (const key of Object.keys(table.properties)) newSchema[key] = table.properties[key] return newSchema as any } /** * Spread a Drizzle Table into a plain object * * If `mode` is 'insert', the schema will be refined for insert * If `mode` is 'select', the schema will be refined for select * If `mode` is undefined, the schema will be spread as is, models will need to be refined manually */ export const spreads = < T extends Record, Mode extends 'select' | 'insert' | undefined, >( models: T, mode?: Mode, ): { [K in keyof T]: Spread } => { const newSchema: Record = {} const keys = Object.keys(models) for (const key of keys) newSchema[key] = spread(models[key], mode) return newSchema as any } ``` ::: 此实用函数会将 Drizzle 模式转换为普通对象,该对象可以通过属性名称作为普通对象进行选择: ¥This utility function will convert Drizzle schema into a plain object, which can pick by property name as plain object: ```ts // ✅ Using spread utility function const user = spread(table.user, 'insert') const createUser = t.Object({ id: user.id, // { type: 'string' } username: user.username, // { type: 'string' } password: user.password // { type: 'string' } }) // ⚠️ Using t.Pick const _createUser = createInsertSchema(table.user) const createUser = t.Pick( _createUser, ['id', 'username', 'password'] ) ``` ### 表格单例 {#table-singleton} ¥Table Singleton 我们建议使用单例模式来存储表结构,这将允许我们从代码库中的任何位置访问表结构: ¥We recommend using a singleton pattern to store the table schema, this will allow us to access the table schema from anywhere in the codebase: ::: code-group ```ts [src/database/model.ts] import { table } from './schema' import { spreads } from './utils' export const db = { insert: spreads({ user: table.user, }, 'insert'), select: spreads({ user: table.user, }, 'select') } as const ``` ::: 这将使我们能够从代码库中的任何位置访问表模式: ¥This will allow us to access the table schema from anywhere in the codebase: ::: code-group ```ts [src/index.ts] import { Elysia } from 'elysia' import { db } from './database/model' const { user } = db.insert new Elysia() .post('/sign-up', ({ body }) => { // Create a new user }, { body: t.Object({ id: user.username, username: user.username, password: user.password }) }) ``` ::: ### 细化 {#refinement} ¥Refinement 如果需要类型优化,可以直接使用 `createInsertSchema` 和 `createSelectSchema` 来优化模式。 ¥If type refinement is needed, you may use `createInsertSchema` and `createSelectSchema` to refine the schema directly. ::: code-group ```ts [src/database/model.ts] import { t } from 'elysia' import { createInsertSchema, createSelectSchema } from 'drizzle-typebox' import { table } from './schema' import { spreads } from './utils' export const db = { insert: spreads({ user: createInsertSchema(table.user, { email: t.String({ format: 'email' }) }), }, 'insert'), select: spreads({ user: createSelectSchema(table.user, { email: t.String({ format: 'email' }) }) }, 'select') } as const ``` ::: 在上面的代码中,我们改进了 `user.email` 模式,使其包含 `format` 属性。 ¥In the code above, we refine a `user.email` schema to include a `format` property `spread` 实用函数将跳过精炼架构,因此你可以按原样使用它。 ¥The `spread` utility function will skip a refined schema, so you can use it as is. *** 更多信息,请参阅 [Drizzle ORM](https://drizzle.nodejs.cn) 和 [Drizzle TypeBox](https://drizzle.nodejs.cn/docs/typebox) 文档。 ¥For more information, please refer to the [Drizzle ORM](https://drizzle.nodejs.cn) and [Drizzle TypeBox](https://drizzle.nodejs.cn/docs/typebox) documentation. --- --- url: 'https://elysiajs.com/integrations/expo.md' --- # 与 Expo 集成 {#integration-with-expo} ¥Integration with Expo 从 Expo SDK 50 和 App Router v3 开始,Expo 允许我们直接在 Expo 应用中创建 API 路由。 ¥Starting from Expo SDK 50, and App Router v3, Expo allows us to create API route directly in an Expo app. 1. 如果 Expo 应用不存在,则创建一个: ```typescript bun create expo-app --template tabs ``` 2. 创建 app/\[...slugs]+api.ts 3. 在 \[...slugs]+api.ts 中,创建或导入现有的 Elysia 服务器 4. 使用你想要公开的方法名称导出处理程序 ```typescript // app/[...slugs]+api.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', () => 'hello Next') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle // [!code ++] export const POST = app.handle // [!code ++] ``` 由于符合 WinterCG 标准,Elysia 将按预期正常工作,但是,如果你在 Node 上运行 Expo,某些插件(例如 Elysia Static)可能无法正常工作。 ¥Elysia will work normally as expected because of WinterCG compliance, however, some plugins like **Elysia Static** may not work if you are running Expo on Node. 你可以将 Elysia 服务器视为普通的 Expo API 路由。 ¥You can treat the Elysia server as if normal Expo API route. 通过这种方法,你可以将前端和后端共存于一个存储库中,并且 [使用 Eden 实现端到端类型安全](https://elysia.nodejs.cn/eden/overview.html) 可以同时执行客户端和服务器操作。 ¥With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type safety with Eden](https://elysia.nodejs.cn/eden/overview.html) with both client-side and server action. 更多信息请参阅 [API 路由](https://expo.nodejs.cn/router/reference/api-routes/)。 ¥Please refer to [API route](https://expo.nodejs.cn/router/reference/api-routes/) for more information. ## 前缀 {#prefix} ¥Prefix 如果你将 Elysia 服务器放置在应用路由的根目录之外,则需要将前缀注释为 Elysia 服务器。 ¥If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. 例如,如果你将 Elysia 服务器放在 app/api/\[...slugs]+api.ts 中,则需要将 Elysia 服务器的前缀注释为 /api。 ¥For example, if you place Elysia server in **app/api/\[...slugs]+api.ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript // app/api/[...slugs]+api.ts import { Elysia, t } from 'elysia' const app = new Elysia({ prefix: '/api' }) .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) export const GET = app.handle export const POST = app.handle ``` 这将确保 Elysia 路由在你放置它的任何位置都能正常工作。 ¥This will ensure that Elysia routing will work properly in any location you place it in. ## 部署 {#deployment} ¥Deployment 你可以直接使用 Elysia 的 API 路由,并在需要时像普通的 Elysia 应用一样部署,或者使用 [实验性 Expo 服务器运行时](https://expo.nodejs.cn/router/reference/api-routes/#deployment)。 ¥You can either directly use API route using Elysia and deploy as normal Elysia app normally if need or using [experimental Expo server runtime](https://expo.nodejs.cn/router/reference/api-routes/#deployment). 如果你使用 Expo 服务器运行时,则可以使用 `expo export` 命令为你的 expo 应用创建优化构建,这将包含一个使用 Elysia 的 Expo 函数,路径为 dist/server/\_expo/functions/\[...slugs]+api.js。 ¥If you are using Expo server runtime, you may use `expo export` command to create optimized build for your expo app, this will include an Expo function which is using Elysia at **dist/server/\_expo/functions/\[...slugs]+api.js** ::: tip 提示 请注意,Expo 函数被视为 Edge 函数而非普通服务器,因此直接运行 Edge 函数不会分配任何端口。 ¥Please note that Expo Functions are treated as Edge functions instead of normal server, so running the Edge function directly will not allocate any port. ::: 你可以使用 Expo 提供的 Expo 函数适配器来部署你的 Edge Function。 ¥You may use the Expo function adapter provided by Expo to deploy your Edge Function. 目前 Expo 支持以下适配器: ¥Currently Expo support the following adapter: * [Express](https://expo.nodejs.cn/router/reference/api-routes/#express) * [Netlify](https://expo.nodejs.cn/router/reference/api-routes/#netlify) * [Vercel](https://expo.nodejs.cn/router/reference/api-routes/#vercel) --- --- url: 'https://elysiajs.com/integrations/nuxt.md' --- # 与 Nuxt 集成 {#integration-with-nuxt} ¥Integration with Nuxt 我们可以使用 Nuxt 社区插件 [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia),通过 Eden Treaty 在 Nuxt API 路由上设置 Elysia。 ¥We can use [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia), a community plugin for Nuxt, to setup Elysia on Nuxt API route with Eden Treaty. 1. 使用以下命令安装插件: ```bash bun add elysia @elysiajs/eden bun add -d nuxt-elysia ``` 2. 将 `nuxt-elysia` 添加到 Nuxt 配置中: ```ts export default defineNuxtConfig({ modules: [ // [!code ++] 'nuxt-elysia' // [!code ++] ] // [!code ++] }) ``` 3. 在项目根目录下创建 `api.ts`: ```typescript [api.ts] export default () => new Elysia() // [!code ++] .get('/hello', () => ({ message: 'Hello world!' })) // [!code ++] ``` 4. 在你的 Nuxt 应用中使用 Eden Treaty: ```vue ``` 这将自动设置 Elysia 在 Nuxt API 路由上运行。 ¥This will automatically setup Elysia to run on Nuxt API route automatically. ## 前缀 {#prefix} ¥Prefix 默认情况下,Elysia 将挂载在 /\_api 上,但我们可以使用 `nuxt-elysia` 配置进行自定义。 ¥By default, Elysia will be mounted on **/\_api** but we can customize it with `nuxt-elysia` config. ```ts export default defineNuxtConfig({ nuxtElysia: { path: '/api' // [!code ++] } }) ``` 这将把 Elysia 挂载到 /api 而不是 /\_api。 ¥This will mount Elysia on **/api** instead of **/\_api**. 更多配置,请参阅 [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) ¥For more configuration, please refer to [nuxt-elysia](https://github.com/tkesgar/nuxt-elysia) --- --- url: 'https://elysiajs.com/integrations/prisma.md' --- # Prisma {#prisma} [Prisma](https://prisma.nodejs.cn) 是一个 ORM,允许我们以类型安全的方式与数据库交互。 ¥[Prisma](https://prisma.nodejs.cn) is an ORM that allows us to interact with databases in a type-safe manner. 它提供了一种使用 Prisma 模式文件定义数据库模式的方法,然后基于该模式生成 TypeScript 类型。 ¥It provides a way to define your database schema using a Prisma schema file, and then generates TypeScript types based on that schema. ### Prismabox {#prismabox} [Prismabox](https://github.com/m1212e/prismabox) 是一个库,可以从 Prisma 模式生成 TypeBox 或 Elysia 验证模型。 ¥[Prismabox](https://github.com/m1212e/prismabox) is a library that generate TypeBox or Elysia validation models from Prisma schema. 我们可以使用 Prismabox 将 Prisma 模式转换为 Elysia 验证模型,然后可以使用这些模型来确保 Elysia 中的类型验证。 ¥We can use Prismabox to convert Prisma schema into Elysia validation models, which can then be used to ensure type validation in Elysia. ### 工作原理如下: {#heres-how-it-works} ¥Here's how it works: 1. 在 Prisma Schema 中定义数据库模式。 2. 添加 `prismabox` 生成器,用于生成 Elysia 模式。 3. 使用转换后的 Elysia 验证模型来确保类型验证。 4. OpenAPI 模式由 Elysia 验证模型生成。 5. 添加 [Eden 条约](/eden/overview),为前端添加类型安全。 ``` * ——————————————— * | | | -> | Documentation | * ————————— * * ———————— * OpenAPI | | | | | prismabox | | ——————— | * ——————————————— * | Prisma | —————————-> | Elysia | | | | | ——————— | * ——————————————— * * ————————— * * ———————— * Eden | | | | -> | Frontend Code | | | * ——————————————— * ``` ## 安装 {#installation} ¥Installation 要安装 Prisma,请运行以下命令命令: ¥To install Prisma, run the following command: ```bash bun add @prisma/client prismabox && \ bun add -d prisma ``` ## Prisma 模式 {#prisma-schema} ¥Prisma schema 假设你已经有一个 `prisma/schema.prisma`。 ¥Assuming you already have a `prisma/schema.prisma`. 我们可以将 `prismabox` 生成器添加到 Prisma 模式文件,如下所示: ¥We can add a `prismabox` generator to the Prisma schema file as follows: ::: code-group ```ts [prisma/schema.prisma] generator client { provider = "prisma-client-js" output = "../generated/prisma" } datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator prismabox { // [!code ++] provider = "prismabox" // [!code ++] typeboxImportDependencyName = "elysia" // [!code ++] typeboxImportVariableName = "t" // [!code ++] inputModel = true // [!code ++] output = "../generated/prismabox" // [!code ++] } // [!code ++] model User { id String @id @default(cuid()) email String @unique name String? posts Post[] } model Post { id String @id @default(cuid()) title String content String? published Boolean @default(false) author User @relation(fields: [authorId], references: [id]) authorId String } ``` ::: 这将在 `generated/prismabox` 目录中生成 Elysia 验证模型。 ¥This will generate Elysia validation models in the `generated/prismabox` directory. 每个模型都有自己的文件,并且模型将根据 Prisma 模型名称命名。 ¥Each model will have its own file, and the models will be named based on the Prisma model names. 例如: ¥For example: * `User` 模型将生成为 `generated/prismabox/User.ts` * `Post` 模型将生成为 `generated/prismabox/Post.ts` ## 使用生成的模型 {#using-generated-models} ¥Using generated models 然后我们可以将生成的模型导入到 Elysia 应用中: ¥Then we can import the generated models in our Elysia application: ::: code-group ```ts [src/index.ts] import { Elysia, t } from 'elysia' import { PrismaClient } from '../generated/prisma' // [!code ++] import { UserPlain, UserPlainInputCreate } from '../generated/prismabox/User' // [!code ++] const prisma = new PrismaClient() const app = new Elysia() .put( '/', async ({ body }) => prisma.user.create({ data: body }), { body: UserPlainInputCreate, // [!code ++] response: UserPlain // [!code ++] } ) .get( '/id/:id', async ({ params: { id }, status }) => { const user = await prisma.user.findUnique({ where: { id } }) if (!user) return status(404, 'User not found') return user }, { response: { 200: UserPlain, // [!code ++] 404: t.String() // [!code ++] } } ) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ::: 这使我们能够在 Elysia 验证模型中重用数据库模式。 ¥This allows us to reuse the database schema in Elysia validation models. *** 更多信息,请参阅 [Prisma](https://prisma.nodejs.cn) 和 [Prismabox](https://github.com/m1212e/prismabox) 文档。 ¥For more information, please refer to the [Prisma](https://prisma.nodejs.cn), and [Prismabox](https://github.com/m1212e/prismabox) documentation. --- --- url: 'https://elysiajs.com/integrations/sveltekit.md' --- # 与 SvelteKit 集成 {#integration-with-sveltekit} ¥Integration with SvelteKit 使用 SvelteKit,你可以在服务器路由上运行 Elysia。 ¥With SvelteKit, you can run Elysia on server routes. 1. 创建 src/routes/\[...slugs]/+server.ts。 2. 在 +server.ts 中,创建或导入一个现有的 Elysia 服务器 3. 使用你想要公开的方法名称导出处理程序。你还可以使用 `fallback` 让 Elysia 处理所有方法。 ```typescript // src/routes/[...slugs]/+server.ts import { Elysia, t } from 'elysia'; const app = new Elysia() .get('/', () => 'hello SvelteKit') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) type RequestHandler = (v: { request: Request }) => Response | Promise export const GET: RequestHandler = ({ request }) => app.handle(request) export const POST: RequestHandler = ({ request }) => app.handle(request) // or simply export const fallback: RequestHandler = ({ request }) => app.handle(request) ``` 你可以将 Elysia 服务器视为普通的 SvelteKit 服务器路由。 ¥You can treat the Elysia server as a normal SvelteKit server route. 通过这种方法,你可以将前端和后端共存于一个存储库中,并且 [使用 Eden 实现端到端类型安全](https://elysia.nodejs.cn/eden/overview.html) 可以同时执行客户端和服务器操作。 ¥With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type-safety with Eden](https://elysia.nodejs.cn/eden/overview.html) with both client-side and server action 更多信息请参阅 [SvelteKit 路由](https://kit.svelte.dev/docs/routing#server)。 ¥Please refer to [SvelteKit Routing](https://kit.svelte.dev/docs/routing#server) for more information. ## 前缀 {#prefix} ¥Prefix 如果你将 Elysia 服务器放置在应用路由的根目录之外,则需要将前缀注释为 Elysia 服务器。 ¥If you place an Elysia server not in the root directory of the app router, you need to annotate the prefix to the Elysia server. 例如,如果你将 Elysia 服务器放置在 src/routes/api/\[...slugs]/+server.ts 中,则需要将 Elysia 服务器的前缀注释为 /api。 ¥For example, if you place Elysia server in **src/routes/api/\[...slugs]/+server.ts**, you need to annotate prefix as **/api** to Elysia server. ```typescript twoslash // src/routes/api/[...slugs]/+server.ts import { Elysia, t } from 'elysia'; const app = new Elysia({ prefix: '/api' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) type RequestHandler = (v: { request: Request }) => Response | Promise export const fallback: RequestHandler = ({ request }) => app.handle(request) ``` 这将确保 Elysia 路由在你放置它的任何位置都能正常工作。 ¥This will ensure that Elysia routing will work properly in any location you place it. --- --- url: 'https://elysiajs.com/integrations/vercel.md' --- # 与 Vercel Function 集成 {#integration-with-vercel-function} ¥Integration with Vercel Function Vercel 函数默认支持 Web 标准框架,因此你无需任何额外配置即可在 Vercel 函数上运行 Elysia。 ¥Vercel Function support Web Standard Framework by default, so you can run Elysia on Vercel Function without any additional configuration. 1. 在 src/index.ts 创建一个文件 2. 在 src/index.ts 中,创建或导入现有的 Elysia 服务器 3. 将 Elysia 服务器导出为默认导出 ```typescript import { Elysia, t } from 'elysia' export default new Elysia() .get('/', () => 'Hello Vercel Function') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) ``` 4. 添加构建脚本,使用 `tsdown` 或类似脚本将代码打包成单个文件。 ```json { "scripts": { "build": "tsdown src/index.ts -d api --dts" } } ``` 5. 创建 vercel.json 文件,将所有端点重写到 Elysia 服务器 ```json { "$schema": "https://openapi.vercel.sh/vercel.json", "rewrites": [ { "source": "/(.*)", "destination": "/api" } ] } ``` 此配置会将所有请求重写到 `/api` 路由,Elysia 服务器就是在该路由中定义的。 ¥This configuration will rewrite all requests to the `/api` route, which is where Elysia server is defined. Elysia 无需额外配置即可使用 Vercel 函数,因为它默认支持 Web 标准框架。 ¥No additional configuration is needed for Elysia to work with Vercel Function, as it supports the Web Standard Framework by default. ## 如果这不起作用 {#if-this-doesnt-work} ¥If this doesn't work 确保将 Elysia 服务器导出为默认导出,并且构建输出是位于 `/api/index.js` 中的单个文件。 ¥Make sure to export the Elysia server as default export, and the build output is a single file locate in `/api/index.js`. 你还可以使用 Elysia 的内置功能,例如验证、错误处理、[OpenAPI](/plugins/openapi.html) 等,就像在其他任何环境中一样。 ¥You can also use Elysia's built-in features like validation, error handling, [OpenAPI](/plugins/openapi.html) and more, just like you would in any other environment. 更多信息,请参阅 [Vercel 函数文档](https://vercel.com/docs/functions?framework=other)。 ¥For additional information, please refer to [Vercel Function documentation](https://vercel.com/docs/functions?framework=other). --- --- url: 'https://elysiajs.com/migrate/from-express.md' --- # 从 Express 到 Elysia {#from-express-to-elysia} ¥From Express to Elysia 本指南面向 Express 用户,帮助他们了解 Elysia 与 Express 之间的差异(包括语法),以及如何通过示例将应用从 Express 迁移到 Elysia。 ¥This guide is for Express users who want to see the differences from Express including syntax, and how to migrate your application from Express to Elysia by example. Express 是一个流行的 Node.js Web 框架,广泛用于构建 Web 应用和 API。它以简洁灵活而闻名。 ¥**Express** is a popular web framework for Node.js, and widely used for building web applications and APIs. It is known for its simplicity and flexibility. Elysia 是一个符合人机工程学的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计符合人机工程学且方便开发者使用,重点关注可靠的类型安全性和性能。 ¥**Elysia** is an ergonomic web framework for Bun, Node.js, and runtimes that support Web Standard API. Designed to be ergonomic and developer-friendly with a focus on **sound type safety** and performance. ## 性能 {#performance} ¥Performance 得益于原生的 Bun 实现和静态代码分析,Elysia 的性能相比 Express 有显著提升。 ¥Elysia has significant performance improvements over Express thanks to native Bun implementation, and static code analysis. ## 路由 {#routing} ¥Routing Express 和 Elysia 具有相似的路由语法,使用 `app.get()` 和 `app.post()` 方法定义路由,并且路径参数语法也相似。 ¥Express and Elysia have similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameter syntax. ::: code-group ```ts [Express] 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` 作为请求和响应对象 ::: code-group ```ts [Elysia] 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 建议使用方法链和对象解构。 ¥There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. 如果你不需要使用上下文,Elysia 还支持在响应中使用内联值。 ¥Elysia also supports an inline value for the response if you don't need to use the context. ## 处理程序 {#handler} ¥Handler 两者都具有类似的属性,用于访问输入参数,例如 `headers`、`query`、`params` 和 `body`。 ¥Both has a simliar property for accessing input parameters like `headers`, `query`, `params`, and `body`. ::: code-group ```ts [Express] 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 主体。 ::: code-group ```ts [Elysia] 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 编码数据和表单数据 ## 子路由 {#subrouter} ¥Subrouter Express 使用专用的 `express.Router()` 来声明子路由,而 Elysia 将每个实例视为可即插即用的组件。 ¥Express use a dedicated `express.Router()` for declaring a sub router while Elysia treats every instances as a component that can be plug and play together. ::: code-group ```ts [Express] 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()` 创建子路由 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia 将每个实例视为一个组件 ## 验证 {#validation} ¥Validation Elysia 内置了使用 TypeBox 进行请求验证的支持,确保类型安全,并开箱即用地支持标准 Schema,允许你使用你喜欢的库,例如 Zod、Valibot、ArkType、Effect Schema 等等。虽然 Express 没有提供内置验证,并且需要根据每个验证库进行手动类型声明。 ¥Elysia has a built-in support for request validation using TypeBox sounds type safety, and support for Standard Schema out of the box allowing you to use your favorite library like Zod, Valibot, ArkType, Effect Schema and so on. While Express doesn't offers a built-in validation, and require a manual type declaration based on each validation library. ::: code-group ```ts [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`)来验证请求主体 ::: code-group ```ts twoslash [Elysia TypeBox] 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() }) }) ``` ```ts twoslash [Elysia Zod] import { Elysia } from 'elysia' import { z } from 'zod' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: z.object({ id: z.number() }), body: z.object({ name: z.string() }) }) ``` ```ts twoslash [Elysia Valibot] import { Elysia } from 'elysia' import * as v from 'valibot' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: v.object({ id: v.number() }), body: v.object({ name: v.string() }) }) ``` ::: > Elysia 使用 TypeBox 进行验证,并自动强制类型转换。同时支持使用相同语法的各种验证库,例如 Zod、Valibot。 ## 文件上传 {#file-upload} ¥File upload Express 使用外部库 `multer` 来处理文件上传,而 Elysia 内置了对文件和表单数据的支持,并使用声明式 API 进行 mimetype 验证。 ¥Express use an external library `multer` to handle file upload, while Elysia has a built-in support for file and formdata, mimetype valiation using declarative API. ::: code-group ```ts [Express] 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('No file uploaded') const type = await fileTypeFromFile(file.path) if (!type || !type.mime.startsWith('image/')) return res .status(422) .send('File is not a valid image') const filePath = path.resolve(file.path) res.sendFile(filePath) }) ``` ::: > Express 需要 `express.json()` 中间件来解析 JSON 主体。 ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia 处理文件,并以声明方式进行 mimetype 验证 由于 multer 不验证 mimetype,你需要使用 file-type 或类似的库手动验证 mimetype。 ¥As **multer** doesn't validate mimetype, you need to validate the mimetype manually using **file-type** or similar library. Elysia 验证文件上传,并使用 file-type 自动验证 mimetype。 ¥Elysia validate file upload, and use **file-type** to validate mimetype automatically. ## 中间件 {#middleware} ¥Middleware Express 中间件使用基于队列的单一顺序,而 Elysia 使用基于事件的生命周期提供更精细的控制。 ¥Express middleware use a single queue-based order while Elysia give you a more granular control using an **event-based** lifecycle. Elysia 的生命周期事件如下图所示。 ¥Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > 点击图片放大 虽然 Express 按顺序为请求管道提供单一流程,但 Elysia 可以拦截请求管道中的每个事件。 ¥While Express has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline. ::: code-group ```ts [Express] import express from 'express' const app = express() // Global middleware app.use((req, res, next) => { console.log(`${req.method} ${req.url}`) next() }) app.get( '/protected', // Route-specific middleware (req, res, next) => { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') next() }, (req, res) => { res.send('Protected route') } ) ``` ::: > Express 对按顺序执行的中间件使用单个基于队列的顺序。 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest(({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia 在请求管道中的每个点使用特定的事件拦截器 虽然 Hono 有一个 `next` 函数来调用下一个中间件,但 Elysia 没有。 ¥While Hono has a `next` function to call the next middleware, Elysia does not has one. ## 听起来类型安全 {#sounds-type-safety} ¥Sounds type safety Elysia 的设计注重类型安全。 ¥Elysia is designed to be sounds type safety. 例如,你可以使用 [derive](/essential/life-cycle.html#derive) 和 [resolve](/essential/life-cycle.html#resolve) 以类型安全的方式自定义上下文,而 Express 则不行。 ¥For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Express doesn't. ::: code-group ```ts twoslash [Express] // @errors: 2339 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) // ^? }) const authenticate = (req: Request, res: Response, next: Function) => { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') // @ts-ignore req.token = token.split(' ')[1] next() } app.get('/token', getVersion, authenticate, (req, res) => { req.version // ^? res.send(req.token) // ^? }) ``` ::: > Express 对按顺序执行的中间件使用单个基于队列的顺序。 ::: code-group ```ts twoslash [Elysia] 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` 接口,但它是全局可用的,并且不具备良好的类型安全性,并且不保证该属性在所有请求处理程序中都可用。 ¥While Express can, use `declare module` to extend the `Request` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'express' { interface Request { version: number token: string } } ``` > 这是上述 Express 示例运行所必需的,它不提供声音类型安全。 ## 中间件参数 {#middleware-parameter} ¥Middleware parameter Express 使用函数返回一个插件来定义可复用的特定路由中间件,而 Elysia 使用 [macro](/patterns/macro) 来定义自定义钩子。 ¥Express use a function to return a plugin to define a reusable route-specific middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Express] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 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('Unauthorized') // @ts-ignore req.user = user next() } app.get('/token', role('admin'), (req, res) => { res.send(req.user) // ^? }) ``` ::: > Express 使用函数回调来接受中间件的自定义参数 ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- 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 使用宏将自定义参数传递给自定义中间件 ## 错误处理 {#error-handling} ¥Error handling Express 对所有路由使用单个错误处理程序,而 Elysia 对错误处理提供更精细的控制。 ¥Express use a single error handler for all routes, while Elysia provides a more granular control over error handling. ::: code-group ```ts import express from 'express' const app = express() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.use((error, req, res, next) => { if(error instanceof CustomError) { res.status(500).json({ message: 'Something went wrong!', error }) } }) // route-specific error handler app.get('/error', (req, res) => { throw new CustomError('oh uh') }) ``` ::: > Express 使用中间件处理错误,所有路由使用同一个错误处理程序。 ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia 提供更精细的错误处理控制和作用域机制 虽然 Express 使用中间件提供错误处理,但 Elysia 提供: ¥While Express offers error handling using middleware, Elysia provide: 1. 全局和路由特定的错误处理程序 2. 用于映射 HTTP 状态的简写,以及用于将错误映射到响应的 `toResponse` 3. 为每个错误提供自定义错误代码 错误代码对于日志记录和调试非常有用,并且在区分扩展同一类的不同错误类型时非常重要。 ¥The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## 封装 {#encapsulation} ¥Encapsulation Express 中间件是全局注册的,而 Elysia 通过显式的作用域机制和代码顺序允许你控制插件的副作用。 ¥Express middleware is registered globally, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Express] 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('Unauthorized') next() }) app.use(subRouter) // has side-effect from subRouter app.get('/side-effect', (req, res) => { res.send('hi') }) ``` ::: > Express 不处理中间件的副作用,需要使用前缀来分隔副作用。 ::: code-group ```ts [Elysia] 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) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Elysia 会将副作用封装到插件中。 默认情况下,Elysia 会将生命周期事件和上下文封装到所使用的实例中,因此除非明确说明,否则插件的副作用不会影响父实例。 ¥By default, Elysia will encapsulate lifecycle events and context to the instance that is used, so that the side-effect of a plugin will not affect parent instance unless explicitly stated. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia 提供三种作用域机制: ¥Elysia offers 3 type of scoping mechanism: 1. local - 仅应用于当前实例,无副作用(默认) 2. scoped - 作用域副作用作用于父实例,但不会超出父实例的范围 3. global - 影响所有实例 虽然 Express 可以通过添加前缀来限定中间件副作用的范围,但这并不是真正的封装。副作用仍然存在,但与任何以该前缀开头的路由分开,这给开发者增加了记住哪个前缀具有副作用的心理负担。 ¥While Express can scope the middleware side-effect by adding a prefix, it isn't a true encapsulation. The side-effect is still there but separated to any routes starts with said prefix, adding a mental overhead to the developer to memorize which prefix has side-effect. 你可以执行以下操作: ¥Which you can do the following: 1. 移动代码顺序,但前提是存在具有副作用的单个实例。 2. 添加前缀,但副作用仍然存在。如果其他实例具有相同的前缀,则会产生副作用。 由于 Express 并未提供真正的封装,这会导致调试过程变得非常困难。 ¥This can leads to a nightmarish scenario to debug as Express doesn't offers true encapsulation. ## Cookie {#cookie} Express 使用外部库 `cookie-parser` 来解析 Cookie,而 Elysia 内置了对 Cookie 的支持,并使用基于信号的方法来处理 Cookie。 ¥Express use an external library `cookie-parser` to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies. ::: code-group ```ts [Express] 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` 解析 Cookie ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia 使用基于信号的方法来处理 Cookie ## OpenAPI {#openapi} Express 需要单独的 OpenAPI、验证和类型安全配置,而 Elysia 内置了对 OpenAPI 的支持,并使用架构作为单一事实来源。 ¥Express require a separate configuration for OpenAPI, validation, and type safety while Elysia has a built-in support for OpenAPI using schema as a **single source of truth**. ::: code-group ```ts [Express] import express from 'express' import swaggerUi from 'swagger-ui-express' const app = express() app.use(express.json()) app.post('/users', (req, res) => { // TODO: validate request body res.status(201).json(req.body) }) const swaggerSpec = { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' }, paths: { '/users': { post: { summary: 'Create user', requestBody: { content: { 'application/json': { schema: { type: 'object', properties: { name: { type: 'string', description: 'First name only' }, age: { type: 'integer' } }, required: ['name', 'age'] } } } }, responses: { '201': { description: 'User created' } } } } } } app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec)) ``` ::: > Express 需要单独的 OpenAPI、验证和类型安全配置 ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' // [!code ++] const app = new Elysia() .use(openapi()) // [!code ++] .model({ user: t.Array( t.Object({ name: t.String(), age: t.Number() }) ) }) .post('/users', ({ body }) => body, { // ^? body: 'user', response: { 201: 'user' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia 使用模式作为单一事实来源 Elysia 将根据你提供的模式生成 OpenAPI 规范,并根据该模式验证请求和响应,并自动推断类型。 ¥Elysia will generate OpenAPI specification based on the schema you provided, and validate the request and response based on the schema, and infer type automatically. Elysia 还将在 `model` 中注册的模式附加到 OpenAPI 规范中,允许你在 Swagger 或 Scalar UI 的专用部分中引用该模型。 ¥Elysia also appends the schema registered in `model` to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI. ## 测试 {#testing} ¥Testing Express 使用单个 `supertest` 库来测试应用,而 Elysia 构建于 Web 标准 API 之上,因此可以与任何测试库一起使用。 ¥Express use a single `supertest` library to test the application, while Elysia is built on top of Web Standard API allowing it be used with any testing library. ::: code-group ```ts [Express] 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('should return Hello World', async () => { const res = await request(app).get('/') expect(res.status).toBe(200) expect(res.text).toBe('Hello World') }) }) ``` ::: > Express 使用 `supertest` 库来测试应用 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return 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](/eden/overview) 的辅助库,用于端到端类型安全,允许我们使用自动补全和完全类型安全进行测试。 ¥Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] 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('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## 端到端类型安全 {#end-to-end-type-safety} ¥End-to-end type safety Elysia 内置了端到端类型安全支持,无需使用 [Eden](/eden/overview) 进行代码生成,而 Express 不提供此功能。 ¥Elysia offers a built-in support for **end-to-end type safety** without code generation using [Eden](/eden/overview), Express doesn't offers one. ::: code-group ```ts twoslash [Elysia] 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) // ^? // ---cut-after--- console.log('ok') ``` ::: 如果端到端类型安全对你来说很重要,那么 Elysia 是正确的选择。 ¥If end-to-end type safety is important for you then Elysia is the right choice. *** Elysia 提供了更符合人机工程学且对开发者更友好的体验,注重性能、类型安全和简洁性。Express 是 Node.js 的流行 Web 框架,但在性能和简洁性方面存在一些局限性。 ¥Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Express is a popular web framework for Node.js, but it has some limitations when it comes to performance and simplicity. 如果你正在寻找一个易于使用、具有良好的开发体验并且基于 Web 标准 API 构建的框架,那么 Elysia 是你的正确选择。 ¥If you are looking for a framework that is easy to use, has a great developer experience, and is built on top of Web Standard API, Elysia is the right choice for you. 或者,如果你来自其他框架,你可以查看: ¥Alternatively, if you are coming from a different framework, you can check out: --- --- url: 'https://elysiajs.com/migrate/from-fastify.md' --- # 从 Fastify 到 Elysia {#from-fastify-to-elysia} ¥From Fastify to Elysia 本指南面向 Fastify 用户,帮助他们了解 Elysia 与 Fastify 之间的差异(包括语法),以及如何通过示例将应用从 Fastify 迁移到 Elysia。 ¥This guide is for Fastify users who want to see a differences from Fastify including syntax, and how to migrate your application from Fastify to Elysia by example. Fastify 是一款快速、低开销的 Node.js Web 框架,设计简洁易用。它建立在 HTTP 模块之上,并提供了一系列功能,使构建 Web 应用变得容易。 ¥**Fastify** is a fast and low overhead web framework for Node.js, designed to be simple and easy to use. It is built on top of the HTTP module and provides a set of features that make it easy to build web applications. Elysia 是一个符合人机工程学的 Web 框架,适用于 Bun、Node.js 和支持 Web 标准 API 的运行时。设计符合人机工程学且方便开发者使用,重点关注可靠的类型安全性和性能。 ¥**Elysia** is an ergonomic web framework for Bun, Node.js, and runtime that supports Web Standard API. Designed to be ergonomic and developer-friendly with a focus on **sound type safety** and performance. ## 性能 {#performance} ¥Performance 得益于原生的 Bun 实现和静态代码分析,Elysia 的性能相比 Fastify 有显著提升。 ¥Elysia has significant performance improvements over Fastify thanks to native Bun implementation, and static code analysis. ## 路由 {#routing} ¥Routing Fastify 和 Elysia 的路由语法相似,使用 `app.get()` 和 `app.post()` 方法来定义路由,路径参数的语法也类似。 ¥Fastify and Elysia has similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameters syntax. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() app.get('/', (request, reply) => { res.send('Hello World') }) app.post('/id/:id', (request, reply) => { reply.status(201).send(req.params.id) }) app.listen({ port: 3000 }) ``` ::: > Fastify 使用 `request` 和 `reply` 作为请求和响应对象 ::: code-group ```ts [Elysia] 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 建议使用方法链和对象解构。 ¥There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. 如果你不需要使用上下文,Elysia 还支持在响应中使用内联值。 ¥Elysia also supports an inline value for the response if you don't need to use the context. ## 处理程序 {#handler} ¥Handler 两者都具有类似的属性,用于访问输入参数,例如 `headers`、`query`、`params` 和 `body`,并自动将请求正文解析为 JSON、URL 编码数据和表单数据。 ¥Both has a simliar property for accessing input parameters like `headers`, `query`, `params`, and `body`, and automatically parse the request body to JSON, URL-encoded data, and formdata. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() app.post('/user', (request, reply) => { const limit = request.query.limit const name = request.body.name const auth = request.headers.authorization reply.send({ limit, name, auth }) }) ``` ::: > Fastify 解析数据并将其放入 `request` 对象 ::: code-group ```ts [Elysia] 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 解析数据并将其放入 `context` 对象 ## 子路由 {#subrouter} ¥Subrouter Fastify 使用函数回调函数定义子路由,而 Elysia 将每个实例视为可即插即用的组件。 ¥Fastify use a function callback to define a subrouter while Elysia treats every instances as a component that can be plug and play together. ::: code-group ```ts [Fastify] import fastify, { FastifyPluginCallback } from 'fastify' const subRouter: FastifyPluginCallback = (app, opts, done) => { app.get('/user', (request, reply) => { reply.send('Hello User') }) } const app = fastify() app.register(subRouter, { prefix: '/api' }) ``` ::: > Fastify 使用函数回调函数声明子路由。 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia 将每个实例视为一个组件 Elysia 在构造函数中设置前缀,而 Fastify 要求你在选项中设置前缀。 ¥While Elysia set the prefix in the constructor, Fastify requires you to set the prefix in the options. ## 验证 {#validation} ¥Validation Elysia 内置了对请求验证的支持,使用 TypeBox 开箱即用地保证了声音类型安全,而 Fastify 使用 JSON Schema 声明模式,并使用 ajv 进行验证。 ¥Elysia has a built-in support for request validation with sounds type safety out of the box using **TypeBox** while Fastify use JSON Schema for declaring schema, and **ajv** for validation. 然而,它不会自动推断类型,你需要使用像 `@fastify/type-provider-json-schema-to-ts` 这样的类型提供程序来推断类型。 ¥However, doesn't infer type automatically, and you need to use a type provider like `@fastify/type-provider-json-schema-to-ts` to infer type. ::: code-group ```ts [Fastify] import fastify from 'fastify' import { JsonSchemaToTsProvider } from '@fastify/type-provider-json-schema-to-ts' const app = fastify().withTypeProvider() app.patch( '/user/:id', { schema: { params: { type: 'object', properties: { id: { type: 'string', pattern: '^[0-9]+$' } }, required: ['id'] }, body: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, } }, (request, reply) => { // map string to number request.params.id = +request.params.id reply.send({ params: request.params, body: request.body }) } }) ``` ::: > Fastify 使用 JSON Schema 进行验证 ::: code-group ```ts twoslash [Elysia TypeBox] 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() }) }) ``` ```ts twoslash [Elysia Zod] import { Elysia } from 'elysia' import { z } from 'zod' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: z.object({ id: z.number() }), body: z.object({ name: z.string() }) }) ``` ```ts twoslash [Elysia Valibot] import { Elysia } from 'elysia' import * as v from 'zod' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: v.object({ id: v.number() }), body: v.object({ name: v.string() }) }) ``` ::: > Elysia 使用 TypeBox 进行验证,并自动强制类型转换。同时支持使用相同语法的各种验证库,例如 Zod、Valibot。 或者,Fastify 也可以使用 TypeBox 或 Zod 进行验证,并使用 `@fastify/type-provider-typebox` 自动推断类型。 ¥Alternatively, Fastify can also use **TypeBox** or **Zod** for validation using `@fastify/type-provider-typebox` to infer type automatically. 虽然 Elysia 更喜欢使用 TypeBox 进行验证,但 Elysia 也支持标准 Schema,允许你开箱即用地使用 Zod、Valibot、ArkType、Effect Schema 等库。 ¥While Elysia **prefers TypeBox** for validation, Elysia also support for Standard Schema allowing you to use library like Zod, Valibot, ArkType, Effect Schema and so on out of the box. ## 文件上传 {#file-upload} ¥File upload Fastify 使用 `fastify-multipart` 处理文件上传,并在底层使用 `Busboy`。而 Elysia 使用 Web 标准 API 处理表单数据,并使用声明式 API 进行 mimetype 验证。 ¥Fastify use a `fastify-multipart` to handle file upload which use `Busboy` under the hood while Elysia use Web Standard API for handling formdata, mimetype valiation using declarative API. 然而,Fastify 不提供直接的文件验证方法,例如文件大小和 MIME 类型,并且需要一些变通方法来验证文件。 ¥However, Fastify doesn't offers a straight forward way for file validation, eg. file size and mimetype, and required some workarounds to validate the file. ::: code-group ```ts [Fastify] import fastify from 'fastify' import multipart from '@fastify/multipart' import { fileTypeFromBuffer } from 'file-type' const app = fastify() app.register(multipart, { attachFieldsToBody: 'keyValues' }) app.post( '/upload', { schema: { body: { type: 'object', properties: { file: { type: 'object' } }, required: ['file'] } } }, async (req, res) => { const file = req.body.file if (!file) return res.status(422).send('No file uploaded') const type = await fileTypeFromBuffer(file) if (!type || !type.mime.startsWith('image/')) return res.status(422).send('File is not a valid image') res.header('Content-Type', type.mime) res.send(file) } ) ``` ::: > Fastift 使用 `fastify-multipart` 处理文件上传,并使用伪造的 `type: object` 来允许使用 Buffer。 ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia 处理文件,并使用 `t.File` 进行 mimetype 验证 由于 multer 不验证 mimetype,你需要使用 file-type 或类似的库手动验证 mimetype。 ¥As **multer** doesn't validate mimetype, you need to validate the mimetype manually using **file-type** or similar library. Elysia 会验证文件上传,并使用 file-type 自动验证 mimetype。 ¥While Elysia, validate file upload, and use **file-type** to validate mimetype automatically. ## 生命周期事件 {#lifecycle-event} ¥Lifecycle Event Fastify 和 Elysia 都使用基于事件的方法,生命周期事件有些相似。 ¥Both Fastify and Elysia has some what similar lifecycle event using event-based approach. ### Elysia 生命周期 {#elysia-lifecycle} ¥Elysia Lifecycle Elysia 的生命周期事件如下图所示。 ¥Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > 点击图片放大 ### Fastify 生命周期 {#fastify-lifecycle} ¥Fastify Lifecycle Fastify 的生命周期事件如下图所示。 ¥Fastify's Life Cycle event can be illustrated as the following. ``` Incoming Request │ └─▶ Routing │ └─▶ Instance Logger │ 4**/5** ◀─┴─▶ onRequest Hook │ 4**/5** ◀─┴─▶ preParsing Hook │ 4**/5** ◀─┴─▶ Parsing │ 4**/5** ◀─┴─▶ preValidation Hook │ 400 ◀─┴─▶ Validation │ 4**/5** ◀─┴─▶ preHandler Hook │ 4**/5** ◀─┴─▶ User Handler │ └─▶ Reply │ 4**/5** ◀─┴─▶ preSerialization Hook │ └─▶ onSend Hook │ 4**/5** ◀─┴─▶ Outgoing Response │ └─▶ onResponse Hook ``` 两者在拦截请求和响应生命周期事件的语法也有些相似,但是 Elysia 不需要你调用 `done` 来继续生命周期事件。 ¥Both also has somewhat similar syntax for intercepting the request and response lifecycle events, however Elysia doesn't require you to call `done` to continue the lifecycle event. ::: code-group ```ts [Fastify] import fastify from 'fastify' const app = fastify() // Global middleware app.addHook('onRequest', (request, reply, done) => { console.log(`${request.method} ${request.url}`) done() }) app.get( '/protected', { // Route-specific middleware preHandler(request, reply, done) { const token = request.headers.authorization if (!token) reply.status(401).send('Unauthorized') done() } }, (request, reply) => { reply.send('Protected route') } ) ``` ::: > Fastify 使用 `addHook` 注册中间件,并要求调用 `done` 来继续生命周期事件。 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest(({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia 会自动检测生命周期事件,无需调用 `done` 即可继续生命周期事件。 ## 听起来类型安全 {#sounds-type-safety} ¥Sounds type safety Elysia 的设计注重类型安全。 ¥Elysia is designed to be sounds type safety. 例如,你可以使用 [derive](/essential/life-cycle.html#derive) 和 [resolve](/essential/life-cycle.html#resolve) 以类型安全的方式自定义上下文,而 Fastify 则不行。 ¥For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Fastify doesn't. ::: code-group ```ts twoslash [Fastify] // @errors: 2339 import fastify from 'fastify' const app = fastify() app.decorateRequest('version', 2) app.get('/version', (req, res) => { res.send(req.version) // ^? }) app.get( '/token', { preHandler(req, res, done) { const token = req.headers.authorization if (!token) return res.status(401).send('Unauthorized') // @ts-ignore req.token = token.split(' ')[1] done() } }, (req, res) => { req.version // ^? res.send(req.token) // ^? } ) app.listen({ port: 3000 }) ``` ::: > Fastify 使用 `decorateRequest`,但不提供声音类型安全 ::: code-group ```ts twoslash [Elysia] 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 使用 `decorate` 扩展上下文,使用 `resolve` 向上下文添加自定义属性 虽然 Fastify 可以使用 `declare module` 扩展 `FastifyRequest` 接口,但它是全局可用的,并且不具备良好的类型安全性,并且不保证该属性在所有请求处理程序中都可用。 ¥While Fastify can, use `declare module` to extend the `FastifyRequest` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'fastify' { interface FastifyRequest { version: number token: string } } ``` > 这是上述 Fastify 示例运行所必需的,它不提供声音类型安全。 ## 中间件参数 {#middleware-parameter} ¥Middleware parameter Fastify 使用函数返回 Fastify 插件来定义命名中间件,而 Elysia 使用 [macro](/patterns/macro) 来定义自定义钩子。 ¥Fastify use a function to return Fastify plugin to define a named middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Fastify] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 import fastify from 'fastify' import type { FastifyRequest, FastifyReply } from 'fastify' const app = fastify() const role = (role: 'user' | 'admin') => (request: FastifyRequest, reply: FastifyReply, next: Function) => { const user = findUser(request.headers.authorization) if (user.role !== role) return reply.status(401).send('Unauthorized') // @ts-ignore request.user = user next() } app.get( '/token', { preHandler: role('admin') }, (request, reply) => { reply.send(request.user) // ^? } ) ``` ::: > Fastify 使用函数回调函数接受中间件的自定义参数。 ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- 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 使用宏将自定义参数传递给自定义中间件 虽然 Fastify 使用函数回调,但它需要返回一个函数,该函数将被放置在事件处理程序中,或者返回一个以钩子形式表示的对象。当需要多个自定义函数时,处理起来会比较困难,因为你需要将它们合并到一个对象中。 ¥While Fastify use a function callback, it needs to return a function to be placed in an event handler or an object represented as a hook which can be hard to handle when there are need for multiple custom functions as you need to reconcile them into a single object. ## 错误处理 {#error-handling} ¥Error handling Fastify 和 Elysia 都提供了一个生命周期事件来处理错误。 ¥Both Fastify and Elysia offers a lifecycle event to handle error. ::: code-group ```ts import fastify from 'fastify' const app = fastify() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.setErrorHandler((error, request, reply) => { if (error instanceof CustomError) reply.status(500).send({ message: 'Something went wrong!', error }) }) app.get( '/error', { // route-specific error handler errorHandler(error, request, reply) { reply.send({ message: 'Only for this route!', error }) } }, (request, reply) => { throw new CustomError('oh uh') } ) ``` ::: > Fastify 使用 `setErrorHandler` 作为全局错误处理程序,`errorHandler` 作为特定路由的错误处理程序 ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia 提供自定义错误代码、状态的简写以及用于将错误映射到响应的 `toResponse`。 虽然两者都提供使用生命周期事件的错误处理,但 Elysia 还提供: ¥While Both offers error handling using lifecycle event, Elysia also provide: 1. 自定义错误代码 2. 用于映射 HTTP 状态的简写,以及用于将错误映射到响应的 `toResponse` 错误代码对于日志记录和调试非常有用,并且在区分扩展同一类的不同错误类型时非常重要。 ¥The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## 封装 {#encapsulation} ¥Encapsulation Fastify 封装了插件的副作用,而 Elysia 通过显式的作用域机制和代码顺序控制插件的副作用。 ¥Fastify encapsulate plugin side-effect, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Fastify] import fastify from 'fastify' import type { FastifyPluginCallback } from 'fastify' const subRouter: FastifyPluginCallback = (app, opts, done) => { app.addHook('preHandler', (request, reply) => { if (!request.headers.authorization?.startsWith('Bearer ')) reply.code(401).send({ error: 'Unauthorized' }) }) done() } const app = fastify() .get('/', (request, reply) => { reply.send('Hello World') }) .register(subRouter) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > Fastify 封装了插件的副作用。 ::: code-group ```ts [Elysia] 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) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > 除非明确说明,否则 Elysia 会封装插件的副作用。 两者都具有插件的封装机制,以防止副作用。 ¥Both has a encapsulate mechanism of a plugin to prevent side-effect. 然而,Elysia 可以通过声明作用域来明确指定哪个插件应该具有副作用,而 Fastify 始终会对其进行封装。 ¥However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia 提供三种作用域机制: ¥Elysia offers 3 type of scoping mechanism: 1. local - 仅应用于当前实例,无副作用(默认) 2. scoped - 作用域副作用作用于父实例,但不会超出父实例的范围 3. global - 影响所有实例 *** 由于 Fastify 不提供作用域机制,我们需要: ¥As Fastify doesn't offers a scoping mechanism, we need to either: 1. 为每个钩子创建一个函数并手动添加它们 2. 使用高阶函数,并将其应用于需要该效果的实例 然而,如果处理不当,这可能会导致重复的副作用。 ¥However, this can caused a duplicated side-effect if not handled carefully. ```ts import fastify from 'fastify' import type { FastifyRequest, FastifyReply, FastifyPluginCallback } from 'fastify' const log = (request: FastifyRequest, reply: FastifyReply, done: Function) => { console.log('Middleware executed') done() } const app = fastify() app.addHook('onRequest', log) app.get('/main', (request, reply) => { reply.send('Hello from main!') }) const subRouter: FastifyPluginCallback = (app, opts, done) => { app.addHook('onRequest', log) // This would log twice app.get('/sub', (request, reply) => { return reply.send('Hello from sub router!') }) done() } app.register(subRouter, { prefix: '/sub' }) app.listen({ port: 3000 }) ``` 在这种情况下,Elysia 提供了插件去重机制,以防止重复的副作用。 ¥In this scenario, Elysia offers a plugin deduplication mechanism to prevent duplicated side-effect. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ name: 'subRouter' }) // [!code ++] .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) .as('scoped') const app = new Elysia() .get('/', 'Hello World') .use(subRouter) .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] // side-effect only called once .get('/side-effect', () => 'hi') ``` 通过使用唯一的 `name`,Elysia 只需应用一次插件,不会造成重复的副作用。 ¥By using a unique `name`, Elysia will apply the plugin only once, and will not cause duplicated side-effect. ## Cookie {#cookie} Fastify 使用 `@fastify/cookie` 解析 Cookie,而 Elysia 内置了对 Cookie 的支持,并使用基于信号的方法来处理 Cookie。 ¥Fastify use `@fastify/cookie` to parse cookies, while Elysia has a built-in support for cookie and use a signal-based approach to handle cookies. ::: code-group ```ts [Fastify] import fastify from 'fastify' import cookie from '@fastify/cookie' const app = fastify() app.use(cookie, { secret: 'secret', hook: 'onRequest' }) app.get('/', function (request, reply) { request.unsignCookie(request.cookies.name) reply.setCookie('name', 'value', { path: '/', signed: true }) }) ``` ::: > Fastify 使用 `unsignCookie` 验证 cookie 签名,使用 `setCookie` 设置 cookie。 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia 使用基于信号的方法处理 Cookie,并自动进行签名验证 ## OpenAPI {#openapi} 两者都使用 Swagger 提供 OpenAPI 文档,但 Elysia 默认使用 Scalar UI,这是一个更现代、更用户友好的 OpenAPI 文档界面。 ¥Both offers OpenAPI documentation using Swagger, however Elysia default to Scalar UI which is a more modern and user-friendly interface for OpenAPI documentation. ::: code-group ```ts [Fastify] import fastify from 'fastify' import swagger from '@fastify/swagger' const app = fastify() app.register(swagger, { openapi: '3.0.0', info: { title: 'My API', version: '1.0.0' } }) app.addSchema({ $id: 'user', type: 'object', properties: { name: { type: 'string', description: 'First name only' }, age: { type: 'integer' } }, required: ['name', 'age'] }) app.post( '/users', { schema: { summary: 'Create user', body: { $ref: 'user#' }, response: { '201': { $ref: 'user#' } } } }, (req, res) => { res.status(201).send(req.body) } ) await fastify.ready() fastify.swagger() ``` ::: > Fastify 使用 `@fastify/swagger` 通过 Swagger 进行 OpenAPI 文档处理 ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' // [!code ++] const app = new Elysia() .use(openapi()) // [!code ++] .model({ user: t.Array( t.Object({ name: t.String(), age: t.Number() }) ) }) .post('/users', ({ body }) => body, { // ^? body: 'user', response: { 201: 'user' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia 默认使用 `@elysiajs/swagger` 进行 OpenAPI 文档处理,使用 Scalar 或 Swagger 进行文档处理 两者都使用 `$ref` 为 OpenAPI 文档提供模型引用,但 Fastify 不提供类型安全,也不提供用于指定模型名称的自动补齐功能,而 Elysia 则提供。 ¥Both offers model reference using `$ref` for OpenAPI documentation, however Fastify doesn't offers type-safety, and auto-completion for specifying model name while Elysia does. ## 测试 {#testing} ¥Testing Fastify 内置了使用 `fastify.inject()` 模拟网络请求进行测试的支持,而 Elysia 使用 Web 标准 API 来执行实际请求。 ¥Fastify has a built-in support for testing using `fastify.inject()` to **simulate** network request while Elysia use a Web Standard API to do an **actual** request. ::: code-group ```ts [Fastify] import fastify from 'fastify' import request from 'supertest' import { describe, it, expect } from 'vitest' function build(opts = {}) { const app = fastify(opts) app.get('/', async function (request, reply) { reply.send({ hello: 'world' }) }) return app } describe('GET /', () => { it('should return Hello World', async () => { const app = build() const response = await app.inject({ url: '/', method: 'GET', }) expect(res.status).toBe(200) expect(res.text).toBe('Hello World') }) }) ``` ::: > Fastify 使用 `fastify.inject()` 模拟网络请求。 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return 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](/eden/overview) 的辅助库,用于端到端类型安全,允许我们使用自动补全和完全类型安全进行测试。 ¥Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] 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('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## 端到端类型安全 {#end-to-end-type-safety} ¥End-to-end type safety Elysia 内置了端到端类型安全支持,无需使用 [Eden](/eden/overview) 进行代码生成,而 Fastify 不提供此功能。 ¥Elysia offers a built-in support for **end-to-end type safety** without code generation using [Eden](/eden/overview), while Fastify doesn't offers one. ::: code-group ```ts twoslash [Elysia] 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) // ^? // ---cut-after--- console.log('ok') ``` ::: 如果端到端类型安全对你来说很重要,那么 Elysia 是正确的选择。 ¥If end-to-end type safety is important for you then Elysia is the right choice. *** Elysia 提供了更符合人机工程学且对开发者更友好的体验,注重性能、类型安全和简洁性。Fastify 是 Node.js 的成熟框架之一,但它不具备下一代框架提供的完善的类型安全和端到端类型安全。 ¥Elysia offers a more ergonomic and developer-friendly experience with a focus on performance, type safety, and simplicity while Fastify is one of the established framework for Node.js, but doesn't has **sounds type safety** and **end-to-end type safety** offers by next generation framework. 如果你正在寻找一个易于使用、具有良好的开发体验并且基于 Web 标准 API 构建的框架,那么 Elysia 是你的正确选择。 ¥If you are looking for a framework that is easy to use, has a great developer experience, and is built on top of Web Standard API, Elysia is the right choice for you. 或者,如果你来自其他框架,你可以查看: ¥Alternatively, if you are coming from a different framework, you can check out: --- --- url: 'https://elysiajs.com/migrate/from-hono.md' --- # 从 Hono 到 Elysia {#from-hono-to-elysia} ¥From Hono to Elysia 本指南面向 Hono 用户,帮助他们了解 Elysia 与 Hono 之间的差异(包括语法),以及如何通过示例将应用从 Hono 迁移到 Elysia。 ¥This guide is for Hono users who want to see a differences from Elysia including syntax, and how to migrate your application from Hono to Elysia by example. Hono 是基于 Web 标准构建的快速轻量级框架。它与 Deno、Bun、Cloudflare Workers 和 Node.js 等多种运行时广泛兼容。 ¥**Hono** is a fast and lightweight built on Web Standard. It has broad compatibility with multiple runtime like Deno, Bun, Cloudflare Workers, and Node.js. Elysia 是一个符合人机工程学的 Web 框架。设计符合人机工程学且方便开发者使用,重点关注可靠的类型安全性和性能。 ¥**Elysia** is an ergonomic web framework. Designed to be ergonomic and developer-friendly with a focus on **sound type safety** and performance. 这两个框架都基于 Web 标准 API 构建,语法略有不同。Hono 提供了与多种运行时的更高兼容性,而 Elysia 则专注于特定的一组运行时。 ¥Both frameworks are built on top of Web Standard API, and have slightly different syntax. Hono offers more compatibility with multiple runtimes while Elysia focuses on a specific set of runtimes. ## 性能 {#performance} ¥Performance 得益于静态代码分析,Elysia 的性能相比 Hono 有显著提升。 ¥Elysia has significant performance improvements over Hono thanks to static code analysis. ## 路由 {#routing} ¥Routing Hono 和 Elysia 的路由语法相似,使用 `app.get()` 和 `app.post()` 方法来定义路由,路径参数语法也相似。 ¥Hono and Elysia has similar routing syntax, using `app.get()` and `app.post()` methods to define routes and similar path parameters syntax. 两者都使用单个 `Context` 参数来处理请求和响应,并直接返回响应。 ¥Both use a single `Context` parameters to handle request and response, and return a response directly. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() app.get('/', (c) => { return c.text('Hello World') }) app.post('/id/:id', (c) => { c.status(201) return c.text(req.params.id) }) export default app ``` ::: > Hono 使用辅助函数 `c.text` 和 `c.json` 返回响应 ::: code-group ```ts [Elysia] 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` 并直接返回响应 Hono 使用 `c.text` 和 `c.json` 来扭曲响应,而 Elysia 会自动将值映射到响应。 ¥While Hono use a `c.text`, and `c.json` to warp a response, Elysia map a value to a response automatically. 样式指南略有不同,Elysia 建议使用方法链和对象解构。 ¥There is a slight different in style guide, Elysia recommends usage of method chaining and object destructuring. Hono 的端口分配取决于运行时和适配器,而 Elysia 使用单一的 `listen` 方法启动服务器。 ¥Hono port allocation is depends on runtime, and adapter while Elysia use a single `listen` method to start the server. ## 处理程序 {#handler} ¥Handler Hono 使用函数手动解析查询、标头和正文,而 Elysia 自动解析属性。 ¥Hono use a function to parse query, header, and body manually while Elysia automatically parse properties. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() app.post('/user', async (c) => { const limit = c.req.query('limit') const { name } = await c.body() const auth = c.req.header('authorization') return c.json({ limit, name, auth }) }) ``` ::: > Hono 会自动解析请求体,但不适用于查询和请求头。 ::: code-group ```ts [Elysia] 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 使用静态代码分析来分析要解析的内容 Elysia 使用静态代码分析来确定要解析的内容,并且只解析所需的属性。 ¥Elysia use **static code analysis** to determine what to parse, and only parse the required properties. 这对于性能和类型安全非常有用。 ¥This is useful for performance and type safety. ## 子路由 {#subrouter} ¥Subrouter 两者都可以继承另一个实例作为路由,但 Elysia 将每个实例都视为可用作子路由的组件。 ¥Both can inherits another instance as a router, but Elysia treat every instances as a component which can be used as a subrouter. ::: code-group ```ts [Hono] import { Hono } from 'hono' const subRouter = new Hono() subRouter.get('/user', (c) => { return c.text('Hello User') }) const app = new Hono() app.route('/api', subRouter) ``` ::: > Hono **require** 用于分隔子路由的前缀 > > ¥a prefix to separate the subrouter ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ prefix: '/api' }) .get('/user', 'Hello User') const app = new Elysia() .use(subRouter) ``` ::: > Elysia 使用可选的前缀构造函数来定义一个 Hono 需要前缀来分隔子路由,而 Elysia 无需前缀。 ¥While Hono requires a prefix to separate the subrouter, Elysia doesn't require a prefix to separate the subrouter. ## 验证 {#validation} ¥Validation Hono 通过外部包支持各种验证器,而 Elysia 内置了使用 TypeBox 的验证功能,并支持开箱即用的标准 Schema,让你无需额外依赖库即可使用你常用的库,例如 Zod、Valibot、ArkType、Effect Schema 等。Elysia 还提供与 OpenAPI 的无缝集成,并在后台进行类型推断。 ¥While Hono supports for various validator via external package, Elysia has a built-in validation using **TypeBox**, and support for Standard Schema out of the box allowing you to use your favorite library like Zod, Valibot, ArkType, Effect Schema and so on without additional library. Elysia also offers seamless integration with OpenAPI, and type inference behind the scene. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { zValidator } from '@hono/zod-validator' import { z } from 'zod' const app = new Hono() app.patch( '/user/:id', zValidator( 'param', z.object({ id: z.coerce.number() }) ), zValidator( 'json', z.object({ name: z.string() }) ), (c) => { return c.json({ params: c.req.param(), body: c.req.json() }) } ) ``` ::: > 崩坏 3 使用基于管道 ::: code-group ```ts twoslash [Elysia TypeBox] 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() }) }) ``` ```ts twoslash [Elysia Zod] import { Elysia } from 'elysia' import { z } from 'zod' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: z.object({ id: z.number() }), body: z.object({ name: z.string() }) }) ``` ```ts twoslash [Elysia Valibot] import { Elysia } from 'elysia' import * as v from 'zod' const app = new Elysia() .patch('/user/:id', ({ params, body }) => ({ // ^? params, body // ^? }), { params: v.object({ id: v.number() }), body: v.object({ name: v.string() }) }) ``` ::: > Elysia 使用 TypeBox 进行验证,并自动强制类型转换。同时支持使用相同语法的各种验证库,例如 Zod、Valibot。 两者都自动提供从模式到上下文的类型推断。 ¥Both offers type inference from schema to context automatically. ## 文件上传 {#file-upload} ¥File upload Hono 和 Elysia 都使用 Web 标准 API 来处理文件上传,但 Elysia 内置了声明式文件验证功能,使用 file-type 来验证 mimetype。 ¥Both Hono, and Elysia use Web Standard API to handle file upload, but Elysia has a built-in declarative support for file validation using **file-type** to validate mimetype. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { z } from 'zod' import { zValidator } from '@hono/zod-validator' import { fileTypeFromBlob } from 'file-type' const app = new Hono() app.post( '/upload', zValidator( 'form', z.object({ file: z.instanceof(File) }) ), async (c) => { const body = await c.req.parseBody() const type = await fileTypeFromBlob(body.image as File) if (!type || !type.mime.startsWith('image/')) { c.status(422) return c.text('File is not a valid image') } return new Response(body.image) } ) ``` ::: > Hono 需要一个单独的 `file-type` 库来验证 mimetype ::: code-group ```ts [Elysia] import { Elysia, t } from 'elysia' const app = new Elysia() .post('/upload', ({ body }) => body.file, { body: t.Object({ file: t.File({ type: 'image' }) }) }) ``` ::: > Elysia 处理文件,并以声明方式进行 mimetype 验证 由于 Web 标准 API 不验证 mimetype,信任客户端提供的 `content-type` 存在安全风险,因此 Hono 需要外部库,而 Elysia 使用 `file-type` 自动验证 mimetype。 ¥As Web Standard API doesn't validate mimetype, it is a security risk to trust `content-type` provided by the client so external library is required for Hono, while Elysia use `file-type` to validate mimetype automatically. ## 中间件 {#middleware} ¥Middleware Hono 中间件使用类似于 Express 的基于队列的单线程,而 Elysia 使用基于事件的生命周期提供更精细的控制。 ¥Hono middleware use a single queue-based order similar to Express while Elysia give you a more granular control using an **event-based** lifecycle. Elysia 的生命周期事件如下图所示。 ¥Elysia's Life Cycle event can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > 点击图片放大 虽然 Hono 按顺序具有单个请求管道流,但 Elysia 可以拦截请求管道中的每个事件。 ¥While Hono has a single flow for request pipeline in order, Elysia can intercept each event in a request pipeline. ::: code-group ```ts [Hono] import { Hono } from 'hono' const app = new Hono() // Global middleware app.use(async (c, next) => { console.log(`${c.method} ${c.url}`) await next() }) app.get( '/protected', // Route-specific middleware async (c, next) => { const token = c.headers.authorization if (!token) { c.status(401) return c.text('Unauthorized') } await next() }, (req, res) => { res.send('Protected route') } ) ``` ::: > Hono 使用基于队列的单向执行中间件 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia() // Global middleware .onRequest(({ method, path }) => { console.log(`${method} ${path}`) }) // Route-specific middleware .get('/protected', () => 'protected', { beforeHandle({ status, headers }) { if (!headers.authorizaton) return status(401) } }) ``` ::: > Elysia 在请求管道中的每个点使用特定的事件拦截器 虽然 Hono 有一个 `next` 函数来调用下一个中间件,但 Elysia 没有。 ¥While Hono has a `next` function to call the next middleware, Elysia does not has one. ## 听起来类型安全 {#sounds-type-safety} ¥Sounds type safety Elysia 的设计注重类型安全。 ¥Elysia is designed to be sounds type safety. 例如,你可以使用 [derive](/essential/life-cycle.html#derive) 和 [resolve](/essential/life-cycle.html#resolve) 以类型安全的方式自定义上下文,而 Hono 则不行。 ¥For example, you can customize context in a **type safe** manner using [derive](/essential/life-cycle.html#derive) and [resolve](/essential/life-cycle.html#resolve) while Hono doesn't. ::: code-group ```ts twoslash [Hono] // @errors: 2339, 2769 import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const app = new Hono() const getVersion = createMiddleware(async (c, next) => { c.set('version', 2) await next() }) app.use(getVersion) app.get('/version', getVersion, (c) => { return c.text(c.get('version') + '') }) const authenticate = createMiddleware(async (c, next) => { const token = c.req.header('authorization') if (!token) { c.status(401) return c.text('Unauthorized') } c.set('token', token.split(' ')[1]) await next() }) app.post('/user', authenticate, async (c) => { c.get('version') return c.text(c.get('token')) }) ``` ::: > Hono 使用中间件来扩展上下文,但类型不安全 ::: code-group ```ts twoslash [Elysia] 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 在请求管道中的每个点使用特定的事件拦截器 虽然 Hono 可以使用 `declare module` 扩展 `ContextVariableMap` 接口,但它是全局可用的,并且不具备良好的类型安全性,并且不能保证该属性在所有请求处理程序中都可用。 ¥While Hono can, use `declare module` to extend the `ContextVariableMap` interface, it is globally available and doesn't have sounds type safety, and doesn't garantee that the property is available in all request handlers. ```ts declare module 'hono' { interface ContextVariableMap { version: number token: string } } ``` > 这是上述 Hono 示例运行所必需的,它不提供声音类型安全。 ## 中间件参数 {#middleware-parameter} ¥Middleware parameter Hono 使用回调函数定义可复用的特定路由中间件,而 Elysia 使用 [macro](/patterns/macro) 函数定义自定义钩子。 ¥Hono use a callback function to define a reusable route-specific middleware, while Elysia use [macro](/patterns/macro) to define a custom hook. ::: code-group ```ts twoslash [Hono] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- // @errors: 2339 2589 2769 import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const app = new Hono() const role = (role: 'user' | 'admin') => createMiddleware(async (c, next) => { const user = findUser(c.req.header('Authorization')) if(user.role !== role) { c.status(401) return c.text('Unauthorized') } c.set('user', user) await next() }) app.get('/user/:id', role('admin'), (c) => { return c.json(c.get('user')) }) ``` ::: > Hono 使用回调返回 `createMiddleware` 来创建可重用的中间件,但类型不安全 ::: code-group ```ts twoslash [Elysia] const findUser = (authorization?: string) => { return { name: 'Jane Doe', role: 'admin' as const } } // ---cut--- 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 使用宏将自定义参数传递给自定义中间件 ## 错误处理 {#error-handling} ¥Error handling Hono 提供了一个适用于所有路由的 `onError` 函数,而 Elysia 则提供了更精细的错误处理控制。 ¥Hono provide a `onError` function which apply to all routes while Elysia provides a more granular control over error handling. ::: code-group ```ts import { Hono } from 'hono' const app = new Hono() class CustomError extends Error { constructor(message: string) { super(message) this.name = 'CustomError' } } // global error handler app.onError((error, c) => { if(error instanceof CustomError) { c.status(500) return c.json({ message: 'Something went wrong!', error }) } }) // route-specific error handler app.get('/error', (req, res) => { throw new CustomError('oh uh') }) ``` ::: > Hono 使用 `onError` 函数处理错误,所有路由使用同一个错误处理程序 ::: code-group ```ts twoslash [Elysia] import { Elysia } from 'elysia' class CustomError extends Error { // Optional: custom HTTP status code status = 500 constructor(message: string) { super(message) this.name = 'CustomError' } // Optional: what should be sent to the client toResponse() { return { message: "If you're seeing this, our dev forgot to handle this error", error: this } } } const app = new Elysia() // Optional: register custom error class .error({ CUSTOM: CustomError, }) // Global error handler .onError(({ error, code }) => { if(code === 'CUSTOM') // ^? return { message: 'Something went wrong!', error } }) .get('/error', () => { throw new CustomError('oh uh') }, { // Optional: route specific error handler error({ error }) { return { message: 'Only for this route!', error } } }) ``` ::: > Elysia 提供更精细的错误处理控制和作用域机制 Hono 提供类似中间件的错误处理,而 Elysia 提供: ¥While Hono offers error handling using middleware-like, Elysia provide: 1. 全局和路由特定的错误处理程序 2. 用于映射 HTTP 状态的简写,以及用于将错误映射到响应的 `toResponse` 3. 为每个错误提供自定义错误代码 错误代码对于日志记录和调试非常有用,并且在区分扩展同一类的不同错误类型时非常重要。 ¥The error code is useful for logging and debugging, and is important when differentiating between different error types extending the same class. ## 封装 {#encapsulation} ¥Encapsulation Hono 封装了插件的副作用,而 Elysia 通过显式的作用域机制和代码顺序让你可以控制插件的副作用。 ¥Hono encapsulate plugin side-effect, while Elysia give you a control over side-effect of a plugin via explicit scoping mechanism, and order-of-code. ::: code-group ```ts [Hono] import { Hono } from 'hono' const subRouter = new Hono() subRouter.get('/user', (c) => { return c.text('Hello User') }) const app = new Hono() app.route('/api', subRouter) ``` ::: > Hono 封装了插件的副作用 ::: code-group ```ts [Elysia] 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) // doesn't have side-effect from subRouter .get('/side-effect', () => 'hi') ``` ::: > 除非明确说明,否则 Elysia 会封装插件的副作用。 两者都具有插件的封装机制,以防止副作用。 ¥Both has a encapsulate mechanism of a plugin to prevent side-effect. 然而,Elysia 可以通过声明作用域来明确指定哪个插件应该具有副作用,而 Fastify 始终会对其进行封装。 ¥However, Elysia can explicitly stated which plugin should have side-effect by declaring a scoped while Fastify always encapsulate it. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia() .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) // Scoped to parent instance but not beyond .as('scoped') // [!code ++] const app = new Elysia() .get('/', 'Hello World') .use(subRouter) // [!code ++] // now have side-effect from subRouter .get('/side-effect', () => 'hi') ``` Elysia 提供三种作用域机制: ¥Elysia offers 3 type of scoping mechanism: 1. local - 仅应用于当前实例,无副作用(默认) 2. scoped - 作用域副作用作用于父实例,但不会超出父实例的范围 3. global - 影响所有实例 *** 由于 Hono 不提供作用域机制,我们需要: ¥As Hono doesn't offers a scoping mechanism, we need to either: 1. 为每个钩子创建一个函数并手动添加它们 2. 使用高阶函数,并将其应用于需要该效果的实例 然而,如果处理不当,这可能会导致重复的副作用。 ¥However, this can caused a duplicated side-effect if not handled carefully. ```ts [Hono] import { Hono } from 'hono' import { createMiddleware } from 'hono/factory' const middleware = createMiddleware(async (c, next) => { console.log('called') await next() }) const app = new Hono() const subRouter = new Hono() app.use(middleware) app.get('/main', (c) => c.text('Hello from main!')) subRouter.use(middleware) // This would log twice subRouter.get('/sub', (c) => c.text('Hello from sub router!')) app.route('/sub', subRouter) export default app ``` 在这种情况下,Elysia 提供了插件去重机制,以防止重复的副作用。 ¥In this scenario, Elysia offers a plugin deduplication mechanism to prevent duplicated side-effect. ```ts [Elysia] import { Elysia } from 'elysia' const subRouter = new Elysia({ name: 'subRouter' }) // [!code ++] .onBeforeHandle(({ status, headers: { authorization } }) => { if(!authorization?.startsWith('Bearer ')) return status(401) }) .as('scoped') const app = new Elysia() .get('/', 'Hello World') .use(subRouter) .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] .use(subRouter) // [!code ++] // side-effect only called once .get('/side-effect', () => 'hi') ``` 通过使用唯一的 `name`,Elysia 只需应用一次插件,不会造成重复的副作用。 ¥By using a unique `name`, Elysia will apply the plugin only once, and will not cause duplicated side-effect. ## Cookie {#cookie} Hono 在 `hono/cookie` 下内置了 cookie 实用函数,而 Elysia 使用基于信号的方法来处理 cookie。 ¥Hono has a built-in cookie utility functions under `hono/cookie`, while Elysia use a signal-based approach to handle cookies. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { getSignedCookie, setSignedCookie } from 'hono/cookie' const app = new Hono() app.get('/', async (c) => { const name = await getSignedCookie(c, 'secret', 'name') await setSignedCookie( c, 'name', 'value', 'secret', { maxAge: 1000, } ) }) ``` ::: > Hono 使用实用函数来处理 Cookie ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' const app = new Elysia({ cookie: { secret: 'secret' } }) .get('/', ({ cookie: { name } }) => { // signature verification is handle automatically name.value // cookie signature is signed automatically name.value = 'value' name.maxAge = 1000 * 60 * 60 * 24 }) ``` ::: > Elysia 使用基于信号的方法来处理 Cookie ## OpenAPI {#openapi} Hono 需要额外的精力来描述规范,而 Elysia 可以将规范无缝集成到架构中。 ¥Hono require additional effort to describe the specification, while Elysia seamless integrate the specification into the schema. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { describeRoute, openAPISpecs } from 'hono-openapi' import { resolver, validator as zodValidator } from 'hono-openapi/zod' import { swaggerUI } from '@hono/swagger-ui' import { z } from '@hono/zod-openapi' const app = new Hono() const model = z.array( z.object({ name: z.string().openapi({ description: 'first name only' }), age: z.number() }) ) const detail = await resolver(model).builder() console.log(detail) app.post( '/', zodValidator('json', model), describeRoute({ validateResponse: true, summary: 'Create user', requestBody: { content: { 'application/json': { schema: detail.schema } } }, responses: { 201: { description: 'User created', content: { 'application/json': { schema: resolver(model) } } } } }), (c) => { c.status(201) return c.json(c.req.valid('json')) } ) app.get('/ui', swaggerUI({ url: '/doc' })) app.get( '/doc', openAPISpecs(app, { documentation: { info: { title: 'Hono API', version: '1.0.0', description: 'Greeting API' }, components: { ...detail.components } } }) ) export default app ``` ::: > Hono 需要额外的精力来描述规范。 ::: code-group ```ts twoslash [Elysia] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' // [!code ++] const app = new Elysia() .use(openapi()) // [!code ++] .model({ user: t.Array( t.Object({ name: t.String(), age: t.Number() }) ) }) .post('/users', ({ body }) => body, { // ^? body: 'user', response: { 201: 'user' }, detail: { summary: 'Create user' } }) ``` ::: > Elysia 将规范无缝集成到模式中 Hono 有单独的函数来描述路由规范和验证,并且需要一些工作才能正确设置。 ¥Hono has separate function to describe route specification, validation, and require some effort to setup properly. Elysia 使用你提供的模式用于生成 OpenAPI 规范,并验证请求/响应,并从单一数据源自动推断类型。 ¥Elysia use schema you provide to generate the OpenAPI specification, and validate the request/response, and infer type automatically all from a **single source of truth**. Elysia 还将 `model` 中注册的 Schema 附加到 OpenAPI 规范中,允许你在 Swagger 或 Scalar UI 的专用部分中引用该模型,同时 Hono 会将 Schema 内联到路由中。 ¥Elysia also appends the schema registered in `model` to the OpenAPI spec, allowing you to reference the model in a dedicated section in Swagger or Scalar UI while Hono inline the schema to the route. ## 测试 {#testing} ¥Testing 两者都基于 Web 标准 API 构建,因此可以与任何测试库一起使用。 ¥Both is built on top of Web Standard API allowing it be used with any testing library. ::: code-group ```ts [Hono] import { Hono } from 'hono' import { describe, it, expect } from 'vitest' const app = new Hono() .get('/', (c) => c.text('Hello World')) describe('GET /', () => { it('should return Hello World', async () => { const res = await app.request('/') expect(res.status).toBe(200) expect(await res.text()).toBe('Hello World') }) }) ``` ::: > Hono 内置了 `request` 方法来执行请求 ::: code-group ```ts [Elysia] import { Elysia } from 'elysia' import { describe, it, expect } from 'vitest' const app = new Elysia() .get('/', 'Hello World') describe('GET /', () => { it('should return 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](/eden/overview) 的辅助库,用于端到端类型安全,允许我们使用自动补全和完全类型安全进行测试。 ¥Alternatively, Elysia also offers a helper library called [Eden](/eden/overview) for End-to-end type safety, allowing us to test with auto-completion, and full type safety. ```ts twoslash [Elysia] 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('should return Hello World', async () => { const { data, error, status } = await api.hello.get() expect(status).toBe(200) expect(data).toBe('Hello World') // ^? }) }) ``` ## 端到端类型安全 {#end-to-end-type-safety} ¥End-to-end type safety 两者都提供端到端的类型安全,但 Hono 似乎不提供基于状态码的类型安全错误处理。 ¥Both offers end-to-end type safety, however Hono doesn't seems to offers type-safe error handling based on status code. ::: code-group ```ts twoslash [Hono] import { Hono } from 'hono' import { hc } from 'hono/client' import { z } from 'zod' import { zValidator } from '@hono/zod-validator' const app = new Hono() .post( '/mirror', zValidator( 'json', z.object({ message: z.string() }) ), (c) => c.json(c.req.valid('json')) ) const client = hc('/') const response = await client.mirror.$post({ json: { message: 'Hello, world!' } }) const data = await response.json() // ^? console.log(data) ``` ::: > Hono 使用 `hc` 函数执行请求,并提供端到端的类型安全 ::: code-group ```ts twoslash [Elysia] 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) // ^? // ---cut-after--- console.log('ok') ``` ::: > Elysia 使用 `treaty` 运行请求,并提供端到端类型安全 虽然两者都提供端到端的类型安全,但 Elysia 提供了基于状态码的更类型安全的错误处理,而 Hono 则没有。 ¥While both offers end-to-end type safety, Elysia offers a more type-safe error handling based on status code while Hono doesn't. 使用每个框架相同用途的代码来测量类型推断速度,Elysia 的类型检查速度比 Hono 快 2.3 倍。 ¥Using the same purpose code for each framework to measure type inference speed, Elysia is 2.3x faster than Hono for type checking. ![Elysia eden type inference performance](/migrate/elysia-type-infer.webp) > Elysia 需要 536 毫秒来推断 Elysia 和 Eden(点击放大) ![Hono HC type inference performance](/migrate/hono-type-infer.webp) > Hono 需要 1.27 秒才能推断出 Hono 和 HC 的错误(已中止)(点击放大) 1.27 秒并不反映推断的整个时长,而是从开始到因错误 "类型实例化过深深度甚至可能无限。" 而中止的时长,当模式过大时会发生这种情况。 ¥The 1.27 seconds doesn't reflect the entire duration of the inference, but a duration from start to aborted by error **"Type instantiation is excessively deep and possibly infinite."** which happens when there are too large schema. ![Hono HC code showing excessively deep error](/migrate/hono-hc-infer.webp) > Hono HC 显示过深错误 这是由大型模式引起的,Hono 不支持超过 100 条包含复杂主体的路由和响应验证,而 Elysia 没有这个问题。 ¥This is caused by the large schema, and Hono doesn't support over a 100 routes with complex body, and response validation while Elysia doesn't have this issue. ![Elysia Eden code showing type inference without error](/migrate/elysia-eden-infer.webp) > Elysia Eden 代码展示了无错误的类型推断 Elysia 具有更快的类型推断性能,并且至少在 2,000 条包含复杂主体和响应验证的路由中没有 "类型实例化过深深度甚至可能无限。"。 ¥Elysia has a faster type inference performance, and doesn't have **"Type instantiation is excessively deep and possibly infinite."** *at least* up to 2,000 routes with complex body, and response validation. 如果端到端类型安全对你来说很重要,那么 Elysia 是正确的选择。 ¥If end-to-end type safety is important for you then Elysia is the right choice. *** 两者都是基于 Web 标准 API 构建的下一代 Web 框架,但略有不同。 ¥Both are the next generation web framework built on top of Web Standard API with slight differences. Elysia 的设计符合人机工程学且对开发者友好,注重声音类型安全,并且性能优于 Hono。 ¥Elysia is designed to be ergonomic and developer-friendly with a focus on **sounds type safety**, and has beter performance than Hono. 虽然 Hono 与多种运行时(尤其是 Cloudflare Workers)具有广泛的兼容性,并且拥有更大的用户群。 ¥While Hono offers a broad compatibility with multiple runtimes, especially with Cloudflare Workers, and a larger user base. 或者,如果你来自其他框架,你可以查看: ¥Alternatively, if you are coming from a different framework, you can check out: --- --- url: 'https://elysiajs.com/key-concept.md' --- # 关键概念 {#key-concept} ¥Key Concept 虽然 Elysia 是一个简单的库,但它有一些关键概念需要你理解才能有效地使用它。 ¥Although Elysia is a simple library, it has some key concepts that you need to understand to use it effectively. 本页涵盖了你应该了解的 Elysia 最重要的概念。 ¥This page covers most important concepts of Elysia that you should know. ::: tip 提示 我们强烈建议你在进一步了解 Elysia 之前阅读此页面。 ¥We **highly recommend** you to read this page before learning more about Elysia. ::: ## 一切都是组件 {#everything-is-a-component} ¥Everything is a component 每个 Elysia 实例都是一个组件。 ¥Every Elysia instance is a component. 组件是可以插入其他实例的插件。 ¥A component is a plugin that could plug into other instances. 它可以是路由、存储、服务或其他任何东西。 ¥It could be a router, a store, a service, or anything else. ```ts twoslash import { Elysia } from 'elysia' const store = new Elysia() .state({ visitor: 0 }) const router = new Elysia() .use(store) .get('/increase', ({ store }) => store.visitor++) const app = new Elysia() .use(router) .get('/', ({ store }) => store) .listen(3000) ``` 这会迫使你将应用分解成小块,以便于添加或删除功能。 ¥This forces you to break down your application into small pieces, making it easy for you to add or remove features. 在 [plugin](/essential/plugin.html) 中了解更多信息。 ¥Learn more about this in [plugin](/essential/plugin.html). ## 方法链 {#method-chaining} ¥Method Chaining Elysia 代码应始终使用方法链。 ¥Elysia code should always use **method chaining**. 由于 Elysia 类型系统复杂,Elysia 中的每个方法都会返回一个新的类型引用。 ¥As Elysia type system is complex, every method in Elysia returns a new type reference. 这对于确保类型完整性和推断非常重要。 ¥**This is important** to ensure type integrity and inference. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) // ^? .listen(3000) ``` 在上面的代码中,state 返回了一个新的 ElysiaInstance 类型,并添加了一个类型化的 `build` 属性。 ¥In the code above, **state** returns a new **ElysiaInstance** type, adding a typed `build` property. ### 不要在没有方法链的情况下使用 Elysia {#dont-use-elysia-without-method-chaining} ¥Don't use Elysia without method chaining 如果没有方法链,Elysia 不会保存这些新类型,从而导致无法进行类型推断。 ¥Without using method chaining, Elysia doesn't save these new types, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` 我们建议始终使用方法链来提供准确的类型推断。 ¥We recommend to **always use method chaining** to provide an accurate type inference. ## 范围 {#scope} ¥Scope 默认情况下,每个实例中的事件/生命周期彼此隔离。 ¥By default, event/life-cycle in each instance is isolated from each other. ```ts twoslash // @errors: 2339 import { Elysia } from 'elysia' const ip = new Elysia() .derive(({ server, request }) => ({ ip: server?.requestIP(request) })) .get('/ip', ({ ip }) => ip) const server = new Elysia() .use(ip) .get('/ip', ({ ip }) => ip) .listen(3000) ``` 在此示例中,`ip` 属性仅在其自身实例中共享,而不在 `server` 实例中共享。 ¥In this example, the `ip` property is only shared in its own instance but not in the `server` instance. 要共享生命周期(在我们的例子中是 `ip` 属性与 `server` 实例),我们需要明确说明它可以共享。 ¥To share the lifecycle, in our case, an `ip` property with `server` instance, we need to **explicitly say** that it could be shared. ```ts twoslash import { Elysia } from 'elysia' const ip = new Elysia() .derive( { as: 'global' }, // [!code ++] ({ server, request }) => ({ ip: server?.requestIP(request) }) ) .get('/ip', ({ ip }) => ip) const server = new Elysia() .use(ip) .get('/ip', ({ ip }) => ip) .listen(3000) ``` 在此示例中,`ip` 属性在 `ip` 和 `server` 实例之间共享,因为我们将其定义为 `global`。 ¥In this example, `ip` property is shared between `ip` and `server` instance because we define it as `global`. 这会迫使你考虑每个属性的作用域,防止你在实例之间意外共享属性。 ¥This forces you to think about the scope of each property, preventing you from accidentally sharing the property between instances. 在 [scope](/essential/plugin.html#scope) 中了解更多信息。 ¥Learn more about this in [scope](/essential/plugin.html#scope). ## 依赖 {#dependency} ¥Dependency 默认情况下,每个实例在应用于另一个实例时都会重新执行。 ¥By default, each instance will be re-executed every time it's applied to another instance. 这可能会导致同一方法被重复多次应用,而某些方法(例如生命周期或路由)应该只调用一次。 ¥This can cause a duplication of the same method being applied multiple times, whereas some methods, like **lifecycle** or **routes**, should only be called once. 为了防止生命周期方法重复,我们可以为实例添加唯一标识符。 ¥To prevent lifecycle methods from being duplicated, we can add **a unique identifier** to the instance. ```ts twoslash import { Elysia } from 'elysia' const ip = new Elysia({ name: 'ip' }) // [!code ++] .derive( { as: 'global' }, ({ server, request }) => ({ ip: server?.requestIP(request) }) ) .get('/ip', ({ ip }) => ip) const router1 = new Elysia() .use(ip) .get('/ip-1', ({ ip }) => ip) const router2 = new Elysia() .use(ip) .get('/ip-2', ({ ip }) => ip) const server = new Elysia() .use(router1) .use(router2) ``` 这将通过使用唯一名称应用数据去重来防止 `ip` 属性被多次调用。 ¥This will prevent the `ip` property from being called multiple times by applying deduplication using a unique name. 这使我们能够多次重用同一个实例,而不会降低性能。迫使你思考每个实例的依赖。 ¥This allows us to reuse the same instance multiple times without the performance penalty. Forcing you to think about the dependencies of each instance. 在 [插件数据去重](/essential/plugin.html#plugin-deduplication) 中了解更多信息。 ¥Learn more about this in [plugin deduplication](/essential/plugin.html#plugin-deduplication). ### 服务定位器 {#service-locator} ¥Service Locator 当你将带有状态/装饰器的插件应用于实例时,该实例将获得类型安全。 ¥When you apply a plugin with state/decorators to an instance, the instance will gain type safety. 但如果你不将插件应用到另一个实例,它将无法推断类型。 ¥But if you don't apply the plugin to another instance, it will not be able to infer the type. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const child = new Elysia() // ❌ 'a' is missing .get('/', ({ a }) => a) const main = new Elysia() .decorate('a', 'a') .use(child) ``` Elysia 引入了服务定位器模式来解决这个问题。 ¥Elysia introduces the **Service Locator** pattern to counteract this. 我们仅提供了插件引用,方便 Elysia 查找服务以添加类型安全性。 ¥We simply provide the plugin reference for Elysia to find the service to add type safety. ```typescript twoslash // @errors: 2339 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) // With `setup`, type will be inferred const child = new Elysia() .use(setup) // [!code ++] .get('/', ({ a }) => a) // ^? const main = new Elysia() .use(child) ``` 正如 [dependencies](#dependencies) 中提到的,我们可以使用 `name` 属性对实例进行数据去重,这样就不会有任何性能损失或生命周期重复。 ¥As mentioned in [dependencies](#dependencies), we can use the `name` property to deduplicate the instance so it will not have any performance penalty or lifecycle duplication. ## 代码顺序 {#order-of-code} ¥Order of code Elysia 生命周期代码的顺序非常重要。 ¥The order of Elysia's life-cycle code is very important. 因为事件只有在注册后才会应用于路由。 ¥Because event will only apply to routes **after** it is registered. 如果将 onError 事件放在插件之前,插件将不会继承 onError 事件。 ¥If you put the onError before plugin, plugin will not inherit the onError event. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .get('/', () => 'hi') .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` 控制台应记录以下内容: ¥Console should log the following: ```bash 1 ``` 请注意,它不会记录 2,因为该事件是在路由之后注册的,因此不会应用于路由。 ¥Notice that it doesn't log **2**, because the event is registered after the route so it is not applied to the route. 在 [代码顺序](/essential/life-cycle.html#order-of-code) 中了解更多信息。 ¥Learn more about this in [order of code](/essential/life-cycle.html#order-of-code). ## 类型推断 {#type-inference} ¥Type Inference Elysia 拥有复杂的类型系统,允许你从实例推断类型。 ¥Elysia has a complex type system that allows you to infer types from the instance. ```ts twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .post('/', ({ body }) => body, { // ^? body: t.Object({ name: t.String() }) }) ``` 如果可能,请始终使用内联函数来提供准确的类型推断。 ¥If possible, **always use an inline function** to provide an accurate type inference. 如果你需要应用单独的函数,例如 MVC 的控制器模式,建议从内联函数中解构属性,以避免不必要的类型推断。 ¥If you need to apply a separate function, eg. MVC's controller pattern, it's recommended to destructure properties from inline function to prevent unnecessary type inference. ```ts twoslash import { Elysia, t } from 'elysia' abstract class Controller { static greet({ name }: { name: string }) { return 'hello ' + name } } const app = new Elysia() .post('/', ({ body }) => Controller.greet(body), { body: t.Object({ name: t.String() }) }) ``` ### TypeScript {#typescript} 我们可以通过访问 `static` 属性来获取每个 Elysia/TypeBox 类型的类型定义,如下所示: ¥We can get a type definitions of every Elysia/TypeBox's type by accessing `static` property as follows: ```ts twoslash import { t } from 'elysia' const MyType = t.Object({ hello: t.Literal('Elysia') }) type MyType = typeof MyType.static // ^? ``` 这允许 Elysia 自动推断并提供类型,从而减少声明重复模式的需要。 ¥This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema 单个 Elysia/TypeBox 模式可用于: ¥A single Elysia/TypeBox schema can be used for: * 运行时验证 * 数据强制转换 * TypeScript 类型 * OpenAPI 架构 这使我们能够将模式作为单一事实来源。 ¥This allows us to make a schema as a **single source of truth**. 在 [最佳实践:MVC 控制器](/essential/best-practice.html#controller) 中了解更多信息。 ¥Learn more about this in [Best practice: MVC Controller](/essential/best-practice.html#controller). --- --- url: 'https://elysiajs.com/essential/best-practice.md' --- # 最佳实践 {#best-practice} ¥Best Practice Elysia 是一个模式无关的框架,使用哪种编码模式由你和你的团队自行决定。 ¥Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team. 然而,在尝试将 MVC 模式 [(模型-视图-控制器)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 与 Elysia 结合时,存在一些问题,我们发现很难解耦和处理类型。 ¥However, there are several concerns when trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and we found it hard to decouple and handle types. 本页是如何遵循 Elysia 结构最佳实践并结合 MVC 模式的指南,但可以适应你喜欢的任何编码模式。 ¥This page is a guide on how to follow Elysia structure best practices combined with the MVC pattern, but it can be adapted to any coding pattern you prefer. ## 文件夹结构 {#folder-structure} ¥Folder Structure Elysia 对文件夹结构没有硬性要求,你可以自行决定如何组织代码。 ¥Elysia is unopinionated about folder structure, leaving you to **decide** how to organize your code yourself. 但是,如果你没有考虑具体的结构,我们建议使用基于功能的文件夹结构,其中每个功能都有自己的文件夹,其中包含控制器、服务和模型。 ¥However, **if you don't have a specific structure in mind**, we recommend a feature-based folder structure where each feature has its own folder containing controllers, services, and models. ``` | src | modules | auth | index.ts (Elysia controller) | service.ts (service) | model.ts (model) | user | index.ts (Elysia controller) | service.ts (service) | model.ts (model) | utils | a | index.ts | b | index.ts ``` 这种结构允许你轻松查找和管理代码,并将相关代码放在一起。 ¥This structure allows you to easily find and manage your code and keep related code together. 以下是如何将代码分发到基于特性的文件夹结构的示例代码: ¥Here's an example code of how to distribute your code into a feature-based folder structure: ::: code-group ```typescript [auth/index.ts] // Controller handle HTTP related eg. routing, request validation import { Elysia } from 'elysia' import { Auth } from './service' import { AuthModel } from './model' export const auth = new Elysia({ prefix: '/auth' }) .get( '/sign-in', async ({ body, cookie: { session } }) => { const response = await Auth.signIn(body) // Set session cookie session.value = response.token return response }, { body: AuthModel.signInBody, response: { 200: AuthModel.signInResponse, 400: AuthModel.signInInvalid } } ) ``` ```typescript [auth/service.ts] // Service handle business logic, decoupled from Elysia controller import { status } from 'elysia' import type { AuthModel } from './model' // If the class doesn't need to store a property, // you may use `abstract class` to avoid class allocation export abstract class Auth { static async signIn({ username, password }: AuthModel.signInBody) { const user = await sql` SELECT password FROM users WHERE username = ${username} LIMIT 1` if (await Bun.password.verify(password, user.password)) // You can throw an HTTP error directly throw status( 400, 'Invalid username or password' satisfies AuthModel.signInInvalid ) return { username, token: await generateAndSaveTokenToDB(user.id) } } } ``` ```typescript [auth/model.ts] // Model define the data structure and validation for the request and response import { t } from 'elysia' export namespace AuthModel { // Define a DTO for Elysia validation export const signInBody = t.Object({ username: t.String(), password: t.String(), }) // Define it as TypeScript type export type signInBody = typeof signInBody.static // Repeat for other models export const signInResponse = t.Object({ username: t.String(), token: t.String(), }) export type signInResponse = typeof signInResponse.static export const signInInvalid = t.Literal('Invalid username or password') export type signInInvalid = typeof signInInvalid.static } ``` ::: 每个文件都有其各自的职责,如下所示: ¥Each file has its own responsibility as follows: * 控制器:处理 HTTP 路由、请求验证和 Cookie。 * 服务:处理业务逻辑,尽可能与 Elysia 控制器分离。 * 模型:定义请求和响应的数据结构和验证。 你可以根据需要调整此结构,并使用你喜欢的任何编码模式。 ¥Feel free to adapt this structure to your needs and use any coding pattern you prefer. ## 方法链 {#method-chaining} ¥Method Chaining Elysia 代码应始终使用方法链。 ¥Elysia code should always use **method chaining**. 由于 Elysia 的类型系统复杂,Elysia 中的每个方法都会返回一个新的类型引用。 ¥As Elysia's type system is complex, every method in Elysia returns a new type reference. 这对于确保类型完整性和推断非常重要。 ¥**This is important** to ensure type integrity and inference. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) .listen(3000) ``` 在上面的代码中,state 返回了一个新的 ElysiaInstance 类型,并添加了一个 `build` 类型。 ¥In the code above **state** returns a new **ElysiaInstance** type, adding a `build` type. ### ❌ 不要做的:不使用方法使用 Elysia 链式调用 {#-dont-use-elysia-without-method-chaining} ¥❌ Don't: Use Elysia without method chaining 如果没有方法链,Elysia 不会保存这些新类型,从而导致无法进行类型推断。 ¥Without using method chaining, Elysia doesn't save these new types, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` 我们建议始终使用方法链来提供准确的类型推断。 ¥We recommend to **always use method chaining** to provide accurate type inference. ## 控制器 {#controller} ¥Controller > 1 个 Elysia 实例 = 1 个控制器 Elysia 会采取多种措施来确保类型完整性,如果你将整个 `Context` 类型传递给控制器​​,可能会出现以下问题: ¥Elysia does a lot to ensure type integrity, if you pass an entire `Context` type to a controller, these might be the problems: 1. Elysia 类型复杂,严重依赖于插件和多层链接。 2. 难以输入,Elysia 类型随时可能更改,尤其是在使用装饰器和存储时。 3. 类型转换可能会导致类型完整性丧失,或无法确保类型与运行时代码之间的一致性。 4. 这使得 [Sucrose](/blog/elysia-10#sucrose)(Elysia 的 "种类" 编译器)静态分析你的代码更具挑战性。 ### ❌ 不要做的:创建单独的控制器 {#-dont-create-a-separate-controller} ¥❌ Don't: Create a separate controller 不要创建单独的控制器,而是使用 Elysia 本身作为控制器: ¥Don't create a separate controller, use Elysia itself as a controller instead: ```typescript import { Elysia, t, type Context } from 'elysia' abstract class Controller { static root(context: Context) { return Service.doStuff(context.stuff) } } // ❌ Don't new Elysia() .get('/', Controller.hi) ``` 将整个 `Controller.method` 传递给 Elysia 相当于让两个控制器来回传递数据。这违背了框架和 MVC 模式本身的设计。 ¥By passing an entire `Controller.method` to Elysia is an equivalent of having 2 controllers passing data back and forth. It's against the design of framework and MVC pattern itself. ### ✅ 应该做的:使用 Elysia 作为控制器 {#-do-use-elysia-as-a-controller} ¥✅ Do: Use Elysia as a controller 不如将 Elysia 实例本身视为控制器。 ¥Instead treat an Elysia instance as a controller itself instead. ```typescript import { Elysia } from 'elysia' import { Service } from './service' new Elysia() .get('/', ({ stuff }) => { Service.doStuff(stuff) }) ``` ### 测试 {#testing} ¥Testing 你可以使用 `handle` 直接调用函数(及其生命周期)来测试你的控制器。 ¥You can test your controller using `handle` to directly call a function (and it's lifecycle) ```typescript import { Elysia } from 'elysia' import { Service } from './service' import { describe, it, expect } from 'bun:test' const app = new Elysia() .get('/', ({ stuff }) => { Service.doStuff(stuff) return 'ok' }) describe('Controller', () => { it('should work', async () => { const response = await app .handle(new Request('http://localhost/')) .then((x) => x.text()) expect(response).toBe('ok') }) }) ``` 你可以在 [单元测试](/patterns/unit-test.html) 中找到更多关于测试的信息。 ¥You may find more information about testing in [Unit Test](/patterns/unit-test.html). ## 服务 {#service} ¥Service 服务是一组实用程序/辅助函数,它们被解耦为业务逻辑,用于模块/控制器(在本例中为 Elysia 实例)。 ¥Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance. 任何可以与控制器解耦的技术逻辑都可以存在于服务中。 ¥Any technical logic that can be decoupled from controller may live inside a **Service**. Elysia 中有两种类型的服务: ¥There are 2 types of service in Elysia: 1. 非请求依赖服务 2. 请求依赖服务 ### ✅ 应该做的:抽象出非请求依赖的服务 {#-do-abstract-away-non-request-dependent-service} ¥✅ Do: Abstract away non-request dependent service 我们建议从 Elysia 中抽象出一个服务类/函数。 ¥We recommend abstracting a service class/function away from Elysia. 如果服务或函数未绑定到 HTTP 请求或不访问 `Context`,建议将其实现为静态类或函数。 ¥If the service or function isn't tied to an HTTP request or doesn't access a `Context`, it's recommended to implement it as a static class or function. ```typescript import { Elysia, t } from 'elysia' abstract class Service { static fibo(number: number): number { if(number < 2) return number return Service.fibo(number - 1) + Service.fibo(number - 2) } } new Elysia() .get('/fibo', ({ body }) => { return Service.fibo(body) }, { body: t.Numeric() }) ``` 如果你的服务不需要存储属性,你可以改用 `abstract class` 和 `static` 来避免分配类实例。 ¥If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance. ### ✅ 应该做的:将依赖服务作为 Elysia 实例请求 {#-do-request-dependent-service-as-elysia-instance} ¥✅ Do: Request dependent service as Elysia instance 如果服务是依赖于请求的服务或需要处理 HTTP 请求,我们建议将其抽象为 Elysia 实例,以确保类型完整性和推断: ¥**If the service is a request-dependent service** or needs to process HTTP requests, we recommend abstracting it as an Elysia instance to ensure type integrity and inference: ```typescript import { Elysia } from 'elysia' // ✅ Do const AuthService = new Elysia({ name: 'Auth.Service' }) .macro({ isSignIn: { resolve({ cookie, status }) { if (!cookie.session.value) return status(401) return { session: cookie.session.value, } } } }) const UserController = new Elysia() .use(AuthService) .get('/profile', ({ Auth: { user } }) => user, { isSignIn: true }) ``` ::: tip 提示 Elysia 默认处理 [插件数据去重](/essential/plugin.html#plugin-deduplication),因此你无需担心性能问题,因为如果你指定 "name" 属性,它将是单例。 ¥Elysia handles [plugin deduplication](/essential/plugin.html#plugin-deduplication) by default, so you don't have to worry about performance, as it will be a singleton if you specify a **"name"** property. ::: ### ✅ 应该做的:仅装饰请求依赖属性 {#-do-decorate-only-request-dependent-property} ¥✅ Do: Decorate only request dependent property 建议仅将与请求相关的属性(例如 `requestIP`、`requestTime` 或 `session`)`decorate` 化。 ¥It's recommended to `decorate` only request-dependent properties, such as `requestIP`, `requestTime`, or `session`. 过度使用装饰器可能会将你的代码与 Elysia 绑定,使其更难测试和重用。 ¥Overusing decorators may tie your code to Elysia, making it harder to test and reuse. ```typescript import { Elysia } from 'elysia' new Elysia() .decorate('requestIP', ({ request }) => request.headers.get('x-forwarded-for') || request.ip) .decorate('requestTime', () => Date.now()) .decorate('session', ({ cookie }) => cookie.session.value) .get('/', ({ requestIP, requestTime, session }) => { return { requestIP, requestTime, session } }) ``` ### ❌ 不要做的:将整个 `Context` 传递给服务 {#-dont-pass-entire-context-to-a-service} ¥❌ Don't: Pass entire `Context` to a service Context 是一种高度动态的类型,可以从 Elysia 实例推断出来。 ¥**Context is a highly dynamic type** that can be inferred from Elysia instance. 不要将整个 `Context` 传递给服务,而是使用对象解构提取所需内容并将其传递给服务。 ¥Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service. ```typescript import type { Context } from 'elysia' class AuthService { constructor() {} // ❌ Don't do this isSignIn({ status, cookie: { session } }: Context) { if (session.value) return status(401) } } ``` 由于 Elysia 类型复杂,并且严重依赖于插件和多层级的链接,因此由于其高度动态,手动输入类型可能具有挑战性。 ¥As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic. ### ⚠️ 从 Elysia 实例推断上下文 {#-infers-context-from-elysia-instance} ¥⚠️ Infers Context from Elysia instance 如果绝对必要,你可以从 Elysia 实例本身推断 `Context` 类型: ¥In case of **absolute necessity**, you may infer the `Context` type from the Elysia instance itself: ```typescript import { Elysia, type InferContext } from 'elysia' const setup = new Elysia() .state('a', 'a') .decorate('b', 'b') class AuthService { constructor() {} // ✅ Do isSignIn({ status, cookie: { session } }: InferContext) { if (session.value) return status(401) } } ``` 然而,我们建议尽可能避免这种情况,而使用 [Elysia 即服务](#✅-do-use-elysia-as-a-controller)。 ¥However we recommend to avoid this if possible, and use [Elysia as a service](#✅-do-use-elysia-as-a-controller) instead. 你可以在 [必备:处理程序](/essential/handler) 中找到更多关于 [InferContext](/essential/handler#infercontext) 的信息。 ¥You may find more about [InferContext](/essential/handler#infercontext) in [Essential: Handler](/essential/handler). ## 模型 {#model} ¥Model 模型或 [DTO(数据传输对象)](https://en.wikipedia.org/wiki/Data_transfer_object) 由 [Elysia.t (验证)](/essential/validation.html#elysia-type) 处理。 ¥Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/essential/validation.html#elysia-type). Elysia 内置了验证系统,可以从代码中推断类型并在运行时进行验证。 ¥Elysia has a validation system built-in which can infers type from your code and validate it at runtime. ### ❌ 不要做的:将类实例声明为模型 {#-dont-declare-a-class-instance-as-a-model} ¥❌ Don't: Declare a class instance as a model 不要将类实例声明为模型: ¥Do not declare a class instance as a model: ```typescript // ❌ Don't class CustomBody { username: string password: string constructor(username: string, password: string) { this.username = username this.password = password } } // ❌ Don't interface ICustomBody { username: string password: string } ``` ### ✅ 应该做的:使用 Elysia 的验证系统 {#-do-use-elysias-validation-system} ¥✅ Do: Use Elysia's validation system 与其声明类或接口,不如使用 Elysia 的验证系统来定义模型: ¥Instead of declaring a class or interface, use Elysia's validation system to define a model: ```typescript twoslash // ✅ Do import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // Optional if you want to get the type of the model // Usually if we didn't use the type, as it's already inferred by Elysia type CustomBody = typeof customBody.static // ^? export { customBody } ``` 我们可以通过使用模型中的 `typeof` 和 `.static` 属性来获取模型的类型。 ¥We can get type of model by using `typeof` with `.static` property from the model. 然后你可以使用 `CustomBody` 类型推断请求主体的类型。 ¥Then you can use the `CustomBody` type to infer the type of the request body. ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // ---cut--- // ✅ Do new Elysia() .post('/login', ({ body }) => { // ^? return body }, { body: customBody }) ``` ### ❌ 不要做的:声明与模型不同的类型 {#-dont-declare-type-separate-from-the-model} ¥❌ Don't: Declare type separate from the model 不要声明与模型不同的类型,而是使用 `typeof` 和 `.static` 属性来获取模型的类型。 ¥Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model. ```typescript // ❌ Don't import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) type CustomBody = { username: string password: string } // ✅ Do const customBody = t.Object({ username: t.String(), password: t.String() }) type CustomBody = typeof customBody.static ``` ### 组 {#group} ¥Group 你可以将多个模型组合成一个对象,使其更具条理。 ¥You can group multiple models into a single object to make it more organized. ```typescript import { Elysia, t } from 'elysia' export const AuthModel = { sign: t.Object({ username: t.String(), password: t.String() }) } const models = AuthModel.models ``` ### 模型注入 {#model-injection} ¥Model Injection 虽然这是可选的,但如果你严格遵循 MVC 模式,你可能希望将其像服务一样注入到控制器中。我们推荐使用 [Elysia 参考模型](/essential/validation#reference-model) ¥Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using [Elysia reference model](/essential/validation#reference-model) 使用 Elysia 的模型引用 ¥Using Elysia's model reference ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) const AuthModel = new Elysia() .model({ 'auth.sign': customBody }) const models = AuthModel.models const UserController = new Elysia({ prefix: '/auth' }) .use(AuthModel) .post('/sign-in', async ({ body, cookie: { session } }) => { // ^? return true }, { body: 'auth.sign' }) ``` 这种方法有几个好处: ¥This approach provide several benefits: 1. 允许我们命名模型并提供自动补齐功能。 2. 修改模式以供以后使用,或执行 [remap](/essential/handler.html#remap)。 3. 在 OpenAPI 合规客户端(例如 OpenAPI)中显示为 "models"。 4. 由于模型类型将在注册期间被缓存,因此提高 TypeScript 推断速度。 ## 复用插件 {#reuse-a-plugin} ¥Reuse a plugin 可以多次复用插件来提供类型推断。 ¥It's ok to reuse plugins multiple time to provide type inference. Elysia 默认自动处理插件数据去重,性能几乎可以忽略不计。 ¥Elysia handle plugin deduplication automatically by default, and the performance is negligible. 要创建一个唯一的插件,你可以为 Elysia 实例提供名称或可选种子。 ¥To create a unique plugin, you may provide a **name** or optional **seed** to an Elysia instance. ```typescript import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'my-plugin' }) .decorate("type", "plugin") const app = new Elysia() .use(plugin) .use(plugin) .use(plugin) .use(plugin) .listen(3000) ``` 这允许 Elysia 通过重用已注册的插件来提高性能,而无需一遍又一遍地处理插件。 ¥This allows Elysia to improve performance by reusing the registered plugins instead of processing the plugin over and over again. --- --- url: 'https://elysiajs.com/essential/handler.md' --- # 处理程序 {#handler} ¥Handler 一个处理程序(handler)是一个响应每个路由请求的函数。 ¥A handler is a function that responds to the request for each route. 接受请求信息并返回响应给客户端。 ¥Accepting request information and returning a response to the client. 或者,在其他框架中,处理程序也称为控制器。 ¥Alternatively, a handler is also known as a **Controller** in other frameworks. ```typescript import { Elysia } from 'elysia' new Elysia() // the function `() => 'hello world'` is a handler .get('/', () => 'hello world') .listen(3000) ``` 处理程序可以是文字值,并且可以内联。 ¥A handler may be a literal value, and can be inlined. ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/', 'Hello Elysia') .get('/video', file('kyuukurarin.mp4')) .listen(3000) ``` 使用内联值始终返回相同的值,这对于优化文件等静态资源的性能非常有用。 ¥Using an inline value always returns the same value which is useful to optimize performance for static resources like files. 这允许 Elysia 提前编译响应以优化性能。 ¥This allows Elysia to compile the response ahead of time to optimize performance. ::: tip 提示 提供内联值并非缓存。 ¥Providing an inline value is not a cache. 静态资源值、标头和状态可以使用生命周期进行动态变异。 ¥Static resource values, headers and status can be mutated dynamically using lifecycle. ::: ## 上下文 {#context} ¥Context Context 包含每个请求独有的请求信息,除 `store` (全局可变状态) 外,其他请求信息均不共享。 ¥**Context** contains request information which is unique for each request, and is not shared except for `store` (global mutable state). ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', (context) => context.path) // ^ This is a context ``` Context 只能在路由处理程序中检索。它包含: ¥**Context** can only be retrieved in a route handler. It consists of: * path - 请求的路径名 * body - [HTTP 消息](https://web.nodejs.cn/en-US/docs/Web/HTTP/Messages),表单或文件上传。 * query - [查询字符串](https://en.wikipedia.org/wiki/Query_string),以 JavaScript 对象的形式包含用于搜索查询的附加参数。(查询从路径名后从 '?' 问号开始的值中提取) * params - Elysia 的路径参数解析为 JavaScript 对象 * headers - [HTTP 标头](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers),请求的附加信息,例如 User-Agent、Content-Type、缓存提示。 * request - [Web 标准请求](https://web.nodejs.cn/en-US/docs/Web/API/Request) * redirect - 一个用于重定向响应的函数 * store - 一个用于 Elysia 实例的全局可变存储 * cookie - 一个用于与 Cookie 交互的全局可变信号存储(包括 get/set 方法) * set - 应用于响应的属性: * status - [HTTP 状态](https://web.nodejs.cn/en-US/docs/Web/HTTP/Status),如果未设置,则默认为 200。 * headers - 响应头 * redirect - 响应作为重定向到的路径 * error - 一个用于返回自定义状态码的函数 * server - Bun 服务器实例 ## 设置 {#set} ¥Set set 是一个可变属性,它形成一个可通过 `Context.set` 访问的响应。 ¥**set** is a mutable property that form a response accessible via `Context.set`. * set.status - 设置自定义状态码 * set.headers - 附加自定义标头 * set.redirect - 附加重定向 ```ts twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ set, status }) => { set.headers = { 'X-Teapot': 'true' } return status(418, 'I am a teapot') }) .listen(3000) ``` ### status {#status} 我们可以使用以下任一方式返回自定义状态码: ¥We can return a custom status code by using either: * 状态函数(推荐) * set.status (遗留) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/error', ({ error }) => error(418, 'I am a teapot')) .get('/set.status', ({ set }) => { set.status = 418 return 'I am a teapot' }) .listen(3000) ``` ### 状态函数 {#status-function} ¥status function 用于返回响应状态码的专用 `status` 函数。 ¥A dedicated `status` function for returning status code with response. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', ({ status }) => status(418, "Kirifuji Nagisa")) .listen(3000) ``` 建议在主处理程序中使用 `status`,因为它具有更好的推断能力: ¥It's recommended to use `status` inside the main handler as it has better inference: * 允许 TypeScript 检查返回值是否正确匹配响应模式 * 基于状态码自动补齐类型缩小 * 使用端到端类型安全 ([Eden](/eden/overview)) 进行错误处理的类型缩小 ### set.status {#setstatus} 如果未提供,则设置默认状态码。 ¥Set a default status code if not provided. 建议在只需要返回特定状态码的插件中使用它,同时允许用户返回自定义值。例如,HTTP 201/206 或 403/405 等。 ¥It's recommended to use this in a plugin that only needs to return a specific status code while allowing the user to return a custom value. For example, HTTP 201/206 or 403/405, etc. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(({ set }) => { set.status = 418 return 'Kirifuji Nagisa' }) .get('/', () => 'hi') .listen(3000) ``` 与 `status` 函数不同,`set.status` 无法推断返回值的类型,因此无法检查返回值的类型是否与响应模式正确匹配。 ¥Unlike `status` function, `set.status` cannot infer the return value type, therefore it can't check if the return value is correctly type to response schema. ::: tip 提示 HTTP 状态指示响应类型。如果路由处理程序成功执行且没有错误,Elysia 将返回状态码 200。 ¥HTTP Status indicates the type of response. If the route handler is executed successfully without error, Elysia will return the status code 200. ::: 你还可以使用状态代码的通用名称(而不是数字)来设置状态代码。 ¥You can also set a status code using the common name of the status code instead of using a number. ```typescript twoslash // @errors 2322 import { Elysia } from 'elysia' new Elysia() .get('/', ({ set }) => { set.status // ^? return 'Kirifuji Nagisa' }) .listen(3000) ``` ### set.headers {#setheaders} 允许我们附加或删除以对象形式表示的响应标头。 ¥Allowing us to append or delete response headers represented as an Object. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ set }) => { set.headers['x-powered-by'] = 'Elysia' return 'a mimir' }) .listen(3000) ``` ::: warning 警告 标头名称应小写,以强制 HTTP 标头和自动补齐的大小写一致性,例如,使用 `set-cookie` 而不是 `Set-Cookie`。 ¥The names of headers should be lowercase to force case-sensitivity consistency for HTTP headers and auto-completion, eg. use `set-cookie` rather than `Set-Cookie`. ::: ### redirect {#redirect} 将请求重定向到其他资源。 ¥Redirect a request to another resource. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', ({ redirect }) => { return redirect('https://youtu.be/whpVWVWBW4U?&t=8') }) .get('/custom-status', ({ redirect }) => { // You can also set custom status to redirect return redirect('https://youtu.be/whpVWVWBW4U?&t=8', 302) }) .listen(3000) ``` 使用重定向时,返回值不是必需的,将被忽略。由于响应将来自其他资源。 ¥When using redirect, returned value is not required and will be ignored. As response will be from another resource. ## 服务器 {#server} ¥Server 可以通过 `Context.server` 访问服务器实例,并与服务器进行交互。 ¥Server instance is accessible via `Context.server` to interact with the server. 由于服务器可能运行在不同的环境中(测试),因此可以为空。 ¥Server could be nullable as it could be running in a different environment (test). 如果服务器正在使用 Bun 运行(分配),则 `server` 将可用(非空)。 ¥If server is running (allocating) using Bun, `server` will be available (not null). ```typescript import { Elysia } from 'elysia' new Elysia() .get('/port', ({ server }) => { return server?.port }) .listen(3000) ``` ### 请求 IP {#request-ip} ¥Request IP 我们可以使用 `server.requestIP` 方法获取请求 IP ¥We can get request IP by using `server.requestIP` method ```typescript import { Elysia } from 'elysia' new Elysia() .get('/ip', ({ server, request }) => { return server?.requestIP(request) }) .listen(3000) ``` ## 响应 {#response} ¥Response Elysia 建立在 Web 标准请求/响应之上。 ¥Elysia is built on top of Web Standard Request/Response. 为了符合 Web 标准,Elysia 会将路由处理程序返回的值映射到 [响应](https://web.nodejs.cn/en-US/docs/Web/API/Response) 中。 ¥To comply with the Web Standard, a value returned from route handler will be mapped into a [Response](https://web.nodejs.cn/en-US/docs/Web/API/Response) by Elysia. 让你专注于业务逻辑而不是样板代码。 ¥Letting you focus on business logic rather than boilerplate code. ```typescript import { Elysia } from 'elysia' new Elysia() // Equivalent to "new Response('hi')" .get('/', () => 'hi') .listen(3000) ``` 如果你更喜欢显式的 Response 类,Elysia 也会自动处理。 ¥If you prefer an explicit Response class, Elysia also handles that automatically. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => new Response('hi')) .listen(3000) ``` ::: tip 提示 使用原始值或 `Response` 的性能几乎相同(+- 0.1%),因此无论性能如何,都可以选择你喜欢的一种。 ¥Using a primitive value or `Response` has near identical performance (+- 0.1%), so pick the one you prefer, regardless of performance. ::: ## 表单数据 {#formdata} ¥Formdata 我们可以直接从处理程序返回 `form` 实用程序来返回 `FormData`。 ¥We may return a `FormData` by using returning `form` utility directly from the handler. ```typescript import { Elysia, form, file } from 'elysia' new Elysia() .get('/', () => form({ name: 'Tea Party', images: [file('nagi.web'), file('mika.webp')] })) .listen(3000) ``` 即使需要返回文件或多部分表单数据,此模式也非常有用。 ¥This pattern is useful if even need to return a file or multipart form data. ### 返回单个文件 {#return-a-single-file} ¥Return a single file 或者,你可以直接返回 `file` 而不返回 `form`,从而返回单个文件。 ¥Or alternatively, you can return a single file by returning `file` directly without `form`. ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/', file('nagi.web')) .listen(3000) ``` ## 句柄 {#handle} ¥Handle 由于 Elysia 建立在 Web 标准请求之上,我们可以使用 `Elysia.handle` 以编程方式对其进行测试。 ¥As Elysia is built on top of Web Standard Request, we can programmatically test it using `Elysia.handle`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'hello') .post('/hi', () => 'hi') .listen(3000) app.handle(new Request('http://localhost/')).then(console.log) ``` Elysia.handle 是一个用于处理发送到服务器的实际请求的函数。 ¥**Elysia.handle** is a function to process an actual request sent to the server. ::: tip 提示 与单元测试的模拟不同,你可以期望它的行为类似于发送到服务器的实际请求。 ¥Unlike unit test's mock, **you can expect it to behave like an actual request** sent to the server. 但它对于模拟或创建单元测试也很有用。 ¥But also useful for simulating or creating unit tests. ::: ## Stream {#stream} 要使用带有 `yield` 关键字的生成器函数返回开箱即用的响应流。 ¥To return a response streaming out of the box by using a generator function with `yield` keyword. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* () { yield 1 yield 2 yield 3 }) ``` 在这个例子中,我们可以使用 `yield` 关键字流式传输响应。 ¥This this example, we may stream a response by using `yield` keyword. ## 服务器发送事件 (SSE) {#server-sent-events-sse} ¥Server Sent Events (SSE) Elysia 通过提供 `sse` 实用函数来支持 [服务器发送事件](https://web.nodejs.cn/en-US/docs/Web/API/Server-sent_events)。 ¥Elysia supports [Server Sent Events](https://web.nodejs.cn/en-US/docs/Web/API/Server-sent_events) by providing a `sse` utility function. ```typescript twoslash import { Elysia, sse } from 'elysia' new Elysia() .get('/sse', function* () { yield sse('hello world') yield sse({ event: 'message', data: { message: 'This is a message', timestamp: new Date().toISOString() }, }) }) ``` 当值被封装在 `sse` 中时,Elysia 会自动将响应标头设置为 `text/event-stream`,并将数据格式化为 SSE 事件。 ¥When a value is wrapped in `sse`, Elysia will automatically set the response headers to `text/event-stream` and format the data as an SSE event. ### 设置标头 {#set-headers} ¥Set headers Elysia 将延迟返回响应头,直到生成第一个块。 ¥Elysia will defers returning response headers until the first chunk is yielded. 这使我们能够在响应流式传输之前设置标头。 ¥This allows us to set headers before the response is streamed. ```typescript twoslash import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* ({ set }) { // This will set headers set.headers['x-name'] = 'Elysia' yield 1 yield 2 // This will do nothing set.headers['x-id'] = '1' yield 3 }) ``` 生成第一个块后,Elysia 将在同一响应中发送标头和第一个块。 ¥Once the first chunk is yielded, Elysia will send the headers and the first chunk in the same response. 在生成第一个块后设置标头将不起作用。 ¥Setting headers after the first chunk is yielded will do nothing. ### 条件流 {#conditional-stream} ¥Conditional Stream 如果响应没有返回 yield,Elysia 会自动将流转换为正常响应。 ¥If the response is returned without yield, Elysia will automatically convert stream to normal response instead. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/ok', function* () { if (Math.random() > 0.5) return 'ok' yield 1 yield 2 yield 3 }) ``` 这使我们能够有条件地流式传输响应,或在必要时返回正常响应。 ¥This allows us to conditionally stream a response or return a normal response if necessary. ### 中止 {#abort} ¥Abort 在流式传输响应时,请求通常在响应完全流式传输之前就被取消,这是很常见的情况。 ¥While streaming a response, it's common that request may be cancelled before the response is fully streamed. 当请求取消时,Elysia 将自动停止生成器函数。 ¥Elysia will automatically stop the generator function when the request is cancelled. ### Eden {#eden} [Eden](/eden/overview) 会将流响应解释为 `AsyncGenerator`,从而允许我们使用 `for await` 循环来消费流。 ¥[Eden](/eden/overview) will interpret a stream response as `AsyncGenerator` allowing us to use `for await` loop to consume the stream. ```typescript twoslash import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia() .get('/ok', function* () { yield 1 yield 2 yield 3 }) const { data, error } = await treaty(app).ok.get() if (error) throw error for await (const chunk of data) console.log(chunk) ``` ## 扩展上下文 {#extending-context} ¥Extending context 由于 Elysia 仅提供基本信息,我们可以根据具体需求自定义 Context,例如: ¥As Elysia only provides essential information, we can customize Context for our specific need for instance: * 提取用户 ID 作为变量 * 注入通用模式存储库 * 添加数据库连接 我们可以使用以下 API 扩展 Elysia 的上下文以自定义上下文: ¥We may extend Elysia's context by using the following APIs to customize the Context: * [state](#state) - 全局可变状态 * [decorate](#decorate) - 为 Context 分配了附加属性 * [derive](#derive) / [resolve](#resolve) - 从现有属性创建新值 ### 何时扩展上下文 {#when-to-extend-context} ¥When to extend context 你应该只在以下情况下扩展上下文: ¥You should only extend context when: * 属性是全局可变状态,使用 [state](#state) 可在多个路由之间共享。 * 属性使用 [decorate](#decorate) 与请求或响应关联。 * 属性使用 [derive](#derive)/[resolve](#resolve) 从现有属性派生。 否则,我们建议单独定义一个值或函数,而不是扩展上下文。 ¥Otherwise, we recommend defining a value or function separately than extending the context. ::: tip 提示 建议将与请求和响应相关的属性或常用函数分配给 Context,以便分离关注点。 ¥It's recommended to assign properties related to request and response, or frequently used functions to Context for separation of concerns. ::: ## 状态 {#state} ¥State 状态是 Elysia 应用之间共享的全局可变对象或状态。 ¥**State** is a global mutable object or state shared across the Elysia app. 调用 state 后,value 将在调用时添加到 store 属性中,并可在处理程序中使用。 ¥Once **state** is called, value will be added to **store** property **once at call time**, and can be used in handler. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('version', 1) .get('/a', ({ store: { version } }) => version) // ^? .get('/b', ({ store }) => store) .get('/c', () => 'still ok') .listen(3000) ``` ### 何时使用 {#when-to-use} ¥When to use * 当你需要在多个路由之间共享原始可变值时 * 如果你想使用非原始类型或 `wrapper` 值或类来改变内部状态,请改用 [decorate](#decorate)。 ### 关键要点 {#key-takeaway} ¥Key takeaway * store 是整个 Elysia 应用的单一真实来源全局可变对象的表示。 * state 是一个用于分配初始值进行存储的函数,该值稍后可以进行修改。 * 确保在处理程序中使用它之前先赋值。 ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' new Elysia() // ❌ TypeError: counter doesn't exist in store .get('/error', ({ store }) => store.counter) .state('counter', 0) // ✅ Because we assigned a counter before, we can now access it .get('/', ({ store }) => store.counter) ``` ::: tip 提示 请注意,我们不能在赋值之前使用状态值。 ¥Beware that we cannot use a state value before assign. Elysia 自动将状态值注册到存储中,无需显式指定类型或额外的 TypeScript 泛型。 ¥Elysia registers state values into the store automatically without explicit type or additional TypeScript generic needed. ::: ## 装饰 {#decorate} ¥Decorate decorate 在调用时直接为 Context 分配一个附加属性。 ¥**decorate** assigns an additional property to **Context** directly **at call time**. ```typescript twoslash import { Elysia } from 'elysia' class Logger { log(value: string) { console.log(value) } } new Elysia() .decorate('logger', new Logger()) // ✅ defined from the previous line .get('/', ({ logger }) => { logger.log('hi') return 'hi' }) ``` ### 何时使用 {#when-to-use-1} ¥When to use * Context 的常量或只读值对象 * 可能包含内部可变状态的非原始值或类 * 为所有处理程序添加额外的函数、单例或不可变属性。 ### 关键要点 {#key-takeaway-1} ¥Key takeaway * 与 state 不同,修饰的值不应该被修改,尽管这是可能的。 * 确保在处理程序中使用它之前先赋值。 ## 派生 {#derive} ¥Derive 从 Context 中的现有属性中检索值并分配新属性。 ¥Retrieve values from existing properties in **Context** and assign new properties. 在转换生命周期中发生请求时,Derive 会进行赋值,从而允许我们 "derive" (从现有属性创建新属性)。 ¥Derive assigns when request happens **at transform lifecycle** allowing us to "derive" (create new properties from existing properties). ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .derive(({ headers }) => { const auth = headers['authorization'] return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` 因为 derive 在新请求启动后才会赋值,所以 derive 可以访问请求属性,例如 headers、query、body,而 store 和 decorate 则不能。 ¥Because **derive** is assigned once a new request starts, **derive** can access request properties like **headers**, **query**, **body** where **store**, and **decorate** can't. ### 何时使用 {#when-to-use-2} ¥When to use * 根据 Context 中现有属性创建一个新属性,无需验证或类型检查 * 当你需要访问请求属性(例如 headers、query、body)而不进行验证时 ### 关键要点 {#key-takeaway-2} ¥Key takeaway * 与 state 和 decorate 不同,derive 不是在调用时赋值,而是在新请求启动时赋值。 * derive 在转换时调用,或者在验证发生之前,Elysia 无法安全地确认请求属性的类型,导致结果为未知。如果你想从类型化的请求属性中分配新值,你可能需要改用 [resolve](#resolve)。 ## 解析 {#resolve} ¥Resolve 与 [derive](#derive) 相同,resolve 允许我们为 context 分配新属性。 ¥Same as [derive](#derive), resolve allow us to assign a new property to context. Resolve 在 beforeHandle 生命周期或验证之后被调用,这使我们能够安全地获取请求属性。 ¥Resolve is called at **beforeHandle** lifecycle or **after validation**, allowing us to **derive** request properties safely. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .guard({ headers: t.Object({ bearer: t.String({ pattern: '^Bearer .+$' }) }) }) .resolve(({ headers }) => { return { bearer: headers.bearer.slice(7) } }) .get('/', ({ bearer }) => bearer) ``` ### 何时使用 {#when-to-use-3} ¥When to use * 根据 Context 中现有属性创建一个新属性,并保证类型完整性(已检查类型) * 当你需要访问请求属性(例如 headers、query、body)并进行验证时 ### 关键要点 {#key-takeaway-3} ¥Key takeaway * resolve 在 beforeHandle 调用,即验证完成后调用。Elysia 可以安全地确认请求属性的类型,从而生成正确的类型。 ### 来自 resolve/derive 的错误 {#error-from-resolvederive} ¥Error from resolve/derive 由于 resolve 和 derive 基于 transform 和 beforeHandle 生命周期,因此我们可以从 resolve 和 derive 中返回错误。如果 derive 返回错误,Elysia 将提前退出并将错误作为响应返回。 ¥As resolve and derive is based on **transform** and **beforeHandle** lifecycle, we can return an error from resolve and derive. If error is returned from **derive**, Elysia will return early exit and return the error as response. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .derive(({ headers, status }) => { const auth = headers['authorization'] if(!auth) return status(400) return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` ## 模式 {#pattern} ¥Pattern state、decorate 提供了类似的 API 模式,用于将属性赋值给 Context,如下所示: ¥**state**, **decorate** offers a similar APIs pattern for assigning property to Context as the following: * key-value * object * remap 其中 derive 只能与 remap 一起使用,因为它依赖于现有值。 ¥Where **derive** can be only used with **remap** because it depends on existing value. ### key-value {#key-value} 我们可以使用 state 和 decorate 来通过键值模式赋值。 ¥We can use **state**, and **decorate** to assign a value using a key-value pattern. ```typescript import { Elysia } from 'elysia' class Logger { log(value: string) { console.log(value) } } new Elysia() .state('counter', 0) .decorate('logger', new Logger()) ``` 此模式非常适合于设置单个属性的可读性。 ¥This pattern is great for readability for setting a single property. ### 对象 {#object} ¥Object 最好将多个属性的赋值包含在一个对象中,以便进行单次赋值。 ¥Assigning multiple properties is better contained in an object for a single assignment. ```typescript import { Elysia } from 'elysia' new Elysia() .decorate({ logger: new Logger(), trace: new Trace(), telemetry: new Telemetry() }) ``` 对象提供了一个重复性更低的 API 来设置多个值。 ¥The object offers a less repetitive API for setting multiple values. ### 重新映射 {#remap} ¥Remap Remap 是一种函数重新赋值。 ¥Remap is a function reassignment. 允许我们从现有值创建新值,例如重命名或删除属性。 ¥Allowing us to create a new value from existing value like renaming or removing a property. 通过提供一个函数,并返回一个全新的对象来重新赋值。 ¥By providing a function, and returning an entirely new object to reassign the value. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' new Elysia() .state('counter', 0) .state('version', 1) .state(({ version, ...store }) => ({ ...store, elysiaVersion: 1 })) // ✅ Create from state remap .get('/elysia-version', ({ store }) => store.elysiaVersion) // ❌ Excluded from state remap .get('/version', ({ store }) => store.version) ``` 使用状态重映射从现有值创建新的初始值是个好主意。 ¥It's a good idea to use state remap to create a new initial value from the existing value. 然而,需要注意的是,Elysia 不提供这种方法的响应性,因为 remap 只会分配一个初始值。 ¥However, it's important to note that Elysia doesn't offer reactivity from this approach, as remap only assigns an initial value. ::: tip 提示 使用 remap,Elysia 会将返回的对象视为新属性,并删除对象中缺少的任何属性。 ¥Using remap, Elysia will treat a returned object as a new property, removing any property that is missing from the object. ::: ## 词缀 {#affix} ¥Affix 为了提供更流畅的体验,某些插件可能包含大量属性值,逐一重新映射可能会很麻烦。 ¥To provide a smoother experience, some plugins might have a lot of property value which can be overwhelming to remap one-by-one. Affix 函数由前缀和后缀组成,允许我们重新映射实例的所有属性。 ¥The **Affix** function which consists of **prefix** and **suffix**, allowing us to remap all property of an instance. ```ts twoslash import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate({ argon: 'a', boron: 'b', carbon: 'c' }) const app = new Elysia() .use( setup .prefix('decorator', 'setup') ) .get('/', ({ setupCarbon, ...rest }) => setupCarbon) ``` 允许我们轻松地批量重新映射插件的属性,避免插件的名称冲突。 ¥Allowing us to bulk remap a property of the plugin effortlessly, preventing the name collision of the plugin. 默认情况下,affix 将自动处理运行时和类型级代码,并将属性重新映射到驼峰命名规范。 ¥By default, **affix** will handle both runtime, type-level code automatically, remapping the property to camelCase as naming convention. 在某些情况下,我们还可以重新映射插件的 `all` 属性: ¥In some condition, we can also remap `all` property of the plugin: ```ts twoslash import { Elysia } from 'elysia' const setup = new Elysia({ name: 'setup' }) .decorate({ argon: 'a', boron: 'b', carbon: 'c' }) const app = new Elysia() .use(setup.prefix('all', 'setup')) // [!code ++] .get('/', ({ setupCarbon, ...rest }) => setupCarbon) ``` ## 参考和值 {#reference-and-value} ¥Reference and value 要更改状态,建议使用引用进行更改,而不是使用实际值。 ¥To mutate the state, it's recommended to use **reference** to mutate rather than using an actual value. 从 JavaScript 访问属性时,如果我们将对象属性中的原始值定义为新值,则引用将丢失,该值将被视为新的独立值。 ¥When accessing the property from JavaScript, if we define a primitive value from an object property as a new value, the reference is lost, the value is treated as new separate value instead. 例如: ¥For example: ```typescript const store = { counter: 0 } store.counter++ console.log(store.counter) // ✅ 1 ``` 我们可以使用 store.counter 来访问和修改属性。 ¥We can use **store.counter** to access and mutate the property. 但是,如果我们将计数器定义为新值 ¥However, if we define a counter as a new value ```typescript const store = { counter: 0 } let counter = store.counter counter++ console.log(store.counter) // ❌ 0 console.log(counter) // ✅ 1 ``` 一旦将原始值重新定义为新变量,引用 "link" 将会丢失,从而导致意外行为。 ¥Once a primitive value is redefined as a new variable, the reference **"link"** will be missing, causing unexpected behavior. 这可以应用于 `store`,因为它是一个全局可变对象。 ¥This can apply to `store`, as it's a global mutable object instead. ```typescript import { Elysia } from 'elysia' new Elysia() .state('counter', 0) // ✅ Using reference, value is shared .get('/', ({ store }) => store.counter++) // ❌ Creating a new variable on primitive value, the link is lost .get('/error', ({ store: { counter } }) => counter) ``` --- --- url: 'https://elysiajs.com/integrations/cheat-sheet.md' --- # 备忘单 {#cheat-sheet} ¥Cheat Sheet 以下是 Elysia 常见模式的快速概述 ¥Here are a quick overview for a common Elysia patterns ## 你好,世界 {#hello-world} ¥Hello World 一个简单的 Hello World ¥A simple hello world ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'Hello World') .listen(3000) ``` ## 自定义 HTTP 方法 {#custom-http-method} ¥Custom HTTP Method 使用自定义 HTTP 方法/动词定义路由 ¥Define route using custom HTTP methods/verbs 参见 [路由](/essential/route.html#custom-method) ¥See [Route](/essential/route.html#custom-method) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/hi', () => 'Hi') .post('/hi', () => 'From Post') .put('/hi', () => 'From Put') .route('M-SEARCH', '/hi', () => 'Custom Method') .listen(3000) ``` ## 路径参数 {#path-parameter} ¥Path Parameter 使用动态路径参数 ¥Using dynamic path parameter 参见 [路径](/essential/route.html#path-type) ¥See [Path](/essential/route.html#path-type) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) .get('/rest/*', () => 'Rest') .listen(3000) ``` ## 返回 JSON {#return-json} ¥Return JSON Elysia 自动将响应转换为 JSON ¥Elysia converts response to JSON automatically 参见 [处理程序](/essential/handler.html) ¥See [Handler](/essential/handler.html) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/json', () => { return { hello: 'Elysia' } }) .listen(3000) ``` ## 返回文件 {#return-a-file} ¥Return a file 文件可以作为表单数据响应返回。 ¥A file can be return in as formdata response 响应必须是 1 级深度对象 ¥The response must be a 1-level deep object ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/json', () => { return { hello: 'Elysia', image: file('public/cat.jpg') } }) .listen(3000) ``` ## 标头和状态 {#header-and-status} ¥Header and status 设置自定义标头和状态码 ¥Set a custom header and a status code 参见 [处理程序](/essential/handler.html) ¥See [Handler](/essential/handler.html) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', ({ set, status }) => { set.headers['x-powered-by'] = 'Elysia' return status(418, "I'm a teapot") }) .listen(3000) ``` ## 组 {#group} ¥Group 为子路由定义一次前缀 ¥Define a prefix once for sub routes 参见 [组](/essential/route.html#group) ¥See [Group](/essential/route.html#group) ```typescript import { Elysia } from 'elysia' new Elysia() .get("/", () => "Hi") .group("/auth", app => { return app .get("/", () => "Hi") .post("/sign-in", ({ body }) => body) .put("/sign-up", ({ body }) => body) }) .listen(3000) ``` ## Schema {#schema} 强制路由的数据类型 ¥Enforce a data type of a route 参见 [验证](/essential/validation) ¥See [Validation](/essential/validation) ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/mirror', ({ body: { username } }) => username, { body: t.Object({ username: t.String(), password: t.String() }) }) .listen(3000) ``` ## 文件上传 {#file-upload} ¥File upload 参见 [Validation#file](/essential/validation#file) ¥See [Validation#file](/essential/validation#file) ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/body', ({ body }) => body, { // ^? body: t.Object({ file: t.File({ format: 'image/*' }), multipleFiles: t.Files() }) }) .listen(3000) ``` ## 生命周期钩子 {#lifecycle-hook} ¥Lifecycle Hook 按顺序拦截 Elysia 事件 ¥Intercept an Elysia event in order 参见 [生命周期](/essential/life-cycle.html) ¥See [Lifecycle](/essential/life-cycle.html) ```typescript import { Elysia, t } from 'elysia' new Elysia() .onRequest(() => { console.log('On request') }) .on('beforeHandle', () => { console.log('Before handle') }) .post('/mirror', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }), afterHandle: () => { console.log("After handle") } }) .listen(3000) ``` ## 守护 {#guard} ¥Guard 强制子路由的数据类型 ¥Enforce a data type of sub routes 参见 [范围](/essential/plugin.html#scope) ¥See [Scope](/essential/plugin.html#scope) ```typescript twoslash // @errors: 2345 import { Elysia, t } from 'elysia' new Elysia() .guard({ response: t.String() }, (app) => app .get('/', () => 'Hi') // Invalid: will throws error, and TypeScript will report error .get('/invalid', () => 1) ) .listen(3000) ``` ## 自定义上下文 {#custom-context} ¥Custom context 为路由上下文添加自定义变量 ¥Add custom variable to route context 参见 [上下文](/essential/handler.html#context) ¥See [Context](/essential/handler.html#context) ```typescript import { Elysia } from 'elysia' new Elysia() .state('version', 1) .decorate('getDate', () => Date.now()) .get('/version', ({ getDate, store: { version } }) => `${version} ${getDate()}`) .listen(3000) ``` ## 重定向 {#redirect} ¥Redirect 重定向响应 ¥Redirect a response 参见 [处理程序](/essential/handler.html#redirect) ¥See [Handler](/essential/handler.html#redirect) ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'hi') .get('/redirect', ({ redirect }) => { return redirect('/') }) .listen(3000) ``` ## 插件 {#plugin} ¥Plugin 创建一个单独的实例 ¥Create a separate instance 参见 [插件](/essential/plugin) ¥See [Plugin](/essential/plugin) ```typescript import { Elysia } from 'elysia' const plugin = new Elysia() .state('plugin-version', 1) .get('/hi', () => 'hi') new Elysia() .use(plugin) .get('/version', ({ store }) => store['plugin-version']) .listen(3000) ``` ## Web Socket {#web-socket} 使用 Web Socket 创建实时连接 ¥Create a realtime connection using Web Socket 参见 [Web Socket](/patterns/websocket) ¥See [Web Socket](/patterns/websocket) ```typescript import { Elysia } from 'elysia' new Elysia() .ws('/ping', { message(ws, message) { ws.send('hello ' + message) } }) .listen(3000) ``` ## OpenAPI 文档 {#openapi-documentation} ¥OpenAPI documentation 使用 Scalar(或可选)创建交互式文档 Swagger) ¥Create interactive documentation using Scalar (or optionally Swagger) 参见 [openapi](/plugins/openapi.html) ¥See [openapi](/plugins/openapi.html) ```typescript import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' const app = new Elysia() .use(openapi()) .listen(3000) console.log(`View documentation at "${app.server!.url}openapi" in your browser`); ``` ## 单元测试 {#unit-test} ¥Unit Test 编写 Elysia 应用的单元测试 ¥Write a unit test of your Elysia app 参见 [单元测试](/patterns/unit-test) ¥See [Unit Test](/patterns/unit-test) ```typescript // test/index.test.ts import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' describe('Elysia', () => { it('return a response', async () => { const app = new Elysia().get('/', () => 'hi') const response = await app .handle(new Request('http://localhost/')) .then((res) => res.text()) expect(response).toBe('hi') }) }) ``` ## 自定义 body 解析器 {#custom-body-parser} ¥Custom body parser 创建用于解析正文的自定义逻辑 ¥Create custom logic for parsing body 参见 [解析](/essential/life-cycle.html#parse) ¥See [Parse](/essential/life-cycle.html#parse) ```typescript import { Elysia } from 'elysia' new Elysia() .onParse(({ request, contentType }) => { if (contentType === 'application/custom-type') return request.text() }) ``` ## GraphQL {#graphql} 使用 GraphQL Yoga 或 Apollo 创建自定义 GraphQL 服务器 ¥Create a custom GraphQL server using GraphQL Yoga or Apollo 参见 [GraphQL Yoga](/plugins/graphql-yoga) ¥See [GraphQL Yoga](/plugins/graphql-yoga) ```typescript import { Elysia } from 'elysia' import { yoga } from '@elysiajs/graphql-yoga' const app = new Elysia() .use( yoga({ typeDefs: /* GraphQL */` type Query { hi: String } `, resolvers: { Query: { hi: () => 'Hello from Elysia' } } }) ) .listen(3000) ``` --- --- url: 'https://elysiajs.com/patterns/macro.md' --- # 宏 {#macro} ¥Macro 宏类似于一个函数,可以控制生命周期事件、Schema 和上下文,并具有完全的类型安全性。 ¥Macro is similar to a function that have a control over the lifecycle event, schema, context with full type safety. 一旦定义,它将在钩子中可用,并可通过添加属性来激活。 ¥Once defined, it will be available in hook and can be activated by adding the property. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia({ name: 'plugin' }) .macro({ hi: (word: string) => ({ beforeHandle() { console.log(word) } }) }) const app = new Elysia() .use(plugin) .get('/', () => 'hi', { hi: 'Elysia' // [!code ++] }) ``` 访问路径应该将 "Elysia" 记录为结果。 ¥Accessing the path should log **"Elysia"** as the results. ## 属性简写 {#property-shorthand} ¥Property shorthand 从 Elysia 1.2.10 版本开始,宏对象中的每个属性可以是函数或对象。 ¥Starting from Elysia 1.2.10, each property in the macro object can be a function or an object. 如果属性是对象,它将被转换为接受布尔参数的函数,并在参数为 true 时执行该函数。 ¥If the property is an object, it will be translated to a function that accept a boolean parameter, and will be executed if the parameter is true. ```typescript import { Elysia } from 'elysia' export const auth = new Elysia() .macro({ // This property shorthand isAuth: { resolve: () => ({ user: 'saltyaom' }) }, // is equivalent to isAuth(enabled: boolean) { if(!enabled) return return { resolve() { return { user } } } } }) ``` ## API {#api} 宏与钩子具有相同的 API。 ¥**macro** has the same API as hook. 在前面的示例中,我们创建了一个接受字符串的 hi 宏。 ¥In previous example, we create a **hi** macro accepting a **string**. 然后,我们将 hi 赋值给 "Elysia",该值被返回给 hi 函数,之后该函数向 beforeHandle 堆栈添加了一个新事件。 ¥We then assigned **hi** to **"Elysia"**, the value was then sent back to the **hi** function, and then the function added a new event to **beforeHandle** stack. 这相当于将函数推送到 beforeHandle,如下所示: ¥Which is an equivalent of pushing function to **beforeHandle** as the following: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'hi', { beforeHandle() { console.log('Elysia') } }) ``` 当逻辑比接受新函数更复杂时,宏会大放异彩,例如为每个路由创建一个授权层。 ¥**macro** shine when a logic is more complex than accepting a new function, for example creating an authorization layer for each route. ```typescript twoslash // @filename: auth.ts import { Elysia } from 'elysia' export const auth = new Elysia() .macro({ isAuth: { resolve() { return { user: 'saltyaom' } } }, role(role: 'admin' | 'user') { return {} } }) // @filename: index.ts // ---cut--- import { Elysia } from 'elysia' import { auth } from './auth' const app = new Elysia() .use(auth) .get('/', ({ user }) => user, { // ^? isAuth: true, role: 'admin' }) ``` 宏还可以向上下文注册一个新属性,从而允许我们直接从上下文访问该属性的值。 ¥Macro can also register a new property to the context, allowing us to access the value directly from the context. 该字段可以接受从字符串到函数的任何内容,这使我们能够创建自定义生命周期事件。 ¥The field can accept anything ranging from string to function, allowing us to create a custom life cycle event. 宏将根据钩子中的定义从上到下按顺序执行,确保堆栈以正确的顺序处理。 ¥**macro** will be executed in order from top-to-bottom according to definition in hook, ensure that the stack is handled in the correct order. ## 解析 {#resolve} ¥Resolve 通过返回带有 [**resolve**](/essential/life-cycle.html#resolve) 函数的对象,可以向上下文添加属性。 ¥You add a property to the context by returning an object with a [**resolve**](/essential/life-cycle.html#resolve) function. ```ts twoslash import { Elysia } from 'elysia' new Elysia() .macro({ user: (enabled: true) => ({ resolve: () => ({ user: 'Pardofelis' }) }) }) .get('/', ({ user }) => user, { // ^? user: true }) ``` 在上面的例子中,我们通过返回一个带有 resolve 函数的对象,向上下文中添加了一个新的属性 user。 ¥In the example above, we add a new property **user** to the context by returning an object with a **resolve** function. 以下是宏解析可能有用的示例: ¥Here's an example that macro resolve could be useful: * 执行身份验证并将用户添加到上下文 * 运行额外的数据库查询并将数据添加到上下文 * 向上下文添加新属性 ### 带解析的宏扩展 {#macro-extension-with-resolve} ¥Macro extension with resolve 由于 TypeScript 的限制,扩展其他宏的宏无法将类型推断到 resolve 函数中。 ¥Due to TypeScript limitation, macro that extends other macro cannot infer type into **resolve** function. 我们提供了一个命名的单宏作为解决此限制的解决方法。 ¥We provide a named single macro as a workaround to this limitation. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .macro('user', { resolve: () => ({ user: 'lilith' as const }) }) .macro('user2', { user: true, resolve: ({ user }) => { // ^? } }) ``` ## Schema {#schema} 你可以为宏定义自定义架构,以确保使用该宏的路由传递正确的类型。 ¥You can define a custom schema for your macro, to make sure that the route using the macro is passing the correct type. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .macro({ withFriends: { body: t.Object({ friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')]) }) } }) .post('/', ({ body }) => body.friends, { // ^? body: t.Object({ name: t.Literal('Lilith') }), withFriends: true }) ``` 带有 Schema 的宏会自动验证和推断类型以确保类型安全,并且它可以与现有 Schema 共存。 ¥Macro with schema will automatically validate and infer type to ensure type safety, and it can co-exist with existing schema as well. 你还可以堆叠来自不同宏的多个模式,甚至来自标准验证器,它们将无缝协作。 ¥You can also stack multiple schema from different macro, or even from Standard Validator and it will work together seamlessly. ### Schema 生命周期在同一个宏中 {#schema-with-lifecycle-in-the-same-macro} ¥Schema with lifecycle in the same macro 与 [带解析的宏扩展](#macro-extension-with-resolve) 类似, ¥Similar to [Macro extension with resolve](#macro-extension-with-resolve), 宏 Schema 也支持同一宏内生命周期的类型推断,但由于 TypeScript 的限制,仅限于命名单个宏。 ¥Macro schema also support type inference for **lifecycle within the same macro** **BUT** only with named single macro due to TypeScript limitation. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .macro('withFriends', { body: t.Object({ friends: t.Tuple([t.Literal('Fouco'), t.Literal('Sartre')]) }), beforeHandle({ body: { friends } }) { // ^? } }) ``` 如果你想在同一个宏中使用生命周期类型推断,你可能需要使用命名的单个宏而不是多个堆叠宏。 ¥If you want to use lifecycle type inference within the same macro, you might want to use a named single macro instead of multiple stacked macro > 不要与使用宏模式推断路由生命周期事件的类型相混淆。这样就很好了,但这个限制仅适用于在同一个宏中使用生命周期。 ## 扩展 {#extension} ¥Extension 宏可以扩展其他宏,允许你在现有宏的基础上进行构建。 ¥Macro can extends other macro, allowing you to build upon existing one. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .macro({ sartre: { body: t.Object({ sartre: t.Literal('Sartre') }) }, fouco: { body: t.Object({ fouco: t.Literal('Fouco') }) }, lilith: { fouco: true, sartre: true, body: t.Object({ lilith: t.Literal('Lilith') }) } }) .post('/', ({ body }) => body, { // ^? lilith: true }) // ---cut-after--- // ``` 这允许你在现有宏的基础上构建并添加更多功能。 ¥This allow you to build upon existing macro, and add more functionality to it. ## 数据去重 {#deduplication} ¥Deduplication 宏会自动删除重复的生命周期事件,确保每个生命周期事件只执行一次。 ¥Macro will automatically deduplicate the lifecycle event, ensuring that each lifecycle event is only executed once. 默认情况下,Elysia 将使用属性值作为种子,但你可以通过提供自定义种子来覆盖它。 ¥By default, Elysia will use the property value as the seed, but you can override it by providing a custom seed. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .macro({ sartre: (role: string) => ({ seed: role, // [!code ++] body: t.Object({ sartre: t.Literal('Sartre') }) }) }) ``` 但是,如果你意外创建了循环依赖,Elysia 的限制堆栈为 16,以防止运行时和类型推断中的无限循环。 ¥However, if you evert accidentally create a circular dependency, Elysia have a limit stack of 16 to prevent infinite loop in both runtime and type inference. 如果路由已包含 OpenAPI 详细信息,它会将这些详细信息合并在一起,但优先使用路由详细信息而不是宏详细信息。 ¥If the route already has OpenAPI detail, it will merge the detail together but prefers the route detail over macro detail. --- --- url: 'https://elysiajs.com/quick-start.md' --- # 快速入门 {#quick-start} ¥Quick Start Elysia 是一个 TypeScript 后端框架,支持多种运行时,但针对 Bun 进行了优化。 ¥Elysia is a TypeScript backend framework with multiple runtime support but optimized for Bun. 但是,你可以将 Elysia 与其他运行时(例如 Node.js)一起使用。 ¥However, you can use Elysia with other runtimes like Node.js. \ Elysia 针对 Bun 进行了优化,Bun 是一个 JavaScript 运行时,旨在成为 Node.js 的直接替代品。 ¥Elysia is optimized for Bun which is a JavaScript runtime that aims to be a drop-in replacement for Node.js. 你可以使用以下命令安装 Bun: ¥You can install Bun with the command below: ::: code-group ```bash [MacOS/Linux] curl -fsSL https://bun.sh/install | bash ``` ```bash [Windows] powershell -c "irm bun.sh/install.ps1 | iex" ``` ::: \ 我们建议使用 `bun create elysia` 启动新的 Elysia 服务器,它会自动设置所有设置。 ¥We recommend starting a new Elysia server using `bun create elysia`, which sets up everything automatically. ```bash bun create elysia app ``` 完成后,你应该会在目录中看到名为 `app` 的文件夹。 ¥Once done, you should see the folder name `app` in your directory. ```bash cd app ``` 通过以下方式启动开发服务器: ¥Start a development server by: ```bash bun dev ``` 导航到 [localhost:3000](http://localhost:3000),你应该会看到 "你好,Elysia"。 ¥Navigate to [localhost:3000](http://localhost:3000) should greet you with "Hello Elysia". ::: tip 提示 Elysia 附带 `dev` 命令,可在文件更改时自动重新加载服务器。 ¥Elysia ships you with `dev` command to automatically reload your server on file change. ::: 要手动创建新的 Elysia 应用,请将 Elysia 安装为包: ¥To manually create a new Elysia app, install Elysia as a package: ```typescript bun add elysia bun add -d @types/bun ``` 这将安装 Elysia 和 Bun 类型定义。 ¥This will install Elysia and Bun type definitions. 创建一个新的文件 `src/index.ts` 并添加以下代码: ¥Create a new file `src/index.ts` and add the following code: ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` 打开你的 `package.json` 文件并添加以下脚本: ¥Open your `package.json` file and add the following scripts: ```json { "scripts": { "dev": "bun --watch src/index.ts", "build": "bun build src/index.ts --target bun --outdir ./dist", "start": "NODE_ENV=production bun dist/index.js", "test": "bun test" } } ``` 这些脚本涉及应用开发的不同阶段: ¥These scripts refer to the different stages of developing an application: * dev - 以开发模式启动 Elysia,并在代码更改时自动重新加载。 * build - 构建用于生产环境的应用。 * start - 启动 Elysia 生产服务器。 如果你使用 TypeScript,请确保创建并更新 `tsconfig.json`,使其包含 `compilerOptions.strict` 至 `true`: ¥If you are using TypeScript, make sure to create, and update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` Node.js 是用于服务器端应用的 JavaScript 运行时,也是 Elysia 支持的最流行的 JavaScript 运行时。 ¥Node.js is a JavaScript runtime for server-side applications, the most popular runtime for JavaScript which Elysia supports. 你可以使用以下命令安装 Node.js: ¥You can install Node.js with the command below: ::: code-group ```bash [MacOS] brew install node ``` ```bash [Windows] choco install nodejs ``` ```bash [apt (Linux)] sudo apt install nodejs ``` ```bash [pacman (Arch)] pacman -S nodejs npm ``` ::: ## 设置 {#setup} ¥Setup 我们建议在 Node.js 项目中使用 TypeScript。 ¥We recommend using TypeScript for your Node.js project. \ 要使用 TypeScript 创建新的 Elysia 应用,我们建议安装带有 `tsx` 的 Elysia: ¥To create a new Elysia app with TypeScript, we recommend installing Elysia with `tsx`: ::: code-group ```bash [bun] bun add elysia @elysiajs/node && \ bun add -d tsx @types/node typescript ``` ```bash [pnpm] pnpm add elysia @elysiajs/node && \ pnpm add -D tsx @types/node typescript ``` ```bash [npm] npm install elysia @elysiajs/node && \ npm install --save-dev tsx @types/node typescript ``` ```bash [yarn] yarn add elysia @elysiajs/node && \ yarn add -D tsx @types/node typescript ``` ::: 这将安装 Elysia、TypeScript 和 `tsx`。 ¥This will install Elysia, TypeScript, and `tsx`. `tsx` 是一个 CLI,它将 TypeScript 转换为 JavaScript,具有热重载以及你期望从现代开发环境中获得的更多功能。 ¥`tsx` is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment. 创建一个新的文件 `src/index.ts` 并添加以下代码: ¥Create a new file `src/index.ts` and add the following code: ```typescript import { Elysia } from 'elysia' import { node } from '@elysiajs/node' const app = new Elysia({ adapter: node() }) .get('/', () => 'Hello Elysia') .listen(3000, ({ hostname, port }) => { console.log( `🦊 Elysia is running at ${hostname}:${port}` ) }) ``` 打开你的 `package.json` 文件并添加以下脚本: ¥Open your `package.json` file and add the following scripts: ```json { "scripts": { "dev": "tsx watch src/index.ts", "build": "tsc src/index.ts --outDir dist", "start": "NODE_ENV=production node dist/index.js" } } ``` 这些脚本涉及应用开发的不同阶段: ¥These scripts refer to the different stages of developing an application: * dev - 以开发模式启动 Elysia,并在代码更改时自动重新加载。 * build - 构建用于生产环境的应用。 * start - 启动 Elysia 生产服务器。 确保创建 `tsconfig.json` ¥Make sure to create `tsconfig.json` ```bash npx tsc --init ``` 不要忘记更新 `tsconfig.json` 以包含 `compilerOptions.strict` 到 `true`: ¥Don't forget to update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` ::: warning 警告 如果你在不使用 TypeScript 的情况下使用 Elysia,你可能会遗漏一些功能,例如自动补全、高级类型检查和端到端类型安全,而这些正是 Elysia 的核心功能。 ¥If you use Elysia without TypeScript you may miss out on some features like auto-completion, advanced type checking and end-to-end type safety, which are the core features of Elysia. ::: 要使用 JavaScript 创建新的 Elysia 应用,首先安装 Elysia: ¥To create a new Elysia app with JavaScript, starts by installing Elysia: ::: code-group ```bash [pnpm] bun add elysia @elysiajs/node ``` ```bash [pnpm] pnpm add elysia @elysiajs/node ``` ```bash [npm] npm install elysia @elysiajs/node ``` ```bash [yarn] yarn add elysia @elysiajs/node ``` ::: 这将安装 Elysia、TypeScript 和 `tsx`。 ¥This will install Elysia, TypeScript, and `tsx`. `tsx` 是一个 CLI,它将 TypeScript 转换为 JavaScript,具有热重载以及你期望从现代开发环境中获得的更多功能。 ¥`tsx` is a CLI that transpiles TypeScript to JavaScript with hot-reload and several more feature you expected from a modern development environment. 创建一个新的文件 `src/index.ts` 并添加以下代码: ¥Create a new file `src/index.ts` and add the following code: ```javascript import { Elysia } from 'elysia' import { node } from '@elysiajs/node' const app = new Elysia({ adapter: node() }) .get('/', () => 'Hello Elysia') .listen(3000, ({ hostname, port }) => { console.log( `🦊 Elysia is running at ${hostname}:${port}` ) }) ``` 打开你的 `package.json` 文件并添加以下脚本: ¥Open your `package.json` file and add the following scripts: ```json { "type", "module", "scripts": { "dev": "node src/index.ts", "start": "NODE_ENV=production node src/index.js" } } ``` 这些脚本涉及应用开发的不同阶段: ¥These scripts refer to the different stages of developing an application: * dev - 以开发模式启动 Elysia,并在代码更改时自动重新加载。 * start - 启动 Elysia 生产服务器。 确保创建 `tsconfig.json` ¥Make sure to create `tsconfig.json` ```bash npx tsc --init ``` 不要忘记更新 `tsconfig.json` 以包含 `compilerOptions.strict` 到 `true`: ¥Don't forget to update `tsconfig.json` to include `compilerOptions.strict` to `true`: ```json { "compilerOptions": { "strict": true } } ``` Elysia 是一个符合 WinterCG 标准的库,这意味着只要框架或运行时支持 Web 标准请求/响应,就可以运行 Elysia。 ¥Elysia is a WinterCG compliance library, which means if a framework or runtime supports Web Standard Request/Response, it can run Elysia. 首先,使用以下命令安装 Elysia: ¥First, install Elysia with the command below: ::: code-group ```bash [bun] bun install elysia ``` ```bash [pnpm] pnpm install elysia ``` ```bash [npm] npm install elysia ``` ```bash [yarn] yarn add elysia ``` ::: 接下来,选择支持 Web 标准请求/响应的运行时。 ¥Next, select a runtime that supports Web Standard Request/Response. 我们有一些建议: ¥We have a few recommendations: ### 不在列表中? {#not-on-the-list} ¥Not on the list? 如果你正在使用自定义运行时,则可以访问 `app.fetch` 来手动处理请求和响应。 ¥If you are using a custom runtime, you may access `app.fetch` to handle the request and response manually. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .listen(3000) export default app.fetch console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ## 后续步骤 {#next-steps} ¥Next Steps 我们建议你查看以下任一方法: ¥We recommend checking out the either one of the following: 如果你有任何问题,欢迎在我们的 [Discord](https://discord.gg/eaFJ2KDJck) 社区中提问。 ¥If you have any questions, feel free to ask in our [Discord](https://discord.gg/eaFJ2KDJck) community. --- --- url: 'https://elysiajs.com/plugins/bearer.md' --- # 承载器插件 {#bearer-plugin} ¥Bearer Plugin 用于检索 Bearer 令牌的 [elysia](https://github.com/elysiajs/elysia) 插件。 ¥Plugin for [elysia](https://github.com/elysiajs/elysia) for retrieving the Bearer token. 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/bearer ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { bearer } from '@elysiajs/bearer' const app = new Elysia() .use(bearer()) .get('/sign', ({ bearer }) => bearer, { beforeHandle({ bearer, set, status }) { if (!bearer) { set.headers[ 'WWW-Authenticate' ] = `Bearer realm='sign', error="invalid_request"` return status(400, 'Unauthorized') } } }) .listen(3000) ``` 此插件用于检索 [RFC6750](https://www.rfc-editor.org/rfc/rfc6750#section-2) 中指定的 Bearer 令牌。 ¥This plugin is for retrieving a Bearer token specified in [RFC6750](https://www.rfc-editor.org/rfc/rfc6750#section-2). 此插件不处理你服务器的身份验证。相反,该插件将决定权留给开发者,让他们自己应用逻辑来处理验证检查。 ¥This plugin DOES NOT handle authentication validation for your server. Instead, the plugin leaves the decision to developers to apply logic for handling validation check themselves. --- --- url: 'https://elysiajs.com/patterns/mount.md' --- # 挂载 {#mount} ¥Mount WinterCG 是一个 Web 互操作运行时的标准。Cloudflare、Deno、Vercel Edge Runtime、Netlify Function 以及其他各种工具均支持此功能,它允许 Web 服务器在使用 Web 标准定义(如 `Fetch`、`Request` 和 `Response`)的运行时之间互操作运行。 ¥WinterCG is a standard for web-interoperable runtimes. Supported by Cloudflare, Deno, Vercel Edge Runtime, Netlify Function, and various others, it allows web servers to run interoperably across runtimes that use Web Standard definitions like `Fetch`, `Request`, and `Response`. Elysia 符合 WinterCG 标准。我们已针对 Bun 进行了优化,但如果可能的话,也会公开支持其他运行时。 ¥Elysia is WinterCG compliant. We are optimized to run on Bun but also openly support other runtimes if possible. 理论上,这允许任何符合 WinterCG 标准的框架或代码一起运行,从而允许 Elysia、Hono、Remix、Itty Router 等框架在一个简单的函数中一起运行。 ¥In theory, this allows any framework or code that is WinterCG compliant to be run together, allowing frameworks like Elysia, Hono, Remix, Itty Router to run together in a simple function. 秉承这一理念,我们为 Elysia 实现了相同的逻辑,引入了 `.mount` 方法,使其能够与任何兼容 WinterCG 的框架或代码一起运行。 ¥Adhering to this, we implemented the same logic for Elysia by introducing `.mount` method to run with any framework or code that is WinterCG compliant. ## 挂载 {#mount-1} ¥Mount 使用 .mount,[只需传递 `fetch` 函数](https://twitter.com/saltyAom/status/1684786233594290176): ¥To use **.mount**, [simply pass a `fetch` function](https://twitter.com/saltyAom/status/1684786233594290176): ```ts import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello from Elysia') .mount('/hono', hono.fetch) ``` Fetch 函数是一个接受 Web 标准请求并返回 Web 标准响应的函数,其定义如下: ¥A **fetch** function is a function that accepts a Web Standard Request and returns a Web Standard Response with the definition of: ```ts // Web Standard Request-like object // Web Standard Response type fetch = (request: RequestLike) => Response ``` 默认情况下,此声明用于: ¥By default, this declaration is used by: * Bun * Deno * Vercel Edge 运行时 * Cloudflare Worker * Netlify Edge 函数 * 混合函数处理程序 * 等等 这允许你在单个服务器环境中执行所有上述代码,从而可以与 Elysia 无缝交互。你还可以在单​​个部署中重用现有函数,从而无需使用反向代理来管理多个服务器。 ¥This allows you to execute all the aforementioned code in a single server environment, making it possible to interact seamlessly with Elysia. You can also reuse existing functions within a single deployment, eliminating the need for a reverse proxy to manage multiple servers. 如果框架还支持 .mount 函数,则可以无限嵌套支持该函数的框架。 ¥If the framework also supports a **.mount** function, you can deeply nest a framework that supports it. ```ts import { Elysia } from 'elysia' import { Hono } from 'hono' 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 {#reusing-elysia} ¥Reusing Elysia 此外,你可以在服务器上复用多个现有的 Elysia 项目。 ¥Moreover, you can re-use multiple existing Elysia projects on your server. ```ts import { Elysia } from '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。 ¥If an instance passed to `mount` is an Elysia instance, it will be resolved with `use` automatically, providing type-safety and support for Eden by default. 这使得可互操作的框架和运行时成为现实。 ¥This makes the possibility of an interoperable framework and runtime a reality. --- --- url: 'https://elysiajs.com/essential/plugin.md' --- # 插件 {#plugin} ¥Plugin 插件是一种将功能分解为更小部分的模式。为我们的 Web 服务器创建可重用组件。 ¥Plugin is a pattern that decouples functionality into smaller parts. Creating reusable components for our web server. 定义插件就是定义一个单独的实例。 ¥Defining a plugin is to define a separate instance. ```typescript twoslash 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 来使用该插件。 ¥We can use the plugin by passing an instance to **Elysia.use**. 该插件将继承插件实例的所有属性,包括状态、装饰、派生、路由、生命周期等。 ¥The plugin will inherit all properties of the plugin instance, including **state**, **decorate**, **derive**, **route**, **lifecycle**, etc. Elysia 还会自动处理类型推断,因此你可以想象在主实例上调用所有其他实例。 ¥Elysia will also handle the type inference automatically as well, so you can imagine as if you call all of the other instances on the main one. ::: tip 提示 请注意,插件不包含 .listen,因为 .listen 会为使用情况分配端口,而我们只希望主实例分配该端口。 ¥Notice that the plugin doesn't contain **.listen**, because **.listen** will allocate a port for the usage, and we only want the main instance to allocate the port. ::: ## 插件 {#plugin-1} ¥Plugin 每个 Elysia 实例都可以是一个插件。 ¥Every Elysia instance can be a plugin. 我们可以将逻辑解耦到一个新的单独的 Elysia 实例中,并将其用作插件。 ¥We can decouple our logic into a new separate Elysia instance and use it as a plugin. 首先,我们在差异文件中定义一个实例: ¥First, we define an instance in a difference file: ```typescript twoslash // plugin.ts import { Elysia } from 'elysia' export const plugin = new Elysia() .get('/plugin', () => 'hi') ``` 然后我们将实例导入主文件: ¥And then we import the instance into the main file: ```typescript import { Elysia } from 'elysia' import { plugin } from './plugin' const app = new Elysia() .use(plugin) .listen(3000) ``` ### 配置 {#config} ¥Config 为了使插件更有用,建议允许通过配置进行自定义。 ¥To make the plugin more useful, allowing customization via config is recommended. 你可以创建一个函数,该函数接受可能改变插件行为的参数,以提高其可复用性。 ¥You can create a function that accepts parameters that may change the behavior of the plugin to make it more reusable. ```typescript import { Elysia } from 'elysia' const version = (version = 1) => new Elysia() .get('/version', version) const app = new Elysia() .use(version(1)) .listen(3000) ``` ### 函数式回调 {#functional-callback} ¥Functional callback 建议定义一个新的插件实例,而不是使用函数回调。 ¥It's recommended to define a new plugin instance instead of using a function callback. 函数式回调允许我们访问主实例的现有属性。例如,检查特定路由或存储是否存在。 ¥Functional callback allows us to access the existing property of the main instance. For example, checking if specific routes or stores existed. 要定义函数式回调,请创建一个接受 Elysia 作为参数的函数。 ¥To define a functional callback, create a function that accepts Elysia as a parameter. ```typescript twoslash 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) ``` 传递给 `Elysia.use` 后,函数式回调的行为与普通插件相同,只是属性直接赋值给主实例。 ¥Once passed to `Elysia.use`, functional callback behaves as a normal plugin except the property is assigned directly to the main instance. ::: tip 提示 你不必担心函数式回调和创建实例之间的性能差异。 ¥You shall not worry about the performance difference between a functional callback and creating an instance. Elysia 可以在几毫秒内创建 10,000 个实例,新的 Elysia 实例的类型推断性能甚至比函数式回调更好。 ¥Elysia can create 10k instances in a matter of milliseconds, the new Elysia instance has even better type inference performance than the functional callback. ::: ## 插件数据去重 {#plugin-deduplication} ¥Plugin Deduplication 默认情况下,Elysia 将注册任何插件并处理类型定义。 ¥By default, Elysia will register any plugin and handle type definitions. 有些插件可能会被多次使用以提供类型推断,从而导致重复设置初始值或路由。 ¥Some plugins may be used multiple times to provide type inference, resulting in duplication of setting initial values or routes. Elysia 通过使用名称和可选种子来区分实例,从而避免了重复实例: ¥Elysia avoids this by differentiating the instance by using **name** and **optional seeds** to help Elysia identify instance duplication: ```typescript import { Elysia } from 'elysia' const plugin = (config: { prefix: T }) => new Elysia({ name: 'my-plugin', // [!code ++] seed: config, // [!code ++] }) .get(`${config.prefix}/hi`, () => 'Hi') const app = new Elysia() .use( plugin({ prefix: '/v2' }) ) .listen(3000) ``` Elysia 将使用名称和种子创建校验和,以识别实例是否已注册,如果已注册,Elysia 将跳过插件的注册。 ¥Elysia will use **name** and **seed** to create a checksum to identify if the instance has been registered previously or not, if so, Elysia will skip the registration of the plugin. 如果未提供种子,Elysia 将仅使用名称来区分实例。这意味着即使你多次注册该插件,它也只会注册一次。 ¥If seed is not provided, Elysia will only use **name** to differentiate the instance. This means that the plugin is only registered once even if you registered it multiple times. ```typescript 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 通过重用已注册的插件来提高性能,而无需一遍又一遍地处理插件。 ¥This allows Elysia to improve performance by reusing the registered plugins instead of processing the plugin over and over again. ::: tip 提示 种子可以是任何内容,从字符串到复杂对象或类。 ¥Seed could be anything, varying from a string to a complex object or class. 如果提供的值是类,Elysia 将尝试使用 `.toString` 方法生成校验和。 ¥If the provided value is class, Elysia will then try to use the `.toString` method to generate a checksum. ::: ### 服务定位器 {#service-locator} ¥Service Locator 当你将带有状态/装饰器的插件应用于实例时,该实例将获得类型安全。 ¥When you apply a plugin with state/decorators to an instance, the instance will gain type safety. 但如果你不将插件应用到另一个实例,它将无法推断类型。 ¥But if you don't apply the plugin to another instance, it will not be able to infer the type. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const child = new Elysia() // ❌ 'a' is missing .get('/', ({ a }) => a) const main = new Elysia() .decorate('a', 'a') .use(child) ``` Elysia 引入了服务定位器模式来解决这个问题。 ¥Elysia introduces the **Service Locator** pattern to counteract this. Elysia 将查找插件校验和并获取其值或注册一个新值。从插件推断类型。 ¥Elysia will lookup the plugin checksum and get the value or register a new one. Infer the type from the plugin. 因此,我们必须提供插件参考,以便 Elysia 找到合适的服务来增加类型安全性。 ¥So we have to provide the plugin reference for Elysia to find the service to add type safety. ```typescript twoslash // @errors: 2339 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) // With `setup`, type will be inferred const child = new Elysia() .use(setup) // [!code ++] .get('/', ({ a }) => a) // ^? const main = new Elysia() .use(child) ``` ## 守护 {#guard} ¥Guard Guard 允许我们将钩子和方案一次性应用于多个路由。 ¥Guard allows us to apply hook and schema into multiple routes all at once. ```typescript twoslash const signUp = (a: T) => a const signIn = (a: T) => a const isUserExists = (a: T) => a // ---cut--- import { Elysia, t } from 'elysia' new Elysia() .guard( { // [!code ++] body: t.Object({ // [!code ++] username: t.String(), // [!code ++] password: t.String() // [!code ++] }) // [!code ++] }, // [!code ++] (app) => // [!code ++] app .post('/sign-up', ({ body }) => signUp(body)) .post('/sign-in', ({ body }) => signIn(body), { // ^? beforeHandle: isUserExists }) ) .get('/', 'hi') .listen(3000) ``` 此代码将 `body` 的验证应用于 '/sign-in' 和 '/sign-up',而不是逐一内联模式,但不应用于 '/'。 ¥This code applies validation for `body` to both '/sign-in' and '/sign-up' instead of inlining the schema one by one but applies not to '/'. 我们可以将路由验证总结如下: ¥We can summarize the route validation as the following: | 路径 | 已验证 | | -------- | --- | | /sign-up | ✅ | | /sign-in | ✅ | | / | ❌ | Guard 接受与内联钩子相同的参数,唯一的区别在于你可以将钩子应用于作用域内的多个路由。 ¥Guard accepts the same parameter as inline hook, the only difference is that you can apply hook to multiple routes in the scope. 这意味着上面的代码被翻译成: ¥This means that the code above is translated into: ```typescript twoslash const signUp = (a: T) => a const signIn = (a: T) => a const isUserExists = (a: any) => a // ---cut--- 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) ``` ### 分组守护 {#grouped-guard} ¥Grouped Guard 我们可以通过为组提供 3 个参数来使用带前缀的组。 ¥We can use a group with prefixes by providing 3 parameters to the group. 1. 前缀 - 路由前缀 2. 守护 - Schema 3. 范围 - Elysia 应用回调 使用与 guard 相同的 API,将第二个参数应用于该参数,而不是将 group 和 guard 嵌套在一起。 ¥With the same API as guard apply to the 2nd parameter, instead of nesting group and guard together. 请考虑以下示例: ¥Consider the following example: ```typescript twoslash 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) ``` 从嵌套的 groupped 守卫中,我们可以通过为组的第二个参数提供守卫作用域来将组和守卫合并在一起: ¥From nested groupped guard, we may merge group and guard together by providing guard scope to 2nd parameter of group: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/v1', (app) => app.guard( // [!code --] { body: t.Literal('Rikuhachima Aru') }, (app) => app.post('/student', ({ body }) => body) ) // [!code --] ) .listen(3000) ``` 其语法如下: ¥Which results in the follows syntax: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/v1', { body: t.Literal('Rikuhachima Aru') }, (app) => app.post('/student', ({ body }) => body) // ^? ) .listen(3000) ``` ## 范围 {#scope} ¥Scope 默认情况下,钩子和模式仅适用于当前实例。 ¥By default, hook and schema will apply to **current instance only**. Elysia 具有封装范围,以防止意外的副作用。 ¥Elysia has an encapsulation scope for to prevent unintentional side effects. 作用域类型用于指定钩子的作用域,是封装的还是全局的。 ¥Scope type is to specify the scope of hook whether is should be encapsulated or global. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const plugin = new Elysia() .derive(() => { return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ⚠️ Hi is missing .get('/parent', ({ hi }) => hi) ``` 从上面的代码中,我们可以看到父实例中缺少 `hi`,因为如果未指定作用域,则默认为本地作用域,并且不会应用于父实例。 ¥From the above code, we can see that `hi` is missing from the parent instance because the scope is local by default if not specified, and will not apply to parent. 要将钩子应用到父实例,我们可以使用 `as` 来指定钩子的作用域。 ¥To apply the hook to the parent instance, we can use the `as` to specify scope of the hook. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const plugin = new Elysia() .derive({ as: 'scoped' }, () => { // [!code ++] return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` ### 范围级别 {#scope-level} ¥Scope level Elysia 有以下 3 个级别的作用域:作用域类型如下: ¥Elysia has 3 levels of scope as the following: Scope type are as the following: * 本地(默认) - 仅应用于当前实例和后代 * scoped - 应用于父级、当前实例和后代 * global - 应用于所有应用该插件的实例(所有父级、当前实例和后代) 让我们通过以下示例回顾一下每种作用域类型的功能: ¥Let's review what each scope type does by using the following example: ```typescript import { Elysia } from 'elysia' // ? Value base on table value provided below const type = 'local' const child = new Elysia() .get('/child', 'hi') const current = new Elysia() .onBeforeHandle({ as: type }, () => { // [!code ++] 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` 值,结果应如下所示: ¥By changing the `type` value, the result should be as follows: | type | child | current | parent | main | | -------- | ----- | ------- | ------ | ---- | | 'local' | ✅ | ✅ | ❌ | ❌ | | 'scoped' | ✅ | ✅ | ✅ | ❌ | | 'global' | ✅ | ✅ | ✅ | ✅ | ### 范围转换 {#scope-cast} ¥Scope cast 要将钩子应用到父级,可以使用以下方法之一: ¥To apply hook to parent may use one of the following: 1. `inline as` 仅适用于单个钩子 2. `guard as` 适用于某个守卫中的所有钩子 3. `instance as` 适用于某个实例中的所有钩子 ### 1. 内联为 {#inline-as} ¥ Inline as 每个事件监听器都会接受 `as` 参数来指定钩子的作用域。 ¥Every event listener will accept `as` parameter to specify the scope of the hook. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia() .derive({ as: 'scoped' }, () => { // [!code ++] return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` 然而,此方法仅适用于单个钩子,可能不适用于多个钩子。 ¥However, this method is apply to only a single hook, and may not be suitable for multiple hooks. ### 2. 守护为 {#guard-as} ¥ Guard as 每个事件监听器都会接受 `as` 参数来指定钩子的作用域。 ¥Every event listener will accept `as` parameter to specify the scope of the hook. ```typescript import { Elysia, t } from 'elysia' const plugin = new Elysia() .guard({ as: 'scoped', // [!code ++] response: t.String(), beforeHandle() { console.log('ok') } }) .get('/child', 'ok') const main = new Elysia() .use(plugin) .get('/parent', 'hello') ``` Guard 允许我们在指定作用域的情况下,一次性将 `schema` 和 `hook` 应用于多个路由。 ¥Guard alllowing us to apply `schema` and `hook` to multiple routes all at once while specifying the scope. 但是,它不支持 `derive` 和 `resolve` 方法。 ¥However, it doesn't support `derive` and `resolve` method. ### 3. 实例为 {#instance-as} ¥ Instance as `as` 将读取当前实例的所有钩子和架构范围,并进行修改。 ¥`as` will read all hooks and schema scope of the current instance, modify. ```typescript twoslash import { Elysia } from 'elysia' const plugin = new Elysia() .derive(() => { return { hi: 'ok' } }) .get('/child', ({ hi }) => hi) .as('scoped') // [!code ++] const main = new Elysia() .use(plugin) // ✅ Hi is now available .get('/parent', ({ hi }) => hi) ``` 有时我们也想将插件重新应用于父实例,但由于受 `scoped` 机制的限制,它只能应用于一个父实例。 ¥Sometimes we want to reapply plugin to parent instance as well but as it's limited by `scoped` mechanism, it's limited to 1 parent only. 要应用于父实例,我们需要将作用域提升到父实例,而 `as` 是实现此目的的完美方法。 ¥To apply to the parent instance, we need to **lift the scope up** to the parent instance, and `as` is the perfect method to do so. 这意味着如果你拥有 `local` 作用域,并希望将其应用于父实例,则可以使用 `as('scoped')` 来提升它。 ¥Which means if you have `local` scope, and want to apply it to the parent instance, you can use `as('scoped')` to lift it up. ```typescript twoslash // @errors: 2304 2345 import { Elysia, t } from 'elysia' const plugin = new Elysia() .guard({ response: t.String() }) .onBeforeHandle(() => { console.log('called') }) .get('/ok', () => 'ok') .get('/not-ok', () => 1) .as('scoped') // [!code ++] const instance = new Elysia() .use(plugin) .get('/no-ok-parent', () => 2) .as('scoped') // [!code ++] const parent = new Elysia() .use(instance) // This now error because `scoped` is lifted up to parent .get('/ok', () => 3) ``` ### 后代 {#descendant} ¥Descendant 默认情况下,插件仅将钩子应用于自身及其后代。 ¥By default plugin will **apply hook to itself and descendants** only. 如果钩子在插件中注册,继承该插件的实例将不会继承钩子和模式。 ¥If the hook is registered in a plugin, instances that inherit the plugin will **NOT** inherit hooks and schema. ```typescript 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') ``` 要将钩子应用于全局,我们需要将钩子指定为全局。 ¥To apply hook to globally, we need to specify hook as global. ```typescript 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') ``` ## 延迟加载 {#lazy-load} ¥Lazy Load 默认情况下,模块会主动加载。 ¥Modules are eagerly loaded by default. Elysia 会加载所有模块,然后在启动服务器之前注册并索引所有模块。这强制要求所有模块在开始接受请求之前都已加载。 ¥Elysia loads all modules then registers and indexes all of them before starting the server. This enforces that all the modules have loaded before it starts accepting requests. 虽然这对于大多数应用来说没有问题,但对于运行在无服务器环境或边缘函数中的服务器来说,这可能会成为瓶颈,因为在这些环境中启动时间非常重要。 ¥While this is fine for most applications, it may become a bottleneck for a server running in a serverless environment or an edge function, in which the startup time is important. 延迟加载可以通过在服务器启动后逐步索引模块来减少启动时间。 ¥Lazy-loading can help decrease startup time by deferring modules to be gradually indexed after the server start. 当某些模块很重且导入启动时间至关重要时,延迟加载模块是一个不错的选择。 ¥Lazy-loading modules are a good option when some modules are heavy and importing startup time is crucial. 默认情况下,任何未使用 await 的异步插件都被视为延迟模块,导入语句被视为延迟加载模块。 ¥By default, any async plugin without await is treated as a deferred module and the import statement as a lazy-loading module. 两者都将在服务器启动后注册。 ¥Both will be registered after the server is started. ### 延迟加载模块 {#deferred-module} ¥Deferred Module deferred 模块是一个异步插件,可以在服务器启动后注册。 ¥The deferred module is an async plugin that can be registered after the server is started. ```typescript // 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 } ``` 在主文件中: ¥And in the main file: ```typescript import { Elysia } from 'elysia' import { loadStatic } from './plugin' const app = new Elysia() .use(loadStatic) ``` Elysia 静态插件也是一个延迟模块,因为它异步加载文件并注册文件路径。 ¥Elysia static plugin is also a deferred module, as it loads files and registers files path asynchronously. ### 延迟加载模块 {#lazy-load-module} ¥Lazy Load Module 与异步插件相同,延迟加载模块将在服务器启动后注册。 ¥Same as the async plugin, the lazy-load module will be registered after the server is started. 延迟加载模块可以是同步函数或异步函数,只要该模块与 `import` 一起使用,就会被延迟加载。 ¥A lazy-load module can be both sync or async function, as long as the module is used with `import` the module will be lazy-loaded. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .use(import('./plugin')) ``` 当模块计算量大且/或阻塞时,建议使用模块延迟加载。 ¥Using module lazy-loading is recommended when the module is computationally heavy and/or blocking. 为了确保模块在服务器启动前注册,我们可以在延迟模块上使用 `await`。 ¥To ensure module registration before the server starts, we can use `await` on the deferred module. ### 测试 {#testing} ¥Testing 在测试环境中,我们可以使用 `await app.modules` 等待延迟加载和懒加载模块。 ¥In a test environment, we can use `await app.modules` to wait for deferred and lazy-loading modules. ```typescript 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') }) }) ``` --- --- url: 'https://elysiajs.com/plugins/overview.md' --- # 概述 {#overview} ¥Overview Elysia 采用模块化和轻量级设计。 ¥Elysia is designed to be modular and lightweight. 遵循与 Arch Linux 相同的理念(顺便说一句,我使用的是 Arch): ¥Following the same idea as Arch Linux (btw, I use Arch): > 设计决策是根据开发者的共识逐一做出的。 这是为了确保开发者最终能够获得他们想要创建的高性能 Web 服务器。此外,Elysia 还包含预构建的通用模式插件,方便开发者使用: ¥This is to ensure developers end up with a performant web server they intend to create. By extension, Elysia includes pre-built common pattern plugins for convenient developer usage: ## 官方插件: {#official-plugins} ¥Official plugins: * [承载器](/plugins/bearer) - 自动检索 [承载器](https://swagger.io/docs/specification/authentication/bearer-authentication/) 令牌 * [CORS](/plugins/cors) - 设置 [跨域资源共享 (CORS)](https://web.nodejs.cn/en-US/docs/Web/HTTP/CORS) * [Cron](/plugins/cron) - 设置 [cron](https://en.wikipedia.org/wiki/Cron) 任务 * [Eden](/eden/overview) - Elysia 的端到端类型安全客户端 * [GraphQL Apollo](/plugins/graphql-apollo) - 在 Elysia 上运行 [Apollo GraphQL](https://www.apollographql.com/) * [GraphQL Yoga](/plugins/graphql-yoga) - 在 Elysia 上运行 [GraphQL Yoga](https://github.com/dotansimha/graphql-yoga) * [HTML](/plugins/html) - 处理 HTML 响应 * [JWT](/plugins/jwt) - 使用 [JWTs](https://jwt.io/) 进行身份验证 * [OpenAPI](/plugins/openapi) - 生成 [OpenAPI](https://swagger.io/specification/) 文档 * [OpenTelemetry](/plugins/opentelemetry) - 添加对 OpenTelemetry 的支持 * [服务器计时](/plugins/server-timing) - 使用 [服务器计时 API](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/Server-Timing) 审计性能瓶颈 * [静态](/plugins/static) - 提供静态文件/文件夹 * [Stream](/plugins/stream) - 集成响应流和 [服务器发送的事件(上证所)](https://web.nodejs.cn/en-US/docs/Web/API/Server-sent_events) * [WebSocket](/patterns/websocket) - 支持 [WebSockets](https://web.nodejs.cn/en-US/docs/Web/API/WebSocket) ## 社区插件: {#community-plugins} ¥Community plugins: * [创建 ElysiaJS](https://github.com/kravetsone/create-elysiajs) - 轻松构建 Elysia 项目与环境(支持 ORM、Linter 和插件)! * [Lucia 身份验证](https://github.com/pilcrowOnPaper/lucia) - 身份验证,简洁明了 * [Elysia Clerk](https://github.com/wobsoriano/elysia-clerk) - 非官方 Clerk 身份验证插件 * [Elysia Polyfills](https://github.com/bogeychan/elysia-polyfills) - 在 Node.js 和 Deno 上运行 Elysia 生态系统 * [Vite 服务器](https://github.com/kravetsone/elysia-vite-server) - 此插件在 `development` 模式下启动并装饰 [`vite`](https://vitejs.dev/) 开发服务器,并在 `production` 模式下提供静态文件(如果需要) * [Vite](https://github.com/timnghg/elysia-vite) - 服务入口 HTML 文件,并注入 Vite 脚本 * [Nuxt](https://github.com/trylovetom/elysiajs-nuxt) - 轻松将 Elysia 与 Nuxt! 集成 * [Remix](https://github.com/kravetsone/elysia-remix) - 使用 [Remix](https://remix.nodejs.cn/) 并支持 `HMR`(由 [`vite`](https://vitejs.dev/) 提供支持)!关闭一个长期存在的插件请求 [#12](https://github.com/elysiajs/elysia/issues/12) * [同步](https://github.com/johnny-woodtke/elysiajs-sync) - 一个由 [Dexie.js](https://dexie.org/) 驱动的轻量级离线优先数据同步框架 * [连接中间件](https://github.com/kravetsone/elysia-connect-middleware) - 此插件允许你直接在 Elysia 中使用 [`express`](https://www.npmjs.com/package/express)/[`connect`](https://www.npmjs.com/package/connect) 中间件! * [Elysia HTTP 异常](https://github.com/codev911/elysia-http-exception) - Elysia 插件支持 HTTP 4xx/5xx 错误处理,并带有结构化异常类 * [Elysia Helmet](https://github.com/DevTobias/elysia-helmet) - 使用各种 HTTP 标头保护 Elysia 应用 * [Vite 插件 SSR](https://github.com/timnghg/elysia-vite-plugin-ssr) - 使用 Elysia 服务器的 Vite SSR 插件 * [OAuth 2.0](https://github.com/kravetsone/elysia-oauth2) - 一个用于 [OAuth 2.0](https://en.wikipedia.org/wiki/OAuth) 授权流程的插件,支持超过 42 个提供程序,并且类型安全! * [OAuth2](https://github.com/bogeychan/elysia-oauth2) - 处理 OAuth 2.0 授权码流程 * [OAuth2 资源服务器](https://github.com/ap-1/elysia-oauth2-resource-server) - 一个用于验证来自 OAuth2 提供程序的 JWT 令牌与 JWKS 端点的插件,支持颁发者、受众和范围验证。 * [Elysia OpenID 客户端](https://github.com/macropygia/elysia-openid-client) - 基于 [openid-client](https://github.com/panva/node-openid-client) 的 OpenID 客户端 * [速率限制](https://github.com/rayriffy/elysia-rate-limit) - 简单、轻量级的速率限制器 * [Logysia](https://github.com/tristanisham/logysia) - 经典日志中间件 * [Logestic](https://github.com/cybercoder-naj/logestic) - 一个高级且可自定义的 ElysiaJS 日志库 * [记录器](https://github.com/bogeychan/elysia-logger) - 基于 [pino](https://github.com/pinojs/pino) 的日志中间件 * [Elylog](https://github.com/eajr/elylog) - 具有一定自定义功能的简单 stdout 日志库 * [Elysia.js 的 Logify](https://github.com/0xrasla/logify) - 一个美观、快速且类型安全的 Elysia.js 应用日志中间件 * [Nice 记录器](https://github.com/tanishqmanuja/nice-logger) - 虽然不是最美观的,但对于 Elysia 来说,这是一个相当不错且实用的日志记录器。 * [Sentry](https://github.com/johnny-woodtke/elysiajs-sentry) - 使用此 [Sentry](https://docs.sentry.io/) 插件捕获跟踪和错误 * [Elysia Lambda 表达式](https://github.com/TotalTechGeek/elysia-lambda) - 在 AWS Lambda 上部署 * [装饰器](https://github.com/gaurishhs/elysia-decorators) - 使用 TypeScript 装饰器 * [自动加载](https://github.com/kravetsone/elysia-autoload) - 基于目录结构的文件系统路由,可生成支持 [`Bun.build`](https://github.com/kravetsone/elysia-autoload?tab=readme-ov-file#bun-build-usage) 的 [Eden](https://elysia.nodejs.cn/eden/overview.html) 类型 * [Msgpack](https://github.com/kravetsone/elysia-msgpack) - 允许你使用 [MessagePack](https://msgpack.org) * [XML](https://github.com/kravetsone/elysia-xml) - 允许你使用 XML * [自动路由](https://github.com/wobsoriano/elysia-autoroutes) - 文件系统路由 * [群组路由](https://github.com/itsyoboieltr/elysia-group-router) - 基于文件系统和文件夹的群组路由 * [基本身份验证](https://github.com/itsyoboieltr/elysia-basic-auth) - 基本 HTTP 身份验证 * [ETag](https://github.com/bogeychan/elysia-etag) - 自动生成 HTTP [ETag](https://web.nodejs.cn/en-US/docs/Web/HTTP/Headers/ETag) * [CDN 缓存](https://github.com/johnny-woodtke/elysiajs-cdn-cache) - Elysia 的 Cache-Control 插件 - 无需再手动设置 HTTP 标头 * [基本身份验证](https://github.com/eelkevdbos/elysia-basic-auth) - 基本 HTTP 身份验证(使用 `request` 事件) * [i18n](https://github.com/eelkevdbos/elysia-i18next) - 基于 [i18next](https://www.i18next.com/) 的 [i18n](https://web.nodejs.cn/en-US/docs/Mozilla/Add-ons/WebExtensions/API/i18n) 封装器 * [Elysia 请求 ID](https://github.com/gtramontina/elysia-requestid) - 添加/转发请求 ID(`X-Request-ID` 或自定义) * [Elysia HTMX](https://github.com/gtramontina/elysia-htmx) - [HTMX](https://htmx.nodejs.cn/) 的上下文助手 * [Elysia HMR HTML](https://github.com/gtrabanco/elysia-hmr-html) - 修改目录中任何文件时重新加载 HTML 文件 * [Elysia 注入 HTML](https://github.com/gtrabanco/elysia-inject-html) - 在 HTML 文件中注入 HTML 代码 * [Elysia HTTP 错误](https://github.com/yfrans/elysia-http-error) - 从 Elysia 处理程序返回 HTTP 错误 * [Elysia Http 状态码](https://github.com/sylvain12/elysia-http-status-code) - 集成 HTTP 状态码 * [NoCache](https://github.com/gaurishhs/elysia-nocache) - 禁用缓存 * [Elysia Tailwind](https://github.com/gtramontina/elysia-tailwind) - 在插件中编译 [Tailwindcss](https://tailwind.nodejs.cn/)。 * [Elysia Compression](https://github.com/gusb3ll/elysia-compression) - 压缩响应 * [Elysia IP 地址](https://github.com/gaurishhs/elysia-ip) - 获取 IP 地址 * [OAuth2 服务器](https://github.com/myazarc/elysia-oauth2-server) - 使用 Elysia 开发 OAuth2 服务器 * [Elysia Flash 消息](https://github.com/gtramontina/elysia-flash-messages) - 启用闪退消息 * [Elysia AuthKit](https://github.com/gtramontina/elysia-authkit) - 非官方 [WorkOS 的 AuthKit](https://www.authkit.com/) 身份验证 * [Elysia 错误处理程序](https://github.com/gtramontina/elysia-error-handler) - 更简单的错误处理 * [Elysia 环境](https://github.com/yolk-oss/elysia-env) - 使用 typebox 的类型安全环境变量 * [Elysia Drizzle Schema](https://github.com/Edsol/elysia-drizzle-schema) - 有助于在 Elysia OpenAPI 模型中使用 Drizzle ORM schema。 * [Unify-Elysia](https://github.com/qlaffont/unify-elysia) - 统一 Elysia 的错误代码 * [Unify-Elysia-GQL](https://github.com/qlaffont/unify-elysia-gql) - 统一 Elysia GraphQL 服务器(Yoga 和 Apollo)的错误代码 * [Elysia Auth Drizzle](https://github.com/qlaffont/elysia-auth-drizzle) - 使用 JWT(Header/Cookie/QueryParam)处理身份验证的库。 * [graceful-server-elysia](https://github.com/qlaffont/graceful-server-elysia) - 受 [graceful-server](https://github.com/gquittet/graceful-server) 启发的库。 * [Logixlysia](https://github.com/PunGrumpy/logixlysia) - 一个美观简洁的 ElysiaJS 日志中间件,带有颜色和时间戳功能。 * [Elysia 故障](https://github.com/vitorpldev/elysia-fault) - 一个简单且可自定义的错误处理中间件,可以创建你自己的 HTTP 错误。 * [Elysia Compress](https://github.com/vermaysha/elysia-compress) - 受 [@fastify/compress](https://github.com/fastify/fastify-compress) 启发的 ElysiaJS 响应压缩插件 * [@labzzhq/compressor](https://github.com/labzzhq/compressor/) - 精简的辉煌,扩展的结果:适用于 Elysia 和 Bunnyhop 的 HTTP 压缩器,支持 gzip、deflate 和 brotli。 * [Elysia 接受](https://github.com/morigs/elysia-accepts) - Elysia 插件支持 accept 标头解析和内容协商 * [Elysia Compression](https://github.com/chneau/elysia-compression) - Elysia 插件支持响应压缩 * [Elysia 日志记录器](https://github.com/chneau/elysia-logger) - Elysia 插件支持记录 HTTP 请求和响应,灵感源自 [hono/logger](https://hono.nodejs.cn/docs/middleware/builtin/logger) * [Elysia CQRS](https://github.com/jassix/elysia-cqrs) - Elysia 插件支持 CQRS 模式 * [Elysia Supabase](https://github.com/mastermakrela/elysia-supabase) - 将 [Supabase](https://supabase.com/) 身份验证和数据库功能无缝集成到 Elysia,允许轻松访问经过身份验证的用户数据和 Supabase 客户端实例。对 [边缘函数](https://supabase.com/docs/guides/functions) 特别有用。 * [Elysia XSS](https://www.npmjs.com/package/elysia-xss) - 一个 Elysia.js 插件,通过清理请求体数据来提供 XSS(跨站脚本)保护。 * [Elysiajs Helmet](https://www.npmjs.com/package/elysiajs-helmet) - 一个全面的 Elysia.js 应用安全中间件,可通过设置各种 HTTP 标头来保护你的应用安全。 * [Elysia.js 装饰器](https://github.com/Ateeb-Khan-97/better-elysia) - 使用这个小型库无缝开发和集成 API、Websocket 和 Streaming API。 * [Elysia Protobuf](https://github.com/ilyhalight/elysia-protobuf) - Elysia 支持 protobuf。 * [Elysia Prometheus](https://github.com/m1handr/elysia-prometheus) - Elysia 插件支持为 Prometheus 公开 HTTP 指标。 * [Elysia 远程数据转换服务 (DTS)](https://github.com/rayriffy/elysia-remote-dts) - 一个插件,为 Eden Treaty 远程提供 .d.ts 类型以供使用。 * [Cap Checkpoint 插件 Elysia](https://capjs.js.org/guide/middleware/elysia.html) - 类似 Cloudflare 的 Cap 中间件,一款轻量级、现代的开源 CAPTCHA 替代方案,采用 SHA-256 PoW 设计。 * [Elysia 背景](https://github.com/staciax/elysia-background) - Elysia.js 的后台任务处理插件 * [@fedify/elysia](https://github.com/fedify-dev/fedify/tree/main/packages/elysia) - 一个与 ActivityPub 服务器框架 [Fedify](https://fedify.dev/) 无缝集成的插件。 ## 补充项目: {#complementary-projects} ¥Complementary projects: * [prismabox](https://github.com/m1212e/prismabox) - 基于数据库模型的 TypeBox 方案生成器,与 Elysia 完美兼容 *** 如果你有为 Elysia 编写的插件,请点击下方的在 GitHub 上编辑此页面,将你的插件添加到列表中。 👇 ¥If you have a plugin written for Elysia, feel free to add your plugin to the list by **clicking Edit this page on GitHub** below 👇 --- --- url: 'https://elysiajs.com/tutorial.md' --- # Elysia 教程 {#elysia-tutorial} ¥Elysia Tutorial 我们将构建一个小型的 CRUD 注意 API 服务器。 ¥We will be building a small CRUD note-taking API server. 没有数据库或其他 "生产就绪" 功能。本教程将仅关注 Elysia 功能以及如何使用 Elysia。 ¥There's no database or other "production ready" features. This tutorial is going to only focus on Elysia feature and how to use Elysia only. 如果你继续操作,预计大约需要 15-20 分钟。 ¥We expect it to take around 15-20 minutes if you follow along. *** ### 来自其他框架? {#from-other-framework} ¥From other framework? 如果你使用过其他流行的框架,例如 Express、Fastify 或 Hono,你会发现 Elysia 非常容易上手,只有一些区别。 ¥If you have used other popular frameworks like Express, Fastify, or Hono, you will find Elysia right at home with just a few differences. ### 不喜欢本教程? {#not-a-fan-of-tutorial} ¥Not a fan of tutorial? 如果你更喜欢自己动手尝试,可以跳过本教程,直接转到 [关键概念](/key-concept) 页面,以更好地了解 Elysia 的工作原理。 ¥If you prefer a more try-it-yourself approach, you can skip this tutorial and go straight to the [key concept](/key-concept) page to get a good understanding of how Elysia works. ### llms.txt {#llmstxt} 或者,你可以下载 llms.txt 或 llms-full.txt,并将其提供给你喜欢的 LLM,例如 ChatGPT、Claude 或 Gemini,以获得更具交互性的体验。 ¥Alternatively, you can download llms.txt or llms-full.txt and feed it to your favorite LLMs like ChatGPT, Claude or Gemini to get a more interactive experience. ## 设置 {#setup} ¥Setup Elysia 设计运行于 [Bun](https://bun.sh)(Node.js 的替代运行时),但它也可以运行于 Node.js 或任何支持 Web 标准 API 的运行时。 ¥Elysia is designed to run on [Bun](https://bun.sh), an alternative runtime to Node.js but it can also run on Node.js or any runtime that support Web Standard API. 但是,在本教程中,我们将使用 Bun。 ¥However, in this tutorial we will be using Bun. 如果尚未安装 Bun,请安装。 ¥Install Bun if you haven't already. ::: code-group ```bash [MacOS/Linux] curl -fsSL https://bun.sh/install | bash ``` ```bash [Windows] powershell -c "irm bun.sh/install.ps1 | iex" ``` ::: ### 创建新项目 {#create-a-new-project} ¥Create a new project ```bash # Create a new project bun create elysia hi-elysia # cd into the project cd hi-elysia # Install dependencies bun install ``` 这将创建一个包含 Elysia 和基本 TypeScript 配置的准系统项目。 ¥This will create a barebone project with Elysia and basic TypeScript config. ### 启动开发服务器 {#start-the-development-server} ¥Start the development server ```bash bun dev ``` 打开浏览器并访问 ,你应该会在屏幕上看到“Hello Elysia”消息。 ¥Open your browser and go to ****, you should see **Hello Elysia** message on the screen. Elysia 使用带有 `--watch` 标志的 Bun,以便在进行更改时自动重新加载服务器。 ¥Elysia use Bun with `--watch` flag to automatically reload the server when you make changes. ## 路由 {#route} ¥Route 要添加新路由,我们需要指定一个 HTTP 方法、一个路径名和一个值。 ¥To add a new route, we specify an HTTP method, a pathname, and a value. 让我们首先按如下方式打开 `src/index.ts` 文件: ¥Let's start by opening the `src/index.ts` file as follows: ```typescript [index.ts] import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .get('/hello', 'Do you miss me?') // [!code ++] .listen(3000) ``` 打开 ,你应该会看到“Do you miss me?”。 ¥Open ****, you should see **Do you miss me?**. 我们可以使用多种 HTTP 方法,但在本教程中我们将使用以下方法: ¥There are several HTTP methods we can use, but we will use the following for this tutorial: * get * post * put * patch * delete 还有其他方法可用,请使用与 `get` 相同的语法 ¥Other methods are available, use the same syntax as `get` ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') .get('/hello', 'Do you miss me?') // [!code --] .post('/hello', 'Do you miss me?') // [!code ++] .listen(3000) ``` Elysia 接受值和函数作为响应。 ¥Elysia accepts both value and function as a response. 但是,我们可以使用函数访问 `Context`(路由和实例信息)。 ¥However, we can use function to access `Context` (route and instance information). ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', () => 'Hello Elysia') // [!code --] .get('/', ({ path }) => path) // [!code ++] .post('/hello', 'Do you miss me?') .listen(3000) ``` ## OpenAPI {#openapi} 在浏览器中输入 URL 只能与 GET 方法交互。要与其他方法交互,我们需要一个 REST 客户端,例如 Postman 或 Insomnia。 ¥Entering a URL to the browser can only interact with the GET method. To interact with other methods, we need a REST Client like Postman or Insomnia. 幸运的是,Elysia 自带了一个带有 [Scalar](https://scalar.com) 的 OpenAPI Schema,可以与我们的 API 交互。 ¥Luckily, Elysia comes with a **OpenAPI Schema** with [Scalar](https://scalar.com) to interact with our API. ```bash # Install the OpenAPI plugin bun add @elysiajs/openapi ``` 然后将插件应用到 Elysia 实例。 ¥Then apply the plugin to the Elysia instance. ```typescript import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' const app = new Elysia() // Apply the openapi plugin .use(openapi()) // [!code ++] .get('/', ({ path }) => path) .post('/hello', 'Do you miss me?') .listen(3000) ``` 导航到 ,你应该会看到如下文档: ¥Navigate to ****, you should see the documentation like this: ![Scalar Documentation landing](/tutorial/scalar-landing.webp) 现在,我们可以与我们创建的所有路由进行交互。 ¥Now we can interact with all the routes we have created. 滚动到 /hello 并点击蓝色的测试请求按钮以显示表单。 ¥Scroll to **/hello** and click a blue **Test Request** button to show the form. 点击黑色的“发送”按钮即可查看结果。 ¥We can see the result by clicking the black **Send** button. ![Scalar Documentation landing](/tutorial/scalar-request.webp) ## 装饰 {#decorate} ¥Decorate 但是,对于更复杂的数据,我们可能希望使用类来处理复杂数据,因为它允许我们定义自定义方法和属性。 ¥However, for more complex data we may want to use class for complex data as it allows us to define custom methods and properties. 现在,让我们创建一个单例类来存储我们的注意。 ¥Now, let's create a singleton class to store our notes. ```typescript import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' class Note { // [!code ++] constructor(public data: string[] = ['Moonhalo']) {} // [!code ++] } // [!code ++] const app = new Elysia() .use(openapi()) .decorate('note', new Note()) // [!code ++] .get('/note', ({ note }) => note.data) // [!code ++] .listen(3000) ``` `decorate` 允许我们将单例类注入 Elysia 实例,以便我们在路由处理程序中访问它。 ¥`decorate` allows us to inject a singleton class into the Elysia instance, allowing us to access it in the route handler. 打开 ,我们应该会在屏幕上看到 \["月晕"]。 ¥Open ****, we should see **\["Moonhalo"]** on the screen. 有关 Scalar 文档,我们可能需要刷新页面才能看到新的更改。 ¥For Scalar documentation, we may need to reload the page to see the new changes. ![Scalar Documentation landing](/tutorial/scalar-moonhalo.webp) ## 路径参数 {#path-parameter} ¥Path parameter 现在,让我们通过索引检索一条注意。 ¥Now let's retrieve a note by its index. 我们可以通过在路径参数前添加冒号来定义路径参数。 ¥We can define a path parameter by prefixing it with a colon. ```typescript twoslash // @errors: 7015 import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(openapi()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get('/note/:index', ({ note, params: { index } }) => { // [!code ++] return note.data[index] // [!code ++] }) // [!code ++] .listen(3000) ``` 让我们暂时忽略错误。 ¥Let's ignore the error for now. 打开 ,我们应该会在屏幕上看到 Moonhalo。 ¥Open ****, we should see **Moonhalo** on the screen. path 参数允许我们从 URL 中检索特定部分。在我们的例子中,我们从 /note/0 检索一个 "0" 并将其放入名为 index 的变量中。 ¥The path parameter allows us to retrieve a specific part from the URL. In our case, we retrieve a **"0"** from **/note/0** put into a variable named **index**. ## 验证 {#validation} ¥Validation 上述错误是一个警告,提示路径参数可以是任意字符串,而数组索引应为数字。 ¥The error above is a warning that the path parameter can be any string, while an array index should be a number. 例如,/note/0 有效,但 /note/zero 无效。 ¥For example, **/note/0** is valid, but **/note/zero** is not. 我们可以通过声明一个模式来强制执行和验证类型: ¥We can enforce and validate type by declaring a schema: ```typescript import { Elysia, t } from 'elysia' // [!code ++] import { openapi } from '@elysiajs/openapi' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(openapi()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index } }) => { return note.data[index] }, { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] } // [!code ++] ) .listen(3000) ``` 我们从 Elysia 导入 t 来定义路径参数的模式。 ¥We import **t** from Elysia to define a schema for the path parameter. 现在,如果我们尝试访问 ,我们应该会看到一条错误消息。 ¥Now, if we try to access ****, we should see an error message. 此代码解决了我们之前由于 TypeScript 警告而看到的错误。 ¥This code resolves the error we saw earlier because of a **TypeScript warning**. Elysia 模式不仅在运行时强制执行验证,还会推断 TypeScript 类型以进行自动补齐和提前检查错误,并提供 Scalar 文档。 ¥Elysia schema doesn't only enforce validation on the runtime, but it also infers a TypeScript type for auto-completion and checking error ahead of time, and a Scalar documentation. 大多数框架只提供其中一项功能,或者单独提供,需要我们分别更新每个功能,但 Elysia 将所有功能都作为单一事实来源提供。 ¥Most frameworks provide only one of these features or provide them separately requiring us to update each one separately, but Elysia provides all of them as a **Single Source of Truth**. ### 验证类型 {#validation-type} ¥Validation type Elysia 提供以下属性的验证: ¥Elysia provides validation for the following properties: * params - 路径参数 * query - URL 查询字符串 * body - 请求正文 * headers - 请求头 * cookie - cookie * response - 响应正文 它们都与上面的示例共享相同的语法。 ¥All of them share the same syntax as the example above. ## 状态码 {#status-code} ¥Status code 默认情况下,Elysia 将为所有路由返回状态码 200,即使响应为错误。 ¥By default, Elysia will return a status code of 200 for all routes even if the response is an error. 例如,如果我们尝试访问 ,我们应该在屏幕上看到 undefined,而这不应该是 200 状态码(OK)。 ¥For example, if we try to access ****, we should see **undefined** on the screen which shouldn't be a 200 status code (OK). 我们可以通过返回错误来更改状态码。 ¥We can change the status code by returning an error ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(openapi()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { // [!code ++] return note.data[index] ?? status(404) // [!code ++] }, { params: t.Object({ index: t.Number() }) } ) .listen(3000) ``` 现在,如果我们尝试访问 ,我们应该会在屏幕上看到“未找到”消息,状态码为 404。 ¥Now, if we try to access ****, we should see **Not Found** on the screen with a status code of 404. 我们还可以通过将字符串传递给错误函数来返回自定义消息。 ¥We can also return a custom message by passing a string to the error function. ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' class Note { constructor(public data: string[] = ['Moonhalo']) {} } const app = new Elysia() .use(openapi()) .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'oh no :(') // [!code ++] }, { params: t.Object({ index: t.Number() }) } ) .listen(3000) ``` ## 插件 {#plugin} ¥Plugin 主实例开始变得拥挤,我们可以将路由处理程序移至单独的文件并将其作为插件导入。 ¥The main instance is starting to get crowded, we can move the route handler to a separate file and import it as a plugin. 创建一个名为 note.ts 的新文件: ¥Create a new file named **note.ts**: ::: code-group ```typescript [note.ts] import { Elysia, t } from 'elysia' class Note { constructor(public data: string[] = ['Moonhalo']) {} } export const note = new Elysia() .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'oh no :(') }, { params: t.Object({ index: t.Number() }) } ) ``` ::: 然后在 index.ts 文件中,将注释应用到主实例中: ¥Then on the **index.ts**, apply **note** into the main instance: ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { note } from './note' // [!code ++] class Note { // [!code --] constructor(public data: string[] = ['Moonhalo']) {} // [!code --] } // [!code --] const app = new Elysia() .use(openapi()) .use(note) // [!code ++] .decorate('note', new Note()) // [!code --] .get('/note', ({ note }) => note.data) // [!code --] .get( // [!code --] '/note/:index', // [!code --] ({ note, params: { index }, status }) => { // [!code --] return note.data[index] ?? status(404, 'oh no :(') // [!code --] }, // [!code --] { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) // [!code --] .listen(3000) ``` ::: 打开 ,你应该会像之前一样再次看到“oh no :(”。 ¥Open **** and you should see **oh no :(** again like before. 我们刚刚通过声明一个新的 Elysia 实例创建了一个注意插件。 ¥We have just created a **note** plugin, by declaring a new Elysia instance. 每个插件都是 Elysia 的一个独立实例,它拥有自己的路由、中间件和装饰器,这些可以应用于其他实例。 ¥Each plugin is a separate instance of Elysia which has its own routes, middlewares, and decorators which can be applied to other instances. ## 应用 CRUD {#applying-crud} ¥Applying CRUD 我们可以应用相同的模式来创建、更新和删除路由。 ¥We can apply the same pattern to create, update, and delete routes. ::: code-group ```typescript [note.ts] import { Elysia, t } from 'elysia' class Note { constructor(public data: string[] = ['Moonhalo']) {} add(note: string) {// [!code ++] this.data.push(note) // [!code ++] return this.data // [!code ++] } // [!code ++] remove(index: number) { // [!code ++] return this.data.splice(index, 1) // [!code ++] } // [!code ++] update(index: number, note: string) { // [!code ++] return (this.data[index] = note) // [!code ++] } // [!code ++] } export const note = new Elysia() .decorate('note', new Note()) .get('/note', ({ note }) => note.data) .put('/note', ({ note, body: { data } }) => note.add(data), { // [!code ++] body: t.Object({ // [!code ++] data: t.String() // [!code ++] }) // [!code ++] }) // [!code ++] .get( '/note/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .delete( // [!code ++] '/note/:index', // [!code ++] ({ note, params: { index }, status }) => { // [!code ++] if (index in note.data) return note.remove(index) // [!code ++] return status(422) // [!code ++] }, // [!code ++] { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] .patch( // [!code ++] '/note/:index', // [!code ++] ({ note, params: { index }, body: { data }, status }) => { // [!code ++] if (index in note.data) return note.update(index, data) // [!code ++] return status(422) // [!code ++] }, // [!code ++] { // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }), // [!code ++] body: t.Object({ // [!code ++] data: t.String() // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] ``` ::: 现在让我们打开 并尝试进行 CRUD 操作。 ¥Now let's open **** and try playing around with CRUD operations. ## 组 {#group} ¥Group 仔细观察,我们会发现 note 插件中的所有路由都共享一个 /note 前缀。 ¥If we look closely, all of the routes in the **note** plugin share a **/note** prefix. 我们可以通过声明前缀来简化此过程。 ¥We can simplify this by declaring **prefix** ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) // [!code ++] .decorate('note', new Note()) .get('/', ({ note }) => note.data) // [!code ++] .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }, { params: t.Object({ index: t.Number() }) } ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { params: t.Object({ index: t.Number() }), body: t.Object({ data: t.String() }) } ) ``` ::: ## 守护 {#guard} ¥Guard 现在我们可能会注意到插件中有几个路由具有参数验证功能。 ¥Now we may notice that there are several routes in plugin that has **params** validation. 我们可以定义一个守卫来将验证应用于插件中的路由。 ¥We may define a **guard** to apply validation to routes in the plugin. ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .guard({ // [!code ++] params: t.Object({ // [!code ++] index: t.Number() // [!code ++] }) // [!code ++] }) // [!code ++] .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }, { // [!code --] params: t.Object({ // [!code --] index: t.Number() // [!code --] }) // [!code --] } // [!code --] ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { params: t.Object({ // [!code --] index: t.Number() // [!code --] }), // [!code --] body: t.Object({ data: t.String() }) } ) ``` ::: 在调用 guard 并将其绑定到插件后,验证将应用于所有路由。 ¥Validation will be applied to all routes **after guard** is called and tied to the plugin. ## 生命周期 {#lifecycle} ¥Lifecycle 现在在实际使用中,我们可能需要在处理请求之前执行一些操作,例如记录日志。 ¥Now in real-world usage, we may want to do something like logging before the request is processed. 与其为每个路由内联 `console.log`,不如应用一个生命周期,在请求处理之前/之后进行拦截。 ¥Instead of inline `console.log` for each route, we may apply a **lifecycle** that intercepts the request before/after it is processed. 我们可以使用多种生命周期,但在本例中我们将使用 `onTransform`。 ¥There are several lifecycles that we can use, but in this case we will be using `onTransform`. ::: code-group ```typescript [note.ts] export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .onTransform(function log({ body, params, path, request: { method } }) { // [!code ++] console.log(`${method} ${path}`, { // [!code ++] body, // [!code ++] params // [!code ++] }) // [!code ++] }) // [!code ++] .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { body: t.Object({ data: t.String() }) }) .guard({ params: t.Object({ index: t.Number() }) }) .get('/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { if (index in note.data) return note.update(index, data) return status(422) }, { body: t.Object({ data: t.String() }) } ) ``` ::: `onTransform` 在路由之后、验证之前调用,因此我们可以执行一些操作,例如记录定义的请求,而不会触发 404 Not found 路由。 ¥`onTransform` is called after **routing but before validation**, so we can do something like logging the request that is defined without triggering the **404 Not found** route. 这使我们能够在处理请求之前记录日志,并且可以看到请求主体和路径参数。 ¥This allows us to log the request before it is processed, and we can see the request body and path parameters. ### 范围 {#scope} ¥Scope 默认情况下,生命周期钩子被封装。Hook 应用于同一实例中的路由,不应用于其他插件(未在同一插件中定义的路由)。 ¥By default, the **lifecycle hook is encapsulated**. Hook is applied to routes in the same instance, and is not applied to other plugins (routes that are not defined in the same plugin). 这意味着 `onTransform` 钩子中的日志函数将不会在其他实例上被调用,除非我们将其明确定义为 `scoped` 或 `global`。 ¥This means the log function, in the `onTransform` hook, will not be called on other instances, unless we explicitly defined it as `scoped` or `global`. ## 身份验证 {#authentication} ¥Authentication 现在我们可能想在路由中添加限制,这样只有注意的所有者才能更新或删除它。 ¥Now we may want to add restrictions to our routes, so only the owner of the note can update or delete it. 让我们创建一个 `user.ts` 文件来处理用户身份验证: ¥Let's create a `user.ts` file that will handle the user authentication: ```typescript [user.ts] import { Elysia, t } from 'elysia' // [!code ++] // [!code ++] export const user = new Elysia({ prefix: '/user' }) // [!code ++] .state({ // [!code ++] user: {} as Record, // [!code ++] session: {} as Record // [!code ++] }) // [!code ++] .put( // [!code ++] '/sign-up', // [!code ++] async ({ body: { username, password }, store, status }) => { // [!code ++] if (store.user[username]) // [!code ++] return status(400, { // [!code ++] success: false, // [!code ++] message: 'User already exists' // [!code ++] }) // [!code ++] // [!code ++] store.user[username] = await Bun.password.hash(password) // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] message: 'User created' // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] body: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }) // [!code ++] } // [!code ++] ) // [!code ++] .post( // [!code ++] '/sign-in', // [!code ++] async ({ // [!code ++] store: { user, session }, // [!code ++] status, // [!code ++] body: { username, password }, // [!code ++] cookie: { token } // [!code ++] }) => { // [!code ++] if ( // [!code ++] !user[username] || // [!code ++] !(await Bun.password.verify(password, user[username])) // [!code ++] ) // [!code ++] return status(400, { // [!code ++] success: false, // [!code ++] message: 'Invalid username or password' // [!code ++] }) // [!code ++] const key = crypto.getRandomValues(new Uint32Array(1))[0] // [!code ++] session[key] = username // [!code ++] token.value = key // [!code ++] return { // [!code ++] success: true, // [!code ++] message: `Signed in as ${username}` // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] body: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] cookie: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ) // [!code ++] } // [!code ++] ) // [!code ++] ``` 现在有很多事情需要解开: ¥Now there are a lot of things to unwrap here: 1. 我们创建了一个包含两个路由的新实例,分别用于注册和登录。 2. 在实例中,我们定义了一个内存存储 `user` 和 `session`。 * 2.1 `user` 将保存 `username` 和 `password` 的键值对 * 2.2 `session` 将保存 `session` 和 `username` 的键值对 3. 在 `/sign-up` 中,我们使用 argon2id 插入用户名和哈希密码 4. 在 `/sign-in` 中,我们执行以下操作: * 4.1 检查用户是否存在并验证密码 * 4.2 如果密码匹配,则在 `session` 中生成一个新的会话 * 4.3 将会话的值设置为 cookie `token` * 4.4 将 `secret` 附加到 cookie 中以添加哈希值并阻止攻击者篡改 cookie ::: tip 提示 由于我们使用内存存储,每次重新加载或每次编辑代码时数据都会被清除。 ¥As we are using an in-memory store, the data are wiped out every reload or every time we edit the code. 我们将在本教程的后续部分修复该问题。 ¥We will fix that in the later part of the tutorial. ::: 现在,如果我们想检查用户是否已登录,我们可以检查 `token` cookie 的值并与 `session` 存储进行检查。 ¥Now if we want to check if a user is signed in, we could check for value of `token` cookie and check with the `session` store. ## 参考模型 {#reference-model} ¥Reference Model 然而,我们可以认识到 `/sign-in` 和 `/sign-up` 都共享相同的 `body` 模型。 ¥However, we can recognize that both `/sign-in` and `/sign-up` both share the same `body` model. 与其到处复制粘贴模型,不如使用引用模型,通过指定名称来重用它。 ¥Instead of copy-pasting the model all over the place, we could use a **reference model** to reuse the model by specifying a name. 要创建参考模型,我们可以使用 `.model` 并传递模型的名称和值: ¥To create a **reference model**, we may use `.model` and pass the name and the value of models: ```typescript [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }) .state({ user: {} as Record, session: {} as Record }) .model({ // [!code ++] signIn: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] session: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ), // [!code ++] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code ++] }) // [!code ++] .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' // [!code ++] } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', // [!code ++] cookie: 'session' // [!code ++] } ) ``` 添加一个或多个模型后,我们可以通过在架构中引用它们的名称来重用它们,而无需提供文字类型,同时提供相同的功能和类型安全性。 ¥After adding a model/models, we can reuse them by referencing their name in the schema instead of providing a literal type while providing the same functionality and type safety. `Elysia.model` 可以接受多个重载: ¥`Elysia.model` could accept multiple overloads: 1. 提供一个对象,将所有键值对注册为模型 2. 提供一个函数,访问所有之前的模型,然后返回新的模型 最后,我们可以添加 `/profile` 和 `/sign-out` 路由,如下所示: ¥Finally, we could add the `/profile` and `/sign-out` routes as follows: ```typescript [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( // [!code ++] '/sign-out', // [!code ++] ({ cookie: { token } }) => { // [!code ++] token.remove() // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] message: 'Signed out' // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] cookie: 'optionalSession' // [!code ++] } // [!code ++] ) // [!code ++] .get( // [!code ++] '/profile', // [!code ++] ({ cookie: { token }, store: { session }, status }) => { // [!code ++] const username = session[token.value] // [!code ++] // [!code ++] if (!username) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] // [!code ++] return { // [!code ++] success: true, // [!code ++] username // [!code ++] } // [!code ++] }, // [!code ++] { // [!code ++] cookie: 'session' // [!code ++] } // [!code ++] ) // [!code ++] ``` 由于我们要在 `note` 中应用 `authorization`,因此需要重复两件事: ¥As we are going to apply `authorization` in the `note`, we are going to need to repeat two things: 1. 检查用户是否存在 2. 获取用户 ID(本例中为 'username') 对于 1.我们可以使用宏来代替守卫。 ¥For **1.** instead of using guard, we could use a **macro**. ## 插件数据去重 {#plugin-deduplication} ¥Plugin deduplication 由于我们要在多个模块(用户和注释)中重复使用此钩子,因此我们需要提取服务(实用程序)部分并将其应用于这两个模块。 ¥As we are going to reuse this hook in multiple modules (user, and note), let's extract the service (utility) part out and apply it to both modules. ```ts [user.ts] // @errors: 2538 import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) // [!code ++] .state({ // [!code ++] user: {} as Record, // [!code ++] session: {} as Record // [!code ++] }) // [!code ++] .model({ // [!code ++] signIn: t.Object({ // [!code ++] username: t.String({ minLength: 1 }), // [!code ++] password: t.String({ minLength: 8 }) // [!code ++] }), // [!code ++] session: t.Cookie( // [!code ++] { // [!code ++] token: t.Number() // [!code ++] }, // [!code ++] { // [!code ++] secrets: 'seia' // [!code ++] } // [!code ++] ), // [!code ++] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code ++] }) // [!code ++] export const user = new Elysia({ prefix: '/user' }) .use(userService) // [!code ++] .state({ // [!code --] user: {} as Record, // [!code --] session: {} as Record // [!code --] }) // [!code --] .model({ // [!code --] signIn: t.Object({ // [!code --] username: t.String({ minLength: 1 }), // [!code --] password: t.String({ minLength: 8 }) // [!code --] }), // [!code --] session: t.Cookie( // [!code --] { // [!code --] token: t.Number() // [!code --] }, // [!code --] { // [!code --] secrets: 'seia' // [!code --] } // [!code --] ), // [!code --] optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) // [!code --] }) // [!code --] ``` 此处的 `name` 属性非常重要,因为它是插件的唯一标识符,用于防止重复实例(类似于单例)。 ¥The `name` property here is very important, as it's a unique identifier for the plugin to prevent duplicate instances (like a singleton). 如果我们不使用插件来定义实例,则每次使用插件时都会注册钩子/生命周期和路由。 ¥If we were to define the instance without the plugin, hook/lifecycle and routes are going to be registered every time the plugin is used. 我们的目的是将此插件(服务)应用于多个模块以提供实用功能,这使得数据去重非常重要,因为生命周期不应该被注册两次。 ¥Our intention is to apply this plugin (service) to multiple modules to provide utility function, this make deduplication very important as life-cycle shouldn't be registered twice. ## 宏 {#macro} ¥Macro 宏允许我们定义一个带有自定义生命周期管理的自定义钩子。 ¥Macro allows us to define a custom hook with custom life-cycle management. 要定义宏,我们可以按如下方式使用 `.macro`: ¥To define a macro, we could use `.macro` as follows: ```ts [user.ts] import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { // [!code ++] if (!enabled) return // [!code ++] return { beforeHandle({ status, cookie: { token }, store: { session } }) { // [!code ++] if (!token.value) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] const username = session[token.value as unknown as number] // [!code ++] if (!username) // [!code ++] return status(401, { // [!code ++] success: false, // [!code ++] message: 'Unauthorized' // [!code ++] }) // [!code ++] } // [!code ++] } // [!code ++] } // [!code ++] }) // [!code ++] ``` 我们刚刚创建了一个名为 `isSignIn` 的新宏,它接受一个 `boolean` 值,如果 `boolean` 为 true,则添加一个 `onBeforeHandle` 事件,该事件在验证之后但在主处理程序之前执行,从而允许我们在此处提取身份验证逻辑。 ¥We have just created a new macro name `isSignIn` that accepts a `boolean` value, if it is true, then we add an `onBeforeHandle` event that executes **after validation but before the main handler**, allowing us to extract authentication logic here. 要使用宏,只需按如下方式指定 `isSignIn: true`: ¥To use the macro, simply specify `isSignIn: true` as follows: ```ts [user.ts] import { Elysia, t } from 'elysia' export const user = new Elysia({ prefix: '/user' }).use(userService).get( '/profile', ({ cookie: { token }, store: { session }, status }) => { const username = session[token.value] if (!username) // [!code --] return status(401, { // [!code --] success: false, // [!code --] message: 'Unauthorized' // [!code --] }) // [!code --] return { success: true, username } }, { isSignIn: true, // [!code ++] cookie: 'session' } ) ``` 由于我们指定了 `isSignIn`,我们可以提取命令式检查部分,并在多个路由上重用相同的逻辑,而无需重复复制粘贴相同的代码。 ¥As we specified `isSignIn`, we can extract the imperative checking part, and reuse the same logic on multiple routes without copy-pasting the same code all over again. ::: tip 提示 这看起来像是一个很小的代码更改,用来换取更大的样板代码,但随着服务器变得越来越复杂,用户检查也可能发展成为一个非常复杂的机制。 ¥This may seem like a small code change to trade for a larger boilerplate, but as the server grows more complex, the user-checking could also grow to be a very complex mechanism. ::: ## 解析 {#resolve} ¥Resolve 我们的最后一个目标是从令牌中获取用户名 (id)。我们可以使用 `resolve` 在与 `store` 相同的上下文中定义一个新属性,但仅在每个请求中执行它。 ¥Our last objective is to get the username (id) from the token. We could use `resolve` to define a new property into the same context as `store` but only execute it per request. 与 `decorate` 和 `store` 不同,resolve 在 `beforeHandle` 阶段定义,否则值将在验证后可用。 ¥Unlike `decorate` and `store`, resolve is defined at the `beforeHandle` stage or the value will be available **after validation**. 这确保在创建新属性之前,像 `cookie: 'session'` 这样的属性已经存在。 ¥This ensures that the property like `cookie: 'session'` exists before creating a new property. ```ts [user.ts] export const getUserId = new Elysia() // [!code ++] .use(userService) // [!code ++] .guard({ // [!code ++] cookie: 'session' // [!code ++] }) // [!code ++] .resolve(({ store: { session }, cookie: { token } }) => ({ // [!code ++] username: session[token.value] // [!code ++] })) // [!code ++] ``` 在此示例中,我们使用 `resolve` 定义了一个新的属性 `username`,从而将获取 `username` 的逻辑简化为一个属性。 ¥In this instance, we define a new property `username` by using `resolve`, allowing us to reduce the getting `username` logic into a property instead. 我们没有在这个 `getUserId` 实例中定义名称,因为我们希望 `guard` 和 `resolve` 能够重新应用于多个实例。 ¥We don't define a name in this `getUserId` instance because we want `guard` and `resolve` to reapply into multiple instances. ::: tip 提示 与宏相同,如果获取属性的逻辑很复杂,并且对于像这样的小操作来说可能不值得,那么 `resolve` 会表现良好。但由于在现实世界中,我们需要数据库连接、缓存和队列,因此它可能符合我们的描述。 ¥Same as macro, `resolve` plays well if the logic for getting the property is complex and might not be worth it for a small operation like this. But since in the real-world we are going to need database-connection, caching, and queuing it might make it fit the narrative. ::: ## 范围 {#scope-1} ¥Scope 现在,如果我们尝试使用 `getUserId`,我们可能会注意到属性 `username` 和 `guard` 未被应用。 ¥Now if we try to apply the use of the `getUserId`, we might notice that the property `username` and `guard` isn't applied. ```ts [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` 这是因为 Elysia 封装生命周期默认会执行此操作,如 [lifecycle](#lifecycle) 中所述。 ¥This is because the Elysia **encapsulate lifecycle** does this by default as mentioned in [lifecycle](#lifecycle) 这是有意为之,因为我们不希望每个模块对其他模块产生副作用。副作用可能非常难以调试,尤其是在具有多个(Elysia)依赖的大型代码库中。 ¥This is intentional by design, as we don't want each module to have a side-effect to other modules. Having a side-effect can be very difficult to debug especially in a large codebase with multiple (Elysia) dependencies. 如果我们希望将生命周期应用于父级,我们可以使用以下任一方式显式地注释它可以应用于父级: ¥If we want lifecycle to be applied to the parent, we can explicitly annotate that it could be applied to the parent by using either: 1. scoped - 仅适用于上一级父级,不适用于其他子级 2. global - 应用于所有父级 在我们的例子中,我们希望使用 scoped,因为它仅适用于使用该服务的控制器。 ¥In our case, we want to use **scoped** as it will apply to the controller that uses the service only. 为此,我们需要将该生命周期注释为 `scoped`: ¥To do this, we need to annotate that life-cycle as `scoped`: ```typescript [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ as: 'scoped', // [!code ++] isSignIn: true, cookie: 'session' }) .resolve( { as: 'scoped' }, // [!code ++] ({ store: { session }, cookie: { token } }) => ({ username: session[token.value] }) ) export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ // ^? success: true, username })) ``` 或者,如果我们定义了多个 `scoped`,我们可以使用 `as` 来转换多个生命周期。 ¥Alternatively, if we have multiple `scoped` defined, we could use `as` to cast multiple life-cycles instead. ```ts [user.ts] export const getUserId = new Elysia() .use(userService) .guard({ as: 'scoped', // [!code --] isSignIn: true, cookie: 'session' }) .resolve( { as: 'scoped' }, // [!code --] ({ store: { session }, cookie: { token } }) => ({ username: session[token.value] }) ) .as('scoped') // [!code ++] export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` 两者实现的效果相同,唯一的区别在于单个或多个强制类型转换实例。 ¥Both achieve the same effect, the only difference is single or multiple cast instances. ::: tip 提示 封装在运行时和类型级别均有发生。这使我们能够提前捕获错误。 ¥Encapsulation happens in both runtime, and type-level. This allows us to catch the error ahead of time. ::: 最后,我们可以重用 `userService` 和 `getUserId` 来帮助我们的注意控制器进行授权。 ¥Lastly, we can reuse `userService` and `getUserId` to help with authorization in our **note** controller. 但首先,别忘了在 `index.ts` 文件中导入 `user`: ¥But first, don't forget to import the `user` in the `index.ts` file: ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { note } from './note' import { user } from './user' // [!code ++] const app = new Elysia() .use(openapi()) .use(user) // [!code ++] .use(note) .listen(3000) ``` ::: ## 授权 {#authorization} ¥Authorization 首先,修改 `Note` 类以存储创建注意的用户。 ¥First, let's modify the `Note` class to store the user who created the note. 但是,除了定义 `Memo` 类型之外,我们还可以定义一个备忘 Schema 并从中推断类型,从而允许我们同步运行时和类型级别。 ¥But instead of defining the `Memo` type, we can define a memo schema and infer the type from it, allowing us to sync runtime and type-level. ```typescript [note.ts] import { Elysia, t } from 'elysia' const memo = t.Object({ // [!code ++] data: t.String(), // [!code ++] author: t.String() // [!code ++] }) // [!code ++] type Memo = typeof memo.static // [!code ++] class Note { constructor(public data: string[] = ['Moonhalo']) {} // [!code --] constructor( // [!code ++] public data: Memo[] = [ // [!code ++] { // [!code ++] data: 'Moonhalo', // [!code ++] author: 'saltyaom' // [!code ++] } // [!code ++] ] // [!code ++] ) {} // [!code ++] add(note: string) { // [!code --] add(note: Memo) { // [!code ++] this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: string) { // [!code --] return (this.data[index] = note) // [!code --] } // [!code --] update(index: number, note: Partial) { // [!code ++] return (this.data[index] = { ...this.data[index], ...note }) // [!code ++] } // [!code ++] } export const note = new Elysia({ prefix: '/note' }) .decorate('note', new Note()) .model({ // [!code ++] memo: t.Omit(memo, ['author']) // [!code ++] }) // [!code ++] .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .put('/', ({ note, body: { data } }) => note.add(data), { // [!code --] body: t.Object({ // [!code --] data: t.String() // [!code --] }), // [!code --] }) // [!code --] .put('/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { // [!code ++] body: 'memo' // [!code ++] } ) // [!code ++] .guard({ params: t.Object({ index: t.Number() }) }) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') } ) .delete( '/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) } ) .patch( '/:index', ({ note, params: { index }, body: { data }, status }) => { // [!code --] if (index in note.data) return note.update(index, data) // [!code --] ({ note, params: { index }, body: { data }, status, username }) => { // [!code ++] if (index in note.data) // [!code ++] return note.update(index, { data, author: username })) // [!code ++] return status(422) }, { body: t.Object({ // [!code --] data: t.String() // [!code --] }), // [!code --] body: 'memo' } ) ``` 现在让我们导入并使用 `userService` 和 `getUserId` 将授权应用于注意控制器。 ¥Now let's import, and use `userService`, `getUserId` to apply authorization to the **note** controller. ```typescript [note.ts] import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' // [!code ++] const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) // [!code ++] .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) // [!code ++] .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) ``` 就这样 🎉 ¥And that's it 🎉 我们刚刚通过重用之前创建的服务实现了授权。 ¥We have just implemented authorization by reusing the service we created earlier. ## 错误处理 {#error-handling} ¥Error handling API 最重要的方面之一是确保一切顺利,如果出错,我们需要妥善处理。 ¥One of the most important aspects of an API is to make sure nothing goes wrong, and if it does, we need to handle it properly. 我们使用 `onError` 生命周期来捕获服务器抛出的任何错误。 ¥We use use the `onError` lifecycle to catch any error that is thrown in the server. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { note } from './note' import { user } from './user' const app = new Elysia() .use(openapi()) .onError(({ error, code }) => { // [!code ++] if (code === 'NOT_FOUND') return // [!code ++] console.error(error) // [!code ++] }) // [!code ++] .use(user) .use(note) .listen(3000) ``` ::: 我们刚刚添加了一个错误监听器,它将捕获服务器抛出的任何错误(404 Not Found 除外),并将其记录到控制台。 ¥We have just added an error listener that will catch any error that is thrown in the server, excluding **404 Not Found** and log it to the console. ::: tip 提示 请注意,`onError` 在 `use(note)` 之前使用。这很重要,因为 Elysia 自上而下地应用该方法。必须在路由之前应用监听器。 ¥Notice that `onError` is used before `use(note)`. This is important as Elysia applies the method from top-to-bottom. The listener has to be applied before the route. 由于 `onError` 应用于根实例,因此无需定义作用域,因为它将应用于所有子实例。 ¥And as `onError` is applied on the root instance, it doesn't need to define a scope as it will apply to all children instances. ::: 返回真值将覆盖默认错误响应,因此我们可以返回继承状态码的自定义错误响应。 ¥Returning a truthy value will override a default error response, so we can return a custom error response which inherits the status code. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { note } from './note' const app = new Elysia() .use(openapi()) .onError(({ error, code }) => { // [!code ++] if (code === 'NOT_FOUND') return 'Not Found :(' // [!code ++] console.error(error) // [!code ++] }) // [!code ++] .use(note) .listen(3000) ``` ::: ### 可观察性 {#observability} ¥Observability 现在我们有了一个可以运行的 API,最后一步是确保在部署服务器后一切正常。 ¥Now we have a working API, a final touch is to make sure everything is working after we deployed our server. Elysia 通过 `@elysiajs/opentelemetry` 插件默认支持 OpenTelemetry。 ¥Elysia supports OpenTelemetry by default with the `@elysiajs/opentelemetry` plugin. ```bash bun add @elysiajs/opentelemetry ``` 确保 OpenTelemetry 收集器正在运行,否则我们将使用 Docker 中的 Jaeger。 ¥Make sure to have an OpenTelemetry collector running otherwise we will be using Jaeger from docker. ```bash docker run --name jaeger \ -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ -e COLLECTOR_OTLP_ENABLED=true \ -p 6831:6831/udp \ -p 6832:6832/udp \ -p 5778:5778 \ -p 16686:16686 \ -p 4317:4317 \ -p 4318:4318 \ -p 14250:14250 \ -p 14268:14268 \ -p 14269:14269 \ -p 9411:9411 \ jaegertracing/all-in-one:latest ``` 现在让我们将 OpenTelemetry 插件应用到我们的服务器。 ¥Now let's apply the OpenTelemetry plugin to our server. ::: code-group ```typescript [index.ts] import { Elysia, t } from 'elysia' import { opentelemetry } from '@elysiajs/opentelemetry' // [!code ++] import { openapi } from '@elysiajs/openapi' import { note } from './note' import { user } from './user' const app = new Elysia() .use(opentelemetry()) // [!code ++] .use(openapi()) .onError(({ error, code }) => { if (code === 'NOT_FOUND') return 'Not Found :(' console.error(error) }) .use(note) .use(user) .listen(3000) ``` ::: 现在尝试更多请求并打开 查看跟踪信息。 ¥Now try out some more requests and open to see traces. 选择服务 Elysia 并点击“查找跟踪”,我们应该能够看到我们发出的请求列表。 ¥Select service **Elysia** and click on **Find Traces**, we should be able to see a list of requests that we have made. ![Jaeger showing list of requests](/tutorial/jaeger-list.webp) 点击任意请求,查看每个生命周期钩子处理该请求所需的时间。 ¥Click on any of the requests to see how long each lifecycle hook takes to process the request. ![Jaeger showing request span](/tutorial/jaeger-span.webp) 点击根父级 span 查看请求详情,这将显示请求和响应负载,以及错误(如果有)。 ¥Click on the root parent span to see the request details, this will show you the request and response payload, and errors if have any. ![Jaeger showing request detail](/tutorial/jaeger-detail.webp) Elysia 开箱即用地支持 OpenTelemetry,它会自动与其他支持 OpenTelemetry 的 JavaScript 库集成,例如 Prisma、GraphQL Yoga、Effect 等。 ¥Elysia supports OpenTelemetry out of the box, it automatically integrates with other JavaScript libraries that support OpenTelemetry like Prisma, GraphQL Yoga, Effect, etc. 你还可以使用其他 OpenTelemetry 插件将跟踪发送到其他服务,例如 Zipkin、Prometheus 等。 ¥You can also use other OpenTelemetry plugins to send traces to other services like Zipkin, Prometheus, etc. ## 代码库回顾 {#codebase-recap} ¥Codebase recap 如果你正在遵循以下步骤,你应该拥有如下所示的代码库: ¥If you are following along, you should have a codebase that looks like this: ::: code-group ```typescript twoslash [index.ts] // @errors: 2538 // @filename: user.ts import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(userService) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( '/sign-out', ({ cookie: { token } }) => { token.remove() return { success: true, message: 'Signed out' } }, { cookie: 'optionalSession' } ) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) // @filename: note.ts import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) // @filename: index.ts // ---cut--- import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' import { opentelemetry } from '@elysiajs/opentelemetry' import { note } from './note' import { user } from './user' const app = new Elysia() .use(opentelemetry()) .use(openapi()) .onError(({ error, code }) => { if (code === 'NOT_FOUND') return 'Not Found :(' console.error(error) }) .use(user) .use(note) .listen(3000) ``` ```typescript twoslash [user.ts] // @errors: 2538 import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(userService) .put( '/sign-up', async ({ body: { username, password }, store, status }) => { if (store.user[username]) return status(400, { success: false, message: 'User already exists' }) store.user[username] = await Bun.password.hash(password) return { success: true, message: 'User created' } }, { body: 'signIn' } ) .post( '/sign-in', async ({ store: { user, session }, status, body: { username, password }, cookie: { token } }) => { if ( !user[username] || !(await Bun.password.verify(password, user[username])) ) return status(400, { success: false, message: 'Invalid username or password' }) const key = crypto.getRandomValues(new Uint32Array(1))[0] session[key] = username token.value = key return { success: true, message: `Signed in as ${username}` } }, { body: 'signIn', cookie: 'optionalSession' } ) .get( '/sign-out', ({ cookie: { token } }) => { token.remove() return { success: true, message: 'Signed out' } }, { cookie: 'optionalSession' } ) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) ``` ```typescript twoslash [note.ts] // @errors: 2538 // @filename: user.ts import { Elysia, t } from 'elysia' export const userService = new Elysia({ name: 'user/service' }) .state({ user: {} as Record, session: {} as Record }) .model({ signIn: t.Object({ username: t.String({ minLength: 1 }), password: t.String({ minLength: 8 }) }), session: t.Cookie( { token: t.Number() }, { secrets: 'seia' } ), optionalSession: t.Cookie( { token: t.Optional(t.Number()) }, { secrets: 'seia' } ) }) .macro({ isSignIn(enabled: boolean) { if (!enabled) return return { beforeHandle({ status, cookie: { token }, store: { session } }) { if (!token.value) return status(401, { success: false, message: 'Unauthorized' }) const username = session[token.value as unknown as number] if (!username) return status(401, { success: false, message: 'Unauthorized' }) } } } }) export const getUserId = new Elysia() .use(userService) .guard({ isSignIn: true, cookie: 'session' }) .resolve(({ store: { session }, cookie: { token } }) => ({ username: session[token.value] })) .as('scoped') export const user = new Elysia({ prefix: '/user' }) .use(getUserId) .get('/profile', ({ username }) => ({ success: true, username })) // @filename: note.ts // ---cut--- import { Elysia, t } from 'elysia' import { getUserId, userService } from './user' const memo = t.Object({ data: t.String(), author: t.String() }) type Memo = typeof memo.static class Note { constructor( public data: Memo[] = [ { data: 'Moonhalo', author: 'saltyaom' } ] ) {} add(note: Memo) { this.data.push(note) return this.data } remove(index: number) { return this.data.splice(index, 1) } update(index: number, note: Partial) { return (this.data[index] = { ...this.data[index], ...note }) } } export const note = new Elysia({ prefix: '/note' }) .use(userService) .decorate('note', new Note()) .model({ memo: t.Omit(memo, ['author']) }) .onTransform(function log({ body, params, path, request: { method } }) { console.log(`${method} ${path}`, { body, params }) }) .get('/', ({ note }) => note.data) .use(getUserId) .put( '/', ({ note, body: { data }, username }) => note.add({ data, author: username }), { body: 'memo' } ) .get( '/:index', ({ note, params: { index }, status }) => { return note.data[index] ?? status(404, 'Not Found :(') }, { params: t.Object({ index: t.Number() }) } ) .guard({ params: t.Object({ index: t.Number() }) }) .delete('/:index', ({ note, params: { index }, status }) => { if (index in note.data) return note.remove(index) return status(422) }) .patch( '/:index', ({ note, params: { index }, body: { data }, status, username }) => { if (index in note.data) return note.update(index, { data, author: username }) return status(422) }, { isSignIn: true, body: 'memo' } ) ``` ::: ## 生产环境构建 {#build-for-production} ¥Build for production 最后,我们可以使用 `bun build` 将服务器打包成二进制文件以供生产使用: ¥Finally we can bundle our server into a binary for production using `bun build`: ```bash bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun \ --outfile server \ ./src/index.ts ``` 这条命令有点长,我们来分解一下: ¥This command is a bit long, so let's break it down: 1. `--compile` - 将 TypeScript 编译为二进制文件 2. `--minify-whitespace` - 删除不必要的空格 3. `--minify-syntax` - 压缩 JavaScript 语法以减小文件大小 4. `--target bun` - 以 `bun` 平台为目标,这可以优化目标平台的二进制文件。 5. `--outfile server` - 将二进制文件输出为 `server` 6. `./src/index.ts` - 我们服务器的入口文件(代码库) 现在我们可以使用 `./server` 运行二进制文件,它将像使用 `bun dev` 一样在 3000 端口启动服务器。 ¥Now we can run the binary using `./server` and it will start the server on port 3000 same as using `bun dev`. ```bash ./server ``` 打开浏览器并访问 `http://localhost:3000/openapi`,你应该会看到与使用 dev 命令相同的结果。 ¥Open your browser and navigate to `http://localhost:3000/openapi`, you should see the same result as using the dev command. 通过压缩二进制文件,我们不仅使服务器变得小巧便携,还显著降低了内存使用量。 ¥By minifying the binary not only have we made our server small and portable, we also significantly reduced the memory usage of it. ::: tip 提示 Bun 确实有 `--minify` 标志可以最小化二进制文件,但它包含 `--minify-identifiers`,由于我们使用了 OpenTelemetry,它会重命名函数名称,并使跟踪变得比预期更困难。 ¥Bun does have the `--minify` flag that will minify the binary, however it includes `--minify-identifiers`, and as we are using OpenTelemetry, it's going to rename function names and make tracing harder than it should. ::: ::: warning 警告 练习:尝试运行开发服务器和生产服务器,并比较内存使用情况。 ¥Exercise: Try to run the development server and production server, and compare the memory usage. 开发服务器将使用名为 'bun' 的进程,而生产服务器将使用名为 'server' 的进程。 ¥The development server will use a process named 'bun', while the production server will use the name 'server'. ::: ## 总结 {#wrapping-up} ¥Wrapping up 就这样 🎉 ¥And- that's it 🎉 我们使用 Elysia 创建了一个简单的 API,学习了如何创建一个简单的 API、如何处理错误以及如何使用 OpenTelemetry 观察我们的服务器。 ¥We have created a simple API using Elysia, we have learned how to create a simple API, how to handle errors, and how to observe our server using OpenTelemetry. 你可以更进一步,尝试连接到真实的数据库、连接到真实的前端或使用 WebSocket 实现实时通信。 ¥You could to take a step further by trying to connect to a real database, connect to a real frontend or implement real-time communication with WebSocket. 本教程涵盖了创建 Elysia 服务器所需了解的大部分概念,但你可能还需要了解其他一些有用的概念。 ¥This tutorial covered most of the concepts we need to know to create an Elysia server, however there are other several useful concepts you might want to know. ### 如果你遇到问题 {#if-you-are-stuck} ¥If you are stuck 如果你有任何其他问题,欢迎在 GitHub 讨论区、Discord 和 Twitter 上向我们的社区提问。 ¥If you have any further questions, feel free to ask our community on GitHub Discussions, Discord, and Twitter. 祝你使用 Elysia 一切顺利 ❤️ ¥We wish you well on your journey with Elysia ❤️ --- --- url: 'https://elysiajs.com/integrations/better-auth.md' --- # 更好的身份验证 {#better-auth} ¥Better Auth Better Auth 是一个与框架无关的 TypeScript 身份验证(和授权)框架。 ¥Better Auth is framework-agnostic authentication (and authorization) framework for TypeScript. 它提供了一套全面的开箱即用功能,并包含一个插件生态系统,可简化高级功能的添加。 ¥It provides a comprehensive set of features out of the box and includes a plugin ecosystem that simplifies adding advanced functionalities. 我们建议你在浏览此页面之前先了解一下 [更好的身份验证基本设置](https://www.better-auth.com/docs/installation)。 ¥We recommend going through the [Better Auth basic setup](https://www.better-auth.com/docs/installation) before going through this page. 我们的基本设置如下所示: ¥Our basic setup will look like this: ```ts [auth.ts] import { betterAuth } from 'better-auth' import { Pool } from 'pg' export const auth = betterAuth({ database: new Pool() }) ``` ## 处理程序 {#handler} ¥Handler 设置 Better Auth 实例后,我们可以通过 [mount](/patterns/mount.html) 挂载到 Elysia。 ¥After setting up Better Auth instance, we can mount to Elysia via [mount](/patterns/mount.html). 我们需要将处理程序挂载到 Elysia 端点。 ¥We need to mount the handler to Elysia endpoint. ```ts [index.ts] import { Elysia } from 'elysia' import { auth } from './auth' const app = new Elysia() .mount(auth.handler) // [!code ++] .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` 然后我们就可以使用 `http://localhost:3000/api/auth` 访问 Better Auth 了。 ¥Then we can access Better Auth with `http://localhost:3000/api/auth`. ### 自定义端点 {#custom-endpoint} ¥Custom endpoint 我们建议在使用 [mount](/patterns/mount.html) 时设置前缀路径。 ¥We recommend setting a prefix path when using [mount](/patterns/mount.html). ```ts [index.ts] import { Elysia } from 'elysia' const app = new Elysia() .mount('/auth', auth.handler) // [!code ++] .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` 然后我们就可以使用 `http://localhost:3000/auth/api/auth` 访问 Better Auth 了。 ¥Then we can access Better Auth with `http://localhost:3000/auth/api/auth`. 但 URL 看起来是多余的,我们可以在 Better Auth 实例中将 `/api/auth` 前缀自定义为其他内容。 ¥But the URL looks redundant, we can customize the `/api/auth` prefix to something else in Better Auth instance. ```ts import { betterAuth } from 'better-auth' import { openAPI } from 'better-auth/plugins' import { passkey } from 'better-auth/plugins/passkey' import { Pool } from 'pg' export const auth = betterAuth({ basePath: '/api' // [!code ++] }) ``` 然后我们就可以使用 `http://localhost:3000/auth/api` 访问 Better Auth 了。 ¥Then we can access Better Auth with `http://localhost:3000/auth/api`. 遗憾的是,我们无法将 Better Auth 实例的 `basePath` 设置为空或 `/`。 ¥Unfortunately, we can't set `basePath` of a Better Auth instance to be empty or `/`. ## OpenAPI {#openapi} Better Auth 支持 `openapi` 和 `better-auth/plugins`。 ¥Better Auth support `openapi` with `better-auth/plugins`. 但是,如果我们使用 [@elysiajs/openapi](/plugins/openapi),你可能需要从 Better Auth 实例中提取文档。 ¥However if we are using [@elysiajs/openapi](/plugins/openapi), you might want to extract the documentation from Better Auth instance. 我们可以使用以下代码执行此操作: ¥We may do that with the following code: ```ts import { openAPI } from 'better-auth/plugins' let _schema: ReturnType const getSchema = async () => (_schema ??= auth.api.generateOpenAPISchema()) export const OpenAPI = { getPaths: (prefix = '/auth/api') => getSchema().then(({ paths }) => { const reference: typeof paths = Object.create(null) for (const path of Object.keys(paths)) { const key = prefix + path reference[key] = paths[path] for (const method of Object.keys(paths[path])) { const operation = (reference[key] as any)[method] operation.tags = ['Better Auth'] } } return reference }) as Promise, components: getSchema().then(({ components }) => components) as Promise } as const ``` 然后在使用 `@elysiajs/swagger` 的 Elysia 实例中。 ¥Then in our Elysia instance that use `@elysiajs/swagger`. ```ts import { Elysia } from 'elysia' import { openapi } from '@elysiajs/openapi' import { OpenAPI } from './auth' const app = new Elysia().use( openapi({ documentation: { components: await OpenAPI.components, paths: await OpenAPI.getPaths() } }) ) ``` ## CORS {#cors} 要配置 cors,你可以使用 `@elysiajs/cors` 中的 `cors` 插件。 ¥To configure cors, you can use the `cors` plugin from `@elysiajs/cors`. ```ts import { Elysia } from 'elysia' import { cors } from '@elysiajs/cors' import { auth } from './auth' const app = new Elysia() .use( cors({ origin: 'http://localhost:3001', methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], credentials: true, allowedHeaders: ['Content-Type', 'Authorization'] }) ) .mount(auth.handler) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` ## 宏 {#macro} ¥Macro 你可以将 [macro](https://elysia.nodejs.cn/patterns/macro.html#macro) 和 [resolve](https://elysia.nodejs.cn/essential/handler.html#resolve) 结合使用,在传递到视图之前提供会话和用户信息。 ¥You can use [macro](https://elysia.nodejs.cn/patterns/macro.html#macro) with [resolve](https://elysia.nodejs.cn/essential/handler.html#resolve) to provide session and user information before pass to view. ```ts import { Elysia } from 'elysia' import { auth } from './auth' // user middleware (compute user and session and pass to routes) const betterAuth = new Elysia({ name: 'better-auth' }) .mount(auth.handler) .macro({ auth: { async resolve({ status, request: { headers } }) { const session = await auth.api.getSession({ headers }) if (!session) return status(401) return { user: session.user, session: session.session } } } }) const app = new Elysia() .use(betterAuth) .get('/user', ({ user }) => user, { auth: true }) .listen(3000) console.log( `🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}` ) ``` 这将允许你在所有路由中访问 `user` 和 `session` 对象。 ¥This will allow you to access the `user` and `session` object in all of your routes. --- --- url: 'https://elysiajs.com/plugins/server-timing.md' --- # 服务器计时插件 {#server-timing-plugin} ¥Server Timing Plugin 此插件增加了使用服务器计时 API 审计性能瓶颈的支持。 ¥This plugin adds support for auditing performance bottlenecks with Server Timing API 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/server-timing ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' new Elysia() .use(serverTiming()) .get('/', () => 'hello') .listen(3000) ``` Server Timing 随后会在 '服务器计时' 头中添加日志时长、函数名称以及每个生命周期函数的详细信息。 ¥Server Timing then will append header 'Server-Timing' with log duration, function name, and detail for each life-cycle function. 要检查,请打开浏览器开发者工具 > 网络 > \[通过 Elysia 服务器发出的请求] > 时间。 ¥To inspect, open browser developer tools > Network > \[Request made through Elysia server] > Timing. ![Developer tools showing Server Timing screenshot](/assets/server-timing.webp) 现在你可以轻松审计服务器的性能瓶颈。 ¥Now you can effortlessly audit the performance bottleneck of your server. ## 配置 {#config} ¥Config 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### enabled {#enabled} @default `NODE_ENV !== 'production'` 确定是否应启用服务器计时 ¥Determine whether or not Server Timing should be enabled ### allow {#allow} @default `undefined` 服务器计时是否应记录的条件 ¥A condition whether server timing should be log ### trace {#trace} @default `undefined` 允许服务器计时记录指定的生命周期事件: ¥Allow Server Timing to log specified life-cycle events: Trace 接受以下对象: ¥Trace accepts objects of the following: * 请求:从请求捕获时长 * 解析:从解析捕获时长 * 转换:从转换捕获时长 * beforeHandle:从 beforeHandle 捕获时长 * 句柄:从句柄捕获时长 * afterHandle:从 afterHandle 捕获时长 * 总计:捕获从开始到结束的总时长 ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. * [允许条件](#allow-condition) ## 允许条件 {#allow-condition} ¥Allow Condition 你可以通过 `allow` 属性禁用特定路由上的服务器计时。 ¥You may disable Server Timing on specific routes via `allow` property ```ts twoslash import { Elysia } from 'elysia' import { serverTiming } from '@elysiajs/server-timing' new Elysia() .use( serverTiming({ allow: ({ request }) => { return new URL(request.url).pathname !== '/no-trace' } }) ) ``` --- --- url: 'https://elysiajs.com/at-glance.md' --- # 概览 {#at-a-glance} ¥At a glance Elysia 是一个符合人机工程学的 Web 框架,用于使用 Bun 构建后端服务器。 ¥Elysia is an ergonomic web framework for building backend servers with Bun. Elysia 的设计秉承简洁性和类型安全理念,提供用户熟悉的 API,广泛支持 TypeScript,并针对 Bun 进行了优化。 ¥Designed with simplicity and type-safety in mind, Elysia offers a familiar API with extensive support for TypeScript and is optimized for Bun. 以下是 Elysia 中一个简单的 Hello World 示例。 ¥Here's a simple hello world in Elysia. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', 'Hello Elysia') .get('/user/:id', ({ params: { id }}) => id) .post('/form', ({ body }) => body) .listen(3000) ``` 导航到 [localhost:3000](http://localhost:3000/),你应该会看到结果 '你好,Elysia'。 ¥Navigate to [localhost:3000](http://localhost:3000/) and you should see 'Hello Elysia' as the result. ::: tip 提示 将鼠标悬停在代码片段上即可查看类型定义。 ¥Hover over the code snippet to see the type definition. 在模拟浏览器中,点击蓝色高亮显示的路径即可更改路径并预览响应。 ¥In the mock browser, click on the path highlighted in blue to change paths and preview the response. Elysia 可以在浏览器中运行,你看到的结果实际上是使用 Elysia 执行的。 ¥Elysia can run in the browser, and the results you see are actually executed using Elysia. ::: ## 性能 {#performance} ¥Performance 基于 Bun 构建,并进行静态代码分析等广泛的优化,使 Elysia 能够动态生成优化代码。 ¥Building on Bun and extensive optimization like static code analysis allows Elysia to generate optimized code on the fly. Elysia 的性能优于当今大多数 Web 框架 \[1],甚至可以与 Golang 和 Rust 框架 \[2] 的性能相媲美。 ¥Elysia can outperform most web frameworks available today\[1], and even match the performance of Golang and Rust frameworks\[2]. | 框架 | 运行时 | 平均值 | 纯文本 | 动态参数 | JSON 主体 | | ------------- | ---- | ----------- | ---------- | ---------- | ---------- | | bun | bun | 262,660.433 | 326,375.76 | 237,083.18 | 224,522.36 | | elysia | bun | 255,574.717 | 313,073.64 | 241,891.57 | 211,758.94 | | hyper-express | node | 234,395.837 | 311,775.43 | 249,675 | 141,737.08 | | hono | bun | 203,937.883 | 239,229.82 | 201,663.43 | 170,920.4 | | h3 | node | 96,515.027 | 114,971.87 | 87,935.94 | 86,637.27 | | oak | deno | 46,569.853 | 55,174.24 | 48,260.36 | 36,274.96 | | fastify | bun | 65,897.043 | 92,856.71 | 81,604.66 | 23,229.76 | | fastify | node | 60,322.413 | 71,150.57 | 62,060.26 | 47,756.41 | | koa | node | 39,594.14 | 46,219.64 | 40,961.72 | 31,601.06 | | express | bun | 29,715.537 | 39,455.46 | 34,700.85 | 14,990.3 | | express | node | 15,913.153 | 17,736.92 | 17,128.7 | 12,873.84 | ## TypeScript {#typescript} Elysia 旨在帮助你减少 TypeScript 编写。 ¥Elysia is designed to help you write less TypeScript. Elysia 的类型系统经过微调,可以自动从代码中推断类型,无需编写显式的 TypeScript 代码,同时在运行时和编译时提供类型安全,从而实现最符合人机工程学的开发体验。 ¥Elysia's Type System is fine-tuned to infer types from your code automatically, without needing to write explicit TypeScript, while providing type-safety at both runtime and compile time for the most ergonomic developer experience. 请看以下示例: ¥Take a look at this example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/user/:id', ({ params: { id } }) => id) // ^? .listen(3000) ``` 上述代码创建了一个路径参数 "id"。替换 `:id` 的值将在运行时和类型中传递给 `params.id`,无需手动类型声明。 ¥The above code creates a path parameter "id". The value that replaces `:id` will be passed to `params.id` both at runtime and in types, without manual type declaration. Elysia 的目标是帮助你减少 TypeScript 代码编写,将更多精力放在业务逻辑上。让框架处理复杂类型。 ¥Elysia's goal is to help you write less TypeScript and focus more on business logic. Let the framework handle the complex types. 使用 Elysia 不需要 TypeScript,但建议使用。 ¥TypeScript is not required to use Elysia, but it's recommended. ## 类型完整性 {#type-integrity} ¥Type Integrity 更进一步,Elysia 提供了 Elysia.t,这是一个模式构建器,用于在运行时和编译时验证类型和值,从而为你的数据类型创建单一可信来源。 ¥To take it a step further, Elysia provides **Elysia.t**, a schema builder to validate types and values at both runtime and compile time, creating a single source of truth for your data types. 让我们修改前面的代码,使其只接受数字值而不是字符串。 ¥Let's modify the previous code to accept only a number value instead of a string. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/user/:id', ({ params: { id } }) => id, { // ^? params: t.Object({ id: t.Number() }) }) .listen(3000) ``` 此代码确保我们的路径参数 id 在运行时和编译时(类型级别)始终为数字。 ¥This code ensures that our path parameter **id** will always be a number at both runtime and compile time (type-level). ::: tip 提示 将鼠标悬停在上面代码片段中的 "id" 上,即可查看类型定义。 ¥Hover over "id" in the above code snippet to see a type definition. ::: 借助 Elysia 的模式构建器,我们可以像使用单一数据源的强类型语言一样确保类型安全。 ¥With Elysia's schema builder, we can ensure type safety like a strongly typed language with a single source of truth. ## 标准 Schema {#standard-schema} ¥Standard Schema Elysia 支持 [标准 Schema](https://github.com/standard-schema/standard-schema),允许你使用你喜欢的验证库: ¥Elysia supports [Standard Schema](https://github.com/standard-schema/standard-schema), allowing you to use your favorite validation library: * Zod * Valibot * ArkType * 效果模式 * 是的 * Joi * [以及更多](https://github.com/standard-schema/standard-schema) ```typescript twoslash import { Elysia } from 'elysia' import { z } from 'zod' import * as v from 'valibot' new Elysia() .get('/id/:id', ({ params: { id }, query: { name } }) => id, { // ^? params: z.object({ id: z.coerce.number() }), query: v.object({ name: v.literal('Lilith') }) }) .listen(3000) ``` Elysia 将自动从模式推断类型,允许你使用你喜欢的验证库,同时仍然保持类型安全。 ¥Elysia will infer the types from the schema automatically, allowing you to use your favorite validation library while still maintaining type safety. ## OpenAPI {#openapi} Elysia 默认采用许多标准,例如 OpenAPI、WinterTC 合规性和标准 Schema。允许你与大多数行业标准工具集成,或者至少可以轻松地与你熟悉的工具集成。 ¥Elysia adopts many standards by default, like OpenAPI, WinterTC compliance, and Standard Schema. Allowing you to integrate with most of the industry standard tools or at least easily integrate with tools you are familiar with. 例如,由于 Elysia 默认采用 OpenAPI,因此生成 API 文档只需添加一行代码即可: ¥For instance, because Elysia adopts OpenAPI by default, generating API documentation is as easy as adding a one-liner: ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' new Elysia() .use(openapi()) // [!code ++] .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) ``` 使用 OpenAPI 插件,你可以无缝生成 API 文档页面,无需额外代码或特定配置,并轻松地与你的团队共享。 ¥With the OpenAPI plugin, you can seamlessly generate an API documentation page without additional code or specific configuration and share it with your team effortlessly. ## 从类型生成 OpenAPI {#openapi-from-types} ¥OpenAPI from types Elysia 对 OpenAPI 提供了出色的支持,其架构可用于从单一数据源进行数据验证、类型推断和 OpenAPI 注释。 ¥Elysia has excellent support for OpenAPI with schemas that can be used for data validation, type inference, and OpenAPI annotation from a single source of truth. Elysia 还支持直接从类型生成 OpenAPI 模式,只需一行代码即可生成,让你无需任何手动注释即可获得完整准确的 API 文档。 ¥Elysia also supports OpenAPI schema generation with **1 line directly from types**, allowing you to have complete and accurate API documentation without any manual annotation. ```typescript import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/oepnapi' import { fromTypes } from '@elysiajs/openapi/gen' export const app = new Elysia() .use(openapi({ references: fromTypes('src/index.ts') // [!code ++] })) .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) ``` ## 端到端类型安全 {#end-to-end-type-safety} ¥End-to-end Type Safety 使用 Elysia,类型安全不仅限于服务器端。 ¥With Elysia, type safety is not limited to server-side. 使用 Elysia,你可以使用 Elysia 的客户端库 "Eden" 自动与前端团队同步你的类型,类似于 tRPC。 ¥With Elysia, you can synchronize your types with your frontend team automatically, similar to tRPC, using Elysia's client library, "Eden". ```typescript twoslash import { Elysia, t } from 'elysia' import { openapi } from '@elysiajs/openapi' import { fromTypes } from '@elysiajs/openapi/gen' export const app = new Elysia() .use(openapi({ references: fromTypes('src/index.ts') })) .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) export type App = typeof app ``` 客户端: ¥And on your client-side: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/user/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) export type App = typeof app // @filename: client.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' const app = treaty('localhost:3000') // Get data from /user/617 const { data } = await app.user({ id: 617 }).get() // ^? console.log(data) ``` 借助 Eden,你可以使用现有的 Elysia 类型查询 Elysia 服务器,而无需生成代码,并自动同步前端和后端的类型。 ¥With Eden, you can use the existing Elysia types to query an Elysia server **without code generation** and synchronize types for both frontend and backend automatically. Elysia 不仅能帮助你创建可靠的后端,更能造福于世间一切美好事物。 ¥Elysia is not only about helping you create a confident backend but for all that is beautiful in this world. ## 平台无关 {#platform-agnostic} ¥Platform Agnostic Elysia 是为 Bun 设计的,但并不局限于 Bun。[符合 WinterTC 标准](https://wintertc.org/) 允许你在 Cloudflare Workers、Vercel Edge Functions 以及大多数其他支持 Web 标准请求的运行时上部署 Elysia 服务器。 ¥Elysia was designed for Bun, but is **not limited to Bun**. Being [WinterTC compliant](https://wintertc.org/) allows you to deploy Elysia servers on Cloudflare Workers, Vercel Edge Functions, and most other runtimes that support Web Standard Requests. ## 我们的社区 {#our-community} ¥Our Community 如果你在使用 Elysia 时遇到任何问题或遇到困难,欢迎在 GitHub 讨论区、Discord 或 Twitter 上向我们的社区提问。 ¥If you have questions or get stuck with Elysia, feel free to ask our community on GitHub Discussions, Discord, or Twitter. *** 1.以请求/秒为单位。在 Debian 11、Intel i7-13700K 处理器上,于 2023 年 8 月 6 日在 Bun 0.7.2 上测试了解析查询、路径参数和设置响应头的基准测试。请参阅基准测试条件 [此处](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/c7e26fe3f1bfee7ffbd721dbade10ad72a0a14ab#results)。 ¥1. Measured in requests/second. The benchmark for parsing query, path parameter and set response header on Debian 11, Intel i7-13700K tested on Bun 0.7.2 on 6 Aug 2023. See the benchmark condition [here](https://github.com/SaltyAom/bun-http-framework-benchmark/tree/c7e26fe3f1bfee7ffbd721dbade10ad72a0a14ab#results). 2.基于 [TechEmpower 基准测试第 22 轮](https://www.techempower.com/benchmarks/#section=data-r22\&hw=ph\&test=composite)。 ¥2. Based on [TechEmpower Benchmark round 22](https://www.techempower.com/benchmarks/#section=data-r22\&hw=ph\&test=composite). --- --- url: 'https://elysiajs.com/eden/treaty/overview.md' --- # Eden 条约 {#eden-treaty} ¥Eden Treaty Eden Treaty 是一个用于与服务器交互的对象表示,具有类型安全、自动补齐和错误处理功能。 ¥Eden Treaty is an object representation to interact with a server and features type safety, auto-completion, and error handling. 要使用 Eden Treaty,首先导出你现有的 Elysia 服务器类型: ¥To use Eden Treaty, first export your existing Elysia server type: ```typescript // server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/hi', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] ``` 然后导入服务器类型并在客户端使用 Elysia API: ¥Then import the server type and consume the Elysia API on the client: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/hi', () => 'Hi Elysia') .get('/id/:id', ({ params: { id } }) => id) .post('/mirror', ({ body }) => body, { body: t.Object({ id: t.Number(), name: t.String() }) }) .listen(3000) export type App = typeof app // [!code ++] // @filename: client.ts // ---cut--- // client.ts import { treaty } from '@elysiajs/eden' import type { App } from './server' // [!code ++] const app = treaty('localhost:3000') // response type: 'Hi Elysia' const { data, error } = await app.hi.get() // ^? ``` ## 树状语法 {#tree-like-syntax} ¥Tree like syntax HTTP 路径是文件系统树的资源指示器。 ¥HTTP Path is a resource indicator for a file system tree. 文件系统由多层级文件夹组成,例如: ¥File system consists of multiple levels of folders, for example: * /documents/elysia * /documents/kalpas * /documents/kelvin 每个级别由 /(斜杠)和名称分隔。 ¥Each level is separated by **/** (slash) and a name. 然而,在 JavaScript 中,我们使用 "."(点)而不是 "/"(斜杠)来访问更深层的资源。 ¥However in JavaScript, instead of using **"/"** (slash) we use **"."** (dot) to access deeper resources. Eden Treaty 将 Elysia 服务器转换为树状文件系统,以便 JavaScript 前端进行访问。 ¥Eden Treaty turns an Elysia server into a tree-like file system that can be accessed in the JavaScript frontend instead. | 路径 | 条约 | | ------------ | ------------ | | / | | | /hi | .hi | | /deep/nested | .deep.nested | 结合 HTTP 方法,我们可以与 Elysia 服务器交互。 ¥Combined with the HTTP method, we can interact with the Elysia server. | 路径 | 方法 | 条约 | | ------------ | ---- | ------------------- | | / | GET | .get() | | /hi | GET | .hi.get() | | /deep/nested | GET | .deep.nested.get() | | /deep/nested | POST | .deep.nested.post() | ## 动态路径 {#dynamic-path} ¥Dynamic path 但是,动态路径参数无法使用符号表示。如果完全替换它们,我们就不知道参数名称应该是什么了。 ¥However, dynamic path parameters cannot be expressed using notation. If they are fully replaced, we don't know what the parameter name is supposed to be. ```typescript // ❌ Unclear what the value is supposed to represent? treaty.item['skadi'].get() ``` 为了处理这种情况,我们可以使用函数指定动态路径来提供键值。 ¥To handle this, we can specify a dynamic path using a function to provide a key value instead. ```typescript // ✅ Clear that value is dynamic path is 'name' treaty.item({ name: 'Skadi' }).get() ``` | 路径 | 条约 | | -------------- | --------------------------- | | /item | .item | | /item/:name | .item({ name: 'Skadi' }) | | /item/:name/id | .item({ name: 'Skadi' }).id | --- --- url: 'https://elysiajs.com/patterns/unit-test.md' --- # 单元测试 {#unit-test} ¥Unit Test 由于符合 WinterCG 标准,我们可以使用请求/响应类来测试 Elysia 服务器。 ¥Being WinterCG compliant, we can use Request / Response classes to test an Elysia server. Elysia 提供了 Elysia.handle 方法,该方法接受 Web 标准 [请求](https://web.nodejs.cn/en-US/docs/Web/API/Request) 并返回 [响应](https://web.nodejs.cn/en-US/docs/Web/API/Response),模拟 HTTP 请求。 ¥Elysia provides the **Elysia.handle** method, which accepts a Web Standard [Request](https://web.nodejs.cn/en-US/docs/Web/API/Request) and returns [Response](https://web.nodejs.cn/en-US/docs/Web/API/Response), simulating an HTTP Request. Bun 包含一个内置的 [测试运行者](https://bun.sh/docs/cli/test),它通过 `bun:test` 模块提供类似 Jest 的 API,从而方便创建单元测试。 ¥Bun includes a built-in [test runner](https://bun.sh/docs/cli/test) that offers a Jest-like API through the `bun:test` module, facilitating the creation of unit tests. 在项目根目录下创建 test/index.test.ts 文件,并包含以下内容: ¥Create **test/index.test.ts** in the root of project directory with the following: ```typescript // test/index.test.ts import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' describe('Elysia', () => { it('returns a response', async () => { const app = new Elysia().get('/', () => 'hi') const response = await app .handle(new Request('http://localhost/')) .then((res) => res.text()) expect(response).toBe('hi') }) }) ``` 然后我们可以通过运行 bun test 进行测试。 ¥Then we can perform tests by running **bun test** ```bash bun test ``` 对 Elysia 服务器的新请求必须是完全有效的 URL,而不是 URL 的一部分。 ¥New requests to an Elysia server must be a fully valid URL, **NOT** a part of a URL. 请求必须提供以下 URL: ¥The request must provide URL as the following: | URL | 有效 | | ----------------------- | -- | | | ✅ | | /user | ❌ | 我们还可以使用其他测试库(例如 Jest)来创建 Elysia 单元测试。 ¥We can also use other testing libraries like Jest to create Elysia unit tests. ## Eden 条约测试 {#eden-treaty-test} ¥Eden Treaty test 我们可以使用 Eden Treaty 为 Elysia 服务器创建端到端类型安全测试,如下所示: ¥We may use Eden Treaty to create an end-to-end type safety test for Elysia server as follows: ```typescript twoslash // test/index.test.ts import { describe, expect, it } from 'bun:test' import { Elysia } from 'elysia' import { treaty } from '@elysiajs/eden' const app = new Elysia().get('/hello', 'hi') const api = treaty(app) describe('Elysia', () => { it('returns a response', async () => { const { data, error } = await api.hello.get() expect(data).toBe('hi') // ^? }) }) ``` 有关设置和更多信息,请参阅 [Eden 条约单元测试](/eden/treaty/unit-test)。 ¥See [Eden Treaty Unit Test](/eden/treaty/unit-test) for setup and more information. --- --- url: 'https://elysiajs.com/essential/life-cycle.md' --- # 生命周期 {#lifecycle} ¥Lifecycle 生命周期事件允许你在预定义点拦截重要事件,从而允许你根据需要自定义服务器的行为。 ¥Lifecycle events allow you to intercept important events at predefined points, allowing you to customize the behavior of your server as needed. Elysia 的生命周期如下图所示。 ¥Elysia's lifecycle can be illustrated as the following. ![Elysia Life Cycle Graph](/assets/lifecycle-chart.svg) > 点击图片放大 以下是 Elysia 中可用的请求生命周期事件: ¥Below are the request lifecycle events available in Elysia: ## 为什么 {#why} ¥Why 假设我们想要返回一些 HTML。 ¥Imagine we want to return some HTML. 我们需要将 "内容类型" 标头设置为 "text/html",以便浏览器呈现 HTML。 ¥We need to set **"Content-Type"** headers as **"text/html"** for the browser to render HTML. 如果有大量处理程序(例如约 200 个端点),则显式指定响应为 HTML 可能会重复。 ¥Explicitly specifying that the response is HTML could be repetitive if there are a lot of handlers, say ~200 endpoints. 这会导致大量重复代码,仅仅为了指定 "text/html" "内容类型"。 ¥This can lead to a lot of duplicated code just to specify the **"text/html"** **"Content-Type"** 但是,如果我们发送响应后,能够检测到响应是 HTML 字符串,然后自动附加标头,会怎么样呢? ¥But what if after we send a response, we could detect that the response is an HTML string and then append the header automatically? 这就是生命周期的概念发挥作用的时候了。 ¥That's when the concept of lifecycle comes into play. ## 钩子 {#hook} ¥Hook 我们将每个拦截生命周期事件的函数称为 "hook",因为该函数会挂接到生命周期事件中。 ¥We refer to each function that intercepts the lifecycle event as **"hook"**, as the function hooks into the lifecycle event. 钩子可分为两类: ¥Hooks can be categorized into 2 types: 1. 本地钩子:在特定路由上执行 2. 拦截器钩子:在每条路由上执行 ::: tip 提示 该钩子将接受与处理程序相同的上下文;你可以想象在特定位置添加路由处理程序。 ¥The hook will accept the same Context as a handler; you can imagine adding a route handler but at a specific point. ::: ## 本地钩子 {#local-hook} ¥Local Hook 在特定路由上执行本地钩子。 ¥A local hook is executed on a specific route. 要使用本地钩子,你可以将钩子内联到路由处理程序中: ¥To use a local hook, you can inline hook into a route handler: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) set.headers['Content-Type'] = 'text/html; charset=utf8' } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` 响应应如下所示: ¥The response should be listed as follows: | 路径 | 内容类型 | | --- | ----------------------- | | / | text/html;charset=utf8 | | /hi | text/plain;charset=utf8 | ## 拦截器钩子 {#interceptor-hook} ¥Interceptor Hook 将钩子注册到当前实例的每个后续处理程序中。 ¥Register hook into every handler **of the current instance** that came after. 要添加拦截器钩子,你可以使用 `.on`,后跟一个驼峰命名的生命周期事件: ¥To add an interceptor hook, you can use `.on` followed by a lifecycle event in camelCase: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/none', () => '

Hello World

') .onAfterHandle(({ response, set }) => { if (isHtml(response)) set.headers['Content-Type'] = 'text/html; charset=utf8' }) .get('/', () => '

Hello World

') .get('/hi', () => '

Hello World

') .listen(3000) ``` 响应应如下所示: ¥The response should be listed as follows: | 路径 | 内容类型 | | ----- | ----------------------- | | / | text/html;charset=utf8 | | /hi | text/html;charset=utf8 | | /none | text/plain;charset=utf8 | 其他插件的事件也会应用于路由,因此代码的顺序非常重要。 ¥Events from other plugins are also applied to the route, so the order of code is important. ::: tip 提示 以上代码仅适用于当前实例,不适用于父实例。 ¥The code above will only apply to the current instance, not applying to parent. 有关原因,请参阅 [scope](/essential/plugin#scope)。 ¥See [scope](/essential/plugin#scope) to find out why ::: ## 代码顺序 {#order-of-code} ¥Order of code Elysia 生命周期代码的顺序非常重要。 ¥The order of Elysia's lifecycle code is very important. 因为事件只有在注册后才会应用于路由。 ¥Because an event will only apply to routes **after** it is registered. 如果你将 `onError` 放在插件之前,插件将不会继承 `onError` 事件。 ¥If you put the `onError` before plugin, plugin will not inherit the `onError` event. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .get('/', () => 'hi') .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` 控制台应记录以下内容: ¥Console should log the following: ```bash 1 ``` 请注意,它不会记录 2,因为该事件是在路由之后注册的,因此不会应用于路由。 ¥Notice that it doesn't log **2**, because the event is registered after the route so it is not applied to the route. 这也适用于插件。 ¥This also applies to the plugin. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log('1') }) .use(someRouter) .onBeforeHandle(() => { console.log('2') }) .listen(3000) ``` 在上面的代码中,由于事件是在插件之后注册的,因此只会记录 1 个事件。 ¥In the code above, only **1** will be logged, because the event is registered after the plugin. 这是因为每个事件都将内联到路由处理程序中,以创建真正的封装范围和静态代码分析。 ¥This is because each events will be inline into a route handler to create a true encapsulation scope and static code analysis. 唯一的例外是 `onRequest`,它在路由处理程序之前执行,因此无法将其内联并绑定到路由进程。 ¥The only exception is `onRequest` which is executed before the route handler so it couldn't be inlined and tied to the routing process instead. ## 请求 {#request} ¥Request 收到每个新请求后执行的第一个生命周期事件。 ¥The first lifecycle event to get executed for every new request is received. 由于 `onRequest` 旨在仅提供最重要的上下文以减少开销,因此建议在以下场景中使用: ¥As `onRequest` is designed to provide only the most crucial context to reduce overhead, it is recommended to use in the following scenarios: * 缓存 * 速率限制器 / IP/区域锁定 * 分析 * 提供自定义标头,例如。 CORS #### 示例 {#example} ¥Example 以下是用于对特定 IP 地址实现速率限制的伪代码。 ¥Below is a pseudocode to enforce rate-limits on a certain IP address. ```typescript import { Elysia } from 'elysia' new Elysia() .use(rateLimiter) .onRequest(({ rateLimiter, ip, set, status }) => { if (rateLimiter.check(ip)) return status(420, 'Enhance your calm') }) .get('/', () => 'hi') .listen(3000) ``` 如果 `onRequest` 返回了一个值,它将被用作响应,并且其余的生命周期将被跳过。 ¥If a value is returned from `onRequest`, it will be used as the response and the rest of the lifecycle will be skipped. ### 前置上下文 {#pre-context} ¥Pre Context Context 的 `onRequest` 类型为 `PreContext`,它是 `Context` 的最小表示,具有以下属性:请求:`Request` ¥Context's `onRequest` is typed as `PreContext`, a minimal representation of `Context` with the attribute on the following: request: `Request` * 设置:`Set` * store * decorators Context 不提供 `derived` 值,因为派生基于 `onTransform` 事件。 ¥Context doesn't provide `derived` value because derive is based on `onTransform` event. ## 解析 {#parse} ¥Parse Parse 相当于 Express 中的 body 解析器。 ¥Parse is an equivalent of **body parser** in Express. 一个用于解析请求体(body)的函数,返回值将附加到 `Context.body` 中。如果没有附加到 `Context.body`,Elysia 将继续迭代 `onParse` 指定的其他解析器函数,直到请求体被赋值或所有解析器都执行完毕。 ¥A function to parse body, the return value will be append to `Context.body`, if not, Elysia will continue iterating through additional parser functions assigned by `onParse` until either body is assigned or all parsers have been executed. 默认情况下,Elysia 将解析内容类型为以下类型的主体: ¥By default, Elysia will parse the body with content-type of: * `text/plain` * `application/json` * `multipart/form-data` * `application/x-www-form-urlencoded` 建议使用 `onParse` 事件来提供 Elysia 未提供的自定义 body 解析器。 ¥It's recommended to use the `onParse` event to provide a custom body parser that Elysia doesn't provide. #### 示例 {#example-1} ¥Example 以下是根据自定义标头检索值的示例代码。 ¥Below is an example code to retrieve value based on custom headers. ```typescript import { Elysia } from 'elysia' new Elysia().onParse(({ request, contentType }) => { if (contentType === 'application/custom-type') return request.text() }) ``` 返回值将分配给 `Context.body`。如果没有,Elysia 将继续从 onParse 堆栈中迭代其他解析器函数,直到分配主体或执行完所有解析器为止。 ¥The returned value will be assigned to `Context.body`. If not, Elysia will continue iterating through additional parser functions from **onParse** stack until either body is assigned or all parsers have been executed. ### 上下文 {#context} ¥Context `onParse` Context 扩展自 `Context`,并具有以下附加属性: ¥`onParse` Context is extends from `Context` with additional properties of the following: * contentType:请求的 Content-Type 标头 所有上下文都基于普通上下文,并且可以像路由处理程序中的普通上下文一样使用。 ¥All of the context is based on normal context and can be used like normal context in route handler. ### 解析器 {#parser} ¥Parser 默认情况下,Elysia 将尝试提前确定主体解析函数,并选择最合适的函数以加快解析速度。 ¥By default, Elysia will try to determine body parsing function ahead of time and pick the most suitable function to speed up the process. Elysia 能够通过读取 `body` 来判断主体机能。 ¥Elysia is able to determine that body function by reading `body`. 请看以下示例: ¥Take a look at this example: ```typescript import { Elysia, t } from 'elysia' new Elysia().post('/', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }) }) ``` Elysia 读取了正文的 schema,发现其类型完全是一个对象,因此正文很可能是 JSON 格式。Elysia 随后会提前选择 JSON 正文解析器函数并尝试解析正文。 ¥Elysia read the body schema and found that, the type is entirely an object, so it's likely that the body will be JSON. Elysia then picks the JSON body parser function ahead of time and tries to parse the body. 以下是 Elysia 用于选择 body 解析器类型的标准 ¥Here's a criteria that Elysia uses to pick up type of body parser * `application/json`:主体类型为 `t.Object` * `multipart/form-data`:body 类型为 `t.Object`,并且与 `t.File` 深度相同。 * `application/x-www-form-urlencoded`:主体类型为 `t.URLEncoded` * `text/plain`:其他原始类型 这允许 Elysia 提前优化主体解析器,并减少编译时的开销。 ¥This allows Elysia to optimize body parser ahead of time, and reduce overhead in compile time. ### 显式解析器 {#explicit-parser} ¥Explicit Parser 但是,在某些情况下,如果 Elysia 无法选择正确的主体解析器函数,我们可以通过指定 `type` 明确告诉 Elysia 使用某个函数。 ¥However, in some scenario if Elysia fails to pick the correct body parser function, we can explicitly tell Elysia to use a certain function by specifying `type`. ```typescript import { Elysia } from 'elysia' new Elysia().post('/', ({ body }) => body, { // Short form of application/json parse: 'json' }) ``` 这使我们能够控制 Elysia 的行为,以便在复杂场景中选择符合我们需求的 body 解析器函数。 ¥This allows us to control Elysia behavior for picking body parser function to fit our needs in a complex scenario. `type` 可能是以下之一: ¥`type` may be one of the following: ```typescript type ContentType = | // Shorthand for 'text/plain' | 'text' // Shorthand for 'application/json' | 'json' // Shorthand for 'multipart/form-data' | 'formdata' // Shorthand for 'application/x-www-form-urlencoded' | 'urlencoded' // Skip body parsing entirely | 'none' | 'text/plain' | 'application/json' | 'multipart/form-data' | 'application/x-www-form-urlencoded' ``` ### 跳过正文解析 {#skip-body-parsing} ¥Skip Body Parsing 当你需要将第三方库与 HTTP 处理程序(例如 `trpc`、`orpc`)集成时,它会抛出 `Body is already used` 异常。 ¥When you need to integrate a third-party library with HTTP handler like `trpc`, `orpc`, and it throw `Body is already used`. 这是因为 Web 标准请求只能被解析一次。 ¥This is because Web Standard Request can be parsed only once. Elysia 和第三方库都有自己的 body 解析器,因此你可以通过指定 `parse: 'none'` 跳过 Elysia 端的 body 解析。 ¥Both Elysia and the third-party library both has its own body parser, so you can skip body parsing on Elysia side by specifying `parse: 'none'` ```typescript import { Elysia } from 'elysia' new Elysia() .post( '/', ({ request }) => library.handle(request), { parse: 'none' } ) ``` ### 自定义解析器 {#custom-parser} ¥Custom Parser 你可以使用 `parser` 注册自定义解析器: ¥You can provide register a custom parser with `parser`: ```typescript import { Elysia } from 'elysia' new Elysia() .parser('custom', ({ request, contentType }) => { if (contentType === 'application/elysia') return request.text() }) .post('/', ({ body }) => body, { parse: ['custom', 'json'] }) ``` ## 转换 {#transform} ¥Transform 在验证过程之前立即执行,旨在改变上下文以符合验证或附加新值。 ¥Executed just before **Validation** process, designed to mutate context to conform with the validation or appending new value. 建议在以下情况下使用 transform: ¥It's recommended to use transform for the following: * 修改现有上下文以符合验证要求。 * `derive` 基于 `onTransform`,但支持提供类型。 #### 示例 {#example-2} ¥Example 以下是使用 transform 将参数转换为数值的示例。 ¥Below is an example of using transform to mutate params to be numeric values. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }), transform({ params }) { const id = +params.id if (!Number.isNaN(id)) params.id = id } }) .listen(3000) ``` ## 派生 {#derive} ¥Derive 在验证之前直接将新值附加到上下文。它与 transform 存储在同一个堆栈中。 ¥Append new value to context directly **before validation**. It's stored in the same stack as **transform**. 与 state 和 decorate 不同,derive 会在服务器启动前赋值。derive 在每个请求发生时分配一个属性。这使我们能够将一条信息提取到属性中。 ¥Unlike **state** and **decorate** that assigned value before the server started. **derive** assigns a property when each request happens. This allows us to extract a piece of information into a property instead. ```typescript import { Elysia } from 'elysia' new Elysia() .derive(({ headers }) => { const auth = headers['Authorization'] return { bearer: auth?.startsWith('Bearer ') ? auth.slice(7) : null } }) .get('/', ({ bearer }) => bearer) ``` 因为 derive 在新请求启动后才会赋值,所以 derive 可以访问请求属性,例如 headers、query、body,而 store 和 decorate 则不能。 ¥Because **derive** is assigned once a new request starts, **derive** can access Request properties like **headers**, **query**, **body** where **store**, and **decorate** can't. 与状态不同,并进行装饰。由 derive 分配的属性是唯一的,不会与其他请求共享。 ¥Unlike **state**, and **decorate**. Properties which assigned by **derive** is unique and not shared with another request. ### 队列 {#queue} ¥Queue `derive` 和 `transform` 存储在同一个队列中。 ¥`derive` and `transform` are stored in the same queue. ```typescript import { Elysia } from 'elysia' new Elysia() .onTransform(() => { console.log(1) }) .derive(() => { console.log(2) return {} }) ``` 控制台应打印以下内容: ¥The console should log as the following: ```bash 1 2 ``` ## 处理前 {#before-handle} ¥Before Handle 在验证之后、主路由处理程序之前执行。 ¥Execute after validation and before the main route handler. 设计旨在在运行主处理程序之前提供自定义验证以满足特定需求。 ¥Designed to provide a custom validation to provide a specific requirement before running the main handler. 如果返回了一个值,路由处理程序将被跳过。 ¥If a value is returned, the route handler will be skipped. 建议在以下情况下使用 Before Handle: ¥It's recommended to use Before Handle in the following situations: * 限制访问检查:授权,用户登录 * 自定义数据结构请求要求 #### 示例 {#example-3} ¥Example 以下是使用 before 句柄检查用户登录的示例。 ¥Below is an example of using the before handle to check for user sign-in. ```typescript import { Elysia } from 'elysia' import { validateSession } from './user' new Elysia() .get('/', () => 'hi', { beforeHandle({ set, cookie: { session }, status }) { if (!validateSession(session.value)) return status(401) } }) .listen(3000) ``` 响应应如下所示: ¥The response should be listed as follows: | 已登录 | 响应 | | --- | ---- | | ❌ | 未经授权 | | ✅ | 你好 | ### 守护 {#guard} ¥Guard 当我们需要将相同的 before 句柄应用于多个路由时,可以使用 `guard` 将相同的 before 句柄应用于多个路由。 ¥When we need to apply the same before handle to multiple routes, we can use `guard` to apply the same before handle to multiple routes. ```typescript import { Elysia } from 'elysia' import { signUp, signIn, validateSession, isUserExists } from './user' new Elysia() .guard( { beforeHandle({ set, cookie: { session }, status }) { if (!validateSession(session.value)) return status(401) } }, (app) => app .get('/user/:id', ({ body }) => signUp(body)) .post('/profile', ({ body }) => signIn(body), { beforeHandle: isUserExists }) ) .get('/', () => 'hi') .listen(3000) ``` ## 解析 {#resolve} ¥Resolve 验证后将新值附加到上下文。它与 beforeHandle 存储在同一个堆栈中。 ¥Append new value to context **after validation**. It's stored in the same stack as **beforeHandle**. 解析语法与 [derive](#derive) 相同,以下是从授权插件中检索承载标头的示例。 ¥Resolve syntax is identical to [derive](#derive), below is an example of retrieving a bearer header from the Authorization plugin. ```typescript import { Elysia, t } from 'elysia' new Elysia() .guard( { headers: t.Object({ authorization: t.TemplateLiteral('Bearer ${string}') }) }, (app) => app .resolve(({ headers: { authorization } }) => { return { bearer: authorization.split(' ')[1] } }) .get('/', ({ bearer }) => bearer) ) .listen(3000) ``` 使用 `resolve` 和 `onBeforeHandle` 存储在同一个队列中。 ¥Using `resolve` and `onBeforeHandle` is stored in the same queue. ```typescript import { Elysia } from 'elysia' new Elysia() .onBeforeHandle(() => { console.log(1) }) .resolve(() => { console.log(2) return {} }) .onBeforeHandle(() => { console.log(3) }) ``` 控制台应打印以下内容: ¥The console should log as the following: ```bash 1 2 3 ``` 与 derive 相同,由 resolve 分配的属性是唯一的,不会与其他请求共享。 ¥Same as **derive**, properties which assigned by **resolve** is unique and not shared with another request. ### 守护解析 {#guard-resolve} ¥Guard resolve 由于 resolve 在本地钩子中不可用,建议使用 guard 封装 resolve 事件。 ¥As resolve is not available in local hook, it's recommended to use guard to encapsulate the **resolve** event. ```typescript import { Elysia } from 'elysia' import { isSignIn, findUserById } from './user' new Elysia() .guard( { beforeHandle: isSignIn }, (app) => app .resolve(({ cookie: { session } }) => ({ userId: findUserById(session.value) })) .get('/profile', ({ userId }) => userId) ) .listen(3000) ``` ## 处理后 {#after-handle} ¥After Handle 在主处理程序之后执行,用于将前处理程序和路由处理程序的返回值映射到正确的响应中。 ¥Execute after the main handler, for mapping a returned value of **before handle** and **route handler** into a proper response. 建议在以下情况下使用 After Handle: ¥It's recommended to use After Handle in the following situations: * 将请求转换为新值,例如压缩、事件流 * 根据响应值添加自定义标头,例如。内容类型 #### 示例 {#example-4} ¥Example 以下是使用 after 句柄将 HTML 内容类型添加到响应标头的示例。 ¥Below is an example of using the after handle to add HTML content type to response headers. ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) set.headers['content-type'] = 'text/html; charset=utf8' } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` 响应应如下所示: ¥The response should be listed as follows: | 路径 | 内容类型 | | --- | ----------------------- | | / | text/html;charset=utf8 | | /hi | text/plain;charset=utf8 | ### 返回值 {#returned-value} ¥Returned Value 如果返回值,After Handle 将使用返回值作为新的响应值,除非该值未定义。 ¥If a value is returned After Handle will use a return value as a new response value unless the value is **undefined** 以上示例可以改写如下: ¥The above example could be rewritten as the following: ```typescript import { Elysia } from 'elysia' import { isHtml } from '@elysiajs/html' new Elysia() .get('/', () => '

Hello World

', { afterHandle({ response, set }) { if (isHtml(response)) { set.headers['content-type'] = 'text/html; charset=utf8' return new Response(response) } } }) .get('/hi', () => '

Hello World

') .listen(3000) ``` 与 beforeHandle 不同,afterHandle 返回值后,不会跳过 afterHandle 的迭代。 ¥Unlike **beforeHandle**, after a value is returned from **afterHandle**, the iteration of afterHandle **will **NOT** be skipped.** ### 上下文 {#context-1} ¥Context `onAfterHandle` 上下文扩展自 `Context`,并添加了 `response` 属性,即返回给客户端的响应。 ¥`onAfterHandle` context extends from `Context` with the additional property of `response`, which is the response to return to the client. `onAfterHandle` 上下文基于普通上下文,可以像普通上下文一样在路由处理程序中使用。 ¥The `onAfterHandle` context is based on the normal context and can be used like the normal context in route handlers. ## 映射响应 {#map-response} ¥Map Response 在 "afterHandle" 之后立即执行,旨在提供自定义响应映射。 ¥Executed just after **"afterHandle"**, designed to provide custom response mapping. 建议在以下情况下使用 transform: ¥It's recommended to use transform for the following: * 压缩 * 将值映射到 Web 标准响应中 #### 示例 {#example-5} ¥Example 以下是使用 mapResponse 提供响应压缩的示例。 ¥Below is an example of using mapResponse to provide Response compression. ```typescript import { Elysia } from 'elysia' const encoder = new TextEncoder() new Elysia() .mapResponse(({ responseValue, set }) => { const isJson = typeof response === 'object' const text = isJson ? JSON.stringify(responseValue) : (responseValue?.toString() ?? '') set.headers['Content-Encoding'] = 'gzip' return new Response(Bun.gzipSync(encoder.encode(text)), { headers: { 'Content-Type': `${ isJson ? 'application/json' : 'text/plain' }; charset=utf-8` } }) }) .get('/text', () => 'mapResponse') .get('/json', () => ({ map: 'response' })) .listen(3000) ``` 类似 parse 和 beforeHandle,在返回值后,mapResponse 的下一次迭代将被跳过。 ¥Like **parse** and **beforeHandle**, after a value is returned, the next iteration of **mapResponse** will be skipped. Elysia 将自动处理 mapResponse 中 set.headers 的合并过程。我们无需担心手动将 set.headers 附加到 Response。 ¥Elysia will handle the merging process of **set.headers** from **mapResponse** automatically. We don't need to worry about appending **set.headers** to Response manually. ## 错误处理(错误处理) {#on-error-error-handling} ¥On Error (Error Handling) 专为错误处理而设计。当任何生命周期中抛出错误时,都会执行此函数。 ¥Designed for error handling. It will be executed when an error is thrown in any lifecycle. 建议在以下情况下使用 on Error: ¥It's recommended to use on Error in the following situations: * 提供自定义错误信息 * 故障安全处理、错误处理程序或重试请求 * 日志记录和分析 #### 示例 {#example-6} ¥Example Elysia 捕获处理程序中抛出的所有错误,对错误代码进行分类,然后将其通过管道传输到 `onError` 中间件。 ¥Elysia catches all the errors thrown in the handler, classifies the error code, and pipes them to `onError` middleware. ```typescript import { Elysia } from 'elysia' new Elysia() .onError(({ code, error }) => { return new Response(error.toString()) }) .get('/', () => { throw new Error('Server is during maintenance') return 'unreachable' }) ``` 通过 `onError`,我们可以捕获错误并将其转换为自定义错误消息。 ¥With `onError` we can catch and transform the error into a custom error message. ::: tip 提示 重要的是,必须在我们要应用它的处理程序之前调用 `onError`。 ¥It's important that `onError` must be called before the handler we want to apply it to. ::: ### 自定义 404 消息 {#custom-404-message} ¥Custom 404 message 例如,返回自定义 404 消息: ¥For example, returning custom 404 messages: ```typescript import { Elysia, NotFoundError } from 'elysia' new Elysia() .onError(({ code, status, set }) => { if (code === 'NOT_FOUND') return status(404, 'Not Found :(') }) .post('/', () => { throw new NotFoundError() }) .listen(3000) ``` ### 上下文 {#context-2} ¥Context `onError` Context 扩展自 `Context`,并具有以下附加属性: ¥`onError` Context is extends from `Context` with additional properties of the following: * 错误:抛出的值 * 代码:错误代码 ### 错误代码 {#error-code} ¥Error Code Elysia 错误代码包含以下内容: ¥Elysia error code consists of: * **NOT\_FOUND** * **PARSE** * **VALIDATION** * **INTERNAL\_SERVER\_ERROR** * **INVALID\_COOKIE\_SIGNATURE** * **INVALID\_FILE\_TYPE** * **UNKNOWN** * 数字(基于 HTTP 状态) 默认情况下,抛出的错误代码为 `UNKNOWN`。 ¥By default, the thrown error code is `UNKNOWN`. ::: tip 提示 如果没有返回错误响应,则将使用 `error.name` 返回错误。 ¥If no error response is returned, the error will be returned using `error.name`. ::: ### 本地错误 {#local-error} ¥Local Error 与其他生命周期相同,我们使用 guard 向 [scope](/essential/plugin.html#scope) 中提供一个错误: ¥Same as others life-cycle, we provide an error into an [scope](/essential/plugin.html#scope) using guard: ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', () => 'Hello', { beforeHandle({ set, request: { headers }, error }) { if (!isSignIn(headers)) throw error(401) }, error({ error }) { return 'Handled' } }) .listen(3000) ``` ## 响应后 {#after-response} ¥After Response 在响应发送给客户端之后执行。 ¥Executed after the response sent to the client. 建议在以下情况下使用 After Response: ¥It's recommended to use **After Response** in the following situations: * 清理响应 * 日志记录和分析 #### 示例 {#example-7} ¥Example 以下是使用响应句柄检查用户登录的示例。 ¥Below is an example of using the response handle to check for user sign-in. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(() => { console.log('Response', performance.now()) }) .listen(3000) ``` 控制台应记录以下内容: ¥Console should log as the following: ```bash Response 0.0000 Response 0.0001 Response 0.0002 ``` ### 响应 {#response} ¥Response 与 [映射响应](#map-resonse) 类似,`afterResponse` 也接受 `responseValue` 值。 ¥Similar to [Map Response](#map-resonse), `afterResponse` also accept a `responseValue` value. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(({ responseValue }) => { console.log(responseValue) }) .get('/', () => 'Hello') .listen(3000) ``` `onAfterResponse` 中的 `response` 不是 Web 标准的 `Response`,而是从处理程序返回的值。 ¥`response` from `onAfterResponse`, is not a Web-Standard's `Response` but is a value that is returned from the handler. 要获取标头和处理程序返回的状态,我们可以从上下文访问 `set`。 ¥To get a headers, and status returned from the handler, we can access `set` from the context. ```typescript import { Elysia } from 'elysia' new Elysia() .onAfterResponse(({ set }) => { console.log(set.status, set.headers) }) .get('/', () => 'Hello') .listen(3000) ``` --- --- url: 'https://elysiajs.com/eden/overview.md' --- # 端到端类型安全 {#end-to-end-type-safety} ¥End-to-End Type Safety 假设你有一套玩具火车。 ¥Imagine you have a toy train set. 火车轨道的每一部分都必须与下一部分完美契合,就像拼图碎片一样。 ¥Each piece of the train track has to fit perfectly with the next one, like puzzle pieces. 端到端类型安全就像确保轨道的所有部分都正确匹配,这样火车就不会掉下来或卡住。 ¥End-to-end type safety is like making sure all the pieces of the track match up correctly so the train doesn't fall off or get stuck. 对于一个框架来说,拥有端到端类型安全意味着你可以以类型安全的方式连接客户端和服务器。 ¥For a framework to have end-to-end type safety means you can connect client and server in a type-safe manner. Elysia 提供了一个类似 RPC 的连接器 Eden,无需生成代码即可提供端到端的类型安全。 ¥Elysia provides end-to-end type safety **without code generation** out of the box with an RPC-like connector, **Eden** 其他支持端到端类型安全的框架: ¥Other frameworks that support e2e type safety: * tRPC * Remix * SvelteKit * Nuxt * TS-Rest Elysia 允许你在服务器上更改类型,并且更改会立即反映在客户端,从而有助于自动补齐和类型强制执行。 ¥Elysia allows you to change the type on the server and it will be instantly reflected on the client, helping with auto-completion and type-enforcement. ## Eden {#eden} Eden 是一个类似 RPC 的客户端,它仅使用 TypeScript 的类型推断(而非代码生成)即可实现 Elysia 的端到端类型安全连接。 ¥Eden is an RPC-like client to connect Elysia with **end-to-end type safety** using only TypeScript's type inference instead of code generation. 它允许你轻松同步客户端和服务器类型,大小不到 2KB。 ¥It allows you to sync client and server types effortlessly, weighing less than 2KB. Eden 包含两个模块: ¥Eden consists of 2 modules: 1. Eden 条约(推荐):一个改进版的 Eden Treaty RFC 版本 2. Eden 获取:具有类型安全性的类似 Fetch 的客户端 以下是每个模块的概述、用例和比较。 ¥Below is an overview, use-case and comparison for each module. ## Eden 条约(推荐) {#eden-treaty-recommended} ¥Eden Treaty (Recommended) Eden Treaty 是一个 Elysia 服务器的对象表示,提供端到端的类型安全,并显著提升开发者体验。 ¥Eden Treaty is an object-like representation of an Elysia server providing end-to-end type safety and a significantly improved developer experience. 借助 Eden Treaty,我们可以与具有完整类型支持和自动补齐功能的 Elysia 服务器交互,使用类型收缩进行错误处理,并创建类型安全的单元测试。 ¥With Eden Treaty we can interact with an Elysia server with full-type support and auto-completion, error handling with type narrowing, and create type-safe unit tests. Eden 条约 (Eden Treaty) 用法示例: ¥Example usage of Eden Treaty: ```typescript twoslash // @filename: server.ts import { Elysia, t } from 'elysia' const app = new Elysia() .get('/', 'hi') .get('/users', () => 'Skadi') .put('/nendoroid/:id', ({ body }) => body, { body: t.Object({ name: t.String(), from: t.String() }) }) .get('/nendoroid/:id/name', () => 'Skadi') .listen(3000) export type App = typeof app // @filename: index.ts // ---cut--- import { treaty } from '@elysiajs/eden' import type { App } from './server' const app = treaty('localhost:3000') // @noErrors app. // ^| // Call [GET] at '/' const { data } = await app.get() // Call [PUT] at '/nendoroid/:id' const { data: nendoroid, error } = await app.nendoroid({ id: 1895 }).put({ name: 'Skadi', from: 'Arknights' }) ``` ## Eden 获取 {#eden-fetch} ¥Eden Fetch 对于喜欢使用 Fetch 语法的开发者来说,这是一个类似于 Eden Treaty 的替代方案。 ¥A fetch-like alternative to Eden Treaty for developers that prefers fetch syntax. ```typescript import { edenFetch } from '@elysiajs/eden' import type { App } from './server' const fetch = edenFetch('http://localhost:3000') const { data } = await fetch('/name/:name', { method: 'POST', params: { name: 'Saori' }, body: { branch: 'Arius', type: 'Striker' } }) ``` ::: tip 注意 与 Eden Treaty 不同,Eden Fetch 不为 Elysia 服务器提供 Web Socket 实现。 ¥Unlike Eden Treaty, Eden Fetch doesn't provide Web Socket implementation for Elysia server. ::: --- --- url: 'https://elysiajs.com/patterns/type.md' --- # 类型 {#type} ¥Type 以下是使用 `Elysia.t` 编写验证类型的常见模式。 ¥Here's a common patterns for writing validation types using `Elysia.t`. ## 原始类型 {#primitive-type} ¥Primitive Type TypeBox API 的设计围绕 TypeScript 类型,并且与 TypeScript 类型类似。 ¥The TypeBox API is designed around and is similar to TypeScript types. 有很多熟悉的名称和行为与 TypeScript 对应项相交叉,例如 String、Number、Boolean 和 Object,以及更高级的功能,例如 Intersect、KeyOf 和 Tuple,以实现多功能性。 ¥There are many familiar names and behaviors that intersect with TypeScript counterparts, such as **String**, **Number**, **Boolean**, and **Object**, as well as more advanced features like **Intersect**, **KeyOf**, and **Tuple** for versatility. 如果你熟悉 TypeScript,那么创建 TypeBox 模式的行为与编写 TypeScript 类型相同,只是它在运行时提供实际的类型验证。 ¥If you are familiar with TypeScript, creating a TypeBox schema behaves the same as writing a TypeScript type, except it provides actual type validation at runtime. 要创建你的第一个 schema,请从 Elysia 导入 Elysia.t 并从最基本的类型开始: ¥To create your first schema, import **Elysia.t** from Elysia and start with the most basic type: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/', ({ body }) => `Hello ${body}`, { body: t.String() }) .listen(3000) ``` 此代码告诉 Elysia 验证传入的 HTTP 正文,确保正文为字符串。如果是字符串,则允许其流经请求管道和处理程序。 ¥This code tells Elysia to validate an incoming HTTP body, ensuring that the body is a string. If it is a string, it will be allowed to flow through the request pipeline and handler. 如果形状不匹配,它将向 [错误生命周期](/essential/life-cycle.html#on-error) 抛出错误。 ¥If the shape doesn't match, it will throw an error into the [Error Life Cycle](/essential/life-cycle.html#on-error). ![Elysia Life Cycle](/assets/lifecycle-chart.svg) ### 基本类型 {#basic-type} ¥Basic Type TypeBox 提供与 TypeScript 类型具有相同行为的基本原始类型。 ¥TypeBox provides basic primitive types with the same behavior as TypeScript types. 下表列出了最常见的基本类型: ¥The following table lists the most common basic types: ```typescript t.String() ``` ```typescript string ``` ```typescript t.Number() ``` ```typescript number ``` ```typescript t.Boolean() ``` ```typescript boolean ``` ```typescript t.Array( t.Number() ) ``` ```typescript number[] ``` ```typescript t.Object({ x: t.Number() }) ``` ```typescript { x: number } ``` ```typescript t.Null() ``` ```typescript null ``` ```typescript t.Literal(42) ``` ```typescript 42 ``` Elysia 扩展了 TypeBox 的所有类型,允许你在 Elysia 中使用 TypeBox 的大部分 API。 ¥Elysia extends all types from TypeBox, allowing you to reference most of the API from TypeBox for use in Elysia. 有关 TypeBox 支持的其他类型,请参阅 [TypeBox 的类型](https://github.com/sinclairzx81/typebox#json-types)。 ¥See [TypeBox's Type](https://github.com/sinclairzx81/typebox#json-types) for additional types supported by TypeBox. ### 属性 {#attribute} ¥Attribute TypeBox 可以接受基于 JSON Schema 7 规范的参数,以实现更全面的行为。 ¥TypeBox can accept arguments for more comprehensive behavior based on the JSON Schema 7 specification. ```typescript t.String({ format: 'email' }) ``` ```typescript saltyaom@elysiajs.com ``` ```typescript t.Number({ minimum: 10, maximum: 100 }) ``` ```typescript 10 ``` ```typescript t.Array( t.Number(), { /** * Minimum number of items */ minItems: 1, /** * Maximum number of items */ maxItems: 5 } ) ``` ```typescript [1, 2, 3, 4, 5] ``` ```typescript t.Object( { x: t.Number() }, { /** * @default false * Accept additional properties * that not specified in schema * but still match the type */ additionalProperties: true } ) ``` ```typescript x: 100 y: 200 ``` 有关每个属性的更多解释,请参阅 [JSON Schema 7 规范](https://json-schema.org/draft/2020-12/json-schema-validation)。 ¥See [JSON Schema 7 specification](https://json-schema.org/draft/2020-12/json-schema-validation) for more explanation of each attribute. ## 荣誉提名 {#honorable-mentions} ¥Honorable Mentions 以下是在创建架构时经常发现有用的常见模式。 ¥The following are common patterns often found useful when creating a schema. ### Union {#union} 允许 `t.Object` 中的字段具有多种类型。 ¥Allows a field in `t.Object` to have multiple types. ```typescript t.Union([ t.String(), t.Number() ]) ``` ```typescript string | number ``` ``` Hello 123 ``` ### 可选 {#optional} ¥Optional 允许 `t.Object` 中的字段为未定义或可选。 ¥Allows a field in `t.Object` to be undefined or optional. ```typescript t.Object({ x: t.Number(), y: t.Optional(t.Number()) }) ``` ```typescript { x: number, y?: number } ``` ```typescript { x: 123 } ``` ### 部分 {#partial} ¥Partial 允许 `t.Object` 中的所有字段为可选。 ¥Allows all fields in `t.Object` to be optional. ```typescript t.Partial( t.Object({ x: t.Number(), y: t.Number() }) ) ``` ```typescript { x?: number, y?: number } ``` ```typescript { y: 123 } ``` ## Elysia 类型 {#elysia-type} ¥Elysia Type `Elysia.t` 基于 TypeBox,并预先配置了服务器使用,提供了服务器端验证中常见的其他类型。 ¥`Elysia.t` is based on TypeBox with pre-configuration for server usage, providing additional types commonly found in server-side validation. 你可以在 `elysia/type-system` 中找到 Elysia 类型的所有源代码。 ¥You can find all the source code for Elysia types in `elysia/type-system`. 以下是 Elysia 提供的类型: ¥The following are types provided by Elysia: ### UnionEnum {#unionenum} `UnionEnum` 允许将值设置为指定的值之一。 ¥`UnionEnum` allows the value to be one of the specified values. ```typescript t.UnionEnum(['rapi', 'anis', 1, true, false]) ``` ### 文件 {#file} ¥File 单个文件,通常用于文件上传验证。 ¥A singular file, often useful for **file upload** validation. ```typescript t.File() ``` File 扩展了基础架构的属性,并添加了以下属性: ¥File extends the attributes of the base schema, with additional properties as follows: #### type {#type-1} 指定文件的格式,例如图片、视频或音频。 ¥Specifies the format of the file, such as image, video, or audio. 如果提供的是数组,它将尝试验证任何格式是否有效。 ¥If an array is provided, it will attempt to validate if any of the formats are valid. ```typescript type?: MaybeArray ``` #### minSize {#minsize} 文件的最小大小。 ¥Minimum size of the file. 接受字节数或文件单位后缀: ¥Accepts a number in bytes or a suffix of file units: ```typescript minSize?: number | `${number}${'k' | 'm'}` ``` #### maxSize {#maxsize} 文件的最大大小。 ¥Maximum size of the file. 接受字节数或文件单位后缀: ¥Accepts a number in bytes or a suffix of file units: ```typescript maxSize?: number | `${number}${'k' | 'm'}` ``` #### 文件单元后缀: {#file-unit-suffix} ¥File Unit Suffix: 以下是文件单元的规范:m:兆字节(1048576 字节)k:千字节 (1024 字节) ¥The following are the specifications of the file unit: m: MegaByte (1048576 byte) k: KiloByte (1024 byte) ### 文件 {#files} ¥Files 扩展自 [文件](#file),但增加了对单个字段中文件数组的支持。 ¥Extends from [File](#file), but adds support for an array of files in a single field. ```typescript t.Files() ``` File 扩展了基础架构、数组和 File 的属性。 ¥Files extends the attributes of the base schema, array, and File. ### Cookie {#cookie} 从 Object 类型扩展而来的 Cookie Jar 的对象类表示。 ¥Object-like representation of a Cookie Jar extended from the Object type. ```typescript t.Cookie({ name: t.String() }) ``` Cookie 扩展了 [对象](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-obj) 和 [Cookie](https://github.com/jshttp/cookie#options-1) 的属性,并添加了以下属性: ¥Cookie extends the attributes of [Object](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-obj) and [Cookie](https://github.com/jshttp/cookie#options-1) with additional properties as follows: #### secrets {#secrets} 用于签署 Cookie 的密钥。 ¥The secret key for signing cookies. 接受字符串或字符串数​​组。 ¥Accepts a string or an array of strings. ```typescript secrets?: string | string[] ``` 如果提供的是数组,将使用 [密钥轮换](https://crypto.stackexchange.com/questions/41796/whats-the-purpose-of-key-rotation)。新签名的值将使用第一个密钥作为密钥。 ¥If an array is provided, [Key Rotation](https://crypto.stackexchange.com/questions/41796/whats-the-purpose-of-key-rotation) will be used. The newly signed value will use the first secret as the key. ### 可空值 {#nullable} ¥Nullable 允许值为 null 但不允许为未定义。 ¥Allows the value to be null but not undefined. ```typescript t.Nullable(t.String()) ``` ### MaybeEmpty {#maybeempty} 允许值为 null 和未定义。 ¥Allows the value to be null and undefined. ```typescript t.MaybeEmpty(t.String()) ``` 更多信息,你可以在 [`elysia/type-system`](https://github.com/elysiajs/elysia/blob/main/src/type-system.ts) 中找到类型系统的完整源代码。 ¥For additional information, you can find the full source code of the type system in [`elysia/type-system`](https://github.com/elysiajs/elysia/blob/main/src/type-system.ts). ### 表单 {#form} ¥Form `t.Object` 的语法糖,支持验证 [form](/essential/handler.html#formdata)(FormData)的返回值。 ¥A syntax sugar our `t.Object` with support for verifying return value of [form](/essential/handler.html#formdata) (FormData). ```typescript t.FormData({ someValue: t.File() }) ``` ### UInt8Array {#uint8array} 接受可解析为 `Uint8Array` 的缓冲区。 ¥Accepts a buffer that can be parsed into a `Uint8Array`. ```typescript t.UInt8Array() ``` 当你想要接受可解析为 `Uint8Array` 的缓冲区(例如在二进制文件上传中)时,这很有用。它被设计用于使用 `arrayBuffer` 解析器验证正文,以强制执行正文类型。 ¥This is useful when you want to accept a buffer that can be parsed into a `Uint8Array`, such as in a binary file upload. It's designed to use for the validation of body with `arrayBuffer` parser to enforce the body type. ### ArrayBuffer {#arraybuffer} 接受可解析为 `ArrayBuffer` 的缓冲区。 ¥Accepts a buffer that can be parsed into a `ArrayBuffer`. ```typescript t.ArrayBuffer() ``` 当你想要接受可解析为 `Uint8Array` 的缓冲区(例如在二进制文件上传中)时,这很有用。它被设计用于使用 `arrayBuffer` 解析器验证正文,以强制执行正文类型。 ¥This is useful when you want to accept a buffer that can be parsed into a `Uint8Array`, such as in a binary file upload. It's designed to use for the validation of body with `arrayBuffer` parser to enforce the body type. ### ObjectString {#objectstring} 接受一个可解析为对象的字符串。 ¥Accepts a string that can be parsed into an object. ```typescript t.ObjectString() ``` 当你想要接受一个可以解析为对象但环境不允许的字符串(例如在查询字符串、标头或 FormData 主体中)时,这非常有用。 ¥This is useful when you want to accept a string that can be parsed into an object but the environment does not allow it explicitly, such as in a query string, header, or FormData body. ### BooleanString {#booleanstring} 接受可解析为布尔值的字符串。 ¥Accepts a string that can be parsed into a boolean. 与 [ObjectString](#objectstring) 类似,当你想要接受可解析为布尔值的字符串但环境不允许时,这很有用。 ¥Similar to [ObjectString](#objectstring), this is useful when you want to accept a string that can be parsed into a boolean but the environment does not allow it explicitly. ```typescript t.BooleanString() ``` ### 数字 {#numeric} ¥Numeric Numeric 接受数字字符串或数字,然后将值转换为数字。 ¥Numeric accepts a numeric string or number and then transforms the value into a number. ```typescript t.Numeric() ``` 当传入值是数字字符串(例如路径参数或查询字符串)时,这很有用。 ¥This is useful when an incoming value is a numeric string, for example, a path parameter or query string. Numeric 接受与 [数字实例](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-num) 相同的属性。 ¥Numeric accepts the same attributes as [Numeric Instance](https://json-schema.org/draft/2020-12/json-schema-validation#name-validation-keywords-for-num). ## Elysia 行为 {#elysia-behavior} ¥Elysia behavior Elysia 默认使用 TypeBox。 ¥Elysia use TypeBox by default. 然而,为了帮助简化 HTTP 处理。Elysia 拥有一些专用类型,并且与 TypeBox 的行为存在一些差异。 ¥However, to help making handling with HTTP easier. Elysia has some dedicated type and have some behavior difference from TypeBox. ## 可选 {#optional-1} ¥Optional 要使字段可选,请使用 `t.Optional`。 ¥To make a field optional, use `t.Optional`. 这将允许客户端选择性地提供查询参数。此行为也适用于 `body`、`headers`。 ¥This will allows client to optionally provide a query parameter. This behavior also applied to `body`, `headers`. 这与 TypeBox 不同,TypeBox 中的可选操作是将对象的字段标记为可选。 ¥This is different from TypeBox where optional is to mark a field of object as optional. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/optional', ({ query }) => query, { // ^? query: t.Optional( t.Object({ name: t.String() }) ) }) ``` ## 数字转数字 {#number-to-numeric} ¥Number to Numeric 默认情况下,当路由模式为 `t.Number` 时,Elysia 会将其转换为 [t.Numeric](#numeric)。 ¥By default, Elysia will convert a `t.Number` to [t.Numeric](#numeric) when provided as route schema. 因为解析的 HTTP 标头、查询和 url 参数始终是字符串。这意味着即使一个值是数字,它也会被视为字符串。 ¥Because parsed HTTP headers, query, url parameter is always a string. This means that even if a value is number, it will be treated as string. Elysia 通过检查字符串值是否类似于数字,然后进行适当的转换来覆盖此行为。 ¥Elysia override this behavior by checking if a string value looks like a number then convert it even appropriate. 这仅在用作路由架构时适用,而不是在嵌套的 `t.Object` 中。 ¥This is only applied when it is used as a route schema and not in a nested `t.Object`. ```ts import { Elysia, t } from 'elysia' new Elysia() .get('/:id', ({ id }) => id, { params: t.Object({ // Converted to t.Numeric() id: t.Number() }), body: t.Object({ // NOT converted to t.Numeric() id: t.Number() }) }) // NOT converted to t.Numeric() t.Number() ``` ## 布尔值转布尔字符串 {#boolean-to-booleanstring} ¥Boolean to BooleanString 与 [数字转数字](#number-to-numeric) 类似 ¥Similar to [Number to Numeric](#number-to-numeric) 任何 `t.Boolean` 都将转换为 `t.BooleanString`。 ¥Any `t.Boolean` will be converted to `t.BooleanString`. ```ts import { Elysia, t } from 'elysia' new Elysia() .get('/:id', ({ id }) => id, { params: t.Object({ // Converted to t.Boolean() id: t.Boolean() }), body: t.Object({ // NOT converted to t.Boolean() id: t.Boolean() }) }) // NOT converted to t.BooleanString() t.Boolean() ``` --- --- url: 'https://elysiajs.com/essential/structure.md' --- #### 此页面已移至 [最佳实践](/essential/best-practice) {#this-page-has-been-moved-to-best-practiceessentialbest-practice} ¥This page has been moved to [best practice](/essential/best-practice) # 结构 {#structure} ¥Structure Elysia 是一个模式无关的框架,使用哪种编码模式由你和你的团队自行决定。 ¥Elysia is a pattern-agnostic framework, leaving the decision of which coding patterns to use up to you and your team. 然而,在尝试将 MVC 模式 [(模型-视图-控制器)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) 与 Elysia 结合时,存在一些问题,我们发现很难解耦和处理类型。 ¥However, there are several concerns about trying to adapt an MVC pattern [(Model-View-Controller)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) with Elysia, and we found it's hard to decouple and handle types. 本页是如何遵循 Elysia 结构最佳实践并结合 MVC 模式的指南,但可以适应你喜欢的任何编码模式。 ¥This page is a guide on how to follow Elysia structure best practices combined with MVC pattern but can be adapted to any coding pattern you like. ## 方法链 {#method-chaining} ¥Method Chaining Elysia 代码应始终使用方法链。 ¥Elysia code should always use **method chaining**. 由于 Elysia 的类型系统复杂,Elysia 中的每个方法都会返回一个新的类型引用。 ¥As Elysia's type system is complex, every method in Elysia returns a new type reference. 这对于确保类型完整性和推断非常重要。 ¥**This is important** to ensure type integrity and inference. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .state('build', 1) // Store is strictly typed // [!code ++] .get('/', ({ store: { build } }) => build) .listen(3000) ``` 在上面的代码中,state 返回了一个新的 ElysiaInstance 类型,并添加了一个 `build` 类型。 ¥In the code above **state** returns a new **ElysiaInstance** type, adding a `build` type. ### ❌ 不要做的:不使用方法链式调用 {#-dont-use-without-method-chaining} ¥❌ Don't: Use without method chaining 如果没有方法链,Elysia 不会保存这些新类型,从而导致无法进行类型推断。 ¥Without using method chaining, Elysia doesn't save these new types, leading to no type inference. ```typescript twoslash // @errors: 2339 import { Elysia } from 'elysia' const app = new Elysia() app.state('build', 1) app.get('/', ({ store: { build } }) => build) app.listen(3000) ``` 我们建议始终使用方法链来提供准确的类型推断。 ¥We recommend **always using method chaining** to provide an accurate type inference. ## 控制器 {#controller} ¥Controller > 1 个 Elysia 实例 = 1 个控制器 Elysia 会采取多种措施来确保类型完整性,如果你将整个 `Context` 类型传递给控制器​​,可能会出现以下问题: ¥Elysia does a lot to ensure type integrity, if you pass an entire `Context` type to a controller, these might be the problems: 1. Elysia 类型复杂,严重依赖于插件和多层链接 2. 难以输入,Elysia 类型随时可能更改,尤其是在使用装饰器和存储时。 3. 类型转换可能会导致类型完整性受损,或无法确保类型和运行时代码之间的一致性。 4. 这使得 [Sucrose](/blog/elysia-10#sucrose)(Elysia 的 "种类" 编译器)静态分析你的代码更具挑战性。 ### ❌ 不要做的:创建单独的控制器 {#-dont-create-a-separate-controller} ¥❌ Don't: Create a separate controller 不要创建单独的控制器,而是使用 Elysia 本身作为控制器: ¥Don't create a separate controller, use Elysia itself as a controller instead: ```typescript import { Elysia, t, type Context } from 'elysia' abstract class Controller { static root(context: Context) { return Service.doStuff(context.stuff) } } // ❌ Don't new Elysia() .get('/', Controller.hi) ``` 将整个 `Controller.method` 传递给 Elysia 相当于让两个控制器来回传递数据。这违背了框架和 MVC 模式本身的设计。 ¥Passing an entire `Controller.method` to Elysia is equivalent to having 2 controllers passing data back and forth. It's against the design of the framework and MVC pattern itself. ### ✅ 应该做的:使用 Elysia 作为控制器 {#-do-use-elysia-as-a-controller} ¥✅ Do: Use Elysia as a controller 相反,将 Elysia 实例本身视为控制器。 ¥Instead, treat an Elysia instance as a controller itself. ```typescript import { Elysia } from 'elysia' import { Service } from './service' new Elysia() .get('/', ({ stuff }) => { Service.doStuff(stuff) }) ``` ## 服务 {#service} ¥Service 服务是一组实用程序/辅助函数,它们被解耦为业务逻辑,用于模块/控制器(在本例中为 Elysia 实例)。 ¥Service is a set of utility/helper functions decoupled as a business logic to use in a module/controller, in our case, an Elysia instance. 任何可以与控制器解耦的技术逻辑都可以存在于服务中。 ¥Any technical logic that can be decoupled from controller may live inside a **Service**. Elysia 中有两种类型的服务: ¥There're 2 types of service in Elysia: 1. 非请求依赖服务 2. 请求依赖服务 ### ✅ 应该做的:非请求依赖服务 {#-do-non-request-dependent-service} ¥✅ Do: Non-request dependent service 此类服务无需访问请求或 `Context` 中的任何属性,可以像通常的 MVC 服务模式一样以静态类的形式启动。 ¥This kind of service doesn't need to access any property from the request or `Context`, and can be initiated as a static class same as usual MVC service pattern. ```typescript import { Elysia, t } from 'elysia' abstract class Service { static fibo(number: number): number { if(number < 2) return number return Service.fibo(number - 1) + Service.fibo(number - 2) } } new Elysia() .get('/fibo', ({ body }) => { return Service.fibo(body) }, { body: t.Numeric() }) ``` 如果你的服务不需要存储属性,你可以改用 `abstract class` 和 `static` 来避免分配类实例。 ¥If your service doesn't need to store a property, you may use `abstract class` and `static` instead to avoid allocating class instance. ### 请求依赖服务 {#request-dependent-service} ¥Request Dependent Service 此类服务可能需要请求中的某些属性,因此应以 Elysia 实例的形式启动。 ¥This kind of service may require some property from the request, and should be **initiated as an Elysia instance**. ### ❌ 不要做的:将整个 `Context` 传递给服务 {#-dont-pass-entire-context-to-a-service} ¥❌ Don't: Pass entire `Context` to a service Context 是一种高度动态的类型,可以从 Elysia 实例推断出来。 ¥**Context is a highly dynamic type** that can be inferred from Elysia instance. 不要将整个 `Context` 传递给服务,而是使用对象解构提取所需内容并将其传递给服务。 ¥Do not pass an entire `Context` to a service, instead use object destructuring to extract what you need and pass it to the service. ```typescript import type { Context } from 'elysia' class AuthService { constructor() {} // ❌ Don't do this isSignIn({ status, cookie: { session } }: Context) { if (session.value) return status(401) } } ``` 由于 Elysia 类型复杂,并且严重依赖于插件和多层级的链接,因此由于其高度动态,手动输入类型可能具有挑战性。 ¥As Elysia type is complex, and heavily depends on plugin and multiple level of chaining, it can be challenging to manually type as it's highly dynamic. ### ✅ 应该做的:将依赖服务作为 Elysia 实例请求 {#-do-request-dependent-service-as-elysia-instance} ¥✅ Do: Request dependent service as Elysia instance 我们建议从 Elysia 中抽象出一个服务类。 ¥We recommend abstracting service classes away from Elysia. 但是,如果该服务依赖于请求或需要处理 HTTP 请求,我们建议将其抽象为 Elysia 实例,以确保类型完整性和类型推断: ¥However, **if the service is a request dependent service** or needs to process HTTP requests, ee recommend abstracting it as an Elysia instance to ensure type integrity and inference: ```typescript import { Elysia } from 'elysia' // ✅ Do const AuthService = new Elysia({ name: 'Service.Auth' }) .derive({ as: 'scoped' }, ({ cookie: { session } }) => ({ // This is equivalent to dependency injection Auth: { user: session.value } })) .macro(({ onBeforeHandle }) => ({ // This is declaring a service method isSignIn(value: boolean) { onBeforeHandle(({ Auth, status }) => { if (!Auth?.user || !Auth.user) return status(401) }) } })) const UserController = new Elysia() .use(AuthService) .get('/profile', ({ Auth: { user } }) => user, { isSignIn: true }) ``` ::: tip 提示 Elysia 默认处理 [插件数据去重](/essential/plugin.html#plugin-deduplication) 属性,因此你无需担心性能问题,因为如果你指定了 "name" 属性,它将是单例模式。 ¥Elysia handle [plugin deduplication](/essential/plugin.html#plugin-deduplication) by default so you don't have to worry about performance, as it's going to be Singleton if you specified a **"name"** property. ::: ### ⚠️ 从 Elysia 实例推断上下文 {#-infers-context-from-elysia-instance} ¥⚠️ Infers Context from Elysia instance 如果绝对必要,你可以从 Elysia 实例本身推断 `Context` 类型: ¥If **absolutely necessary**, you may infer the `Context` type from the Elysia instance itself: ```typescript import { Elysia, type InferContext } from 'elysia' const setup = new Elysia() .state('a', 'a') .decorate('b', 'b') class AuthService { constructor() {} // ✅ Do isSignIn({ status, cookie: { session } }: InferContext) { if (session.value) return status(401) } } ``` 但是,我们建议尽可能避免这种情况,而改用 [Elysia 即服务](✅-do-use-elysia-instance-as-a-service)。 ¥However, we recommend avoiding this if possible, and using [Elysia as a service](✅-do-use-elysia-instance-as-a-service) instead. 你可以在 [必备:处理程序](/essential/handler) 中了解有关 [InferContext](/essential/handler#infercontext) 的更多信息。 ¥You can learn more about [InferContext](/essential/handler#infercontext) in [Essential: Handler](/essential/handler). ## 模型 {#model} ¥Model 模型或 [DTO(数据传输对象)](https://en.wikipedia.org/wiki/Data_transfer_object) 由 [Elysia.t (验证)](/validation/overview.html#data-validation) 处理。 ¥Model or [DTO (Data Transfer Object)](https://en.wikipedia.org/wiki/Data_transfer_object) is handle by [Elysia.t (Validation)](/validation/overview.html#data-validation). Elysia 内置了验证系统,可以从代码中推断类型并在运行时进行验证。 ¥Elysia has a validation system built-in which can infers type from your code and validate it at runtime. ### ❌ 不要做的:将类实例声明为模型 {#-dont-declare-a-class-instance-as-a-model} ¥❌ Don't: Declare a class instance as a model 不要将类实例声明为模型: ¥Do not declare a class instance as a model: ```typescript // ❌ Don't class CustomBody { username: string password: string constructor(username: string, password: string) { this.username = username this.password = password } } // ❌ Don't interface ICustomBody { username: string password: string } ``` ### ✅ 应该做的:使用 Elysia 的验证系统 {#-do-use-elysias-validation-system} ¥✅ Do: Use Elysia's validation system 与其声明类或接口,不如使用 Elysia 的验证系统来定义模型: ¥Instead of declaring a class or interface, use Elysia's validation system to define a model: ```typescript twoslash // ✅ Do import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // Optional if you want to get the type of the model // Usually if we didn't use the type, as it's already inferred by Elysia type CustomBody = typeof customBody.static // ^? export { customBody } ``` 我们可以通过使用模型中的 `typeof` 和 `.static` 属性来获取模型的类型。 ¥We can get type of model by using `typeof` with `.static` property from the model. 然后你可以使用 `CustomBody` 类型推断请求主体的类型。 ¥Then you can use the `CustomBody` type to infer the type of the request body. ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) // ---cut--- // ✅ Do new Elysia() .post('/login', ({ body }) => { // ^? return body }, { body: customBody }) ``` ### ❌ 不要做的:声明与模型不同的类型 {#-dont-declare-type-separate-from-the-model} ¥❌ Don't: Declare type separate from the model 不要声明与模型不同的类型,而是使用 `typeof` 和 `.static` 属性来获取模型的类型。 ¥Do not declare a type separate from the model, instead use `typeof` with `.static` property to get the type of the model. ```typescript // ❌ Don't import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) type CustomBody = { username: string password: string } // ✅ Do const customBody = t.Object({ username: t.String(), password: t.String() }) type customBody = typeof customBody.static ``` ### 组 {#group} ¥Group 你可以将多个模型组合成一个对象,使其更具条理。 ¥You can group multiple models into a single object to make it more organized. ```typescript import { Elysia, t } from 'elysia' export const AuthModel = { sign: t.Object({ username: t.String(), password: t.String() }) } ``` ### 模型注入 {#model-injection} ¥Model Injection 虽然这是可选的,但如果你严格遵循 MVC 模式,你可能希望将其像服务一样注入到控制器中。我们推荐使用 [Elysia 参考模型](/essential/validation.html#reference-model) ¥Though this is optional, if you are strictly following MVC pattern, you may want to inject like a service into a controller. We recommended using [Elysia reference model](/essential/validation.html#reference-model) 使用 Elysia 的模型引用 ¥Using Elysia's model reference ```typescript twoslash import { Elysia, t } from 'elysia' const customBody = t.Object({ username: t.String(), password: t.String() }) const AuthModel = new Elysia() .model({ 'auth.sign': customBody }) const UserController = new Elysia({ prefix: '/auth' }) .use(AuthModel) .post('/sign-in', async ({ body, cookie: { session } }) => { // ^? return true }, { body: 'auth.sign' }) ``` 这种方法有几个好处: ¥This approach provide several benefits: 1. 允许我们命名模型并提供自动补齐功能。 2. 修改模式以供以后使用,或执行 [remapping](/patterns/remapping.html#remapping)。 3. 在 OpenAPI 合规客户端(例如 OpenAPI)中显示为 "models"。 4. 由于模型类型将在注册期间被缓存,因此提高 TypeScript 推断速度。 *** 如上所述,Elysia 是一个与模式无关的框架,我们仅提供使用 MVC 模式处理 Elysia 的推荐指南。 ¥As mentioned, Elysia is a pattern-agnostic framework, and we only provide a recommendation guide for handling Elysia with the MVC pattern. 是否遵循此建议完全取决于你和你的团队,这取决于你的偏好和协议。 ¥It’s entirely up to you and your team whether to follow this recommendation based on your preferences and agreement. --- --- url: 'https://elysiajs.com/patterns/trace.md' --- # 跟踪 {#trace} ¥Trace 性能是 Elysia 的一个重要方面。 ¥Performance is an important aspect for Elysia. 我们并非为了基准测试而追求速度,而是希望你在实际场景中拥有一台真正快速的服务器。 ¥We don't want to be fast for benchmarking purposes, we want you to have a real fast server in real-world scenario. 有很多因素会降低我们应用的速度 - 并且很难识别它们,但 trace 可以通过在每个生命周期中注入启动和停止代码来解决这个问题。 ¥There are many factors that can slow down our app - and it's hard to identify them, but **trace** can help solve that problem by injecting start and stop code to each life-cycle. Trace 允许我们在每个生命周期事件的前后注入代码,阻止函数的执行并与之交互。 ¥Trace allows us to inject code to before and after of each life-cycle event, block and interact with the execution of the function. ::: warning 警告 trace 不适用于动态模式 `aot: false`,因为它要求函数是静态的并且在编译时已知,否则会对性能产生很大的影响。 ¥trace doesn't work with dynamic mode `aot: false`, as it requires the function to be static and known at compile time otherwise it will have a large performance impact. ::: ## 跟踪 {#trace-1} ¥Trace Trace 使用回调监听器来确保回调函数在执行下一个生命周期事件之前完成。 ¥Trace use a callback listener to ensure that callback function is finished before moving on to the next lifecycle event. 要使用 `trace`,你需要在 Elysia 实例上调用 `trace` 方法,并传递一个将在每个生命周期事件中执行的回调函数。 ¥To use `trace`, you need to call `trace` method on the Elysia instance, and pass a callback function that will be executed for each life-cycle event. 你可以通过添加 `on` 前缀加上生命周期名称来监听每个生命周期,例如,使用 `onHandle` 来监听 `handle` 事件。 ¥You may listen to each lifecycle by adding `on` prefix followed by the lifecycle name, for example `onHandle` to listen to the `handle` event. ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(async ({ onHandle }) => { onHandle(({ begin, onStop }) => { onStop(({ end }) => { console.log('handle took', end - begin, 'ms') }) }) }) .get('/', () => 'Hi') .listen(3000) ``` 更多信息请参阅 [生命周期事件](/essential/life-cycle#events): ¥Please refer to [Life Cycle Events](/essential/life-cycle#events) for more information: ![Elysia Life Cycle](/assets/lifecycle-chart.svg) ## 子项 {#children} ¥Children 除 `handle` 之外的每个事件都有子事件,子事件是一个事件数组,会在每次生命周期事件的内部执行。 ¥Every event except `handle` has children, which is an array of events that are executed inside for each lifecycle event. 你可以使用 `onEvent` 按顺序监听每个子事件。 ¥You can use `onEvent` to listen to each child event in order ```ts twoslash import { Elysia } from 'elysia' const sleep = (time = 1000) => new Promise((resolve) => setTimeout(resolve, time)) const app = new Elysia() .trace(async ({ onBeforeHandle }) => { onBeforeHandle(({ total, onEvent }) => { console.log('total children:', total) onEvent(({ onStop }) => { onStop(({ elapsed }) => { console.log('child took', elapsed, 'ms') }) }) }) }) .get('/', () => 'Hi', { beforeHandle: [ function setup() {}, async function delay() { await sleep() } ] }) .listen(3000) ``` 在此示例中,总子项数将为 `2`,因为 `beforeHandle` 事件中有 2 个子项。 ¥In this example, total children will be `2` because there are 2 children in the `beforeHandle` event. 然后我们使用 `onEvent` 监听每个子事件并打印每个子事件的持续时间。 ¥Then we listen to each child event by using `onEvent` and print the duration of each child event. ## 跟踪参数 {#trace-parameter} ¥Trace Parameter 调用每个生命周期时 ¥When each lifecycle is called ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() // This is trace parameter // hover to view the type .trace((parameter) => { }) .get('/', () => 'Hi') .listen(3000) ``` `trace` 接受以下参数: ¥`trace` accept the following parameters: ### id - `number` {#id---number} 为每个请求随机生成唯一的 ID ¥Randomly generated unique id for each request ### context - `Context` {#context---context} Elysia 的 [上下文](/essential/handler.html#context),例如 `set`、`store`、`query`、`params` ¥Elysia's [Context](/essential/handler.html#context), eg. `set`, `store`, `query`, `params` ### set - `Context.set` {#set---contextset} `context.set` 的快捷方式,用于设置上下文的标头或状态 ¥Shortcut for `context.set`, to set a headers or status of the context ### store - `Singleton.store` {#store---singletonstore} `context.store` 的快捷方式,用于访问上下文中的数据 ¥Shortcut for `context.store`, to access a data in the context ### time - `number` {#time---number} 请求调用的时间戳 ¥Timestamp of when request is called ### on\[Event] - `TraceListener` {#onevent---tracelistener} 每个生命周期事件的事件监听器。 ¥An event listener for each life-cycle event. 你可以监听以下生命周期: ¥You may listen to the following life-cycle: * onRequest - 接收每个新请求的通知 * onParse - 用于解析主体的函数数组 * onTransform - 在验证之前转换请求和上下文 * onBeforeHandle - 自定义要求在主处理程序之前进行检查,如果返回响应,则可以跳过主处理程序。 * onHandle - 分配给路径的函数 * onAfterHandle - 在将响应发送回客户端之前与响应进行交互 * onMapResponse - 将返回值映射到 Web 标准响应中 * onError - 处理请求过程中抛出的错误 * onAfterResponse - 发送响应后的清理函数 ## 跟踪监听器 {#trace-listener} ¥Trace Listener 每个生命周期事件的监听器 ¥A listener for each life-cycle event ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(({ onBeforeHandle }) => { // This is trace listener // hover to view the type onBeforeHandle((parameter) => { }) }) .get('/', () => 'Hi') .listen(3000) ``` 每个生命周期监听器接受以下内容: ¥Each lifecycle listener accept the following ### name - `string` {#name---string} 函数的名称,如果函数是匿名的,则名称为 `anonymous`。 ¥The name of the function, if the function is anonymous, the name will be `anonymous` ### begin - `number` {#begin---number} 函数启动时间 ¥The time when the function is started ### end - `Promise` {#end---promisenumber} 函数结束时间将在函数结束时解析。 ¥The time when the function is ended, will be resolved when the function is ended ### error - `Promise` {#error---promiseerror--null} 生命周期中抛出的错误将在函数结束时解决。 ¥Error that was thrown in the lifecycle, will be resolved when the function is ended ### onStop - `callback?: (detail: TraceEndDetail) => any` {#onstop---callback-detail-traceenddetail--any} 生命周期结束时执行的回调 ¥A callback that will be executed when the lifecycle is ended ```ts twoslash import { Elysia } from 'elysia' const app = new Elysia() .trace(({ onBeforeHandle, set }) => { onBeforeHandle(({ onStop }) => { onStop(({ elapsed }) => { set.headers['X-Elapsed'] = elapsed.toString() }) }) }) .get('/', () => 'Hi') .listen(3000) ``` 建议在此函数中修改上下文,因为有一个锁定机制可以确保在进入下一个生命周期事件之前上下文修改成功。 ¥It's recommended to mutate context in this function as there's a lock mechanism to ensure the context is mutate successfully before moving on to the next lifecycle event ## TraceEndDetail {#traceenddetail} 传递给 `onStop` 回调的参数 ¥A parameter that passed to `onStop` callback ### end - `number` {#end---number} 函数结束时间 ¥The time when the function is ended ### error - `Error | null` {#error---error--null} 生命周期中抛出的错误 ¥Error that was thrown in the lifecycle ### elapsed - `number` {#elapsed---number} `end - begin` 的生命周期耗时 ¥Elapsed time of the lifecycle or `end - begin` --- --- url: 'https://elysiajs.com/essential/route.md' --- # 路由 {#routing} ¥Routing Web 服务器使用请求的路径和 HTTP 方法来查找正确的资源,称为 "routing"。 ¥Web servers use the request's **path and HTTP method** to look up the correct resource, referred to as **"routing"**. 我们可以通过调用以 HTTP 动词命名的方法,传递路径和匹配时要执行的函数来定义路由。 ¥We can define a route by calling a **method named after HTTP verbs**, passing a path and a function to execute when matched. ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', 'hello') .get('/hi', 'hi') .listen(3000) ``` 我们可以通过访问 来访问 Web 服务器。 ¥We can access the web server by going to **** 默认情况下,Web 浏览器在访问页面时会默认发送 GET 方法。 ¥By default, web browsers will send a GET method when visiting a page. ::: tip 提示 使用上面的交互式浏览器,将鼠标悬停在蓝色高亮区域上,即可查看不同路径之间的不同结果。 ¥Using the interactive browser above, hover on the blue highlight area to see different results between each path. ::: ## 路径类型 {#path-type} ¥Path type Elysia 中的路径可分为 3 种类型: ¥Path in Elysia can be grouped into 3 types: * 静态路径 - 用于定位资源的静态字符串 * 动态路径 - segment 可以是任意值 * wildcards - 直到特定点的路径可以是任意值 你可以将所有路径类型组合在一起,为你的 Web 服务器构建一个行为。 ¥You can use all of the path types together to compose a behavior for your web server. 优先级如下: ¥The priorities are as follows: 1. 静态路径 2. 动态路径 3. wildcards 如果路径解析为静态通配符动态路径,Elysia 将解析静态路径而不是动态路径。 ¥If the path is resolved as the static wild dynamic path is presented, Elysia will resolve the static path rather than the dynamic path ```typescript import { Elysia } from 'elysia' new Elysia() .get('/id/1', 'static path') .get('/id/:id', 'dynamic path') .get('/id/*', 'wildcard path') .listen(3000) ``` 服务器将响应如下: ¥Here the server will respond as follows: | 路径 | 响应 | | ------- | ----- | | /id/1 | 静态路径 | | /id/2 | 动态路径 | | /id/2/a | 通配符路径 | ## 静态路径 {#static-path} ¥Static Path 路径或路径名是用于定位服务器资源的标识符。 ¥A path or pathname is an identifier to locate resources of a server. ```bash http://localhost:/path/page ``` Elysia 使用路径和方法来查找正确的资源。 ¥Elysia uses the path and method to look up the correct resource. 路径从源位置开始。以 / 为前缀,并在搜索查询 (?) 前结束 ¥A path starts after the origin. Prefix with **/** and ends before search query **(?)** 我们可以将 URL 和路径分类如下: ¥We can categorize the URL and path as follows: | URL | 路径 | | ------------------------------------ | ------------ | | | / | | | /hello | | | /hello/world | | | /hello | | | /hello | ::: tip 提示 如果未指定路径,浏览器和 Web 服务器将默认将路径视为 '/'。 ¥If the path is not specified, the browser and web server will treat the path as '/' as a default value. ::: Elysia 将使用 [handler](/essential/handler) 函数查找每个请求中的 [route](/essential/route) 和响应。 ¥Elysia will look up each request for [route](/essential/route) and response using [handler](/essential/handler) function. ## 动态路径 {#dynamic-path} ¥Dynamic path URL 可以是静态的,也可以是动态的。 ¥URLs can be both static and dynamic. 静态路径是硬编码的字符串,可用于在服务器上定位资源,而动态路径则匹配部分内容并捕获值以提取额外信息。 ¥Static paths are hardcoded strings that can be used to locate resources on the server, while dynamic paths match some part and capture the value to extract extra information. 例如,我们可以从路径名中提取用户 ID。例如: ¥For instance, we can extract the user ID from the pathname. For example: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) // ^? .listen(3000) ``` 这里,使用 `/id/:id` 创建了一个动态路径,它告诉 Elysia 匹配到 `/id` 的任何路径。之后的内容将存储在 params 对象中。 ¥Here, a dynamic path is created with `/id/:id` which tells Elysia to match any path up until `/id`. What comes after that is then stored in the **params** object. 请求时,服务器应返回如下响应: ¥When requested, the server should return the response as follows: | 路径 | 响应 | | ---------------------- | -------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | 未找到 | | /id/anything/rest | 未找到 | 动态路径非常适合包含 ID 之类的内容,以便以后使用。 ¥Dynamic paths are great to include things like IDs, which then can be used later. 我们将命名变量路径称为路径参数或简称 params。 ¥We refer to the named variable path as **path parameter** or **params** for short. ## 段 {#segment} ¥Segment URL 段是组成完整路径的每个路径。 ¥URL segments are each path that is composed into a full path. 段由 `/` 分隔。 ¥Segments are separated by `/`. ![Representation of URL segments](/essential/url-segment.webp) Elysia 中的路径参数通过在片段前添加 ':' 前缀加上名称来表示。 ¥Path parameters in Elysia are represented by prefixing a segment with ':' followed by a name. ![Representation of path parameter](/essential/path-parameter.webp) 路径参数允许 Elysia 捕获 URL 的特定片段。 ¥Path parameters allow Elysia to capture a specific segment of a URL. 命名路径参数将存储在 `Context.params` 中。 ¥The named path parameter will then be stored in `Context.params`. | 路由 | 路径 | 参数 | | --------- | ------ | ------- | | /id/:id | /id/1 | id=1 | | /id/:id | /id/hi | id=hi | | /id/:name | /id/hi | name=hi | ## 多个路径参数 {#multiple-path-parameters} ¥Multiple path parameters 你可以拥有任意数量的路径参数,这些参数将被存储到 `params` 对象中。 ¥You can have as many path parameters as you like, which will then be stored into a `params` object. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id) .get('/id/:id/:name', ({ params: { id, name } }) => id + ' ' + name) // ^? .listen(3000) ``` 服务器将响应如下: ¥The server will respond as follows: | 路径 | 响应 | | ---------------------- | -------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | 未找到 | | /id/anything/rest | 其他任何操作 | ## 可选路径参数 {#optional-path-parameters} ¥Optional path parameters 有时我们可能需要一个静态和动态路径来解析同一个处理程序。 ¥Sometime we might want a static and dynamic path to resolve the same handler. 我们可以通过在参数名称后添加问号 `?` 来使路径参数成为可选参数。 ¥We can make a path parameter optional by adding a question mark `?` after the parameter name. ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/:id?', ({ params: { id } }) => `id ${id}`) // ^? .listen(3000) ``` 服务器将响应如下: ¥The server will respond as follows: | 路径 | 响应 | | ----- | ------ | | /id | id 未定义 | | /id/1 | id 1 | ## 通配符 {#wildcards} ¥Wildcards 动态路径允许捕获 URL 的某些部分。 ¥Dynamic paths allow capturing certain segments of the URL. 但是,当你需要路径值更具动态性并希望捕获 URL 段的其余部分时,可以使用通配符。 ¥However, when you need a value of the path to be more dynamic and want to capture the rest of the URL segment, a wildcard can be used. 通配符可以通过使用 "\*" 捕获段后的值,无论其大小。 ¥Wildcards can capture the value after segment regardless of amount by using "\*". ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/id/*', ({ params }) => params['*']) // ^? .listen(3000) ``` 在这种情况下,服务器将响应如下: ¥In this case the server will respond as follows: | 路径 | 响应 | | ---------------------- | ------------- | | /id/1 | 1 | | /id/123 | 123 | | /id/anything | anything | | /id/anything?name=salt | anything | | /id | 未找到 | | /id/anything/rest | anything/rest | 通配符对于捕获到特定点的路径非常有用。 ¥Wildcards are useful for capturing a path until a specific point. ::: tip 提示 你可以将通配符与路径参数一起使用。 ¥You can use a wildcard with a path parameter. ::: ## HTTP 动词 {#http-verb} ¥HTTP Verb HTTP 定义了一组请求方法来指示针对给定资源需要执行的操作。 ¥HTTP defines a set of request methods to indicate the desired action to be performed for a given resource HTTP 动词有多种,但最常用的是: ¥There are several HTTP verbs, but the most common ones are: ### GET {#get} 使用 GET 的请求应该只检索数据。 ¥Requests using GET should only retrieve data. ### POST {#post} 向指定资源提交有效负载,通常会导致状态更改或副作用。 ¥Submits a payload to the specified resource, often causing state change or side effect. ### PUT {#put} 使用请求的有效负载替换目标资源的所有当前表示。 ¥Replaces all current representations of the target resource using the request's payload. ### PATCH {#patch} 对资源应用部分修改。 ¥Applies partial modifications to a resource. ### DELETE {#delete} 删除指定的资源。 ¥Deletes the specified resource. *** 为了处理每个不同的动词,Elysia 默认为多个 HTTP 动词内置了 API,类似于 `Elysia.get`。 ¥To handle each of the different verbs, Elysia has a built-in API for several HTTP verbs by default, similar to `Elysia.get` ```typescript import { Elysia } from 'elysia' new Elysia() .get('/', 'hello') .post('/hi', 'hi') .listen(3000) ``` Elysia HTTP 方法接受以下参数: ¥Elysia HTTP methods accepts the following parameters: * 路径:路径名 * 功能:响应客户端的函数 * 钩子:附加元数据 你可以在 [HTTP 请求方法](https://web.nodejs.cn/en-US/docs/Web/HTTP/Methods) 上阅读有关 HTTP 方法的更多信息。 ¥You can read more about the HTTP methods on [HTTP Request Methods](https://web.nodejs.cn/en-US/docs/Web/HTTP/Methods). ## 自定义方法 {#custom-method} ¥Custom Method 我们可以使用 `Elysia.route` 接受自定义 HTTP 方法。 ¥We can accept custom HTTP Methods with `Elysia.route`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/get', 'hello') .post('/post', 'hi') .route('M-SEARCH', '/m-search', 'connect') // [!code ++] .listen(3000) ``` Elysia.route 接受以下参数: ¥**Elysia.route** accepts the following: * 方法:HTTP 动词 * 路径:路径名 * 功能:响应客户端的函数 * 钩子:附加元数据 导航到每个方法时,你应该看到以下结果: ¥When navigating to each method, you should see the results as the following: | 路径 | 方法 | 结果 | | --------- | -------- | ------- | | /get | GET | hello | | /post | POST | hi | | /m-search | M-SEARCH | connect | ::: tip 提示 基于 [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.1),HTTP 动词区分大小写。 ¥Based on [RFC 7231](https://www.rfc-editor.org/rfc/rfc7231#section-4.1), HTTP Verb is case-sensitive. 建议使用大写约定在 Elysia 中定义自定义 HTTP 动词。 ¥It's recommended to use the UPPERCASE convention for defining a custom HTTP Verb with Elysia. ::: ## Elysia.all {#elysiaall} Elysia 提供了一个 `Elysia.all` 接口,用于处理指定路径的任何 HTTP 方法,其 API 与 Elysia.get 和 Elysia.post 相同。 ¥Elysia provides an `Elysia.all` for handling any HTTP method for a specified path using the same API like **Elysia.get** and **Elysia.post** ```typescript import { Elysia } from 'elysia' new Elysia() .all('/', 'hi') .listen(3000) ``` 任何与路径匹配的 HTTP 方法都将按以下方式处理: ¥Any HTTP method that matches the path, will be handled as follows: | 路径 | 方法 | 结果 | | -- | ------ | -- | | / | GET | hi | | / | POST | hi | | / | DELETE | hi | ## 句柄 {#handle} ¥Handle 大多数开发者使用 REST 客户端(例如 Postman、Insomnia 或 Hoppscotch)来测试他们的 API。 ¥Most developers use REST clients like Postman, Insomnia or Hoppscotch to test their API. 然而,Elysia 可以使用 `Elysia.handle` 进行编程测试。 ¥However, Elysia can be programmatically test using `Elysia.handle`. ```typescript import { Elysia } from 'elysia' const app = new Elysia() .get('/', 'hello') .post('/hi', 'hi') .listen(3000) app.handle(new Request('http://localhost/')).then(console.log) ``` Elysia.handle 是一个用于处理发送到服务器的实际请求的函数。 ¥**Elysia.handle** is a function to process an actual request sent to the server. ::: tip 提示 与单元测试的模拟不同,你可以期望它的行为类似于发送到服务器的实际请求。 ¥Unlike unit test's mock, **you can expect it to behave like an actual request** sent to the server. 但它对于模拟或创建单元测试也很有用。 ¥But also useful for simulating or creating unit tests. ::: ## 404 {#404} 如果没有路径与定义的路由匹配,Elysia 会将请求传递到 [error](/essential/life-cycle.html#on-error) 生命周期,然后返回 HTTP 状态为 404 的 "NOT\_FOUND"。 ¥If no path matches the defined routes, Elysia will pass the request to [error](/essential/life-cycle.html#on-error) life cycle before returning a **"NOT\_FOUND"** with an HTTP status of 404. 我们可以通过从 `error` 生命周期返回一个值来处理自定义 404 错误,如下所示: ¥We can handle a custom 404 error by returning a value from `error` life cycle like this: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .get('/', 'hi') .onError(({ code }) => { if (code === 'NOT_FOUND') { return 'Route not found :(' } }) .listen(3000) ``` 导航到你的 Web 服务器时,你应该看到以下结果: ¥When navigating to your web server, you should see the result as follows: | 路径 | 方法 | 结果 | | --- | ---- | -------- | | / | GET | hi | | / | POST | 未找到路由 :( | | /hi | GET | 未找到路由 :( | 你可以在 [生命周期事件](/essential/life-cycle#events) 和 [错误处理](/essential/life-cycle.html#on-error) 中了解有关生命周期和错误处理的更多信息。 ¥You can learn more about life cycle and error handling in [Life Cycle Events](/essential/life-cycle#events) and [Error Handling](/essential/life-cycle.html#on-error). ::: tip 提示 HTTP 状态用于指示响应类型。默认情况下,如果一切正常,服务器将返回 '200 OK' 状态码(如果路由匹配且没有错误,Elysia 默认返回 200)。 ¥HTTP Status is used to indicate the type of response. By default if everything is correct, the server will return a '200 OK' status code (If a route matches and there is no error, Elysia will return 200 as default) 如果服务器找不到任何要处理的路由(例如本例),则服务器将返回 '404 NOT FOUND' 状态码。 ¥If the server fails to find any route to handle, like in this case, then the server shall return a '404 NOT FOUND' status code. ::: ## 组 {#group} ¥Group 创建 Web 服务器时,通常会有多个路由共享相同的前缀: ¥When creating a web server, you would often have multiple routes sharing the same prefix: ```typescript import { Elysia } from 'elysia' new Elysia() .post('/user/sign-in', 'Sign in') .post('/user/sign-up', 'Sign up') .post('/user/profile', 'Profile') .listen(3000) ``` 这可以通过 `Elysia.group` 进行改进,允许我们通过将前缀分组同时应用于多个路由: ¥This can be improved with `Elysia.group`, allowing us to apply prefixes to multiple routes at the same time by grouping them together: ```typescript twoslash import { Elysia } from 'elysia' new Elysia() .group('/user', (app) => app .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') ) .listen(3000) ``` 此代码的行为与我们的第一个示例相同,其结构应如下: ¥This code behaves the same as our first example and should be structured as follows: | 路径 | 结果 | | ------------- | ---- | | /user/sign-in | 登录 | | /user/sign-up | 注册 | | /user/profile | 配置文件 | `.group()` 还可以接受可选的守卫参数,以减少同时使用组和守卫的重复工作: ¥`.group()` can also accept an optional guard parameter to reduce boilerplate of using groups and guards together: ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .group( '/user', { body: t.Literal('Rikuhachima Aru') }, (app) => app .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') ) .listen(3000) ``` 你可以在 [scope](/essential/plugin.html#scope) 中找到更多关于分组守卫的信息。 ¥You may find more information about grouped guards in [scope](/essential/plugin.html#scope). ### 前缀 {#prefix} ¥Prefix 我们可以将一个组分离到一个单独的插件实例中,通过在构造函数中添加前缀来减少嵌套。 ¥We can separate a group into a separate plugin instance to reduce nesting by providing a **prefix** to the constructor. ```typescript import { Elysia } from 'elysia' const users = new Elysia({ prefix: '/user' }) .post('/sign-in', 'Sign in') .post('/sign-up', 'Sign up') .post('/profile', 'Profile') new Elysia() .use(users) .get('/', 'hello world') .listen(3000) ``` --- --- url: 'https://elysiajs.com/patterns/deploy.md' --- # 部署到生产环境 {#deploy-to-production} ¥Deploy to production 本页是如何将 Elysia 部署到生产的指南。 ¥This page is a guide on how to deploy Elysia to production. ## 编译为二进制文件 {#compile-to-binary} ¥Compile to binary 我们建议在部署到生产环境之前运行构建命令,因为它可以显著减少内存使用量和文件大小。 ¥We recommend running a build command before deploying to production as it could potentially reduce memory usage and file size significantly. 我们建议使用以下命令将 Elysia 编译为单个二进制文件: ¥We recommend compiling Elysia into a single binary using the command as follows: ```bash bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --outfile server \ src/index.ts ``` 这将生成一个可移植的二进制文件 `server`,我们可以运行它来启动服务器。 ¥This will generate a portable binary `server` which we can run to start our server. 与开发环境相比,将服务器编译为二进制文件通常可以显著减少 2-3 倍的内存使用量。 ¥Compiling server to binary usually significantly reduces memory usage by 2-3x compared to development environment. 这条命令有点长,我们来分解一下: ¥This command is a bit long, so let's break it down: 1. \--compile 将 TypeScript 编译为二进制文件 2. \--minify-whitespace 删除不必要的空格 3. \--minify-syntax 压缩 JavaScript 语法以减小文件大小 4. \--outfile server 将二进制文件输出为 `server` 5. src/index.ts 我们服务器的入口文件(代码库) 要启动服务器,只需运行二进制文件即可。 ¥To start our server, simply run the binary. ```bash ./server ``` 编译二进制文件后,你无需在计算机上安装 `Bun` 即可运行服务器。 ¥Once binary is compiled, you don't need `Bun` installed on the machine to run the server. 这非常棒,因为部署服务器无需安装额外的运行时即可运行,从而使二进制文件可移植。 ¥This is great as the deployment server doesn't need to install an extra runtime to run making binary portable. ### 目标 {#target} ¥Target 你还可以添加 `--target` 标志,以针对目标平台优化二进制文件。 ¥You can also add a `--target` flag to optimize the binary for the target platform. ```bash bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --target bun-linux-x64 \ --outfile server \ src/index.ts ``` 以下是可用目标列表: ¥Here's a list of available targets: | 目标 | 操作系统 | 架构 | 现代 | 基线 | Libc | | -------------------- | ------- | ----- | -- | --- | ----- | | bun-linux-x64 | Linux | x64 | ✅ | ✅ | glibc | | bun-linux-arm64 | Linux | arm64 | ✅ | N/A | glibc | | bun-windows-x64 | Windows | x64 | ✅ | ✅ | \* | | bun-windows-arm64 | Windows | arm64 | ❌ | ❌ | \* | | bun-darwin-x64 | macOS | x64 | ✅ | ✅ | \* | | bun-darwin-arm64 | macOS | arm64 | ✅ | N/A | \* | | bun-linux-x64-musl | Linux | x64 | ✅ | ✅ | musl | | bun-linux-arm64-musl | Linux | arm64 | ✅ | N/A | musl | ### 为什么不使用 --minify {#why-not---minify} ¥Why not --minify Bun 确实有 `--minify` 标志可以最小化二进制文件。 ¥Bun does have `--minify` flag that will minify the binary. 但是,如果我们使用 [OpenTelemetry](/plugins/opentelemetry),它会将函数名称缩减为一个字符。 ¥However if we are using [OpenTelemetry](/plugins/opentelemetry), it's going to reduce a function name to a single character. 由于 OpenTelemetry 依赖于函数名,这使得跟踪变得比应有的更困难。 ¥This makes tracing harder than it should as OpenTelemetry relies on a function name. 但是,如果你不使用 OpenTelemetry,你可以选择使用 `--minify`。 ¥However, if you're not using OpenTelemetry, you may opt in for `--minify` instead ```bash bun build \ --compile \ --minify \ --outfile server \ src/index.ts ``` ### 权限 {#permission} ¥Permission 某些 Linux 发行版可能无法运行该二进制文件,如果你使用的是 Linux,我们建议你启用二进制文件的可执行权限: ¥Some Linux distros might not be able to run the binary, we suggest enabling executable permission to a binary if you're on Linux: ```bash chmod +x ./server ./server ``` ### 未知随机中文错误 {#unknown-random-chinese-error} ¥Unknown random Chinese error 如果你尝试将二进制文件部署到服务器,但无法运行并出现随机中文错误。 ¥If you're trying to deploy a binary to your server but unable to run with random chinese character error. 这意味着你正在运行的机器不支持 AVX2。 ¥It means that the machine you're running on **doesn't support AVX2**. 遗憾的是,Bun 需要支持 `AVX2` 硬件的机器。 ¥Unfortunately, Bun requires a machine that has `AVX2` hardware support. 据我们所知,没有其他解决方法。 ¥There's no workaround as far as we know. ## 编译为 JavaScript {#compile-to-javascript} ¥Compile to JavaScript 如果你无法编译为二进制文件或你正在 Windows 服务器上部署。 ¥If you are unable to compile to binary or you are deploying on a Windows server. 你也可以将服务器打包到 JavaScript 文件中。 ¥You may bundle your server to a JavaScript file instead. ```bash bun build \ --minify-whitespace \ --minify-syntax \ --outfile ./dist/index.js \ src/index.ts ``` 这将生成一个可移植的 JavaScript 文件,你可以将其部署到你的服务器上。 ¥This will generate a single portable JavaScript file that you can deploy on your server. ```bash NODE_ENV=production bun ./dist/index.js ``` ## Docker {#docker} 在 Docker 上,我们建议始终编译为二进制文件以减少基础镜像的开销。 ¥On Docker, we recommended to always compile to binary to reduce base image overhead. 以下是使用 Distroless 二进制镜像的示例镜像。 ¥Here's an example image using Distroless image using binary. ```dockerfile [Dockerfile] FROM oven/bun AS build WORKDIR /app # Cache packages installation COPY package.json package.json COPY bun.lock bun.lock RUN bun install COPY ./src ./src ENV NODE_ENV=production RUN bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --outfile server \ src/index.ts FROM gcr.io/distroless/base WORKDIR /app COPY --from=build /app/server server ENV NODE_ENV=production CMD ["./server"] EXPOSE 3000 ``` ### OpenTelemetry {#opentelemetry} 如果你使用 [OpenTelemetry](/integrations/opentelemetry) 部署生产服务器。 ¥If you are using [OpenTelemetry](/integrations/opentelemetry) to deploys production server. 由于 OpenTelemetry 依赖于 monkey-patching `node_modules/`。为了使 make instrumentations 正常工作,我们需要将要被 instrument 的库指定为外部模块,以将其排除在打包之外。 ¥As OpenTelemetry rely on monkey-patching `node_modules/`. It's required that make instrumentations works properly, we need to specify that libraries to be instrument is an external module to exclude it from being bundled. 例如,如果你使用 `@opentelemetry/instrumentation-pg` 来检测 `pg` 库。我们需要将 `pg` 从打包包中排除,并确保它导入 `node_modules/pg`。 ¥For example, if you are using `@opentelemetry/instrumentation-pg` to instrument `pg` library. We need to exclude `pg` from being bundled and make sure that it is importing `node_modules/pg`. 为了使其正常工作,我们可以使用 `--external pg` 将 `pg` 指定为外部模块。 ¥To make this works, we may specified `pg` as an external module with `--external pg` ```bash bun build --compile --external pg --outfile server src/index.ts ``` 这告诉 bun 不要将 `pg` 打包到最终输出文件中,而是在运行时从 `node_modules` 目录导入。所以,在生产服务器上,你还必须保留 `node_modules` 目录。 ¥This tells bun to not `pg` bundled into the final output file, and will be imported from the `node_modules` directory at runtime. So on a production server, you must also keeps the `node_modules` directory. 建议在 `package.json` 中将生产服务器中应可用的软件包指定为 `dependencies`,并使用 `bun install --production` 仅安装生产依赖。 ¥It's recommended to specify packages that should be available in a production server as `dependencies` in `package.json` and use `bun install --production` to install only production dependencies. ```json { "dependencies": { "pg": "^8.15.6" }, "devDependencies": { "@elysiajs/opentelemetry": "^1.2.0", "@opentelemetry/instrumentation-pg": "^0.52.0", "@types/pg": "^8.11.14", "elysia": "^1.2.25" } } ``` 然后在生产服务器上运行构建命令后, ¥Then after running a build command, on a production server ```bash bun install --production ``` 如果 node\_modules 目录仍然包含开发依赖,你可以删除 node\_modules 目录并重新安装生产依赖。 ¥If the node\_modules directory still includes development dependencies, you may remove the node\_modules directory and reinstall production dependencies again. ### Monorepo {#monorepo} 如果你使用 Elysia 和 Monorepo,则可能需要包含依赖 `packages`。 ¥If you are using Elysia with Monorepo, you may need to include dependent `packages`. 如果你使用 Turborepo,则需要将 Dockerfile 放在你的应用目录中,例如 apps/server/Dockerfile。这可能适用于其他 monorepo 管理器,例如 Lerna 等。 ¥If you are using Turborepo, you may place a Dockerfile inside an your apps directory like **apps/server/Dockerfile**. This may apply to other monorepo manager such as Lerna, etc. 假设我们的 monorepo 使用的是 Turborepo,其结构如下: ¥Assume that our monorepo are using Turborepo with structure as follows: * apps * server * **Dockerfile(在此处放置 Dockerfile)** * packages * config 然后我们可以在 monorepo 根目录(而不是应用根目录)上构建 Dockerfile: ¥Then we can build our Dockerfile on monorepo root (not app root): ```bash docker build -t elysia-mono . ``` Dockerfile 如下: ¥With Dockerfile as follows: ```dockerfile [apps/server/Dockerfile] FROM oven/bun:1 AS build WORKDIR /app # Cache packages COPY package.json package.json COPY bun.lock bun.lock COPY /apps/server/package.json ./apps/server/package.json COPY /packages/config/package.json ./packages/config/package.json RUN bun install COPY /apps/server ./apps/server COPY /packages/config ./packages/config ENV NODE_ENV=production RUN bun build \ --compile \ --minify-whitespace \ --minify-syntax \ --outfile server \ src/index.ts FROM gcr.io/distroless/base WORKDIR /app COPY --from=build /app/server server ENV NODE_ENV=production CMD ["./server"] EXPOSE 3000 ``` ## Railway {#railway} [Railway](https://railway.app) 是流行的部署平台之一。 ¥[Railway](https://railway.app) is one of the popular deployment platform. Railway 为每个部署分配一个随机端口,可通过 `PORT` 环境变量访问。 ¥Railway assigns a **random port** to expose for each deployment, which can be accessed via the `PORT` environment variable. 我们需要修改 Elysia 服务器以接受 `PORT` 环境变量,从而符合 Railway 端口的要求。 ¥We need to modify our Elysia server to accept the `PORT` environment variable to comply with Railway port. 我们可以使用 `process.env.PORT` 而不是固定端口,并在开发过程中提供回退。 ¥Instead of a fixed port, we may use `process.env.PORT` and provide a fallback on development instead. ```ts new Elysia() .listen(3000) // [!code --] .listen(process.env.PORT ?? 3000) // [!code ++] ``` 这应该允许 Elysia 拦截 Railway 提供的端口。 ¥This should allows Elysia to intercept port provided by Railway. ::: tip 提示 Elysia 会自动将主机名分配给 `0.0.0.0`,这与 Railway 兼容。 ¥Elysia assign hostname to `0.0.0.0` automatically, which works with Railway ::: --- --- url: 'https://elysiajs.com/patterns/configuration.md' --- # 配置 {#config} ¥Config Elysia 具有可配置的行为,允许我们自定义其功能的各个方面。 ¥Elysia comes with a configurable behavior, allowing us to customize various aspects of its functionality. 我们可以使用构造函数定义配置。 ¥We can define a configuration by using a constructor. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1', normalize: true }) ``` ## adapter {#adapter} ###### 自 1.1.11 起 {#since-1111} ¥Since 1.1.11 用于在不同环境中使用 Elysia 的运行时适配器。 ¥Runtime adapter for using Elysia in different environments. 默认根据环境使用合适的适配器。 ¥Default to appropriate adapter based on the environment. ```ts import { Elysia, t } from 'elysia' import { BunAdapter } from 'elysia/adapter/bun' new Elysia({ adapter: BunAdapter }) ``` ## aot {#aot} ###### 自 0.4.0 起 {#since-040} ¥Since 0.4.0 提前编译。 ¥Ahead of Time compilation. Elysia 内置了 JIT "compiler",可以处理 [优化性能](/blog/elysia-04.html#ahead-of-time-complie)。 ¥Elysia has a built-in JIT *"compiler"* that can [optimize performance](/blog/elysia-04.html#ahead-of-time-complie). ```ts twoslash import { Elysia } from 'elysia' new Elysia({ aot: true }) ``` 禁用提前编译 ¥Disable Ahead of Time compilation #### 选项 - @default `false` {#options---default-false} ¥Options - @default `false` * `true` - 启动服务器前预编译每个路由 * `false` - 完全禁用 JIT。启动速度更快,且不影响性能。 ## detail {#detail} 为实例的所有路由定义 OpenAPI 模式。 ¥Define an OpenAPI schema for all routes of an instance. 此模式将用于为实例的所有路由生成 OpenAPI 文档。 ¥This schema will be used to generate OpenAPI documentation for all routes of an instance. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ detail: { hide: true, tags: ['elysia'] } }) ``` ## encodeSchema {#encodeschema} 在将响应返回给客户端之前,使用自定义 `Encode` 处理自定义 `t.Transform` 模式。 ¥Handle custom `t.Transform` schema with custom `Encode` before returning the response to client. 这使我们能够在将响应发送到客户端之前为你的数据创建自定义编码函数。 ¥This allows us to create custom encode function for your data before sending response to the client. ```ts import { Elysia, t } from 'elysia' new Elysia({ encodeSchema: true }) ``` #### 选项 - @default `true` {#options---default-true} ¥Options - @default `true` * `true` - 向客户端发送响应前运行 `Encode` * `false` - 完全跳过 `Encode` ## name {#name} 定义用于调试和 [插件数据去重](/essential/plugin.html#plugin-deduplication) 的实例名称 ¥Define a name of an instance which is used for debugging and [Plugin Deduplication](/essential/plugin.html#plugin-deduplication) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ name: 'service.thing' }) ``` ## nativeStaticResponse {#nativestaticresponse} ###### 自 1.1.11 起 {#since-1111-1} ¥Since 1.1.11 使用优化函数处理每个相应运行时的内联值。 ¥Use an optimized function for handling inline value for each respective runtime. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ nativeStaticResponse: true }) ``` #### 示例 {#example} ¥Example 如果在 Bun 上启用,Elysia 会在 `Bun.serve.static` 中插入内联值,以提高静态值的性能。 ¥If enabled on Bun, Elysia will insert inline value into `Bun.serve.static` improving performance for static value. ```ts import { Elysia } from 'elysia' // This new Elysia({ nativeStaticResponse: true }).get('/version', 1) // is an equivalent to Bun.serve({ static: { '/version': new Response(1) } }) ``` ## normalize {#normalize} ###### 自 1.1.0 起 {#since-110} ¥Since 1.1.0 Elysia 是否应将字段强制转换为指定的模式。 ¥Whether Elysia should coerce field into a specified schema. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ normalize: true }) ``` 当在输入和输出中发现未在架构中指定的未知属性时,Elysia 应如何处理该字段? ¥When unknown properties that are not specified in schema are found on either input and output, how should Elysia handle the field? 选项 - @default `true` ¥Options - @default `true` * `true`:Elysia 将使用 [精确镜像](/blog/elysia-13.html#exact-mirror) 将字段强制转换为指定的模式。 * `typebox`:Elysia 将使用 [TypeBox 的 Value.Clean](https://github.com/sinclairzx81/typebox) 将字段强制转换为指定的模式。 * `false`:如果请求或响应包含相应处理程序的架构中未明确允许的字段,Elysia 将引发错误。 ## precompile {#precompile} ###### 自 1.0.0 起 {#since-100} ¥Since 1.0.0 Elysia 是否应该在启动服务器之前提前 [预编译所有路由](/blog/elysia-10.html#improved-startup-time)。 ¥Whether Elysia should [precompile all routes](/blog/elysia-10.html#improved-startup-time) ahead of time before starting the server. ```ts twoslash import { Elysia } from 'elysia' new Elysia({ precompile: true }) ``` 选项 - @default `false` ¥Options - @default `false` * `true`:启动服务器前在所有路由上运行 JIT * `false`:按需动态编译路由 建议将其保留为 `false`。 ¥It's recommended to leave it as `false`. ## prefix {#prefix} 为实例的所有路由定义前缀 ¥Define a prefix for all routes of an instance ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1' }) ``` 定义前缀后,所有路由都将以给定值作为前缀。 ¥When prefix is defined, all routes will be prefixed with the given value. #### 示例 {#example-1} ¥Example ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ prefix: '/v1' }).get('/name', 'elysia') // Path is /v1/name ``` ## sanitize {#sanitize} 一个函数或一个函数数组,在验证时调用并拦截每个 `t.String` 请求。 ¥A function or an array of function that calls and intercepts on every `t.String` while validation. 允许我们读取字符串并将其转换为新值。 ¥Allowing us to read and transform a string into a new value. ```ts import { Elysia, t } from 'elysia' new Elysia({ sanitize: (value) => Bun.escapeHTML(value) }) ``` ## seed {#seed} 定义一个用于生成实例校验和的值,用于 [插件数据去重](/essential/plugin.html#plugin-deduplication) ¥Define a value which will be used to generate checksum of an instance, used for [Plugin Deduplication](/essential/plugin.html#plugin-deduplication) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ seed: { value: 'service.thing' } }) ``` 值可以是任何类型,不限于字符串、数字或对象。 ¥The value could be any type not limited to string, number, or object. ## strictPath {#strictpath} Elysia 是否应严格处理路径。 ¥Whether should Elysia handle path strictly. 根据 [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3),路径应该严格等于路由中定义的路径。 ¥According to [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3), a path should be strictly equal to the path defined in the route. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia({ strictPath: true }) ``` #### 选项 - @default `false` {#options---default-false-1} ¥Options - @default `false` * `true` - 严格遵循 [RFC 3986](https://tools.ietf.org/html/rfc3986#section-3.3) 进行路径匹配 * `false` - 支持后缀 '/',反之亦然。 #### 示例 {#example-2} ¥Example ```ts twoslash import { Elysia, t } from 'elysia' // Path can be either /name or /name/ new Elysia({ strictPath: false }).get('/name', 'elysia') // Path can be only /name new Elysia({ strictPath: true }).get('/name', 'elysia') ``` ## serve {#serve} 自定义 HTTP 服务器行为。 ¥Customize HTTP server behavior. Bun 服务配置。 ¥Bun serve configuration. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { hostname: 'elysiajs.com', tls: { cert: Bun.file('cert.pem'), key: Bun.file('key.pem') } }, }) ``` 此配置扩展了 [Bun 服务 API](https://bun.sh/docs/api/http) 和 [Bun TLS](https://bun.sh/docs/api/http#tls) ¥This configuration extends [Bun Serve API](https://bun.sh/docs/api/http) and [Bun TLS](https://bun.sh/docs/api/http#tls) ### 示例:最大主体大小 {#example-max-body-size} ¥Example: Max body size 我们可以通过在 `serve` 配置中设置 [`serve.maxRequestBodySize`](#serve-maxrequestbodysize) 来设置最大正文大小。 ¥We can set the maximum body size by setting [`serve.maxRequestBodySize`](#serve-maxrequestbodysize) in the `serve` configuration. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { maxRequestBodySize: 1024 * 1024 * 256 // 256MB } }) ``` 默认情况下,最大请求体大小为 128MB(1024 \* 1024 \* 128)。定义主体大小限制。 ¥By default the maximum request body size is 128MB (1024 \* 1024 \* 128). Define body size limit. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { // Maximum message size (in bytes) maxPayloadLength: 64 * 1024, } }) ``` ### 示例:HTTPS / TLS {#example-https--tls} ¥Example: HTTPS / TLS 我们可以通过传入 key 和 cert 的值来启用 TLS(SSL 的后继者);两者都是启用 TLS 所必需的。 ¥We can enable TLS (known as successor of SSL) by passing in a value for key and cert; both are required to enable TLS. ```ts import { Elysia, file } from 'elysia' new Elysia({ serve: { tls: { cert: file('cert.pem'), key: file('key.pem') } } }) ``` ### 示例:增加超时时间 {#example-increase-timeout} ¥Example: Increase timeout 我们可以通过在 `serve` 配置中设置 [`serve.idleTimeout`](#serve-idletimeout) 来增加空闲超时时间。 ¥We can increase the idle timeout by setting [`serve.idleTimeout`](#serve-idletimeout) in the `serve` configuration. ```ts import { Elysia } from 'elysia' new Elysia({ serve: { // Increase idle timeout to 30 seconds idleTimeout: 30 } }) ``` 默认情况下,空闲超时时间为 10 秒(在 Bun 上)。 ¥By default the idle timeout is 10 seconds (on Bun). *** ## serve {#serve-1} HTTP 服务器配置。 ¥HTTP server configuration. Elysia 扩展了 Bun 配置,该配置支持开箱即用的 TLS,由 BoringSSL 提供支持。 ¥Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL. 有关可用配置,请参阅 [serve.tls](#serve-tls)。 ¥See [serve.tls](#serve-tls) for available configuration. ### serve.hostname {#servehostname} @default `0.0.0.0` 设置服务器监听的主机名 ¥Set the hostname which the server listens on ### serve.id {#serveid} 使用 ID 唯一标识服务器实例 ¥Uniquely identify a server instance with an ID 此字符串将用于热重载服务器,而不会中断待处理的请求或 WebSocket。如果未提供,则会生成一个值。要禁用热重载,请将此值设置为 `null`。 ¥This string will be used to hot reload the server without interrupting pending requests or websockets. If not provided, a value will be generated. To disable hot reloading, set this value to `null`. ### serve.idleTimeout {#serveidletimeout} @default `10` (10 seconds) 默认情况下,Bun 将空闲超时设置为 10 秒,这意味着如果请求在 10 秒内未完成,它将被中止。 ¥By default, Bun set idle timeout to 10 seconds, which means that if a request is not completed within 10 seconds, it will be aborted. ### serve.maxRequestBodySize {#servemaxrequestbodysize} @default `1024 * 1024 * 128` (128MB) 设置请求主体的最大大小(以字节为单位) ¥Set the maximum size of a request body (in bytes) ### serve.port {#serveport} @default `3000` 监听端口 ¥Port to listen on ### serve.rejectUnauthorized {#serverejectunauthorized} @default `NODE_TLS_REJECT_UNAUTHORIZED` environment variable 如果设置为 `false`,则接受任何证书。 ¥If set to `false`, any certificate is accepted. ### serve.reusePort {#servereuseport} @default `true` 如果需要设置 `SO_REUSEPORT` 标志 ¥If the `SO_REUSEPORT` flag should be set 这允许多个进程绑定到同一端口,这对于负载平衡非常有用。 ¥This allows multiple processes to bind to the same port, which is useful for load balancing 此配置会被 Elysia 覆盖并默认启用。 ¥This configuration is override and turns on by default by Elysia ### serve.unix {#serveunix} 如果设置了该选项,HTTP 服务器将监听 Unix 套接字而不是端口。 ¥If set, the HTTP server will listen on a unix socket instead of a port. (不能与主机名+端口一起使用) ¥(Cannot be used with hostname+port) ### serve.tls {#servetls} 我们可以通过传入 key 和 cert 的值来启用 TLS(SSL 的后继者);两者都是启用 TLS 所必需的。 ¥We can enable TLS (known as successor of SSL) by passing in a value for key and cert; both are required to enable TLS. ```ts import { Elysia, file } from 'elysia' new Elysia({ serve: { tls: { cert: file('cert.pem'), key: file('key.pem') } } }) ``` Elysia 扩展了 Bun 配置,该配置支持开箱即用的 TLS,由 BoringSSL 提供支持。 ¥Elysia extends Bun configuration which supports TLS out of the box, powered by BoringSSL. ### serve.tls.ca {#servetlsca} 可选地覆盖受信任的 CA 证书。默认信任 Mozilla 授权的知名 CA。 ¥Optionally override the trusted CA certificates. Default is to trust the well-known CAs curated by Mozilla. 当使用此选项明确指定 CA 时,Mozilla 的 CA 将被完全替换。 ¥Mozilla's CAs are completely replaced when CAs are explicitly specified using this option. ### serve.tls.cert {#servetlscert} PEM 格式的证书链。每个私钥应提供一个证书链。 ¥Cert chains in PEM format. One cert chain should be provided per private key. 每个证书链应按顺序包含所提供私钥的 PEM 格式证书,以及 PEM 格式的中间证书(如果有),且不包括根 CA(对等方必须预先知道根 CA,请参阅 ca)。 ¥Each cert chain should consist of the PEM formatted certificate for a provided private key, followed by the PEM formatted intermediate certificates (if any), in order, and not including the root CA (the root CA must be pre-known to the peer, see ca). 提供多个证书链时,它们的顺序不必与密钥中的私钥顺序相同。 ¥When providing multiple cert chains, they do not have to be in the same order as their private keys in key. 如果未提供中间证书,对等方将无法验证证书,握手将失败。 ¥If the intermediate certificates are not provided, the peer will not be able to validate the certificate, and the handshake will fail. ### serve.tls.dhParamsFile {#servetlsdhparamsfile} .pem 文件的路径 自定义 Diffie-Helman 参数 ¥File path to a .pem file custom Diffie Helman parameters ### serve.tls.key {#servetlskey} PEM 格式的私钥。PEM 允许加密私钥。加密密钥将使用 options.passphrase 解密。 ¥Private keys in PEM format. PEM allows the option of private keys being encrypted. Encrypted keys will be decrypted with options.passphrase. 使用不同算法的多个密钥可以以未加密的密钥字符串或缓冲区数组的形式提供,也可以以 形式的对象数组的形式提供。 ¥Multiple keys using different algorithms can be provided either as an array of unencrypted key strings or buffers, or an array of objects in the form . 对象形式只能以数组形式出现。 ¥The object form can only occur in an array. object.passphrase 为可选。加密密钥将使用以下方式解密: ¥**object.passphrase** is optional. Encrypted keys will be decrypted with 如果提供了 object.passphrase,则使用 object.passphrase;如果没有提供,则使用 options.passphrase。 ¥**object.passphrase** if provided, or **options.passphrase** if it is not. ### serve.tls.lowMemoryMode {#servetlslowmemorymode} @default `false` 这将 `OPENSSL_RELEASE_BUFFERS` 设置为 1。 ¥This sets `OPENSSL_RELEASE_BUFFERS` to 1. 它会降低整体性能,但会节省一些内存。 ¥It reduces overall performance but saves some memory. ### serve.tls.passphrase {#servetlspassphrase} 单个私钥和/或 PFX 文件的共享密码。 ¥Shared passphrase for a single private key and/or a PFX. ### serve.tls.requestCert {#servetlsrequestcert} @default `false` 如果设置为 `true`,则服务器将请求客户端证书。 ¥If set to `true`, the server will request a client certificate. ### serve.tls.secureOptions {#servetlssecureoptions} 可选地影响 OpenSSL 协议行为,这通常不是必需的。 ¥Optionally affect the OpenSSL protocol behavior, which is not usually necessary. 如果要使用,请务必谨慎! ¥This should be used carefully if at all! 值是 OpenSSL 选项中 SSL\_OP\_\* 选项的数字位掩码 ¥Value is a numeric bitmask of the SSL\_OP\_\* options from OpenSSL Options ### serve.tls.serverName {#servetlsservername} 显式设置服务器名称 ¥Explicitly set a server name ## tags {#tags} 为实例的所有路由定义类似于 [detail](#detail) 的 OpenAPI 模式标签 ¥Define an tags for OpenAPI schema for all routes of an instance similar to [detail](#detail) ```ts twoslash import { Elysia } from 'elysia' new Elysia({ tags: ['elysia'] }) ``` ### systemRouter {#systemrouter} 尽可能使用运行时/框架提供的路由。 ¥Use runtime/framework provided router if possible. 在 Bun 上,Elysia 将使用 [Bun.serve.routes](https://bun.sh/docs/api/http#routing) 并回退到 Elysia 自己的路由。 ¥On Bun, Elysia will use [Bun.serve.routes](https://bun.sh/docs/api/http#routing) and fallback to Elysia's own router. ## websocket {#websocket} 覆盖 websocket 配置 ¥Override websocket configuration 建议将此设置为默认值,因为 Elysia 会自动生成合适的配置来处理 WebSocket。 ¥Recommended to leave this as default as Elysia will generate suitable configuration for handling WebSocket automatically 此配置扩展了 [Bun 的 WebSocket API](https://bun.sh/docs/api/websockets) ¥This configuration extends [Bun's WebSocket API](https://bun.sh/docs/api/websockets) #### 示例 {#example-3} ¥Example ```ts import { Elysia } from 'elysia' new Elysia({ websocket: { // enable compression and decompression perMessageDeflate: true } }) ``` *** --- --- url: 'https://elysiajs.com/patterns/error-handling.md' --- # 错误处理 {#error-handling} ¥Error Handling 本页提供更高级的指南,帮助你有效地使用 Elysia 处理错误。 ¥This page provide a more advance guide for effectively handling errors with Elysia. 如果你还没有阅读 "生命周期 (onError)",我们建议你先阅读它。 ¥If you haven't read **"Life Cycle (onError)"** yet, we recommend you to read it first. ## 自定义验证消息 {#custom-validation-message} ¥Custom Validation Message 定义架构时,可以为每个字段提供自定义验证消息。 ¥When defining a schema, you can provide a custom validation message for each field. 当验证失败时,此消息将按原样返回。 ¥This message will be returned as-is when the validation fails. ```ts import { Elysia } from 'elysia' new Elysia().get('/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number({ error: 'id must be a number' // [!code ++] }) }) }) ``` 如果 `id` 字段验证失败,则返回结果为 `id must be a number`。 ¥If the validation fails on the `id` field, the response will be return as `id must be a number`. ### 验证详情 {#validation-detail} ¥Validation Detail 从 `schema.error` 返回值将按原样返回验证,但有时你可能还希望返回验证详细信息,例如字段名称和预期类型。 ¥Returning as value from `schema.error` will return the validation as-is, but sometimes you may also want to return the validation details, such as the field name and the expected type 你可以使用 `validationDetail` 选项执行此操作。 ¥You can do this by using the `validationDetail` option. ```ts import { Elysia, validationDetail } from 'elysia' // [!code ++] new Elysia().get('/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number({ error: validationDetail('id must be a number') // [!code ++] }) }) }) ``` 这将在响应中包含所有验证详细信息,例如字段名称和预期类型。 ¥This will include all of the validation details in the response, such as the field name and the expected type. 但如果你计划在每个字段中使用 `validationDetail`,手动添加它会很麻烦。 ¥But if you're planned to use `validationDetail` in every field, adding it manually can be annoying. 你可以通过在 `onError` 钩子中处理来自动添加验证详细信息。 ¥You can automatically add validation detail by handling it in `onError` hook. ```ts new Elysia() .onError(({ error, code }) => { if (code === 'VALIDATION') return error.detail(error.message) // [!code ++] }) .get('/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number({ error: 'id must be a number' }) }) }) .listen(3000) ``` 这将为每个验证错误应用自定义验证消息。 ¥This will apply every validation error with a custom message with custom validation message. ## 生产环境中的验证详情 {#validation-detail-on-production} ¥Validation Detail on production 默认情况下,如果 `NODE_ENV` 是 `production`,Elysia 将省略所有验证细节。 ¥By default, Elysia will omitted all validation detail if `NODE_ENV` is `production`. 这样做是为了防止泄露有关验证模式的敏感信息,例如字段名称和预期类型,这些信息可能被攻击者利用。 ¥This is done to prevent leaking sensitive information about the validation schema, such as field names and expected types, which could be exploited by an attacker. Elysia 将仅返回验证失败,不提供任何详细信息。 ¥Elysia will only return that validation failed without any details. ```json { "type": "validation", "on": "body", "found": {}, // Only shown for custom error "message": "x must be a number" } ``` `message` 属性是可选的,默认情况下会省略,除非你在架构中提供自定义错误消息。 ¥The `message` property is optional and is omitted by default unless you provide a custom error message in the schema. ## 自定义错误 {#custom-error} ¥Custom Error Elysia 在类型级别和实现级别都支持自定义错误。 ¥Elysia supports custom error both in the type-level and implementation level. 默认情况下,Elysia 具有一组内置错误类型,例如 `VALIDATION`、`NOT_FOUND`,它们会自动缩小类型范围。 ¥By default, Elysia have a set of built-in error types like `VALIDATION`, `NOT_FOUND` which will narrow down the type automatically. 如果 Elysia 不知道错误,错误代码将为 `UNKNOWN`,默认状态为 `500`。 ¥If Elysia doesn't know the error, the error code will be `UNKNOWN` with default status of `500` 但是,你也可以使用 `Elysia.error` 添加具有类型安全性的自定义错误,这将有助于缩小错误类型范围,从而实现完全类型安全性和自动补齐功能,并自定义状态代码,如下所示: ¥But you can also add a custom error with type safety with `Elysia.error` which will help narrow down the error type for full type safety with auto-complete, and custom status code as follows: ```typescript twoslash import { Elysia } from 'elysia' class MyError extends Error { constructor(public message: string) { super(message) } } new Elysia() .error({ MyError }) .onError(({ code, error }) => { switch (code) { // With auto-completion case 'MyError': // With type narrowing // Hover to see error is typed as `CustomError` return error } }) .get('/:id', () => { throw new MyError('Hello Error') }) ``` ### 自定义状态码 {#custom-status-code} ¥Custom Status Code 你还可以通过在自定义错误类中添加 `status` 属性,为自定义错误提供自定义状态代码。 ¥You can also provide a custom status code for your custom error by adding `status` property in your custom error class. ```typescript import { Elysia } from 'elysia' class MyError extends Error { status = 418 constructor(public message: string) { super(message) } } ``` Elysia 将在抛出错误时使用此状态码。 ¥Elysia will then use this status code when the error is thrown. 或者,你也可以在 `onError` 钩子中手动设置状态码。 ¥Otherwise you can also set the status code manually in the `onError` hook. ```typescript import { Elysia } from 'elysia' class MyError extends Error { constructor(public message: string) { super(message) } } new Elysia() .error({ MyError }) .onError(({ code, error, status }) => { switch (code) { case 'MyError': return status(418, error.message) } }) .get('/:id', () => { throw new MyError('Hello Error') }) ``` ### 自定义错误响应 {#custom-error-response} ¥Custom Error Response 你还可以在自定义错误类中提供自定义 `toResponse` 方法,以便在抛出错误时返回自定义响应。 ¥You can also provide a custom `toResponse` method in your custom error class to return a custom response when the error is thrown. ```typescript import { Elysia } from 'elysia' class MyError extends Error { status = 418 constructor(public message: string) { super(message) } toResponse() { return Response.json({ error: this.message, code: this.status }, { status: 418 }) } } ``` ## 抛出或返回 {#to-throw-or-return} ¥To Throw or Return Elysia 中的大多数错误处理可以通过抛出错误来完成,并将在 `onError` 中处理。 ¥Most of an error handling in Elysia can be done by throwing an error and will be handle in `onError`. 但 `status` 可能会有点令人困惑,因为它既可以用作返回值,也可以抛出错误。 ¥But for `status` it can be a little bit confusing, since it can be used both as a return value or throw an error. 它可以根据你的具体需求返回或抛出异常。 ¥It could either be **return** or **throw** based on your specific needs. * 如果抛出 `status` 异常,它将被 `onError` 中间件捕获。 * 如果返回的是 `status`,它将不会被 `onError` 中间件捕获。 参见以下代码: ¥See the following code: ```typescript import { Elysia, file } from 'elysia' new Elysia() .onError(({ code, error, path }) => { if (code === 418) return 'caught' }) .get('/throw', ({ status }) => { // This will be caught by onError throw status(418) }) .get('/return', ({ status }) => { // This will NOT be caught by onError return status(418) }) ``` --- --- url: 'https://elysiajs.com/integrations/nextjs.md' --- # 与 Next.js 集成 {#integration-with-nextjs} ¥Integration with Next.js 使用 Next.js App Router,我们可以在 Next.js 路由上运行 Elysia。 ¥With Next.js App Router, we can run Elysia on Next.js routes. 1. 在应用路由内创建 api/\[\[...slugs]]/route.ts 2. 在 route.ts 中,创建或导入现有的 Elysia 服务器。 3. 将 Elysia 服务器导出为默认导出 ```typescript // app/api/[[...slugs]]/route.ts import { Elysia, t } from 'elysia' export default new Elysia({ prefix: '/api' }) .get('/', () => 'hello Next') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) ``` 由于符合 WinterCG 标准,Elysia 将按预期正常工作,但是,如果你在 Node 上运行 Next.js,某些插件(例如 Elysia Static)可能无法正常工作。 ¥Elysia will work normally as expected because of WinterCG compliance, however, some plugins like **Elysia Static** may not work if you are running Next.js on Node. 你可以将 Elysia 服务器视为普通的 Next.js API 路由。 ¥You can treat the Elysia server as a normal Next.js API route. 通过这种方法,你可以将前端和后端共存于一个存储库中,并且 [使用 Eden 实现端到端类型安全](https://elysia.nodejs.cn/eden/overview.html) 可以同时执行客户端和服务器操作。 ¥With this approach, you can have co-location of both frontend and backend in a single repository and have [End-to-end type safety with Eden](https://elysia.nodejs.cn/eden/overview.html) with both client-side and server action 更多信息请参阅 [Next.js 路由处理程序](https://next.nodejs.cn/docs/app/building-your-application/routing/route-handlers#static-route-handlers)。 ¥Please refer to [Next.js Route Handlers](https://next.nodejs.cn/docs/app/building-your-application/routing/route-handlers#static-route-handlers) for more information. ## 前缀 {#prefix} ¥Prefix 由于我们的 Elysia 服务器不在应用路由的根目录中,因此需要将前缀注释到 Elysia 服务器。 ¥Because our Elysia server is not in the root directory of the app router, you need to annotate the prefix to the Elysia server. 例如,如果你将 Elysia 服务器放置在 app/user/\[\[...slugs]]/route.ts 中,则需要将 Elysia 服务器的前缀注释为 /user。 ¥For example, if you place Elysia server in **app/user/\[\[...slugs]]/route.ts**, you need to annotate prefix as **/user** to Elysia server. ```typescript // app/user/[[...slugs]]/route.ts import { Elysia, t } from 'elysia' export default new Elysia({ prefix: '/user' }) // [!code ++] .get('/', () => 'hi') .post('/', ({ body }) => body, { body: t.Object({ name: t.String() }) }) ``` 这将确保 Elysia 路由在你放置它的任何位置都能正常工作。 ¥This will ensure that Elysia routing will work properly in any location you place it. --- --- url: 'https://elysiajs.com/plugins/static.md' --- # 静态插件 {#static-plugin} ¥Static Plugin 此插件可以为 Elysia 服务器提供静态文件/文件夹。 ¥This plugin can serve static files/folders for Elysia Server 使用以下工具安装: ¥Install with: ```bash bun add @elysiajs/static ``` 然后使用它: ¥Then use it: ```typescript twoslash import { Elysia } from 'elysia' import { staticPlugin } from '@elysiajs/static' new Elysia() .use(staticPlugin()) .listen(3000) ``` 默认情况下,静态插件默认文件夹为 `public`,并使用 `/public` 前缀注册。 ¥By default, the static plugin default folder is `public`, and registered with `/public` prefix. 假设你的项目结构如下: ¥Suppose your project structure is: ``` | - src | - index.ts | - public | - takodachi.png | - nested | - takodachi.png ``` 可用路径将变为: ¥The available path will become: * /public/takodachi.png * /public/nested/takodachi.png ## 配置 {#config} ¥Config 以下是插件接受的配​​置。 ¥Below is a config which is accepted by the plugin ### assets {#assets} @default `"public"` 要作为静态公开的文件夹的路径 ¥Path to the folder to expose as static ### prefix {#prefix} @default `"/public"` 注册公共文件的路径前缀 ¥Path prefix to register public files ### ignorePatterns {#ignorepatterns} @default `[]` 应忽略不作为静态文件的文件列表 ¥List of files to ignore from serving as static files ### staticLimit {#staticlimit} @default `1024` 默认情况下,静态插件会使用静态名称将路径注册到路由,如果超出限制,路径将被延迟添加到路由以减少内存使用。在内存和性能之间进行权衡。 ¥By default, the static plugin will register paths to the Router with a static name, if the limits are exceeded, paths will be lazily added to the Router to reduce memory usage. Tradeoff memory with performance. ### alwaysStatic {#alwaysstatic} @default `false` 如果设置为 true,则静态文件路径将跳过 `staticLimits` 并注册到路由。 ¥If set to true, static files path will be registered to Router skipping the `staticLimits`. ### headers {#headers} @default `{}` 设置文件的响应标头 ¥Set response headers of files ### indexHTML {#indexhtml} @default `false` 如果设置为 true,则对于任何既不匹配路由也不匹配任何现有静态文件的请求,都将提供静态目录中的 `index.html` 文件。 ¥If set to true, the `index.html` file from the static directory will be served for any request that is matching neither a route nor any existing static file. ## 模式 {#pattern} ¥Pattern 以下是使用该插件的常见模式。 ¥Below you can find the common patterns to use the plugin. * [单个文件](#single-file) ## 单个文件 {#single-file} ¥Single file 假设你只想返回一个文件,你可以使用 `file` 而不是静态插件。 ¥Suppose you want to return just a single file, you can use `file` instead of using the static plugin ```typescript import { Elysia, file } from 'elysia' new Elysia() .get('/file', file('public/takodachi.png')) ``` --- --- url: 'https://elysiajs.com/essential/validation.md' --- # 验证 {#validation} ¥Validation 创建 API 服务器的目的是获取输入并进行处理。 ¥The purpose of creating an API server is to take an input and process it. JavaScript 允许任何数据类型。Elysia 提供了一个开箱即用的数据验证工具,以确保数据格式正确。 ¥JavaScript allows any data to be of any type. Elysia provides a tool to validate data out of the box to ensure that the data is in the correct format. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params: { id } }) => id, { params: t.Object({ id: t.Number() }) }) .listen(3000) ``` ### TypeBox {#typebox} Elysia.t 是一个基于 [TypeBox](https://github.com/sinclairzx81/typebox) 的模式构建器,它在运行时、编译时以及 OpenAPI 模式中提供类型安全,从而支持生成 OpenAPI 文档。 ¥**Elysia.t** is a schema builder based on [TypeBox](https://github.com/sinclairzx81/typebox) that provides type-safety at runtime, compile-time, and for OpenAPI schemas, enabling the generation of OpenAPI documentation. TypeBox 是一个非常快速、轻量级且类型安全的 TypeScript 运行时验证库。Elysia 扩展并自定义了 TypeBox 的默认行为,以满足服务器端验证的要求。 ¥TypeBox is a very fast, lightweight, and type-safe runtime validation library for TypeScript. Elysia extends and customizes the default behavior of TypeBox to match server-side validation requirements. 我们相信验证至少应该由框架原生处理,而不是依赖用户为每个项目设置自定义类型。 ¥We believe that validation should at least be handled by the framework natively, rather than relying on the user to set up a custom type for every project. ### 标准 Schema {#standard-schema} ¥Standard Schema Elysia 还支持 [标准 Schema](https://github.com/standard-schema/standard-schema),允许你使用你最喜欢的验证库: ¥Elysia also support [Standard Schema](https://github.com/standard-schema/standard-schema), allowing you to use your favorite validation library: * Zod * Valibot * ArkType * 效果模式 * 是的 * Joi * [以及更多](https://github.com/standard-schema/standard-schema) 要使用标准模式,只需导入模式并将其提供给路由处理程序即可。 ¥To use Standard Schema, simply import the schema and provide it to the route handler. ```typescript twoslash import { Elysia } from 'elysia' import { z } from 'zod' import * as v from 'valibot' new Elysia() .get('/id/:id', ({ params: { id }, query: { name } }) => id, { // ^? params: z.object({ id: z.coerce.number() }), query: v.object({ name: v.literal('Lilith') }) }) .listen(3000) ``` 你可以在同一个处理程序中同时使用任何验证器,而不会出现任何问题。 ¥You can use any validator together in the same handler without any issue. ### TypeScript {#typescript} 我们可以通过访问 `static` 属性来获取每个 Elysia/TypeBox 类型的类型定义,如下所示: ¥We can get type definitions of every Elysia/TypeBox's type by accessing the `static` property as follows: ```ts twoslash import { t } from 'elysia' const MyType = t.Object({ hello: t.Literal('Elysia') }) type MyType = typeof MyType.static // ^? ``` 这允许 Elysia 自动推断并提供类型,从而减少声明重复模式的需要。 ¥This allows Elysia to infer and provide type automatically, reducing the need to declare duplicate schema 单个 Elysia/TypeBox 模式可用于: ¥A single Elysia/TypeBox schema can be used for: * 运行时验证 * 数据强制转换 * TypeScript 类型 * OpenAPI 架构 这使我们能够将模式作为单一事实来源。 ¥This allows us to make a schema as a **single source of truth**. ## Schema 类型 {#schema-type} ¥Schema type Elysia 支持以下类型的声明式模式: ¥Elysia supports declarative schemas with the following types: *** 这些属性应作为路由处理程序的第三个参数提供,以验证传入的请求。 ¥These properties should be provided as the third argument of the route handler to validate the incoming request. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', () => 'Hello World!', { query: t.Object({ name: t.String() }), params: t.Object({ id: t.Number() }) }) .listen(3000) ``` 响应应如下所示: ¥The response should be as follows: | URL | 查询 | 参数 | | ------------------ | -- | -- | | /id/a | ❌ | ❌ | | /id/1?名称=Elysia | ✅ | ✅ | | /id/1?别名=Elysia | ❌ | ✅ | | /id/a?name=Elysia | ✅ | ❌ | | /id/a?alias=Elysia | ❌ | ❌ | 当提供模式时,系统将自动从模式推断类型,并为 API 文档生成 OpenAPI 类型,从而省去了手动提供类型的冗余工作。 ¥When a schema is provided, the type will be inferred from the schema automatically and an OpenAPI type will be generated for an API documentation, eliminating the redundant task of providing the type manually. ## 守护 {#guard} ¥Guard Guard 可用于将方案应用于多个处理程序。 ¥Guard can be used to apply a schema to multiple handlers. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/none', ({ query }) => 'hi') // ^? .guard({ // [!code ++] query: t.Object({ // [!code ++] name: t.String() // [!code ++] }) // [!code ++] }) // [!code ++] .get('/query', ({ query }) => query) // ^? .listen(3000) ``` 此代码确保查询必须具有名称,并且其后的每个处理程序都必须具有字符串值。响应应如下所示: ¥This code ensures that the query must have **name** with a string value for every handler after it. The response should be listed as follows: 响应应如下所示: ¥The response should be listed as follows: | 路径 | 响应 | | ------------- | ----- | | /none | hi | | /none?name=a | hi | | /query | error | | /query?name=a | a | 如果为同一属性定义了多个全局模式,则最新的模式将优先。如果同时定义了本地模式和全局模式,则本地模式将优先。 ¥If multiple global schemas are defined for the same property, the latest one will take precedence. If both local and global schemas are defined, the local one will take precedence. ### 守护模式类型 {#guard-schema-type} ¥Guard Schema Type Guard 支持两种类型来定义验证。 ¥Guard supports 2 types to define a validation. ### **覆盖(默认)** {#override-default} ¥**override (default)** 如果模式相互冲突,请覆盖模式。 ¥Override schema if schema is collide with each others. ![Elysia run with default override guard showing schema gets override](/blog/elysia-13/schema-override.webp) ### **standalone** {#standalone} 分离冲突的模式,并独立运行两者,从而对两者进行验证。 ¥Separate collided schema, and runs both independently resulting in both being validated. ![Elysia run with standalone merging multiple guard together](/blog/elysia-13/schema-standalone.webp) 要使用 `schema` 定义 schema 类型的 guard: ¥To define schema type of guard with `schema`: ```ts import { Elysia } from 'elysia' new Elysia() .guard({ schema: 'standalone', // [!code ++] response: t.Object({ title: t.String() }) }) ``` ## 正文 {#body} ¥Body 传入的 [HTTP 消息](https://web.nodejs.cn/en-US/docs/Web/HTTP/Messages) 是发送到服务器的数据。它可以采用 JSON、表单数据或任何其他格式。 ¥An incoming [HTTP Message](https://web.nodejs.cn/en-US/docs/Web/HTTP/Messages) is the data sent to the server. It can be in the form of JSON, form-data, or any other format. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/body', ({ body }) => body, { // ^? body: t.Object({ name: t.String() }) }) .listen(3000) ``` 验证应如下所示: ¥The validation should be as follows: | 正文 | 验证 | | ------------------------------------------------- | -- | | { name:'Elysia' } | ✅ | | { name:1 } | ❌ | | { alias:'Elysia' } | ❌ | | `undefined` | ❌ | Elysia 默认禁用 GET 和 HEAD 消息的 body-parser,遵循 HTTP/1.1 [RFC2616](https://www.rfc-editor.org/rfc/rfc2616#section-4.3) 规范。 ¥Elysia disables body-parser for **GET** and **HEAD** messages by default, following the specs of HTTP/1.1 [RFC2616](https://www.rfc-editor.org/rfc/rfc2616#section-4.3) > 如果请求方法未包含实体主体的定义语义,则在处理请求时应忽略消息主体。 大多数浏览器默认禁用 GET 和 HEAD 方法的主体附件。 ¥Most browsers disable the attachment of the body by default for **GET** and **HEAD** methods. #### 规范 {#specs} ¥Specs 验证传入的 [HTTP 消息](https://web.nodejs.cn/en-US/docs/Web/HTTP/Messages)(或主体)。 ¥Validate an incoming [HTTP Message](https://web.nodejs.cn/en-US/docs/Web/HTTP/Messages) (or body). 这些消息是 Web 服务器需要处理的附加消息。 ¥These messages are additional messages for the web server to process. 主体的提供方式与 `fetch` API 中的 `body` 相同。内容类型应根据定义的主体进行设置。 ¥The body is provided in the same way as the `body` in `fetch` API. The content type should be set accordingly to the defined body. ```typescript fetch('https://elysia.nodejs.cn', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Elysia' }) }) ``` ### 文件 {#file} ¥File File 是一种特殊的主体类型,可用于上传文件。 ¥File is a special type of body that can be used to upload files. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .post('/body', ({ body }) => body, { // ^? body: t.Object({ file: t.File({ format: 'image/*' }), multipleFiles: t.Files() }) }) .listen(3000) ``` 通过提供文件类型,Elysia 将自动假定内容类型为 `multipart/form-data`。 ¥By providing a file type, Elysia will automatically assume that the content-type is `multipart/form-data`. ### 文件(标准模式) {#file-standard-schema} ¥File (Standard Schema) 如果你使用的是标准模式,请注意,Elysia 将无法像 `t.File` 一样自动验证内容类型。 ¥If you're using Standard Schema, it's important that Elysia will not be able to valiate content type automatically similar to `t.File`. 但是 Elysia 导出了一个 `fileType`,可以使用魔法数字来验证文件类型。 ¥But Elysia export a `fileType` that can be used to validate file type by using magic number. ```typescript twoslash import { Elysia, fileType } from 'elysia' import { z } from 'zod' new Elysia() .post('/body', ({ body }) => body, { body: z.object({ file: z.file().refine((file) => fileType(file, 'image/jpeg')) // [!code ++] }) }) ``` 使用 `fileType` 验证文件类型非常重要,因为大多数验证器实际上并不能正确验证文件类型,例如检查内容类型及其值,这可能会导致安全漏洞。 ¥It's very important that you **should use** `fileType` to validate the file type as **most validator doesn't actually validate the file** correctly, like checking the content type the value of it which can lead to security vulnerability. ## 查询 {#query} ¥Query 查询是通过 URL 发送的数据。它可以采用 `?key=value` 的形式。 ¥Query is the data sent through the URL. It can be in the form of `?key=value`. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/query', ({ query }) => query, { // ^? query: t.Object({ name: t.String() }) }) .listen(3000) ``` 查询必须以对象的形式提供。 ¥Query must be provided in the form of an object. 验证应如下所示: ¥The validation should be as follows: | 查询 | 验证 | | ----------------------------- | -- | | /?name=Elysia | ✅ | | /?name=1 | ✅ | | /?alias=Elysia | ❌ | | /?name=ElysiaJS\&alias=Elysia | ✅ | | / | ❌ | #### 规范 {#specs-1} ¥Specs 查询字符串是 URL 的一部分,以 ? 开头,可以包含一个或多个查询参数,这些参数是用于向服务器传递附加信息的键值对,通常用于自定义行为,例如过滤或搜索。 ¥A query string is a part of the URL that starts with **?** and can contain one or more query parameters, which are key-value pairs used to convey additional information to the server, usually for customized behavior like filtering or searching. ![URL Object](/essential/url-object.svg) 在 Fetch API 中,查询在 ? 之后提供。 ¥Query is provided after the **?** in Fetch API. ```typescript fetch('https://elysia.nodejs.cn/?name=Elysia') ``` 指定查询参数时,务必理解所有查询参数值都必须以字符串形式表示。这是由它们的编码方式以及附加到 URL 的方式决定的。 ¥When specifying query parameters, it's crucial to understand that all query parameter values must be represented as strings. This is due to how they are encoded and appended to the URL. ### 强制转换 {#coercion} ¥Coercion Elysia 将自动将 `query` 上适用的模式强制转换为相应的类型。 ¥Elysia will coerce applicable schema on `query` to respective type automatically. 有关更多信息,请参阅 [Elysia 行为](/patterns/type#elysia-behavior)。 ¥See [Elysia behavior](/patterns/type#elysia-behavior) for more information. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ query }) => query, { // ^? query: t.Object({ // [!code ++] name: t.Number() // [!code ++] }) // [!code ++] }) .listen(3000) ``` ### 数组 {#array} ¥Array 默认情况下,Elysia 将查询参数视为单个字符串,即使多次指定也是如此。 ¥By default, Elysia treat query parameters as a single string even if specified multiple time. 要使用数组,我们需要将其明确声明为数组。 ¥To use array, we need to explicitly declare it as an array. ```ts twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/', ({ query }) => query, { // ^? query: t.Object({ name: t.Array(t.String()) // [!code ++] }) }) .listen(3000) ``` 一旦 Elysia 检测到某个属性可以赋值给数组,Elysia 就会将其强制转换为指定类型的数组。 ¥Once Elysia detect that a property is assignable to array, Elysia will coerce it to an array of the specified type. 默认情况下,Elysia 使用以下格式格式化查询数组: ¥By default, Elysia format query array with the following format: #### nuqs {#nuqs} 此格式由 [nuqs](https://nuqs.47ng.com) 使用。 ¥This format is used by [nuqs](https://nuqs.47ng.com). 通过使用 , 作为分隔符,属性将被视为数组。 ¥By using **,** as a delimiter, a property will be treated as array. ``` http://localhost?name=rapi,anis,neon&squad=counter { name: ['rapi', 'anis', 'neon'], squad: 'counter' } ``` #### HTML 表单格式 {#html-form-format} ¥HTML form format 如果一个键被多次赋值,该键将被视为数组。 ¥If a key is assigned multiple time, the key will be treated as an array. 这类似于 HTML 表单格式,当多次指定同名输入时。 ¥This is similar to HTML form format when an input with the same name is specified multiple times. ``` http://localhost?name=rapi&name=anis&name=neon&squad=counter // name: ['rapi', 'anis', 'neon'] ``` ## 参数 {#params} ¥Params 参数或路径参数是通过 URL 路径发送的数据。 ¥Params or path parameters are the data sent through the URL path. 它们可以采用 `/key` 的形式。 ¥They can be in the form of `/key`. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params }) => params, { // ^? params: t.Object({ id: t.Number() }) }) ``` 参数必须以对象的形式提供。 ¥Params must be provided in the form of an object. 验证应如下所示: ¥The validation should be as follows: | URL | 验证 | | ----- | -- | | /id/1 | ✅ | | /id/a | ❌ | #### 规范 {#specs-2} ¥Specs 路径参数 (不要与查询字符串或查询参数混淆)。 ¥Path parameter (not to be confused with query string or query parameter). 通常不需要此字段,因为 Elysia 可以自动从路径参数推断类型,除非需要特定的值模式,例如数值或模板字面量模式。 ¥**This field is usually not needed as Elysia can infer types from path parameters automatically**, unless there is a need for a specific value pattern, such as a numeric value or template literal pattern. ```typescript fetch('https://elysia.nodejs.cn/id/1') ``` ### 参数类型推断 {#params-type-inference} ¥Params type inference 如果没有提供 params 模式,Elysia 会自动将其类型推断为字符串。 ¥If a params schema is not provided, Elysia will automatically infer the type as a string. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/id/:id', ({ params }) => params) // ^? ``` ## 标题 {#headers} ¥Headers Headers 是通过请求标头发送的数据。 ¥Headers are the data sent through the request's header. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/headers', ({ headers }) => headers, { // ^? headers: t.Object({ authorization: t.String() }) }) ``` 与其他类型不同,Headers 默认将 `additionalProperties` 设置为 `true`。 ¥Unlike other types, headers have `additionalProperties` set to `true` by default. 这意味着 headers 可以包含任何键值对,但值必须与 schema 匹配。 ¥This means that headers can have any key-value pair, but the value must match the schema. #### 规范 {#specs-3} ¥Specs HTTP 标头允许客户端和服务器通过 HTTP 请求或响应传递附加信息,通常被视为元数据。 ¥HTTP headers let the client and the server pass additional information with an HTTP request or response, usually treated as metadata. 此字段通常用于强制某些特定的标头字段,例如 `Authorization`。 ¥This field is usually used to enforce some specific header fields, for example, `Authorization`. 标头的提供方式与 `fetch` API 中的 `body` 相同。 ¥Headers are provided in the same way as the `body` in `fetch` API. ```typescript fetch('https://elysia.nodejs.cn/', { headers: { authorization: 'Bearer 12345' } }) ``` ::: tip 提示 Elysia 将仅将标头解析为小写键。 ¥Elysia will parse headers as lower-case keys only. 请确保使用标头验证时字段名称小写。 ¥Please make sure that you are using lower-case field names when using header validation. ::: ## Cookie {#cookie} Cookie 是通过请求的 Cookie 发送的数据。 ¥Cookie is the data sent through the request's cookie. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/cookie', ({ cookie }) => cookie, { // ^? cookie: t.Cookie({ cookieName: t.String() }) }) ``` Cookie 必须以 `t.Cookie` 或 `t.Object` 的形式提供。 ¥Cookies must be provided in the form of `t.Cookie` or `t.Object`. 与 `headers` 相同,cookie 默认将 `additionalProperties` 设置为 `true`。 ¥Same as `headers`, cookies have `additionalProperties` set to `true` by default. #### 规范 {#specs-4} ¥Specs HTTP Cookie 是服务器发送给客户端的一小段数据。它是每次访问同一 Web 服务器时发送的数据,以便服务器记住客户端信息。 ¥An HTTP cookie is a small piece of data that a server sends to the client. It's data that is sent with every visit to the same web server to let the server remember client information. 简而言之,它是随每个请求发送的字符串化状态。 ¥In simpler terms, it's a stringified state that is sent with every request. 此字段通常用于强制某些特定的 Cookie 字段。 ¥This field is usually used to enforce some specific cookie fields. Cookie 是一个特殊的标头字段,Fetch API 不接受自定义值,而是由浏览器管理。要发送 Cookie,你必须改用 `credentials` 字段: ¥A cookie is a special header field that the Fetch API doesn't accept a custom value for but is managed by the browser. To send a cookie, you must use a `credentials` field instead: ```typescript fetch('https://elysia.nodejs.cn/', { credentials: 'include' }) ``` ### t.Cookie {#tcookie} `t.Cookie` 是一种特殊类型,相当于 `t.Object`,但允许设置特定于 cookie 的选项。 ¥`t.Cookie` is a special type that is equivalent to `t.Object` but allows to set cookie-specific options. ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .get('/cookie', ({ cookie }) => cookie.name.value, { // ^? cookie: t.Cookie({ name: t.String() }, { secure: true, httpOnly: true }) }) ``` ## 响应 {#response} ¥Response 响应是从处理程序返回的数据。 ¥Response is the data returned from the handler. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/response', () => { return { name: 'Jane Doe' } }, { response: t.Object({ name: t.String() }) }) ``` ### 每个状态的响应 {#response-per-status} ¥Response per status 可以根据状态码设置响应。 ¥Responses can be set per status code. ```typescript import { Elysia, t } from 'elysia' new Elysia() .get('/response', ({ status }) => { if (Math.random() > 0.5) return status(400, { error: 'Something went wrong' }) return { name: 'Jane Doe' } }, { response: { 200: t.Object({ name: t.String() }), 400: t.Object({ error: t.String() }) } }) ``` 这是 Elysia 特有的功能,允许我们将字段设为可选。 ¥This is an Elysia-specific feature, allowing us to make a field optional. ## 错误提供程序 {#error-provider} ¥Error Provider 验证失败时,有两种方法可以自定义错误消息: ¥There are two ways to provide a custom error message when the validation fails: 1. 内联 `status` 属性 2. 使用 [onError](/essential/life-cycle.html#on-error) 事件 ### 错误属性 {#error-property} ¥Error Property Elysia 提供了一个额外的错误属性,允许我们在字段无效时返回自定义错误消息。 ¥Elysia offers an additional **error** property, allowing us to return a custom error message if the field is invalid. ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/', () => 'Hello World!', { body: t.Object({ x: t.Number({ error: 'x must be a number' }) }) }) .listen(3000) ``` 以下是在各种类型上使用 error 属性的示例: ¥The following is an example of using the error property on various types: ```typescript t.String({ format: 'email', error: 'Invalid email :(' }) ``` ``` Invalid Email :( ``` ```typescript t.Array( t.String(), { error: 'All members must be a string' } ) ``` ``` All members must be a string ``` ```typescript t.Object({ x: t.Number() }, { error: 'Invalid object UnU' }) ``` ``` Invalid object UnU ``` ```typescript t.Object({ x: t.Number({ error({ errors, type, validation, value }) { return 'Expected x to be a number' } }) }) ``` ``` Expected x to be a number ``` ## 自定义错误 {#custom-error} ¥Custom Error TypeBox 提供了一个额外的 "error" 属性,允许我们在字段无效时返回自定义错误消息。 ¥TypeBox offers an additional "**error**" property, allowing us to return a custom error message if the field is invalid. ```typescript t.String({ format: 'email', error: 'Invalid email :(' }) ``` ``` Invalid Email :( ``` ```typescript t.Object({ x: t.Number() }, { error: 'Invalid object UnU' }) ``` ``` Invalid object UnU ``` ### 错误消息作为函数 {#error-message-as-function} ¥Error message as function 除了字符串之外,Elysia 类型的错误还可以接受一个函数,以编程方式为每个属性返回自定义错误。 ¥In addition to a string, Elysia type's error can also accept a function to programmatically return a custom error for each property. 错误函数接受与 `ValidationError` 相同的参数 ¥The error function accepts the same arguments as `ValidationError` ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/', () => 'Hello World!', { body: t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) }) .listen(3000) ``` ::: tip 提示 将鼠标悬停在 `error` 上即可查看类型。 ¥Hover over the `error` to see the type. ::: ### 每个字段调用错误 {#error-is-called-per-field} ¥Error is Called Per Field 请注意,只有字段无效时才会调用错误函数。 ¥Please note that the error function will only be called if the field is invalid. 请参考下表: ¥Please consider the following table: ```typescript t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) ``` ```json { x: "hello" } ``` ```typescript t.Object({ x: t.Number({ error() { return 'Expected x to be a number' } }) }) ``` ```json "hello" ``` ```typescript t.Object( { x: t.Number({ error() { return 'Expected x to be a number' } }) }, { error() { return 'Expected value to be an object' } } ) ``` ```json "hello" ``` ### onError {#onerror} 我们可以通过将错误代码缩小到 "VALIDATION" 来自定义基于 [onError](/essential/life-cycle.html#on-error) 事件的验证行为。 ¥We can customize the behavior of validation based on the [onError](/essential/life-cycle.html#on-error) event by narrowing down the error code to "**VALIDATION**". ```typescript twoslash import { Elysia, t } from 'elysia' new Elysia() .onError(({ code, error }) => { if (code === 'VALIDATION') return error.message }) .listen(3000) ``` 从 elysia/error 导入后,缩小的错误类型将被定义为 `ValidationError`。 ¥The narrowed-down error type will be typed as `ValidationError` imported from **elysia/error**. ValidationError 公开一个名为 validator 的属性,类型为 [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck),允许我们开箱即用地与 TypeBox 功能进行交互。 ¥**ValidationError** exposes a property named **validator**, typed as [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck), allowing us to interact with TypeBox functionality out of the box. ```typescript import { Elysia, t } from 'elysia' new Elysia() .onError(({ code, error }) => { if (code === 'VALIDATION') return error.all[0].message }) .listen(3000) ``` ### 错误列表 {#error-list} ¥Error List ValidationError 提供了一个方法 `ValidatorError.all`,允许我们列出所有错误原因。 ¥**ValidationError** provides a method `ValidatorError.all`, allowing us to list all of the error causes. ```typescript import { Elysia, t } from 'elysia' new Elysia() .post('/', ({ body }) => body, { body: t.Object({ name: t.String(), age: t.Number() }), error({ code, error }) { switch (code) { case 'VALIDATION': console.log(error.all) // Find a specific error name (path is OpenAPI Schema compliance) const name = error.all.find( (x) => x.summary && x.path === '/name' ) // If there is a validation error, then log it if(name) console.log(name) } } }) .listen(3000) ``` 有关 TypeBox 验证器的更多信息,请参阅 [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck)。 ¥For more information about TypeBox's validator, see [TypeCheck](https://github.com/sinclairzx81/typebox#typecheck). ## 参考模型 {#reference-model} ¥Reference Model 有时你可能会发现自己声明了重复的模型或多次重复使用同一个模型。 ¥Sometimes you might find yourself declaring duplicate models or re-using the same model multiple times. 通过引用模型,我们可以命名模型并通过引用名称来重用它。 ¥With a reference model, we can name our model and reuse it by referencing the name. 让我们从一个简单的场景开始。 ¥Let's start with a simple scenario. 假设我们有一个使用相同模型处理登录的控制器。 ¥Suppose we have a controller that handles sign-in with the same model. ```typescript twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .post('/sign-in', ({ body }) => body, { body: t.Object({ username: t.String(), password: t.String() }), response: t.Object({ username: t.String(), password: t.String() }) }) ``` 我们可以通过将模型提取为变量并引用它来重构代码。 ¥We can refactor the code by extracting the model as a variable and referencing it. ```typescript twoslash import { Elysia, t } from 'elysia' // Maybe in a different file eg. models.ts const SignDTO = t.Object({ username: t.String(), password: t.String() }) const app = new Elysia() .post('/sign-in', ({ body }) => body, { body: SignDTO, response: SignDTO }) ``` 这种分离关注点的方法是一种有效的方法,但随着应用变得越来越复杂,我们可能会发现自己需要重复使用具有不同控制器的多个模型。 ¥This method of separating concerns is an effective approach, but we might find ourselves reusing multiple models with different controllers as the app gets more complex. 我们可以通过创建 "参考模型" 来解决这个问题,允许我们命名模型并使用自动补齐功能,通过将模型注册到 `model`,在 `schema` 中直接引用它。 ¥We can resolve that by creating a "reference model", allowing us to name the model and use auto-completion to reference it directly in `schema` by registering the models with `model`. ```typescript twoslash import { Elysia, t } from 'elysia' const app = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) .post('/sign-in', ({ body }) => body, { // with auto-completion for existing model name body: 'sign', response: 'sign' }) ``` 当我们想要访问模型组时,可以将 `model` 分离为一个插件,该插件注册后将提供一组模型,而不是多次导入。 ¥When we want to access the model's group, we can separate a `model` into a plugin, which when registered will provide a set of models instead of multiple imports. ```typescript // auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) ``` 然后在实例文件中: ¥Then in an instance file: ```typescript twoslash // @filename: auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ sign: t.Object({ username: t.String(), password: t.String() }) }) // @filename: index.ts // ---cut--- // index.ts import { Elysia } from 'elysia' import { authModel } from './auth.model' const app = new Elysia() .use(authModel) .post('/sign-in', ({ body }) => body, { // with auto-completion for existing model name body: 'sign', response: 'sign' }) ``` 这种方法不仅允许我们分离关注点,还使我们能够在将模型集成到 OpenAPI 文档中时在多个位置重用该模型。 ¥This approach not only allows us to separate concerns but also enables us to reuse the model in multiple places while integrating the model into OpenAPI documentation. ### 多个模型 {#multiple-models} ¥Multiple Models `model` 接受一个对象,其键作为模型名称,值作为模型定义。默认支持多个模型。 ¥`model` accepts an object with the key as a model name and the value as the model definition. Multiple models are supported by default. ```typescript // auth.model.ts import { Elysia, t } from 'elysia' export const authModel = new Elysia() .model({ number: t.Number(), sign: t.Object({ username: t.String(), password: t.String() }) }) ``` ### 命名约定 {#naming-convention} ¥Naming Convention 重复的模型名称将导致 Elysia 抛出错误。为了防止声明重复的模型名称,我们可以使用以下命名约定。 ¥Duplicate model names will cause Elysia to throw an error. To prevent declaring duplicate model names, we can use the following naming convention. 假设我们将所有模型存储在 `models/.ts` 中,并将模型的前缀声明为命名空间。 ¥Let's say that we have all models stored at `models/.ts` and declare the prefix of the model as a namespace. ```typescript import { Elysia, t } from 'elysia' // admin.model.ts export const adminModels = new Elysia() .model({ 'admin.auth': t.Object({ username: t.String(), password: t.String() }) }) // user.model.ts export const userModels = new Elysia() .model({ 'user.auth': t.Object({ username: t.String(), password: t.String() }) }) ``` 这可以在一定程度上防止命名重复,但最终最好让你的团队自行决定命名约定。 ¥This can prevent naming duplication to some extent, but ultimately, it's best to let your team decide on the naming convention. Elysia 提供了一个灵活的选项,有助于避免决策疲劳。 ¥Elysia provides an opinionated option to help prevent decision fatigue.