初始化

This commit is contained in:
expressgy 2025-03-18 18:17:32 +08:00
commit d0dacbb21e
42 changed files with 10077 additions and 0 deletions

12
.env Normal file
View File

@ -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

58
.gitignore vendored Normal file
View File

@ -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*

10
.npmrc Normal file
View File

@ -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"

58
README.md Normal file
View File

@ -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 # 依赖管理
```

59
config/index.js Normal file
View File

@ -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
)

View File

@ -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)

View File

@ -0,0 +1,49 @@
<h1 align="center">Fastify</h1>
## 基准测试
基准测试对于衡量改动可能引起的性能变化很重要. 从用户和贡献者的角度, 我们提供了简便的方法测试你的应用. 这套配置可以自动化你的基准测试, 从不同的分支和不同的 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/\""
```

View File

@ -0,0 +1,193 @@
<h1 align="center">Fastify</h1>
## `Content-Type` 解析
Fastify 原生只支持 `'application/json'``'text/plain'` content type。默认的字符集是 `utf-8`。如果你需要支持其他的 content type你需要使用 `addContentTypeParser` API。*默认的 JSON 或者纯文本解析器也可以被更改或删除。*
*注:假如你决定用 `Content-Type` 自定义 content typeUTF-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)
})
})
```

View File

@ -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`

View File

@ -0,0 +1,262 @@
<h1 align="center">Fastify</h1>
## 装饰器
装饰器 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)。
### 使用方法
<a name="usage"></a>
#### `decorate(name, value, [dependencies])`
<a name="decorate"></a>
该方法用于自定义 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])`
<a name="decorate-reply"></a>
顾名思义,`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])`
<a name="decorate-request"></a>
同理,`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)`
<a name="has-decorator"></a>
用于检查服务器实例上是否存在某个装饰器:
```js
fastify.hasDecorator('utility')
```
#### hasRequestDecorator
<a name="has-request-decorator"></a>
用于检查 Request 实例上是否存在某个装饰器:
```js
fastify.hasRequestDecorator('utility')
```
#### hasReplyDecorator
<a name="has-reply-decorator"></a>
用于检查 Reply 实例上是否存在某个装饰器:
```js
fastify.hasReplyDecorator('utility')
```
### 装饰器与封装
<a name="decorators-encapsulation"></a>
**封装** 的同一个上下文中,如果通过 `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
<a name="getters-setters"></a>
装饰器接受特别的 "getter/setter" 对象。这些对象拥有着名为 `getter``setter` 的函数 (尽管 `setter` 是可选的)。这么做便可以通过装饰器来定义属性。例如:
```js
fastify.decorate('foo', {
getter () {
return 'a getter'
}
})
```
上例会在 Fastify 实例中定义一个 `foo` 属性:
```js
console.log(fastify.foo) // 'a getter'
```

View File

@ -0,0 +1,167 @@
<h1 align="center">Fastify</h1>
<a id="encapsulation"></a>
## 封装
“封装上下文”是 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
<a id="shared-context"></a>
## 在上下文间共享
请注意,在上文例子中,每个上下文都 _仅_ 从父级上下文进行继承,而父级上下文无权访问后代上下文中定义的实体。在某些情况下,我们并不想要这一默认行为。使用 [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

View File

@ -0,0 +1,170 @@
<h1 align="center">Fastify</h1>
<a id="errors"></a>
## 错误
<a name="error-handling"></a>
### 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 会内在地监控错误的触发,以此避免在回复阶段无限循环地抛错 (在路由函数执行后)。
<a name="fastify-error-codes"></a>
### Fastify 错误代码
<a name="FST_ERR_BAD_URL"></a>
#### FST_ERR_BAD_URL
无效的 url。
<a name="FST_ERR_CTP_ALREADY_PRESENT"></a>
#### FST_ERR_CTP_ALREADY_PRESENT
该 content type 的解析器已经被注册。
<a name="FST_ERR_CTP_BODY_TOO_LARGE"></a>
#### FST_ERR_CTP_BODY_TOO_LARGE
请求 body 大小超过限制。
可通过 Fastify 实例的 [`bodyLimit`](Server.md#bodyLimit) 属性改变大小限制。
<a name="FST_ERR_CTP_EMPTY_TYPE"></a>
#### FST_ERR_CTP_EMPTY_TYPE
content type 不能是一个空字符串。
<a name="FST_ERR_CTP_INVALID_CONTENT_LENGTH"></a>
#### FST_ERR_CTP_INVALID_CONTENT_LENGTH
请求 body 大小与 Content-Length 不一致。
<a name="FST_ERR_CTP_INVALID_HANDLER"></a>
#### FST_ERR_CTP_INVALID_HANDLER
该 content type 接收的处理函数无效。
<a name="FST_ERR_CTP_INVALID_MEDIA_TYPE"></a>
#### FST_ERR_CTP_INVALID_MEDIA_TYPE
收到的 media type 不支持 (例如,不存在合适的 `Content-Type` 解析器)。
<a name="FST_ERR_CTP_INVALID_PARSE_TYPE"></a>
#### FST_ERR_CTP_INVALID_PARSE_TYPE
提供的待解析类型不支持。只支持 `string``buffer`
<a name="FST_ERR_CTP_INVALID_TYPE"></a>
#### FST_ERR_CTP_INVALID_TYPE
`Content-Type` 应为一个字符串。
<a name="FST_ERR_DEC_ALREADY_PRESENT"></a>
#### FST_ERR_DEC_ALREADY_PRESENT
已存在同名的装饰器。
<a name="FST_ERR_DEC_MISSING_DEPENDENCY"></a>
#### FST_ERR_DEC_MISSING_DEPENDENCY
缺失依赖导致装饰器无法注册。
<a name="FST_ERR_HOOK_INVALID_HANDLER"></a>
#### FST_ERR_HOOK_INVALID_HANDLER
钩子的回调必须为函数。
<a name="FST_ERR_HOOK_INVALID_TYPE"></a>
#### FST_ERR_HOOK_INVALID_TYPE
钩子名称必须为字符串。
<a name="FST_ERR_LOG_INVALID_DESTINATION"></a>
#### FST_ERR_LOG_INVALID_DESTINATION
日志工具目标地址无效。仅接受 `'stream'``'file'` 作为目标地址。
<a name="FST_ERR_PROMISE_NOT_FULLFILLED"></a>
#### FST_ERR_PROMISE_NOT_FULLFILLED
状态码不为 204 时Promise 的 payload 不能为 'undefined'。
<a id="FST_ERR_REP_ALREADY_SENT"></a>
#### FST_ERR_REP_ALREADY_SENT
响应已发送。
<a name="FST_ERR_REP_INVALID_PAYLOAD_TYPE"></a>
#### FST_ERR_REP_INVALID_PAYLOAD_TYPE
响应 payload 类型无效。只允许 `string``Buffer`
<a name="FST_ERR_SCH_ALREADY_PRESENT"></a>
#### FST_ERR_SCH_ALREADY_PRESENT
`$id` 的 schema 已经存在。
<a name="FST_ERR_SCH_MISSING_ID"></a>
#### FST_ERR_SCH_MISSING_ID
提供的 schema 没有 `$id` 属性。
<a name="FST_ERR_SCH_SERIALIZATION_BUILD"></a>
#### FST_ERR_SCH_SERIALIZATION_BUILD
用于序列化响应的 JSON schema 不合法。
<a name="FST_ERR_SCH_VALIDATION_BUILD"></a>
#### FST_ERR_SCH_VALIDATION_BUILD
用于校验路由的 JSON schema 不合法。
<a id="FST_ERR_SEND_INSIDE_ONERR"></a>
#### FST_ERR_SEND_INSIDE_ONERR
不能在 `onError` 钩子中调用 `send`
<a name="FST_ERR_SEND_UNDEFINED_ERR"></a>
#### FST_ERR_SEND_UNDEFINED_ERR
发生了未定义的错误。

View File

@ -0,0 +1,115 @@
<h1 align="center">Fastify</h1>
## 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``替换方式`

View File

@ -0,0 +1,418 @@
<h1 align="center">Fastify</h1>
## 起步
Hello感谢你来到 Fastify 的世界!<br>
这篇文档将向你介绍 Fastify 框架及其特性,也包含了一些示例和指向其他文档的链接。<br>
那,这就开始吧!
<a name="install"></a>
### 安装
使用 npm 安装:
```
npm i fastify --save
```
使用 yarn 安装:
```
yarn add fastify
```
<a name="first-server"></a>
### 第一个服务器
让我们开始编写第一个服务器吧:
```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 对其提供了开箱即用的支持。<br>
*(我们还建议使用 [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()
```
如此简单,棒极了!<br>
可是,一个复杂的应用需要比上例多得多的代码。当你从头开始构建一个应用时,会遇到一些典型的问题,如多个文件的操作、异步引导,以及代码结构的布置。<br>
幸运的是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``::` 会是最简单的暴露应用的方式。
<a name="first-plugin"></a>
### 第一个插件
就如同在 JavaScript 中一切皆为对象,在 Fastify 中,一切都是插件 (plugin)。<br>
在深入之前,先来看看插件系统是如何工作的吧!<br>
让我们新建一个基本的服务器,但这回我们把路由 (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 提供了帮助应用异步引导的基础功能。为什么这一功能十分重要呢?
考虑一下,当存在数据库操作时,数据库连接需要在服务器接受外部请求之前完成。该如何解决这一问题呢?<br>
典型的解决方案是使用复杂的回调函数或 Promise但如此会造成框架的 API、其他库以及应用程序的代码混杂在一起。<br>
Fastify 则不走寻常路,它从本质上用最轻松的方式解决这一问题!
让我们重写上述示例,加入一个数据库连接。<br>
首先,安装 `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
```
哇,真是快啊!<br>
介绍了一些新概念后,让我们回顾一下迄今为止都做了些什么吧。<br>
如你所见,我们可以使用 `register` 来注册数据库连接器或者路由。
这是 Fastify 最棒的特性之一了!它使得插件按声明的顺序来加载,唯有当前插件加载完毕后,才会加载下一个插件。如此,我们便可以在第一个插件中注册数据库连接器,并在第二个插件中使用它。*(参见 [这里](Plugins.md#handle-the-scope) 了解如何处理插件的作用域)*。
当调用函数 `fastify.listen()`、`fastify.inject()` 或 `fastify.ready()` 时,插件便开始加载了。
MongoDB 的插件使用了 `decorate` API以便在 Fastify 的命名空间下添加自定义对象,如此一来,你就可以在所有地方直接使用这些对象了。我们鼓励运用这一 API因为它有助于提高代码复用率减少重复的代码或逻辑。
更深入的内容,例如插件如何运作、如何新建,以及使用 Fastify 全部的 API 去处理复杂的异步引导的细节,请看[插件指南](Plugins-Guide.md)。
<a name="plugin-loading-order"></a>
### 插件加载顺序
为了保证应用的行为一致且可预测,我们强烈建议你采用以下的顺序来组织代码:
```
└── 来自 Fastify 生态的插件
└── 你自己的插件
└── 装饰器
└── 钩子函数
└── 你的服务应用
```
这确保了你总能访问当前作用域下声明的所有属性。<br/>
如前文所述Fastify 提供了一个可靠的封装模型,它能帮助你的应用成为单一且独立的服务。假如你要为某些路由单独地注册插件,只需复写上述的结构就足够了。
```
└── 来自 Fastify 生态的插件
└── 你自己的插件
└── 装饰器
└── 钩子函数
└── 你的服务应用
└── 服务 A
│ └── 来自 Fastify 生态的插件
│ └── 你自己的插件
│ └── 装饰器
│ └── 钩子函数
│ └── 你的服务应用
└── 服务 B
│ └── 来自 Fastify 生态的插件
│ └── 你自己的插件
│ └── 装饰器
│ └── 钩子函数
│ └── 你的服务应用
```
<a name="validate-data"></a>
### 验证数据
数据的验证在我们的框架中是极为重要的一环,也是核心的概念。<br>
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`<br>
请参阅[验证与序列化](Validation-and-Serialization.md)获取更多信息。
<a name="serialize-data"></a>
### 序列化数据
Fastify 对 JSON 提供了优异的支持,极大地优化了解析 JSON body 与序列化 JSON 输出的过程。<br>
在 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)获取更多信息。
<a name="extend-server"></a>
### 扩展服务器
Fastify 生来十分精简,也具有高可扩展性。我们相信,一个小巧的框架足以实现一个优秀的应用。<br>
换句话说Fastify 并非一个面面俱到的框架,它依赖于自己惊人的[生态系统](https://github.com/fastify/fastify/blob/main/docs/Ecosystem.md)
<a name="test-server"></a>
### 测试服务器
Fastify 并没有提供测试框架,但是我们推荐你在测试中使用 Fastify 的特性及结构。<br>
更多内容请看[测试](Testing.md)
<a name="cli"></a>
### 从命令行启动服务器
感谢 [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
```
<a name="slides"></a>
### 幻灯片与视频 (英文资源)
- 幻灯片
- [为你的 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)

View File

@ -0,0 +1,89 @@
<h1 align="center">Fastify</h1>
## 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
```

View File

@ -0,0 +1,579 @@
<h1 align="center">Fastify</h1>
## 钩子方法
钩子 (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 核心的对象。<br/>
`done` 是调用[生命周期](Lifecycle.md)下一阶段的函数。
[生命周期](Lifecycle.md)一文清晰地展示了各个钩子执行的位置。<br>
钩子可被封装,因此可以运用在特定的路由上。更多信息请看[作用域](#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。<br/>
该钩子并不是为了变更错误而设计的,且调用 `reply.send` 会抛出一个异常。<br/>
它只会在 `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()
})
```
<a name="on-close"></a>
### onClose
使用 `fastify.close()` 停止服务器时被触发。当[插件](Plugins.md)需要一个 "shutdown" 事件时有用,例如关闭一个数据库连接。<br>
该钩子的第一个参数是 Fastify 实例,第二个为 `done` 回调函数。
```js
fastify.addHook('onClose', (instance, done) => {
// 其他代码
done()
})
```
<a name="on-route"></a>
### 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]
})
```
<a name="on-register"></a>
### onRegister
当注册一个新的插件,或创建了新的封装好的上下文后被触发。该钩子在注册的代码**之前**被执行。<br/>
当你的插件需要知晓上下文何时创建完毕,并操作它们时,可以使用这一钩子。<br/>
**注意**:被 [`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)
})
```
<a name="scope"></a>
## 作用域
除了[应用钩子](#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而是当前的作用域。
<a name="route-hooks"></a>
## 路由层钩子
你可以为**单个**路由声明一个或多个自定义的生命周期钩子 ([onRequest](#onrequest)、[onResponse](#onresponse)、[preParsing](#preparsing)、[preValidation](#prevalidation)、[preHandler](#prehandler)、[preSerialization](#preserialization)、[onSend](#onsend)、[onTimeout](#ontimeout) 与 [onError](#onerror))。
如果你这么做,这些钩子总是会作为同一类钩子中的最后一个被执行。<br/>
当你需要进行认证时,这会很有用,而 [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()
})
})
```

View File

@ -0,0 +1,54 @@
<h1 align="center">Fastify</h1>
<a name="lts"></a>
## 长期支持计划
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/
<a name="lts-schedule"></a>
### 计划
| 版本 | 发布日期 | 长期支持结束 | 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 |
<a name="supported-os"></a>
### 经过 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`

View File

@ -0,0 +1,78 @@
<h1 align="center">Fastify</h1>
## 生命周期
下图展示了 Fastify 的内部生命周期。<br>
每个节点右边的分支为生命周期的下一阶段,左边的则是上一个生命周期抛出错误时产生的错误码 *(请注意 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` 函数

View File

@ -0,0 +1,161 @@
<h1 align="center">Fastify</h1>
## 日志
日志默认关闭,你可以在创建 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
}
})
```
<a name="logging-request-id"></a>
默认情况下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。

View File

@ -0,0 +1,28 @@
<h1 align="center">Fastify</h1>
## 中间件
从 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)。

View File

@ -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) 与默认值,以及定义请求 bodyquerystring 等 schema 的新方式!
**v2:**
```ts
interface PingQuerystring {
foo?: number;
}
interface PingParams {
bar?: string;
}
interface PingHeaders {
a?: string;
}
interface PingBody {
baz?: string;
}
server.get<PingQuerystring, PingParams, PingHeaders, PingBody>(
'/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))

View File

@ -0,0 +1,376 @@
<h1 align="center">Fastify</h1>
# 插件漫游指南
首先, `不要恐慌`!
Fastify 从一开始就搭建成非常模块化的系统. 我们搭建了非常强健的 API 来允许你创建命名空间, 来添加工具方法. Fastify 创建的封装模型可以让你在任何时候将你的应用分割成不同的微服务, 而无需重构整个应用.
**内容清单**
- [注册器](#register)
- [装饰器](#decorators)
- [钩子方法](#hooks)
- [如何处理封装与分发](#distribution)
- [ESM 的支持](#esm-support)
- [错误处理](#handle-errors)
- [自定义错误](#custom-errors)
- [发布提醒](#emit-warnings)
- [开始!](#start)
<a name="register"></a>
## 注册器
就像在 JavaScript 万物都是对象, 在 Fastify 万物都是插件.<br>
你的路由, 你的工具方法等等都是插件. 无论添加什么功能的插件, 你都可以使用 Fastify 优秀又独一无二的 API: [`register`](Plugins.md).
```js
fastify.register(
require('./my-plugin'),
{ options }
)
```
`register` 创建一个新的 Fastify 上下文, 这意味着如果你对 Fastify 的实例做任何改动, 这些改动不会反映到上下文的父级上. 换句话说, 封装!
*为什么封装这么重要?*<br>
那么, 假设你创建了一个具有开创性的初创公司, 你会怎么做? 你创建了一个包含所有东西的 API 服务器, 所有东西都在同一个地方, 一个庞然大物!<br>
现在, 你增长得非常迅速, 想要改变架构去尝试微服务. 通常这意味着非常多的工作, 因为交叉依赖和缺少关注点的分离.<br>
Fastify 在这个层面上可以帮助你很多, 多亏了封装模型, 它完全避免了交叉依赖, 并且帮助你将组织成高聚合的代码块.
*让我们回到如何正确地使用 `register`.*<br>
插件必须输出一个有以下参数的方法
```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 添加新的功能, 并且分享给其他的开发者?
<a name="decorators"></a>
## 装饰器
好了, 假设你写了一个非常好的工具方法, 因此你决定在你所有的代码里都能够用这个方法. 你改怎么做? 可能是像以下代码一样:
```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` 调用你的方法, 甚至在你的测试中.<br>
这里神奇的是: 你还记得之前我们讨论的封装? 同时使用 `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` 只存在第一个注册器的上下文中.<br>
让我们更深入地看一下: 当使用 `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` 还需要它们?*<br>
好问题, 是为了让开发者更方便地使用 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)" 事件的时候执行这个方法, 该怎么做?
<a name="hooks"></a>
## 钩子方法
你刚刚构建了工具方法, 现在你需要在每个请求的时候都执行这个方法, 你大概会这样做:
```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)!<br>
```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)
})
```
现在每个请求都会运行工具方法, 很显然你可以注册任意多的需要的钩子方法.<br>
有时, 你希望只在一个路由子集中执行钩子方法, 这个怎么做到? 对了, 封装!
```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 对象.<br>
<a name="distribution"></a>
## 如何处理封装与分发
完美, 现在你知道了(几乎)所有的扩展 Fastify 的工具. 但可能你遇到了一个大问题: 如何分发你的代码?
我们推荐将所有代码包裹在一个`注册器`中分发, 这样你的插件可以支持异步启动 *(`decorate` 是一个同步 API)*, 例如建立数据库链接.
*等等? 你不是告诉我 `register` 会创建封装的上下文, 那么我创建的不是就外层不可见了?*<br>
是的, 我是说过. 但我没告诉你的是, 你可以通过 [`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 实例**的一份拷贝。这就意味着我们可以获取到之前声明的插件所注入的变量了。
<a name="esm-support"></a>
## 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)
}
})
```
<a name="handle-errors"></a>
## 错误处理
你的插件也可能在启动的时候失败. 或许你预料到这个并且在这种情况下有特定的处理逻辑. 你该怎么实现呢?
`after` API 就是你需要的. `after` 注册一个回调, 在注册之后就会调用这个回调, 它可以有三个参数.<br>
回调会基于不同的参数而变化:
1. 如果没有参数并且有个错误, 这个错误会传递到下一个错误处理.
1. 如果有一个参数, 这个参数就是错误对象.
1. 如果有两个参数, 第一个是错误对象, 第二个是完成回调.
1. 如果有三个参数, 第一个是错误对象, 第二个是顶级上下文(除非你同时指定了服务器和复写, 在这个情况下将会是那个复写的返回), 第三个是完成回调.
让我们看看如何使用它:
```js
fastify
.register(require('./database-connector'))
.after(err => {
if (err) throw err
})
```
<a name="custom-errors"></a>
## 自定义错误
假如你的插件需要暴露自定义的错误,[`fastify-error`](https://github.com/fastify/fastify-error) 能帮助你轻松地在代码或插件中生成一致的错误对象。
```js
const createError = require('fastify-error')
const CustomError = createError('ERROR_CODE', 'message')
console.log(new CustomError())
```
<a name="emit-warnings"></a>
## 发布提醒
假如你要提示用户某个 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')
```
<a name="start"></a>
## 开始!
太棒了, 现在你已经知道了所有创建插件需要的关于 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) 重要的安全头部支持
*如果感觉还差什么? 告诉我们! :)*

View File

@ -0,0 +1,186 @@
<h1 align="center">Fastify</h1>
## 插件
Fastify 允许用户通过插件的方式扩展自身的功能。
一个插件可以是一组路由,一个服务器[装饰器](Decorators.md)或者其他任意的东西。 在使用一个或者许多插件时,只需要一个 API `register`<br>
默认, `register` 会创建一个 *新的作用域( Scope )*, 这意味着你能够改变 Fastify 实例(通过`decorate`), 这个改变不会反映到当前作用域, 只会影响到子作用域。 这样可以做到插件的*封装*和*继承*, 我们创建了一个*无回路有向图*(DAG), 因此不会有交叉依赖的问题。
你已经在[起步](Getting-Started.md#register)部分很直观的看到了怎么使用这个 API。
```
fastify.register(plugin, [options])
```
<a name="plugin-options"></a>
### 插件选项
`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) 包装了这个插件。
<a name="route-prefixing-option"></a>
#### 路由前缀选项
如果你传入以 `prefix`为 key , `string` 为值的选项, Fastify 会自动为这个插件下所有的路由添加这个前缀, 更多信息可以查询 [这里](Routes.md#route-prefixing).<br>
注意如果使用了 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 这个选项不会起作用。
<a name="error-handling"></a>
#### 错误处理
错误处理是由 [avvio](https://github.com/mcollina/avvio#error-handling) 解决的。<br>
一个通用的原则, 我们建议在下一个 `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)
})
```
<a name="async-await"></a>
### 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)
```
<a name="esm-support"></a>
#### 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
```
<a name="create-plugin"></a>
### 创建插件
创建插件非常简单, 你只需要创建一个方法, 这个方法接收三个参数: `fastify` 实例、`options` 选项和 `done` 回调。<br>
例子:
```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 作用域, 如果你不需要, 阅读下面的章节。
<a name="handle-scope"></a>
### 处理作用域
如果你使用 `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
```

View File

@ -0,0 +1,230 @@
<h1 align="center">Fastify</h1>
## 推荐方案
本文涵盖了使用 Fastify 的推荐方案及最佳实践。
* [使用反向代理](#reverseproxy)
* [Kubernetes](#kubernetes)
## 使用反向代理
<a id="reverseproxy"></a>
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
<a id="kubernetes"></a>
`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

View File

@ -0,0 +1,431 @@
<h1 align="center">Fastify</h1>
## 回复
- [回复](#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)
<a name="introduction"></a>
### 简介
处理函数的第二个参数为 `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)
})
```
<a name="code"></a>
### .code(statusCode)
如果没有设置 `reply.code``statusCode` 会是 `200`
<a name="statusCode"></a>
### .statusCode
获取或设置 HTTP 状态码。作为 setter 使用时,是 `reply.code()` 的别名。
```js
if (reply.statusCode >= 299) {
reply.statusCode = 500
}
```
<a name="server"></a>
### .server
Fastify 服务器的实例,以当前的[封装上下文](Encapsulation.md)为作用域。
```js
fastify.decorate('util', function util () {
return 'foo'
})
fastify.get('/', async function (req, rep) {
return rep.server.util() // foo
})
```
<a name="header"></a>
### .header(key, value)
设置响应 header。如果值被省略或为 undefined将被强制设成 `''`
更多信息,请看 [`http.ServerResponse#setHeader`](https://nodejs.org/dist/latest-v14.x/docs/api/http.html#http_response_setheader_name_value)。
<a name="getHeader"></a>
### .getHeader(key)
获取已设置的 header 的值。
```js
reply.header('x-foo', 'foo') // 设置 x-foo header 的值为 foo
reply.getHeader('x-foo') // 'foo'
```
<a name="getHeader"></a>
### .removeHeader(key)
清除已设置的 header 的值。
```js
reply.header('x-foo', 'foo')
reply.removeHeader('x-foo')
reply.getHeader('x-foo') // undefined
```
<a name="hasHeader"></a>
### .hasHeader(key)
返回一个 boolean用于检查是否设置了某个 header。
<a name="redirect"></a>
### .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')
```
<a name="call-not-found"></a>
### .callNotFound()
调用自定义的 not found 处理函数。注意,只有在 [`setNotFoundHandler`](Server.md#set-not-found-handler) 中指明的 `preHandler` 钩子会被调用。
```js
reply.callNotFound()
```
<a name="getResponseTime"></a>
### .getResponseTime()
调用自定义响应时间获取函数,来计算自收到请求起的时间。
```js
const milliseconds = reply.getResponseTime()
```
<a name="type"></a>
### .type(contentType, type)
设置响应的 content type。
这是 `reply.header('Content-Type', 'the/type')` 的简写。
```js
reply.type('text/html')
```
如果 `Content-Type` 为 JSON 子类型,并且未设置 charset 参数,则使用 `utf-8` 作为 charset 的默认参数。
<a name="serializer"></a>
### .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) 了解更多关于发送不同类型值的信息。
<a name="raw"></a>
### .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` 的例子。
<a name="sent"></a>
### .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将会记录一个错误。
<a name="hijack"></a>
### .hijack()
有时你需要终止请求生命周期的执行,并手动发送响应。
Fastify 提供了 `reply.hijack()` 方法来完成此任务。在 `reply.send()` 之前的任意节点调用该方法,能阻止 Fastify 自动发送响应,并不再执行之后的生命周期函数 (包括用户编写的处理函数)。
特别注意 (*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。
<a name="send"></a>
### .send(data)
顾名思义,`.send()` 是向用户发送 payload 的函数。
<a name="send-object"></a>
#### 对象
如上文所述,如果你发送 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' })
})
```
<a name="send-string"></a>
#### 字符串
在未设置 `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')
})
```
<a name="send-streams"></a>
#### 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)
})
```
<a name="send-buffers"></a>
#### 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)
})
})
```
<a name="errors"></a>
#### Errors
若使用 *send* 发送一个 *Error* 的实例Fastify 会自动创建一个如下的错误结构:
```js
{
error: String // HTTP 错误信息
code: String // Fastify 的错误代码
message: String // 用户错误信息
statusCode: Number // HTTP 状态码
}
```
你可以向 Error 对象添加自定义属性,例如 `headers`,这可以用来增强 HTTP 响应。<br>
*注意:如果 `send` 一个错误,但状态码小于 400Fastify 会自动将其设为 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。<br>
*注:当自定义错误处理时,你需要自行记录日志*
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')
})
```
<a name="payload-type"></a>
#### 最终 payload 的类型
发送的 payload (序列化之后、经过任意的 [`onSend` 钩子](Hooks.md#the-onsend-hook)) 必须为下列类型之一,否则将会抛出一个错误:
- `string`
- `Buffer`
- `stream`
- `undefined`
- `null`
<a name="async-await-promise"></a>
#### Async-Await 与 Promise
Fastify 原生地处理 promise 并支持 async-await。<br>
*请注意,在下面的例子中我们没有使用 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)。
<a name="then"></a>
### .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` 的使用。

View File

@ -0,0 +1,60 @@
<h1 align="center">Fastify</h1>
## Request
处理函数的第一个参数是 `Request`.<br>
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')
})
```

View File

@ -0,0 +1,498 @@
<h1 align="center">Fastify</h1>
## 路由
路由方法设置你程序的路由。
你可以使用简写定义与完整定义两种方式来设定路由。
- [完整定义](#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)
<a name="full-declaration"></a>
### 完整定义
```js
fastify.route(options)
```
<a name="options"></a>
### 路由选项
* `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' })
}
})
```
<a name="shorthand-declaration"></a>
### 简写定义
上文的路由定义带有 *Hapi* 的风格。要是偏好 *Express/Restify* 的写法Fastify 也是支持的:<br>
`fastify.get(path, [options], handler)`<br>
`fastify.head(path, [options], handler)`<br>
`fastify.post(path, [options], handler)`<br>
`fastify.put(path, [options], handler)`<br>
`fastify.delete(path, [options], handler)`<br>
`fastify.options(path, [options], handler)`<br>
`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` 错误。
<a name="url-building"></a>
### Url 构建
Fastify 同时支持静态与动态的 URL<br>
要注册一个**参数命名**的路径,请在参数名前加上*冒号*。*星号*表示**通配符**。
*注意,静态路由总是在参数路由和通配符之前进行匹配。*
```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
```
<a name="async-await"></a>
### 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)。
<a name="promise-resolution"></a>
### 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 对象。
<a name="route-prefixing"></a>
### 路由前缀
有时你需要维护同一 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` 选项。
<a name="custom-log-level"></a>
### 自定义日志级别
在 Fastify 中为路由里设置不同的日志级别是十分容易的。<br/>
你只需在插件或路由的选项里设置 `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` 访问的全局日志并不会受到影响。*
<a name="custom-log-serializer"></a>
### 自定义日志序列化器
在某些上下文里,你也许需要记录一个大型对象,但这在其他路由中是个负担。这时,你可以定义一些[`序列化器 (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)
```
<a name="routes-config"></a>
### 配置
注册一个新的处理函数,你可以向其传递一个配置对象,并在其中使用它。
```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)
```
<a name="constraints"></a>
### 约束
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 版本声明以便成功匹配。<br/>
对于版本化的路由Fastify 需要请求附带上 `Accept-Version` header。此外相同路径的请求会优先匹配带有版本的控制函数。当前尚不支持 semver 规范中的 advanced ranges 与 pre-releases 语法<br/>
*请注意,这一特性会降低路由的性能。*
```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 的值选择最兼容的版本。<br/>
假如请求未带有 `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)
}
})
```

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,310 @@
<h1 align="center">Serverless</h1>
使用现有的 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);
}
```

View File

@ -0,0 +1,294 @@
<h1 align="center">Fastify</h1>
## 测试
测试是开发应用最重要的一部分。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/<test-file.test.js>
```
- `-O` 表示开启 `only` 选项,只运行设置了 `{only: true}` 的测试
- `-T` 表示不设置超时
- `--node-arg=--inspect-brk` 会启动 node 调试工具
3. 在 VS Code 中创建并运行一个 `Node.js: Attach` 调试配置,不需要额外修改。
现在你便可以在编辑器中检测你的测试文件 (以及 `Fastify` 的其他部分) 了。

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,822 @@
<h1 align="center">Fastify</h1>
## 验证和序列化
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。
<a name="shared-schema"></a>
#### 添加共用 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#' }
}
})
```
<a name="get-shared-schema"></a>
#### 获取共用 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)。
<a name="ajv-plugins"></a>
#### 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']
}
}
}
}
})
```
<a name="schema-validator"></a>
#### 验证生成器
`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` 方法也不清楚你在使用什么工具进行校验。_
<a name="using-other-validation-libraries"></a>
##### 使用其他验证工具
通过 `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)
}
```
<a name="serialization"></a>
### 序列化
通常,你会通过 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)
```
<a name="schema-serializer"></a>
#### 序列化函数生成器
`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 支持
为了能更简单地重用 schemaJSON 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' }
}
}
```
<a name="resources"></a>
### 资源
- [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)

View File

@ -0,0 +1,59 @@
<h1 align="center">Fastify</h1>
# 如何写一个好的插件
首先,要感谢你决定为 Fastify 编写插件。Fastify 本身是一个极简的框架,插件才是它强大功能的来源,所以,谢谢你。<br>
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) 提交了一个插件,我们将会检查你的代码,需要时也会帮忙改进它。
## 文档
文档相当重要。假如你的插件没有好的文档,我们将拒绝将其加入生态列表。缺乏良好的文档会提升用户使用插件的难度,并有可能导致弃用。<br>
以下列出了一些优秀插件文档的示例:
- [`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)
## 许可证
你可以为你的插件使用自己偏好的许可,我们不会强求。<br>
我们推荐 [MIT 许可证](https://choosealicense.com/licenses/mit/),因为我们认为它允许更多人自由地使用代码。其他可替代的许可证参见 [OSI list](https://opensource.org/licenses) 或 GitHub 的 [choosealicense.com](https://choosealicense.com/)。
## 示例
总在你的仓库里添加一个示例文件。这对于用户是相当有帮助的,也提供了一个快速的手段来测试你的插件。使用者们会为此感激的。
## 测试
彻底地测试一个插件,来验证其是否正常执行,是极为重要的。<br>
缺乏测试会影响用户的信任感,也无法保证代码在不同版本的依赖下还能正常工作。
我们不强求使用某一测试工具。我们使用的是 [`tap`](https://www.node-tap.org/),因为它提供了开箱即用的并行测试以及代码覆盖率检测。
## 代码检查
这一项不是强制的,但我们强烈推荐你在插件中使用一个代码检查工具。这可以帮助你保持统一的代码风格,同时避免许多错误。
我们使用 [`standard`](https://standardjs.com/),因为它不需任何配置,并且容易与测试集成。
## 持续集成
这一项也不是强制的,但假如你开源发布你的代码,持续集成能保证其他人的参与不会破坏你的插件,并检查插件是否如预期般工作。[CircleCI](https://circleci.com/) 和 [GitHub Actions](https://github.com/features/actions) 都是对开源项目免费的持续集成系统,且易于安装配置。<br>
此外,你还可以启用 [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)
重要的请求头安全插件。

View File

@ -0,0 +1 @@
<mxfile host="app.diagrams.net" modified="2020-12-06T18:51:58.018Z" agent="5.0 (Macintosh; Intel Mac OS X 10_15_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36" etag="vyaguDTT1c9e-NqGeV_7" version="13.10.9" type="device"><diagram id="hZ89Y7exsLGRT07QCK17" name="Page-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==</diagram></mxfile>

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

1330
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

37
package.json Normal file
View File

@ -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"
}
}

23
src/application.js Normal file
View File

@ -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();

13
src/plugins/index.js Normal file
View File

@ -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!');
}

13
src/routes/index.js Normal file
View File

@ -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' };
// });
}

42
src/utils/logger.js Normal file
View File

@ -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',
}
}
]
}
}

12
src/utils/start.js Normal file
View File

@ -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' // 重置样式
);