From d0dacbb21e93ff65d5d77ded17c9b9b9e2b69a65 Mon Sep 17 00:00:00 2001 From: expressgy Date: Tue, 18 Mar 2025 18:17:32 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 12 + .gitignore | 58 + .npmrc | 10 + README.md | 58 + config/index.js | 59 + doc/fastify-docs/README.md | 41 + doc/fastify-docs/docs/Benchmarking.md | 49 + doc/fastify-docs/docs/ContentTypeParser.md | 193 +++ doc/fastify-docs/docs/Contributing.md | 28 + doc/fastify-docs/docs/Decorators.md | 262 ++++ doc/fastify-docs/docs/Encapsulation.md | 167 +++ doc/fastify-docs/docs/Errors.md | 170 +++ doc/fastify-docs/docs/Fluent-Schema.md | 115 ++ doc/fastify-docs/docs/Getting-Started.md | 418 ++++++ doc/fastify-docs/docs/HTTP2.md | 89 ++ doc/fastify-docs/docs/Hooks.md | 579 +++++++ doc/fastify-docs/docs/LTS.md | 54 + doc/fastify-docs/docs/Lifecycle.md | 78 + doc/fastify-docs/docs/Logging.md | 161 ++ doc/fastify-docs/docs/Middleware.md | 28 + doc/fastify-docs/docs/Migration-Guide-V3.md | 241 +++ doc/fastify-docs/docs/Plugins-Guide.md | 376 +++++ doc/fastify-docs/docs/Plugins.md | 186 +++ doc/fastify-docs/docs/Recommendations.md | 230 +++ doc/fastify-docs/docs/Reply.md | 431 ++++++ doc/fastify-docs/docs/Request.md | 60 + doc/fastify-docs/docs/Routes.md | 498 ++++++ doc/fastify-docs/docs/Server.md | 1217 +++++++++++++++ doc/fastify-docs/docs/Serverless.md | 310 ++++ doc/fastify-docs/docs/Testing.md | 294 ++++ doc/fastify-docs/docs/TypeScript.md | 1250 ++++++++++++++++ .../docs/Validation-and-Serialization.md | 822 ++++++++++ doc/fastify-docs/docs/Write-Plugin.md | 59 + .../resources/encapsulation_context.drawio | 1 + .../docs/resources/encapsulation_context.svg | 3 + package-lock.json | 1330 +++++++++++++++++ package.json | 37 + src/application.js | 23 + src/plugins/index.js | 13 + src/routes/index.js | 13 + src/utils/logger.js | 42 + src/utils/start.js | 12 + 42 files changed, 10077 insertions(+) create mode 100644 .env create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 README.md create mode 100644 config/index.js create mode 100644 doc/fastify-docs/README.md create mode 100644 doc/fastify-docs/docs/Benchmarking.md create mode 100644 doc/fastify-docs/docs/ContentTypeParser.md create mode 100644 doc/fastify-docs/docs/Contributing.md create mode 100644 doc/fastify-docs/docs/Decorators.md create mode 100644 doc/fastify-docs/docs/Encapsulation.md create mode 100644 doc/fastify-docs/docs/Errors.md create mode 100644 doc/fastify-docs/docs/Fluent-Schema.md create mode 100644 doc/fastify-docs/docs/Getting-Started.md create mode 100644 doc/fastify-docs/docs/HTTP2.md create mode 100644 doc/fastify-docs/docs/Hooks.md create mode 100644 doc/fastify-docs/docs/LTS.md create mode 100644 doc/fastify-docs/docs/Lifecycle.md create mode 100644 doc/fastify-docs/docs/Logging.md create mode 100644 doc/fastify-docs/docs/Middleware.md create mode 100644 doc/fastify-docs/docs/Migration-Guide-V3.md create mode 100644 doc/fastify-docs/docs/Plugins-Guide.md create mode 100644 doc/fastify-docs/docs/Plugins.md create mode 100644 doc/fastify-docs/docs/Recommendations.md create mode 100644 doc/fastify-docs/docs/Reply.md create mode 100644 doc/fastify-docs/docs/Request.md create mode 100644 doc/fastify-docs/docs/Routes.md create mode 100644 doc/fastify-docs/docs/Server.md create mode 100644 doc/fastify-docs/docs/Serverless.md create mode 100644 doc/fastify-docs/docs/Testing.md create mode 100644 doc/fastify-docs/docs/TypeScript.md create mode 100644 doc/fastify-docs/docs/Validation-and-Serialization.md create mode 100644 doc/fastify-docs/docs/Write-Plugin.md create mode 100644 doc/fastify-docs/docs/resources/encapsulation_context.drawio create mode 100644 doc/fastify-docs/docs/resources/encapsulation_context.svg create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/application.js create mode 100644 src/plugins/index.js create mode 100644 src/routes/index.js create mode 100644 src/utils/logger.js create mode 100644 src/utils/start.js diff --git a/.env b/.env new file mode 100644 index 0000000..9106155 --- /dev/null +++ b/.env @@ -0,0 +1,12 @@ +# 服务器配置 +PORT=9000 +HOST=0.0.0.0 +BACKLOG=511 + +# 日志配置 +LOG_LEVEL=info +LOG_FILE=./logs/app.log + +# 插件配置 +PLUGIN_NAME=myapp +API_PREFIX=/api/v1 \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52962c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history + +# 0x +profile-* + +# mac files +.DS_Store + +# vim swap files +*.swp + +# webstorm +.idea + +# vscode +.vscode +*code-workspace + +# clinic +profile* +*clinic* +*flamegraph* diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..5fd87b4 --- /dev/null +++ b/.npmrc @@ -0,0 +1,10 @@ +registry=https://registry.npmmirror.com/ +ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ +ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ +ELECTRON_CUSTOM_DIR="{{ version }}" +msvs_version=2022 +#offline=true +prefer-offline=true +no-proxy=true +#https-proxy="http://127.0.0.1:7890" +#proxy="http://127.0.0.1:7890" diff --git a/README.md b/README.md new file mode 100644 index 0000000..297be58 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# RBAC 权限管理系统 + +基于 Fastify 的 Node.js 角色访问控制(RBAC)系统,提供灵活的权限管理能力。 + +## 主要特性 +- 🛡️ 基于 JWT 的身份认证 +- 🔑 角色-权限层级管理 +- 🚦 请求权限验证中间件 +- 📦 支持 PostgreSQL/MySQL 数据库 +- 📝 审计日志记录 + +## 快速开始 + +### 前置要求 +- Node.js v18+ +- 数据库(PostgreSQL/MySQL) +- Redis(用于会话管理) + +### 安装步骤 +```bash +# 克隆仓库 +git clone https://github.com/your-repo.git +cd rbac-system + +# 安装依赖 +npm install + +# 配置环境变量(复制示例文件) +cp .env.example .env + +# 数据库迁移 +npx prisma migrate dev + +# 启动服务 +npm run dev + +## 项目结构 +```bash +├── src/ +│ ├── routes/ # API 路由 +│ │ ├── index.js # 路由入口 +│ │ ├── user.js # 用户相关路由 +│ ├── plugins/ # Fastify 插件 +│ │ ├── db.js # 数据库连接 +│ ├── services/ # 业务逻辑层 +│ │ ├── userService.js # 用户服务 +│ ├── utils/ # 工具类 +│ │ ├── logger.js # 日志工具 +│ ├── app.js # 应用入口 +├── test/ # 测试用例 +│ ├── routes/ +│ │ ├── user.test.js # 用户路由测试 +│ ├── utils/ +│ │ ├── logger.test.js # 日志工具测试 +├── config/ # 配置文件 +│ ├── default.js # 通用配置 +├── package.json # 依赖管理 +``` \ No newline at end of file diff --git a/config/index.js b/config/index.js new file mode 100644 index 0000000..020612a --- /dev/null +++ b/config/index.js @@ -0,0 +1,59 @@ +import { config } from 'dotenv' +import path from 'path' + +// 加载环境变量 +config({ path: path.resolve(process.cwd(), '.env') }) +console.log("[NODE_ENV]:", process.env.NODE_ENV); + + +// 基础配置 +const baseConfig = { + NODE_ENV: process.env.NODE_ENV || 'development', + server: { + port: process.env.PORT || 9000, + host: process.env.HOST || 'localhost', + backlog: process.env.BACKLOG || 511 + }, + logger: { + level: process.env.LOG_LEVEL || 'trace', + filePath: process.env.LOG_FILE || '../logs/app.log', + console: false, + }, + pluginOptions: { + name: process.env.PLUGIN_NAME || 'default', + prefix: process.env.API_PREFIX || '/api' + } +} + +// 环境特定配置 +const envConfig = { + production: { + server: { + host: '0.0.0.0', + }, + logger: { + level: 'warn', + } + }, + development: { + logger: { + level: 'trace', + console: true, + } + } +} + +// 使用深度合并代替展开操作符 +function deepMerge(target, source) { + for (const key of Object.keys(source)) { + if (source[key] instanceof Object && key in target) { + Object.assign(source[key], deepMerge(target[key], source[key])) + } + } + return Object.assign({}, target, source) +} + +export default deepMerge( + baseConfig, + envConfig[process.env.NODE_ENV] || envConfig.development +) \ No newline at end of file diff --git a/doc/fastify-docs/README.md b/doc/fastify-docs/README.md new file mode 100644 index 0000000..4c5ca5e --- /dev/null +++ b/doc/fastify-docs/README.md @@ -0,0 +1,41 @@ +# Fastify 文档中文翻译 + +### 文档目录 +* [入门](docs/Getting-Started.md) +* [服务器方法](docs/Server.md) +* [路由](docs/Routes.md) +* [日志](docs/Logging.md) +* [中间件](docs/Middleware.md) +* [钩子方法](docs/Hooks.md) +* [装饰器](docs/Decorators.md) +* [验证和序列化](docs/Validation-and-Serialization.md) +* [生命周期](docs/Lifecycle.md) +* [回复](docs/Reply.md) +* [请求](docs/Request.md) +* [错误](docs/Errors.md) +* [Content-Type 解析](docs/ContentTypeParser.md) +* [插件](docs/Plugins.md) +* [测试](docs/Testing.md) +* [基准测试](docs/Benchmarking.md) +* [如何写一个好的插件](docs/Write-Plugin.md) +* [插件指南](docs/Plugins-Guide.md) +* [HTTP2](docs/HTTP2.md) +* [长期支持计划](docs/LTS.md) +* [TypeScript 与类型支持](docs/TypeScript.md) +* [Serverless](docs/Serverless.md) +* [推荐方案](docs/Recommendations.md) + +### 其他版本 + +* [v2.x](https://github.com/fastify/docs-chinese/tree/2.x) + +### 翻译指南 + +想要参与到项目当中?欢迎! +为了提供一致的翻译体验,请在开始翻译前参阅[翻译指南](docs/Contributing.md)。感谢! + +### 贡献者 +* [@fralonra](https://github.com/fralonra) +* [@poppinlp](https://github.com/poppinlp) +* [@vincent178](https://github.com/vincent178) +* [@xtx1130](https://github.com/xtx1130) diff --git a/doc/fastify-docs/docs/Benchmarking.md b/doc/fastify-docs/docs/Benchmarking.md new file mode 100644 index 0000000..a97c762 --- /dev/null +++ b/doc/fastify-docs/docs/Benchmarking.md @@ -0,0 +1,49 @@ +

Fastify

+ +## 基准测试 +基准测试对于衡量改动可能引起的性能变化很重要. 从用户和贡献者的角度, 我们提供了简便的方法测试你的应用. 这套配置可以自动化你的基准测试, 从不同的分支和不同的 Node.js 的版本. + +我们将使用以下模块: +- [Autocannon](https://github.com/mcollina/autocannon): 用 Node 写的 HTTP/1.1 基准测试工具. +- [Branch-comparer](https://github.com/StarpTech/branch-comparer): 切换不同的 git 分支, 执行脚本并记录结果. +- [Concurrently](https://github.com/kimmobrunfeldt/concurrently): 并行运行命令. +- [Npx](https://github.com/npm/npx): NPM 包运行器,用于在不同的 Node.js 版本上执行运行脚本和运行本地的二进制文件. 在 npm@5.2.0 版本以上可用. + +## 基本用法 + +### 在当前分支上运行测试 +```sh +npm run benchmark +``` + +### 在不同的 Node.js 版本中运行测试 ✨ +```sh +npx -p node@6 -- npm run benchmark +``` + +## 进阶用法 + +### 在不同的分支上运行测试 +```sh +branchcmp --rounds 2 --script "npm run benchmark" +``` + +### 在不同的分支和不同的 Node.js 版本中运行测试 ✨ +```sh +branchcmp --rounds 2 --script "npm run benchmark" +``` + +### 比较当前的分支和主分支 (Gitflow) +```sh +branchcmp --rounds 2 --gitflow --script "npm run benchmark" +``` +or +```sh +npm run bench +``` + +### 运行不同的用例 + +```sh +branchcmp --rounds 2 -s "node ./node_modules/concurrently -k -s first \"node ./examples/asyncawait.js\" \"node ./node_modules/autocannon -c 100 -d 5 -p 10 localhost:3000/\"" +``` diff --git a/doc/fastify-docs/docs/ContentTypeParser.md b/doc/fastify-docs/docs/ContentTypeParser.md new file mode 100644 index 0000000..66cdddf --- /dev/null +++ b/doc/fastify-docs/docs/ContentTypeParser.md @@ -0,0 +1,193 @@ +

Fastify

+ +## `Content-Type` 解析 +Fastify 原生只支持 `'application/json'` 和 `'text/plain'` content type。默认的字符集是 `utf-8`。如果你需要支持其他的 content type,你需要使用 `addContentTypeParser` API。*默认的 JSON 或者纯文本解析器也可以被更改或删除。* + +*注:假如你决定用 `Content-Type` 自定义 content type,UTF-8 便不再是默认的字符集了。请确保如下包含该字符集:`text/html; charset=utf-8`。* + +和其他的 API 一样,`addContentTypeParser` 被封装在定义它的作用域中了。这就意味着如果你定义在了根作用域中,那么就是全局可用,如果你定义在一个插件中,那么它只能在那个作用域和子作用域中可用。 + +Fastify 自动将解析好的 payload 添加到 [Fastify request](Request.md) 对象,你能通过 `request.body` 访问。 + +### 用法 +```js +fastify.addContentTypeParser('application/jsoff', function (request, payload, done) { + jsoffParser(payload, function (err, body) { + done(err, body) + }) +}) + +// 以相同方式处理多种 content type +fastify.addContentTypeParser(['text/xml', 'application/xml'], function (request, payload, done) { + xmlParser(payload, function (err, body) { + done(err, body) + }) +}) + +// Node 版本 >= 8.0.0 时也支持 async +fastify.addContentTypeParser('application/jsoff', async function (request, payload) { + var res = await jsoffParserAsync(payload) + + return res +}) + +// 处理所有匹配的 content type +fastify.addContentTypeParser(/^image\/.*/, function (request, payload, done) { + imageParser(payload, function (err, body) { + done(err, body) + }) +}) + +// 可以为不同的 content type 使用默认的 JSON/Text 解析器 +fastify.addContentTypeParser('text/json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore')) +``` + +在使用正则匹配 content-type 解析器之前,Fastify 首先会查找解析出 `string` 类型值的解析器。假如提供了重复的类型,那么 Fastify 会按照提供的顺序反向查找。因此,你可以先指定一般的 content type,之后再指定更为特殊的类型,正如下面的例子一样。 + +```js +// 只会调用第二个解析器,因为它也匹配了第一个解析器的类型 +fastify.addContentTypeParser('application/vnd.custom+xml', (request, body, done) => {} ) +fastify.addContentTypeParser('application/vnd.custom', (request, body, done) => {} ) + +// 这才是我们期望的行为。因为 Fastify 会首先尝试匹配 `application/vnd.custom+xml` content type 解析器 +fastify.addContentTypeParser('application/vnd.custom', (request, body, done) => {} ) +fastify.addContentTypeParser('application/vnd.custom+xml', (request, body, done) => {} ) +``` + +在 `hasContentTypeParser` 之外,还有其他 API 可供使用,它们是:`hasContentTypeParser`、`removeContentTypeParser` 与 `removeAllContentTypeParsers`。 + +#### hasContentTypeParser + +使用 `hasContentTypeParser` API 来查询是否存在特定的 content type 解析器。 + +```js +if (!fastify.hasContentTypeParser('application/jsoff')){ + fastify.addContentTypeParser('application/jsoff', function (request, payload, done) { + jsoffParser(payload, function (err, body) { + done(err, body) + }) + }) +} +``` + +#### removeContentTypeParser + +通过 `removeContentTypeParser` 可移除一个或多个 content type 解析器。支持使用 `string` 或 +`RegExp` 来匹配。 + +```js +fastify.addContentTypeParser('text/xml', function (request, payload, done) { + xmlParser(payload, function (err, body) { + done(err, body) + }) +}) + +// 移除内建的 content type 解析器。这时只有上文添加的 text/html 解析器可用。 +Fastiy.removeContentTypeParser(['application/json', 'text/plain']) +``` + +#### removeAllContentTypeParsers + +在上文的例子中,你需要明确指定所有你想移除的 content type。但你也可以使用 `removeAllContentTypeParsers`直接移除所有现存的 content type 解析器。在下面的例子里,我们实现了一样的效果,但不再需要手动指定 content type 了。和 `removeContentTypeParser` 一样,该 API 也支持封装。当你想注册一个[能捕获所有 content type 的解析器](#Catch-All),且忽略内建的解析器时,这个 API 特别有用。 + +```js +Fastiy.removeAllContentTypeParsers() + +fastify.addContentTypeParser('text/xml', function (request, payload, done) { + xmlParser(payload, function (err, body) { + done(err, body) + }) +}) +``` + +**注意**:早先的写法 `function(req, done)` 与 `async function(req)` 仍被支持,但不推荐使用。 + +#### Body Parser + +你可以用两种方式解析消息主体。第一种方法在上面演示过了: 你可以添加定制的 content type 解析器来处理请求。第二种方法你可以在 `addContentTypeParser` API 传递 `parseAs` 参数。它可以是 `'string'` 或者 `'buffer'`。如果你使用 `parseAs` 选项 Fastify 会处理 stream 并且进行一些检查,比如消息主体的 [最大尺寸](Factory.md#factory-body-limit) 和消息主体的长度。如果达到了某些限制,自定义的解析器就不会被调用。 + +```js +fastify.addContentTypeParser('application/json', { parseAs: 'string' }, function (req, body, done) { + try { + var json = JSON.parse(body) + done(null, json) + } catch (err) { + err.statusCode = 400 + done(err, undefined) + } +}) +``` + +查看例子 [`example/parser.js`](https://github.com/fastify/fastify/blob/main/examples/parser.js)。 + +##### 自定义解析器的选项 ++ `parseAs` (string): `'string'` 或者 `'buffer'` 定义了如何收集进来的数据。默认是 `'buffer'`。 ++ `bodyLimit` (number): 自定义解析器能够接收的最大的数据长度,比特为单位。默认是全局的消息主体的长度限制[`Fastify 工厂方法`](Factory.md#bodylimit)。 + +#### 捕获所有 + +有些情况下你需要捕获所有的 content type。通过 Fastify,你只需添加`'*'` content type。 + +```js +fastify.addContentTypeParser('*', function (request, payload, done) { + var data = '' + payload.on('data', chunk => { data += chunk }) + payload.on('end', () => { + done(null, data) + }) +}) +``` + +在这种情况下,所有的没有特定 content type 解析器的请求都会被这个方法处理。 + +对请求流 (stream) 执行管道输送 (pipe) 操作也是有用的。你可以如下定义一个 content 解析器: + +```js +fastify.addContentTypeParser('*', function (request, payload, done) { + done() +}) +``` + +之后通过核心 HTTP request 对象将请求流直接输送到任意位置: + +```js +app.post('/hello', (request, reply) => { + reply.send(request.raw) +}) +``` + +这里有一个将来访的 [json line](https://jsonlines.org/) 对象完整输出到日志的例子: + +```js +const split2 = require('split2') +const pump = require('pump') + + fastify.addContentTypeParser('*', (request, payload, done) => { + done(null, pump(payload, split2(JSON.parse))) +}) + +fastify.route({ + method: 'POST', + url: '/api/log/jsons', + handler: (req, res) => { + req.body.on('data', d => console.log(d)) // 记录每个来访的对象 + } +}) +``` + +关于处理上传的文件,请看[该插件](https://github.com/fastify/fastify-multipart)。 + +如果你真的想将某解析器用于所有 content type,而不仅用于缺少具体解析器的 content type,你应该先调用 `removeAllContentTypeParsers` 方法。 + +```js +// 没有下面这行的话,content type 为 application/json 的 body 将被内建的 json 解析器处理。 +fastify.removeAllContentTypeParsers() + +fastify.addContentTypeParser('*', function (request, payload, done) { + var data = '' + payload.on('data', chunk => { data += chunk }) + payload.on('end', () => { + done(null, data) + }) +}) +``` \ No newline at end of file diff --git a/doc/fastify-docs/docs/Contributing.md b/doc/fastify-docs/docs/Contributing.md new file mode 100644 index 0000000..74f6e49 --- /dev/null +++ b/doc/fastify-docs/docs/Contributing.md @@ -0,0 +1,28 @@ +## 翻译指南 + +如果你发现了翻译的错误,或文档的滞后,请创建一个 issue 来告诉我们,或是按如下方法直接参与翻译。感谢! + +* Fork https://github.com/fastify/docs-chinese +* 创建 Git Branch +* 开始翻译 +* 创建 PR,尽量保证一篇文档一个 commit 一个 PR + +### 风格规范 + +为了保证翻译风格的统一,请按照如下规范进行翻译。 +你可以在[这里](https://github.com/fastify/docs-chinese/issues/66)一起参与规范的讨论。 + +- 所有翻译的文本内容采用 markdown 编写,支持 GMF,不熟悉的话可以参考如下链接: + - https://help.github.com/articles/getting-started-with-writing-and-formatting-on-github/ + - https://help.github.com/categories/writing-on-github/ +- 中文与英文之间需要加一个空格,例如 `你好 hello 你好` +- 中文与数字之间需要加一个空格,例如 `2018 年 12 月 20 日` +- 正文中的英文标点需要转换为对应的中文标点,例如 `,` => `,` + - 例外:英文括号修改为半角括号加一个空格的形式,例如 `()` => ` () ` + - 注意:英文逗号可能需要转换为中文顿号,而非中文逗号 +- 代码片段中的注释需要翻译 +- 不做翻译的内容: + - 常见的缩写内容,例如 `UI`, `HTTP` + - 常见的框架、平台、语言等,例如 `Express`, `Node`, `Java` + - 用于表示工程代码中的一些实例,例如 `Promise`, Node 中的 `Request` +- 对于可能产生误解的名词翻译,如果在术语表中有,请按照对应的内容做翻译;如果术语表中未出现,那么推荐翻译后括号注解原文,例如 `运行时(Runtime)` diff --git a/doc/fastify-docs/docs/Decorators.md b/doc/fastify-docs/docs/Decorators.md new file mode 100644 index 0000000..29fa45e --- /dev/null +++ b/doc/fastify-docs/docs/Decorators.md @@ -0,0 +1,262 @@ +

Fastify

+ +## 装饰器 + +装饰器 API 允许你自定义服务器实例或请求周期中的请求/回复等对象。任意类型的属性都能通过装饰器添加到这些对象上,包括函数、普通对象 (plain object) 以及原生类型。 + +装饰器 API 是 *同步* 的。如果异步地添加装饰器,可能会导致在装饰器完成初始化之前, Fastify 实例就已经引导完毕了。因此,必须将 `register` 方法与 `fastify-plugin` 结合使用。详见 [Plugins](Plugins.md)。 + +通过装饰器 API 来自定义对象,底层的 Javascript 引擎便能对其进行优化。这是因为引擎能在所有的对象实例被初始化与使用前,定义好它们的形状 (shape)。下文的例子则是不推荐的做法,因为它在对象的生命周期中修改了它们的形状: + +```js +// 不推荐的写法!请继续阅读。 + +// 在调用请求处理函数之前 +// 将 user 属性添加到请求上。 +fastify.addHook('preHandler', function (req, reply, done) { + req.user = 'Bob Dylan' + done() +}) + +// 在处理函数中使用 user 属性。 +fastify.get('/', function (req, reply) { + reply.send(`Hello, ${req.user}`) +}) +``` + +由于上述例子在请求对象初始化完成后,还改动了它的形状,因此 JavaScript 引擎必须对该对象去优化。使用装饰器 API 能避开去优化问题: + +```js +// 使用装饰器为请求对象添加 'user' 属性。 +fastify.decorateRequest('user', '') + +// 更新属性。 +fastify.addHook('preHandler', (req, reply, done) => { + req.user = 'Bob Dylan' + done() +}) +// 最后访问它。 +fastify.get('/', (req, reply) => { + reply.send(`Hello, ${req.user}!`) +}) +``` + +装饰器的初始值应该尽可能地与其未来将被设置的值接近。例如,字符串类型的装饰器的初始值为 `''`,对象或函数类型的初始值为 `null`。 + +请注意,上述例子仅可用于基本类型的值,因为引用类型会在所有请求中共享。详见 [decorateRequest](#decorate-request)。 + +更多此话题的内容,请见 [JavaScript engine fundamentals: Shapes and Inline Caches](https://mathiasbynens.be/notes/shapes-ics)。 + +### 使用方法 + + +#### `decorate(name, value, [dependencies])` + + +该方法用于自定义 Fastify [server](Server.md) 实例。 + +例如,为其添加一个新方法: + +```js +fastify.decorate('utility', function () { + // 新功能的代码 +}) +``` + +正如上文所述,还可以传递非函数的值: + +```js +fastify.decorate('conf', { + db: 'some.db', + port: 3000 +}) +``` + +通过装饰属性的名称便可访问值: + +```js +fastify.utility() + +console.log(fastify.conf.db) +``` + +[路由](Routes.md)函数的 `this` 指向 [Fastify server](Server.md): + +```js +fastify.decorate('db', new DbConnection()) + +fastify.get('/', async function (request, reply) { + reply({hello: await this.db.query('world')}) +}) +``` + +可选的 `dependencies` 参数用于指定当前装饰器所依赖的其他装饰器列表。这个列表包含了其他装饰器的名称字符串。在下面的例子里,装饰器 "utility" 依赖于 "greet" 和 "log": + +```js +fastify.decorate('utility', fn, ['greet', 'log']) +``` + +注:使用箭头函数会破坏 `FastifyInstance` 的 `this` 绑定。 + +一旦有依赖项不满足,`decorate` 方法便会抛出异常。依赖项检查是在服务器实例启动前进行的,因此,在运行时不会发生异常。 + +#### `decorateReply(name, value, [dependencies])` + + +顾名思义,`decorateReply` 向 `Reply` 核心对象添加新的方法或属性: + +```js +fastify.decorateReply('utility', function () { + // 新功能的代码 +}) +``` + +注:使用箭头函数会破坏 `this` 和 Fastify `Reply` 实例的绑定。 + +注:使用 `decorateReply` 装饰引用类型,会触发警示: + +```js +// 反面教材 +fastify.decorateReply('foo', { bar: 'fizz'}) +``` +在这个例子里,该对象的引用存在于所有请求中,导致**任何更改都会影响到所有请求,并可能触发安全漏洞或内存泄露**。要合适地封装请求对象,请在 [`'onRequest'` 钩子](Hooks.md#onrequest)里设置新的值。示例如下: + +```js +const fp = require('fastify-plugin') + +async function myPlugin (app) { + app.decorateRequest('foo', null) + app.addHook('onRequest', async (req, reply) => { + req.foo = { bar: 42 } + }) +} + +module.exports = fp(myPlugin) +``` + +关于 `dependencies` 参数,请见 [`decorate`](#decorate)。 + +#### `decorateRequest(name, value, [dependencies])` + + +同理,`decorateRequest` 向 `Request` 核心对象添加新的方法或属性: + +```js +fastify.decorateRequest('utility', function () { + // 新功能的代码 +}) +``` + +注:使用箭头函数会破坏 `this` 和 Fastify `Request` 实例的绑定。 + +注:使用 `decorateRequest` 装饰引用类型,会触发警示: + +```js +// 反面教材 +fastify.decorateRequest('foo', { bar: 'fizz'}) +``` +在这个例子里,该对象的引用存在于所有请求中,导致**任何更改都会影响到所有请求,并可能触发安全漏洞或内存泄露**。要合适地封装请求对象,请在 [`'onRequest'` 钩子](Hooks.md#onrequest)里设置新的值。示例如下: + +```js +const fp = require('fastify-plugin') + +async function myPlugin (app) { + app.decorateRequest('foo', null) + app.addHook('onRequest', async (req, reply) => { + req.foo = { bar: 42 } + }) +} + +module.exports = fp(myPlugin) +``` + +关于 `dependencies` 参数,请见 [`decorate`](#decorate)。 + +#### `hasDecorator(name)` + + +用于检查服务器实例上是否存在某个装饰器: + +```js +fastify.hasDecorator('utility') +``` + +#### hasRequestDecorator + + +用于检查 Request 实例上是否存在某个装饰器: + +```js +fastify.hasRequestDecorator('utility') +``` + +#### hasReplyDecorator + + +用于检查 Reply 实例上是否存在某个装饰器: + +```js +fastify.hasReplyDecorator('utility') +``` + +### 装饰器与封装 + + +在 **封装** 的同一个上下文中,如果通过 `decorate`、`decorateRequest` 以及 `decorateReply` 多次定义了一个同名的的装饰器,将会抛出一个异常。 + +下面的示例会抛出异常: + ```js +const server = require('fastify')() +server.decorateReply('view', function (template, args) { + // 页面渲染引擎的代码。 +}) +server.get('/', (req, reply) => { + reply.view('/index.html', { hello: 'world' }) +}) +// 当在其他地方定义 +// view 装饰器时,抛出异常。 +server.decorateReply('view', function (template, args) { + // 另一个渲染引擎。 +}) +server.listen(3000) +``` + +但下面这个例子不会抛异常: + +```js +const server = require('fastify')() + server.decorateReply('view', function (template, args) { + // 页面渲染引擎的代码。 +}) +server.register(async function (server, opts) { + // 我们在当前封装的插件内添加了一个 view 装饰器。 + // 这么做不会抛出异常。 + // 因为插件外部和内部的 view 装饰器是不一样的。 + server.decorateReply('view', function (template, args) { + // another rendering engine + }) + server.get('/', (req, reply) => { + reply.view('/index.page', { hello: 'world' }) + }) +}, { prefix: '/bar' }) +server.listen(3000) +``` + +### Getter 和 Setter + + +装饰器接受特别的 "getter/setter" 对象。这些对象拥有着名为 `getter` 与 `setter` 的函数 (尽管 `setter` 是可选的)。这么做便可以通过装饰器来定义属性。例如: + +```js +fastify.decorate('foo', { + getter () { + return 'a getter' + } +}) +``` + +上例会在 Fastify 实例中定义一个 `foo` 属性: + +```js +console.log(fastify.foo) // 'a getter' +``` diff --git a/doc/fastify-docs/docs/Encapsulation.md b/doc/fastify-docs/docs/Encapsulation.md new file mode 100644 index 0000000..e5db822 --- /dev/null +++ b/doc/fastify-docs/docs/Encapsulation.md @@ -0,0 +1,167 @@ +

Fastify

+ + +## 封装 + +“封装上下文”是 Fastify 的一个基础特性,负责控制[路由](./Routes.md)能访问的[装饰器](./Decorators.md)、[钩子](./Hooks.md)以及[插件](./Plugins.md)。下图是封装上下文的抽象表现: + +![Figure 1](./resources/encapsulation_context.svg) + +上图可归纳为以下几块内容: + +1. _顶层上下文 (root context)_ +2. 三个 _顶层插件 (root plugin)_ +3. 两个 _子上下文 (child context)_,每个 _子上下文_ 拥有 + * 两个 _子插件 (child plugin)_ + * 一个 _孙子上下文 (grandchild context)_,其又拥有 + - 三个 _子插件 (child plugin)_ + +任意 _子上下文_ 或 _孙子上下文_ 都有权访问 _顶层插件_。_孙子上下文_ 有权访问它上级的 _子上下文_ 中注册的 _子插件_,但 _子上下文_ *无权* 访问它下级的 _孙子上下文中_ 注册的 _子插件_。 + +在 Fastify 中,除了 _顶层上下文_,一切皆为[插件](./Plugins.md)。下文的例子也不例外,所有的“上下文”和“插件”都是包含装饰器、钩子、插件及路由的插件。该例子为有三个路由的 REST API 服务器,第一个路由 (`/one`) 需要鉴权 (使用 [fastify-bearer-auth][bearer]),第二个路由 (`/two`) 无需鉴权,第三个路由 (`/three`) 有权访问第二个路由的上下文: + +```js +'use strict' + +const fastify = require('fastify')() + +fastify.decorateRequest('answer', 42) + +fastify.register(async function authenticatedContext (childServer) { + childServer.register(require('fastify-bearer-auth'), { keys: ['abc123'] }) + + childServer.route({ + path: '/one', + method: 'GET', + handler (request, response) { + response.send({ + answer: request.answer, + // request.foo 会是 undefined,因为该值是在 publicContext 中定义的 + foo: request.foo, + // request.bar 会是 undefined,因为该值是在 grandchildContext 中定义的 + bar: request.bar + }) + } + }) +}) + +fastify.register(async function publicContext (childServer) { + childServer.decorateRequest('foo', 'foo') + + childServer.route({ + path: '/two', + method: 'GET', + handler (request, response) { + response.send({ + answer: request.answer, + foo: request.foo, + // request.bar 会是 undefined,因为该值是在 grandchildContext 中定义的 + bar: request.bar + }) + } + }) + + childServer.register(async function grandchildContext (grandchildServer) { + grandchildServer.decorateRequest('bar', 'bar') + + grandchildServer.route({ + path: '/three', + method: 'GET', + handler (request, response) { + response.send({ + answer: request.answer, + foo: request.foo, + bar: request.bar + }) + } + }) + }) +}) + +fastify.listen(8000) +``` + +上面的例子展示了所有封装相关的概念: + +1. 每个 _子上下文_ (`authenticatedContext`、`publicContext` 及 `grandchildContext`) 都有权访问在 _顶层上下文_ 中定义的 `answer` 请求装饰器。 +2. 只有 `authenticatedContext` 能访问 `fastify-bearer-auth` 插件。 +3. `publicContext` 和 `grandchildContext` 都能访问 `foo` 请求装饰器。 +4. 只有 `grandchildContext` 能访问 `bar` 请求装饰器。 + +启动服务来验证这些概念吧: + +```sh +# curl -H 'authorization: Bearer abc123' http://127.0.0.1:8000/one +{"answer":42} +# curl http://127.0.0.1:8000/two +{"answer":42,"foo":"foo"} +# curl http://127.0.0.1:8000/three +{"answer":42,"foo":"foo","bar":"bar"} +``` + +[bearer]: https://github.com/fastify/fastify-bearer-auth + + +## 在上下文间共享 + +请注意,在上文例子中,每个上下文都 _仅_ 从父级上下文进行继承,而父级上下文无权访问后代上下文中定义的实体。在某些情况下,我们并不想要这一默认行为。使用 [fastify-plugin][fastify-plugin] ,便能允许父级上下文访问到后代上下文中定义的实体。 + +假设上例的 `publicContext` 需要获取 `grandchildContext` 中定义的 `bar` 装饰器,我们可以重写代码如下: + +```js +'use strict' + +const fastify = require('fastify')() +const fastifyPlugin = require('fastify-plugin') + +fastify.decorateRequest('answer', 42) + +// 为了代码清晰,这里省略了 `authenticatedContext` + +fastify.register(async function publicContext (childServer) { + childServer.decorateRequest('foo', 'foo') + + childServer.route({ + path: '/two', + method: 'GET', + handler (request, response) { + response.send({ + answer: request.answer, + foo: request.foo, + bar: request.bar + }) + } + }) + + childServer.register(fastifyPlugin(grandchildContext)) + + async function grandchildContext (grandchildServer) { + grandchildServer.decorateRequest('bar', 'bar') + + grandchildServer.route({ + path: '/three', + method: 'GET', + handler (request, response) { + response.send({ + answer: request.answer, + foo: request.foo, + bar: request.bar + }) + } + }) + } +}) + +fastify.listen(8000) +``` + +重启服务,访问 `/two` 和 `/three` 路由: + +```sh +# curl http://127.0.0.1:8000/two +{"answer":42,"foo":"foo","bar":"bar"} +# curl http://127.0.0.1:8000/three +{"answer":42,"foo":"foo","bar":"bar"} +``` + +[fastify-plugin]: https://github.com/fastify/fastify-plugin \ No newline at end of file diff --git a/doc/fastify-docs/docs/Errors.md b/doc/fastify-docs/docs/Errors.md new file mode 100644 index 0000000..5907546 --- /dev/null +++ b/doc/fastify-docs/docs/Errors.md @@ -0,0 +1,170 @@ +

Fastify

+ + +## 错误 + + +### Node.js 错误处理 + +#### 未捕获的错误 +在 Node.js 里,未捕获的错误容易引起内存泄漏、文件描述符泄漏等生产环境主要的问题。[Domain 模块](https://nodejs.org/en/docs/guides/domain-postmortem/)被设计用来解决这一问题,然而效果不佳。 + +由于不可能合理地处理所有未捕获的错误,目前最好的处理方案就是[使程序崩溃](https://nodejs.org/api/process.html#process_warning_using_uncaughtexception_correctly)。 + +#### 在 Promise 里捕获错误 +未处理的 promise rejection (即未被 `.catch()` 处理) 在 Node.js 中也可能引起内存或文件描述符泄漏。`unhandledRejection` 不被推荐,未处理的 rejection 也不会抛出,因此还是可能会泄露。你应当使用如 [`make-promises-safe`](https://github.com/mcollina/make-promises-safe) 的模块来确保未处理的 rejection _总能_ 被抛出。 + +假如你使用 promise,你应当同时给它们加上 `.catch()`。 + +### Fastify 的错误 +Fastify 遵循不全则无的原则,旨在精而优。因此,确保正确处理错误是开发者需要考虑的问题。 + +#### 输入数据的错误 +由于大部分的错误源于预期外的输入,因此我们推荐[通过 JSON.schema 来验证输入数据](Validation-and-Serialization.md)。 + +#### 在 Fastify 中捕捉未捕获的错误 +在不影响性能的前提下,Fastify 尽可能多地捕捉未捕获的错误。这些错误包括: + +1. 同步路由中的错误。如 `app.get('/', () => { throw new Error('kaboom') })` +2. `async` 路由中的错误。如 `app.get('/', async () => { throw new Error('kaboom') })` + +上述错误都会被安全地捕捉,并移交给 Fastify 默认的错误处理函数,发送一个通用的 `500 Internal Server Error` 响应。 + +要自定义该行为,请见 [`setErrorHandler`](Server.md#seterrorhandler)。 + +### Fastify 生命周期钩子的错误,及自定义错误控制函数 + +在[钩子](Hooks.md#manage-errors-from-a-hook)的文档中提到: +> 假如在钩子执行过程中发生错误,只需把它传递给 `done()`,Fastify 便会自动地关闭请求,并向用户发送合适的错误代码。 + +如果通过 `setErrorHandler` 自定义了一个错误函数,那么错误会被引导到那里,否则被引导到 Fastify 默认的错误函数中去。 + +自定义错误函数应该考虑以下几点: + +- 你可以调用 `reply.send(data)`,正如在[常规路由](Reply.md#senddata)中那样 + - object 会被序列化,并触发 `preSerialization` 钩子 (假如有定义的话) + - string、buffer 及 stream 会被直接发送至客户端 (不会序列化),并附带上合适的 header。 + +- 在错误函数里你可以抛出新的错误 + - 错误 (新的错误,或被重新抛出的错误参数) 会触发 `onError` 钩子,并被发送给用户 + - 在同一个钩子内,一个错误不会被触发两次。Fastify 会内在地监控错误的触发,以此避免在回复阶段无限循环地抛错 (在路由函数执行后)。 + + +### Fastify 错误代码 + + +#### FST_ERR_BAD_URL + +无效的 url。 + + +#### FST_ERR_CTP_ALREADY_PRESENT + +该 content type 的解析器已经被注册。 + + +#### FST_ERR_CTP_BODY_TOO_LARGE + +请求 body 大小超过限制。 + +可通过 Fastify 实例的 [`bodyLimit`](Server.md#bodyLimit) 属性改变大小限制。 + + +#### FST_ERR_CTP_EMPTY_TYPE + +content type 不能是一个空字符串。 + + +#### FST_ERR_CTP_INVALID_CONTENT_LENGTH + +请求 body 大小与 Content-Length 不一致。 + + +#### FST_ERR_CTP_INVALID_HANDLER + +该 content type 接收的处理函数无效。 + + +#### FST_ERR_CTP_INVALID_MEDIA_TYPE + +收到的 media type 不支持 (例如,不存在合适的 `Content-Type` 解析器)。 + + +#### FST_ERR_CTP_INVALID_PARSE_TYPE + +提供的待解析类型不支持。只支持 `string` 和 `buffer`。 + + +#### FST_ERR_CTP_INVALID_TYPE + +`Content-Type` 应为一个字符串。 + + +#### FST_ERR_DEC_ALREADY_PRESENT + +已存在同名的装饰器。 + + +#### FST_ERR_DEC_MISSING_DEPENDENCY + +缺失依赖导致装饰器无法注册。 + + +#### FST_ERR_HOOK_INVALID_HANDLER + +钩子的回调必须为函数。 + + +#### FST_ERR_HOOK_INVALID_TYPE + +钩子名称必须为字符串。 + + +#### FST_ERR_LOG_INVALID_DESTINATION + +日志工具目标地址无效。仅接受 `'stream'` 或 `'file'` 作为目标地址。 + + +#### FST_ERR_PROMISE_NOT_FULLFILLED + +状态码不为 204 时,Promise 的 payload 不能为 'undefined'。 + + +#### FST_ERR_REP_ALREADY_SENT + +响应已发送。 + + +#### FST_ERR_REP_INVALID_PAYLOAD_TYPE + +响应 payload 类型无效。只允许 `string` 或 `Buffer`。 + + +#### FST_ERR_SCH_ALREADY_PRESENT + +同 `$id` 的 schema 已经存在。 + + +#### FST_ERR_SCH_MISSING_ID + +提供的 schema 没有 `$id` 属性。 + + +#### FST_ERR_SCH_SERIALIZATION_BUILD + +用于序列化响应的 JSON schema 不合法。 + + +#### FST_ERR_SCH_VALIDATION_BUILD + +用于校验路由的 JSON schema 不合法。 + + +#### FST_ERR_SEND_INSIDE_ONERR + +不能在 `onError` 钩子中调用 `send`。 + + +#### FST_ERR_SEND_UNDEFINED_ERR + +发生了未定义的错误。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Fluent-Schema.md b/doc/fastify-docs/docs/Fluent-Schema.md new file mode 100644 index 0000000..b9de126 --- /dev/null +++ b/doc/fastify-docs/docs/Fluent-Schema.md @@ -0,0 +1,115 @@ +

Fastify

+ +## Fluent Schema + +在[验证和序列化](Validation-and-Serialization.md)一文中,我们列明了使用 JSON schema 验证输入、优化输出时所有可用的参数。 + +现在,你可以使用 [`fluent-json-schema`](https://github.com/fastify/fluent-json-schema) 来更简单地设置 JSON schema,并且复用常量。 + +### 基本设置 + +```js +const S = require('fluent-json-schema') + +// 你可以使用如下的一个对象,或查询数据库来获取数据 +const MY_KEYS = { + KEY1: 'ONE', + KEY2: 'TWO' +} + +const bodyJsonSchema = S.object() + .prop('someKey', S.string()) + .prop('someOtherKey', S.number()) + .prop('requiredKey', S.array().maxItems(3).items(S.integer()).required()) + .prop('nullableKey', S.mixed([S.TYPES.NUMBER, S.TYPES.NULL])) + .prop('multipleTypesKey', S.mixed([S.TYPES.BOOLEAN, S.TYPES.NUMBER])) + .prop('multipleRestrictedTypesKey', S.oneOf([S.string().maxLength(5), S.number().minimum(10)])) + .prop('enumKey', S.enum(Object.values(MY_KEYS))) + .prop('notTypeKey', S.not(S.array())) + +const queryStringJsonSchema = S.object() + .prop('name', S.string()) + .prop('excitement', S.integer()) + +const paramsJsonSchema = S.object() + .prop('par1', S.string()) + .prop('par2', S.integer()) + +const headersJsonSchema = S.object() + .prop('x-foo', S.string().required()) + +const schema = { + body: bodyJsonSchema, + querystring: queryStringJsonSchema, // (或) query: queryStringJsonSchema + params: paramsJsonSchema, + headers: headersJsonSchema +} + +fastify.post('/the/url', { schema }, handler) +``` + +### 复用 + +使用 `fluent-json-schema`,你可以简单且程序化地处理 schema,并通过 `addSchema()` 来复用它们。 +正如[验证和序列化](Validation-and-Serialization.md#adding-a-shared-schema)一文所述,有两种方法来引用 schema。 + +以下是一些例子: + +**`使用$ref`**:引用外部的 schema。 + +```js +const addressSchema = S.object() + .id('#address') + .prop('line1').required() + .prop('line2') + .prop('country').required() + .prop('city').required() + .prop('zipcode').required() + +const commonSchemas = S.object() + .id('https://fastify/demo') + .definition('addressSchema', addressSchema) + .definition('otherSchema', otherSchema) // 你可以任意添加需要的 schema + +fastify.addSchema(commonSchemas) + +const bodyJsonSchema = S.object() + .prop('residence', S.ref('https://fastify/demo#address')).required() + .prop('office', S.ref('https://fastify/demo#/definitions/addressSchema')).required() + +const schema = { body: bodyJsonSchema } + +fastify.post('/the/url', { schema }, handler) +``` + + +**`替换方式`**:在验证阶段之前,使用共用 schema 替换某些字段。 + +```js +const sharedAddressSchema = { + $id: 'sharedAddress', + type: 'object', + required: ['line1', 'country', 'city', 'zipcode'], + properties: { + line1: { type: 'string' }, + line2: { type: 'string' }, + country: { type: 'string' }, + city: { type: 'string' }, + zipcode: { type: 'string' } + } +} +fastify.addSchema(sharedAddressSchema) + +const bodyJsonSchema = { + type: 'object', + properties: { + vacation: 'sharedAddress#' + } +} + +const schema = { body: bodyJsonSchema } + +fastify.post('/the/url', { schema }, handler) +``` + +特别注意:你可以在 `fastify.addSchema` 方法里混用 `$ref` 和 `替换方式`。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Getting-Started.md b/doc/fastify-docs/docs/Getting-Started.md new file mode 100644 index 0000000..9b01661 --- /dev/null +++ b/doc/fastify-docs/docs/Getting-Started.md @@ -0,0 +1,418 @@ +

Fastify

+ +## 起步 +Hello!感谢你来到 Fastify 的世界!
+这篇文档将向你介绍 Fastify 框架及其特性,也包含了一些示例和指向其他文档的链接。
+那,这就开始吧! + + +### 安装 +使用 npm 安装: +``` +npm i fastify --save +``` +使用 yarn 安装: +``` +yarn add fastify +``` + + +### 第一个服务器 +让我们开始编写第一个服务器吧: +```js +// 加载框架并新建实例 + +// ESM +import Fastify from 'fastify' +const fastify = Fastify({ + logger: true +}) +// CommonJs +const fastify = require('fastify')({ + logger: true +}) + +// 声明路由 +fastify.get('/', function (request, reply) { + reply.send({ hello: 'world' }) +}) + +// 启动服务! +fastify.listen(3000, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + // 服务器监听地址:${address} +}) +``` + +更喜欢使用 `async/await`?Fastify 对其提供了开箱即用的支持。
+*(我们还建议使用 [make-promises-safe](https://github.com/mcollina/make-promises-safe) 来避免文件描述符 (file descriptor) 及内存的泄露)* +```js +// ESM +import Fastify from 'fastify' +const fastify = Fastify({ + logger: true +}) +// CommonJs +const fastify = require('fastify')({ + logger: true +}) + +fastify.get('/', async (request, reply) => { + return { hello: 'world' } +}) + +const start = async () => { + try { + await fastify.listen(3000) + } catch (err) { + fastify.log.error(err) + process.exit(1) + } +} +start() +``` + +如此简单,棒极了!
+可是,一个复杂的应用需要比上例多得多的代码。当你从头开始构建一个应用时,会遇到一些典型的问题,如多个文件的操作、异步引导,以及代码结构的布置。
+幸运的是,Fastify 提供了一个易于使用的平台,它能帮助你解决不限于上述的诸多问题! + +> ## 注 +> 本文档中的示例,默认情况下只监听本地 `127.0.0.1` 端口。要监听所有有效的 IPv4 端口,需要将代码修改为监听 `0.0.0.0`,如下所示: +> +> ```js +> fastify.listen(3000, '0.0.0.0', function (err, address) { +> if (err) { +> fastify.log.error(err) +> process.exit(1) +> } +> fastify.log.info(`server listening on ${address}`) +> }) +> ``` +> +> 类似地,`::1` 表示只允许本地的 IPv6 连接。而 `::` 表示允许所有 IPv6 地址的接入,当操作系统支持时,所有的 IPv4 地址也会被允许。 +> +> 当使用 Docker 或其他容器部署时,使用 `0.0.0.0` 或 `::` 会是最简单的暴露应用的方式。 + + +### 第一个插件 +就如同在 JavaScript 中一切皆为对象,在 Fastify 中,一切都是插件 (plugin)。
+在深入之前,先来看看插件系统是如何工作的吧!
+让我们新建一个基本的服务器,但这回我们把路由 (route) 的声明从入口文件转移到一个外部文件。(参阅[路由声明](Routes.md))。 +```js +// ESM +import Fastify from 'fastify' +import firstRoute from './our-first-route' +const fastify = Fastify({ + logger: true +}) + +fastify.register(firstRoute) + +fastify.listen(3000, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + // 服务器监听地址:${address} +}) +``` + +```js +// CommonJs +const fastify = require('fastify')({ + logger: true +}) + +fastify.register(require('./our-first-route')) + +fastify.listen(3000, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + // 服务器监听地址:${address} +}) +``` + +```js +// our-first-route.js + +async function routes (fastify, options) { + fastify.get('/', async (request, reply) => { + return { hello: 'world' } + }) +} + +module.exports = routes +``` +这个例子调用了 `register` API,它是 Fastify 框架的核心,也是添加路由、插件等的唯一方法。 + +在本文的开头,我们提到 Fastify 提供了帮助应用异步引导的基础功能。为什么这一功能十分重要呢? +考虑一下,当存在数据库操作时,数据库连接需要在服务器接受外部请求之前完成。该如何解决这一问题呢?
+典型的解决方案是使用复杂的回调函数或 Promise,但如此会造成框架的 API、其他库以及应用程序的代码混杂在一起。
+Fastify 则不走寻常路,它从本质上用最轻松的方式解决这一问题! + +让我们重写上述示例,加入一个数据库连接。
+ +首先,安装 `fastify-plugin` 和 `fastify-mongodb`: + +``` +npm i --save fastify-plugin fastify-mongodb +``` + +**server.js** +```js +// ESM +import Fastify from 'fastify' +import dbConnector from './our-db-connector' +import firstRoute from './our-first-route' + +const fastify = Fastify({ + logger: true +}) +fastify.register(dbConnector) +fastify.register(firstRoute) + +fastify.listen(3000, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + // 服务器监听地址:${address} +}) +``` + +```js +// CommonJs +const fastify = require('fastify')({ + logger: true +}) + +fastify.register(require('./our-db-connector')) +fastify.register(require('./our-first-route')) + +fastify.listen(3000, function (err, address) { + if (err) { + fastify.log.error(err) + process.exit(1) + } + // 服务器监听地址:${address} +}) + +``` + +**our-db-connector.js** +```js +// ESM +import fastifyPlugin from 'fastify-plugin' +import fastifyMongo from 'fastify-mongodb' + +async function dbConnector (fastify, options) { + fastify.register(fastifyMongo, { + url: 'mongodb://localhost:27017/test_database' + }) +} + +// 用 fastify-plugin 包装插件,以使插件中声明的装饰器、钩子函数暴露在根作用域里。 +module.exports = fastifyPlugin(dbConnector) + +``` + +```js +// CommonJs +const fastifyPlugin = require('fastify-plugin') + +async function dbConnector (fastify, options) { + fastify.register(require('fastify-mongodb'), { + url: 'mongodb://localhost:27017/test_database' + }) +} +// 用 fastify-plugin 包装插件,以使插件中声明的装饰器、钩子函数暴露在根作用域里。 +module.exports = fastifyPlugin(dbConnector) + +``` + +**our-first-route.js** +```js +async function routes (fastify, options) { + const collection = fastify.mongo.db.collection('test_collection') + + fastify.get('/', async (request, reply) => { + return { hello: 'world' } + }) + + fastify.get('/animals', async (request, reply) => { + const result = await collection.find().toArray() + if (result.length === 0) { + throw new Error('No documents found') + } + return result + }) + + fastify.get('/animals/:animal', async (request, reply) => { + const result = await collection.findOne({ animal: request.params.animal }) + if (!result) { + throw new Error('Invalid value') + } + return result + }) +} + +module.exports = routes +``` + +哇,真是快啊!
+介绍了一些新概念后,让我们回顾一下迄今为止都做了些什么吧。
+如你所见,我们可以使用 `register` 来注册数据库连接器或者路由。 +这是 Fastify 最棒的特性之一了!它使得插件按声明的顺序来加载,唯有当前插件加载完毕后,才会加载下一个插件。如此,我们便可以在第一个插件中注册数据库连接器,并在第二个插件中使用它。*(参见 [这里](Plugins.md#handle-the-scope) 了解如何处理插件的作用域)*。 +当调用函数 `fastify.listen()`、`fastify.inject()` 或 `fastify.ready()` 时,插件便开始加载了。 + +MongoDB 的插件使用了 `decorate` API,以便在 Fastify 的命名空间下添加自定义对象,如此一来,你就可以在所有地方直接使用这些对象了。我们鼓励运用这一 API,因为它有助于提高代码复用率,减少重复的代码或逻辑。 + +更深入的内容,例如插件如何运作、如何新建,以及使用 Fastify 全部的 API 去处理复杂的异步引导的细节,请看[插件指南](Plugins-Guide.md)。 + + +### 插件加载顺序 +为了保证应用的行为一致且可预测,我们强烈建议你采用以下的顺序来组织代码: +``` +└── 来自 Fastify 生态的插件 +└── 你自己的插件 +└── 装饰器 +└── 钩子函数 +└── 你的服务应用 +``` +这确保了你总能访问当前作用域下声明的所有属性。
+如前文所述,Fastify 提供了一个可靠的封装模型,它能帮助你的应用成为单一且独立的服务。假如你要为某些路由单独地注册插件,只需复写上述的结构就足够了。 +``` +└── 来自 Fastify 生态的插件 +└── 你自己的插件 +└── 装饰器 +└── 钩子函数 +└── 你的服务应用 + │ + └── 服务 A + │ └── 来自 Fastify 生态的插件 + │ └── 你自己的插件 + │ └── 装饰器 + │ └── 钩子函数 + │ └── 你的服务应用 + │ + └── 服务 B + │ └── 来自 Fastify 生态的插件 + │ └── 你自己的插件 + │ └── 装饰器 + │ └── 钩子函数 + │ └── 你的服务应用 +``` + + +### 验证数据 +数据的验证在我们的框架中是极为重要的一环,也是核心的概念。
+Fastify 使用 [JSON Schema](https://json-schema.org/) 验证来访的请求。(也支持宽松的 JTD schema,但首先得禁用 `jsonShorthand`)。 + +让我们来看一个验证路由的例子: +```js +const opts = { + schema: { + body: { + type: 'object', + properties: { + someKey: { type: 'string' }, + someOtherKey: { type: 'number' } + } + } + } +} + +fastify.post('/', opts, async (request, reply) => { + return { hello: 'world' } +}) +``` +这个例子展示了如何向路由传递配置选项。选项中包含了一个名为 `schema` 的对象,它便是我们验证路由所用的模式 (schema)。借由 schema,我们可以验证 `body`、`querystring`、`params` 以及 `header`。
+请参阅[验证与序列化](Validation-and-Serialization.md)获取更多信息。 + + +### 序列化数据 +Fastify 对 JSON 提供了优异的支持,极大地优化了解析 JSON body 与序列化 JSON 输出的过程。
+在 schema 的选项中设置 `response` 的值,能够加快 JSON 的序列化 (没错,这很慢!),就像这样: +```js +const opts = { + schema: { + response: { + 200: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + } +} + +fastify.get('/', opts, async (request, reply) => { + return { hello: 'world' } +}) +``` +一旦指明了 schema,序列化的速度就能达到原先的 2-3 倍。这么做同时也保护了潜在的敏感数据不被泄露,因为 Fastify 仅对 schema 里出现的数据进行序列化。 +请参阅 [验证与序列化](Validation-and-Serialization.md)获取更多信息。 + + +### 扩展服务器 +Fastify 生来十分精简,也具有高可扩展性。我们相信,一个小巧的框架足以实现一个优秀的应用。
+换句话说,Fastify 并非一个面面俱到的框架,它依赖于自己惊人的[生态系统](https://github.com/fastify/fastify/blob/main/docs/Ecosystem.md)! + + +### 测试服务器 +Fastify 并没有提供测试框架,但是我们推荐你在测试中使用 Fastify 的特性及结构。
+更多内容请看[测试](Testing.md)! + + +### 从命令行启动服务器 +感谢 [fastify-cli](https://github.com/fastify/fastify-cli),它让 Fastify 集成到了命令行之中。 + +首先,你得安装 `fastify-cli`: + +``` +npm i fastify-cli +``` + +你还可以加入 `-g` 选项来全局安装它。 + +接下来,在 `package.json` 中添加如下行: +```json +{ + "scripts": { + "start": "fastify start server.js" + } +} +``` + +然后,创建你的服务器文件: +```js +// server.js +'use strict' + +module.exports = async function (fastify, opts) { + fastify.get('/', async (request, reply) => { + return { hello: 'world' } + }) +} +``` + +最后,启动你的服务器: +```bash +npm start +``` + + +### 幻灯片与视频 (英文资源) +- 幻灯片 + - [为你的 HTTP 服务器提速](https://mcollina.github.io/take-your-http-server-to-ludicrous-speed) by [@mcollina](https://github.com/mcollina) + - [如果我告诉你 HTTP 可以更快](https://delvedor.github.io/What-if-I-told-you-that-HTTP-can-be-fast) by [@delvedor](https://github.com/delvedor) + +- 视频 + - [为你的 HTTP 服务器提速](https://www.youtube.com/watch?v=5z46jJZNe8k) by [@mcollina](https://github.com/mcollina) + - [如果我告诉你 HTTP 可以更快](https://www.webexpo.net/prague2017/talk/what-if-i-told-you-that-http-can-be-fast/) by [@delvedor](https://github.com/delvedor) diff --git a/doc/fastify-docs/docs/HTTP2.md b/doc/fastify-docs/docs/HTTP2.md new file mode 100644 index 0000000..746f5ec --- /dev/null +++ b/doc/fastify-docs/docs/HTTP2.md @@ -0,0 +1,89 @@ +

Fastify

+ +## HTTP2 + +_Fastify_ 提供了从 Node 8.8.0 开始的对 HTTP2 **实验性支持**,_Fastify_ 支持 HTTPS 和普通文本的 HTTP2 支持。需要注意的是,Node 8.8.1 以上的版本才支持 HTTP2。 + +当前没有任何 HTTP2 相关的 APIs 是可用的,但 Node `req` 和 `res` 可以通过 `Request` 和 `Reply` 接口访问。欢迎相关的 PR。 + +### 安全 (HTTPS) + +所有的现代浏览器都__只能通过安全的连接__ 支持 HTTP2: + +```js +'use strict' + +const fs = require('fs') +const path = require('path') +const fastify = require('fastify')({ + http2: true, + https: { + key: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.key')), + cert: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.cert')) + } +}) + +fastify.get('/', function (request, reply) { + reply.code(200).send({ hello: 'world' }) +}) + +fastify.listen(3000) +``` + +ALPN 协商允许在同一个 socket 上支持 HTTPS 和 HTTP/2。 +Node 核心 `req` 和 `res` 对象可以是 [HTTP/1](https://nodejs.org/api/http.html) +或者 [HTTP/2](https://nodejs.org/api/http2.html)。 +_Fastify_ 自带支持开箱即用: + +```js +'use strict' + +const fs = require('fs') +const path = require('path') +const fastify = require('fastify')({ + http2: true, + https: { + allowHTTP1: true, // 向后支持 HTTP1 + key: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.key')), + cert: fs.readFileSync(path.join(__dirname, '..', 'https', 'fastify.cert')) + } +}) + +// 该路由从 HTTPS 与 HTTP2 均可访问 +fastify.get('/', function (request, reply) { + reply.code(200).send({ hello: 'world' }) +}) + +fastify.listen(3000) +``` + +你可以像这样测试你的新服务器: + +``` +$ npx h2url https://localhost:3000 +``` + +### 纯文本或者不安全 + +如果你搭建微服务,你可以纯文本连接 HTTP2,但是浏览器不支持这样做。 + +```js +'use strict' + +const fastify = require('fastify')({ + http2: true +}) + +fastify.get('/', function (request, reply) { + reply.code(200).send({ hello: 'world' }) +}) + +fastify.listen(3000) +``` + +你可以像这样测试你的新服务器: + +``` +$ npx h2url http://localhost:3000 +``` + diff --git a/doc/fastify-docs/docs/Hooks.md b/doc/fastify-docs/docs/Hooks.md new file mode 100644 index 0000000..24f5bf5 --- /dev/null +++ b/doc/fastify-docs/docs/Hooks.md @@ -0,0 +1,579 @@ +

Fastify

+ +## 钩子方法 + +钩子 (hooks) 让你能够监听应用或请求/响应生命周期之上的特定事件。使用 `fastify.addHook` 可以注册钩子。你必须在事件被触发之前注册相应的钩子,否则,事件将得不到处理。 + +通过钩子方法,你可以与 Fastify 的生命周期直接进行交互。有用于请求/响应的钩子,也有应用级钩子: + +- [请求/响应钩子](#requestreply-hooks) + - [onRequest](#onrequest) + - [preParsing](#preparsing) + - [preValidation](#prevalidation) + - [preHandler](#prehandler) + - [preSerialization](#preserialization) + - [onError](#onerror) + - [onSend](#onsend) + - [onResponse](#onresponse) + - [onTimeout](#ontimeout) + - [在钩子中管理错误](#manage-errors-from-a-hook) + - [在钩子中响应请求](#respond-to-a-request-from-a-hook) +- [应用钩子](#application-hooks) + - [onReady](#onready) + - [onClose](#onclose) + - [onRoute](#onroute) + - [onRegister](#onregister) +- [作用域](#scope) +- [路由层钩子](#route-level-hooks) + +**注意**:使用 `async`/`await` 或返回一个 `Promise` 时,`done` 回调不可用。在这种情况下,仍然使用 `done` 可能会导致难以预料的行为,例如,处理函数的重复调用。 + +## 请求/响应钩子 + +[Request](Request.md) 与 [Reply](Reply.md) 是 Fastify 核心的对象。
+`done` 是调用[生命周期](Lifecycle.md)下一阶段的函数。 + +[生命周期](Lifecycle.md)一文清晰地展示了各个钩子执行的位置。
+钩子可被封装,因此可以运用在特定的路由上。更多信息请看[作用域](#scope)一节。 + +在请求/响应中,有八个可用的钩子 *(按执行顺序排序)*: + +### onRequest +```js +fastify.addHook('onRequest', (request, reply, done) => { + // 其他代码 + done() +}) +``` +或使用 `async/await`: +```js +fastify.addHook('onRequest', async (request, reply) => { + // 其他代码 + await asyncMethod() +}) +``` + +**注意**:在 [onRequest](#onrequest) 钩子中,`request.body` 的值总是 `null`,这是因为 body 的解析发生在 [preValidation](#prevalidation) 钩子之前。 + +### preParsing + +`preParsing` 钩子让你能在解析请求之前转换它们。它的参数除了和其他钩子一样的请求与响应对象外,还多了一个当前请求 payload 的 stream。 + +需要通过 `return` 或回调函数返回值的话,必须返回一个 stream。 + +例如,你可以解压请求的 body: + +```js +fastify.addHook('preParsing', (request, reply, payload, done) => { + // 其他代码 + done(null, newPayload) +}) +``` +或使用 `async/await`: +```js +fastify.addHook('preParsing', async (request, reply, payload) => { + // 其他代码 + await asyncMethod() + return newPayload +}) +``` + +**注意**:在 [preParsing](#preparsing) 钩子中,`request.body` 的值总是 `null`,这是因为 body 的解析发生在 [preValidation](#prevalidation) 钩子之前。 + +**注意**:你应当给返回的 stream 添加 `receivedEncodedLength` 属性。这是为了通过比对请求头的 `Content-Length`,来精确匹配请求的 payload。理想情况下,每收到一块数据都应该更新该属性。 + +**注意**:早先的写法 `function(request, reply, done)` 与 `function(request, reply)` 仍被支持,但不推荐使用。 + +### preValidation + +使用 `preValidation` 钩子时,你可以在校验前修改 payload。示例如下: + +```js +fastify.addHook('preValidation', (request, reply, done) => { + req.body = { ...req.body, importantKey: 'randomString' } + done() +}) +``` +或使用 `async/await`: +```js +fastify.addHook('preValidation', async (request, reply) => { + const importantKey = await generateRandomString() + req.body = { ...req.body, importantKey } +}) +``` + +### preHandler +```js +fastify.addHook('preHandler', (request, reply, done) => { + // 其他代码 + done() +}) +``` +或使用 `async/await`: +```js +fastify.addHook('preHandler', async (request, reply) => { + // 其他代码 + await asyncMethod() +}) +``` +### preSerialization + +`preSerialization` 钩子让你可以在 payload 被序列化之前改动 (或替换) 它。举个例子: + +```js +fastify.addHook('preSerialization', (request, reply, payload, done) => { + const err = null + const newPayload = { wrapped: payload } + done(err, newPayload) +}) +``` +或使用 `async/await`: +```js +fastify.addHook('preSerialization', async (request, reply, payload) => { + return { wrapped: payload } +}) +``` + +注:payload 为 `string`、`Buffer`、`stream` 或 `null` 时,该钩子不会被调用。 + +### onError +```js +fastify.addHook('onError', (request, reply, error, done) => { + // 其他代码 + done() +}) +``` +或使用 `async/await`: +```js +fastify.addHook('onError', async (request, reply, error) => { + // 当自定义错误日志时有用处 + // 你不应该使用这个钩子去更新错误 +}) +``` +`onError` 钩子可用于自定义错误日志,或当发生错误时添加特定的 header。
+该钩子并不是为了变更错误而设计的,且调用 `reply.send` 会抛出一个异常。
+它只会在 `customErrorHandler` 向用户发送错误之后被执行 (要注意的是,默认的 `customErrorHandler` 总是会发送错误)。 +**注意**:与其他钩子不同,`onError` 不支持向 `done` 函数传递错误。 + +### onSend +使用 `onSend` 钩子可以改变 payload。例如: + +```js +fastify.addHook('onSend', (request, reply, payload, done) => { + const err = null; + const newPayload = payload.replace('some-text', 'some-new-text') + done(err, newPayload) +}) +``` +或使用 `async/await`: +```js +fastify.addHook('onSend', async (request, reply, payload) => { + const newPayload = payload.replace('some-text', 'some-new-text') + return newPayload +}) +``` + +你也可以通过将 payload 置为 `null`,发送一个空消息主体的响应: + +```js +fastify.addHook('onSend', (request, reply, payload, done) => { + reply.code(304) + const newPayload = null + done(null, newPayload) +}) +``` + +> 将 payload 设为空字符串 `''` 也可以发送空的消息主体。但要小心的是,这么做会造成 `Content-Length` header 的值为 `0`。而 payload 为 `null` 则不会设置 `Content-Length` header。 + +注:你只能将 payload 修改为 `string`、`Buffer`、`stream` 或 `null`。 + + +### onResponse +```js + +fastify.addHook('onResponse', (request, reply, done) => { + // 其他代码 + done() +}) +``` +或使用 `async/await`: +```js +fastify.addHook('onResponse', async (request, reply) => { + // 其他代码 + await asyncMethod() +}) +``` + +`onResponse` 钩子在响应发出后被执行,因此在该钩子中你无法再向客户端发送数据了。但是你可以在此向外部服务发送数据,比如收集数据。 + +### onTimeout + +```js +fastify.addHook('onTimeout', (request, reply, done) => { + // 其他代码 + done() +}) +``` +Or `async/await`: +```js +fastify.addHook('onTimeout', async (request, reply) => { + // 其他代码 + await asyncMethod() +}) +``` +`onTimeout` 用于监测请求超时,需要在 Fastify 实例上设置 `connectionTimeout` 属性。当请求超时,socket 挂起 (hang up) 时,该钩子执行。因此,在这个钩子里不能再向客户端发送数据了。 + +### 在钩子中管理错误 +在钩子的执行过程中如果发生了错误,只需将错误传递给 `done()`,Fastify 就会自动关闭请求,并发送一个相应的错误码给用户。 + +```js +fastify.addHook('onRequest', (request, reply, done) => { + done(new Error('Some error')) +}) +``` + +如果你想自定义发送给用户的错误码,使用 `reply.code()` 即可: +```js +fastify.addHook('preHandler', (request, reply, done) => { + reply.code(400) + done(new Error('Some error')) +}) +``` +*错误最终会在 [`Reply`](Reply.md#errors) 中得到处理。* + +或者在 `async/await` 函数中抛出错误: +```js +fastify.addHook('onResponse', async (request, reply) => { + throw new Error('Some error') +}) +``` + +### 在钩子中响应请求 + +需要的话,你可以在路由函数执行前响应一个请求,例如进行身份验证。在钩子中响应暗示着钩子的调用链被 __终止__,剩余的钩子将不会执行。假如钩子使用回调的方式,意即不是 `async` 函数,也没有返回 `Promise`,那么只需要调用 `reply.send()`,并且避免触发回调便可。假如钩子是 `async` 函数,那么 `reply.send()` __必须__ 发生在函数返回或 promise resolve 之前,否则请求将会继续下去。当 `reply.send()` 在 promise 调用链之外被调用时,需要 `return reply`,不然请求将被执行两次。 + +__不应当混用回调与 `async`/`Promise`__,否则钩子的调用链会被执行两次。 + +如果你在 `onRequest` 或 `preHandler` 中发出响应,请使用 `reply.send`。 + +```js +fastify.addHook('onRequest', (request, reply, done) => { + reply.send('Early response') +}) + +// 也可使用 async 函数 +fastify.addHook('preHandler', async (request, reply) => { + await something() + reply.send({ hello: 'world' }) + return reply // 在这里是可选的,但这是好的实践 +}) +``` + +如果你想要使用流 (stream) 来响应请求,你应该避免使用 `async` 函数。必须使用 `async` 函数的话,请参考 [test/hooks-async.js](https://github.com/fastify/fastify/blob/94ea67ef2d8dce8a955d510cd9081aabd036fa85/test/hooks-async.js#L269-L275) 中的示例来编写代码。 + +```js +fastify.addHook('onRequest', (request, reply, done) => { + const stream = fs.createReadStream('some-file', 'utf8') + reply.send(stream) +}) +``` + +如果发出响应但没有 `await` 关键字,请确保总是 `return reply`: + +```js +fastify.addHook('preHandler', async (request, reply) => { + setImmediate(() => { reply.send('hello') }) + // 让处理函数等待 promise 链之外发出的响应 + return reply +}) +fastify.addHook('preHandler', async (request, reply) => { + // fastify-static 插件异步地发送文件,因此需要 return reply + reply.sendFile('myfile') + return reply +}) +``` + +## 应用钩子 + +你也可以在应用的生命周期里使用钩子方法。要格外注意的是,这些钩子并未被完全封装。钩子中的 `this` 得到了封装,但处理函数可以响应封装界线外的事件。 + +- [onReady](#onready) +- [onClose](#onclose) +- [onRoute](#onroute) +- [onRegister](#onregister) + +### onReady +在服务器开始监听请求之前,或调用 `.ready()` 方法时触发。在此你无法更改路由,或添加新的钩子。注册的 `onReady` 钩子函数串行执行,只有全部执行完毕时,服务器才会开始监听请求。钩子接受一个回调函数作为参数:`done`,在钩子函数完成后调用。钩子的 `this` 为 Fastify 实例。 + +```js +// 回调写法 +fastify.addHook('onReady', function (done) { + // 其他代码 + const err = null; + done(err) +}) + +// 或 async/await +fastify.addHook('onReady', async function () { + // 异步代码 + await loadCacheFromDatabase() +}) +``` + + +### onClose +使用 `fastify.close()` 停止服务器时被触发。当[插件](Plugins.md)需要一个 "shutdown" 事件时有用,例如关闭一个数据库连接。
+该钩子的第一个参数是 Fastify 实例,第二个为 `done` 回调函数。 +```js +fastify.addHook('onClose', (instance, done) => { + // 其他代码 + done() +}) +``` + + +### onRoute +当注册一个新的路由时被触发。它的监听函数拥有一个唯一的参数:`routeOptions` 对象。该函数是同步的,其本身并不接受回调作为参数。 +```js +fastify.addHook('onRoute', (routeOptions) => { + // 其他代码 + routeOptions.method + routeOptions.schema + routeOptions.url // 路由的完整 URL,包括前缀 + routeOptions.path // `url` 的别名 + routeOptions.routePath // 无前缀的 URL + routeOptions.bodyLimit + routeOptions.logLevel + routeOptions.logSerializers + routeOptions.prefix +}) +``` + +如果在编写插件时,需要自定义程序的路由,比如修改选项或添加新的路由层钩子,你可以在这里添加。 + +```js +fastify.addHook('onRoute', (routeOptions) => { + function onPreSerialization(request, reply, payload, done) { + // 其他代码 + done(null, payload) + } + + // preSerialization 可以是数组或 undefined + routeOptions.preSerialization = [...(routeOptions.preSerialization || []), onPreSerialization] +}) +``` + + +### onRegister +当注册一个新的插件,或创建了新的封装好的上下文后被触发。该钩子在注册的代码**之前**被执行。
+当你的插件需要知晓上下文何时创建完毕,并操作它们时,可以使用这一钩子。
+**注意**:被 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 所封装的插件不会触发该钩子。 +```js +fastify.decorate('data', []) + +fastify.register(async (instance, opts) => { + instance.data.push('hello') + console.log(instance.data) // ['hello'] + + instance.register(async (instance, opts) => { + instance.data.push('world') + console.log(instance.data) // ['hello', 'world'] + }), { prefix: '/hola' }) +}), { prefix: '/ciao' }) + +fastify.register(async (instance, opts) => { + console.log(instance.data) // [] +}), { prefix: '/hello' }) + +fastify.addHook('onRegister', (instance, opts) => { + // 从旧数组浅拷贝, + // 生成一个新数组, + // 使用户获得一个 + // 封装好的 `data` 的实例 + instance.data = instance.data.slice() + + // 新注册实例的选项 + console.log(opts.prefix) +}) +``` + + +## 作用域 +除了[应用钩子](#application-hooks),所有的钩子都是封装好的。这意味着你可以通过 `register` 来决定在何处运行它们,正如[插件指南](Plugins-Guide.md)所述。如果你传递一个函数,那么该函数会获得 Fastify 的上下文,如此你便能使用 Fastify 的 API 了。 + +```js +fastify.addHook('onRequest', function (request, reply, done) { + const self = this // Fastify 上下文 + done() +}) +``` + +要注意的是,每个钩子内的 Fastify 上下文都和注册路由时的插件一样,举例如下: + +```js +fastify.addHook('onRequest', async function (req, reply) { + if (req.raw.url === '/nested') { + assert.strictEqual(this.foo, 'bar') + } else { + assert.strictEqual(this.foo, undefined) + } +}) + +fastify.get('/', async function (req, reply) { + assert.strictEqual(this.foo, undefined) + return { hello: 'world' } +}) + +fastify.register(async function plugin (fastify, opts) { + fastify.decorate('foo', 'bar') + + fastify.get('/nested', async function (req, reply) { + assert.strictEqual(this.foo, 'bar') + return { hello: 'world' } + }) +}) +``` + +提醒:使用[箭头函数](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Arrow_functions)的话,`this` 将不会是 Fastify,而是当前的作用域。 + + + +## 路由层钩子 +你可以为**单个**路由声明一个或多个自定义的生命周期钩子 ([onRequest](#onrequest)、[onResponse](#onresponse)、[preParsing](#preparsing)、[preValidation](#prevalidation)、[preHandler](#prehandler)、[preSerialization](#preserialization)、[onSend](#onsend)、[onTimeout](#ontimeout) 与 [onError](#onerror))。 +如果你这么做,这些钩子总是会作为同一类钩子中的最后一个被执行。
+当你需要进行认证时,这会很有用,而 [preParsing](#preparsing) 与 [preValidation](#prevalidation) 钩子正是为此而生。 +你也可以通过数组定义多个路由层钩子。 + +```js +fastify.addHook('onRequest', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('onResponse', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('preParsing', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('preValidation', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('preHandler', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('preSerialization', (request, reply, payload, done) => { + // 你的代码 + done(null, payload) +}) + +fastify.addHook('onSend', (request, reply, payload, done) => { + // 你的代码 + done(null, payload) +}) + +fastify.addHook('onTimeout', (request, reply, done) => { + // 你的代码 + done() +}) + +fastify.addHook('onError', (request, reply, error, done) => { + // 你的代码 + done() +}) + +fastify.route({ + method: 'GET', + url: '/', + schema: { ... }, + onRequest: function (request, reply, done) { + // 该钩子总是在共享的 `onRequest` 钩子后被执行 + done() + }, + onResponse: function (request, reply, done) { + // 该钩子总是在共享的 `onResponse` 钩子后被执行 + done() + }, + preParsing: function (request, reply, done) { + // 该钩子总是在共享的 `preParsing` 钩子后被执行 + done() + }, + preValidation: function (request, reply, done) { + // 该钩子总是在共享的 `preValidation` 钩子后被执行 + done() + }, + preHandler: function (request, reply, done) { + // 该钩子总是在共享的 `preHandler` 钩子后被执行 + done() + }, + // // 使用数组的例子。所有钩子都支持这一语法。 + // + // preHandler: [function (request, reply, done) { + // // 该钩子总是在共享的 `preHandler` 钩子后被执行 + // done() + // }], + preSerialization: (request, reply, payload, done) => { + // 该钩子总是在共享的 `preSerialization` 钩子后被执行 + done(null, payload) + }, + onSend: (request, reply, payload, done) => { + // 该钩子总是在共享的 `onSend` 钩子后被执行 + done(null, payload) + }, + onTimeout: (request, reply, done) => { + // 该钩子总是在共享的 `onTimeout` 钩子后被执行 + done() + }, + onError: (request, reply, error, done) => { + // 该钩子总是在共享的 `onError` 钩子后被执行 + done() + }, + handler: function (request, reply) { + reply.send({ hello: 'world' }) + } +}) +``` + +**注**:两个选项都接受一个函数数组作为参数。 + +## 诊断通道钩子 + +> **注:** `诊断通道` (diagnostics_channel) 是当前 Node.js 的试验特性, +> 因此,其 API 即便在补丁版本中也可能会发生变动。 +> 对于 Fastify 支持的 Node.js 版本,不兼容 `诊断通道` 的, +> 将会使用 [polyfill](https://www.npmjs.com/package/diagnostics_channel)。 +> 而 polyfill 都不支持的版本将无法使用该特性。 + +当前版本在初始化时,会有一个[`诊断通道`](https://nodejs.org/api/diagnostics_channel.html)发布 `'fastify.initialization'` 事件。此时,Fastify 的实例将会作为回调函数参数的一个属性,该实例可以添加钩子、插件、路由及其他任意内容。 + +举例来说,某个监控工具可以如下使用(当然这是一个简化的例子)。在典型的“探测工具优先加载 (require instrumentation +tools first)”风格下,这段代码会在被监控的应用初始化时加载。 + +```js +const tracer = /* 某个监控工具 */ +const dc = require('diagnostics_channel') +const channel = dc.channel('fastify.initialization') +const spans = new WeakMap() + +channel.subscribe(function ({ fastify }) { + fastify.addHook('onRequest', (request, reply, done) => { + const span = tracer.startSpan('fastify.request') + spans.set(request, span) + done() + }) + + fastify.addHook('onResponse', (request, reply, done) => { + const span = spans.get(request) + span.finish() + done() + }) +}) +``` \ No newline at end of file diff --git a/doc/fastify-docs/docs/LTS.md b/doc/fastify-docs/docs/LTS.md new file mode 100644 index 0000000..529dd7e --- /dev/null +++ b/doc/fastify-docs/docs/LTS.md @@ -0,0 +1,54 @@ +

Fastify

+ + + +## 长期支持计划 + +Fastify 长期支持计划 (LTS) 以本文档为准: + +1. 主要版本发布,[语义化版本][semver] X.Y.Z 发布版本中的 "X" 发布,至少有6个月的支持。特定版本的发布日期可以从[https://github.com/fastify/fastify/releases](https://github.com/fastify/fastify/releases)查到。 + +1. 主要版本将一直获得安全更新,直到下个主要版本发布后的6个月后。在这之后我们依然会发布安全补丁,只要社区有提供,且不会破坏其他约束,例如,支持的 Node.js 最低版本。 + +1. 主要版本会针对其长期支持期内的所有[长期支持的 Node.js 版本](https://github.com/nodejs/Release)进行测试验证。这意味着,只有每行中最新的 Node.js 版本会得到支持。 + +"月" 意思为连续的30天。 + +> ## 安全版本与语义化 Security Releases and Semver +> +> 由于为主要版本提供长期支持十分重要, +> 我们有时需要在 _次要版本_ 上发布重大的改动。 +> 这些变更 _总是_ 会记录在[发布记录](https://github.com/fastify/fastify/releases)里。 +> +> 要避免自动升级到重大的安全更新版本, +> 你可以使用波浪号 (`~`) 来标识版本范围。 +> 例如,将依赖标识为 `"fastify": "~3.15.x"`, +> 可以为 3.15 更新补丁,也避免了自动升级到 3.16。 +> 这么做会使你的应用变得脆弱,因此请谨慎使用。 + +[semver]: https://semver.org/ + + + +### 计划 + +| 版本 | 发布日期 | 长期支持结束 | Node.js 版本 | +| :------ | :----------- | :-------------- | :------------------- | +| 1.0.0 | 2018-03-06 | 2019-09-01 | 6, 8, 9, 10, 11 | +| 2.0.0 | 2019-02-25 | 2021-01-31 | 6, 8, 10, 12, 14 | +| 3.0.0 | 2020-07-07 | 待定 | 10, 12, 14, 16 | + + + +### 经过 CI (持续集成) 测试的操作系统 + +Fastify 使用 GitHub Actions 来进行 CI 测试,请参阅 [GitHub 相关文档](https://docs.github.com/cn/actions/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources)来获取下表 YAML 工作流标签所对应的最新虚拟机环境。 + +| 系统 | YAML 工作流标签 | 包管理器 | Node.js | +|---------|------------------------|---------------------------|--------------| +| Linux | `ubuntu-latest` | npm | 10,12,14,16 | +| Linux | `ubuntu-18.04` | yarn,pnpm | 10,12 | +| Windows | `windows-latest` | npm | 10,12,14,16 | +| MacOS | `macos-latest` | npm | 10,12,14,16 | + +使用 [yarn](https://yarnpkg.com/) 命令需添加 `--ignore-engines`。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Lifecycle.md b/doc/fastify-docs/docs/Lifecycle.md new file mode 100644 index 0000000..850ca6a --- /dev/null +++ b/doc/fastify-docs/docs/Lifecycle.md @@ -0,0 +1,78 @@ +

Fastify

+ +## 生命周期 +下图展示了 Fastify 的内部生命周期。
+每个节点右边的分支为生命周期的下一阶段,左边的则是上一个生命周期抛出错误时产生的错误码 *(请注意 Fastify 会自动处理所有的错误)*。 + +``` +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 +``` + +在`用户编写的处理函数`执行前或执行时,你可以调用 `reply.hijack()` 以使得 Fastify: +- 终止运行所有钩子及用户的处理函数 +- 不再自动发送响应 + +特别注意 (*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。 + +## 响应生命周期 + +不管用户如何处理请求,结果无非以下几种: + +- 异步函数中返回 payload +- 异步函数中抛出 `Error` +- 同步函数中发送 payload +- 同步函数中发送 `Error` 实例 + +当响应被劫持时 (即调用了 `reply.hijack()`) 会跳过之后的步骤,否则,响应被提交后的数据流向如下: + +``` + ★ schema validation Error + │ + └─▶ schemaErrorFormatter + │ + reply sent ◀── JSON ─┴─ Error instance + │ + │ ★ throw an Error + ★ send or return │ │ + │ │ │ + │ ▼ │ + reply sent ◀── JSON ─┴─ Error instance ──▶ setErrorHandler ◀─────┘ + │ + reply sent ◀── JSON ─┴─ Error instance ──▶ onError Hook + │ + └─▶ reply sent +``` + +注:`reply sent` 意味着 JSON payload 将会如下被序列化: + +- 通过[响应序列化方法](Server.md#setreplyserializer) (如有设置) +- 或当为返回的 HTTP 状态码设置了 JSON schema 时,通过[序列化函数生成器](Server.md#setserializercompiler) +- 或通过默认的 `JSON.stringify` 函数 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Logging.md b/doc/fastify-docs/docs/Logging.md new file mode 100644 index 0000000..839a669 --- /dev/null +++ b/doc/fastify-docs/docs/Logging.md @@ -0,0 +1,161 @@ +

Fastify

+ +## 日志 + +日志默认关闭,你可以在创建 Fastify 实例时传入 `{ logger: true }` 或者 `{ logger: { level: 'info' } }` 选项来开启它。要注意的是,日志无法在运行时启用。为此,我们使用了 +[abstract-logging](https://www.npmjs.com/package/abstract-logging)。 + +Fastify 专注于性能,因此使用了 [pino](https://github.com/pinojs/pino) 作为日志工具。默认的日志级别为 `'info'`。 + +开启日志相当简单: + +```js +const fastify = require('fastify')({ + logger: true +}) + +fastify.get('/', options, function (request, reply) { + request.log.info('Some info about the current request') + reply.send({ hello: 'world' }) +}) +``` + +在路由函数之外,你可以通过 Fastify 实例上挂载的 Pino 实例来记录日志: +```js +fastify.log.info('Something important happened!'); +``` + +如果你想为日志配置选项,直接将选项传递给 Fastify 实例就可以了。 +你可以在 [Pino 的文档](https://github.com/pinojs/pino/blob/master/docs/api.md#pinooptions-stream)中找到全部选项。如果你想指定文件地址,可以: + +```js +const fastify = require('fastify')({ + logger: { + level: 'info', + file: '/path/to/file' // 将调用 pino.destination() + } +}) +fastify.get('/', options, function (request, reply) { + request.log.info('Some info about the current request') + reply.send({ hello: 'world' }) +}) +``` + +如果需要向 Pino 传送自定义流 (stream),仅需在 `logger` 对象中添加 `stream` 一项即可。 + +```js +const split = require('split2') +const stream = split(JSON.parse) + +const fastify = require('fastify')({ + logger: { + level: 'info', + stream: stream + } +}) +``` + + +默认情况下,Fastify 给每个请求分配了一个 ID 以便跟踪。如果头部存在 "request-id" 即使用该值,否则会生成一个新的增量 ID。你可以通过 Fastify 工厂函数的 [`requestIdHeader`](Server.md#factory-request-id-header) 与 [`genReqId`](Server.md#genreqid) 来进行自定义。 + +默认的日志工具使用标准的序列化工具,生成包括 `req`、`res` 与 `err` 属性在内的序列化对象。`req` 对象是 Fastify [`Request`](./Request.md) 对象,而 `res` 则是 Fastify [`Reply`](./Reply.md) 对象。可以借由指定自定义的序列化工具来改变这一行为。 +```js +const fastify = require('fastify')({ + logger: { + serializers: { + req (request) { + return { url: request.url } + } + } + } +}) +``` +响应的 payload 与 header 可以按如下方式记录日志 (即便这是*不推荐*的做法): + +```js +const fastify = require('fastify')({ + logger: { + prettyPrint: true, + serializers: { + res (reply) { + // 默认 + return { + statusCode: reply.statusCode + } + }, + req (request) { + return { + method: request.method, + url: request.url, + path: request.path, + parameters: request.parameters, + // 记录 header 可能会触犯隐私法律,例如 GDPR (译注:General Data Protection Regulation)。你应该用 "redact" 选项来移除敏感的字段。此外,验证数据也可能在日志中泄露。 + headers: request.headers + }; + } + } + } +}); +``` +**注**:在 `req` 方法中,body 无法被序列化。因为请求是在创建子日志时就序列化了,而此时 body 尚未被解析。 + +以下是记录 `req.body` 的一个方法 + +```js +app.addHook('preHandler', function (req, reply, done) { + if (req.body) { + req.log.info({ body: req.body }, 'parsed body') + } + done() +}) +``` + +*Pino 之外的日志工具会忽略该选项。* + +你还可以提供自定义的日志实例。将实例传入,取代配置选项即可。提供的示例必须实现 Pino 的接口,换句话说,便是拥有下列方法: +`info`、`error`、`debug`、`fatal`、`warn`、`trace`、`child`。 + +示例: + +```js +const log = require('pino')({ level: 'info' }) +const fastify = require('fastify')({ logger: log }) + +log.info('does not have request information') + +fastify.get('/', function (request, reply) { + request.log.info('includes request information, but is the same logger instance as `log`') + reply.send({ hello: 'world' }) +}) +``` + +*当前请求的日志实例在[生命周期](Lifecycle.md)的各部分均可使用。* + +## 日志修订 + +[Pino](https://getpino.io) 支持低开销的日志修订,以隐藏特定内容。 +举例来说,出于安全方面的考虑,我们也许想在 HTTP header 的日志中隐藏 `Authorization` 这一个 header: + +```js +const fastify = Fastify({ + logger: { + stream: stream, + redact: ['req.headers.authorization'], + level: 'info', + serializers: { + req (request) { + return { + method: request.method, + url: request.url, + headers: request.headers, + hostname: request.hostname, + remoteAddress: request.ip, + remotePort: request.socket.remotePort + } + } + } + } +}) +``` + +更多信息请看 https://getpino.io/#/docs/redaction。 diff --git a/doc/fastify-docs/docs/Middleware.md b/doc/fastify-docs/docs/Middleware.md new file mode 100644 index 0000000..dbe0eb0 --- /dev/null +++ b/doc/fastify-docs/docs/Middleware.md @@ -0,0 +1,28 @@ +

Fastify

+ +## 中间件 + +从 3.0.0 版本开始,Fastify 便不再内建地支持中间件了,你需要通过插件例如 [`fastify-express`](https://github.com/fastify/fastify-express) 或 [`middie`](https://github.com/fastify/middie) 来使用它们。 + +以下是通过 [`fastify-express`](https://github.com/fastify/fastify-express) 插件,来使用 express 中间件的示例: + +```js +await fastify.register(require('fastify-express')) +fastify.use(require('cors')()) +fastify.use(require('dns-prefetch-control')()) +fastify.use(require('frameguard')()) +fastify.use(require('hsts')()) +fastify.use(require('ienoopen')()) +fastify.use(require('x-xss-protection')()) +``` + +或者通过 [`middie`](https://github.com/fastify/middie),它提供了对简单的 express 风格的中间件的支持,但性能更佳: + +```js +await fastify.register(require('middie')) +fastify.use(require('cors')()) +``` + +### 替代 + +Fastify 提供了最常用中间件的替代品,例如:[`fastify-helmet`](https://github.com/fastify/fastify-helmet) 之于 [`helmet`](https://github.com/helmetjs/helmet),[`fastify-cors`](https://github.com/fastify/fastify-cors) 之于 [`cors`](https://github.com/expressjs/cors),以及 [`fastify-static`](https://github.com/fastify/fastify-static) 之于 [`serve-static`](https://github.com/expressjs/serve-static)。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Migration-Guide-V3.md b/doc/fastify-docs/docs/Migration-Guide-V3.md new file mode 100644 index 0000000..d08e8ee --- /dev/null +++ b/doc/fastify-docs/docs/Migration-Guide-V3.md @@ -0,0 +1,241 @@ +# 迁移到 V3 + +本文帮助你从 Fastify v2 迁移到 v3。 + +开始之前,请确保所有 v2 中不推荐用法的警示都已修复。v2 的这些特性在新版都已被移除,升级后你将不能使用它们。([#1750](https://github.com/fastify/fastify/pull/1750)) + +## 重大改动 + +### 中间件支持 ([#2014](https://github.com/fastify/fastify/pull/2014)) + +从 Fastify v3 开始,框架本身便不再支持中间件功能了。 + +要使用 Express 的中间件的话,请安装 [`fastify-express`](https://github.com/fastify/fastify-express) 或 [`middie`](https://github.com/fastify/middie)。 + +**v2:** + +```js +// 在 Fastify v2 中使用 Express 的 `cors` 中间件。 +fastify.use(require('cors')()); +``` + +**v3:** + +```js +// 在 Fastify v3 中使用 Express 的 `cors` 中间件。 +await fastify.register(require('fastify-express')); +fastify.use(require('cors')()); +``` + +### 日志序列化 ([#2017](https://github.com/fastify/fastify/pull/2017)) + +日志的[序列化器](Logging.md)得到了升级,现在它接受 Fastify 的 [`Request`](Request.md) 和 [`Reply`](Reply.md) 对象,而非原生的对象。 + +任何依赖于原生对象而非 Fastify 对象上的 `request` 或 `reply` 属性的自定义的序列化器,都应当升级。 + +**v2:** + +```js +const fastify = require('fastify')({ + logger: { + serializers: { + res(res) { + return { + statusCode: res.statusCode, + customProp: res.customProp + }; + } + } + } +}); +``` + +**v3:** + +```js +const fastify = require('fastify')({ + logger: { + serializers: { + res(reply) { + return { + statusCode: reply.statusCode, // 无需更改 + customProp: reply.raw.customProp // 从 res 对象 (译注:即 Node.js 原生的响应对象,此处为 raw) 中记录属性 + }; + } + } + } +}); +``` + +### schema 代入 (schema substitution) ([#2023](https://github.com/fastify/fastify/pull/2023)) + +非标准`替换方式`的支持被移除了,取而代之的是符合 JSON Schema 标准的 `$ref` 方案。要更好地理解这一改变,请阅读[《Fastify v3 的验证与序列化》](https://dev.to/eomm/validation-and-serialization-in-fastify-v3-2e8l)一文。 + +**v2:** + +```js +const schema = { + body: 'schemaId#' +}; +fastify.route({ method, url, schema, handler }); +``` + +**v3:** + +```js +const schema = { + body: { + $ref: 'schemaId#' + } +}; +fastify.route({ method, url, schema, handler }); +``` + +### schema 验证选项 ([#2023](https://github.com/fastify/fastify/pull/2023)) + +为了未来工具的改善,`setSchemaCompiler` 和 `setSchemaResolver` 被替换成了 `setValidatorCompiler`。要更好地理解这一改变,请阅读[《Fastify v3 的验证与序列化》](https://dev.to/eomm/validation-and-serialization-in-fastify-v3-2e8l)一文。 + +**v2:** + +```js +const fastify = Fastify(); +const ajv = new AJV(); +ajv.addSchema(schemaA); +ajv.addSchema(schemaB); + +fastify.setSchemaCompiler(schema => ajv.compile(schema)); +fastify.setSchemaResolver(ref => ajv.getSchema(ref).schema); +``` + +**v3:** + +```js +const fastify = Fastify(); +const ajv = new AJV(); +ajv.addSchema(schemaA); +ajv.addSchema(schemaB); + +fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => + ajv.compile(schema) +); +``` + +### preParsing 钩子的行为 ([#2286](https://github.com/fastify/fastify/pull/2286)) + +为了支持针对请求 payload 的操作,从 Fastify v3 开始,`preParsing` 钩子的行为发生了微小的变化。 + +该钩子现在有一个额外的参数,`payload`,因此,新的函数签名是 `fn(request, reply, payload, done)` 或 `async fn(request, reply, payload)`。 + +钩子可以通过 `done(null, stream)` 回调,或 async 函数返回一个 stream。 + +如果返回了新的 stream,那么在之后的钩子里,这个新的 stream 将代替原有的 stream。这种做法的一个用途是处理经过压缩的请求。 + +新的 stream 还应当添加 `receivedEncodedLength` 属性,来反映客户端数据的真实大小。举例而言,假如请求被压缩了,那么该属性便是压缩后的 payload 的大小。该属性可以 (且应当) 在 `data` 事件中动态更新。 + +原先 Fastify v2 的语法仍然受支持,但不推荐使用。 + +### 钩子的行为 ([#2004](https://github.com/fastify/fastify/pull/2004)) + +为了支持钩子的封装,从 Fastify v3 开始,`onRoute` 与 `onRegister` 钩子的行为发生了微小的变化。 + +- `onRoute` - 现在改为了异步调用,且在同一个封装的作用域内会继承。因此,你得在注册其他插件 _之前_ 注册该钩子。 +- `onRegister` - 和 onRoute 一样。唯一区别在于现在第一次的调用者不是框架本身了,而是首个注册的插件。 + +### Content Type 解析器的语法 ([#2286](https://github.com/fastify/fastify/pull/2286)) + +在 Fastify v3 中,Content Type 解析器现在有了单一函数签名。 + +新的签名是 `fn(request, payload, done)` 或 `async fn(request, payload)`。注意现在 `request` 是 Fastify 的请求对象,而不是 `IncomingMessage` 了。默认情况下,payload 是一个 stream。如果在 `addContentTypeParser` 中使用了 `parseAs` 选项,那么 `payload` 会被当做该选项的值来对待 (string 或 buffer)。 + +原先的函数签名 `fn(req, [done])` 或 `fn(req, payload, [done])` (这里 `req` 是 `IncomingMessage`) 仍然受支持,但不推荐使用。 + +### TypeScript 支持 + +版本 3 的类型系统发生了改变。新的系统带来了泛型约束 (generic constraining) 与默认值,以及定义请求 body,querystring 等 schema 的新方式! + +**v2:** + +```ts +interface PingQuerystring { + foo?: number; +} + +interface PingParams { + bar?: string; +} + +interface PingHeaders { + a?: string; +} + +interface PingBody { + baz?: string; +} + +server.get( + '/ping/:bar', + opts, + (request, reply) => { + console.log(request.query); // 类型为 `PingQuerystring` + console.log(request.params); // 类型为 `PingParams` + console.log(request.headers); // 类型为 `PingHeaders` + console.log(request.body); // 类型为 `PingBody` + } +); +``` + +**v3:** + +```ts +server.get<{ + Querystring: PingQuerystring; + Params: PingParams; + Headers: PingHeaders; + Body: PingBody; +}>('/ping/:bar', opts, async (request, reply) => { + console.log(request.query); // 类型为 `PingQuerystring` + console.log(request.params); // 类型为 `PingParams` + console.log(request.headers); // 类型为 `PingHeaders` + console.log(request.body); // 类型为 `PingBody` +}); +``` + +### 管理未捕获异常 ([#2073](https://github.com/fastify/fastify/pull/2073)) + +在同步的路由方法里,一旦一个错误被抛出,服务器将会如设定的那样崩溃,且不调用 `.setErrorHandler()` 方法。现在这一行为发生了改变,所有在同步或异步的路由中未捕获的异常,都将得到控制。 + +**v2:** + +```js +fastify.setErrorHandler((error, request, reply) => { + // 不会调用 + reply.send(error) +}) +fastify.get('/', (request, reply) => { + const maybeAnArray = request.body.something ? [] : 'I am a string' + maybeAnArray.substr() // 抛错:[].substr is not a function,同时服务器崩溃 +}) +``` + +**v3:** + +```js +fastify.setErrorHandler((error, request, reply) => { + // 会调用 + reply.send(error) +}) +fastify.get('/', (request, reply) => { + const maybeAnArray = request.body.something ? [] : 'I am a string' + maybeAnArray.substr() // 抛错:[].substr is not a function,但错误得到控制 +}) +``` + +## 更多特性与改善 + +- 不管如何注册钩子,它们总拥有一致的上下文 ([#2005](https://github.com/fastify/fastify/pull/2005)) +- 推荐使用 [`request.raw`](Request.md) 及 [`reply.raw`](Reply.md),而非 `request.req` 和 `reply.res` ([#2008](https://github.com/fastify/fastify/pull/2008)) +- 移除 `modifyCoreObjects` 选项 ([#2015](https://github.com/fastify/fastify/pull/2015)) +- 添加 [`connectionTimeout`](Server.md#factory-connection-timeout) 选项 ([#2086](https://github.com/fastify/fastify/pull/2086)) +- 添加 [`keepAliveTimeout`](Server.md#factory-keep-alive-timeout) 选项 ([#2086](https://github.com/fastify/fastify/pull/2086)) +- [插件](Plugins.md#async-await)支持 async-await ([#2093](https://github.com/fastify/fastify/pull/2093)) +- 支持将对象作为错误抛出 ([#2134](https://github.com/fastify/fastify/pull/2134)) \ No newline at end of file diff --git a/doc/fastify-docs/docs/Plugins-Guide.md b/doc/fastify-docs/docs/Plugins-Guide.md new file mode 100644 index 0000000..87a7c7b --- /dev/null +++ b/doc/fastify-docs/docs/Plugins-Guide.md @@ -0,0 +1,376 @@ +

Fastify

+ +# 插件漫游指南 +首先, `不要恐慌`! + +Fastify 从一开始就搭建成非常模块化的系统. 我们搭建了非常强健的 API 来允许你创建命名空间, 来添加工具方法. Fastify 创建的封装模型可以让你在任何时候将你的应用分割成不同的微服务, 而无需重构整个应用. + +**内容清单** +- [注册器](#register) +- [装饰器](#decorators) +- [钩子方法](#hooks) +- [如何处理封装与分发](#distribution) +- [ESM 的支持](#esm-support) +- [错误处理](#handle-errors) +- [自定义错误](#custom-errors) +- [发布提醒](#emit-warnings) +- [开始!](#start) + + +## 注册器 +就像在 JavaScript 万物都是对象, 在 Fastify 万物都是插件.
+你的路由, 你的工具方法等等都是插件. 无论添加什么功能的插件, 你都可以使用 Fastify 优秀又独一无二的 API: [`register`](Plugins.md). +```js +fastify.register( + require('./my-plugin'), + { options } +) +``` +`register` 创建一个新的 Fastify 上下文, 这意味着如果你对 Fastify 的实例做任何改动, 这些改动不会反映到上下文的父级上. 换句话说, 封装! + +*为什么封装这么重要?*
+那么, 假设你创建了一个具有开创性的初创公司, 你会怎么做? 你创建了一个包含所有东西的 API 服务器, 所有东西都在同一个地方, 一个庞然大物!
+现在, 你增长得非常迅速, 想要改变架构去尝试微服务. 通常这意味着非常多的工作, 因为交叉依赖和缺少关注点的分离.
+Fastify 在这个层面上可以帮助你很多, 多亏了封装模型, 它完全避免了交叉依赖, 并且帮助你将组织成高聚合的代码块. + +*让我们回到如何正确地使用 `register`.*
+插件必须输出一个有以下参数的方法 +```js +module.exports = function (fastify, options, done) {} +``` +`fastify` 就是封装的 Fastify 实例, `options` 就是选项对象, 而 `done` 是一个在插件准备好了之后**必须**要调用的方法. + +Fastify 的插件模型是完全可重入的和基于图(数据结构)的, 它能够处理任何异步代码并且保证插件的加载顺序, 甚至是关闭顺序! *如何做到的?* 很高兴你发问了, 查看下 [`avvio`](https://github.com/mcollina/avvio)! Fastify 在 `.listen()`, `.inject()` 或者 `.ready()` 被调用了之后开始加载插件. + +在插件里面你可以做任何想要做的事情, 注册路由, 工具方法 (我们马上会看到这个) 和进行嵌套的注册, 只要记住当所有都设置好了后调用 `done`! +```js +module.exports = function (fastify, options, done) { + fastify.get('/plugin', (request, reply) => { + reply.send({ hello: 'world' }) + }) + + done() +} +``` + +那么现在你已经知道了如何使用 `register` API 并且知道它是怎么工作的, 但我们如何给 Fastify 添加新的功能, 并且分享给其他的开发者? + + +## 装饰器 +好了, 假设你写了一个非常好的工具方法, 因此你决定在你所有的代码里都能够用这个方法. 你改怎么做? 可能是像以下代码一样: +```js +// your-awesome-utility.js +module.exports = function (a, b) { + return a + b +} +``` +```js +const util = require('./your-awesome-utility') +console.log(util('that is ', 'awesome')) +``` +现在你需要在所有需要这个方法的文件中引入它. (别忘了你可能在测试中也需要它). + +Fastify 提供了一个更优雅的方法, *装饰器*. +创建一个装饰器非常简单, 只要使用 [`decorate`](Decorators.md) API: +```js +fastify.decorate('util', (a, b) => a + b) +``` +现在你可以在任意地方通过 `fastify.util` 调用你的方法, 甚至在你的测试中.
+这里神奇的是: 你还记得之前我们讨论的封装? 同时使用 `register` 和 `decorate` 可以实现, 让我用例子来阐明这个事情: +```js +fastify.register((instance, opts, done) => { + instance.decorate('util', (a, b) => a + b) + console.log(instance.util('that is ', 'awesome')) + + done() +}) + +fastify.register((instance, opts, done) => { + console.log(instance.util('that is ', 'awesome')) // 这里会抛错 + + done() +}) +``` +在第二个注册器中调用 `instance.util` 会抛错, 因为 `util` 只存在第一个注册器的上下文中.
+让我们更深入地看一下: 当使用 `register` API 每次都会创建一个新的上下文而且这避免了上文提到的这个状况. + +但是注意, 封装只会在父级和同级中有效, 不会在子级中有效. +```js +fastify.register((instance, opts, done) => { + instance.decorate('util', (a, b) => a + b) + console.log(instance.util('that is ', 'awesome')) + + fastify.register((instance, opts, done) => { + console.log(instance.util('that is ', 'awesome')) // 这里不会抛错 + done() + }) + + done() +}) + +fastify.register((instance, opts, done) => { + console.log(instance.util('that is ', 'awesome')) // 这里会抛错 + + done() +}) +``` +*PS: 如果你需要全局的工具方法, 请注意要声明在应用根作用域上. 或者你可以使用 `fastify-plugin` 工具, [参考](#distribution).* + +`decorate` 不是唯一可以用来扩展服务器的功能的 API, 你还可以使用 `decorateRequest` 和 `decorateReply`. + +*`decorateRequest` 和 `decorateReply`? 为什么我们已经有了 `decorate` 还需要它们?*
+好问题, 是为了让开发者更方便地使用 Fastify. 让我们看看这个例子: +```js +fastify.decorate('html', payload => { + return generateHtml(payload) +}) + +fastify.get('/html', (request, reply) => { + reply + .type('text/html') + .send(fastify.html({ hello: 'world' })) +}) +``` +这个可行, 但可以变得更好! +```js +fastify.decorateReply('html', function (payload) { + this.type('text/html') // this 是 'Reply' 对象 + this.send(generateHtml(payload)) +}) + +fastify.get('/html', (request, reply) => { + reply.html({ hello: 'world' }) +}) +``` + +你可以对 `request` 对象做同样的事: +```js +fastify.decorate('getHeader', (req, header) => { + return req.headers[header] +}) + +fastify.addHook('preHandler', (request, reply, done) => { + request.isHappy = fastify.getHeader(request.raw, 'happy') + done() +}) + +fastify.get('/happiness', (request, reply) => { + reply.send({ happy: request.isHappy }) +}) +``` +这个也可行, 但可以变得更好! +```js +fastify.decorateRequest('setHeader', function (header) { + this.isHappy = this.headers[header] +}) + +fastify.decorateRequest('isHappy', false) // 这会添加到 Request 对象的原型中, 好快! + +fastify.addHook('preHandler', (request, reply, done) => { + request.setHeader('happy') + done() +}) + +fastify.get('/happiness', (request, reply) => { + reply.send({ happy: request.isHappy }) +}) +``` + +我们见识了如何扩展服务器的功能并且如何处理封装系统, 但是假如你需要加一个方法, 每次在服务器 "[emits](Lifecycle.md)" 事件的时候执行这个方法, 该怎么做? + + +## 钩子方法 +你刚刚构建了工具方法, 现在你需要在每个请求的时候都执行这个方法, 你大概会这样做: +```js +fastify.decorate('util', (request, key, value) => { request[key] = value }) + +fastify.get('/plugin1', (request, reply) => { + fastify.util(request, 'timestamp', new Date()) + reply.send(request) +}) + +fastify.get('/plugin2', (request, reply) => { + fastify.util(request, 'timestamp', new Date()) + reply.send(request) +}) +``` +我想大家都同意这个代码是很糟的. 代码重复, 可读性差并且不能扩展. + +那么你该怎么消除这个问题呢? 是的, 使用[钩子方法](Hooks.md)!
+```js +fastify.decorate('util', (request, key, value) => { request[key] = value }) + +fastify.addHook('preHandler', (request, reply, done) => { + fastify.util(request, 'timestamp', new Date()) + done() +}) + +fastify.get('/plugin1', (request, reply) => { + reply.send(request) +}) + +fastify.get('/plugin2', (request, reply) => { + reply.send(request) +}) +``` +现在每个请求都会运行工具方法, 很显然你可以注册任意多的需要的钩子方法.
+有时, 你希望只在一个路由子集中执行钩子方法, 这个怎么做到? 对了, 封装! + +```js +fastify.register((instance, opts, done) => { + instance.decorate('util', (request, key, value) => { request[key] = value }) + + instance.addHook('preHandler', (request, reply, done) => { + instance.util(request, 'timestamp', new Date()) + done() + }) + + instance.get('/plugin1', (request, reply) => { + reply.send(request) + }) + + done() +}) + +fastify.get('/plugin2', (request, reply) => { + reply.send(request) +}) +``` +现在你的钩子方法只会在第一个路由中运行! + +你可能已经注意到, `request` and `reply` 不是标准的 Nodejs *request* 和 *response* 对象, 而是 Fastify 对象.
+ + +## 如何处理封装与分发 +完美, 现在你知道了(几乎)所有的扩展 Fastify 的工具. 但可能你遇到了一个大问题: 如何分发你的代码? + +我们推荐将所有代码包裹在一个`注册器`中分发, 这样你的插件可以支持异步启动 *(`decorate` 是一个同步 API)*, 例如建立数据库链接. + +*等等? 你不是告诉我 `register` 会创建封装的上下文, 那么我创建的不是就外层不可见了?*
+是的, 我是说过. 但我没告诉你的是, 你可以通过 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 模块告诉 Fastify 不要进行封装. +```js +const fp = require('fastify-plugin') +const dbClient = require('db-client') + +function dbPlugin (fastify, opts, done) { + dbClient.connect(opts.url, (err, conn) => { + fastify.decorate('db', conn) + done() + }) +} + +module.exports = fp(dbPlugin) +``` +你还可以告诉 `fastify-plugin` 去检查安装的 Fastify 版本, 万一你需要特定的 API. + +正如前面所述,Fastify 在 `.listen()`、`.inject()` 以及 `.ready()` 被调用,也即插件被声明 __之后__ 才开始加载插件。这么一来,即使插件通过 [`decorate`](Decorators.md) 向外部的 Fastify 实例注入了变量,在调用 `.listen()`、`.inject()` 和 `.ready()` 之前,这些变量是获取不到的。 + +当你需要在 `register` 方法的 `options` 参数里使用另一个插件注入的变量时,你可以向 `options` 传递一个函数参数,而不是对象: +```js +const fastify = require('fastify')() +const fp = require('fastify-plugin') +const dbClient = require('db-client') + +function dbPlugin (fastify, opts, done) { + dbClient.connect(opts.url, (err, conn) => { + fastify.decorate('db', conn) + done() + }) +} + +fastify.register(fp(dbPlugin), { url: 'https://example.com' }) +fastify.register(require('your-plugin'), parent => { + return { connection: parent.db, otherOption: 'foo-bar' } +}) +``` +在上面的例子中,`register` 方法的第二个参数的 `parent` 变量是注册了插件的**外部 Fastify 实例**的一份拷贝。这就意味着我们可以获取到之前声明的插件所注入的变量了。 + + +## ESM 的支持 + +自 [Node.js `v13.3.0`](https://nodejs.org/api/esm.html) 开始, ESM 也被支持了!写插件时,你只需要将其作为 ESM 模块导出即可! + +```js +// plugin.mjs +async function plugin (fastify, opts) { + fastify.get('/', async (req, reply) => { + return { hello: 'world' } + }) +} + +export default plugin +``` + +__注意__:Fastify 不支持具名导入 ESM 模块,但支持 `default` 导入。 + +```js +// server.mjs +import Fastify from 'fastify' + +const fastify = Fastify() + +///... + +fastify.listen(3000, (err, address) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` + + +## 错误处理 +你的插件也可能在启动的时候失败. 或许你预料到这个并且在这种情况下有特定的处理逻辑. 你该怎么实现呢? +`after` API 就是你需要的. `after` 注册一个回调, 在注册之后就会调用这个回调, 它可以有三个参数.
+回调会基于不同的参数而变化: + +1. 如果没有参数并且有个错误, 这个错误会传递到下一个错误处理. +1. 如果有一个参数, 这个参数就是错误对象. +1. 如果有两个参数, 第一个是错误对象, 第二个是完成回调. +1. 如果有三个参数, 第一个是错误对象, 第二个是顶级上下文(除非你同时指定了服务器和复写, 在这个情况下将会是那个复写的返回), 第三个是完成回调. + +让我们看看如何使用它: +```js +fastify + .register(require('./database-connector')) + .after(err => { + if (err) throw err + }) +``` + + +## 自定义错误 +假如你的插件需要暴露自定义的错误,[`fastify-error`](https://github.com/fastify/fastify-error) 能帮助你轻松地在代码或插件中生成一致的错误对象。 + +```js +const createError = require('fastify-error') +const CustomError = createError('ERROR_CODE', 'message') +console.log(new CustomError()) +``` + + +## 发布提醒 +假如你要提示用户某个 API 不被推荐,或某个特殊场景需要注意,你可以使用 [`fastify-warning`](https://github.com/fastify/fastify-warning)。 + +```js +const warning = require('fastify-warning')() +warning.create('FastifyDeprecation', 'FST_ERROR_CODE', 'message') +warning.emit('FST_ERROR_CODE') +``` + + +## 开始! +太棒了, 现在你已经知道了所有创建插件需要的关于 Fastify 和它的插件系统的知识, 如果你写了插件请告诉我们! 我们会将它加入到 [*生态*](https://github.com/fastify/fastify#ecosystem) 章节中! + +如果你想要看看真正的插件例子, 查看: +- [`point-of-view`](https://github.com/fastify/point-of-view) +给 Fastify 提供模版 (*ejs, pug, handlebars, marko*) 支持. +- [`fastify-mongodb`](https://github.com/fastify/fastify-mongodb) +Fastify MongoDB 连接插件, 可以在全局使用同一 MongoDb 连接池. +- [`fastify-multipart`](https://github.com/fastify/fastify-multipart) +Multipart 支持 +- [`fastify-helmet`](https://github.com/fastify/fastify-helmet) 重要的安全头部支持 + + +*如果感觉还差什么? 告诉我们! :)* diff --git a/doc/fastify-docs/docs/Plugins.md b/doc/fastify-docs/docs/Plugins.md new file mode 100644 index 0000000..10b8927 --- /dev/null +++ b/doc/fastify-docs/docs/Plugins.md @@ -0,0 +1,186 @@ +

Fastify

+ +## 插件 +Fastify 允许用户通过插件的方式扩展自身的功能。 +一个插件可以是一组路由,一个服务器[装饰器](Decorators.md)或者其他任意的东西。 在使用一个或者许多插件时,只需要一个 API `register`。
+ +默认, `register` 会创建一个 *新的作用域( Scope )*, 这意味着你能够改变 Fastify 实例(通过`decorate`), 这个改变不会反映到当前作用域, 只会影响到子作用域。 这样可以做到插件的*封装*和*继承*, 我们创建了一个*无回路有向图*(DAG), 因此不会有交叉依赖的问题。 + +你已经在[起步](Getting-Started.md#register)部分很直观的看到了怎么使用这个 API。 +``` +fastify.register(plugin, [options]) +``` + + +### 插件选项 +`fastify.register` 可选参数列表支持一组预定义的 Fastify 可用的参数, 除了当插件使用了 [fastify-plugin](https://github.com/fastify/fastify-plugin)。 选项对象会在插件被调用传递进去, 无论这个插件是否用了 fastify-plugin。 当前支持的选项有: + ++ [`日志级别`](Routes.md#custom-log-level) ++ [`日志序列化器`](Routes.md#custom-log-serializer) ++ [`前缀`](Plugins.md#route-prefixing-options) + +**注意:当使用 fastify-plugin 时,这些选项会被忽略** + +Fastify 有可能在将来会直接支持其他的选项。 因此为了避免冲突, 插件应该考虑给选项加入命名空间。 举个例子, 插件 `foo` 可以像以下代码一样注册: + +```js +fastify.register(require('fastify-foo'), { + prefix: '/foo', + foo: { + fooOption1: 'value', + fooOption2: 'value' + } +}) +``` + +如果不考虑冲突, 插件可以简化成直接接收对象参数: + +```js +fastify.register(require('fastify-foo'), { + prefix: '/foo', + fooOption1: 'value', + fooOption2: 'value' +}) +``` + +`options` 参数还可以是一个在插件注册时确定的 `函数`,这个函数的第一位参数是 Fastify 实例: + +```js +const fp = require('fastify-plugin') + +fastify.register(fp((fastify, opts, done) => { + fastify.decorate('foo_bar', { hello: 'world' }) + + done() +})) + +// fastify-foo 的 options 参数会是 { hello: 'world' } +fastify.register(require('fastify-foo'), parent => parent.foo_bar) +``` + +传给函数的 Fastify 实例是插件声明时**外部 Fastify 实例**的最新状态,允许你访问**注册顺序**在前的插件通过 [`decorate`](Decorators.md) 注入的变量。这在需要依赖前置插件对于 Fastify 实例的改动时派得上用场,比如,使用已存在的数据库连接来包装你的插件。 + +请记住,传给函数的 Fastify 实例和传给插件的实例是一样的,不是外部 Fastify 实例的引用,而是拷贝。任何对函数的实例参数的操作结果,都会和在插件函数中操作的结果一致。也就是说,如果调用了 `decorate`,被注入的变量在插件函数中也是可用的,除非你使用 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 包装了这个插件。 + + +#### 路由前缀选项 +如果你传入以 `prefix`为 key , `string` 为值的选项, Fastify 会自动为这个插件下所有的路由添加这个前缀, 更多信息可以查询 [这里](Routes.md#route-prefixing).
+注意如果使用了 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 这个选项不会起作用。 + + +#### 错误处理 +错误处理是由 [avvio](https://github.com/mcollina/avvio#error-handling) 解决的。
+一个通用的原则, 我们建议在下一个 `after` 或 `ready` 代码块中处理错误, 否则错误将出现在 `listen` 回调里。 + +```js +fastify.register(require('my-plugin')) + +// `after` 将在上一个 `register` 结束后执行 +fastify.after(err => console.log(err)) + +// `ready` 将在所有 `register` 结束后执行 +fastify.ready(err => console.log(err)) + +// `listen` 是一个特殊的 `ready`, +// 因此它的执行时机与 `ready` 一致 +fastify.listen(3000, (err, address) => { + if (err) console.log(err) +}) +``` + + +### async/await + +`after`、`ready` 与 `listen` 支持 *async/await*,同时 `fastify` 也是一个 [Thenable](https://promisesaplus.com/) 对象。 + +```js +await fastify.register(require('my-plugin')) + +await fastify.after() + +await fastify.ready() + +await fastify.listen(3000) +``` + + +#### ESM 的支持 + +自 [Node.js `v13.3.0`](https://nodejs.org/api/esm.html) 开始, ESM 也被支持了! + +```js +// main.mjs +import Fastify from 'fastify' +const fastify = Fastify() + +fastify.register(import('./plugin.mjs')) + +fastify.listen(3000, console.log) + +// plugin.mjs +async function plugin (fastify, opts) { + fastify.get('/', async (req, reply) => { + return { hello: 'world' } + }) +} + +export default plugin +``` + + +### 创建插件 +创建插件非常简单, 你只需要创建一个方法, 这个方法接收三个参数: `fastify` 实例、`options` 选项和 `done` 回调。
+例子: +```js +module.exports = function (fastify, opts, done) { + fastify.decorate('utility', () => {}) + + fastify.get('/', handler) + + done() +} +``` +你也可以在一个 `register` 内部添加其他 `register`: +```js +module.exports = function (fastify, opts, done) { + fastify.decorate('utility', () => {}) + + fastify.get('/', handler) + + fastify.register(require('./other-plugin')) + + done() +} +``` +有时候, 你需要知道这个服务器何时即将关闭, 例如在你必须关闭数据库连接的时候。 要知道什么时候发生这种情况, 你可以用 [`'onClose'`](Hooks.md#on-close) 钩子。 + +别忘了 `register` 会创建一个新的 Fastify 作用域, 如果你不需要, 阅读下面的章节。 + + +### 处理作用域 +如果你使用 `register` 仅仅是为了通过[`decorate`](Decorators.md)扩展服务器的功能, 你需要告诉 Fastify 不要创建新的上下文, 不然你的改动不会影响其他作用域中的用户。 + +你有两种方式告诉 Fastify 避免创建新的上下文: +- 使用 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 模块 +- 使用 `'skip-override'` 隐藏属性 + +我们建议使用 `fastify-plugin` 模块, 因为它是专门用来为你解决这个问题, 并且你可以传一个能够支持的 Fastify 版本范围的参数。 +```js +const fp = require('fastify-plugin') + +module.exports = fp(function (fastify, opts, done) { + fastify.decorate('utility', () => {}) + done() +}, '0.x') +``` +参考 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 文档了解更多这个模块。 + +如果你不用 `fastify-plugin` 模块, 可以使用 `'skip-override'` 隐藏属性, 但我们不推荐这么做。 如果将来 Fastify API 改变了, 你需要去更新你的模块, 如果使用 `fastify-plugin`, 你可以对向后兼容放心。 +```js +function yourPlugin (fastify, opts, done) { + fastify.decorate('utility', () => {}) + done() +} +yourPlugin[Symbol.for('skip-override')] = true +module.exports = yourPlugin +``` diff --git a/doc/fastify-docs/docs/Recommendations.md b/doc/fastify-docs/docs/Recommendations.md new file mode 100644 index 0000000..e2cf102 --- /dev/null +++ b/doc/fastify-docs/docs/Recommendations.md @@ -0,0 +1,230 @@ +

Fastify

+ +## 推荐方案 + +本文涵盖了使用 Fastify 的推荐方案及最佳实践。 + +* [使用反向代理](#reverseproxy) +* [Kubernetes](#kubernetes) + +## 使用反向代理 + + +Node.js 作为各框架中的先行者,在标准库中提供了易用的 web 服务器。在它现世前,PHP、Python 等语言的使用者,要么需要一个支持该语言的 web 服务器,要么需要一个搭配该语言的 [CGI 网关](cgi)。而 Node.js 能让用户专注于 _直接_ 处理 HTTP 请求的应用本身,这样一来,新的诱惑点变成了处理多个域名的请求、监听多端口 (如 HTTP _和_ HTTPS),接着将应用直接暴露于 Internet 来处理请求。 + +Fastify 团队**强烈**地认为上述做法是一种反面模式,是非常不理想的实践: + +1. 它分离了程序的关注点,增加了不必要的复杂度。 +2. 它限制了程序的[水平拓展](scale-horiz)。 + +请看《[生产环境可用的 Node.js 为何还需要反向代理?][why-use]》一文,这里有更深入的讨论。 + +考虑以下具体案例: + +1. 应用需要多个实例来处理负载。 +1. 应用需要 TLS 终端 (TLS termination)。 +1. 应用需要将 HTTP 请求转发至 HTTPS。 +1. 应用需要处理多域名。 +1. 应用需要处理静态资源,例如 jpeg 文件。 + +反向代理的解决方案有很多种,例如 AWS 与 GCP,具体根据环境来择用。对于上述的案例,我们可以使用 [HAProxy][haproxy] 或 [Nginx][nginx]。 + +### HAProxy + +```conf +# global 定义了 HAProxy 实例的基础配置。 +global + log /dev/log syslog + maxconn 4096 + chroot /var/lib/haproxy + user haproxy + group haproxy + + # 设置 TLS 基础配置。 + tune.ssl.default-dh-param 2048 + ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 + ssl-default-bind-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS + ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 + ssl-default-server-ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:RSA+AESGCM:RSA+AES:!aNULL:!MD5:!DSS + +# defaults 定义了接下来每个子段落的默认配置,直到出现另一段 defaults。 +defaults + log global + mode http + option httplog + option dontlognull + retries 3 + option redispatch + # 下面的选项使 haproxy 关闭与后端的连接,而不是保持连接。 + # 这么做能减轻 Node 进程发生预期外的连接重置错误。 + option http-server-close + maxconn 2000 + timeout connect 5000 + timeout client 50000 + timeout server 50000 + + # 压缩特定类型的内容。 + compression algo gzip + compression type text/html text/plain text/css application/javascript + +# frontend 定义一个公开的监听器,即客户端所关注的“http 服务器”。 +frontend proxy + # 这里的 IP 地址为服务器的_公开_ IP 地址。 + # 在这个例子里,我们使用一个私有的地址。 + bind 10.0.0.10:80 + # 将所有非 TLS 的请求转发至 HTTPS 端口的相同 URL。 + redirect scheme https code 308 if !{ ssl_fc } + # 从技术角度讲,这里的 use_backend 指令没有用处, + # 因为我们已经将所有到达该监听器的请求转发至 HTTPS 了。 + # 在此提到该指令仅仅是为了示例的完整性。 + use_backend default-server + +# 这段 frontend 定义了主要的 TLS 监听器。 +# 在此我们定义 TLS 证书,以及如何引流来访的请求。 +frontend proxy-ssl + # 本例的 `/etc/haproxy/certs` 文件夹保存了以域名命名的 PEM 格式证书。 + # 当 HAProxy 启动时,它会加载该文件夹中的证书, + # 并使用服务器名称指示协议 (SNI) 将证书应用于对应的连接。 + bind 10.0.0.10:443 ssl crt /etc/haproxy/certs + + # 定义静态资源处理规则。 + # 任何包括 `/static` 路径 (如 `https://one.example.com/static/foo.jpeg`) 的请求, + # 将被重定向至静态资源服务器。 + acl is_static path -i -m beg /static + use_backend static-backend if is_static + + # 根据请求的域名,转发至对应的 Node.js 服务器。 + # `acl` 开头这行用于匹配主机名,并定义了一个布尔值用于判断匹配与否。 + # `use_backend` 这行则在该布尔值为真时进行转发。 + acl example1 hdr_sub(Host) one.example.com + use_backend example1-backend if example1 + + acl example2 hdr_sub(Host) two.example.com + use_backend example2-backend if example2 + + # 最后,我们提供一个后备的重定向,以防上述规则均不适用。 + default_backend default-server + +# backend 告诉 HAProxy 去哪里获取请求所需的信息。 +# 在此我们定义 Node.js 应用及静态资源等其他服务器的地址。 +backend default-server + # 在这个例子中,我们将未匹配域名的请求默认转发到一个能处理所有请求的后端。 + # 值得注意的是,后端服务器不需要处理 TLS 请求。 + # 这被称为“TLS 终端 (TLS termination)”:TLS 连接在反向代理中即得到处理。 + # 代理至有 TLS 请求处理能力的后端也是可行的,但这不在本示例的内容范围之内。 + server server1 10.10.10.2:80 + +# 通过轮询调度,将 `https://one.example.com` 的请求代理至三个后端服务器。 +backend example1-backend + server example1-1 10.10.11.2:80 + server example1-2 10.10.11.2:80 + server example2-2 10.10.11.3:80 + +# 处理 `https://two.example.com` 的请求。 +backend example2-backend + server example2-1 10.10.12.2:80 + server example2-2 10.10.12.2:80 + server example2-3 10.10.12.3:80 + +# 处理静态资源请求。 +backend static-backend + server static-server1 10.10.9.2:80 +``` + +[cgi]: https://en.wikipedia.org/wiki/Common_Gateway_Interface +[scale-horiz]: https://en.wikipedia.org/wiki/Scalability#Horizontal +[why-use]: https://web.archive.org/web/20190821102906/https://medium.com/intrinsic/why-should-i-use-a-reverse-proxy-if-node-js-is-production-ready-5a079408b2ca +[haproxy]: https://www.haproxy.org/ + +### Nginx + +```nginx +upstream fastify_app { + # 更多信息请见:http://nginx.org/en/docs/http/ngx_http_upstream_module.html + server 10.10.11.1:80; + server 10.10.11.2:80; + server 10.10.11.3:80 backup; +} + +server { + # 默认服务器 + listen 80 default_server; + listen [::]:80 default_server; + + # 指定端口 + # listen 80; + # listen [::]:80; + # server_name example.tld; + + location / { + return 301 https://$host$request_uri; + } +} + +server { + # 默认服务器 + listen 443 ssl http2 default_server; + listen [::]:443 ssl http2 default_server; + + # 指定端口 + # listen 443 ssl http2; + # listen [::]:443 ssl http2; + # server_name example.tld; + + # 密钥 + ssl_certificate /path/to/fullchain.pem; + ssl_certificate_key /path/to/private.pem; + ssl_trusted_certificate /path/to/chain.pem; + + # 通过 https://ssl-config.mozilla.org/ 生成最佳配置 + ssl_session_timeout 1d; + ssl_session_cache shared:FastifyApp:10m; + ssl_session_tickets off; + + # 现代化配置 + ssl_protocols TLSv1.3; + ssl_prefer_server_ciphers off; + + # HTTP 严格传输安全 (HSTS) (需要 ngx_http_headers_module 模块) (63072000 秒) + add_header Strict-Transport-Security "max-age=63072000" always; + + # 在线证书状态协议缓存 (OCSP stapling) + ssl_stapling on; + ssl_stapling_verify on; + + # 自定义域名解析器 (resolver) + # resolver 127.0.0.1; + + location / { + # 更多信息请见:http://nginx.org/en/docs/http/ngx_http_proxy_module.html + proxy_http_version 1.1; + proxy_cache_bypass $http_upgrade; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://fastify_app:3000; + } +} +``` + +[nginx]: https://nginx.org/ + +## Kubernetes + + +`readinessProbe` ([默认情况下](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/#configure-probes)) 使用 pod 的 IP 作为主机名。而 Fastify 默认监听的是 `127.0.0.1`。在这种情况下,探针 (probe) 无法探测到应用。这时,应用必须监听 `0.0.0.0`,或在 `readinessProbe.httpGet` 中如下指定一个主机名,才能正常工作: + +```yaml +readinessProbe: + httpGet: + path: /health + port: 4000 + initialDelaySeconds: 30 + periodSeconds: 30 + timeoutSeconds: 3 + successThreshold: 1 + failureThreshold: 5 \ No newline at end of file diff --git a/doc/fastify-docs/docs/Reply.md b/doc/fastify-docs/docs/Reply.md new file mode 100644 index 0000000..bc760b3 --- /dev/null +++ b/doc/fastify-docs/docs/Reply.md @@ -0,0 +1,431 @@ +

Fastify

+ +## 回复 +- [回复](#reply) + - [简介](#introduction) + - [.code(statusCode)](#codestatuscode) + - [.statusCode](#statusCode) + - [.server](#server) + - [.header(key, value)](#headerkey-value) + - [.getHeader(key)](#getheaderkey) + - [.removeHeader(key)](#removeheaderkey) + - [.hasHeader(key)](#hasheaderkey) + - [.redirect([code,] dest)](#redirectcode--dest) + - [.callNotFound()](#callnotfound) + - [.getResponseTime()](#getresponsetime) + - [.type(contentType)](#typecontenttype) + - [.raw](#raw) + - [.serializer(func)](#serializerfunc) + - [.sent](#sent) + - [.hijack](#hijack) + - [.send(data)](#senddata) + - [对象](#objects) + - [字符串](#strings) + - [Streams](#streams) + - [Buffers](#buffers) + - [Errors](#errors) + - [最终 payload 的类型](#type-of-the-final-payload) + - [Async-Await 与 Promise](#async-await-and-promises) + - [.then](#then) + + +### 简介 +处理函数的第二个参数为 `Reply`。 +Reply 是 Fastify 的一个核心对象。它暴露了以下函数及属性: + +- `.code(statusCode)` - 设置状态码。 +- `.status(statusCode)` - `.code(statusCode)` 的别名。 +- `.server` - Fastify 实例的引用。 +- `.statusCode` - 获取或设置 HTTP 状态码。 +- `.header(name, value)` - 设置响应 header。 +- `.getHeader(name)` - 获取某个 header 的值。 +- `.removeHeader(key)` - 清除已设置的 header 的值。 +- `.hasHeader(name)` - 检查某个 header 是否设置。 +- `.type(value)` - 设置 `Content-Type` header。 +- `.redirect([code,] dest)` - 重定向至指定的 url,状态码可选 (默认为 `302`)。 +- `.callNotFound()` - 调用自定义的 not found 处理函数。 +- `.serialize(payload)` - 使用默认的或自定义的 JSON 序列化工具序列化指定的 payload,并返回处理后的结果。 +- `.serializer(function)` - 设置自定义的 payload 序列化工具。 +- `.send(payload)` - 向用户发送 payload。类型可以是纯文本、buffer、JSON、stream,或一个 Error 对象。 +- `.sent` - 一个 boolean,检查 `send` 是否已被调用。 +- `.raw` - Node 原生的 [`http.ServerResponse`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_class_http_serverresponse) 对象。 +- `.res` *(不推荐,请使用 `.raw`)* - Node 原生的 [`http.ServerResponse`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_class_http_serverresponse) 对象。 +- `.log` - 请求的日志实例。 +- `.request` - 请求。 +- `.context` - [请求的 context](Request.md#Request) 属性。 + +```js +fastify.get('/', options, function (request, reply) { + // 你的代码 + reply + .code(200) + .header('Content-Type', 'application/json; charset=utf-8') + .send({ hello: 'world' }) +}) +``` + +另外,`Reply` 能够访问请求的上下文: + +```js +fastify.get('/', {config: {foo: 'bar'}}, function (request, reply) { + reply.send('handler config.foo = ' + reply.context.config.foo) +}) +``` + + +### .code(statusCode) +如果没有设置 `reply.code`,`statusCode` 会是 `200`。 + + +### .statusCode +获取或设置 HTTP 状态码。作为 setter 使用时,是 `reply.code()` 的别名。 +```js +if (reply.statusCode >= 299) { + reply.statusCode = 500 +} +``` + + +### .server +Fastify 服务器的实例,以当前的[封装上下文](Encapsulation.md)为作用域。 + +```js +fastify.decorate('util', function util () { + return 'foo' +}) +fastify.get('/', async function (req, rep) { + return rep.server.util() // foo +}) +``` + + +### .header(key, value) +设置响应 header。如果值被省略或为 undefined,将被强制设成 `''`。 + +更多信息,请看 [`http.ServerResponse#setHeader`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_response_setheader_name_value)。 + + +### .getHeader(key) +获取已设置的 header 的值。 +```js +reply.header('x-foo', 'foo') // 设置 x-foo header 的值为 foo +reply.getHeader('x-foo') // 'foo' +``` + + +### .removeHeader(key) + +清除已设置的 header 的值。 +```js +reply.header('x-foo', 'foo') +reply.removeHeader('x-foo') +reply.getHeader('x-foo') // undefined +``` + + +### .hasHeader(key) +返回一个 boolean,用于检查是否设置了某个 header。 + + +### .redirect([code ,] dest) +重定向请求至指定的 URL,状态码可选,当未通过 `code` 方法设置时,默认为 `302`。 + +示例 (不调用 `reply.code()`):状态码 `302`,重定向至 `/home` +```js +reply.redirect('/home') +``` + +示例 (不调用 `reply.code()`):状态码 `303`,重定向至 `/home` +```js +reply.redirect(303, '/home') +``` + +示例 (调用 `reply.code()`):状态码 `303`,重定向至 `/home` +```js +reply.code(303).redirect('/home') +``` + +示例 (调用 `reply.code()`):状态码 `302`,重定向至 `/home` +```js +reply.code(303).redirect(302, '/home') +``` + + +### .callNotFound() +调用自定义的 not found 处理函数。注意,只有在 [`setNotFoundHandler`](Server.md#set-not-found-handler) 中指明的 `preHandler` 钩子会被调用。 + +```js +reply.callNotFound() +``` + + +### .getResponseTime() +调用自定义响应时间获取函数,来计算自收到请求起的时间。 + +```js +const milliseconds = reply.getResponseTime() +``` + + +### .type(contentType, type) +设置响应的 content type。 +这是 `reply.header('Content-Type', 'the/type')` 的简写。 + +```js +reply.type('text/html') +``` +如果 `Content-Type` 为 JSON 子类型,并且未设置 charset 参数,则使用 `utf-8` 作为 charset 的默认参数。 + + +### .serializer(func) +`.send()` 方法会默认将 `Buffer`、`stream`、`string`、`undefined`、`Error` 之外类型的值 JSON-序列化。假如你需要在特定的请求上使用自定义的序列化工具,你可以通过 `.serializer()` 来实现。要注意的是,如果使用了自定义的序列化工具,你必须同时设置 `'Content-Type'` header。 + +```js +reply + .header('Content-Type', 'application/x-protobuf') + .serializer(protoBuf.serialize) +``` + +注意,你并不需要在一个 `handler` 内部使用这一工具,因为 Buffers、streams 以及字符串 (除非已经设置了序列化工具) 被认为是已序列化过的。 + +```js +reply + .header('Content-Type', 'application/x-protobuf') + .send(protoBuf.serialize(data)) +``` + +请看 [`.send()`](#send) 了解更多关于发送不同类型值的信息。 + + +### .raw +Node 核心的 [`http.ServerResponse`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_class_http_serverresponse) 对象。使用 `Reply.raw` 上的方法会跳过 Fastify 对 HTTP 响应的处理逻辑,所以请谨慎使用。以下是一个例子: + +```js +app.get('/cookie-2', (req, reply) => { + reply.setCookie('session', 'value', { secure: false }) // 这行不会应用 + + // 在这个例子里我们只使用了 nodejs 的 http 响应对象 + reply.raw.writeHead(200, { 'Content-Type': 'text/plain' }) + reply.raw.write('ok') + reply.raw.end() +}) +``` +在《[回复](Reply.md#getheaders)》里有另一个误用 `Reply.raw` 的例子。 + + +### .sent + +如你所见,`.sent` 属性表明是否已通过 `reply.send()` 发送了一个响应。 + +当控制器是一个 async 函数或返回一个 promise 时,可以手动设置 `reply.sent = true`,以防 promise resolve 时自动调用 `reply.send()`。通过设置 `reply.sent = +true`,程序能完全掌控底层的请求,且相关钩子不会被触发。 + +请看范例: + +```js +app.get('/', (req, reply) => { + reply.sent = true + reply.raw.end('hello world') + + return Promise.resolve('this will be skipped') // 译注:该处会被跳过 +}) +``` + +如果处理函数 reject,将会记录一个错误。 + + +### .hijack() +有时你需要终止请求生命周期的执行,并手动发送响应。 + +Fastify 提供了 `reply.hijack()` 方法来完成此任务。在 `reply.send()` 之前的任意节点调用该方法,能阻止 Fastify 自动发送响应,并不再执行之后的生命周期函数 (包括用户编写的处理函数)。 + +特别注意 (*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。 + + +### .send(data) +顾名思义,`.send()` 是向用户发送 payload 的函数。 + + +#### 对象 +如上文所述,如果你发送 JSON 对象时,设置了输出的 schema,那么 `send` 会使用 [fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify) 来序列化对象。否则,将使用 `JSON.stringify()`。 +```js +fastify.get('/json', options, function (request, reply) { + reply.send({ hello: 'world' }) +}) +``` + + +#### 字符串 +在未设置 `Content-Type` 的时候,字符串会以 `text/plain; charset=utf-8` 类型发送。如果设置了 `Content-Type`,且使用自定义序列化工具,那么 `send` 发出的字符串会被序列化。否则,字符串不会有任何改动 (除非 `Content-Type` 的值为 `application/json; charset=utf-8`,这时,字符串会像对象一样被 JSON-序列化,正如上一节所述)。 +```js +fastify.get('/json', options, function (request, reply) { + reply.send('plain string') +}) +``` + + +#### Streams +*send* 开箱即用地支持 stream。如果在未设置 `'Content-Type'` header 的情况下发送 stream,它会被设定为 `'application/octet-stream'`。 +```js +fastify.get('/streams', function (request, reply) { + const fs = require('fs') + const stream = fs.createReadStream('some-file', 'utf8') + reply.send(stream) +}) +``` + + +#### Buffers +未设置 `'Content-Type'` header 的情况下发送 buffer,*send* 会将其设置为 `'application/octet-stream'`。 +```js +const fs = require('fs') +fastify.get('/streams', function (request, reply) { + fs.readFile('some-file', (err, fileBuffer) => { + reply.send(err || fileBuffer) + }) +}) +``` + + +#### Errors +若使用 *send* 发送一个 *Error* 的实例,Fastify 会自动创建一个如下的错误结构: + +```js +{ + error: String // HTTP 错误信息 + code: String // Fastify 的错误代码 + message: String // 用户错误信息 + statusCode: Number // HTTP 状态码 +} +``` + +你可以向 Error 对象添加自定义属性,例如 `headers`,这可以用来增强 HTTP 响应。
+*注意:如果 `send` 一个错误,但状态码小于 400,Fastify 会自动将其设为 500。* + +贴士:你可以通过 [`http-errors`](https://npm.im/http-errors) 或 [`fastify-sensible`](https://github.com/fastify/fastify-sensible) 来简化生成的错误: + +```js +fastify.get('/', function (request, reply) { + reply.send(httpErrors.Gone()) +}) +``` + +你可以通过如下方式自定义 JSON 错误的输出: + +- 为自定义状态码设置响应 JSON schema。 +- 为 `Error` 实例添加额外属性。 + +请注意,如果返回的状态码不在响应 schema 列表里,那么默认行为将被应用。 + +```js +fastify.get('/', { + schema: { + response: { + 501: { + type: 'object', + properties: { + statusCode: { type: 'number' }, + code: { type: 'string' }, + error: { type: 'string' }, + message: { type: 'string' }, + time: { type: 'string' } + } + } + } + } +}, function (request, reply) { + const error = new Error('This endpoint has not been implemented') + error.time = 'it will be implemented in two weeks' + reply.code(501).send(error) +}) +``` + +如果你想自定义错误处理,请看 [`setErrorHandler`](Server.md#seterrorhandler) API。
+*注:当自定义错误处理时,你需要自行记录日志* + +API: + + ```js +fastify.setErrorHandler(function (error, request, reply) { + request.log.warn(error) + var statusCode = error.statusCode >= 400 ? error.statusCode : 500 + reply + .code(statusCode) + .type('text/plain') + .send(statusCode >= 500 ? 'Internal server error' : error.message) +}) +``` + +路由生成的 not found 错误会使用 [`setNotFoundHandler`](Server.md#setnotfoundhandler)。 +API: + +```js +fastify.setNotFoundHandler(function (request, reply) { + reply + .code(404) + .type('text/plain') + .send('a custom not found') +}) +``` + + +#### 最终 payload 的类型 +发送的 payload (序列化之后、经过任意的 [`onSend` 钩子](Hooks.md#the-onsend-hook)) 必须为下列类型之一,否则将会抛出一个错误: + +- `string` +- `Buffer` +- `stream` +- `undefined` +- `null` + + +#### Async-Await 与 Promise +Fastify 原生地处理 promise 并支持 async-await。
+*请注意,在下面的例子中我们没有使用 reply.send。* +```js +const delay = promisify(setTimeout) + +fastify.get('/promises', options, function (request, reply) { + return delay(200).then(() => { return { hello: 'world' }}) +}) + +fastify.get('/async-await', options, async function (request, reply) { + await delay(200) + return { hello: 'world' } +}) +``` + +被 reject 的 promise 默认发送 `500` 状态码。要修改回复,可以 reject 一个 promise,或在 `async 函数` 中进行 `throw` 操作,同时附带一个有 `statusCode` (或 `status`) 与 `message` 属性的对象。 + +```js +fastify.get('/teapot', async function (request, reply) { + const err = new Error() + err.statusCode = 418 + err.message = 'short and stout' + throw err +}) + +fastify.get('/botnet', async function (request, reply) { + throw { statusCode: 418, message: 'short and stout' } + // 这一 json 对象将被发送给客户端 +}) +``` + +想要了解更多?请看 [Routes#async-await](Routes.md#async-await)。 + + +### .then(fulfilled, rejected) + +顾名思义,`Reply` 对象能被等待。换句话说,`await reply` 将会等待,直到回复被发送。 +如上的 `await` 语法调用了 `reply.then()`。 + +`reply.then(fulfilled, rejected)` 接受两个参数: + +- `fulfilled` 会在响应完全发送后被调用。 +- `rejected` 会在底层的 stream 出现错误时被调用。例如,socket 连接被破坏时。 + +更多细节,请看: + +- https://github.com/fastify/fastify/issues/1864,关于该特性的讨论。 +- https://promisesaplus.com/,thenable 的定义。 +- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then,`then` 的使用。 diff --git a/doc/fastify-docs/docs/Request.md b/doc/fastify-docs/docs/Request.md new file mode 100644 index 0000000..48cd74b --- /dev/null +++ b/doc/fastify-docs/docs/Request.md @@ -0,0 +1,60 @@ +

Fastify

+ +## Request +处理函数的第一个参数是 `Request`.
+Request 是 Fastify 的核心对象,包含了以下字段: +- `query` - 解析后的 querystring,其格式由 [`querystringParser`](Server.md#querystringParser) 指定。 +- `body` - 消息主体 +- `params` - URL 参数 +- [`headers`](#headers) - header 的 getter 与 setter +- `raw` - Node 原生的 HTTP 请求 +- `req` *(不推荐,请使用 `.raw`)* - Node 原生的 HTTP 请求 +- `server` - Fastify 服务器的实例,以当前的[封装上下文](Encapsulation.md)为作用域。 +- `id` - 请求 ID +- `log` - 请求的日志实例 +- `ip` - 请求方的 ip 地址 +- `ips` - x-forwarder-for header 中保存的请求源 ip 数组,按访问先后排序 (仅当 [`trustProxy`](Server.md#factory-trust-proxy) 开启时有效) +- `hostname` - 请求方的主机名 (当 [`trustProxy`](Server.md#factory-trust-proxy) 启用时,从 `X-Forwarded-Host` header 中获取)。为了兼容 HTTP/2,当没有相关 header 存在时,将返回 `:authority`。 +- `protocol` - 请求协议 (`https` 或 `http`) +- `method` - 请求方法 +- `url` - 请求路径 +- `routerMethod` - 处理请求的路由函数 +- `routerPath` - 处理请求的路由的匹配模式 +- `is404` - 当请求被 404 处理时为 true,反之为 false +- `connection` - 不推荐,请使用 `socket`。请求的底层连接 +- `socket` - 请求的底层连接 +- `context` - Fastify 内建的对象。你不应该直接使用或修改它,但可以访问它的下列特殊属性: + - `context.config` - 路由的 [`config`](Routes.md#routes-config) 对象。 + +### Headers + +`request.headers` 返回来访请求的 header 对象。你也可以如下设置自定义的 header: + +```js +request.headers = { + 'foo': 'bar', + 'baz': 'qux' +} +``` + +该操作能向请求 header 添加新的值,且该值能通过 `request.headers.bar` 读取。此外,`request.raw.headers` 能让你访问标准的请求 header。 + +```js +fastify.post('/:params', options, function (request, reply) { + console.log(request.body) + console.log(request.query) + console.log(request.params) + console.log(request.headers) + console.log(request.raw) + console.log(request.server) + console.log(request.id) + console.log(request.ip) + console.log(request.ips) + console.log(request.hostname) + console.log(request.protocol) + console.log(request.url) + console.log(request.routerMethod) + console.log(request.routerPath) + request.log.info('some info') +}) +``` diff --git a/doc/fastify-docs/docs/Routes.md b/doc/fastify-docs/docs/Routes.md new file mode 100644 index 0000000..0a53ef4 --- /dev/null +++ b/doc/fastify-docs/docs/Routes.md @@ -0,0 +1,498 @@ +

Fastify

+ +## 路由 + +路由方法设置你程序的路由。 +你可以使用简写定义与完整定义两种方式来设定路由。 + +- [完整定义](#full-declaration) +- [路由选项](#options) +- [简写定义](#shorthand-declaration) +- [URL 参数](#url-building) +- [使用 `async`/`await`](#async-await) +- [Promise 取舍](#promise-resolution) +- [路由前缀](#route-prefixing) +- 日志 + - [自定义日志级别](#custom-log-level) + - [自定义日志序列化器](#custom-log-serializer) +- [配置路由的处理函数](#routes-config) +- [路由约束](#constraints) + + +### 完整定义 + +```js +fastify.route(options) +``` + + +### 路由选项 + +* `method`:支持的 HTTP 请求方法。目前支持 `'DELETE'`、`'GET'`、`'HEAD'`、`'PATCH'`、`'POST'`、`'PUT'` 以及 `'OPTIONS'`。它还可以是一个 HTTP 方法的数组。 +* `url`:路由匹配的 URL 路径 (别名:`path`)。 +* `schema`:用于验证请求与回复的 schema 对象。 +必须符合 [JSON Schema](https://json-schema.org/) 格式。请看[这里](Validation-and-Serialization.md)了解更多信息。 + + * `body`:当为 POST 或 PUT 方法时,校验请求主体。 + * `querystring` 或 `query`:校验 querystring。可以是一个完整的 JSON Schema 对象,它包括了值为 `object` 的 `type` 属性以及包含参数的 `properties` 对象,也可以仅仅是 `properties` 对象中的值 (见下文示例)。 + * `params`:校验 url 参数。 + * `response`:过滤并生成用于响应的 schema,能帮助提升 10-20% 的吞吐量。 +* `exposeHeadRoute`:为任意 `GET` 路由创建一个对应的 `HEAD` 路由。默认值为服务器实例上的 [`exposeHeadRoutes`](Server.md#exposeHeadRoutes) 选项的值。如果你不想禁用该选项,又希望自定义 `HEAD` 处理函数,请在 `GET` 路由前定义该处理函数。 +* `attachValidation`:当 schema 校验出错时,将一个 `validationError` 对象添加到请求中,否则错误将被发送给错误处理函数。 +* `onRequest(request, reply, done)`:每当接收到一个请求时触发的[函数](Hooks.md#onrequest)。可以是一个函数数组。 +* `preParsing(request, reply, done)`:解析请求前调用的[函数](Hooks.md#preparsing)。可以是一个函数数组。 +* `preValidation(request, reply, done)`:在共享的 `preValidation` 钩子之后执行的[函数](Hooks.md#prevalidation),在路由层进行认证等场景中会有用处。可以是一个函数数组。 +* `preHandler(request, reply, done)`:处理请求之前调用的[函数](Hooks.md#prehandler)。可以是一个函数数组。 +* `preSerialization(request, reply, payload, done)`:序列化之前调用的[函数](Hooks.md#preserialization)。可以是一个函数数组。 +* `onSend(request, reply, payload, done)`:响应即将发送前调用的[函数](Hooks.md#route-hooks)。可以是一个函数数组。 +* `onResponse(request, reply, done)`:当响应发送后调用的[函数](Hooks.md#onresponse)。因此,在这个函数内部,不允许再向客户端发送数据。可以是一个函数数组。 +* `handler(request, reply)`:处理请求的函数。函数被调用时,[Fastify server](Server.md) 将会与 `this` 进行绑定。注意,使用箭头函数会破坏这一绑定。 +* `errorHandler(error, request, reply)`:在请求作用域内使用的自定义错误控制函数。覆盖默认的全局错误函数,以及由 [`setErrorHandler`](Server.md#setErrorHandler) 设置的请求错误函数。你可以通过 `instance.errorHandler` 访问默认的错误函数,在没有插件覆盖的情况下,其指向 Fastify 默认的 `errorHandler`。 +* `validatorCompiler({ schema, method, url, httpPart })`:生成校验请求的 schema 的函数。详见[验证与序列化](Validation-and-Serialization.md#schema-validator)。 +* `serializerCompiler({ { schema, method, url, httpStatus } })`:生成序列化响应的 schema 的函数。详见[验证与序列化](Validation-and-Serialization.md#schema-serializer)。 +* `schemaErrorFormatter(errors, dataVar)`:生成一个函数,用于格式化来自 schema 校验函数的错误。详见[验证与序列化](Validation-and-Serialization.md#schema-validator)。在当前路由上会覆盖全局的 schema 错误格式化函数,以及 `setSchemaErrorFormatter` 设置的值。 +* `bodyLimit`:一个以字节为单位的整形数,默认值为 `1048576` (1 MiB),防止默认的 JSON 解析器解析超过此大小的请求主体。你也可以通过 `fastify(options)`,在首次创建 Fastify 实例时全局设置该值。 +* `logLevel`:设置日志级别。详见下文。 +* `logSerializers`:设置当前路由的日志序列化器。 +* `config`:存放自定义配置的对象。 +* `version`:一个符合[语义化版本控制规范 (semver)](https://semver.org/) 的字符串。[示例](Routes.md#version)。 +`prefixTrailingSlash`:一个字符串,决定如何处理带前缀的 `/` 路由。 + * `both` (默认值):同时注册 `/prefix` 与 `/prefix/`。 + * `slash`:只会注册 `/prefix/`。 + * `no-slash`:只会注册 `/prefix`。 + + `request` 的相关内容请看[请求](Request.md)一文。 + + `reply` 请看[回复](Reply.md)一文。 + +**注意:** 在[钩子](Hooks.md)一文中有 `onRequest`、`preParsing`、`preValidation`、`preHandler`、`preSerialization`、`onSend` 以及 `onResponse` 更详尽的说明。此外,要在 `handler` 之前就发送响应,请参阅[在钩子中响应请求](Hooks.md#respond-to-a-request-from-a-hook)。 + +示例: +```js +fastify.route({ + method: 'GET', + url: '/', + schema: { + querystring: { + name: { type: 'string' }, + excitement: { type: 'integer' } + }, + response: { + 200: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + }, + handler: function (request, reply) { + reply.send({ hello: 'world' }) + } +}) +``` + + +### 简写定义 +上文的路由定义带有 *Hapi* 的风格。要是偏好 *Express/Restify* 的写法,Fastify 也是支持的:
+`fastify.get(path, [options], handler)`
+`fastify.head(path, [options], handler)`
+`fastify.post(path, [options], handler)`
+`fastify.put(path, [options], handler)`
+`fastify.delete(path, [options], handler)`
+`fastify.options(path, [options], handler)`
+`fastify.patch(path, [options], handler)` + +示例: +```js +const opts = { + schema: { + response: { + 200: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + } +} +fastify.get('/', opts, (request, reply) => { + reply.send({ hello: 'world' }) +}) +``` + +`fastify.all(path, [options], handler)` 会给所有支持的 HTTP 方法添加相同的处理函数。 + +处理函数还可以写到 `options` 对象里: +```js +const opts = { + schema: { + response: { + 200: { + type: 'object', + properties: { + hello: { type: 'string' } + } + } + } + }, + handler: function (request, reply) { + reply.send({ hello: 'world' }) + } +} +fastify.get('/', opts) +``` + +> 注:假如同时在 `options` 和简写方法的第三个参数里指明了处理函数,将会抛出重复的 `handler` 错误。 + + +### Url 构建 +Fastify 同时支持静态与动态的 URL
+要注册一个**参数命名**的路径,请在参数名前加上*冒号*。*星号*表示**通配符**。 +*注意,静态路由总是在参数路由和通配符之前进行匹配。* + +```js +// 参数路由 +fastify.get('/example/:userId', (request, reply) => {}) +fastify.get('/example/:userId/:secretToken', (request, reply) => {}) + +// 通配符 +fastify.get('/example/*', (request, reply) => {}) +``` + +正则表达式路由亦被支持。但要注意,正则表达式会严重拖累性能! +```js +// 正则表达的参数路由 +fastify.get('/example/:file(^\\d+).png', (request, reply) => {}) +``` + +你还可以在同一组斜杠 ("/") 里定义多个参数。就像这样: +```js +fastify.get('/example/near/:lat-:lng/radius/:r', (request, reply) => {}) +``` +*使用短横线 ("-") 来分隔参数。* + +最后,同时使用多参数和正则表达式也是允许的。 +```js +fastify.get('/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', (request, reply) => {}) +``` +在这个例子里,任何未被正则匹配的符号均可作为参数的分隔符。 + +多参数的路由会影响性能,所以应该尽量使用单参数,对于高频访问的路由来说更是如此。 +如果你对路由的底层感兴趣,可以查看[find-my-way](https://github.com/delvedor/find-my-way)。 + +双冒号表示字面意义上的一个冒号,这样就不必通过参数来实现带冒号的路由了。举例如下: +```js +fastify.post('/name::verb') // 将被解释为 /name:verb +``` + + +### Async Await +你是 `async/await` 的使用者吗?我们为你考虑了一切! +```js +fastify.get('/', options, async function (request, reply) { + var data = await getData() + var processed = await processData(data) + return processed +}) +``` + +如你所见,我们不再使用 `reply.send` 向用户发送数据,只需返回消息主体就可以了! + +当然,需要的话你还是可以使用 `reply.send` 发送数据。 +```js +fastify.get('/', options, async function (request, reply) { + var data = await getData() + var processed = await processData(data) + reply.send(processed) +}) +``` + +假如在路由中,`reply.send()` 脱离了 promise 链,在一个基于回调的 API 中被调用,你可以使用 `await reply`: + +```js +fastify.get('/', options, async function (request, reply) { + setImmediate(() => { + reply.send({ hello: 'world' }) + }) + await reply +}) +``` + +返回回复也是可行的: + +```js +fastify.get('/', options, async function (request, reply) { + setImmediate(() => { + reply.send({ hello: 'world' }) + }) + return reply +}) +``` + +**警告:** +* 如果你同时使用 `return value` 与 `reply.send(value)`,那么只会发送第一次,同时还会触发警告日志,因为你试图发送两次响应。 +* 不能返回 `undefined`。更多细节请看 [promise 取舍](#promise-resolution)。 + + +### Promise 取舍 + +假如你的处理函数是一个 `async` 函数,或返回了一个 promise,请注意一种必须支持回调函数和 promise 控制流的特殊情况:如果 promise 被 resolve 为 `undefined`,请求会被挂起,并触发一个*错误*日志。 + +1. 如果你想使用 `async/await` 或 promise,但通过 `reply.send` 返回值: + - **别** `return` 任何值。 + - **别**忘了 `reply.send`。 +2. 如果你想使用 `async/await` 或 promise: + - **别**使用 `reply.send`。 + - **别**返回 `undefined`。 + +通过这一方法,我们便可以最小代价同时支持 `回调函数风格` 以及 `async-await`。尽管这么做十分自由,我们还是强烈建议仅使用其中的一种,因为应用的错误处理方式应当保持一致。 + +**注意**:每个 async 函数各自返回一个 promise 对象。 + + +### 路由前缀 +有时你需要维护同一 API 的多个不同版本。一般的做法是在所有的路由之前加上版本号,例如 `/v1/user`。 +Fastify 提供了一个快捷且智能的方法来解决上述问题,无需手动更改全部路由。这就是*路由前缀*。让我们来看下吧: + +```js +// server.js +const fastify = require('fastify')() + +fastify.register(require('./routes/v1/users'), { prefix: '/v1' }) +fastify.register(require('./routes/v2/users'), { prefix: '/v2' }) + +fastify.listen(3000) +``` + +```js +// routes/v1/users.js +module.exports = function (fastify, opts, done) { + fastify.get('/user', handler_v1) + done() +} +``` + +```js +// routes/v2/users.js +module.exports = function (fastify, opts, done) { + fastify.get('/user', handler_v2) + done() +} +``` +在编译时 Fastify 自动处理了前缀,因此两个不同路由使用相同的路径名并不会产生问题。*(这也意味着性能一点儿也不受影响!)*。 + +现在,你的客户端就可以访问下列路由了: +- `/v1/user` +- `/v2/user` + +根据需要,你可以多次设置路由前缀,它也支持嵌套的 `register` 以及路由参数。 +请注意,当使用了 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 时,这一选项是无效的。 + +#### 处理带前缀的 / 路由 + +根据前缀是否以 `/` 结束,路径为 `/` 的路由的匹配模式有所不同。举例来说,前缀为 `/something/` 的 `/` 路由只会匹配 `something`,而前缀为 `/something` 则会匹配 `/something` 和 `/something/`。 + +要改变这一行为,请见上文 `prefixTrailingSlash` 选项。 + + +### 自定义日志级别 +在 Fastify 中为路由里设置不同的日志级别是十分容易的。
+你只需在插件或路由的选项里设置 `logLevel` 为相应的[值](https://github.com/pinojs/pino/blob/master/docs/api.md#level-string)即可。 + +要注意的是,如果在插件层面上设置了 `logLevel`,那么 [`setNotFoundHandler`](Server.md#setnotfoundhandler) 和 [`setErrorHandler`](Server.md#seterrorhandler) 也会受到影响。 + +```js +// server.js +const fastify = require('fastify')({ logger: true }) + +fastify.register(require('./routes/user'), { logLevel: 'warn' }) +fastify.register(require('./routes/events'), { logLevel: 'debug' }) + +fastify.listen(3000) +``` + +你也可以直接将其传给路由: +```js +fastify.get('/', { logLevel: 'warn' }, (request, reply) => { + reply.send({ hello: 'world' }) +}) +``` +*自定义的日志级别仅对路由生效,通过 `fastify.log` 访问的全局日志并不会受到影响。* + + +### 自定义日志序列化器 + +在某些上下文里,你也许需要记录一个大型对象,但这在其他路由中是个负担。这时,你可以定义一些[`序列化器 (serializer)`](https://github.com/pinojs/pino/blob/master/docs/api.md#bindingsserializers-object),并将它们设置在正确的上下文之上! + +```js +const fastify = require('fastify')({ logger: true }) +fastify.register(require('./routes/user'), { + logSerializers: { + user: (value) => `My serializer one - ${value.name}` + } +}) +fastify.register(require('./routes/events'), { + logSerializers: { + user: (value) => `My serializer two - ${value.name} ${value.surname}` + } +}) +fastify.listen(3000) +``` + +你可以通过上下文来继承序列化器: + +```js +const fastify = Fastify({ + logger: { + level: 'info', + serializers: { + user (req) { + return { + method: req.method, + url: req.url, + headers: req.headers, + hostname: req.hostname, + remoteAddress: req.ip, + remotePort: req.socket.remotePort + } + } + } + } +}) +fastify.register(context1, { + logSerializers: { + user: value => `My serializer father - ${value}` + } +}) +async function context1 (fastify, opts) { + fastify.get('/', (req, reply) => { + req.log.info({ user: 'call father serializer', key: 'another key' }) + // 打印结果: { user: 'My serializer father - call father serializer', key: 'another key' } + reply.send({}) + }) +} +fastify.listen(3000) +``` + + +### 配置 +注册一个新的处理函数,你可以向其传递一个配置对象,并在其中使用它。 + +```js +// server.js +const fastify = require('fastify')() + +function handler (req, reply) { + reply.send(reply.context.config.output) +} + +fastify.get('/en', { config: { output: 'hello world!' } }, handler) +fastify.get('/it', { config: { output: 'ciao mondo!' } }, handler) + +fastify.listen(3000) +``` + + +### 约束 + +Fastify 允许你基于请求的某些属性,例如 `Host` header 或 [`find-my-way`](https://github.com/delvedor/find-my-way) 指定的其他值,来限制路由仅匹配特定的请求。路由选项里的 `constraints` 属性便是用于这一特性。Fastify 有两个内建的约束属性:`version` 及 `host`。你可以自定义约束策略,来判断某路由是否处理一个请求。 + +#### 版本约束 + +你可以在路由的 `constraints` 选项中提供一个 `version` 键。路由版本化允许你为相同路径的路由设置多个处理函数,并根据请求的 `Accept-Version` header 来做匹配。 `Accept-Version` header 的值请遵循 [semver](https://semver.org/) 规范,路由也应当附带对应的 semver 版本声明以便成功匹配。
+对于版本化的路由,Fastify 需要请求附带上 `Accept-Version` header。此外,相同路径的请求会优先匹配带有版本的控制函数。当前尚不支持 semver 规范中的 advanced ranges 与 pre-releases 语法
+*请注意,这一特性会降低路由的性能。* + +```js +fastify.route({ + method: 'GET', + url: '/', + constraints: { version: '1.2.0' }, + handler: function (request, reply) { + reply.send({ hello: 'world' }) + } +}) + +fastify.inject({ + method: 'GET', + url: '/', + headers: { + 'Accept-Version': '1.x' // 也可以是 '1.2.0' 或 '1.2.x' + } +}, (err, res) => { + // { hello: 'world' } +}) +``` + +> ## ⚠ 安全提示 +> 记得设置 [`Vary`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary) 响应头 +> 为用于区分版本的值 (如 `'Accept-Version'`), +> 来避免缓存污染攻击 (cache poisoning attacks)。你也可以在代理或 CDN 层设置该值。 +> +> ```js +> const append = require('vary').append +> fastify.addHook('onSend', async (req, reply) => { +> if (req.headers['accept-version']) { // 或其他自定义 header +> let value = reply.getHeader('Vary') || '' +> const header = Array.isArray(value) ? value.join(', ') : String(value) +> if ((value = append(header, 'Accept-Version'))) { // 或其他自定义 header +> reply.header('Vary', value) +> } +> } +> }) +> ``` + +如果你声明了多个拥有相同主版本或次版本号的版本,Fastify 总是会根据 `Accept-Version` header 的值选择最兼容的版本。
+假如请求未带有 `Accept-Version` header,那么将返回一个 404 错误。 + +新建 Fastify 实例时,可以通过设置 [`constraints`](Server.md#constraints) 选项,来自定义版本匹配的逻辑。 + +#### Host 约束 + +你可以在路由的 `constraints` 选项中提供一个 `host` 键,使得该路由根据请求的 `Host` header 来做匹配。 `host` 约束的值可以是精确匹配的字符串,也可以是任意匹配的正则表达式 + +```js +fastify.route({ + method: 'GET', + url: '/', + constraints: { host: 'auth.fastify.io' }, + handler: function (request, reply) { + reply.send('hello world from auth.fastify.io') + } +}) + +fastify.inject({ + method: 'GET', + url: '/', + headers: { + 'Host': 'example.com' + } +}, (err, res) => { + // 返回 404,因为 host 不匹配 +}) + +fastify.inject({ + method: 'GET', + url: '/', + headers: { + 'Host': 'auth.fastify.io' + } +}, (err, res) => { + // => 'hello world from auth.fastify.io' +}) +``` + +正则形式的 `host` 约束也可用于匹配任意的子域 (或其他模式): + +```js +fastify.route({ + method: 'GET', + url: '/', + constraints: { host: /.*\.fastify\.io/ }, // 匹配 fastify.io 的任意子域 + handler: function (request, reply) { + reply.send('hello world from ' + request.headers.host) + } +}) +``` \ No newline at end of file diff --git a/doc/fastify-docs/docs/Server.md b/doc/fastify-docs/docs/Server.md new file mode 100644 index 0000000..895fa24 --- /dev/null +++ b/doc/fastify-docs/docs/Server.md @@ -0,0 +1,1217 @@ +

Fastify

+ + +## 工厂函数 + +Fastify 模块导出了一个工厂函数,可以用于创建新的 Fastify server 实例。这个工厂函数的参数是一个配置对象,用于自定义最终生成的实例。本文描述了这一对象中可用的属性。 + +- [http2](./Server.md#http2) +- [https](./Server.md#https) +- [connectionTimeout](./Server.md#connectiontimeout) +- [keepAliveTimeout](./Server.md#keepalivetimeout) +- [ignoreTrailingSlash](./Server.md#ignoretrailingslash) +- [maxParamLength](./Server.md#maxparamlength) +- [onProtoPoisoning](./Server.md#onprotopoisoning) +- [onConstructorPoisoning](./Server.md#onconstructorpoisoning) +- [logger](./Server.md#logger) +- [serverFactory](./Server.md#serverfactory) +- [jsonShorthand](./Server.md#jsonshorthand) +- [caseSensitive](./Server.md#casesensitive) +- [requestIdHeader](./Server.md#requestidheader) +- [requestIdLogLabel](./Server.md#requestidloglabel) +- [genReqId](./Server.md#genreqid) +- [trustProxy](./Server.md#trustProxy) +- [pluginTimeout](./Server.md#plugintimeout) +- [querystringParser](./Server.md#querystringParser) +- [exposeHeadRoutes](./Server.md#exposeheadroutes) +- [constraints](./Server.md#constraints) +- [return503OnClosing](./Server.md#return503onclosing) +- [ajv](./Server.md#ajv) +- [serializerOpts](./Server.md#serializeropts) +- [http2SessionTimeout](./Server.md#http2sessiontimeout) +- [frameworkErrors](./Server.md#frameworkerrors) +- [clientErrorHandler](./Server.md#clienterrorhandler) +- [rewriteUrl](./Server.md#rewriteurl) +- [实例](./Server.md#instance) +- [服务器方法](./Server.md#server-methods) +- [initialConfig](./Server.md#initialConfig) + + +### `http2` + +设置为 `true`,则会使用 Node.js 原生的 [HTTP/2](https://nodejs.org/dist/latest-v14.x/docs/api/http2.html) 模块来绑定 socket。 + ++ 默认值:`false` + + +### `https` + +用于配置服务器的 TLS socket 的对象。其选项与 Node.js 原生的 [`createServer` 方法](https://nodejs.org/dist/latest-v14.x/docs/api/https.html#https_https_createserver_options_requestlistener)一致。 +当值为 `null` 时,socket 连接将不会配置 TLS。 + +当 +http2 + 选项设置时,`https` 选项也会被应用。 + ++ 默认值:`null` + + +### `connectionTimeout` + +定义服务器超时,单位为毫秒。作用请见 [`server.timeout` 属性](https://nodejs.org/api/http.html#http_server_timeout)的文档。当指定了 `serverFactory` 时,该选项被忽略。 + ++ 默认值:`0` (无超时) + + +### `keepAliveTimeout` + +定义服务器 keep-alive 超时,单位为毫秒。作用请见 [`server.keepAliveTimeout` 属性](https://nodejs.org/api/http.html#http_server_timeout)的文档。仅当使用 HTTP/1 时有效。当指定了 `serverFactory` 时,该选项被忽略。 + ++ 默认值:`5000` (5 秒) + + +### `ignoreTrailingSlash` + +Fastify 使用 [find-my-way](https://github.com/delvedor/find-my-way) 处理路由。该选项为 `true` 时,尾斜杠将被省略。 +这一选项应用于 server 实例上注册的*所有*路由。 + ++ 默认值:`false` + +```js +const fastify = require('fastify')({ + ignoreTrailingSlash: true +}) + +// 同时注册 "/foo" 与 "/foo/" +fastify.get('/foo/', function (req, reply) { + reply.send('foo') +}) + +// 同时注册 "/bar" 与 "/bar/" +fastify.get('/bar', function (req, reply) { + reply.send('bar') +}) +``` + + +### `maxParamLength` +你可以为通过 `maxParamLength` 选项为带参路由 (无论是标准的、正则匹配的,还是复数的) 设置最大参数长度。选项的默认值为 100 字符。
+当使用正则匹配的路由时,这非常有用,可以帮你抵御 [DoS 攻击](https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS)。
+*当达到长度限制时,将触发 not found 路由。* + + +### `bodyLimit` + +定义服务器可接受的最大 payload,以字节为单位。 + ++ 默认值:`1048576` (1MiB) + + +### `onProtoPoisoning` + +由 [secure-json-parse](https://github.com/fastify/secure-json-parse) 提供的功能,指定解析带有 `__proto__` 键的 JSON 对象时框架的行为。 +更多关于原型污染 (prototype poisoning) 的内容请看 https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061。 + +允许的值为 `'error'`、`'remove'` 与 `'ignore'`。 + ++ 默认值:`'error'` + + +### `onConstructorPoisoning` + +由 [secure-json-parse](https://github.com/fastify/secure-json-parse) 提供的功能,指定解析带有 `constructor` 的 JSON 对象时框架的行为。 +更多关于原型污染的内容请看 https://hueniverse.com/a-tale-of-prototype-poisoning-2610fa170061。 + +允许的值为 `'error'`、`'remove'` 与 `'ignore'`。 + ++ 默认值:`'error'` + + +### `logger` + +Fastify 依托 [Pino](https://getpino.io/) 内建了一个日志工具。该属性用于配置日志实例。 + +属性可用的值为: + ++ 默认: `false`。禁用日志。所有记录日志的方法将会指向一个空日志工具 [abstract-logging](https://npm.im/abstract-logging) 的实例。 + ++ `pinoInstance`: 一个已被实例化的 Pino 实例。内建的日志工具将指向这个实例。 + ++ `object`: 标准的 Pino [选项对象](https://github.com/pinojs/pino/blob/c77d8ec5ce/docs/API.md#constructor)。 +它会被直接传递进 Pino 的构造函数。如果下列属性未在该对象中定义,它们将被相应地添加: + * `level`: 最低的日志级别。若未被设置,则默认为 `'info'`。 + * `serializers`: 序列化函数的哈希。默认情况下,序列化函数应用在 `req` (来访的请求对象)、`res` (发送的响应对象) 以及 `err` (标准的 `Error` 对象) 之上。当一个日志方法接收到含有上述任意属性的对象时,对应的序列化器将会作用于该属性。举例如下: + ```js + fastify.get('/foo', function (req, res) { + req.log.info({req}) // 日志输出经过序列化的请求对象 + res.send('foo') + }) + ``` + 用户提供的序列化函数将会覆盖对应属性默认的序列化函数。 ++ `loggerInstance`:自定义日志工具实例。日志工具必须实现 Pino 的接口,即拥有如下方法:`info`, `error`, `debug`, `fatal`, `warn`, `trace`, `child`。例如: +```js +const pino = require('pino')(); + +const customLogger = { + info: function (o, ...n) {}, + warn: function (o, ...n) {}, + error: function (o, ...n) {}, + fatal: function (o, ...n) {}, + trace: function (o, ...n) {}, + debug: function (o, ...n) {}, + child: function() { + const child = Object.create(this); + child.pino = pino.child(...arguments); + return child; + }, +}; + +const fastify = require('fastify')({logger: customLogger}); +``` + + +### `disableRequestLogging` +默认情况下当开启日志时,Fastify 会在收到请求与发送该请求的响应时记录 `info` 级别的日志。你可以设置该选项为 `true` 来禁用该功能。这时,通过自定义 `onRequest` 和 `onResponse` 钩子,你能更灵活地记录一个请求的开始与结束。 + ++ 默认值:`false` + +```js +// 例子:通过钩子再造被禁用的请求日志功能。 +fastify.addHook('onRequest', (req, reply, done) => { + req.log.info({ url: req.raw.url, id: req.id }, 'received request') + done() +}) + +fastify.addHook('onResponse', (req, reply, done) => { + req.log.info({ url: req.raw.originalUrl, statusCode: reply.raw.statusCode }, 'request completed') + done() +}) +``` + +请注意,该选项同时也会禁止默认的 `onResponse` 钩子在响应的回调函数出错时记录错误日志。 + + +### `serverFactory` +通过 `serverFactory` 选项,你可以向 Fastify 传递一个自定义的 HTTP server。
+`serverFactory` 函数的参数为 `handler` 函数及一个选项对象。`handler` 函数的参数为 `request` 和 `response` 对象,选项对象则与你传递给 Fastify 的一致。 + +```js +const serverFactory = (handler, opts) => { + const server = http.createServer((req, res) => { + handler(req, res) + }) + + return server +} + +const fastify = Fastify({ serverFactory }) + +fastify.get('/', (req, reply) => { + reply.send({ hello: 'world' }) +}) + +fastify.listen(3000) +``` +Fastify 内在地使用 Node 原生 HTTP server 的 API。因此,如果你使用一个自定义的 server,你必须保证暴露了相同的 API。不这么做的话,你可以在 `serverFactory` 函数内部 `return` 语句之前,向 server 实例添加新的属性。
+ + +### `jsonShorthand` + ++ 默认值:`true` + +当未发现 JSON Schema 规范中合法的根属性时,Fastify 会默认地自动推断该根属性。如果你想实现自定义的 schema 校验编译器,例如使用 JTD (JSON Type Definition) 代替 JSON schema,你应当设置该选项为 `false` 来确保 schema 不被修改且不会被当成 JSON Schema 处理。 + +```js +const AjvJTD = require('ajv/dist/jtd'/* 只在 AJV v7 以上版本生效 */) +const ajv = new AjvJTD({ + // 当遇到非法 JTD schema 对象时抛出错误。 + allErrors: process.env.NODE_ENV === 'development' +}) +const fastify = Fastify({ jsonShorthand: false }) +fastify.setValidatorCompiler(({ schema }) => { + return ajv.compile(schema) +}) +fastify.post('/', { + schema: { + body: { + properties: { + foo: { type: 'uint8' } + } + } + }, + handler (req, reply) { reply.send({ ok: 1 }) } +}) +``` + +**注:目前 Fastify 并不会在发现非法 schema 时抛错。因此,假如你在已有项目中关闭了该选项,请确保现存所有的 schema 不会因此变得非法,因为它们会被当作笼统的一类进行处理。** + + +### `caseSensitive` + +默认值为 `true`,此时路由对大小写敏感。这就意味着 `/foo` 与 `/Foo` 是两个不同的路由。当该选项为 `false` 时,路由大小写不敏感,`/foo`、`/Foo` 以及 `/FOO` 都是一样的。 + +将 `caseSensitive` 设置为 `false`,会导致所有路径变为小写,除了路由参数与通配符。 + +```js +fastify.get('/user/:username', (request, reply) => { + // 原 URL: /USER/NodeJS + console.log(request.params.username) // -> 'NodeJS' +}) +``` + +要注意的是,将该选项设为 `false` 与 [RFC3986](https://tools.ietf.org/html/rfc3986#section-6.2.2.1) 相悖。 + +此外,该选项不影响 query string 的解析。要让 query string 忽略大小写,请看 [`querystringParser`](./Server.md#querystringParser)。 + + +### `requestIdHeader` + +用来获知请求 ID 的 header 名。请看[请求 ID](Logging.md#logging-request-id) 一节。 + ++ 默认值:`'request-id'` + + +### `requestIdLogLabel` + +定义日志中请求 ID 的标签。 + ++ 默认值:`'reqId'` + + +### `genReqId` +用于生成请求 ID 的函数。参数为来访的请求对象。 + ++ 默认值:`'request-id' header 的值 (当存在时) 或单调递增的整数` +在分布式系统中,你可能会特别想覆盖如下默认的 ID 生成行为。要生成 `UUID`,请看[hyperid](https://github.com/mcollina/hyperid)。 + ```js +let i = 0 +const fastify = require('fastify')({ + genReqId: function (req) { return i++ } +}) +``` + +**注意:当设置了 [requestIdHeader](#requestidheader) 中定义的 header (默认为 'request-id') 时,genReqId _不会_ 被调用。** + + +### `trustProxy` + +通过开启 `trustProxy` 选项,Fastify 会认为使用了代理服务,且 `X-Forwarded-*` header 是可信的,否则该值被认为是极具欺骗性的。 + +```js +const fastify = Fastify({ trustProxy: true }) +``` + ++ 默认值:`false` ++ `true/false`: 信任所有代理 (`true`) 或不信任任意的代理 (`false`)。 ++ `string`: 只信任给定的 IP/CIDR (例如 `'127.0.0.1'`)。可以是一组用英文逗号分隔的地址 (例如 `'127.0.0.1,192.168.1.1/24'`)。 ++ `Array`: 只信任给定的 IP/CIDR 列表 (例如 `['127.0.0.1']`)。 ++ `number`: 信任来自前置代理服务器的第n跳 (hop) 地址作为客户端。 ++ `Function`: 自定义的信任函数,第一个参数为 `address` + ```js + function myTrustFn(address, hop) { + return address === '1.2.3.4' || hop === 1 + } + ``` + +更多示例详见 [`proxy-addr`](https://www.npmjs.com/package/proxy-addr)。 + +你还可以通过 [`request`](Request.md) 对象获取 `ip`、`ips`、`hostname` 与 `protocol` 的值。 + +```js +fastify.get('/', (request, reply) => { + console.log(request.ip) + console.log(request.ips) + console.log(request.hostname) + console.log(request.protocol) +}) +``` + +**注:如果请求存在多个 x-forwarded-hostx-forwarded-proto header,只会根据最后一个产生 request.hostnamerequest.protocol** + + +### `pluginTimeout` + +单个插件允许加载的最长时间,以毫秒计。如果某个插件加载超时,则 [`ready`](Server.md#ready) 会抛出一个含有 `'ERR_AVVIO_PLUGIN_TIMEOUT'` 代码的 `Error` 对象。 + ++ 默认值:`10000` + + + ### `querystringParser` + +Fastify 默认使用 Node.js 核心的 `querystring` 模块作为 query string 解析器。
+你可以通过 `querystringParser` 选项来使用自定义的解析器,例如 [`qs`](https://www.npmjs.com/package/qs)。 + +```js +const qs = require('qs') +const fastify = require('fastify')({ + querystringParser: str => qs.parse(str) +}) +``` + +你也可以改变默认解析器的行为,例如忽略 query string 的大小写: + +```js +const querystring = require('querystring') +const fastify = require('fastify')({ + querystringParser: str => querystring.parse(str.toLowerCase()) +}) +``` + +若你只想忽略键的大小写,我们推荐你使用自定义解析器。 + + +### `exposeHeadRoutes` + +自动为每个 `GET` 路由添加对应的 `HEAD` 路由。如果你不想禁用该选项,又希望自定义 `HEAD` 处理函数,请在 `GET` 路由前定义该处理函数。 + ++ 默认值:`false` + + +### `constraints` + +Fastify 内建的路由约束由 `find-my-way` 支持,允许使用 `version` 或 `host` 来约束路由。通过为 `find-my-way` 提供 `constraints` 对象,你可以添加新的约束策略,或覆盖原有策略。更多内容可见于 [find-my-way](https://github.com/delvedor/find-my-way) 的文档。 + +```js +const customVersionStrategy = { + storage: function () { + let versions = {} + return { + get: (version) => { return versions[version] || null }, + set: (version, store) => { versions[version] = store }, + del: (version) => { delete versions[version] }, + empty: () => { versions = {} } + } + }, + deriveVersion: (req, ctx) => { + return req.headers['accept'] + } +} + +const fastify = require('fastify')({ + constraints: { + version: customVersionStrategy + } +}) +``` + + +### `return503OnClosing` + +调用 `close` 方法后返回 503 状态码。 +如果为 `false`,服务器会正常处理请求。 + ++ 默认值:`true` + + +### `ajv` + +配置 Fastify 使用的 Ajv 6 实例。这使得你无需提供一个自定义的实例。 + ++ 默认值: + +```js +{ + customOptions: { + removeAdditional: true, + useDefaults: true, + coerceTypes: true, + allErrors: false, + nullable: true + }, + plugins: [] +} +``` + +```js +const fastify = require('fastify')({ + ajv: { + customOptions: { + nullable: false // 参见 [ajv 的配置选项](https://ajv.js.org/#options) + }, + plugins: [ + require('ajv-merge-patch'), + [require('ajv-keywords'), 'instanceof'] + // 用法: [plugin, pluginOptions] - 插件与选项 + // 用法: plugin - 仅插件 + ] + } +}) +``` + + +### `serializerOpts` + +自定义用于序列化响应 payload 的 [`fast-json-stringify`](https://github.com/fastify/fast-json-stringify#options) 实例的配置: + +```js +const fastify = require('fastify')({ + serializerOpts: { + rounding: 'ceil' + } +}) +``` + + +### `http2SessionTimeout` + +为每个 HTTP/2 会话设置默认[超时时间](https://nodejs.org/api/http2.html#http2_http2session_settimeout_msecs_callback)。超时后,会话将关闭。默认值:`5000` 毫秒。 + +要注意的是,使用 HTTP/2 时需要提供一个优雅的“close”体验。一个低的默认值有助于减轻拒绝服务型攻击 (Denial-of-Service Attacks) 的影响。但当你的服务器使用负载均衡策略,或能自动扩容时,则可以延长超时时间。Node 的默认值为 `0`,即无超时。 + + +### `frameworkErrors` + ++ 默认值:`null` + +对于最常见的场景,Fastify 已经提供了默认的错误处理方法。这个选项允许你重写这些处理方法。 + +*注:目前只实现了 `FST_ERR_BAD_URL` 这个错误。* + +```js +const fastify = require('fastify')({ + frameworkErrors: function (error, req, res) { + if (error instanceof FST_ERR_BAD_URL) { + res.code(400) + return res.send("Provided url is not valid") + } else { + res.send(err) + } + } +}) +``` + + +### `clientErrorHandler` + +设置 [clientErrorHandler](https://nodejs.org/api/http.html#http_event_clienterror) 来监听客户端连接造成的 `error` 事件,并响应 `400` 状态码。 + +设置了该选项,会覆盖默认的 `clientErrorHandler`。 + ++ 默认值: +```js +function defaultClientErrorHandler (err, socket) { + if (err.code === 'ECONNRESET') { + return + } + + const body = JSON.stringify({ + error: http.STATUS_CODES['400'], + message: 'Client Error', + statusCode: 400 + }) + this.log.trace({ err }, 'client error') + + if (socket.writable) { + socket.end(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`) + } +} +``` + +*注:`clientErrorHandler` 使用底层的 socket,故处理函数需要返回格式正确的 HTTP 响应信息,包括状态行、HTTP header 以及 body。在写入之前,为了避免 socket 已被销毁,你还应该检查 socket 是否依然可写。* + +```js +const fastify = require('fastify')({ + clientErrorHandler: function (err, socket) { + const body = JSON.stringify({ + error: { + message: 'Client error', + code: '400' + } + }) + // `this` 为 fastify 实例 + this.log.trace({ err }, 'client error') + // 处理函数应当发送正确的 HTTP 响应信息。 + socket.end(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`) + } +}) +``` + + +### `rewriteUrl` + +设置一个异步函数,返回一个字符串,用于重写 URL。 + +> 重写 URL 会修改 `req` 对象的 `url` 属性 + +```js +function rewriteUrl (req) { // req 是 Node.js 的 HTTP 请求对象 + return req.url === '/hi' ? '/hello' : req.url; +} +``` + +要注意的是,`rewriteUrl` 在处理路由 _前_ 被调用,它不是封装的,而是整个应用级别的。 + +## 实例 + +### 服务器方法 + + +#### 服务器 +`fastify.server`:由 [**`Fastify 的工厂函数`**](Server.md) 生成的 Node 原生 [server](https://nodejs.org/api/http.html#http_class_http_server) 对象。 + + +#### after +当前插件及在其中注册的所有插件加载完毕后调用。总在 `fastify.ready` 之前执行。 + +```js +fastify + .register((instance, opts, done) => { + console.log('当前插件') + done() + }) + .after(err => { + console.log('当前插件之后') + }) + .register((instance, opts, done) => { + console.log('下一个插件') + done() + }) + .ready(err => { + console.log('万事俱备') + }) +``` + +当 `after()` 没有回调参数时,它返回一个 `Promise`: + +```js +fastify.register(async (instance, opts) => { + console.log('Current plugin') +}) + +await fastify.after() +console.log('After current plugin') + +fastify.register(async (instance, opts) => { + console.log('Next plugin') +}) + +await fastify.ready() + +console.log('Everything has been loaded') +``` + + +#### ready +当所有插件的加载都完成时调用。如有错误发生,它会传递一个 `error` 参数。 +```js +fastify.ready(err => { + if (err) throw err +}) +``` +调用时不加参数,它会返回一个 `Promise` 对象: + +```js +fastify.ready().then(() => { + console.log('successfully booted!') +}, (err) => { + console.log('an error happened', err) +}) +``` + + +#### listen +所有的插件加载完毕、`ready` 事件触发后,在指定的端口启动服务器。它的回调函数与 Node 原生方法的回调相同。默认情况下,服务器监听 `localhost` 所决定的地址 (`127.0.0.1` 或 `::1`,取决于操作系统)。将地址设置为 `0.0.0.0` 可监听所有的 IPV4 地址。设置为 `::` 则可监听所有的 IPV6 地址,在某些系统中,这么做亦可同时监听所有 IPV4 地址。监听所有的接口要格外谨慎,因为这种方式存在着固有的[安全风险](https://web.archive.org/web/20170831174611/https://snyk.io/blog/mongodb-hack-and-secure-defaults/)。 + +```js +fastify.listen(3000, (err, address) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` + +指定监听的地址: + +```js +fastify.listen(3000, '127.0.0.1', (err, address) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` + +指定积压队列 (backlog queue size) 的大小: + +```js +fastify.listen(3000, '127.0.0.1', 511, (err, address) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` + +没有提供回调函数时,它会返回一个 Promise 对象: + +```js +fastify.listen(3000) + .then((address) => console.log(`server listening on ${address}`)) + .catch(err => { + console.log('Error starting server:', err) + process.exit(1) + }) +``` + +你还可以在使用 Promise 的同时指定地址: + +```js +fastify.listen(3000, '127.0.0.1') + .then((address) => console.log(`server listening on ${address}`)) + .catch(err => { + console.log('Error starting server:', err) + process.exit(1) + }) +``` + +当部署在 Docker 或其它容器上时,明智的做法是监听 `0.0.0.0`。因为默认情况下,这些容器并未将映射的端口暴露在 `127.0.0.1`: + +```js +fastify.listen(3000, '0.0.0.0', (err, address) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` + +假如未设置 `port` (或设为 0),则会自动选择一个随机可用的端口 (之后可通过 `fastify.server.address().port` 获知)。 + + +#### getDefaultRoute +获取服务器 `defaultRoute` 属性的方法: + +```js +const defaultRoute = fastify.getDefaultRoute() +``` + + +#### setDefaultRoute +设置服务器 `defaultRoute` 属性的方法: + +```js +const defaultRoute = function (req, res) { + res.end('hello world') +} + +fastify.setDefaultRoute(defaultRoute) +``` + + +#### routing +访问内部路由库的 `lookup` 方法,该方法将请求匹配到合适的处理函数: + +```js +fastify.routing(req, res) +``` + + +#### route +将路由添加到服务器的方法,支持简写。请看[这里](Routes.md)。 + + +#### close +`fastify.close(callback)`:调用这个函数来关闭服务器实例,并触发 [`'onClose'`](Hooks.md#on-close) 钩子。
+服务器会向所有新的请求发送 `503` 错误,并销毁它们。 +要改变这一行为,请见 [`return503OnClosing`](Server.md#factory-return-503-on-closing)。 + +如果无参调用,它会返回一个 Promise: + + ```js +fastify.close().then(() => { + console.log('successfully closed!') +}, (err) => { + console.log('an error happened', err) +}) +``` + + +#### decorate* +向 Fastify 实例、响应或请求添加装饰器函数。参阅[这里](Decorators.md)了解更多。 + + +#### register +Fastify 允许用户通过插件扩展功能。插件可以是一组路由、装饰器或其他。请看[这里](Plugins.md)。 + + +#### addHook +向 Fastify 添加特定的生命周期钩子函数,请看[这里](Hooks.md)。 + + +#### prefix +添加在路由前的完整路径。 + +示例: + +```js +fastify.register(function (instance, opts, done) { + instance.get('/foo', function (request, reply) { + // 输出:"prefix: /v1" + request.log.info('prefix: %s', instance.prefix) + reply.send({prefix: instance.prefix}) + }) + + instance.register(function (instance, opts, done) { + instance.get('/bar', function (request, reply) { + // 输出:"prefix: /v1/v2" + request.log.info('prefix: %s', instance.prefix) + reply.send({prefix: instance.prefix}) + }) + + done() + }, { prefix: '/v2' }) + + done() +}, { prefix: '/v1' }) +``` + + +#### pluginName +当前插件的名称。有三种定义插件名称的方式(按顺序)。 + +1. 如果插件使用 [fastify-plugin](https://github.com/fastify/fastify-plugin),那么名称为元数据 (metadata) 中的 `name`。 +2. 如果插件通过 `module.exports` 导出,使用文件名。 +3. 如果插件通过常规的 [函数定义](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions#Defining_functions),则使用函数名。 + +*回退方案*:插件函数的头两行将作为插件名,并使用 `--` 替代换行符。这有助于在处理涉及许多插件的问题时,找到根源。 + +重点:如果你要处理一些通过 [fastify-plugin](https://github.com/fastify/fastify-plugin) 包装的嵌套的异名插件,由于没有生成新的定义域,因此不会去覆盖上下文数据,而是将各插件名加入一个数组。在这种情况下,会按涉及到的插件的启动顺序,以 `plugin-A -> plugin-B` 的格式来展示插件名称。 + + +#### log +日志的实例,详见[这里](Logging.md)。 + + +#### version +Fastify 实例的版本。可在插件中使用。详见[插件](Plugins.md#handle-the-scope)一文。 + + +#### inject +伪造 HTTP 注入 (作为测试之用) 。请看[更多内容](Testing.md#inject)。 + + +#### addSchema +`fastify.addSchema(schemaObj)`,向 Fastify 实例添加 JSON schema。你可以通过 `$ref` 关键字在应用的任意位置使用它。
+更多内容,请看[验证和序列化](Validation-and-Serialization.md)。 + + +#### getSchemas +`fastify.getSchemas()`,返回一个对象,包含所有通过 `addSchema` 添加的 schema,对象的键是 JSON schema 的 `$id`。 + + +#### getSchema +`fastify.getSchema(id)`,返回通过 `addSchema` 添加的拥有匹配 `id` 的 schema,未找到则返回 `undefined`。 + + +#### setReplySerializer +作用于未设置 [Reply.serializer(func)](Reply.md#serializerfunc) 的所有路由的默认序列化方法。这个处理函数是完全封装的,因此,不同的插件允许有不同的错误处理函数。 +注:仅当状态码为 `2xx` 时才被调用。关于错误处理,请看 [`setErrorHandler`](Server.md#seterrorhandler)。 + + ```js +fastify.setReplySerializer(function (payload, statusCode){ + // 使用同步函数序列化 payload + return `my serialized ${statusCode} content: ${payload}` +}) +``` + + +#### setValidatorCompiler +为所有的路由设置 schema 校验编译器 (validator compiler)。详见 [#schema-validator](Validation-and-Serialization.md#schema-validator)。 + + +#### setSchemaErrorFormatter +为所有的路由设置 schema 错误格式化器 (schema error formatter)。详见 [#error-handling](Validation-and-Serialization.md#schemaerrorformatter)。 + + +#### setSerializerCompiler +为所有的路由设置 schema 序列化编译器 (serializer compiler)。详见 [#schema-serializer](Validation-and-Serialization.md#schema-serializer)。 +**注:** [`setReplySerializer`](#set-reply-serializer) 有更高的优先级! + + +#### validatorCompiler +该属性用于获取 schema 校验器。未设置校验器时,在服务器启动前,该值是 `null`,之后是一个签名为 `function ({ schema, method, url, httpPart })` 的函数。该函数将 `schema` 参数编译为一个校验数据的函数,并返回生成的函数。 +`schema` 参数能访问到所有通过 [`.addSchema`](#add-schema) 添加的共用 schema。 + + +#### serializerCompiler +该属性用于获取 schema 序列化器。未设置序列化器时,在服务器启动前,该值是 `null`,之后是一个签名为 `function ({ schema, method, url, httpPart })` 的函数。该函数将 `schema` 参数编译为一个校验数据的函数,并返回生成的函数。 +`schema` 参数能访问到所有通过 [`.addSchema`](#add-schema) 添加的共用 schema。 + + +#### schemaErrorFormatter +该属性设置一个函数用于格式化 `validationCompiler` 在校验 schema 时发生的错误。详见 [#error-handling](Validation-and-Serialization.md#schemaerrorformatter)。 + + +#### schemaController +该属性用于管理: +- `bucket`:应用的 schema 的存放位置 +- `compilersFactory`:必须编译 JSON schema 的模块 + +可用于 Fastify 无法分辨保存于某些数据结构中的 schema 之时。在 [issue #2446](https://github.com/fastify/fastify/issues/2446) 里有一个通过该属性解决问题的例子。 + +另一个用例是微调所有 schema 的处理过程。这么做可以用 Ajv 8 替代默认的 Ajv 6!例子见下文。 + +```js +const fastify = Fastify({ + schemaController: { + /** + * 以下的 factory 函数会在每次调用 `fastify.register()` 时被执行。 + * 若父级上下文添加了 schema,它会作为参数传入 factory 函数。 + * @param {object} parentSchemas 会由 `bucket` 对象的 `getSchemas()` 方法返回。 + + */ + bucket: function factory (parentSchemas) { + return { + addSchema (inputSchema) { + // 该函数保存用户添加的 schema。 + // 调用 `fastify.addSchema()` 时被执行。 + }, + getSchema (schema$id) { + // 该函数返回通过 `schema$id` 检索得到的原始 schema。 + // 调用 `fastify.getSchema()` 时被执行。 + return aSchema + }, + getSchemas () { + // 返回路由 schema 中通过 $ref 引用的所有 schema。 + // 返回对象以 schema 的 `$id` 为键,以原始内容为值。 + const allTheSchemaStored = { + 'schema$id1': schema1, + 'schema$id2': schema2 + } + return allTheSchemaStored + } + } + }, + + /** + * 编译器的 factory 函数让你能充分地控制 Fastify 生命周期里的验证器与序列化器,并给你的编译器提供封装。 + */ + compilersFactory: { + /** + * 以下的 factory 函数会在每次需要一个新的验证器实例时被执行。 + * 当新的 schema 被添加到封装上下文时,调用 `fastify.register()` 也会执行该函数。 + * 若父级上下文添加了 schema,它会作为参数传入 factory 函数。 + * @param {object} externalSchemas 这些 schema 将被 `bucket.getSchemas()` 返回。需要处理外部引用 $ref。 + * @param {object} ajvServerOption 服务器的 `ajv` 选项。 + */ + buildValidator: function factory (externalSchemas, ajvServerOption) { + // 该 factory 函数必须返回一个 schema 校验编译器。 + // 详见 [#schema-validator](Validation-and-Serialization.md#schema-validator)。 + const yourAjvInstance = new Ajv(ajvServerOption.customOptions) + return function validatorCompiler ({ schema, method, url, httpPart }) { + return yourAjvInstance.compile(schema) + } + }, + + /** + * 以下的 factory 函数会在每次需要一个新的序列化器实例时被执行。 + * 当新的 schema 被添加到封装上下文时,调用 `fastify.register()` 也会执行该函数。 + * 若父级上下文添加了 schema,它会作为参数传入 factory 函数。 + * @param {object} externalSchemas 这些 schema 将被 `bucket.getSchemas()` 返回。需要处理外部引用 $ref。 + * @param {object} serializerOptsServerOption 服务器的 `serializerOpts` 选项。 + */ + buildSerializer: function factory (externalSchemas, serializerOptsServerOption) { + // 该 factory 函数必须返回一个 schema 序列化编译器。 + // 详见 [#schema-serializer](Validation-and-Serialization.md#schema-serializer)。 + return function serializerCompiler ({ schema, method, url, httpStatus }) { + return data => JSON.stringify(data) + } + } + } + } +}); +``` + +##### 将 Ajv 8 作为默认的 schema 验证器 + +Ajv 8 是 Ajv 6 的后继版本,拥有许多改进与新特性。Ajv 8 新特性 (如 JTD 与 Standalone 模式) 的用法详见 [`@fastify/ajv-compiler` 的文档](https://github.com/fastify/ajv-compiler#usage)。 + +下列代码可以将 Ajv 8 作为默认的 schema 验证器使用: + +```js +const AjvCompiler = require('@fastify/ajv-compiler') // 必须是 v2.x.x 版本 + +// 请注意,默认情况下 Ajv 8 不支持 schema 的关键词 `format`, +// 因此需要手动添加 +const ajvFormats = require('ajv-formats') + +const app = fastify({ + ajv: { + customOptions: { + validateFormats: true + }, + plugins: [ajvFormats] + }, + schemaController: { + compilersFactory: { + buildValidator: AjvCompiler() + } + } +}) + +// 大功告成!现在你可以在 schema 中使用 Ajv 8 的选项与关键词了! +``` + + +#### setNotFoundHandler + +`fastify.setNotFoundHandler(handler(request, reply))`:为 404 状态 (not found) 设置处理函数 (handler)。向 `fastify.register()` 传递不同的 [`prefix` 选项](Plugins.md#route-prefixing-option),就可以为不同的插件设置不同的处理函数。这些处理函数被视为常规的路由处理函数,因此它们的请求会经历一个完整的 [Fastify 生命周期](Lifecycle.md#lifecycle)。 + +你也可以为 404 处理函数注册 [`preValidation`](Hooks.md/#route-hooks) 或 [`preHandler`](Hooks.md/#route-hooks) 钩子。 + +_注:通过此方法注册的 `preValidation` 钩子会在遇到未知路由时触发,但手动调用 [`reply.callNotFound`](Reply.md#call-not-found) 方法时则**不会**_。此时只有 preHandler 会执行。 + +```js +fastify.setNotFoundHandler({ + preValidation: (req, reply, done) => { + // 你的代码 + done() + } , + preHandler: (req, reply, done) => { + // 你的代码 + done() + } +}, function (request, reply) { + // 设置了 preValidation 与 preHandler 钩子的默认 not found 处理函数 +}) + +fastify.register(function (instance, options, done) { + instance.setNotFoundHandler(function (request, reply) { + // '/v1' 开头的 URL 的 not found 处理函数, + // 未设置 preValidation 与 preHandler 钩子 + }) + done() +}, { prefix: '/v1' }) +``` + +Fastify 启动时,会在插件注册之前就调用 `setNotFoundHandler` 方法添加默认的 404 处理函数。假如你想拓展默认 404 处理函数的行为,例如与插件一同使用,你可以在插件上下文内,不传参数地调用 `fastify.setNotFoundHandler()`。 + + +#### setErrorHandler + +`fastify.setErrorHandler(handler(error, request, reply))`:设置任意时刻的错误处理函数。错误处理函数绑定在 Fastify 实例之上,是完全封装 (fully encapsulated) 的,因此不同插件的处理函数可以不同。支持 *async-await* 语法。
+*注:假如错误的 `statusCode` 小于 400,在处理错误前 Fastify 将会自动将其设为 500。* + +```js +fastify.setErrorHandler(function (error, request, reply) { + // 记录错误 + this.log.error(error) + // 发送错误响应 + reply.status(409).send({ ok: false }) +}) +``` + +当没有设置错误处理函数时,Fastify 会调用一个默认函数。你能通过 `fastify.errorHandler` 访问该函数。它根据 `statusCode` 相应地记录日志。 + +```js +var statusCode = error.statusCode +if (statusCode >= 500) { + log.error(error) +} else if (statusCode >= 400) { + log.info(error) +} else { + log.error(error) +} +``` + + +#### printRoutes + +`fastify.printRoutes()`:打印路由的基数树 (radix tree),可作调试之用。可以用 `fastify.printRoutes({ commonPrefix: false })` 来打印扁平化后的路由
+*记得在 `ready` 函数的内部或之后调用它。* + +```js +fastify.get('/test', () => {}) +fastify.get('/test/hello', () => {}) +fastify.get('/hello/world', () => {}) + +fastify.ready(() => { + console.log(fastify.printRoutes()) + // └── / + // ├── test (GET) + // │ └── /hello (GET) + // └── hel + // ├── lo/world (GET) + // └── licopter (GET) + + console.log(fastify.printRoutes({ commonPrefix: false })) + // └── / (-) + // ├── test (GET) + // │ └── /hello (GET) + // ├── hello/world (GET) + // └── helicopter (GET) +}) +``` + +`fastify.printRoutes({ includeMeta: (true | []) })` 会打印出路由的 `route.store` 对象上的属性。`includeMeta` 的值可以是属性名的数组 (例如:`['onRequest', Symbol('key')]`),也可以只是一个 `true`,表示显示所有属性。简写 `fastify.printRoutes({ includeHooks: true })` 将包含所有的[钩子](Hooks.md)。 + +```js + console.log(fastify.printRoutes({ includeHooks: true, includeMeta: ['metaProperty'] })) + // └── / + // ├── test (GET) + // │ • (onRequest) ["anonymous()","namedFunction()"] + // │ • (metaProperty) "value" + // │ └── /hello (GET) + // └── hel + // ├── lo/world (GET) + // │ • (onTimeout) ["anonymous()"] + // └── licopter (GET) + + console.log(fastify.printRoutes({ includeHooks: true })) + // └── / + // ├── test (GET) + // │ • (onRequest) ["anonymous()","namedFunction()"] + // │ └── /hello (GET) + // └── hel + // ├── lo/world (GET) + // │ • (onTimeout) ["anonymous()"] + // └── licopter (GET) +``` + + +#### printPlugins + +`fastify.printPlugins()`:打印 avvio 内部的插件树,可用于调试插件注册顺序相关的问题。
+*请在 `ready` 事件的回调中或事件触发之后调用该方法。* + +```js +fastify.register(async function foo (instance) { + instance.register(async function bar () {}) +}) +fastify.register(async function baz () {}) + +fastify.ready(() => { + console.error(fastify.printPlugins()) + // 输出: + // └── root + // ├── foo + // │ └── bar + // └── baz +}) +``` + + +#### addContentTypeParser + +`fastify.addContentTypeParser(content-type, options, parser)` 用于给指定 content type 自定义解析器,当你使用自定义的 content types 时会很有帮助。例如 `text/json, application/vnd.oasis.opendocument.text`。`content-type` 是一个字符串、字符串数组或正则表达式。 + +```js +// 传递给 getDefaultJsonParser 的两个参数用于配置原型污染以及构造函数污染,允许的值为 'ignore'、'remove' 和 'error'。设置为 ignore 会跳过校验,和直接调用 JSON.parse() 效果相同。详见 `secure-json-parse` 的文档。 + +fastify.addContentTypeParser('text/json', { asString: true }, fastify.getDefaultJsonParser('ignore', 'ignore')) +``` + + +#### getDefaultJsonParser + +`fastify.getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning)` 接受两个参数。第一个参数是原型污染的配置,第二个则是构造函数污染的配置。详见 `secure-json-parse` 的文档。 + + +#### defaultTextParser + +`fastify.defaultTextParser()` 可用于将 content 解析为纯文本。 + +```js +fastify.addContentTypeParser('text/json', { asString: true }, fastify.defaultTextParser()) +``` + + +#### errorHandler + +`fastify.errorHandler` 使用 Fastify 默认的错误处理函数来处理错误。 + +```js +fastify.get('/', { + errorHandler: (error, request, reply) => { + if (error.code === 'SOMETHING_SPECIFIC') { + reply.send({ custom: 'response' }) + return + } + + fastify.errorHandler(error, request, response) + } +}, handler) +``` + + +#### initialConfig + +`fastify.initialConfig`:暴露一个记录了 Fastify 初始选项的只读对象。 + +当前暴露的属性有: +- connectionTimeout +- keepAliveTimeout +- bodyLimit +- caseSensitive +- http2 +- https (返回 `false`/`true`。当特别指明时,返回 `{ allowHTTP1: true/false }`) +- ignoreTrailingSlash +- disableRequestLogging +- maxParamLength +- onProtoPoisoning +- onConstructorPoisoning +- pluginTimeout +- requestIdHeader +- requestIdLogLabel +- http2SessionTimeout + +```js +const { readFileSync } = require('fs') +const Fastify = require('fastify') + +const fastify = Fastify({ + https: { + allowHTTP1: true, + key: readFileSync('./fastify.key'), + cert: readFileSync('./fastify.cert') + }, + logger: { level: 'trace'}, + ignoreTrailingSlash: true, + maxParamLength: 200, + caseSensitive: true, + trustProxy: '127.0.0.1,192.168.1.1/24', +}) + +console.log(fastify.initialConfig) +/* +输出: +{ + caseSensitive: true, + https: { allowHTTP1: true }, + ignoreTrailingSlash: true, + maxParamLength: 200 +} +*/ + +fastify.register(async (instance, opts) => { + instance.get('/', async (request, reply) => { + return instance.initialConfig + /* + 返回: + { + caseSensitive: true, + https: { allowHTTP1: true }, + ignoreTrailingSlash: true, + maxParamLength: 200 + } + */ + }) + + instance.get('/error', async (request, reply) => { + // 会抛出错误 + // 因为 initialConfig 是只读的,不可修改 + instance.initialConfig.https.allowHTTP1 = false + + return instance.initialConfig + }) +}) + +// 开始监听 +fastify.listen(3000, (err) => { + if (err) { + fastify.log.error(err) + process.exit(1) + } +}) +``` diff --git a/doc/fastify-docs/docs/Serverless.md b/doc/fastify-docs/docs/Serverless.md new file mode 100644 index 0000000..094d152 --- /dev/null +++ b/doc/fastify-docs/docs/Serverless.md @@ -0,0 +1,310 @@ +

Serverless

+ +使用现有的 Fastify 应用运行无服务器 (serverless) 应用与 REST API。 + +Fastify 无法直接运行在无服务器平台上,需要做一点修改。本文为如何在知名的无服务器平台上运行 Fastify 应用提供指导。 + +#### 我应该在无服务器平台上使用 Fastify 吗? + +取决于你自己!FaaS 通常使用精简专注的函数,但你依然可以运行完整的 web 应用。但请牢记,应用越繁重,初始化越漫长。在无服务器环境中运行 Fastify,最好的方式便是使用例如 Google Cloud Run、AWS Fargate 以及 Azure Container Instances 之类的平台,它们能同时处理多个请求,因此能充分利用 Fastify 的特性。 + +开发便利是通过 Fastify 构建无服务器应用的优势之一。在本地环境,Fastify 应用无需任何额外工具即可运作,而相同的代码加上一些额外内容便能在无服务器平台上运行。 + +### 目录 + +- [AWS Lambda](#aws-lambda) +- [Google Cloud Run](#google-cloud-run) +- [Netlify Lambda](#netlify-lambda) +- [Vercel](#vercel) + +## AWS Lambda + +以下是使用 Fastify 在 AWS Lambda 和 Amazon API Gateway 架构上构建无服务器 web 应用/服务的示例。 + +*注:使用 [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) 仅是一种可行方案。* + +### app.js + +```js +const fastify = require('fastify'); + +function init() { + const app = fastify(); + app.get('/', (request, reply) => reply.send({ hello: 'world' })); + return app; +} + +if (require.main === module) { + // 直接调用,即执行 "node app" + init().listen(3000, (err) => { + if (err) console.error(err); + console.log('server listening on 3000'); + }); +} else { + // 作为模块引入 => 用于 aws lambda + module.exports = init; +} +``` + +你可以简单地把初始化代码包裹于可选的 [serverFactory](https://www.fastify.io/docs/latest/Server/#serverfactory) 选项里。 + +当执行 lambda 函数时,我们不需要监听特定的端口,因此,在这个例子里我们只要导出 `init` 函数即可。 +在 [`lambda.js`](https://www.fastify.io/docs/latest/Serverless/#lambda-js) 里,我们会用到它。 + +当像往常一样运行 Fastify 应用, +比如执行 `node app.js` 时 *(可以用 `require.main === module` 来判断)*, +你可以监听某个端口,如此便能本地运行应用了。 + +### lambda.js + +```js +const awsLambdaFastify = require('aws-lambda-fastify') +const init = require('./app'); + +const proxy = awsLambdaFastify(init()) +// 或 +// const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] }) + +exports.handler = proxy; +// 或 +// exports.handler = (event, context, callback) => proxy(event, context, callback); +// 或 +// exports.handler = (event, context) => proxy(event, context); +// 或 +// exports.handler = async (event, context) => proxy(event, context); +``` + +我们只需要引入 [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) (请确保安装了该依赖 `npm i --save aws-lambda-fastify`) 以及我们写的 [`app.js`](https://www.fastify.io/docs/latest/Serverless/#app-js),并使用 `app` 作为唯一参数调用导出的 `awsLambdaFastify` 函数。 +以上步骤返回的 `proxy` 函数拥有正确的签名,可作为 lambda 的处理函数。 +如此,所有的请求事件 (API Gateway 的请求) 都会被代理到 [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) 的 `proxy` 函数。 + +### 示例 + +你可以在[这里](https://github.com/claudiajs/example-projects/tree/master/fastify-app-lambda)找到使用 [claudia.js](https://claudiajs.com/tutorials/serverless-express.html) 的可部署的例子。 + + +### 注意事项 + +- 你没法操作 [stream](https://www.fastify.io/docs/latest/Reply/#streams),因为 API Gateway 还不支持它。 +- API Gateway 的超时时间为 29 秒,请务必在此时限内回复。 + +## Google Cloud Run + +与 AWS Lambda 和 Google Cloud Functions 不同,Google Cloud Run 是一个无服务器**容器**环境。它的首要目的是提供一个能运行任意容器的底层抽象 (infrastucture-abstracted) 的环境。因此,你能将 Fastify 部署在 Google Cloud Run 上,而且相比正常的写法,只需要改动极少的代码。 + +*参照以下步骤部署 Google Cloud Run。如果你对 gcloud 还不熟悉,请看其[入门文档](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)*。 + +### 调整 Fastify 服务器 + +为了让 Fastify 能正确地在容器里监听请求,请确保设置了正确的端口与地址: + +```js +function build() { + const fastify = Fastify({ trustProxy: true }) + return fastify +} + +async function start() { + // Google Cloud Run 会设置这一环境变量, + // 因此,你可以使用它判断程序是否运行在 Cloud Run 之中 + const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined + + // 监听 Cloud Run 提供的端口 + const port = process.env.PORT || 3000 + + // 监听 Cloud Run 中所有的 IPV4 地址 + const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined + + try { + const server = build() + const address = await server.listen(port, address) + console.log(`Listening on ${address}`) + } catch (err) { + console.error(err) + process.exit(1) + } +} + +module.exports = build + +if (require.main === module) { + start() +} +``` + +### 添加 Dockerfile + +你可以添加任意合法的 `Dockerfile`,用于打包运行 Node 程序。在 [gcloud 官方文档](https://github.com/knative/docs/blob/2d654d1fd6311750cc57187a86253c52f273d924/docs/serving/samples/hello-world/helloworld-nodejs/Dockerfile)中,你能找到一份基本的 `Dockerfile`。 + +```Dockerfile +# 使用官方 Node.js 10 镜像。 +# https://hub.docker.com/_/node +FROM node:10 + +# 创建并切换到应用目录。 +WORKDIR /usr/src/app + +# 拷贝应用依赖清单至容器镜像。 +# 使用通配符来确保 package.json 和 package-lock.json 均被复制。 +# 独立地拷贝这些文件,能防止代码改变时重复执行 npm install。 +COPY package*.json ./ + +# 安装生产环境依赖。 +RUN npm install --only=production + +# 复制本地代码到容器镜像。 +COPY . . + +# 启动容器时运行服务。 +CMD [ "npm", "start" ] +``` + +### 添加 .dockerignore + +添加一份如下的 `.dockerignore`,可以将仅用于构建的文件排除在容器之外 (能减小容器大小,加快构建速度): + +```.dockerignore +Dockerfile +README.md +node_modules +npm-debug.log +``` + +### 提交构建 + +接下来,使用以下命令将你的应用构建成一个 Docker 镜像 (将 `PROJECT-ID` 和 `APP-NAME` 替换为 Google 云平台的项目 id 和 app 名称): + +```bash +gcloud builds submit --tag gcr.io/PROJECT-ID/APP-NAME +``` + +### 部署镜像 + +镜像构建之后,使用如下命令部署它: + +```bash +gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed +``` + +如此,便能从 Google 云平台提供的链接访问你的应用了。 + +## netlify-lambda + +首先,完成与 **AWS Lambda** 有关的准备工作。 + +新建 `functions` 文件夹,在其中创建 `server.js` (应用的入口文件)。 + +### functions/server.js + +```js +export { handler } from '../lambda.js'; // 记得将路径修改为你的应用中对应的 `lambda.js` 的路径 +``` + +### netlify.toml + +```toml +[build] + # 构建站点时执行的命令 + command = "npm run build:functions" + # 发布到 netlify CDN 的文件夹 + # 同时也是应用的前端 + # publish = "build" + # 构建好的 Lambda 函数的目录 + functions = "functions-build" # 总是为构建后的 `functions` 文件夹名称加上 `-build` 后缀 +``` + +### webpack.config.netlify.js + +**别忘记添加这个文件,否则会有不少问题** + +```js +const nodeExternals = require('webpack-node-externals'); +const dotenv = require('dotenv-safe'); +const webpack = require('webpack'); + +const env = process.env.NODE_ENV || 'production'; +const dev = env === 'development'; + +if (dev) { + dotenv.config({ allowEmptyValues: true }); +} + +module.exports = { + mode: env, + devtool: dev ? 'eval-source-map' : 'none', + externals: [nodeExternals()], + devServer: { + proxy: { + '/.netlify': { + target: 'http://localhost:9000', + pathRewrite: { '^/.netlify/functions': '' } + } + } + }, + module: { + rules: [] + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.APP_ROOT_PATH': JSON.stringify('/'), + 'process.env.NETLIFY_ENV': true, + 'process.env.CONTEXT': env + }) + ] +}; +``` + +### Scripts + +在 `package.json` 的 *scripts* 里加上这一命令 + +```json +"scripts": { +... +"build:functions": "netlify-lambda build functions --config ./webpack.config.netlify.js" +... +} +``` + +这样就完成了。 + +## Vercel + +[Vercel](https://vercel.com) 针对 Node.js 应用提供了零配置部署方案。要使用 now,只需要如下配置你的 `vercel.json` 文件: + +```json +{ + "rewrites": [ + { + "source": "/(.*)", + "destination": "/api/serverless.js" + } + ] +} +``` + +之后,写一个 `api/serverless.js` 文件: + +```js +"use strict"; + +// 读取 .env 文件 +import * as dotenv from "dotenv"; +dotenv.config(); + +// 引入 Fastify 框架 +import Fastify from "fastify"; + +// 实例化 Fastify +const app = Fastify({ + logger: true, +}); + +// 将应用注册为一个常规插件 +app.register(import("../src/app")); + +export default async (req, res) => { + await app.ready(); + app.server.emit('request', req, res); +} +``` \ No newline at end of file diff --git a/doc/fastify-docs/docs/Testing.md b/doc/fastify-docs/docs/Testing.md new file mode 100644 index 0000000..a2fc20a --- /dev/null +++ b/doc/fastify-docs/docs/Testing.md @@ -0,0 +1,294 @@ +

Fastify

+ +## 测试 + +测试是开发应用最重要的一部分。Fastify 处理测试非常灵活并且它兼容绝大多数框架 (例如 [Tap](https://www.npmjs.com/package/tap)。下面的例子都会用这个演示)。 + +让我们 `cd` 进入一个全新的 'testing-example' 文件夹,并在终端里输入 `npm init -y`。 + +执行 `npm install fastify && npm install tap pino-pretty --save-dev`。 + +### 关注点分离让测试变得轻松 + +首先,我们将应用代码与服务器代码分离: + +**app.js**: + +```js +'use strict' + +const fastify = require('fastify') + +function build(opts={}) { + const app = fastify(opts) + app.get('/', async function (request, reply) { + return { hello: 'world' } + }) + + return app +} + +module.exports = build +``` + +**server.js**: + +```js +'use strict' + +const server = require('./app')({ + logger: { + level: 'info', + prettyPrint: true + } +}) + +server.listen(3000, (err, address) => { + if (err) { + console.log(err) + process.exit(1) + } +}) +``` + +### 使用 fastify.inject() 的好处 + +感谢有 [`light-my-request`](https://github.com/fastify/light-my-request),Fastify 自带了伪造的 HTTP 注入。 + +在进行任何测试之前,我们通过 `.inject` 方法向路由发送假的请求: + +**app.test.js**: + +```js +'use strict' + +const build = require('./app') + +const test = async () => { + const app = build() + + const response = await app.inject({ + method: 'GET', + url: '/' + }) + + console.log('status code: ', response.statusCode) + console.log('body: ', response.body) +} +test() +``` + +我们的代码运行在异步函数里,因此可以使用 async/await。 + +`.inject` 确保了所有注册的插件都已引导完毕,可以开始测试应用了。之后请求方法将被传递到路由函数中去。使用 await 可以存储响应,且避免了回调函数。 + +在终端执行 `node app.test.js` 来开始测试。 + +```sh +status code: 200 +body: {"hello":"world"} +``` + +### HTTP 注入测试 + +现在我们能用真实的测试语句代替 `console.log` 了! + +在 `package.json` 里修改 "test" script 如下: + +`"test": "tap --reporter=list --watch"` + +**app.test.js**: + +```js +'use strict' + +const { test } = require('tap') +const build = require('./app') + +test('requests the "/" route', async t => { + const app = build() + + const response = await app.inject({ + method: 'GET', + url: '/' + }) + t.equal(response.statusCode, 200, 'returns a status code of 200') +}) +``` + +执行 `npm test`,查看结果! + +`inject` 方法能完成的不只有简单的 GET 请求: +```js +fastify.inject({ + method: String, + url: String, + query: Object, + payload: Object, + headers: Object, + cookies: Object +}, (error, response) => { + // 你的测试 +}) +``` + +忽略回调函数,可以链式调用 `.inject` 提供的方法: + +```js +fastify + .inject() + .get('/') + .headers({ foo: 'bar' }) + .query({ foo: 'bar' }) + .end((err, res) => { // 调用 .end 触发请求 + console.log(res.payload) + }) +``` + +或是用 promise 的版本 + +```js +fastify + .inject({ + method: String, + url: String, + query: Object, + payload: Object, + headers: Object, + cookies: Object + }) + .then(response => { + // 你的测试 + }) + .catch(err => { + // 处理错误 + }) +``` + +Async await 也是支持的! +```js +try { + const res = await fastify.inject({ method: String, url: String, payload: Object, headers: Object }) + // 你的测试 +} catch (err) { + // 处理错误 +} +``` + +#### 另一个例子: + +**app.js** +```js +const Fastify = require('fastify') + +function buildFastify () { + const fastify = Fastify() + + fastify.get('/', function (request, reply) { + reply.send({ hello: 'world' }) + }) + + return fastify +} + +module.exports = buildFastify +``` + +**test.js** +```js +const tap = require('tap') +const buildFastify = require('./app') + +tap.test('GET `/` route', t => { + t.plan(4) + + const fastify = buildFastify() + + // 在测试的最后,我们强烈建议你调用 `.close()` + // 方法来确保所有与外部服务的连接被关闭。 + t.teardown(() => fastify.close()) + + fastify.inject({ + method: 'GET', + url: '/' + }, (err, response) => { + t.error(err) + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'application/json; charset=utf-8') + t.same(response.json(), { hello: 'world' }) + }) +}) +``` + +### 测试正在运行的服务器 +你还可以在 fastify.listen() 启动服务器之后,或是 fastify.ready() 初始化路由与插件之后,进行 Fastify 的测试。 + +#### 举例: + +使用之前例子的 **app.js**。 + +**test-listen.js** (用 [`Request`](https://www.npmjs.com/package/request) 测试) +```js +const tap = require('tap') +const request = require('request') +const buildFastify = require('./app') + +tap.test('GET `/` route', t => { + t.plan(5) + + const fastify = buildFastify() + + t.teardown(() => fastify.close()) + + fastify.listen(0, (err) => { + t.error(err) + + request({ + method: 'GET', + url: 'http://localhost:' + fastify.server.address().port + }, (err, response, body) => { + t.error(err) + t.equal(response.statusCode, 200) + t.equal(response.headers['content-type'], 'application/json; charset=utf-8') + t.same(JSON.parse(body), { hello: 'world' }) + }) + }) +}) +``` + +**test-ready.js** (用 [`SuperTest`](https://www.npmjs.com/package/supertest) 测试) +```js +const tap = require('tap') +const supertest = require('supertest') +const buildFastify = require('./app') + +tap.test('GET `/` route', async (t) => { + const fastify = buildFastify() + + t.teardown(() => fastify.close()) + + await fastify.ready() + + const response = await supertest(fastify.server) + .get('/') + .expect(200) + .expect('Content-Type', 'application/json; charset=utf-8') + t.same(response.body, { hello: 'world' }) +}) +``` + +### 如何检测 tap 的测试 +1. 设置 `{only: true}` 选项,将需要检测的测试与其他测试分离 +```javascript +test('should ...', {only: true}, t => ...) +``` +2. 通过 `npx` 运行 `tap` +```bash +> npx tap -O -T --node-arg=--inspect-brk test/ +``` +- `-O` 表示开启 `only` 选项,只运行设置了 `{only: true}` 的测试 +- `-T` 表示不设置超时 +- `--node-arg=--inspect-brk` 会启动 node 调试工具 +3. 在 VS Code 中创建并运行一个 `Node.js: Attach` 调试配置,不需要额外修改。 + +现在你便可以在编辑器中检测你的测试文件 (以及 `Fastify` 的其他部分) 了。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/TypeScript.md b/doc/fastify-docs/docs/TypeScript.md new file mode 100644 index 0000000..274eb2f --- /dev/null +++ b/doc/fastify-docs/docs/TypeScript.md @@ -0,0 +1,1250 @@ +

Fastify

+ +## TypeScript + +Fastify 是用普通的 JavaScript 编写的,因此,类型定义的维护并不容易。可喜的是,自版本 2 以来,维护者和贡献者们已经在类型维护上投入了巨大的努力。 + +版本 3 的类型系统发生了改变。新的系统带来了泛型约束 (generic constraining) 与默认值,以及定义请求 body,querystring 等 schema 的新方式!在团队改善框架和类型定义的协作中,难免有所纰漏。我们鼓励你**参与贡献**。请记得在开始前阅读 [`CONTRIBUTING.md`](https://github.com/fastify/fastify/blob/HEAD/CONTRIBUTING.md) 一文! + +> 本文档介绍的是 Fastify 3.x 版本的类型 + +> 插件不一定包含类型定义。更多内容请看[插件](#plugins)。我们鼓励用户提交 PR 来改善插件的类型支持。 + +别忘了安装 `@types/node`。 + +## 从例子中学习 + +通过例子来学习 Fastify 的类型系统是最好的途径!以下四个例子涵盖了最常见的开发场景。例子之后是更详尽深入的文档。 + +### 起步 + +这个例子展示了如何使用 Fastify 和 TypeScript 构建一个最简单的 http 服务器。 + +1. 创建一个 npm 项目,安装 Fastify、typescript 和 node.js 的类型文件: + ```bash + npm init -y + npm i fastify + npm i -D typescript @types/node + ``` +2. 在 `package.json` 的 `"scripts"` 里添加以下内容: + ```json + { + "scripts": { + "build": "tsc -p tsconfig.json", + "start": "node index.js" + } + } + ``` +3. 初始化 TypeScript 配置文件: + ```bash + npx tsc --init + ``` + 或使用一个[推荐的配置文件](https://github.com/tsconfig/bases#node-10-tsconfigjson)。 +4. 创建 `index.ts` ,在此编写服务器的代码。 +5. 将下列代码添加到该文件中: + ```typescript + import fastify from 'fastify' + + const server = fastify() + + server.get('/ping', async (request, reply) => { + return 'pong\n' + }) + + server.listen(8080, (err, address) => { + if (err) { + console.error(err) + process.exit(1) + } + console.log(`Server listening at ${address}`) + }) + ``` +6. 执行 `npm run build`。这么做会将 `index.ts` 编译为能被 Node.js 运行的 `index.js`。如果遇到了错误,请在 [fastify/help](https://github.com/fastify/help/) 发布 issue。 +7. 执行 `npm run start` 来启动 Fastify 服务器。 +8. 你将看到控制台输出: `Server listening at http://127.0.0.1:8080`。 +9. 通过 `curl localhost:8080/ping` 访问服务,你将收到 `pong`。 + +🎉 现在,你有了一个能用的 TypeScript 写的 Fastify 服务器!这个例子演示了在 3.x 版本中,类型系统有多么简单。默认情况下,类型系统会假定你使用的是 `http` 服务器。后续的例子将为你展现更多内容,例如,创建较为复杂的服务器 (`https` 与 `http2`),以及指定路由的 schema! + +> 更多使用 TypeScript 初始化 Fastify 的示例 (如启用 HTTP2),请在[这里][Fastify]查阅详细的 API。 + +### 使用泛型 + +类型系统重度依赖于泛型属性来提供最精确的开发时体验。有人可能会认为这么做有些麻烦,但这是值得的!这个例子将展示如何在路由 schema 中实现泛型,以及路由层 `request` 对象上的动态属性。 + +1. 照着上面例子的 1-4 步来初始化项目。 +2. 在 `index.ts` 中定义两个接口 (interface),`IQuerystring` 和 `IHeaders`: + ```typescript + interface IQuerystring { + username: string; + password: string; + } + + interface IHeaders { + 'h-Custom': string; + } + ``` +3. 使用这两个接口,定义一个新的 API 路由,并将它们用作泛型。路由方法的简写形式 (如 `.get`) 接受一个泛型对象 `RouteGenericInterface`,它包含了五个具名属性:`Body`、`Querystring`、`Params`、`Headers` 以及 `Reply`。`Body`、`Querystring`、`Params` 和 `Headers` 四个接口会随着路由方法向下传递,到达路由处理函数中的 `request` 实例,`Reply` 接口则会到达 `reply` 实例。 + ```typescript + server.get<{ + Querystring: IQuerystring, + Headers: IHeaders + }>('/auth', async (request, reply) => { + const { username, password } = request.query + const customerHeader = request.headers['h-Custom'] + // 处理请求数据 + + return `logged in!` + }) + ``` + +4. 执行 `npm run build` 和 `npm run start` 来构建并运行项目。 +5. 访问 api: + ```bash + curl localhost:8080/auth?username=admin&password=Password123! + ``` + 将会返回 `logged in!`。 +6. 此外,泛型接口还可以用在路由层钩子方法中。在上面的路由内加上一个 `preValidation` 钩子: + ```typescript + server.get<{ + Querystring: IQuerystring, + Headers: IHeaders + }>('/auth', { + preValidation: (request, reply, done) => { + const { username, password } = request.query + done(username !== 'admin' ? new Error('Must be admin') : undefined) + } + // 或使用 async + // preValidation: async (request, reply) => { + // const { username, password } = request.query + // return username !== "admin" ? new Error("Must be admin") : undefined; + // } + }, async (request, reply) => { + const customerHeader = request.headers['h-Custom'] + // 处理请求数据 + return `logged in!` + }) + ``` +7. 构建运行之后,使用任何值不为 `admin` 的 `username` 查询字符串访问服务。你将收到一个 500 错误:`{"statusCode":500,"error":"Internal Server Error","message":"Must be admin"}` + + 干得漂亮。现在你能够为每个路由定义接口,并拥有严格类型的请求与响应实例了。Fastify 类型系统的其他部分依赖于泛型属性。关于如何使用它们,请参照后文详细的类型系统文档。 + +### JSON Schema + +你可以通过 JSON Schema 来验证请求与响应。给 Fastify 路由定义 schema 还能提高吞吐量!更多信息请见[验证和序列化](Validation-and-Serialization.md)。 + +此外,在路由处理函数 (包括 pre-validation 等钩子) 中使用定义好的类型也是有好处的。 + +以下列出了几种实现方案。 + +#### typebox + +[typebox](https://www.npmjs.com/package/@sinclair/typebox) 能帮助你同时构建类型与 schema。通过 typebox 在代码里定义好 schema 之后,你便能将其当作类型或 schema 来使用。 + +在 Fastify 路由中验证 payload,你可以这么做: + +1. 安装 `typebox`。 + + ```bash + npm i @sinclair/typebox + ``` + +2. 使用 `Type` 定义 schema,并通过 `Static` 创建相应的类型。 + + ```typescript + import { Static, Type } from '@sinclair/typebox' + + const User = Type.Object({ + name: Type.String(), + mail: Type.Optional(Type.String({ format: "email" })), + }); + type UserType = Static; + ``` + +3. 在路由中使用定义好的类型与 schema。 + + ```typescript + const app = fastify(); + + app.post<{ Body: UserType; Reply: UserType }>( + "/", + { + schema: { + body: User, + response: { + 200: User, + }, + }, + }, + (req, rep) => { + const { body: user } = req; + /* user 的类型如下: + * const user: StaticProperties<{ + * name: TString; + * mail: TOptional; + * }> + */ + //... + rep.status(200).send(user); + } + ); + ``` + +#### Schemas in JSON Files + +在上一个例子里,我们使用接口定义了请求 querystring 和 header 的类型。许多用户使用 JSON Schema 来处理这些工作,幸运的是,有一套方法能将现有的 JSON Schema 转换为 TypeScript 接口! + +1. 完成 '起步' 中例子的 1-4 步。 +2. 安装 `json-schema-to-typescript` 模块: + + ```bash + npm i -D json-schema-to-typescript + ``` + +3. 新建一个名为 `schemas` 的文件夹。在其中添加 `headers.json` 与 `querystring.json` 两个文件,将下面的 schema 定义粘贴到对应文件中。 + + ```json + { + "title": "Headers Schema", + "type": "object", + "properties": { + "h-Custom": { "type": "string" } + }, + "additionalProperties": false, + "required": ["h-Custom"] + } + ``` + + ```json + { + "title": "Querystring Schema", + "type": "object", + "properties": { + "username": { "type": "string" }, + "password": { "type": "string" } + }, + "additionalProperties": false, + "required": ["username", "password"] + } + ``` + +4. 在 package.json 里加上一行 `compile-schemas` 脚本: + + ```json + { + "scripts": { + "compile-schemas": "json2ts -i schemas -o types" + } + } + ``` + + `json2ts` 是囊括在 `json-schema-to-typescript` 中的命令行工具。`schemas` 是输入路径,`types` 则是输出路径。 +5. 执行 `npm run compile-schemas`,在 `types` 文件夹下生成两个新文件。 +6. 更新 `index.ts`: + + ```typescript + import fastify from 'fastify' + + // 导入 json schema + import QuerystringSchema from './schemas/querystring.json' + import HeadersSchema from './schemas/headers.json' + + // 导入生成的接口 + import { QuerystringSchema as QuerystringSchemaInterface } from './types/querystring' + import { HeadersSchema as HeadersSchemaInterface } from './types/headers' + + const server = fastify() + + server.get<{ + Querystring: QuerystringSchemaInterface, + Headers: HeadersSchemaInterface + }>('/auth', { + schema: { + querystring: QuerystringSchema, + headers: HeadersSchema + }, + preValidation: (request, reply, done) => { + const { username, password } = request.query + done(username !== 'admin' ? new Error('Must be admin') : undefined) + } + }, async (request, reply) => { + const customerHeader = request.headers['h-Custom'] + // 处理请求数据 + return `logged in!` + }) + + server.route<{ + Querystring: QuerystringSchemaInterface, + Headers: HeadersSchemaInterface + }>({ + method: 'GET', + url: '/auth2', + schema: { + querystring: QuerystringSchema, + headers: HeadersSchema + }, + preHandler: (request, reply, done) => { + const { username, password } = request.query + const customerHeader = request.headers['h-Custom'] + done() + }, + handler: (request, reply) => { + const { username, password } = request.query + const customerHeader = request.headers['h-Custom'] + reply.status(200).send({username}); + } + }) + + server.listen(8080, (err, address) => { + if (err) { + console.error(err) + process.exit(0) + } + console.log(`Server listening at ${address}`) + }) + ``` + 要特别关注文件顶部的导入。虽然看上去有些多余,但你必须同时导入 schema 与生成的接口。 + +真棒!现在你就能同时运用 JSON Schema 与 TypeScript 的定义了。 + +#### json-schema-to-ts + +不想基于 schema 生成类型,而是直接使用它们的话,你可以考虑 [json-schema-to-ts](https://www.npmjs.com/package/json-schema-to-ts) 模块。 + +安装该模块为 dev-dependency: + +```bash +npm install -D json-schema-to-ts +``` + +你可以像定义正常的对象一样定义 schema。但得注意要用 *const* 来定义,原因见该模块的文档。 + +```typescript +const todo = { + type: 'object', + properties: { + name: { type: 'string' }, + description: { type: 'string' }, + done: { type: 'boolean' }, + }, + required: ['name'], +} as const; +``` + +通过类型 `FromSchema` 你可以基于 schema 构建一个类型,并在函数中使用它。 + +```typescript +import { FromSchema } from "json-schema-to-ts"; +fastify.post<{ Body: FromSchema }>( + '/todo', + { + schema: { + body: todo, + response: { + 201: { + type: 'string', + }, + }, + } + }, + async (request, reply): Promise => { + + /* + request.body 的类型如下: + { + [x: string]: unknown; + description?: string; + done?: boolean; + name: string; + } + */ + + request.body.name // 不会抛出类型错误 + request.body.notthere // 会抛出类型错误 + + reply.status(201).send(); + }, +); +``` + +### 插件 + +拓展性强的插件生态系统是 Fastify 最突出的特性之一。插件完全支持类型系统,并利用了[声明合并]() (declaration merging) 模式的优势。下面的例子将分为三个部分:用 TypeScript 编写 Fastify 插件,为插件编写类型定义,以及在 TypeScript 项目中使用插件。 + +#### 用 TypeScript 编写 Fastify 插件 + +1. 初始化新的 npm 项目,并安装必需的依赖。 + ```bash + npm init -y + npm i fastify fastify-plugin + npm i -D typescript @types/node + ``` +2. 在 `package.json` 的 `"scripts"` 中加上一行 `build`,`"types"` 中写入 `'index.d.ts'`: + ```json + { + "types": "index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json" + } + } + ``` +3. 初始化 TypeScript 配置文件: + ```bash + npx typescript --init + ``` + 文件生成后,启用 `"compilerOptions"` 对象中的 `"declaration"` 选项。 + ```json + { + "compileOptions": { + "declaration": true + } + } + ``` +4. 新建 `index.ts` 文件,在这里编写插件代码。 +5. 在 `index.ts` 中写入以下代码。 + ```typescript + import { FastifyPluginCallback, FastifyPluginAsync } from 'fastify' + import fp from 'fastify-plugin' + + // 利用声明合并,将插件的属性加入合适的 fastify 接口。 + declare module 'fastify' { + interface FastifyRequest { + myPluginProp: string + } + interface FastifyReply { + myPluginProp: number + } + } + + // 定义选项 + export interface MyPluginOptions { + myPluginOption: string + } + + // 使用回调函数定义插件 + const myPluginCallback: FastifyPluginCallback = (fastify, options, done) => { + fastify.decorateRequest('myPluginProp', 'super_secret_value') + fastify.decorateReply('myPluginProp', options.myPluginOption) + + done() + } + + // 使用 promise 定义插件 + const myPluginAsync: FastifyPluginAsync = async (fastify, options) => { + fastify.decorateRequest('myPluginProp', 'super_secret_value') + fastify.decorateReply('myPluginProp', options.myPluginOption) + } + + // 使用 fastify-plugin 导出插件 + export default fp(myPluginCallback, '3.x') + // 或者 + // export default fp(myPluginAsync, '3.x') + ``` +6. 运行 `npm run build` 编译,生成 JavaScript 源文件以及类型定义文件。 +7. 如此一来,插件便完工了。你可以[发布到 npm] 或直接本地使用。 + > 并非将插件发布到 npm _才能_ 使用。你可以将其放在 Fastify 项目内,并像引用任意代码一样引用它!请确保声明文件在项目编译的范围内,以便能被 TypeScript 处理器使用。 + +#### 为插件编写类型定义 + +以下例子是为 JavaScript 编写的 Fastify 插件所作,展示了如何在插件中加入 TypeScript 支持,以方便用户使用。 + +1. 初始化新的 npm 项目,并安装必需的依赖。 + ```bash + npm init -y + npm i fastify-plugin + ``` +2. 新建 `index.js` 和 `index.d.ts`。 +3. 将这两个文件写入 package.json 的 `main` 和 `types` 中 (文件名不一定为 `index`,但推荐都使用这个名字): + ```json + { + "main": "index.js", + "types": "index.d.ts" + } + ``` +4. 在 `index.js` 中加入以下代码: + ```javascript + // 极力推荐使用 fastify-plugin 包装插件 + const fp = require('fastify-plugin') + + function myPlugin (instance, options, done) { + + // 用自定义函数 myPluginFunc 装饰 fastify 实例 + instance.decorate('myPluginFunc', (input) => { + return input.toUpperCase() + }) + + done() + } + + module.exports = fp(myPlugin, { + fastify: '3.x', + name: 'my-plugin' // 被 fastify-plugin 用来获取属性名 + }) + ``` +5. 在 `index.d.ts` 中加入以下代码: + ```typescript + import { FastifyPlugin } from 'fastify' + + interface PluginOptions { + //... + } + // 你可以导出任意内容 + // 在此,我们导出之前添加的装饰器 + export interface myPluginFunc { + (input: string): string + } + + // 利用声明合并将自定义属性加入 Fastify 的类型系统 + declare module 'fastify' { + interface FastifyInstance { + myPluginFunc: myPluginFunc + } + } + + // fastify-plugin 会自动添加具名导出,因此请确保加上该类型。 + // 如果缺少 `module.exports.myPlugin`,变量名会通过 `options.name` 属性获取。 + export const myPlugin: FastifyPlugin + // fastify-plugin 会自动在导出的插件上添加 `.default` 属性。详见下文。 + export default myPlugin + ``` + +__注意__:v2.3.0 及以上版本的 [fastify-plugin](https://github.com/fastify/fastify-plugin) 会自动给导出的插件添加 `default` 属性以及具名导出。为了更好的开发体验,请确保在类型文件中加上了 `export default` 与 `export const myPlugin`。完整的例子可以查看 [fastify-swagger](https://github.com/fastify/fastify-swagger/blob/main/index.d.ts)。 + +这样一来,该插件便能被任意 TypeScript 项目使用了! + +Fastify 的插件系统允许开发者装饰 Fastify 以及 request/reply 的实例。更多信息请见[声明合并与泛型继承](https://dev.to/ethanarrowood/is-declaration-merging-and-generic-inheritance-at-the-same-time-impossible-53cp)一文。 + +#### 使用插件 + +在 TypeScript 中使用插件和在 JavaScript 中使用一样简单,只需要用到 `import/from` 而已,除了一个特殊情况。 + +Fastify 插件使用声明合并来修改已有的 Fastify 类型接口 (详见上一个例子)。声明合并没有那么 _聪明_,只要插件的类型定义在 TypeScript 解释器的范围内,那么**不管**插件本身是否被使用,这些定义都会被包括。这是 TypeScript 的限制,目前无法规避。 + +尽管如此,还是有一些建议能帮助改善这种状况: +- 确保 [ESLint](https://eslint.org/docs/rules/no-unused-vars) 开启了 `no-unused-vars`,并且所有导入的插件都得到了加载。 +- 通过诸如 [depcheck](https://www.npmjs.com/package/depcheck) 或 [npm-check](https://www.npmjs.com/package/npm-check) 的工具来验证所有插件都在项目中得到了使用。 + +## 原生 JavaScript 的代码补全 + +原生 JavaScript 能根据发布好的类型,依照 [TypeScript 的 JSDoc 参考](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) 来完成代码补全 (例如 [Intellisense](https://code.visualstudio.com/docs/editor/intellisense))。 + +举个例子: + +```js +/** @type {import('fastify').FastifyPluginAsync<{ optionA: boolean, optionB: string }>} */ +module.exports = async function (fastify, { optionA, optionB }) { + fastify.get('/look', () => 'at me'); +} +``` + +## API 类型系统文档 + +本节详述了所有在 Fastify 3.x 版本中可用的类型。 + +所有 `http`、`https` 以及 `http2` 的类型来自 `@types/node`。 + +[泛型](#generics)的文档包括了其默认值以及约束值。更多关于 TypeScript 泛型的信息请阅读以下文章。 +- [泛型参数默认值](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-3.html#generic-parameter-defaults) +- [泛型约束](https://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints) + +#### 如何导入 + +Fastify 的 API 都首先来自于 `fastify()` 方法。在 JavaScript 中,通过 `const fastify = require('fastify')` 来导入。在 TypeScript 中,建议的做法是使用 `import/from` 语法,这样类型能得到处理。有如下几种导入的方法。 + +1. `import fastify from 'fastify'` + - 类型得到了处理,但无法通过点标记 (dot notation) 访问 + - 例子: + ```typescript + import fastify from 'fastify' + + const f = fastify() + f.listen(8080, () => { console.log('running') }) + ``` + - 通过解构赋值访问类型 + ```typescript + import fastify, { FastifyInstance } from 'fastify' + + const f: FastifyInstance = fastify() + f.listen(8080, () => { console.log('running') }) + ``` + - 主 API 方法也可以使用解构赋值 + ```typescript + import { fastify, FastifyInstance } from 'fastify' + + const f: FastifyInstance = fastify() + f.listen(8080, () => { console.log('running') }) + ``` +2. `import * as Fastify from 'fastify'` + - 类型得到了处理,并可通过点标记访问 + - 主 API 方法要用稍微不同的语法调用 (见例子) + - 例子: + ```typescript + import * as Fastify from 'fastify' + + const f: Fastify.FastifyInstance = Fastify.fastify() + f.listen(8080, () => { console.log('running') }) + ``` +3. `const fastify = require('fastify')` + - 语法有效,也能正确地导入。然而并**不**支持类型 + - 例子: + ```typescript + const fastify = require('fastify') + + const f = fastify() + f.listen(8080, () => { console.log('running') }) + ``` + - 支持解构,且能处理类型 + ```typescript + const { fastify } = require('fastify') + + const f = fastify() + f.listen(8080, () => { console.log('running') }) + ``` + +#### 泛型 + +许多类型定义共用了某些泛型参数。它们都在本节有详尽的描述。 + +多数定义依赖于 `@node/types` 中的 `http`、`https` 与 `http2` 模块。 + +##### RawServer +底层 Node.js server 的类型。 + +默认值:`http.Server` + +约束值:`http.Server`、`https.Server`、`http2.Http2Server`、`http2.Http2SecureServer` + +必要的泛型参数 (Enforces generic parameters):[`RawRequest`][RawRequestGeneric]、[`RawReply`][RawReplyGeneric] + +##### RawRequest +底层 Node.js request 的类型。 + +默认值:[`RawRequestDefaultExpression`][RawRequestDefaultExpression] + +约束值:`http.IncomingMessage`、`http2.Http2ServerRequest` + +被 [`RawServer`][RawServerGeneric] 约束。 + +##### RawReply +底层 Node.js response 的类型。 + +默认值:[`RawReplyDefaultExpression`][RawReplyDefaultExpression] + +约束值:`http.ServerResponse`、`http2.Http2ServerResponse` + +被 [`RawServer`][RawServerGeneric] 约束。 + +##### Logger +Fastify 日志工具。 + +默认值:[`FastifyLoggerOptions`][FastifyLoggerOptions] + +被 [`RawServer`][RawServerGeneric] 约束。 + +##### RawBody +为 content-type-parser 方法提供的泛型参数。 + +约束值:`string | Buffer` + +--- + +#### Fastify + +##### fastify<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [Logger][LoggerGeneric]>(opts?: [FastifyServerOptions][FastifyServerOptions]): [FastifyInstance][FastifyInstance] +[源码](https://github.com/fastify/fastify/blob/main/fastify.d.ts#L19) + +Fastify 首要的 API 方法。默认情况下创建一个 HTTP 服务器。通过可辨识联合 (discriminant unions) 及重载的方法 (overload methods),类型系统能自动地根据传递给该方法的选项 (详见下文例子),推断出服务器的类型 (http、https 或 http2)。同时,可拓展的泛型类型系统允许用户拓展底层的 Node.js Server、Request 和 Reply 对象。此外,自定义日志类型则可以运用 `Logger` 泛型。详见下文的例子和泛型分类说明。 + +###### 例子 1:标准的 HTTP 服务器 + +无需指明 `Server` 的具体类型,因为默认值就是 HTTP 服务器。 +```typescript +import fastify from 'fastify' + +const server = fastify() +``` +回顾“从例子中学习”的[起步](#getting-started)一节的示例来获取更详细的内容。 + +###### 例子 2:HTTPS 服务器 + +1. 从 `@types/node` 与 `fastify` 导入模块。 + ```typescript + import fs from 'fs' + import path from 'path' + import fastify from 'fastify' + ``` +2. 按照官方 [Node.js https 服务器指南](https://nodejs.org/en/knowledge/HTTP/servers/how-to-create-a-HTTPS-server/)的步骤,创建 `key.pem` 与 `cert.pem` 文件。 +3. 实例化一个 Fastify https 服务器,并添加一个路由: + ```typescript + const server = fastify({ + https: { + key: fs.readFileSync(path.join(__dirname, 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'cert.pem')) + } + }) + + server.get('/', async function (request, reply) { + return { hello: 'world' } + }) + + server.listen(8080, (err, address) => { + if (err) { + console.error(err) + process.exit(0) + } + console.log(`Server listening at ${address}`) + }) + ``` +4. 构建并运行!执行 `curl -k https://localhost:8080` 来测试服务。 + +###### 例子 3:HTTP2 服务器 + +HTTP2 服务器有两种类型,非安全与安全。两种类型都需要在 `options` 对象中设置 `http2` 属性的值为 `true`。设置 `https` 属性会创建一个安全的 http2 服务器;忽略该属性则创建非安全的服务器。 + +```typescript +const insecureServer = fastify({ http2: true }) +const secureServer = fastify({ + http2: true, + https: {} // 使用 https 服务的 `key.pem` 和 `cert.pem` 文件 +}) +``` + +更多细节详见 Fastify 的 [HTTP2](HTTP2.md) 文档。 + +###### 例子 4:拓展 HTTP 服务器 + +你不仅可以指定服务器的类型,还可以指定请求与响应的类型,即指定特殊的属性、方法等!在服务器实例化之时指定类型,则之后的实例都可应用自定义的类型。 +```typescript +import fastify from 'fastify' +import http from 'http' + +interface customRequest extends http.IncomingMessage { + mySpecialProp: string +} + +const server = fastify() + +server.get('/', async (request, reply) => { + const someValue = request.raw.mySpecialProp // 由于 `customRequest` 接口的存在,TypeScript 能知道这是一个字符串 + return someValue.toUpperCase() +}) +``` + +###### 例子 5:指定日志类型 + +Fastify 使用 [Pino](https://getpino.io/#/) 作为日志工具。其中一些属性可以在构建 Fastify 实例时,在 `logger` 字段中配置。如果需要的属性未被暴露出来,你也能通过将一个外部配置好的 Pino 实例 (或其他兼容的日志工具) 传给这个字段,来配置这些属性。这么做也允许你自定义序列化工具,详见[日志](Logging.md)的文档。 + +要使用 Pino 的外部实例,请将 `@types/pino` 添加到 devDependencies 中,并把实例传给 `logger` 字段: + +```typescript +import fastify from 'fastify' +import pino from 'pino' + +const server = fastify({ + logger: pino({ + level: 'info', + redact: ['x-userinfo'], + messageKey: 'message' + }) +}) + +server.get('/', async (request, reply) => { + server.log.info('log message') + return 'another message' +}) +``` + +--- + +##### fastify.HTTPMethods +[源码](https://github.com/fastify/fastify/blob/main/types/utils.d.ts#L8) + +`'DELETE' | 'GET' | 'HEAD' | 'PATCH' | 'POST' | 'PUT' | 'OPTIONS'` 的联合类型 (Union type) + +##### fastify.RawServerBase +[源码](https://github.com/fastify/fastify/blob/main/types/utils.d.ts#L13) + +依赖于 `@types/node` 的模块 `http`、`https`、`http2` + +`http.Server | https.Server | http2.Http2Server | http2.Http2SecureServer` 的联合类型 + +##### fastify.RawServerDefault +[源码](https://github.com/fastify/fastify/blob/main/types/utils.d.ts#L18) + +依赖于 `@types/node` 的模块 `http` + +`http.Server` 的类型别名 + +--- + +##### fastify.FastifyServerOptions<[RawServer][RawServerGeneric], [Logger][LoggerGeneric]> + +[源码](../fastify.d.ts#L29) + +Fastify 服务器实例化时,调用 [`fastify()`][Fastify] 方法使用到的接口。泛型参数 `RawServer` 和 `Logger` 会随此方法向下传递。 + +关于用 TypeScript 实例化一个 Fastify 服务器的例子,请见 [fastify][Fastify] 主方法的类型定义。 + +##### fastify.FastifyInstance<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RequestGeneric][FastifyRequestGenericInterface], [Logger][LoggerGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/instance.d.ts#L16) + +表示 Fastify 服务器对象的接口,[`fastify()`][Fastify] 方法的返回值。假如你使用了 `decorate` 方法,借由[声明合并](https://www.typescriptlang.org/docs/handbook/declaration-merging.html)可以拓展该接口。 + +通过泛型级联 (generic cascading),实例上所有的方法都能继承实例化时的泛型属性。这意味着只要指定了服务器、请求或响应的类型,所有方法都能随之确定这些对象的类型。 + +具体说明请看“[从例子中学习](#learn-by-example)”一节,或 [fastify][Fastify] 方法中更简洁的例子。 + +--- + +#### Request + +##### fastify.FastifyRequest<[RequestGeneric][FastifyRequestGenericInterface], [RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric]> +[源码](https://github.com/fastify/fastify/blob/main/types/request.d.ts#L15) + +该接口包含了 Fastify 请求对象的属性。这些属性无视请求类型 (http 或 http2),也无关路由层级。因此在 GET 请求中访问 `request.body` 并不会抛错 (假如 GET 有 body 😉)。 + +假如你需要为 `FastifyRequest` 对象添加自定义属性 (例如使用 [`decorateRequest`][DecorateRequest] 方法时),你应该针对该接口应用声明合并。 + +在 [`FastifyRequest`][FastifyRequest] 里有基本的范例。更详细的例子请见“从例子中学习”的[插件](#plugins)一节。 + +###### 例子 +```typescript +import fastify from 'fastify' + +const server = fastify() + +server.decorateRequest('someProp', 'hello!') + +server.get('/', async (request, reply) => { + const { someProp } = request // 需要通过声明合并将该属性添加到 request 接口上 + return someProp +}) + +// 以下声明必须在 typescript 解释器的作用域内 +declare module 'fastify' { + interface FastifyRequest { // 引用的是接口而非类型 + someProp: string + } +} + +// 你也可以如此定义 request 的类型 +type CustomRequest = FastifyRequest<{ + Body: { test: boolean }; +}> + +server.get('/typedRequest', async (request: CustomRequest, reply: FastifyReply) => { + return request.body.test +}) +``` + +##### fastify.RequestGenericInterface +[源码](https://github.com/fastify/fastify/blob/main/types/request.d.ts#L4) + +Fastify 的请求对象有四个动态属性:`body`、`params`、`query` 以及 `headers`,它们对应的类型可以通过该接口设定。这是具名属性接口,允许开发者忽略他们不想指定的类型。所有忽略的属性默认为 `unknown`。四个属性名为:`Body`、`Querystring`、`Params` 和 `Headers`。 + +```typescript +import fastify, { RequestGenericInterface } from 'fastify' + +const server = fastify() + +interface requestGeneric extends RequestGenericInterface { + Querystring: { + name: string + } +} + +server.get('/', async (request, reply) => { + const { name } = request.query // 此时 query 属性上有了 name + return name.toUpperCase() +}) +``` + +在“从例子中学习”的 [JSON Schema](#jsonschema) 一节中,你能找到更具体的范例。 + +##### fastify.RawRequestDefaultExpression\<[RawServer][RawServerGeneric]\> +[源码](https://github.com/fastify/fastify/blob/main/types/utils.d.ts#L23) + +依赖于 `@types/node` 的模块 `http`、`https`、`http2` + +泛型参数 `RawServer` 的默认值为 [`RawServerDefault`][RawServerDefault] + +如果 `RawServer` 的类型为 `http.Server` 或 `https.Server`,那么该表达式返回 `http.IncomingMessage`,否则返回 `http2.Http2ServerRequest`。 + +```typescript +import http from 'http' +import http2 from 'http2' +import { RawRequestDefaultExpression } from 'fastify' + +RawRequestDefaultExpression // -> http.IncomingMessage +RawRequestDefaultExpression // -> http2.Http2ServerRequest +``` + +--- + +#### Reply + +##### fastify.FastifyReply<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]> +[源码](https://github.com/fastify/fastify/blob/main/types/reply.d.ts#L32) + +该接口包含了 Fastify 添加到 Node.js 标准的 reply 对象上的属性。这些属性和 reply 对象的类型 (http 或 http2) 无关。 + +假如你需要为 FastifyReply 对象添加自定义属性 (例如使用 `decorateReply` 方法时),你应该针对该接口应用声明合并。 + +在 [`FastifyReply`][FastifyReply] 里有基本的范例。更详细的例子请见“从例子中学习”的[插件](#plugins)一节。 + +###### 例子 +```typescript +import fastify from 'fastify' + +const server = fastify() + +server.decorateReply('someProp', 'world') + +server.get('/', async (request, reply) => { + const { someProp } = reply //需要通过声明合并将该属性添加到 reply 接口上 + return someProp +}) + +// 以下声明必须在 typescript 解释器的作用域内 +declare module 'fastify' { + interface FastifyReply { // 引用的是接口而非类型 + someProp: string + } +} +``` + +##### fastify.RawReplyDefaultExpression<[RawServer][RawServerGeneric]> +[源码](https://github.com/fastify/fastify/blob/main/types/utils.d.ts#L27) + +依赖于 `@types/node` 的模块 `http`、`https`、`http2` + +泛型参数 `RawServer` 的默认值为 [`RawServerDefault`][RawServerDefault] + +如果 `RawServer` 的类型为 `http.Server` 或 `https.Server`,那么该表达式返回 `http.ServerResponse`,否则返回 `http2.Http2ServerResponse`。 + +```typescript +import http from 'http' +import http2 from 'http2' +import { RawReplyDefaultExpression } from 'fastify' + +RawReplyDefaultExpression // -> http.ServerResponse +RawReplyDefaultExpression // -> http2.Http2ServerResponse +``` + +--- + +#### 插件 + +通过插件,用户能拓展 Fastify 的功能。一个插件可以是一组路由,也可以是一个装饰器,或其它逻辑。要激活一个插件,需调用 [`fastify.register()`][FastifyRegister] 方法。 + +创建插件时,我们推荐使用 `fastify-plugin`。在“从例子中学习”的[插件](#plugins)一节中有使用 TypeScript 创建插件的指南。 + +##### fastify.FastifyPluginCallback<[Options][FastifyPluginOptions]> +[源码](https://github.com/fastify/fastify/blob/main/types/plugin.d.ts#L9) + +[`fastify.register()`][FastifyRegister] 使用的接口方法定义。 + +##### fastify.FastifyPluginAsync<[Options][FastifyPluginOptions]> +[源码](https://github.com/fastify/fastify/blob/main/types/plugin.d.ts#L20) + +[`fastify.register()`][FastifyRegister] 使用的接口方法定义。 + +##### fastify.FastifyPlugin<[Options][FastifyPluginOptions]> +[源码](https://github.com/fastify/fastify/blob/main/types/plugin.d.ts#L29) + +[`fastify.register()`][FastifyRegister] 使用的接口方法定义。 + +通用的 `FastifyPlugin` 已不推荐使用,取而代之的是上述 `FastifyPluginCallback` 以及 `FastifyPluginAsync`。这是因为 `FastifyPlugin` 无法正确推断出异步函数的类型。 + +##### fastify.FastifyPluginOptions +[源码](https://github.com/fastify/fastify/blob/main/types/plugin.d.ts#L31) + +一个用于约束 [`fastify.register()`][FastifyRegister] 的 `options` 参数为对象类型的宽松类型对象 (loosely typed object)。在创建插件时,将插件的选项定义为此接口 (`interface MyPluginOptions extends FastifyPluginOptions`),传递给 register 方法。 + +--- + +#### Register + +##### fastify.FastifyRegister(plugin: [FastifyPluginCallback][FastifyPluginCallback], opts: [FastifyRegisterOptions][FastifyRegisterOptions]) +[源码](https://github.com/fastify/fastify/blob/main/types/register.d.ts#L9) +##### fastify.FastifyRegister(plugin: [FastifyPluginAsync][FastifyPluginAsync], opts: [FastifyRegisterOptions][FastifyRegisterOptions]) +[源码](https://github.com/fastify/fastify/blob/main/types/register.d.ts#L9) +##### fastify.FastifyRegister(plugin: [FastifyPlugin][FastifyPlugin], opts: [FastifyRegisterOptions][FastifyRegisterOptions]) +[源码](https://github.com/fastify/fastify/blob/main/types/register.d.ts#L9) + +指定 [`fastify.register()`](Server.md#register) 类型的类型接口,返回一个拥有默认值为 [FastifyPluginOptions][FastifyPluginOptions] 的 `Options` 泛型的函数签名。当调用此函数时,根据 FastifyPlugin 参数能推断出该泛型,因此不必特别指定。options 参数是插件选项以及 `prefix: string` 和 `logLevel` ([LogLevel][LogLevel]) 两个属性的交叉类型。 + +以下例子展示了 options 的推断: + +```typescript +const server = fastify() + +const plugin: FastifyPlugin<{ + option1: string; + option2: boolean; +}> = function (instance, opts, done) { } + +fastify().register(plugin, {}) // 错误 - options 对象缺失了必要的属性 +fastify().register(plugin, { option1: '', option2: true }) // OK - options 对象包括了必要的属性 +``` + +在“从例子中学习”的[插件](#plugins)一节中有使用 TypeScript 创建插件的详细示例。 + +##### fastify.FastifyRegisterOptions +[源码](https://github.com/fastify/fastify/blob/main/types/register.d.ts#L16) + +该类型是 `Options` 泛型以及包括 `prefix: string` 和 `logLevel` ([LogLevel][LogLevel]) 两个可选属性的未导出接口 `RegisterOptions` 的交叉类型。也可以被指定为返回前述交叉类型的函数。 + +--- + +#### 日志 + +请在[指定日志类型](#example-5-specifying-logger-types)的例子中,查阅自定义日志工具的细节。 + +##### fastify.FastifyLoggerOptions<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/logger.d.ts#L17) + +Fastify 内建日志工具的接口定义,模仿了 [Pino.js](https://getpino.io/#/) 的接口定义。当通过服务器选项启用日志时,参照[日志](Logging.md)文档使用它。 + +##### fastify.FastifyLogFn + +[源码](https://github.com/fastify/fastify/blob/main/types/logger.d.ts#L7) + +一个重载函数接口,实现 Fastify 调用日志的方法,会传递到所有 FastifyLoggerOptions 中启用的日志级别属性。 + +##### fastify.LogLevel + +[源码](https://github.com/fastify/fastify/blob/main/types/logger.d.ts#L12) + +`'info' | 'error' | 'debug' | 'fatal' | 'warn' | 'trace'` 的联合类型 + +--- + +#### Context + +context 类型定义和类型系统中其它高度动态化的部分类似。路由上下文 (context) 在路由函数内可用。 + +##### fastify.FastifyContext + +[源码](https://github.com/fastify/fastify/blob/main/types/context.d.ts#L6) + +有一个默认为 `unknown` 的必填属性 `config` 的接口。可用泛型或重载来指定。 + +此类型定义可能尚不完善。假如你有改进它的建议,欢迎在 [fastify/fastify](https://github.com/fastify/fastify) 仓库发布一个 issue。感谢! + +--- + +#### 路由 + +Fastify 的其中一条核心原则便是强大的路由。本节中多数的类型被 Fastify 实例的 `.route` 及 `.get/.post` 等方法内在地使用。 + +##### fastify.RouteHandlerMethod<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/route.d.ts#L105) + +路由控制函数的类型声明,有两个参数:类型为 `FastifyRequest` 的 `request`,以及类型为 `FastifyReply` 的 `reply`。泛型参数会传递给这些参数。当控制函数为同步函数时,返回 `void`,异步则返回 `Promise`。 + +##### fastify.RouteOptions<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/route.d.ts#L78) + +拓展了 RouteShorthandOptions 的接口,并添加以下三个必填属性: +1. `method` 单个或一组 [HTTP 方法][HTTPMethods]。 +2. `url` 路由路径字符串。 +3. `handler` 路由控制函数,详见 [RouteHandlerMethod][]。 + +##### fastify.RouteShorthandMethod<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/route.d.ts#12) + +一个重载函数接口,用于定义 `.get/.post` 等简写方法的三种不同形式。 + +##### fastify.RouteShorthandOptions<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/route.d.ts#55) + +包含所有路由基本选项的接口。所有属性都是可选的。该接口是 RouteOptions 和 RouteShorthandOptionsWithHandler 接口的基础。 + +##### fastify.RouteShorthandOptionsWithHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/route.d.ts#93) + +向 RouteShorthandOptions 接口添加一个必填属性:`handler`,类型为 RouteHandlerMethod。 + +--- + +#### Parsers + +##### RawBody + +一个为 `string` 或 `Buffer` 的泛型类型。 + +##### fastify.FastifyBodyParser<[RawBody][RawBodyGeneric], [RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/content-type-parser.d.ts#L7) + +定义 body 解析器 (body parser) 的函数类型。使用 `RawBody` 泛型指定被解析的 body。 + +##### fastify.FastifyContentTypeParser<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/content-type-parser.d.ts#L17) + +定义 body 解析器的函数类型。使用 `RawRequest` 泛型定义 content。 + +##### fastify.AddContentTypeParser<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric]> + +[源码](https://github.com/fastify/fastify/blob/main/types/content-type-parser.d.ts#L46) + +`addContentTypeParser` 方法的重载函数接口。当 `parseAs` 出现在 `opts` 参数中时,`parser` 参数使用 [FastifyBodyParser][],否则使用 [FastifyContentTypeParser][]。 + +##### fastify.hasContentTypeParser + +[源码](https://github.com/fastify/fastify/blob/main/types/content-type-parser.d.ts#L63) + +检查指定 content type 解析器是否存在的方法。 + +--- + +#### 错误 + +##### fastify.FastifyError + +[源码](https://github.com/fastify/fastify/blob/main/types/error.d.ts#L17) + +FastifyError 是自定义的错误对象,包括了状态码及校验结果。 + +拓展了 Node.js 的 `Error` 类型,并加入了两个可选属性:`statusCode: number` 和 `validation: ValiationResult[]`。 + +##### fastify.ValidationResult + +[源码](https://github.com/fastify/fastify/blob/main/types/error.d.ts#L4) + +路由校验内在地依赖于 Ajv,一个高性能 JSON schema 校验工具。 + +该接口会传递给 FastifyError 的实例。 + +--- + +#### 钩子 + +##### fastify.onRequestHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L17) + +`onRequest` 是第一个被执行的钩子,其下一个钩子为 `preParsing`。 + +注意:在 `onRequest` 钩子中,request.body 永远为 null,因为此时 body 尚未解析 (解析发生在 `preHandler` 钩子之前)。 + +##### fastify.preParsingHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L35) + +`preParsing` 是第二个钩子,前一个为 `onRequest`,下一个为 `preValidation`。 + +注意:在 `preParsing` 钩子中,request.body 永远为 null,因为此时 body 尚未解析 (解析发生在 `preValidation` 钩子之前)。 + +注意:你应当给返回的 stream 添加 `receivedEncodedLength` 属性。这是为了通过比对请求头的 `Content-Length`,来精确匹配请求的 payload。理想情况下,每收到一块数据都应该更新该属性。 + +##### fastify.preValidationHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L53) + +`preValidation` 是第三个钩子,前一个为 `preParsing`,下一个为 `preHandler`。 + +注意:在 `preValidation` 钩子中,request.body 永远为 null,因为此时 body 尚未解析 (解析发生在 `preHandler` 钩子之前)。 + +##### fastify.preHandlerHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L70) + +`preHandler` 是第四个钩子,前一个为 `preValidation`,下一个为 `preSerialization`。 + +##### fastify.preSerializationHookHandler(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], payload: PreSerializationPayload, done: (err: [FastifyError][FastifyError] | null, res?: unknown) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L94) + +`preSerialization` 是第五个钩子,前一个为 `preHandler`,下一个为 `onSend`。 + +注:当 payload 为 string、Buffer、stream 或 null 时,该钩子不会执行。 + +##### fastify.onSendHookHandler(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], payload: OnSendPayload, done: (err: [FastifyError][FastifyError] | null, res?: unknown) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L114) + +你可以在 `onSend` 钩子中变更 payload。这是第六个钩子,前一个为 `preSerialization`,下一个为 `onResponse`。 + +注:你只能将 payload 改为 string、Buffer、stream 或 null。 + +##### fastify.onResponseHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L134) + +`onResponse` 是第七个,也是最后一个钩子,前一个为 `onSend`。 + +该钩子在响应发出后执行,因此无法再发送更多数据了。但是你可以在此向外部服务发送数据,执行收集数据之类的任务。 + +##### fastify.onErrorHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(request: [FastifyRequest][FastifyRequest], reply: [FastifyReply][FastifyReply], error: [FastifyError][FastifyError], done: () => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L154) + +该钩子可用于自定义错误日志,或当发生错误时添加特定的 header。 + +该钩子并不是为了变更错误而设计的,且调用 reply.send 会抛出一个异常。 + +它只会在 customErrorHandler 向用户发送错误之后被执行 (要注意的是,默认的 customErrorHandler 总是会发送错误)。 + +注意:与其他钩子不同,该钩子不支持向 done 函数传递错误。 + +##### fastify.onRouteHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [RequestGeneric][FastifyRequestGenericInterface], [ContextConfig][ContextConfigGeneric]>(opts: [RouteOptions][RouteOptions] & { path: string; prefix: string }): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L174) + +当注册一个新的路由时被触发。它的监听函数拥有一个唯一的参数:routeOptions 对象。该接口是同步的,因此,监听函数不接受回调作为参数。 + +##### fastify.onRegisterHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [Logger][LoggerGeneric]>(instance: [FastifyInstance][FastifyInstance], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L191) + +当注册一个新的插件,或创建了新的封装好的上下文后被触发。该钩子在注册的代码之前被执行。 + +当你的插件需要知晓上下文何时创建完毕,并操作它们时,可以使用这一钩子。 + +注:被 fastify-plugin 所封装的插件不会触发该钩子。 + +##### fastify.onCloseHookHandler<[RawServer][RawServerGeneric], [RawRequest][RawRequestGeneric], [RawReply][RawReplyGeneric], [Logger][LoggerGeneric]>(instance: [FastifyInstance][FastifyInstance], done: (err?: [FastifyError][FastifyError]) => void): Promise\ | void + +[源码](https://github.com/fastify/fastify/blob/main/types/hooks.d.ts#L206) + +使用 fastify.close() 停止服务器时被触发。当插件需要一个 "shutdown" 事件时有用,例如关闭一个数据库连接。 + + + +[Fastify]: #fastifyrawserver-rawrequest-rawreply-loggeropts-fastifyserveroptions-fastifyinstance +[RawServerGeneric]: #rawserver +[RawRequestGeneric]: #rawrequest +[RawReplyGeneric]: #rawreply +[LoggerGeneric]: #logger +[RawBodyGeneric]: #rawbody +[HTTPMethods]: #fastifyhttpmethods +[RawServerBase]: #fastifyrawserverbase +[RawServerDefault]: #fastifyrawserverdefault +[FastifyRequest]: #fastifyfastifyrequestrawserver-rawrequest-requestgeneric +[FastifyRequestGenericInterface]: #fastifyrequestgenericinterface +[RawRequestDefaultExpression]: #fastifyrawrequestdefaultexpressionrawserver +[FastifyReply]: #fastifyfastifyreplyrawserver-rawreply-contextconfig +[RawReplyDefaultExpression]: #fastifyrawreplydefaultexpression +[FastifyServerOptions]: #fastifyfastifyserveroptions-rawserver-logger +[FastifyInstance]: #fastifyfastifyinstance +[FastifyLoggerOptions]: #fastifyfastifyloggeroptions +[ContextConfigGeneric]: #ContextConfigGeneric +[FastifyPlugin]: ##fastifyfastifypluginoptions-rawserver-rawrequest-requestgeneric +[FastifyPluginCallback]: #fastifyfastifyplugincallbackoptions +[FastifyPluginAsync]: #fastifyfastifypluginasyncoptions +[FastifyPluginOptions]: #fastifyfastifypluginoptions +[FastifyRegister]: #fastifyfastifyregisterrawserver-rawrequest-requestgenericplugin-fastifyplugin-opts-fastifyregisteroptions +[FastifyRegisterOptions]: #fastifyfastifytregisteroptions +[LogLevel]: #fastifyloglevel +[FastifyError]: #fastifyfastifyerror +[RouteOptions]: #fastifyrouteoptionsrawserver-rawrequest-rawreply-requestgeneric-contextconfig \ No newline at end of file diff --git a/doc/fastify-docs/docs/Validation-and-Serialization.md b/doc/fastify-docs/docs/Validation-and-Serialization.md new file mode 100644 index 0000000..eb88dd3 --- /dev/null +++ b/doc/fastify-docs/docs/Validation-and-Serialization.md @@ -0,0 +1,822 @@ +

Fastify

+ +## 验证和序列化 +Fastify 使用基于 schema 的途径,从本质上将 schema 编译成了高性能的函数,来实现路由的验证与输出的序列化。我们推荐使用 [JSON Schema](https://json-schema.org/),虽然这并非必要。 + +> ## ⚠ 安全须知 +> 应当将 schema 的定义写入代码。 +> 因为不管是验证还是序列化,都会使用 `new Function()` 来动态生成代码并执行。 +> 所以,用户提供的 schema 是不安全的。 +> 更多内容,请看 [Ajv](https://npm.im/ajv) 与 [fast-json-stringify](https://npm.im/fast-json-stringify)。 + +### 核心观念 +验证与序列化的任务分别由两个可定制的工具完成: +- [Ajv 6](https://www.npmjs.com/package/ajv/v/6.12.6) 用于验证请求。 +- [fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify) 用于序列化响应的 body。 + +这些工具相互独立,但共享通过 `.addSchema(schema)` 方法添加到 Fastify 实例上的 JSON schema。 + + +#### 添加共用 schema (shared schema) +得益于 `addSchema` API,你能向 Fastify 实例添加多个 schema,并在程序的不同部分复用它们。 +像往常一样,该 API 是封装好的。 + +共用 schema 可以通过 JSON Schema 的 [**`$ref`**](https://tools.ietf.org/html/draft-handrews-json-schema-01#section-8) 关键字复用。 +以下是引用方法的 _总结_: + ++ `myField: { $ref: '#foo'}` 将在当前 schema 内搜索 `$id: '#foo'` 字段。 ++ `myField: { $ref: '#/definitions/foo'}` 将在当前 schema 内搜索 `definitions.foo` 字段。 ++ `myField: { $ref: 'http://url.com/sh.json#'}` 会搜索含 `$id: 'http://url.com/sh.json'` 的共用 schema。 ++ `myField: { $ref: 'http://url.com/sh.json#/definitions/foo'}` 会搜索含 `$id: 'http://url.com/sh.json'` 的共用 schema,并使用其 `definitions.foo` 字段。 ++ `myField: { $ref: 'http://url.com/sh.json#foo'}` 会搜索含 `$id: 'http://url.com/sh.json'` 的共用 schema,并使用其内部带 `$id: '#foo'` 的对象。 + + +**简单用法:** + +```js +fastify.addSchema({ + $id: 'http://example.com/', + type: 'object', + properties: { + hello: { type: 'string' } + } +}) + +fastify.post('/', { + handler () {}, + schema: { + body: { + type: 'array', + items: { $ref: 'http://example.com#/properties/hello' } + } + } +}) +``` + +**`$ref` 作为根引用 (root reference):** + +```js +fastify.addSchema({ + $id: 'commonSchema', + type: 'object', + properties: { + hello: { type: 'string' } + } +}) + +fastify.post('/', { + handler () {}, + schema: { + body: { $ref: 'commonSchema#' }, + headers: { $ref: 'commonSchema#' } + } +}) +``` + + +#### 获取共用 schema + +当自定义验证器或序列化器的时候,Fastify 不再能控制它们,此时 `.addSchema` 方法失去了作用。 +要获取添加到 Fastify 实例上的 schema,你可以使用 `.getSchemas()`: + +```js +fastify.addSchema({ + $id: 'schemaId', + type: 'object', + properties: { + hello: { type: 'string' } + } +}) + +const mySchemas = fastify.getSchemas() +const mySchema = fastify.getSchema('schemaId') +``` + +`getSchemas` 方法也是封装好的,返回的是指定作用域中可用的共用 schema: + +```js +fastify.addSchema({ $id: 'one', my: 'hello' }) +// 只返回 schema `one` +fastify.get('/', (request, reply) => { reply.send(fastify.getSchemas()) }) + +fastify.register((instance, opts, done) => { + instance.addSchema({ $id: 'two', my: 'ciao' }) + // 会返回 schema `one` 与 `two` + instance.get('/sub', (request, reply) => { reply.send(instance.getSchemas()) }) + + instance.register((subinstance, opts, done) => { + subinstance.addSchema({ $id: 'three', my: 'hola' }) + // 会返回 schema `one`、`two` 和 `three` + subinstance.get('/deep', (request, reply) => { reply.send(subinstance.getSchemas()) }) + done() + }) + done() +}) +``` + +### 验证 +路由的验证是依赖 [Ajv 6](https://www.npmjs.com/package/ajv/v/6.12.6) 实现的。这是一个高性能的 JSON schema 校验工具。验证输入十分简单,只需将字段加入路由的 schema 中即可! + +支持的验证类型如下: +- `body`:当请求方法为 POST、PUT 或 PATCH 时,验证 body。 +- `querystring` 或 `query`:验证 querystring。 +- `params`:验证路由参数。 +- `headers`:验证 header。 + +所有的验证都可以是一个完整的 JSON Schema 对象 (包括值为 `object` 的 `type` 属性以及包含参数的 `properties` 对象),也可以是一个没有 `type` 与 `properties`,而仅仅在顶层列明参数的简单变种 (见下文示例)。 + +> ℹ 想要使用最新版 Ajv (Ajv 8) 的话,请查阅 [`schemaController`](Server.md#schema-controller) 一节,里边描述了比自定义校验器更简单的方法。 + +示例: +```js +const bodyJsonSchema = { + type: 'object', + required: ['requiredKey'], + properties: { + someKey: { type: 'string' }, + someOtherKey: { type: 'number' }, + requiredKey: { + type: 'array', + maxItems: 3, + items: { type: 'integer' } + }, + nullableKey: { type: ['number', 'null'] }, // 或 { type: 'number', nullable: true } + multipleTypesKey: { type: ['boolean', 'number'] }, + multipleRestrictedTypesKey: { + oneOf: [ + { type: 'string', maxLength: 5 }, + { type: 'number', minimum: 10 } + ] + }, + enumKey: { + type: 'string', + enum: ['John', 'Foo'] + }, + notTypeKey: { + not: { type: 'array' } + } + } +} + +const queryStringJsonSchema = { + type: 'object', + properties: { + name: { type: 'string' }, + excitement: { type: 'integer' } + } +} + +const paramsJsonSchema = { + type: 'object', + properties: { + par1: { type: 'string' }, + par2: { type: 'number' } + } +} + +const headersJsonSchema = { + type: 'object', + properties: { + 'x-foo': { type: 'string' } + }, + required: ['x-foo'] +} + +const schema = { + body: bodyJsonSchema, + querystring: queryStringJsonSchema, + params: paramsJsonSchema, + headers: headersJsonSchema +} + +fastify.post('/the/url', { schema }, handler) +``` + +*请注意,为了通过校验,并在后续过程中使用正确类型的数据,Ajv 会尝试将数据[隐式转换](https://github.com/epoberezkin/ajv#coercing-data-types)为 schema 中 `type` 属性指明的类型。* + +Fastify 提供给 Ajv 的默认配置并不支持隐式转换 querystring 中的数组参数。但是,Fastify 允许你通过设置 Ajv 实例的 [`customOptions`](Server.md#ajv) 选项为 'array',来将参数转换为数组。举例如下: + +```js +const opts = { + schema: { + querystring: { + type: 'object', + properties: { + ids: { + type: 'array', + default: [] + }, + }, + } + } +} + +fastify.get('/', opts, (request, reply) => { + reply.send({ params: request.query }) +}) + +fastify.listen(3000, (err) => { + if (err) throw err +}) +``` + +默认情况下,该处的请求将返回 `400`: + +```sh +curl -X GET "http://localhost:3000/?ids=1 + +{"statusCode":400,"error":"Bad Request","message":"querystring/hello should be array"} +``` + +设置 `coerceTypes` 的值为 'array' 将修复该问题: + +```js +const ajv = new Ajv({ + removeAdditional: true, + useDefaults: true, + coerceTypes: 'array', // 看这里 + allErrors: true +}) + +fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { + return ajv.compile(schema) +}) +``` + +```sh +curl -X GET "http://localhost:3000/?ids=1 + +{"params":{"hello":["1"]}} +``` + +你还可以给每个参数类型 (body, query string, param, header) 都自定义 schema 校验器。 + +下面的例子改变了 ajv 的默认选项,禁用了 `body` 的强制类型转换。 + +```js +const schemaCompilers = { + body: new Ajv({ + removeAdditional: false, + coerceTypes: false, + allErrors: true + }), + params: new Ajv({ + removeAdditional: false, + coerceTypes: true, + allErrors: true + }), + querystring: new Ajv({ + removeAdditional: false, + coerceTypes: true, + allErrors: true + }), + headers: new Ajv({ + removeAdditional: false, + coerceTypes: true, + allErrors: true + }) +} + +server.setValidatorCompiler(req => { + if (!req.httpPart) { + throw new Error('Missing httpPart') + } + const compiler = schemaCompilers[req.httpPart] + if (!compiler) { + throw new Error(`Missing compiler for ${req.httpPart}`) + } + return compiler.compile(req.schema) +}) +``` + +更多信息请看[这里](https://ajv.js.org/coercion.html)。 + + +#### Ajv 插件 + +你可以给默认的 `ajv` 实例提供一组插件。这些插件必须**兼容 Ajv 6**。 + +> 插件格式参见 [`ajv 选项`](Server.md#ajv) + +```js +const fastify = require('fastify')({ + ajv: { + plugins: [ + require('ajv-merge-patch') + ] + } +}) + +fastify.post('/', { + handler (req, reply) { reply.send({ ok: 1 }) }, + schema: { + body: { + $patch: { + source: { + type: 'object', + properties: { + q: { + type: 'string' + } + } + }, + with: [ + { + op: 'add', + path: '/properties/q', + value: { type: 'number' } + } + ] + } + } + } +}) + +fastify.post('/foo', { + handler (req, reply) { reply.send({ ok: 1 }) }, + schema: { + body: { + $merge: { + source: { + type: 'object', + properties: { + q: { + type: 'string' + } + } + }, + with: { + required: ['q'] + } + } + } + } +}) +``` + + +#### 验证生成器 + +`validatorCompiler` 返回一个用于验证 body、URL、路由参数、header 以及 querystring 的函数。默认返回一个实现了 [ajv](https://ajv.js.org/) 验证接口的函数。Fastify 内在地使用该函数以加速验证。 + +Fastify 使用的 [ajv 基本配置](https://github.com/epoberezkin/ajv#options-to-modify-validated-data)如下: + +```js +{ + removeAdditional: true, // 移除额外属性 + useDefaults: true, // 当属性或项目缺失时,使用 schema 中预先定义好的 default 的值代替 + coerceTypes: true, // 根据定义的 type 的值改变数据类型 + nullable: true // 支持 OpenAPI Specification 3.0 版本的 "nullable" 关键字 +} +``` + +上述配置可通过 [`ajv.customOptions`](Server.md#factory-ajv) 修改。 + +假如你想改变或增加额外的选项,你需要创建一个自定义的实例,并覆盖已存在的实例: + +```js +const fastify = require('fastify')() +const Ajv = require('ajv') +const ajv = new Ajv({ + // fastify 使用的默认参数(如果需要) + removeAdditional: true, + useDefaults: true, + coerceTypes: true, + nullable: true, + // 任意其他参数 + // ... +}) +fastify.setValidatorCompiler(({ schema, method, url, httpPart }) => { + return ajv.compile(schema) +}) +``` + +_**注意:** 如果你使用自定义校验工具的实例(即使是 Ajv),你应当向该实例而非 Fastify 添加 schema,因为在这种情况下,Fastify 默认的校验工具不再使用,而 `addSchema` 方法也不清楚你在使用什么工具进行校验。_ + + +##### 使用其他验证工具 + +通过 `setValidatorCompiler` 函数,你可以轻松地将 `ajv` 替换为几乎任意的 Javascript 验证工具 (如 [joi](https://github.com/hapijs/joi/)、[yup](https://github.com/jquense/yup/) 等),或自定义它们。 + +```js +const Joi = require('@hapi/joi') + +fastify.post('/the/url', { + schema: { + body: Joi.object().keys({ + hello: Joi.string().required() + }).required() + }, + validatorCompiler: ({ schema, method, url, httpPart }) => { + return data => schema.validate(data) + } +}, handler) +``` + +```js +const yup = require('yup') +// 等同于前文 ajv 基本配置的 yup 的配置 +const yupOptions = { + strict: false, + abortEarly: false, // 返回所有错误(译注:为 true 时出现首个错误后即返回) + stripUnknown: true, // 移除额外属性 + recursive: true +} +fastify.post('/the/url', { + schema: { + body: yup.object({ + age: yup.number().integer().required(), + sub: yup.object().shape({ + name: yup.string().required() + }).required() + }) + }, + validatorCompiler: ({ schema, method, url, httpPart }) => { + return function (data) { + // 当设置 strict = false 时, yup 的 `validateSync` 函数在验证成功后会返回经过转换的值,而失败时则会抛错。 + try { + const result = schema.validateSync(data, yupOptions) + return { value: result } + } catch (e) { + return { error: e } + } + } + } +}, handler) +``` + +##### 其他验证工具的验证信息 + +Fastify 的错误验证与其默认的验证引擎 `ajv` 紧密结合,错误最终会经由 `schemaErrorsText` 函数转化为便于阅读的信息。然而,也正是由于 `schemaErrorsText` 与 `ajv` 的强关联性,当你使用其他校验工具时,可能会出现奇怪或不完整的错误信息。 + +要规避以上问题,主要有两个途径: + +1. 确保自定义的 `schemaCompiler` 返回的错误结构与 `ajv` 的一致 (当然,由于各引擎的差异,这是件困难的活儿)。 +2. 使用自定义的 `errorHandler` 拦截并格式化验证错误。 + +Fastify 给所有的验证错误添加了两个属性,来帮助你自定义 `errorHandler`: + +* validation:来自 `schemaCompiler` 函数的验证函数所返回的对象上的 `error` 属性的内容。 +* validationContext:验证错误的上下文 (body、params、query、headers)。 + +以下是一个自定义 `errorHandler` 来处理验证错误的例子: + +```js +const errorHandler = (error, request, reply) => { + const statusCode = error.statusCode + let response + + const { validation, validationContext } = error + + // 检验是否发生了验证错误 + if (validation) { + response = { + // validationContext 的值可能是 'body'、'params'、'headers' 或 'query' + message: `A validation error occured when validating the ${validationContext}...`, + // 验证工具返回的结果 + errors: validation + } + } else { + response = { + message: 'An error occurred...' + } + } + + // 其余代码。例如,记录错误日志。 + // ... + + reply.status(statusCode).send(response) +} +``` + + +### 序列化 +通常,你会通过 JSON 格式将数据发送至客户端。鉴于此,Fastify 提供了一个强大的工具——[fast-json-stringify](https://www.npmjs.com/package/fast-json-stringify) 来帮助你。当你在路由选项中提供了输出的 schema 时,它能派上用场。 +我们推荐你编写一个输出的 schema,因为这能让应用的吞吐量提升 100-400% (根据 payload 的不同而有所变化),也能防止敏感信息的意外泄露。 + +示例: +```js +const schema = { + response: { + 200: { + type: 'object', + properties: { + value: { type: 'string' }, + otherValue: { type: 'boolean' } + } + } + } +} + +fastify.post('/the/url', { schema }, handler) +``` + +如你所见,响应的 schema 是建立在状态码的基础之上的。当你想对多个状态码使用同一个 schema 时,你可以使用类似 `'2xx'` 的表达方法,例如: +```js +const schema = { + response: { + '2xx': { + type: 'object', + properties: { + value: { type: 'string' }, + otherValue: { type: 'boolean' } + } + }, + 201: { + // 对比写法 + value: { type: 'string' } + } + } +} + +fastify.post('/the/url', { schema }, handler) +``` + + +#### 序列化函数生成器 + +`serializerCompiler` 返回一个根据输入参数返回字符串的函数。你应该提供一个函数,用于序列化所有定义了 `response` JSON Schema 的路由。 + +```js +fastify.setSerializerCompiler(({ schema, method, url, httpStatus }) => { + return data => JSON.stringify(data) +}) + +fastify.get('/user', { + handler (req, reply) { + reply.send({ id: 1, name: 'Foo', image: 'BIG IMAGE' }) + }, + schema: { + response: { + '2xx': { + id: { type: 'number' }, + name: { type: 'string' } + } + } + } +}) +``` + +*假如你需要在特定位置使用自定义的序列化工具,你可以使用 [`reply.serializer(...)`](Reply.md#serializerfunc)。* + +### 错误控制 +当某个请求 schema 校验失败时,Fastify 会自动返回一个包含校验结果的 400 响应。举例来说,假如你的路由有一个如下的 schema: + ```js +const schema = { + body: { + type: 'object', + properties: { + name: { type: 'string' } + }, + required: ['name'] + } +} +``` +当校验失败时,路由会立即返回一个包含以下内容的响应: + ```js +{ + "statusCode": 400, + "error": "Bad Request", + "message": "body should have required property 'name'" +} +``` + +如果你想在路由内部控制错误,可以设置 `attachValidation` 选项。当出现 _验证错误_ 时,请求的 `validationError` 属性将会包含一个 `Error` 对象,在这对象内部有原始的验证结果 `validation`,如下所示: + ```js +const fastify = Fastify() + fastify.post('/', { schema, attachValidation: true }, function (req, reply) { + if (req.validationError) { + // `req.validationError.validation` 包含了原始的验证错误信息 + reply.code(400).send(req.validationError) + } +}) +``` + +#### `schemaErrorFormatter` + +如果你需要自定义错误的格式化,可以给 Fastify 实例化时的选项添加 `schemaErrorFormatter`,其值为返回一个错误的同步函数。函数的 this 指向 Fastify 服务器实例。 + +`errors` 是 Fastify schema 错误 (`FastifySchemaValidationError`) 的一个数组。 +`dataVar` 是当前验证的 schema 片段 (params | body | querystring | headers)。 + +```js +const fastify = Fastify({ + schemaErrorFormatter: (errors, dataVar) => { + // ... 自定义的格式化逻辑 + return new Error(myErrorMessage) + } +}) + +// 或 +fastify.setSchemaErrorFormatter(function (errors, dataVar) { + this.log.error({ err: errors }, 'Validation failed') + // ... 自定义的格式化逻辑 + return new Error(myErrorMessage) +}) +``` + +你还可以使用 [setErrorHandler](https://www.fastify.io/docs/latest/Server/#seterrorhandler) 方法来自定义一个校验错误响应,如下: + ```js +fastify.setErrorHandler(function (error, request, reply) { + if (error.validation) { + // error.validationContext 是 [body, params, querystring, headers] 之中的值 + reply.status(422).send(new Error(`validation failed of the ${error.validationContext}`)) + } +}) +``` + +假如你想轻松愉快地自定义错误响应,请查看 [`ajv-errors`](https://github.com/epoberezkin/ajv-errors)。具体的例子可以移步[这里](https://github.com/fastify/example/blob/HEAD/validation-messages/custom-errors-messages.js)。 + + +下面的例子展示了如何通过自定义 AJV,为 schema 的**每个属性添加自定义错误信息**。 +其中的注释描述了在不同场景下设置不同信息的方法。 + +```js +const fastify = Fastify({ + ajv: { + customOptions: { jsonPointers: true }, + plugins: [ + require('ajv-errors') + ] + } +}) + +const schema = { + body: { + type: 'object', + properties: { + name: { + type: 'string', + errorMessage: { + type: 'Bad name' + } + }, + age: { + type: 'number', + errorMessage: { + type: 'Bad age', // 为除了必填外的所有限制 + min: 'Too young' // 自定义错误信息 + } + } + }, + required: ['name', 'age'], + errorMessage: { + required: { + name: 'Why no name!', // 为必填设置 + age: 'Why no age!' // 错误信息 + } + } + } +} + +fastify.post('/', { schema, }, (request, reply) => { + reply.send({ + hello: 'world' + }) +}) +``` + +想要本地化错误信息,请看 [ajv-i18n](https://github.com/epoberezkin/ajv-i18n) + +```js +const localize = require('ajv-i18n') + +const fastify = Fastify() + +const schema = { + body: { + type: 'object', + properties: { + name: { + type: 'string', + }, + age: { + type: 'number', + } + }, + required: ['name', 'age'], + } +} + +fastify.setErrorHandler(function (error, request, reply) { + if (error.validation) { + localize.ru(error.validation) + reply.status(400).send(error.validation) + return + } + reply.send(error) +}) +``` + +### JSON Schema 支持 + +为了能更简单地重用 schema,JSON Schema 提供了一些功能,来结合 Fastify 的共用 schema。 + +| 用例 | 验证器 | 序列化器 | +|-----------------------------------|-----------|------------| +| 引用 (`$ref`) `$id` | ✔ | ✔️ | +| 引用 (`$ref`) `/definitions` | ✔️ | ✔️ | +| 引用 (`$ref`) 共用 schema `$id` | ✔ | ✔️ | +| 引用 (`$ref`) 共用 schema `/definitions` | ✔ | ✔️ | + +#### 示例 + +##### 同一个 JSON Schema 中对 `$id` 的引用 ($ref) + +```js +const refToId = { + type: 'object', + definitions: { + foo: { + $id: '#address', + type: 'object', + properties: { + city: { type: 'string' } + } + } + }, + properties: { + home: { $ref: '#address' }, + work: { $ref: '#address' } + } +} +``` + +##### 同一个 JSON Schema 中对 `/definitions` 的引用 ($ref) +```js +const refToDefinitions = { + type: 'object', + definitions: { + foo: { + $id: '#address', + type: 'object', + properties: { + city: { type: 'string' } + } + } + }, + properties: { + home: { $ref: '#/definitions/foo' }, + work: { $ref: '#/definitions/foo' } + } +} +``` + +##### 对外部共用 schema 的 `$id` 的引用 ($ref) +```js +fastify.addSchema({ + $id: 'http://foo/common.json', + type: 'object', + definitions: { + foo: { + $id: '#address', + type: 'object', + properties: { + city: { type: 'string' } + } + } + } +}) + +const refToSharedSchemaId = { + type: 'object', + properties: { + home: { $ref: 'http://foo/common.json#address' }, + work: { $ref: 'http://foo/common.json#address' } + } +} +``` + +##### 对外部共用 schema 的 `/definitions` 的引用 ($ref) +```js +fastify.addSchema({ + $id: 'http://foo/shared.json', + type: 'object', + definitions: { + foo: { + type: 'object', + properties: { + city: { type: 'string' } + } + } + } +}) + +const refToSharedSchemaDefinitions = { + type: 'object', + properties: { + home: { $ref: 'http://foo/shared.json#/definitions/foo' }, + work: { $ref: 'http://foo/shared.json#/definitions/foo' } + } +} +``` + + +### 资源 +- [JSON Schema](https://json-schema.org/) +- [理解 JSON Schema](https://spacetelescope.github.io/understanding-json-schema/) +- [fast-json-stringify 文档](https://github.com/fastify/fast-json-stringify) +- [Ajv 文档](https://github.com/epoberezkin/ajv/blob/master/README.md) +- [Ajv i18n](https://github.com/epoberezkin/ajv-i18n) +- [Ajv 自定义错误](https://github.com/epoberezkin/ajv-errors) +- 使用核心方法自定义错误处理,并实现错误文件转储的[例子](https://github.com/fastify/example/tree/main/validation-messages) \ No newline at end of file diff --git a/doc/fastify-docs/docs/Write-Plugin.md b/doc/fastify-docs/docs/Write-Plugin.md new file mode 100644 index 0000000..08483a7 --- /dev/null +++ b/doc/fastify-docs/docs/Write-Plugin.md @@ -0,0 +1,59 @@ +

Fastify

+ +# 如何写一个好的插件 +首先,要感谢你决定为 Fastify 编写插件。Fastify 本身是一个极简的框架,插件才是它强大功能的来源,所以,谢谢你。
+Fastify 的核心原则是高性能、低成本、提供优秀的用户体验。当编写插件时,这些原则应当被遵循。因此,本文我们将会分析一个优质的插件所具有的特征。 + +*需要一些灵感?你可以在 issue 中使用 ["plugin suggestion"](https://github.com/fastify/fastify/issues?q=is%3Aissue+is%3Aopen+label%3A%22plugin+suggestion%22) 标签!* + +## 代码 +Fastify 运用了不同的技术来优化代码,其大部分都被写入了文档。我们强烈建议你阅读 [插件指南](Plugins-Guide.md) 一文,以了解所有可用于构建插件的 API 及其用法。 + +存有疑虑或寻求建议?我们非常高兴能帮助你!只需在我们的 [求助仓库](https://github.com/fastify/help) 提一个 issue 即可! + +一旦你向我们的 [生态列表](https://github.com/fastify/fastify/blob/main/docs/Ecosystem.md) 提交了一个插件,我们将会检查你的代码,需要时也会帮忙改进它。 + +## 文档 +文档相当重要。假如你的插件没有好的文档,我们将拒绝将其加入生态列表。缺乏良好的文档会提升用户使用插件的难度,并有可能导致弃用。
+以下列出了一些优秀插件文档的示例: +- [`fastify-caching`](https://github.com/fastify/fastify-caching) +- [`fastify-compress`](https://github.com/fastify/fastify-compress) +- [`fastify-cookie`](https://github.com/fastify/fastify-cookie) +- [`point-of-view`](https://github.com/fastify/point-of-view) +- [`under-pressure`](https://github.com/fastify/under-pressure) + +## 许可证 +你可以为你的插件使用自己偏好的许可,我们不会强求。
+我们推荐 [MIT 许可证](https://choosealicense.com/licenses/mit/),因为我们认为它允许更多人自由地使用代码。其他可替代的许可证参见 [OSI list](https://opensource.org/licenses) 或 GitHub 的 [choosealicense.com](https://choosealicense.com/)。 + +## 示例 +总在你的仓库里添加一个示例文件。这对于用户是相当有帮助的,也提供了一个快速的手段来测试你的插件。使用者们会为此感激的。 + +## 测试 +彻底地测试一个插件,来验证其是否正常执行,是极为重要的。
+缺乏测试会影响用户的信任感,也无法保证代码在不同版本的依赖下还能正常工作。 + +我们不强求使用某一测试工具。我们使用的是 [`tap`](https://www.node-tap.org/),因为它提供了开箱即用的并行测试以及代码覆盖率检测。 + +## 代码检查 +这一项不是强制的,但我们强烈推荐你在插件中使用一个代码检查工具。这可以帮助你保持统一的代码风格,同时避免许多错误。 + +我们使用 [`standard`](https://standardjs.com/),因为它不需任何配置,并且容易与测试集成。 + +## 持续集成 +这一项也不是强制的,但假如你开源发布你的代码,持续集成能保证其他人的参与不会破坏你的插件,并检查插件是否如预期般工作。[CircleCI](https://circleci.com/) 和 [GitHub Actions](https://github.com/features/actions) 都是对开源项目免费的持续集成系统,且易于安装配置。
+此外,你还可以启用 [Dependabot](https://dependabot.com/) 或 [Snyk](https://snyk.io/) 等服务,它可以帮你将依赖保持在最新版本,并检查在 Fastify 的新版本上你的插件是否存在问题。 + +## 让我们开始吧! +棒极了,现在你已经了解了如何为 Fastify 写一个好插件! +当你完成了一个插件(或更多)之后,请让我们知道!我们会将其添加到 [生态](https://github.com/fastify/fastify#ecosystem) 一节中! + +想看更多真实的例子?请参阅: +- [`point-of-view`](https://github.com/fastify/point-of-view) +Fastify 的模板渲染 (*ejs, pug, handlebars, marko*) 插件。 +- [`fastify-mongodb`](https://github.com/fastify/fastify-mongodb) +Fastify 的 MongoDB 连接插件,通过它你可以在你服务器的每个部分共享同一个 MongoDB 连接池。 +- [`fastify-multipart`](https://github.com/fastify/fastify-multipart) +为 Fastify 提供 mltipart 支持。 +- [`fastify-helmet`](https://github.com/fastify/fastify-helmet) +重要的请求头安全插件。 \ No newline at end of file diff --git a/doc/fastify-docs/docs/resources/encapsulation_context.drawio b/doc/fastify-docs/docs/resources/encapsulation_context.drawio new file mode 100644 index 0000000..eb6952b --- /dev/null +++ b/doc/fastify-docs/docs/resources/encapsulation_context.drawio @@ -0,0 +1 @@ +7ZpPk5owGMY/jcd2SCKIx0q3todOO+thjzsRAmQ2Eopx1f30DRKUNLCrncXN6uKMA0/+EN5feJNndICCxWZa4Dz9ySPCBtCJNgP0dQDhaDiS36WwrQQfOJWQFDSqJHAQZvSJKLGutqIRWWoVBedM0FwXQ55lJBSahouCr/VqMWf6XXOcEEOYhZiZ6h2NRKqewnUO+ndCk7S+M3BUyQLXlZWwTHHE1w0J3QxQUHAuqrPFJiCsjF0dl6rdt47S/cAKkoljGjyhO8+Z3U9YQP3bP1PHvc9+fFK9PGK2Ug98Ww4IOgHPBNkINXSxreOxTqkgsxyH5fVaIh+gSSoWTF4BeYqXeUUhphsibzyJKWMBZ7zYNUexW36k/kgKQWWYvzCaZLJM8LKngq+yqGy362wpCv5AGq293VH2KkfX0KtD6mZM6geUtyObhqRiNCV8QUSxlVVU6VDh2uqX6wN8d6y0tAF+L2I14ZJ9zwcm8kRhOQER6kL0m60SmhmE5DzLy9O84CFZytFMXmA2x+FDsov8r5VgNCNK12H8CzKOYRi2QYq8ued6rwMDOq5Gwzdp+C0wUF8shtfMYmwXC/eKWSDfLhbATFJG/MtQ5sc/+35Jx/O6B+fZmABPz9wAmUFBXktU+guLZ0QlSKncgHQvr/rcemGCdk299rX1lAn5POSXp+nbhXzUGfI3TQsRJn7cmha80CfzuBcKdan+XrS8FmfNFf4HIZOQXYjGBiJp7rIo7DF57cN/ZPLqHwrUmdQrbjO1wRYoALp9LbKOnW8OAZFLRm2Qxt4I4X5WmNqtjT7rOyEA3norZFrqq6cEkH2Y4AcmI+MhaB0m03EbYM5tLFCL2zrzLheY5vddO4thByubnAXodnOXsnE9HoOd1gJcvvv7D0SWMTL93/s3FydTsc9cmJ7PilfnFfdDJ0Oyz1zAy7eApyc4+8wFvHwPeHrGO6e5kJeHH9Z3ZY1/J6Cbvw== \ No newline at end of file diff --git a/doc/fastify-docs/docs/resources/encapsulation_context.svg b/doc/fastify-docs/docs/resources/encapsulation_context.svg new file mode 100644 index 0000000..c965948 --- /dev/null +++ b/doc/fastify-docs/docs/resources/encapsulation_context.svg @@ -0,0 +1,3 @@ + + +
Root Context
Root Context
Root Plugin
Root Plugin
Root Plugin
Root Plugin
Root Plugin
Root Plugin
Child Context
Child Context
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Grandchild Context
Grandchild Context
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Child Context
Child Context
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Grandchild Context
Grandchild Context
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Child Plugin
Child Plug...
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..09c34bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1330 @@ +{ + "name": "yuheng", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "yuheng", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.7", + "fastify": "^5.2.1", + "fastify-plugin": "^5.0.1", + "pino": "^9.6.0", + "pino-multi-stream": "^6.0.0", + "pino-pretty": "^13.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^3.1.9" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.2", + "resolved": "https://registry.npmmirror.com/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz", + "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/@fastify/error/-/error-4.0.0.tgz", + "integrity": "sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==", + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.2", + "resolved": "https://registry.npmmirror.com/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.2.tgz", + "integrity": "sha512-YdR7gqlLg1xZAQa+SX4sMNzQHY5pC54fu9oC5aYSUqBhyn6fkLkrdtKlpVdCNPlwuUuXA1PjFTEmvMF6ZVXVGw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/@fastify/forwarded/-/forwarded-3.0.0.tgz", + "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==", + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmmirror.com/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz", + "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==", + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.1.0", + "resolved": "https://registry.npmmirror.com/avvio/-/avvio-9.1.0.tgz", + "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==", + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmmirror.com/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmmirror.com/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmmirror.com/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmmirror.com/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "license": "MIT" + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz", + "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^2.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmmirror.com/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.6", + "resolved": "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.0.6.tgz", + "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/fastify/-/fastify-5.2.1.tgz", + "integrity": "sha512-rslrNBF67eg8/Gyn7P2URV8/6pz8kSAscFL4EThZJ8JBMaXacVdVE4hmUcnPNKERl5o/xTiBSLfdowBRhVF1WA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.0", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.0.0", + "process-warning": "^4.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^3.0.1", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-my-way": { + "version": "9.2.0", + "resolved": "https://registry.npmmirror.com/find-my-way/-/find-my-way-9.2.0.tgz", + "integrity": "sha512-d3uCir8Hmg7W1Ywp8nKf2lJJYU9Nwinvo+1D39Dn09nz65UKXIxUh7j7K8zeWhxqe1WrkS7FJyON/Q/3lPoc6w==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "license": "MIT" + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmmirror.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz", + "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmmirror.com/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nodemon": { + "version": "3.1.9", + "resolved": "https://registry.npmmirror.com/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.6.0", + "resolved": "https://registry.npmmirror.com/pino/-/pino-9.6.0.tgz", + "integrity": "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^4.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-multi-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/pino-multi-stream/-/pino-multi-stream-6.0.0.tgz", + "integrity": "sha512-oCuTtaDSUB5xK1S45r9oWE0Dj8RWdHVvaGTft5pO/rmzgIqQRkilf5Ooilz3uRm0IYj8sPRho3lVx48LCmXjvQ==", + "deprecated": "No longer supported. Use the multi-stream support in the latest core Pino", + "license": "MIT", + "dependencies": { + "pino": "^7.0.0" + } + }, + "node_modules/pino-multi-stream/node_modules/on-exit-leak-free": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/on-exit-leak-free/-/on-exit-leak-free-0.2.0.tgz", + "integrity": "sha512-dqaz3u44QbRXQooZLTUKU41ZrzYrcvLISVgbrzbyCMxpmSLJvZ3ZamIJIZ29P6OhZIkNIQKosdeM6t1LYbA9hg==", + "license": "MIT" + }, + "node_modules/pino-multi-stream/node_modules/pino": { + "version": "7.11.0", + "resolved": "https://registry.npmmirror.com/pino/-/pino-7.11.0.tgz", + "integrity": "sha512-dMACeu63HtRLmCG8VKdy4cShCPKaYDR4youZqoSWLxl5Gu99HUw8bw75thbPv9Nip+H+QYX8o3ZJbTdVZZ2TVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.0.0", + "on-exit-leak-free": "^0.2.0", + "pino-abstract-transport": "v0.5.0", + "pino-std-serializers": "^4.0.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.1.0", + "safe-stable-stringify": "^2.1.0", + "sonic-boom": "^2.2.1", + "thread-stream": "^0.15.1" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-multi-stream/node_modules/pino-abstract-transport": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/pino-abstract-transport/-/pino-abstract-transport-0.5.0.tgz", + "integrity": "sha512-+KAgmVeqXYbTtU2FScx1XS3kNyfZ5TrXY07V96QnUSFqo2gAqlvmaxH67Lj7SWazqsMabf+58ctdTcBgnOLUOQ==", + "license": "MIT", + "dependencies": { + "duplexify": "^4.1.2", + "split2": "^4.0.0" + } + }, + "node_modules/pino-multi-stream/node_modules/pino-std-serializers": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-4.0.0.tgz", + "integrity": "sha512-cK0pekc1Kjy5w9V2/n+8MkZwusa6EyyxfeQCB799CQRhRt/CqYKiWs5adeu8Shve2ZNffvfC/7J64A2PJo1W/Q==", + "license": "MIT" + }, + "node_modules/pino-multi-stream/node_modules/process-warning": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-1.0.0.tgz", + "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", + "license": "MIT" + }, + "node_modules/pino-multi-stream/node_modules/real-require": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.1.0.tgz", + "integrity": "sha512-r/H9MzAWtrv8aSVjPCMFpDMl5q66GqtmmRkRjpHTsp4zBAa+snZyiQNlMONiUmEJcsnaw0wCauJ2GWODr/aFkg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/pino-multi-stream/node_modules/sonic-boom": { + "version": "2.8.0", + "resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-2.8.0.tgz", + "integrity": "sha512-kuonw1YOYYNOve5iHdSahXPOK49GqwA+LZhI6Wz/l0rP57iKyXXIHaRagOBHAPmGwJC6od2Z9zgvZ5loSgMlVg==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/pino-multi-stream/node_modules/thread-stream": { + "version": "0.15.2", + "resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-0.15.2.tgz", + "integrity": "sha512-UkEhKIg2pD+fjkHQKyJO3yoIvAP3N6RlNFt2dUhcS1FGvCD1cQa1M/PGknCLFIyZdtJOWQjejp7bdNqmN7zwdA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.1.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.0.0", + "resolved": "https://registry.npmmirror.com/pino-pretty/-/pino-pretty-13.0.0.tgz", + "integrity": "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^2.4.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==", + "license": "BSD-3-Clause" + }, + "node_modules/pino-std-serializers": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", + "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==", + "license": "MIT" + }, + "node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmmirror.com/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmmirror.com/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmmirror.com/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/safe-regex2/-/safe-regex2-4.0.1.tgz", + "integrity": "sha512-goqsB+bSlOmVX+CiFX2PFc1OV88j5jvBqIM+DgqrucHnUguAUNtiNOs+aTadq2NqsLQ+TQ3UEVG3gtSFcdlkCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmmirror.com/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/secure-json-parse/-/secure-json-parse-3.0.2.tgz", + "integrity": "sha512-H6nS2o8bWfpFEV6U38sOSjS7bTbdgbCGU9wEM6W14P5H0QOsz94KCusifV44GpHDTu2nqZbuDNhTzu+mjDSw1w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/sonic-boom/-/sonic-boom-4.2.0.tgz", + "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmmirror.com/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/thread-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/thread-stream/-/thread-stream-3.1.0.tgz", + "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmmirror.com/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..343efed --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "yuheng", + "version": "1.0.0", + "description": "", + "main": "src/application.js", + "type": "module", + "imports": { + "#/*": "./*", + "#common/*": "./src/common/*", + "#config/*": "./config/*", + "#start": "./src/utils/start.js", + "#src/*": "./src/*" + }, + "engines": { + "node": ">=22" + }, + "scripts": { + "start": "cross-env NODE_ENV=production node src/application.js", + "dev": "cross-env NODE_ENV=development nodemon src/application.js", + "lint": "eslint --ext .js src" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "dotenv": "^16.4.7", + "fastify": "^5.2.1", + "fastify-plugin": "^5.0.1", + "pino": "^9.6.0", + "pino-multi-stream": "^6.0.0", + "pino-pretty": "^13.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "nodemon": "^3.1.9" + } +} diff --git a/src/application.js b/src/application.js new file mode 100644 index 0000000..4f9647a --- /dev/null +++ b/src/application.js @@ -0,0 +1,23 @@ +// ESM +import "#start" +import Fastify from 'fastify'; +import config from '#config/index.js' +import plugin from '#src/plugins/index.js'; +import routes from '#src/routes/index.js'; +import logger from '#src/utils/logger.js'; + +async function start() { + // 创建Fastify实例 + const fastify = new Fastify({ + logger + }) + // 加载插件 + await fastify.register(plugin); + // 加载路由 + fastify.register(routes); + + await fastify.listen(config.server); // 使用配置中的服务器参数 +} + + +start(); \ No newline at end of file diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 0000000..15fc3a2 --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,13 @@ +export default async function plugin(fastify, opts) { + fastify.log.warn('Register Global Plugin!'); + fastify.decorate('config', opts.config); + fastify.decorate('cfg', opts.config); + await new Promise((resolve) => { + setTimeout(() => { + resolve(); + }, 1000); + }); + fastify.log.warn(fastify.config); + fastify.log.warn(fastify.cfg); + fastify.log.warn('Register Global Plugin complete!'); +} \ No newline at end of file diff --git a/src/routes/index.js b/src/routes/index.js new file mode 100644 index 0000000..5f83f71 --- /dev/null +++ b/src/routes/index.js @@ -0,0 +1,13 @@ +export default async function routes(fastify, options) { + // 定义一个GET请求的路由,路径为根路径 + fastify.get('/', async (request, reply) => { + return { hello: 'world' }; + }); + + // 你可以在这里添加更多的路由 + // 例如: + // fastify.get('/another-route', async (request, reply) => { + // return { message: 'This is another route' }; + // }); +} + diff --git a/src/utils/logger.js b/src/utils/logger.js new file mode 100644 index 0000000..f1666cb --- /dev/null +++ b/src/utils/logger.js @@ -0,0 +1,42 @@ +import config from "#config/index.js" +export default{ + // 日志级别顺序(从高到低): + // fatal(致命) > error(错误) > warn(警告) > info(信息) > debug(调试) > trace(追踪) + // 高级别日志会包含低级别日志(设置warn会包含error和fatal) + level: config.logger.level, + transport: { + // 修正为独立的多传输配置 + targets: [ + config.logger.console && { + target: 'pino-pretty', + options: { + colorize: true, + translateTime: 'yyyy-mm-dd HH:MM:ss', + ignore: 'pid,hostname,reqId,res,responseTime', + // 可用字段列表: + // - pid: 进程ID + // - hostname: 主机名 + // - level: 日志级别(不建议忽略) + // - time: 时间戳(需配合translateTime使用) + // - msg/message: 日志消息(必须保留) + // - module: 模块名称(自定义字段) + // - name: 日志名称 + // - v: pino版本号 + // - reqId: 请求ID + // - res: 响应对象 + // - req: 请求对象 + // - responseTime: 响应时间 + // - 支持通配符如 'req*'(忽略所有 req 开头字段) + } + }, + { + target: 'pino/file', + options: { + destination: config.logger.filePath, // 文件路径从配置读取 + mkdir: true, + ignore: 'pid,hostname', + } + } + ] + } +} \ No newline at end of file diff --git a/src/utils/start.js b/src/utils/start.js new file mode 100644 index 0000000..49b4ccf --- /dev/null +++ b/src/utils/start.js @@ -0,0 +1,12 @@ +const text = '> Si Hi <'; // 需要显示的文本 +const terminalWidth = process.stdout.columns || 100; +const padding = Math.max(0, Math.floor((terminalWidth - text.length * 1.5) / 2)); // 中文每个字占2字符宽度 + +console.log( + '\x1B[48;5;0m%s\x1B[0m', // 灰色背景 + '\x1B[32;5;12m\x1B[1m ' + // 白色加粗 + '-'.repeat(padding) + + text + + '-'.repeat(padding)+ + ' \x1B[0m' // 重置样式 +); \ No newline at end of file