数据库
This commit is contained in:
parent
d0dacbb21e
commit
a8253e7de5
9
.env
9
.env
@ -9,4 +9,11 @@ LOG_FILE=./logs/app.log
|
||||
|
||||
# 插件配置
|
||||
PLUGIN_NAME=myapp
|
||||
API_PREFIX=/api/v1
|
||||
API_PREFIX=/api/v1
|
||||
|
||||
# 数据库
|
||||
DB_HOST=172.16.1.10
|
||||
DB_USER=nie
|
||||
DB_PASSWORD=Hxl1314521
|
||||
DB_NAME=yuheng
|
||||
DB_PORT=3306
|
||||
|
30
.prettier.config.cjs
Normal file
30
.prettier.config.cjs
Normal file
@ -0,0 +1,30 @@
|
||||
// 转换为 CommonJS 格式(重要!)
|
||||
module.exports = {
|
||||
printWidth: 100, // 最大行宽
|
||||
semi: true, // 强制分号
|
||||
singleQuote: true, // 使用单引号
|
||||
trailingComma: 'all', // 自动尾逗号
|
||||
tabWidth: 4, // 缩进空格数
|
||||
|
||||
// 新增常用配置
|
||||
arrowParens: 'avoid', // 箭头函数单参数省略括号
|
||||
bracketSpacing: true, // 对象花括号空格 { key: value }
|
||||
endOfLine: 'auto', // 自动识别换行符
|
||||
jsxBracketSameLine: false, // JSX 闭合标签换行(React 项目适用)
|
||||
|
||||
// 文件类型覆盖规则
|
||||
overrides: [
|
||||
{
|
||||
files: '*.md',
|
||||
options: {
|
||||
proseWrap: 'always' // Markdown 自动换行
|
||||
}
|
||||
},
|
||||
{
|
||||
files: '*.json',
|
||||
options: {
|
||||
tabWidth: 4 // JSON 使用 2 空格缩进
|
||||
}
|
||||
}
|
||||
]
|
||||
}; // 移除结尾分号保持风格统一
|
11
.prettierignore
Normal file
11
.prettierignore
Normal file
@ -0,0 +1,11 @@
|
||||
# 忽略环境文件
|
||||
.env
|
||||
*.env
|
||||
|
||||
# 忽略模块类型冲突文件
|
||||
*.cjs
|
||||
*.mjs
|
||||
|
||||
# 忽略的目录
|
||||
node_modules/*
|
||||
doc/*
|
@ -3,6 +3,7 @@
|
||||
基于 Fastify 的 Node.js 角色访问控制(RBAC)系统,提供灵活的权限管理能力。
|
||||
|
||||
## 主要特性
|
||||
|
||||
- 🛡️ 基于 JWT 的身份认证
|
||||
- 🔑 角色-权限层级管理
|
||||
- 🚦 请求权限验证中间件
|
||||
@ -12,12 +13,14 @@
|
||||
## 快速开始
|
||||
|
||||
### 前置要求
|
||||
|
||||
- Node.js v18+
|
||||
- 数据库(PostgreSQL/MySQL)
|
||||
- Redis(用于会话管理)
|
||||
|
||||
### 安装步骤
|
||||
```bash
|
||||
|
||||
````bash
|
||||
# 克隆仓库
|
||||
git clone https://github.com/your-repo.git
|
||||
cd rbac-system
|
||||
@ -55,4 +58,4 @@ npm run dev
|
||||
├── config/ # 配置文件
|
||||
│ ├── default.js # 通用配置
|
||||
├── package.json # 依赖管理
|
||||
```
|
||||
````
|
||||
|
@ -1,10 +1,9 @@
|
||||
import { config } from 'dotenv'
|
||||
import path from 'path'
|
||||
import { config } from 'dotenv';
|
||||
import path from 'path';
|
||||
|
||||
// 加载环境变量
|
||||
config({ path: path.resolve(process.cwd(), '.env') })
|
||||
console.log("[NODE_ENV]:", process.env.NODE_ENV);
|
||||
|
||||
config({ path: path.resolve(process.cwd(), '.env') });
|
||||
console.log('[NODE_ENV]:', process.env.NODE_ENV);
|
||||
|
||||
// 基础配置
|
||||
const baseConfig = {
|
||||
@ -12,7 +11,7 @@ const baseConfig = {
|
||||
server: {
|
||||
port: process.env.PORT || 9000,
|
||||
host: process.env.HOST || 'localhost',
|
||||
backlog: process.env.BACKLOG || 511
|
||||
backlog: process.env.BACKLOG || 511,
|
||||
},
|
||||
logger: {
|
||||
level: process.env.LOG_LEVEL || 'trace',
|
||||
@ -21,9 +20,17 @@ const baseConfig = {
|
||||
},
|
||||
pluginOptions: {
|
||||
name: process.env.PLUGIN_NAME || 'default',
|
||||
prefix: process.env.API_PREFIX || '/api'
|
||||
}
|
||||
}
|
||||
prefix: process.env.API_PREFIX || '/api',
|
||||
},
|
||||
db: {
|
||||
host: process.env.DB_HOST || '127.0.0.1',
|
||||
port: process.env.DB_PORT || 3306,
|
||||
user: process.env.DB_USER || 'root',
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME || 'yuheng',
|
||||
ssl: process.env.NODE_ENV === 'production' ? { rejectUnauthorized: true } : null,
|
||||
},
|
||||
};
|
||||
|
||||
// 环境特定配置
|
||||
const envConfig = {
|
||||
@ -33,27 +40,24 @@ const envConfig = {
|
||||
},
|
||||
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]))
|
||||
Object.assign(source[key], deepMerge(target[key], source[key]));
|
||||
}
|
||||
}
|
||||
return Object.assign({}, target, source)
|
||||
return Object.assign({}, target, source);
|
||||
}
|
||||
|
||||
export default deepMerge(
|
||||
baseConfig,
|
||||
envConfig[process.env.NODE_ENV] || envConfig.development
|
||||
)
|
||||
export default deepMerge(baseConfig, envConfig[process.env.NODE_ENV] || envConfig.development);
|
||||
|
@ -1,33 +1,34 @@
|
||||
# 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)
|
||||
|
||||
- [入门](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)
|
||||
- [v2.x](https://github.com/fastify/docs-chinese/tree/2.x)
|
||||
|
||||
### 翻译指南
|
||||
|
||||
@ -35,7 +36,8 @@
|
||||
为了提供一致的翻译体验,请在开始翻译前参阅[翻译指南](docs/Contributing.md)。感谢!
|
||||
|
||||
### 贡献者
|
||||
* [@fralonra](https://github.com/fralonra)
|
||||
* [@poppinlp](https://github.com/poppinlp)
|
||||
* [@vincent178](https://github.com/vincent178)
|
||||
* [@xtx1130](https://github.com/xtx1130)
|
||||
|
||||
- [@fralonra](https://github.com/fralonra)
|
||||
- [@poppinlp](https://github.com/poppinlp)
|
||||
- [@vincent178](https://github.com/vincent178)
|
||||
- [@xtx1130](https://github.com/xtx1130)
|
||||
|
@ -1,9 +1,11 @@
|
||||
<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): 并行运行命令.
|
||||
@ -12,11 +14,13 @@
|
||||
## 基本用法
|
||||
|
||||
### 在当前分支上运行测试
|
||||
|
||||
```sh
|
||||
npm run benchmark
|
||||
```
|
||||
|
||||
### 在不同的 Node.js 版本中运行测试 ✨
|
||||
|
||||
```sh
|
||||
npx -p node@6 -- npm run benchmark
|
||||
```
|
||||
@ -24,20 +28,25 @@ 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
|
||||
```
|
||||
|
@ -1,57 +1,84 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
## `Content-Type` 解析
|
||||
Fastify 原生只支持 `'application/json'` 和 `'text/plain'` content type。默认的字符集是 `utf-8`。如果你需要支持其他的 content type,你需要使用 `addContentTypeParser` API。*默认的 JSON 或者纯文本解析器也可以被更改或删除。*
|
||||
|
||||
*注:假如你决定用 `Content-Type` 自定义 content type,UTF-8 便不再是默认的字符集了。请确保如下包含该字符集:`text/html; charset=utf-8`。*
|
||||
Fastify 原生只支持 `'application/json'` 和 `'text/plain'` content type。默认的字符集是 `utf-8`。如果你需要支持其他的 content type,你需要使用 `addContentTypeParser` API。_默认的 JSON 或者纯文本解析器也可以被更改或删除。_
|
||||
|
||||
_注:假如你决定用 `Content-Type` 自定义 content type,UTF-8 便不再是默认的字符集了。请确保如下包含该字符集:`text/html; charset=utf-8`。_
|
||||
|
||||
和其他的 API 一样,`addContentTypeParser` 被封装在定义它的作用域中了。这就意味着如果你定义在了根作用域中,那么就是全局可用,如果你定义在一个插件中,那么它只能在那个作用域和子作用域中可用。
|
||||
|
||||
Fastify 自动将解析好的 payload 添加到 [Fastify request](Request.md) 对象,你能通过 `request.body` 访问。
|
||||
|
||||
### 用法
|
||||
|
||||
```js
|
||||
fastify.addContentTypeParser('application/jsoff', function (request, payload, done) {
|
||||
jsoffParser(payload, function (err, body) {
|
||||
done(err, body)
|
||||
})
|
||||
})
|
||||
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)
|
||||
})
|
||||
})
|
||||
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)
|
||||
fastify.addContentTypeParser(
|
||||
"application/jsoff",
|
||||
async function (request, payload) {
|
||||
var res = await jsoffParserAsync(payload);
|
||||
|
||||
return res
|
||||
})
|
||||
return res;
|
||||
},
|
||||
);
|
||||
|
||||
// 处理所有匹配的 content type
|
||||
// 处理所有匹配的 content type
|
||||
fastify.addContentTypeParser(/^image\/.*/, function (request, payload, done) {
|
||||
imageParser(payload, function (err, body) {
|
||||
done(err, body)
|
||||
})
|
||||
})
|
||||
done(err, body);
|
||||
});
|
||||
});
|
||||
|
||||
// 可以为不同的 content type 使用默认的 JSON/Text 解析器
|
||||
fastify.addContentTypeParser('text/json', { parseAs: 'string' }, fastify.getDefaultJsonParser('ignore', 'ignore'))
|
||||
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.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) => {} )
|
||||
fastify.addContentTypeParser(
|
||||
"application/vnd.custom",
|
||||
(request, body, done) => {},
|
||||
);
|
||||
fastify.addContentTypeParser(
|
||||
"application/vnd.custom+xml",
|
||||
(request, body, done) => {},
|
||||
);
|
||||
```
|
||||
|
||||
在 `hasContentTypeParser` 之外,还有其他 API 可供使用,它们是:`hasContentTypeParser`、`removeContentTypeParser` 与 `removeAllContentTypeParsers`。
|
||||
@ -61,12 +88,15 @@ fastify.addContentTypeParser('application/vnd.custom+xml', (request, body, done)
|
||||
使用 `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)
|
||||
})
|
||||
})
|
||||
if (!fastify.hasContentTypeParser("application/jsoff")) {
|
||||
fastify.addContentTypeParser(
|
||||
"application/jsoff",
|
||||
function (request, payload, done) {
|
||||
jsoffParser(payload, function (err, body) {
|
||||
done(err, body);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
@ -76,14 +106,14 @@ if (!fastify.hasContentTypeParser('application/jsoff')){
|
||||
`RegExp` 来匹配。
|
||||
|
||||
```js
|
||||
fastify.addContentTypeParser('text/xml', function (request, payload, done) {
|
||||
fastify.addContentTypeParser("text/xml", function (request, payload, done) {
|
||||
xmlParser(payload, function (err, body) {
|
||||
done(err, body)
|
||||
})
|
||||
})
|
||||
done(err, body);
|
||||
});
|
||||
});
|
||||
|
||||
// 移除内建的 content type 解析器。这时只有上文添加的 text/html 解析器可用。
|
||||
Fastiy.removeContentTypeParser(['application/json', 'text/plain'])
|
||||
Fastiy.removeContentTypeParser(["application/json", "text/plain"]);
|
||||
```
|
||||
|
||||
#### removeAllContentTypeParsers
|
||||
@ -91,51 +121,58 @@ Fastiy.removeContentTypeParser(['application/json', 'text/plain'])
|
||||
在上文的例子中,你需要明确指定所有你想移除的 content type。但你也可以使用 `removeAllContentTypeParsers`直接移除所有现存的 content type 解析器。在下面的例子里,我们实现了一样的效果,但不再需要手动指定 content type 了。和 `removeContentTypeParser` 一样,该 API 也支持封装。当你想注册一个[能捕获所有 content type 的解析器](#Catch-All),且忽略内建的解析器时,这个 API 特别有用。
|
||||
|
||||
```js
|
||||
Fastiy.removeAllContentTypeParsers()
|
||||
Fastiy.removeAllContentTypeParsers();
|
||||
|
||||
fastify.addContentTypeParser('text/xml', function (request, payload, done) {
|
||||
fastify.addContentTypeParser("text/xml", function (request, payload, done) {
|
||||
xmlParser(payload, function (err, body) {
|
||||
done(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) 和消息主体的长度。如果达到了某些限制,自定义的解析器就不会被调用。
|
||||
你可以用两种方式解析消息主体。第一种方法在上面演示过了: 你可以添加定制的 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)
|
||||
}
|
||||
})
|
||||
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)。
|
||||
|
||||
- `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)
|
||||
})
|
||||
})
|
||||
fastify.addContentTypeParser("*", function (request, payload, done) {
|
||||
var data = "";
|
||||
payload.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
payload.on("end", () => {
|
||||
done(null, data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
在这种情况下,所有的没有特定 content type 解析器的请求都会被这个方法处理。
|
||||
@ -143,36 +180,36 @@ fastify.addContentTypeParser('*', function (request, payload, done) {
|
||||
对请求流 (stream) 执行管道输送 (pipe) 操作也是有用的。你可以如下定义一个 content 解析器:
|
||||
|
||||
```js
|
||||
fastify.addContentTypeParser('*', function (request, payload, done) {
|
||||
done()
|
||||
})
|
||||
fastify.addContentTypeParser("*", function (request, payload, done) {
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
之后通过核心 HTTP request 对象将请求流直接输送到任意位置:
|
||||
|
||||
```js
|
||||
app.post('/hello', (request, reply) => {
|
||||
reply.send(request.raw)
|
||||
})
|
||||
app.post("/hello", (request, reply) => {
|
||||
reply.send(request.raw);
|
||||
});
|
||||
```
|
||||
|
||||
这里有一个将来访的 [json line](https://jsonlines.org/) 对象完整输出到日志的例子:
|
||||
|
||||
```js
|
||||
const split2 = require('split2')
|
||||
const pump = require('pump')
|
||||
const split2 = require("split2");
|
||||
const pump = require("pump");
|
||||
|
||||
fastify.addContentTypeParser('*', (request, payload, done) => {
|
||||
done(null, pump(payload, split2(JSON.parse)))
|
||||
})
|
||||
fastify.addContentTypeParser("*", (request, payload, done) => {
|
||||
done(null, pump(payload, split2(JSON.parse)));
|
||||
});
|
||||
|
||||
fastify.route({
|
||||
method: 'POST',
|
||||
url: '/api/log/jsons',
|
||||
method: "POST",
|
||||
url: "/api/log/jsons",
|
||||
handler: (req, res) => {
|
||||
req.body.on('data', d => console.log(d)) // 记录每个来访的对象
|
||||
}
|
||||
})
|
||||
req.body.on("data", (d) => console.log(d)); // 记录每个来访的对象
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
关于处理上传的文件,请看[该插件](https://github.com/fastify/fastify-multipart)。
|
||||
@ -181,13 +218,15 @@ fastify.route({
|
||||
|
||||
```js
|
||||
// 没有下面这行的话,content type 为 application/json 的 body 将被内建的 json 解析器处理。
|
||||
fastify.removeAllContentTypeParsers()
|
||||
fastify.removeAllContentTypeParsers();
|
||||
|
||||
fastify.addContentTypeParser('*', function (request, payload, done) {
|
||||
var data = ''
|
||||
payload.on('data', chunk => { data += chunk })
|
||||
payload.on('end', () => {
|
||||
done(null, data)
|
||||
})
|
||||
})
|
||||
```
|
||||
fastify.addContentTypeParser("*", function (request, payload, done) {
|
||||
var data = "";
|
||||
payload.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
payload.on("end", () => {
|
||||
done(null, data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
@ -2,10 +2,10 @@
|
||||
|
||||
如果你发现了翻译的错误,或文档的滞后,请创建一个 issue 来告诉我们,或是按如下方法直接参与翻译。感谢!
|
||||
|
||||
* Fork https://github.com/fastify/docs-chinese
|
||||
* 创建 Git Branch
|
||||
* 开始翻译
|
||||
* 创建 PR,尽量保证一篇文档一个 commit 一个 PR
|
||||
- Fork https://github.com/fastify/docs-chinese
|
||||
- 创建 Git Branch
|
||||
- 开始翻译
|
||||
- 创建 PR,尽量保证一篇文档一个 commit 一个 PR
|
||||
|
||||
### 风格规范
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
- 中文与英文之间需要加一个空格,例如 `你好 hello 你好`
|
||||
- 中文与数字之间需要加一个空格,例如 `2018 年 12 月 20 日`
|
||||
- 正文中的英文标点需要转换为对应的中文标点,例如 `,` => `,`
|
||||
- 例外:英文括号修改为半角括号加一个空格的形式,例如 `()` => ` () `
|
||||
- 例外:英文括号修改为半角括号加一个空格的形式,例如 `()` => `()`
|
||||
- 注意:英文逗号可能需要转换为中文顿号,而非中文逗号
|
||||
- 代码片段中的注释需要翻译
|
||||
- 不做翻译的内容:
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
装饰器 API 允许你自定义服务器实例或请求周期中的请求/回复等对象。任意类型的属性都能通过装饰器添加到这些对象上,包括函数、普通对象 (plain object) 以及原生类型。
|
||||
|
||||
装饰器 API 是 *同步* 的。如果异步地添加装饰器,可能会导致在装饰器完成初始化之前, Fastify 实例就已经引导完毕了。因此,必须将 `register` 方法与 `fastify-plugin` 结合使用。详见 [Plugins](Plugins.md)。
|
||||
装饰器 API 是 _同步_ 的。如果异步地添加装饰器,可能会导致在装饰器完成初始化之前, Fastify 实例就已经引导完毕了。因此,必须将 `register` 方法与 `fastify-plugin` 结合使用。详见 [Plugins](Plugins.md)。
|
||||
|
||||
通过装饰器 API 来自定义对象,底层的 Javascript 引擎便能对其进行优化。这是因为引擎能在所有的对象实例被初始化与使用前,定义好它们的形状 (shape)。下文的例子则是不推荐的做法,因为它在对象的生命周期中修改了它们的形状:
|
||||
|
||||
@ -13,32 +13,32 @@
|
||||
|
||||
// 在调用请求处理函数之前
|
||||
// 将 user 属性添加到请求上。
|
||||
fastify.addHook('preHandler', function (req, reply, done) {
|
||||
req.user = 'Bob Dylan'
|
||||
done()
|
||||
})
|
||||
fastify.addHook("preHandler", function (req, reply, done) {
|
||||
req.user = "Bob Dylan";
|
||||
done();
|
||||
});
|
||||
|
||||
// 在处理函数中使用 user 属性。
|
||||
fastify.get('/', function (req, reply) {
|
||||
reply.send(`Hello, ${req.user}`)
|
||||
})
|
||||
fastify.get("/", function (req, reply) {
|
||||
reply.send(`Hello, ${req.user}`);
|
||||
});
|
||||
```
|
||||
|
||||
由于上述例子在请求对象初始化完成后,还改动了它的形状,因此 JavaScript 引擎必须对该对象去优化。使用装饰器 API 能避开去优化问题:
|
||||
|
||||
```js
|
||||
// 使用装饰器为请求对象添加 'user' 属性。
|
||||
fastify.decorateRequest('user', '')
|
||||
fastify.decorateRequest("user", "");
|
||||
|
||||
// 更新属性。
|
||||
fastify.addHook('preHandler', (req, reply, done) => {
|
||||
req.user = 'Bob Dylan'
|
||||
done()
|
||||
})
|
||||
fastify.addHook("preHandler", (req, reply, done) => {
|
||||
req.user = "Bob Dylan";
|
||||
done();
|
||||
});
|
||||
// 最后访问它。
|
||||
fastify.get('/', (req, reply) => {
|
||||
reply.send(`Hello, ${req.user}!`)
|
||||
})
|
||||
fastify.get("/", (req, reply) => {
|
||||
reply.send(`Hello, ${req.user}!`);
|
||||
});
|
||||
```
|
||||
|
||||
装饰器的初始值应该尽可能地与其未来将被设置的值接近。例如,字符串类型的装饰器的初始值为 `''`,对象或函数类型的初始值为 `null`。
|
||||
@ -48,9 +48,11 @@ fastify.get('/', (req, reply) => {
|
||||
更多此话题的内容,请见 [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) 实例。
|
||||
@ -58,42 +60,42 @@ fastify.get('/', (req, reply) => {
|
||||
例如,为其添加一个新方法:
|
||||
|
||||
```js
|
||||
fastify.decorate('utility', function () {
|
||||
fastify.decorate("utility", function () {
|
||||
// 新功能的代码
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
正如上文所述,还可以传递非函数的值:
|
||||
|
||||
```js
|
||||
fastify.decorate('conf', {
|
||||
db: 'some.db',
|
||||
port: 3000
|
||||
})
|
||||
fastify.decorate("conf", {
|
||||
db: "some.db",
|
||||
port: 3000,
|
||||
});
|
||||
```
|
||||
|
||||
通过装饰属性的名称便可访问值:
|
||||
|
||||
```js
|
||||
fastify.utility()
|
||||
fastify.utility();
|
||||
|
||||
console.log(fastify.conf.db)
|
||||
console.log(fastify.conf.db);
|
||||
```
|
||||
|
||||
[路由](Routes.md)函数的 `this` 指向 [Fastify server](Server.md):
|
||||
|
||||
```js
|
||||
fastify.decorate('db', new DbConnection())
|
||||
fastify.decorate("db", new DbConnection());
|
||||
|
||||
fastify.get('/', async function (request, reply) {
|
||||
reply({hello: await this.db.query('world')})
|
||||
})
|
||||
fastify.get("/", async function (request, reply) {
|
||||
reply({ hello: await this.db.query("world") });
|
||||
});
|
||||
```
|
||||
|
||||
可选的 `dependencies` 参数用于指定当前装饰器所依赖的其他装饰器列表。这个列表包含了其他装饰器的名称字符串。在下面的例子里,装饰器 "utility" 依赖于 "greet" 和 "log":
|
||||
|
||||
```js
|
||||
fastify.decorate('utility', fn, ['greet', 'log'])
|
||||
fastify.decorate("utility", fn, ["greet", "log"]);
|
||||
```
|
||||
|
||||
注:使用箭头函数会破坏 `FastifyInstance` 的 `this` 绑定。
|
||||
@ -101,14 +103,15 @@ fastify.decorate('utility', fn, ['greet', 'log'])
|
||||
一旦有依赖项不满足,`decorate` 方法便会抛出异常。依赖项检查是在服务器实例启动前进行的,因此,在运行时不会发生异常。
|
||||
|
||||
#### `decorateReply(name, value, [dependencies])`
|
||||
|
||||
<a name="decorate-reply"></a>
|
||||
|
||||
顾名思义,`decorateReply` 向 `Reply` 核心对象添加新的方法或属性:
|
||||
|
||||
```js
|
||||
fastify.decorateReply('utility', function () {
|
||||
fastify.decorateReply("utility", function () {
|
||||
// 新功能的代码
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
注:使用箭头函数会破坏 `this` 和 Fastify `Reply` 实例的绑定。
|
||||
@ -117,34 +120,36 @@ fastify.decorateReply('utility', function () {
|
||||
|
||||
```js
|
||||
// 反面教材
|
||||
fastify.decorateReply('foo', { bar: 'fizz'})
|
||||
fastify.decorateReply("foo", { bar: "fizz" });
|
||||
```
|
||||
|
||||
在这个例子里,该对象的引用存在于所有请求中,导致**任何更改都会影响到所有请求,并可能触发安全漏洞或内存泄露**。要合适地封装请求对象,请在 [`'onRequest'` 钩子](Hooks.md#onrequest)里设置新的值。示例如下:
|
||||
|
||||
```js
|
||||
const fp = require('fastify-plugin')
|
||||
const fp = require("fastify-plugin");
|
||||
|
||||
async function myPlugin (app) {
|
||||
app.decorateRequest('foo', null)
|
||||
app.addHook('onRequest', async (req, reply) => {
|
||||
req.foo = { bar: 42 }
|
||||
})
|
||||
async function myPlugin(app) {
|
||||
app.decorateRequest("foo", null);
|
||||
app.addHook("onRequest", async (req, reply) => {
|
||||
req.foo = { bar: 42 };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fp(myPlugin)
|
||||
module.exports = fp(myPlugin);
|
||||
```
|
||||
|
||||
关于 `dependencies` 参数,请见 [`decorate`](#decorate)。
|
||||
|
||||
#### `decorateRequest(name, value, [dependencies])`
|
||||
|
||||
<a name="decorate-request"></a>
|
||||
|
||||
同理,`decorateRequest` 向 `Request` 核心对象添加新的方法或属性:
|
||||
|
||||
```js
|
||||
fastify.decorateRequest('utility', function () {
|
||||
fastify.decorateRequest("utility", function () {
|
||||
// 新功能的代码
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
注:使用箭头函数会破坏 `this` 和 Fastify `Request` 实例的绑定。
|
||||
@ -153,110 +158,120 @@ fastify.decorateRequest('utility', function () {
|
||||
|
||||
```js
|
||||
// 反面教材
|
||||
fastify.decorateRequest('foo', { bar: 'fizz'})
|
||||
fastify.decorateRequest("foo", { bar: "fizz" });
|
||||
```
|
||||
|
||||
在这个例子里,该对象的引用存在于所有请求中,导致**任何更改都会影响到所有请求,并可能触发安全漏洞或内存泄露**。要合适地封装请求对象,请在 [`'onRequest'` 钩子](Hooks.md#onrequest)里设置新的值。示例如下:
|
||||
|
||||
```js
|
||||
const fp = require('fastify-plugin')
|
||||
const fp = require("fastify-plugin");
|
||||
|
||||
async function myPlugin (app) {
|
||||
app.decorateRequest('foo', null)
|
||||
app.addHook('onRequest', async (req, reply) => {
|
||||
req.foo = { bar: 42 }
|
||||
})
|
||||
async function myPlugin(app) {
|
||||
app.decorateRequest("foo", null);
|
||||
app.addHook("onRequest", async (req, reply) => {
|
||||
req.foo = { bar: 42 };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fp(myPlugin)
|
||||
module.exports = fp(myPlugin);
|
||||
```
|
||||
|
||||
关于 `dependencies` 参数,请见 [`decorate`](#decorate)。
|
||||
|
||||
#### `hasDecorator(name)`
|
||||
|
||||
<a name="has-decorator"></a>
|
||||
|
||||
用于检查服务器实例上是否存在某个装饰器:
|
||||
|
||||
```js
|
||||
fastify.hasDecorator('utility')
|
||||
fastify.hasDecorator("utility");
|
||||
```
|
||||
|
||||
#### hasRequestDecorator
|
||||
|
||||
<a name="has-request-decorator"></a>
|
||||
|
||||
用于检查 Request 实例上是否存在某个装饰器:
|
||||
|
||||
```js
|
||||
fastify.hasRequestDecorator('utility')
|
||||
fastify.hasRequestDecorator("utility");
|
||||
```
|
||||
|
||||
#### hasReplyDecorator
|
||||
|
||||
<a name="has-reply-decorator"></a>
|
||||
|
||||
用于检查 Reply 实例上是否存在某个装饰器:
|
||||
|
||||
```js
|
||||
fastify.hasReplyDecorator('utility')
|
||||
fastify.hasReplyDecorator("utility");
|
||||
```
|
||||
|
||||
### 装饰器与封装
|
||||
|
||||
<a name="decorators-encapsulation"></a>
|
||||
|
||||
在 **封装** 的同一个上下文中,如果通过 `decorate`、`decorateRequest` 以及 `decorateReply` 多次定义了一个同名的的装饰器,将会抛出一个异常。
|
||||
|
||||
下面的示例会抛出异常:
|
||||
```js
|
||||
const server = require('fastify')()
|
||||
server.decorateReply('view', function (template, args) {
|
||||
|
||||
```js
|
||||
const server = require("fastify")();
|
||||
server.decorateReply("view", function (template, args) {
|
||||
// 页面渲染引擎的代码。
|
||||
})
|
||||
server.get('/', (req, reply) => {
|
||||
reply.view('/index.html', { hello: 'world' })
|
||||
})
|
||||
});
|
||||
server.get("/", (req, reply) => {
|
||||
reply.view("/index.html", { hello: "world" });
|
||||
});
|
||||
// 当在其他地方定义
|
||||
// view 装饰器时,抛出异常。
|
||||
server.decorateReply('view', function (template, args) {
|
||||
server.decorateReply("view", function (template, args) {
|
||||
// 另一个渲染引擎。
|
||||
})
|
||||
server.listen(3000)
|
||||
});
|
||||
server.listen(3000);
|
||||
```
|
||||
|
||||
但下面这个例子不会抛异常:
|
||||
|
||||
```js
|
||||
const server = require('fastify')()
|
||||
server.decorateReply('view', function (template, args) {
|
||||
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)
|
||||
});
|
||||
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.decorate("foo", {
|
||||
getter() {
|
||||
return "a getter";
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
上例会在 Fastify 实例中定义一个 `foo` 属性:
|
||||
|
||||
```js
|
||||
console.log(fastify.foo) // 'a getter'
|
||||
console.log(fastify.foo); // 'a getter'
|
||||
```
|
||||
|
@ -1,6 +1,7 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
<a id="encapsulation"></a>
|
||||
|
||||
## 封装
|
||||
|
||||
“封装上下文”是 Fastify 的一个基础特性,负责控制[路由](./Routes.md)能访问的[装饰器](./Decorators.md)、[钩子](./Hooks.md)以及[插件](./Plugins.md)。下图是封装上下文的抽象表现:
|
||||
@ -12,73 +13,73 @@
|
||||
1. _顶层上下文 (root context)_
|
||||
2. 三个 _顶层插件 (root plugin)_
|
||||
3. 两个 _子上下文 (child context)_,每个 _子上下文_ 拥有
|
||||
* 两个 _子插件 (child plugin)_
|
||||
* 一个 _孙子上下文 (grandchild context)_,其又拥有
|
||||
- 三个 _子插件 (child plugin)_
|
||||
- 两个 _子插件 (child plugin)_
|
||||
- 一个 _孙子上下文 (grandchild context)_,其又拥有
|
||||
- 三个 _子插件 (child plugin)_
|
||||
|
||||
任意 _子上下文_ 或 _孙子上下文_ 都有权访问 _顶层插件_。_孙子上下文_ 有权访问它上级的 _子上下文_ 中注册的 _子插件_,但 _子上下文_ *无权* 访问它下级的 _孙子上下文中_ 注册的 _子插件_。
|
||||
任意 _子上下文_ 或 _孙子上下文_ 都有权访问 _顶层插件_。_孙子上下文_ 有权访问它上级的 _子上下文_ 中注册的 _子插件_,但 _子上下文_ _无权_ 访问它下级的 _孙子上下文中_ 注册的 _子插件_。
|
||||
|
||||
在 Fastify 中,除了 _顶层上下文_,一切皆为[插件](./Plugins.md)。下文的例子也不例外,所有的“上下文”和“插件”都是包含装饰器、钩子、插件及路由的插件。该例子为有三个路由的 REST API 服务器,第一个路由 (`/one`) 需要鉴权 (使用 [fastify-bearer-auth][bearer]),第二个路由 (`/two`) 无需鉴权,第三个路由 (`/three`) 有权访问第二个路由的上下文:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fastify = require('fastify')()
|
||||
const fastify = require("fastify")();
|
||||
|
||||
fastify.decorateRequest('answer', 42)
|
||||
fastify.decorateRequest("answer", 42);
|
||||
|
||||
fastify.register(async function authenticatedContext (childServer) {
|
||||
childServer.register(require('fastify-bearer-auth'), { keys: ['abc123'] })
|
||||
fastify.register(async function authenticatedContext(childServer) {
|
||||
childServer.register(require("fastify-bearer-auth"), { keys: ["abc123"] });
|
||||
|
||||
childServer.route({
|
||||
path: '/one',
|
||||
method: 'GET',
|
||||
handler (request, response) {
|
||||
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
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
bar: request.bar,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
fastify.register(async function publicContext (childServer) {
|
||||
childServer.decorateRequest('foo', 'foo')
|
||||
fastify.register(async function publicContext(childServer) {
|
||||
childServer.decorateRequest("foo", "foo");
|
||||
|
||||
childServer.route({
|
||||
path: '/two',
|
||||
method: 'GET',
|
||||
handler (request, response) {
|
||||
path: "/two",
|
||||
method: "GET",
|
||||
handler(request, response) {
|
||||
response.send({
|
||||
answer: request.answer,
|
||||
foo: request.foo,
|
||||
// request.bar 会是 undefined,因为该值是在 grandchildContext 中定义的
|
||||
bar: request.bar
|
||||
})
|
||||
}
|
||||
})
|
||||
bar: request.bar,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
childServer.register(async function grandchildContext (grandchildServer) {
|
||||
grandchildServer.decorateRequest('bar', 'bar')
|
||||
childServer.register(async function grandchildContext(grandchildServer) {
|
||||
grandchildServer.decorateRequest("bar", "bar");
|
||||
|
||||
grandchildServer.route({
|
||||
path: '/three',
|
||||
method: 'GET',
|
||||
handler (request, response) {
|
||||
path: "/three",
|
||||
method: "GET",
|
||||
handler(request, response) {
|
||||
response.send({
|
||||
answer: request.answer,
|
||||
foo: request.foo,
|
||||
bar: request.bar
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
bar: request.bar,
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
fastify.listen(8000)
|
||||
fastify.listen(8000);
|
||||
```
|
||||
|
||||
上面的例子展示了所有封装相关的概念:
|
||||
@ -102,6 +103,7 @@ fastify.listen(8000)
|
||||
[bearer]: https://github.com/fastify/fastify-bearer-auth
|
||||
|
||||
<a id="shared-context"></a>
|
||||
|
||||
## 在上下文间共享
|
||||
|
||||
请注意,在上文例子中,每个上下文都 _仅_ 从父级上下文进行继承,而父级上下文无权访问后代上下文中定义的实体。在某些情况下,我们并不想要这一默认行为。使用 [fastify-plugin][fastify-plugin] ,便能允许父级上下文访问到后代上下文中定义的实体。
|
||||
@ -109,50 +111,50 @@ fastify.listen(8000)
|
||||
假设上例的 `publicContext` 需要获取 `grandchildContext` 中定义的 `bar` 装饰器,我们可以重写代码如下:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fastify = require('fastify')()
|
||||
const fastifyPlugin = require('fastify-plugin')
|
||||
const fastify = require("fastify")();
|
||||
const fastifyPlugin = require("fastify-plugin");
|
||||
|
||||
fastify.decorateRequest('answer', 42)
|
||||
fastify.decorateRequest("answer", 42);
|
||||
|
||||
// 为了代码清晰,这里省略了 `authenticatedContext`
|
||||
|
||||
fastify.register(async function publicContext (childServer) {
|
||||
childServer.decorateRequest('foo', 'foo')
|
||||
fastify.register(async function publicContext(childServer) {
|
||||
childServer.decorateRequest("foo", "foo");
|
||||
|
||||
childServer.route({
|
||||
path: '/two',
|
||||
method: 'GET',
|
||||
handler (request, response) {
|
||||
path: "/two",
|
||||
method: "GET",
|
||||
handler(request, response) {
|
||||
response.send({
|
||||
answer: request.answer,
|
||||
foo: request.foo,
|
||||
bar: request.bar
|
||||
})
|
||||
}
|
||||
})
|
||||
bar: request.bar,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
childServer.register(fastifyPlugin(grandchildContext))
|
||||
childServer.register(fastifyPlugin(grandchildContext));
|
||||
|
||||
async function grandchildContext (grandchildServer) {
|
||||
grandchildServer.decorateRequest('bar', 'bar')
|
||||
async function grandchildContext(grandchildServer) {
|
||||
grandchildServer.decorateRequest("bar", "bar");
|
||||
|
||||
grandchildServer.route({
|
||||
path: '/three',
|
||||
method: 'GET',
|
||||
handler (request, response) {
|
||||
path: "/three",
|
||||
method: "GET",
|
||||
handler(request, response) {
|
||||
response.send({
|
||||
answer: request.answer,
|
||||
foo: request.foo,
|
||||
bar: request.bar
|
||||
})
|
||||
}
|
||||
})
|
||||
bar: request.bar,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
fastify.listen(8000)
|
||||
fastify.listen(8000);
|
||||
```
|
||||
|
||||
重启服务,访问 `/two` 和 `/three` 路由:
|
||||
@ -164,4 +166,4 @@ fastify.listen(8000)
|
||||
{"answer":42,"foo":"foo","bar":"bar"}
|
||||
```
|
||||
|
||||
[fastify-plugin]: https://github.com/fastify/fastify-plugin
|
||||
[fastify-plugin]: https://github.com/fastify/fastify-plugin
|
||||
|
@ -1,28 +1,35 @@
|
||||
<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') })`
|
||||
@ -35,6 +42,7 @@ Fastify 遵循不全则无的原则,旨在精而优。因此,确保正确处
|
||||
### Fastify 生命周期钩子的错误,及自定义错误控制函数
|
||||
|
||||
在[钩子](Hooks.md#manage-errors-from-a-hook)的文档中提到:
|
||||
|
||||
> 假如在钩子执行过程中发生错误,只需把它传递给 `done()`,Fastify 便会自动地关闭请求,并向用户发送合适的错误代码。
|
||||
|
||||
如果通过 `setErrorHandler` 自定义了一个错误函数,那么错误会被引导到那里,否则被引导到 Fastify 默认的错误函数中去。
|
||||
@ -42,6 +50,7 @@ Fastify 遵循不全则无的原则,旨在精而优。因此,确保正确处
|
||||
自定义错误函数应该考虑以下几点:
|
||||
|
||||
- 你可以调用 `reply.send(data)`,正如在[常规路由](Reply.md#senddata)中那样
|
||||
|
||||
- object 会被序列化,并触发 `preSerialization` 钩子 (假如有定义的话)
|
||||
- string、buffer 及 stream 会被直接发送至客户端 (不会序列化),并附带上合适的 header。
|
||||
|
||||
@ -50,19 +59,23 @@ Fastify 遵循不全则无的原则,旨在精而优。因此,确保正确处
|
||||
- 在同一个钩子内,一个错误不会被触发两次。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 大小超过限制。
|
||||
@ -70,101 +83,121 @@ Fastify 遵循不全则无的原则,旨在精而优。因此,确保正确处
|
||||
可通过 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
|
||||
|
||||
发生了未定义的错误。
|
||||
发生了未定义的错误。
|
||||
|
@ -9,43 +9,45 @@
|
||||
### 基本设置
|
||||
|
||||
```js
|
||||
const S = require('fluent-json-schema')
|
||||
const S = require("fluent-json-schema");
|
||||
|
||||
// 你可以使用如下的一个对象,或查询数据库来获取数据
|
||||
const MY_KEYS = {
|
||||
KEY1: 'ONE',
|
||||
KEY2: 'TWO'
|
||||
}
|
||||
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()))
|
||||
.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())
|
||||
.prop("name", S.string())
|
||||
.prop("excitement", S.integer());
|
||||
|
||||
const paramsJsonSchema = S.object()
|
||||
.prop('par1', S.string())
|
||||
.prop('par2', S.integer())
|
||||
.prop("par1", S.string())
|
||||
.prop("par2", S.integer());
|
||||
|
||||
const headersJsonSchema = S.object()
|
||||
.prop('x-foo', S.string().required())
|
||||
const headersJsonSchema = S.object().prop("x-foo", S.string().required());
|
||||
|
||||
const schema = {
|
||||
body: bodyJsonSchema,
|
||||
querystring: queryStringJsonSchema, // (或) query: queryStringJsonSchema
|
||||
params: paramsJsonSchema,
|
||||
headers: headersJsonSchema
|
||||
}
|
||||
headers: headersJsonSchema,
|
||||
};
|
||||
|
||||
fastify.post('/the/url', { schema }, handler)
|
||||
fastify.post("/the/url", { schema }, handler);
|
||||
```
|
||||
|
||||
### 复用
|
||||
@ -59,57 +61,62 @@ fastify.post('/the/url', { schema }, handler)
|
||||
|
||||
```js
|
||||
const addressSchema = S.object()
|
||||
.id('#address')
|
||||
.prop('line1').required()
|
||||
.prop('line2')
|
||||
.prop('country').required()
|
||||
.prop('city').required()
|
||||
.prop('zipcode').required()
|
||||
.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
|
||||
.id("https://fastify/demo")
|
||||
.definition("addressSchema", addressSchema)
|
||||
.definition("otherSchema", otherSchema); // 你可以任意添加需要的 schema
|
||||
|
||||
fastify.addSchema(commonSchemas)
|
||||
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()
|
||||
.prop("residence", S.ref("https://fastify/demo#address"))
|
||||
.required()
|
||||
.prop("office", S.ref("https://fastify/demo#/definitions/addressSchema"))
|
||||
.required();
|
||||
|
||||
const schema = { body: bodyJsonSchema }
|
||||
const schema = { body: bodyJsonSchema };
|
||||
|
||||
fastify.post('/the/url', { schema }, handler)
|
||||
fastify.post("/the/url", { schema }, handler);
|
||||
```
|
||||
|
||||
|
||||
**`替换方式`**:在验证阶段之前,使用共用 schema 替换某些字段。
|
||||
|
||||
```js
|
||||
const sharedAddressSchema = {
|
||||
$id: 'sharedAddress',
|
||||
type: 'object',
|
||||
required: ['line1', 'country', 'city', 'zipcode'],
|
||||
$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)
|
||||
line1: { type: "string" },
|
||||
line2: { type: "string" },
|
||||
country: { type: "string" },
|
||||
city: { type: "string" },
|
||||
zipcode: { type: "string" },
|
||||
},
|
||||
};
|
||||
fastify.addSchema(sharedAddressSchema);
|
||||
|
||||
const bodyJsonSchema = {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
vacation: 'sharedAddress#'
|
||||
}
|
||||
}
|
||||
vacation: "sharedAddress#",
|
||||
},
|
||||
};
|
||||
|
||||
const schema = { body: bodyJsonSchema }
|
||||
const schema = { body: bodyJsonSchema };
|
||||
|
||||
fastify.post('/the/url', { schema }, handler)
|
||||
fastify.post("/the/url", { schema }, handler);
|
||||
```
|
||||
|
||||
特别注意:你可以在 `fastify.addSchema` 方法里混用 `$ref` 和 `替换方式`。
|
||||
特别注意:你可以在 `fastify.addSchema` 方法里混用 `$ref` 和 `替换方式`。
|
||||
|
@ -1,78 +1,88 @@
|
||||
<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'
|
||||
import Fastify from "fastify";
|
||||
const fastify = Fastify({
|
||||
logger: true
|
||||
})
|
||||
logger: true,
|
||||
});
|
||||
// CommonJs
|
||||
const fastify = require('fastify')({
|
||||
logger: true
|
||||
})
|
||||
const fastify = require("fastify")({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
// 声明路由
|
||||
fastify.get('/', function (request, reply) {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
fastify.get("/", function (request, reply) {
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
|
||||
// 启动服务!
|
||||
fastify.listen(3000, function (err, address) {
|
||||
if (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
// 服务器监听地址:${address}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
更喜欢使用 `async/await`?Fastify 对其提供了开箱即用的支持。<br>
|
||||
*(我们还建议使用 [make-promises-safe](https://github.com/mcollina/make-promises-safe) 来避免文件描述符 (file descriptor) 及内存的泄露)*
|
||||
_(我们还建议使用 [make-promises-safe](https://github.com/mcollina/make-promises-safe) 来避免文件描述符 (file descriptor) 及内存的泄露)_
|
||||
|
||||
```js
|
||||
// ESM
|
||||
import Fastify from 'fastify'
|
||||
import Fastify from "fastify";
|
||||
const fastify = Fastify({
|
||||
logger: true
|
||||
})
|
||||
logger: true,
|
||||
});
|
||||
// CommonJs
|
||||
const fastify = require('fastify')({
|
||||
logger: true
|
||||
})
|
||||
const fastify = require("fastify")({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.get('/', async (request, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
fastify.get("/", async (request, reply) => {
|
||||
return { hello: "world" };
|
||||
});
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen(3000)
|
||||
await fastify.listen(3000);
|
||||
} catch (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
start()
|
||||
};
|
||||
start();
|
||||
```
|
||||
|
||||
如此简单,棒极了!<br>
|
||||
@ -80,16 +90,17 @@ start()
|
||||
幸运的是,Fastify 提供了一个易于使用的平台,它能帮助你解决不限于上述的诸多问题!
|
||||
|
||||
> ## 注
|
||||
>
|
||||
> 本文档中的示例,默认情况下只监听本地 `127.0.0.1` 端口。要监听所有有效的 IPv4 端口,需要将代码修改为监听 `0.0.0.0`,如下所示:
|
||||
>
|
||||
> ```js
|
||||
> fastify.listen(3000, '0.0.0.0', function (err, address) {
|
||||
> fastify.listen(3000, "0.0.0.0", function (err, address) {
|
||||
> if (err) {
|
||||
> fastify.log.error(err)
|
||||
> process.exit(1)
|
||||
> fastify.log.error(err);
|
||||
> process.exit(1);
|
||||
> }
|
||||
> fastify.log.info(`server listening on ${address}`)
|
||||
> })
|
||||
> fastify.log.info(`server listening on ${address}`);
|
||||
> });
|
||||
> ```
|
||||
>
|
||||
> 类似地,`::1` 表示只允许本地的 IPv6 连接。而 `::` 表示允许所有 IPv6 地址的接入,当操作系统支持时,所有的 IPv4 地址也会被允许。
|
||||
@ -97,57 +108,61 @@ start()
|
||||
> 当使用 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'
|
||||
import Fastify from "fastify";
|
||||
import firstRoute from "./our-first-route";
|
||||
const fastify = Fastify({
|
||||
logger: true
|
||||
})
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.register(firstRoute)
|
||||
fastify.register(firstRoute);
|
||||
|
||||
fastify.listen(3000, function (err, address) {
|
||||
if (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
// 服务器监听地址:${address}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
// CommonJs
|
||||
const fastify = require('fastify')({
|
||||
logger: true
|
||||
})
|
||||
const fastify = require("fastify")({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.register(require('./our-first-route'))
|
||||
fastify.register(require("./our-first-route"));
|
||||
|
||||
fastify.listen(3000, function (err, address) {
|
||||
if (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
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' }
|
||||
})
|
||||
async function routes(fastify, options) {
|
||||
fastify.get("/", async (request, reply) => {
|
||||
return { hello: "world" };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = routes
|
||||
module.exports = routes;
|
||||
```
|
||||
|
||||
这个例子调用了 `register` API,它是 Fastify 框架的核心,也是添加路由、插件等的唯一方法。
|
||||
|
||||
在本文的开头,我们提到 Fastify 提供了帮助应用异步引导的基础功能。为什么这一功能十分重要呢?
|
||||
@ -164,110 +179,110 @@ 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'
|
||||
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)
|
||||
logger: true,
|
||||
});
|
||||
fastify.register(dbConnector);
|
||||
fastify.register(firstRoute);
|
||||
|
||||
fastify.listen(3000, function (err, address) {
|
||||
if (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
// 服务器监听地址:${address}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
```js
|
||||
// CommonJs
|
||||
const fastify = require('fastify')({
|
||||
logger: true
|
||||
})
|
||||
const fastify = require("fastify")({
|
||||
logger: true,
|
||||
});
|
||||
|
||||
fastify.register(require('./our-db-connector'))
|
||||
fastify.register(require('./our-first-route'))
|
||||
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)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
// 服务器监听地址:${address}
|
||||
})
|
||||
|
||||
});
|
||||
```
|
||||
|
||||
**our-db-connector.js**
|
||||
|
||||
```js
|
||||
// ESM
|
||||
import fastifyPlugin from 'fastify-plugin'
|
||||
import fastifyMongo from 'fastify-mongodb'
|
||||
import fastifyPlugin from "fastify-plugin";
|
||||
import fastifyMongo from "fastify-mongodb";
|
||||
|
||||
async function dbConnector (fastify, options) {
|
||||
async function dbConnector(fastify, options) {
|
||||
fastify.register(fastifyMongo, {
|
||||
url: 'mongodb://localhost:27017/test_database'
|
||||
})
|
||||
url: "mongodb://localhost:27017/test_database",
|
||||
});
|
||||
}
|
||||
|
||||
// 用 fastify-plugin 包装插件,以使插件中声明的装饰器、钩子函数暴露在根作用域里。
|
||||
module.exports = fastifyPlugin(dbConnector)
|
||||
|
||||
module.exports = fastifyPlugin(dbConnector);
|
||||
```
|
||||
|
||||
```js
|
||||
// CommonJs
|
||||
const fastifyPlugin = require('fastify-plugin')
|
||||
const fastifyPlugin = require("fastify-plugin");
|
||||
|
||||
async function dbConnector (fastify, options) {
|
||||
fastify.register(require('fastify-mongodb'), {
|
||||
url: 'mongodb://localhost:27017/test_database'
|
||||
})
|
||||
async function dbConnector(fastify, options) {
|
||||
fastify.register(require("fastify-mongodb"), {
|
||||
url: "mongodb://localhost:27017/test_database",
|
||||
});
|
||||
}
|
||||
// 用 fastify-plugin 包装插件,以使插件中声明的装饰器、钩子函数暴露在根作用域里。
|
||||
module.exports = fastifyPlugin(dbConnector)
|
||||
|
||||
module.exports = fastifyPlugin(dbConnector);
|
||||
```
|
||||
|
||||
**our-first-route.js**
|
||||
|
||||
```js
|
||||
async function routes (fastify, options) {
|
||||
const collection = fastify.mongo.db.collection('test_collection')
|
||||
async function routes(fastify, options) {
|
||||
const collection = fastify.mongo.db.collection("test_collection");
|
||||
|
||||
fastify.get('/', async (request, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
fastify.get("/", async (request, reply) => {
|
||||
return { hello: "world" };
|
||||
});
|
||||
|
||||
fastify.get('/animals', async (request, reply) => {
|
||||
const result = await collection.find().toArray()
|
||||
fastify.get("/animals", async (request, reply) => {
|
||||
const result = await collection.find().toArray();
|
||||
if (result.length === 0) {
|
||||
throw new Error('No documents found')
|
||||
throw new Error("No documents found");
|
||||
}
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
|
||||
fastify.get('/animals/:animal', async (request, reply) => {
|
||||
const result = await collection.findOne({ animal: request.params.animal })
|
||||
fastify.get("/animals/:animal", async (request, reply) => {
|
||||
const result = await collection.findOne({ animal: request.params.animal });
|
||||
if (!result) {
|
||||
throw new Error('Invalid value')
|
||||
throw new Error("Invalid value");
|
||||
}
|
||||
return result
|
||||
})
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = routes
|
||||
module.exports = routes;
|
||||
```
|
||||
|
||||
哇,真是快啊!<br>
|
||||
介绍了一些新概念后,让我们回顾一下迄今为止都做了些什么吧。<br>
|
||||
如你所见,我们可以使用 `register` 来注册数据库连接器或者路由。
|
||||
这是 Fastify 最棒的特性之一了!它使得插件按声明的顺序来加载,唯有当前插件加载完毕后,才会加载下一个插件。如此,我们便可以在第一个插件中注册数据库连接器,并在第二个插件中使用它。*(参见 [这里](Plugins.md#handle-the-scope) 了解如何处理插件的作用域)*。
|
||||
这是 Fastify 最棒的特性之一了!它使得插件按声明的顺序来加载,唯有当前插件加载完毕后,才会加载下一个插件。如此,我们便可以在第一个插件中注册数据库连接器,并在第二个插件中使用它。_(参见 [这里](Plugins.md#handle-the-scope) 了解如何处理插件的作用域)_。
|
||||
当调用函数 `fastify.listen()`、`fastify.inject()` 或 `fastify.ready()` 时,插件便开始加载了。
|
||||
|
||||
MongoDB 的插件使用了 `decorate` API,以便在 Fastify 的命名空间下添加自定义对象,如此一来,你就可以在所有地方直接使用这些对象了。我们鼓励运用这一 API,因为它有助于提高代码复用率,减少重复的代码或逻辑。
|
||||
@ -275,8 +290,11 @@ MongoDB 的插件使用了 `decorate` API,以便在 Fastify 的命名空间下
|
||||
更深入的内容,例如插件如何运作、如何新建,以及使用 Fastify 全部的 API 去处理复杂的异步引导的细节,请看[插件指南](Plugins-Guide.md)。
|
||||
|
||||
<a name="plugin-loading-order"></a>
|
||||
|
||||
### 插件加载顺序
|
||||
|
||||
为了保证应用的行为一致且可预测,我们强烈建议你采用以下的顺序来组织代码:
|
||||
|
||||
```
|
||||
└── 来自 Fastify 生态的插件
|
||||
└── 你自己的插件
|
||||
@ -284,8 +302,10 @@ MongoDB 的插件使用了 `decorate` API,以便在 Fastify 的命名空间下
|
||||
└── 钩子函数
|
||||
└── 你的服务应用
|
||||
```
|
||||
|
||||
这确保了你总能访问当前作用域下声明的所有属性。<br/>
|
||||
如前文所述,Fastify 提供了一个可靠的封装模型,它能帮助你的应用成为单一且独立的服务。假如你要为某些路由单独地注册插件,只需复写上述的结构就足够了。
|
||||
|
||||
```
|
||||
└── 来自 Fastify 生态的插件
|
||||
└── 你自己的插件
|
||||
@ -309,68 +329,82 @@ MongoDB 的插件使用了 `decorate` API,以便在 Fastify 的命名空间下
|
||||
```
|
||||
|
||||
<a name="validate-data"></a>
|
||||
|
||||
### 验证数据
|
||||
|
||||
数据的验证在我们的框架中是极为重要的一环,也是核心的概念。<br>
|
||||
Fastify 使用 [JSON Schema](https://json-schema.org/) 验证来访的请求。(也支持宽松的 JTD schema,但首先得禁用 `jsonShorthand`)。
|
||||
|
||||
让我们来看一个验证路由的例子:
|
||||
|
||||
```js
|
||||
const opts = {
|
||||
schema: {
|
||||
body: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
someKey: { type: 'string' },
|
||||
someOtherKey: { type: 'number' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
someKey: { type: "string" },
|
||||
someOtherKey: { type: "number" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fastify.post('/', opts, async (request, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
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',
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
hello: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
fastify.get('/', opts, async (request, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
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`:
|
||||
@ -382,6 +416,7 @@ npm i fastify-cli
|
||||
你还可以加入 `-g` 选项来全局安装它。
|
||||
|
||||
接下来,在 `package.json` 中添加如下行:
|
||||
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
@ -391,25 +426,30 @@ npm i fastify-cli
|
||||
```
|
||||
|
||||
然后,创建你的服务器文件:
|
||||
|
||||
```js
|
||||
// server.js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
module.exports = async function (fastify, opts) {
|
||||
fastify.get('/', async (request, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
}
|
||||
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)
|
||||
|
||||
|
@ -8,26 +8,26 @@ _Fastify_ 提供了从 Node 8.8.0 开始的对 HTTP2 **实验性支持**,_Fast
|
||||
|
||||
### 安全 (HTTPS)
|
||||
|
||||
所有的现代浏览器都__只能通过安全的连接__ 支持 HTTP2:
|
||||
所有的现代浏览器都**只能通过安全的连接** 支持 HTTP2:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fastify = require('fastify')({
|
||||
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'))
|
||||
}
|
||||
})
|
||||
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.get("/", function (request, reply) {
|
||||
reply.code(200).send({ hello: "world" });
|
||||
});
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
ALPN 协商允许在同一个 socket 上支持 HTTPS 和 HTTP/2。
|
||||
@ -36,25 +36,25 @@ Node 核心 `req` 和 `res` 对象可以是 [HTTP/1](https://nodejs.org/api/http
|
||||
_Fastify_ 自带支持开箱即用:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const fastify = require('fastify')({
|
||||
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'))
|
||||
}
|
||||
})
|
||||
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.get("/", function (request, reply) {
|
||||
reply.code(200).send({ hello: "world" });
|
||||
});
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
你可以像这样测试你的新服务器:
|
||||
@ -68,17 +68,17 @@ $ npx h2url https://localhost:3000
|
||||
如果你搭建微服务,你可以纯文本连接 HTTP2,但是浏览器不支持这样做。
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fastify = require('fastify')({
|
||||
http2: true
|
||||
})
|
||||
const fastify = require("fastify")({
|
||||
http2: true,
|
||||
});
|
||||
|
||||
fastify.get('/', function (request, reply) {
|
||||
reply.code(200).send({ hello: 'world' })
|
||||
})
|
||||
fastify.get("/", function (request, reply) {
|
||||
reply.code(200).send({ hello: "world" });
|
||||
});
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
你可以像这样测试你的新服务器:
|
||||
@ -86,4 +86,3 @@ fastify.listen(3000)
|
||||
```
|
||||
$ npx h2url http://localhost:3000
|
||||
```
|
||||
|
||||
|
@ -36,21 +36,24 @@
|
||||
[生命周期](Lifecycle.md)一文清晰地展示了各个钩子执行的位置。<br>
|
||||
钩子可被封装,因此可以运用在特定的路由上。更多信息请看[作用域](#scope)一节。
|
||||
|
||||
在请求/响应中,有八个可用的钩子 *(按执行顺序排序)*:
|
||||
在请求/响应中,有八个可用的钩子 _(按执行顺序排序)_:
|
||||
|
||||
### onRequest
|
||||
|
||||
```js
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
fastify.addHook("onRequest", (request, reply, done) => {
|
||||
// 其他代码
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
或使用 `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook('onRequest', async (request, reply) => {
|
||||
fastify.addHook("onRequest", async (request, reply) => {
|
||||
// 其他代码
|
||||
await asyncMethod()
|
||||
})
|
||||
await asyncMethod();
|
||||
});
|
||||
```
|
||||
|
||||
**注意**:在 [onRequest](#onrequest) 钩子中,`request.body` 的值总是 `null`,这是因为 body 的解析发生在 [preValidation](#prevalidation) 钩子之前。
|
||||
@ -64,18 +67,20 @@ fastify.addHook('onRequest', async (request, reply) => {
|
||||
例如,你可以解压请求的 body:
|
||||
|
||||
```js
|
||||
fastify.addHook('preParsing', (request, reply, payload, done) => {
|
||||
fastify.addHook("preParsing", (request, reply, payload, done) => {
|
||||
// 其他代码
|
||||
done(null, newPayload)
|
||||
})
|
||||
done(null, newPayload);
|
||||
});
|
||||
```
|
||||
|
||||
或使用 `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook('preParsing', async (request, reply, payload) => {
|
||||
fastify.addHook("preParsing", async (request, reply, payload) => {
|
||||
// 其他代码
|
||||
await asyncMethod()
|
||||
return newPayload
|
||||
})
|
||||
await asyncMethod();
|
||||
return newPayload;
|
||||
});
|
||||
```
|
||||
|
||||
**注意**:在 [preParsing](#preparsing) 钩子中,`request.body` 的值总是 `null`,这是因为 body 的解析发生在 [preValidation](#prevalidation) 钩子之前。
|
||||
@ -89,119 +94,135 @@ fastify.addHook('preParsing', async (request, reply, payload) => {
|
||||
使用 `preValidation` 钩子时,你可以在校验前修改 payload。示例如下:
|
||||
|
||||
```js
|
||||
fastify.addHook('preValidation', (request, reply, done) => {
|
||||
req.body = { ...req.body, importantKey: 'randomString' }
|
||||
done()
|
||||
})
|
||||
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 }
|
||||
})
|
||||
fastify.addHook("preValidation", async (request, reply) => {
|
||||
const importantKey = await generateRandomString();
|
||||
req.body = { ...req.body, importantKey };
|
||||
});
|
||||
```
|
||||
|
||||
### preHandler
|
||||
|
||||
```js
|
||||
fastify.addHook('preHandler', (request, reply, done) => {
|
||||
fastify.addHook("preHandler", (request, reply, done) => {
|
||||
// 其他代码
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
或使用 `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook('preHandler', async (request, reply) => {
|
||||
fastify.addHook("preHandler", async (request, reply) => {
|
||||
// 其他代码
|
||||
await asyncMethod()
|
||||
})
|
||||
await asyncMethod();
|
||||
});
|
||||
```
|
||||
|
||||
### preSerialization
|
||||
|
||||
`preSerialization` 钩子让你可以在 payload 被序列化之前改动 (或替换) 它。举个例子:
|
||||
|
||||
```js
|
||||
fastify.addHook('preSerialization', (request, reply, payload, done) => {
|
||||
const err = null
|
||||
const newPayload = { wrapped: payload }
|
||||
done(err, newPayload)
|
||||
})
|
||||
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 }
|
||||
})
|
||||
fastify.addHook("preSerialization", async (request, reply, payload) => {
|
||||
return { wrapped: payload };
|
||||
});
|
||||
```
|
||||
|
||||
注:payload 为 `string`、`Buffer`、`stream` 或 `null` 时,该钩子不会被调用。
|
||||
|
||||
### onError
|
||||
|
||||
```js
|
||||
fastify.addHook('onError', (request, reply, error, done) => {
|
||||
fastify.addHook("onError", (request, reply, error, done) => {
|
||||
// 其他代码
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
或使用 `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook('onError', async (request, reply, error) => {
|
||||
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) => {
|
||||
fastify.addHook("onSend", (request, reply, payload, done) => {
|
||||
const err = null;
|
||||
const newPayload = payload.replace('some-text', 'some-new-text')
|
||||
done(err, newPayload)
|
||||
})
|
||||
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
|
||||
})
|
||||
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)
|
||||
})
|
||||
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) => {
|
||||
fastify.addHook("onResponse", (request, reply, done) => {
|
||||
// 其他代码
|
||||
await asyncMethod()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
或使用 `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook("onResponse", async (request, reply) => {
|
||||
// 其他代码
|
||||
await asyncMethod();
|
||||
});
|
||||
```
|
||||
|
||||
`onResponse` 钩子在响应发出后被执行,因此在该钩子中你无法再向客户端发送数据了。但是你可以在此向外部服务发送数据,比如收集数据。
|
||||
@ -209,88 +230,97 @@ fastify.addHook('onResponse', async (request, reply) => {
|
||||
### onTimeout
|
||||
|
||||
```js
|
||||
fastify.addHook('onTimeout', (request, reply, done) => {
|
||||
fastify.addHook("onTimeout", (request, reply, done) => {
|
||||
// 其他代码
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
Or `async/await`:
|
||||
|
||||
```js
|
||||
fastify.addHook('onTimeout', async (request, reply) => {
|
||||
fastify.addHook("onTimeout", async (request, reply) => {
|
||||
// 其他代码
|
||||
await asyncMethod()
|
||||
})
|
||||
await asyncMethod();
|
||||
});
|
||||
```
|
||||
|
||||
`onTimeout` 用于监测请求超时,需要在 Fastify 实例上设置 `connectionTimeout` 属性。当请求超时,socket 挂起 (hang up) 时,该钩子执行。因此,在这个钩子里不能再向客户端发送数据了。
|
||||
|
||||
### 在钩子中管理错误
|
||||
|
||||
在钩子的执行过程中如果发生了错误,只需将错误传递给 `done()`,Fastify 就会自动关闭请求,并发送一个相应的错误码给用户。
|
||||
|
||||
```js
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
done(new Error('Some error'))
|
||||
})
|
||||
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'))
|
||||
})
|
||||
fastify.addHook("preHandler", (request, reply, done) => {
|
||||
reply.code(400);
|
||||
done(new Error("Some error"));
|
||||
});
|
||||
```
|
||||
*错误最终会在 [`Reply`](Reply.md#errors) 中得到处理。*
|
||||
|
||||
_错误最终会在 [`Reply`](Reply.md#errors) 中得到处理。_
|
||||
|
||||
或者在 `async/await` 函数中抛出错误:
|
||||
|
||||
```js
|
||||
fastify.addHook('onResponse', async (request, reply) => {
|
||||
throw new Error('Some error')
|
||||
})
|
||||
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`,那么只需要调用 `reply.send()`,并且避免触发回调便可。假如钩子是 `async` 函数,那么 `reply.send()` **必须** 发生在函数返回或 promise resolve 之前,否则请求将会继续下去。当 `reply.send()` 在 promise 调用链之外被调用时,需要 `return reply`,不然请求将被执行两次。
|
||||
|
||||
__不应当混用回调与 `async`/`Promise`__,否则钩子的调用链会被执行两次。
|
||||
**不应当混用回调与 `async`/`Promise`**,否则钩子的调用链会被执行两次。
|
||||
|
||||
如果你在 `onRequest` 或 `preHandler` 中发出响应,请使用 `reply.send`。
|
||||
|
||||
```js
|
||||
fastify.addHook('onRequest', (request, reply, done) => {
|
||||
reply.send('Early response')
|
||||
})
|
||||
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 // 在这里是可选的,但这是好的实践
|
||||
})
|
||||
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)
|
||||
})
|
||||
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') })
|
||||
fastify.addHook("preHandler", async (request, reply) => {
|
||||
setImmediate(() => {
|
||||
reply.send("hello");
|
||||
});
|
||||
// 让处理函数等待 promise 链之外发出的响应
|
||||
return reply
|
||||
})
|
||||
fastify.addHook('preHandler', async (request, reply) => {
|
||||
return reply;
|
||||
});
|
||||
fastify.addHook("preHandler", async (request, reply) => {
|
||||
// fastify-static 插件异步地发送文件,因此需要 return reply
|
||||
reply.sendFile('myfile')
|
||||
return reply
|
||||
})
|
||||
reply.sendFile("myfile");
|
||||
return reply;
|
||||
});
|
||||
```
|
||||
|
||||
## 应用钩子
|
||||
@ -303,71 +333,84 @@ fastify.addHook('preHandler', async (request, reply) => {
|
||||
- [onRegister](#onregister)
|
||||
|
||||
### onReady
|
||||
|
||||
在服务器开始监听请求之前,或调用 `.ready()` 方法时触发。在此你无法更改路由,或添加新的钩子。注册的 `onReady` 钩子函数串行执行,只有全部执行完毕时,服务器才会开始监听请求。钩子接受一个回调函数作为参数:`done`,在钩子函数完成后调用。钩子的 `this` 为 Fastify 实例。
|
||||
|
||||
```js
|
||||
// 回调写法
|
||||
fastify.addHook('onReady', function (done) {
|
||||
fastify.addHook("onReady", function (done) {
|
||||
// 其他代码
|
||||
const err = null;
|
||||
done(err)
|
||||
})
|
||||
done(err);
|
||||
});
|
||||
|
||||
// 或 async/await
|
||||
fastify.addHook('onReady', async function () {
|
||||
fastify.addHook("onReady", async function () {
|
||||
// 异步代码
|
||||
await loadCacheFromDatabase()
|
||||
})
|
||||
await loadCacheFromDatabase();
|
||||
});
|
||||
```
|
||||
|
||||
<a name="on-close"></a>
|
||||
|
||||
### onClose
|
||||
|
||||
使用 `fastify.close()` 停止服务器时被触发。当[插件](Plugins.md)需要一个 "shutdown" 事件时有用,例如关闭一个数据库连接。<br>
|
||||
该钩子的第一个参数是 Fastify 实例,第二个为 `done` 回调函数。
|
||||
|
||||
```js
|
||||
fastify.addHook('onClose', (instance, done) => {
|
||||
fastify.addHook("onClose", (instance, done) => {
|
||||
// 其他代码
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
<a name="on-route"></a>
|
||||
|
||||
### onRoute
|
||||
|
||||
当注册一个新的路由时被触发。它的监听函数拥有一个唯一的参数:`routeOptions` 对象。该函数是同步的,其本身并不接受回调作为参数。
|
||||
|
||||
```js
|
||||
fastify.addHook('onRoute', (routeOptions) => {
|
||||
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
|
||||
})
|
||||
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) => {
|
||||
fastify.addHook("onRoute", (routeOptions) => {
|
||||
function onPreSerialization(request, reply, payload, done) {
|
||||
// 其他代码
|
||||
done(null, payload)
|
||||
done(null, payload);
|
||||
}
|
||||
|
||||
|
||||
// preSerialization 可以是数组或 undefined
|
||||
routeOptions.preSerialization = [...(routeOptions.preSerialization || []), onPreSerialization]
|
||||
})
|
||||
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', [])
|
||||
|
||||
@ -398,40 +441,42 @@ fastify.addHook('onRegister', (instance, opts) => {
|
||||
```
|
||||
|
||||
<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.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')
|
||||
fastify.addHook("onRequest", async function (req, reply) {
|
||||
if (req.raw.url === "/nested") {
|
||||
assert.strictEqual(this.foo, "bar");
|
||||
} else {
|
||||
assert.strictEqual(this.foo, undefined)
|
||||
assert.strictEqual(this.foo, undefined);
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
fastify.get('/', async function (req, reply) {
|
||||
assert.strictEqual(this.foo, undefined)
|
||||
return { hello: 'world' }
|
||||
})
|
||||
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.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' }
|
||||
})
|
||||
})
|
||||
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,而是当前的作用域。
|
||||
@ -439,6 +484,7 @@ fastify.register(async function plugin (fastify, opts) {
|
||||
<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) 钩子正是为此而生。
|
||||
@ -576,4 +622,4 @@ channel.subscribe(function ({ fastify }) {
|
||||
done()
|
||||
})
|
||||
})
|
||||
```
|
||||
```
|
||||
|
@ -32,11 +32,11 @@ Fastify 长期支持计划 (LTS) 以本文档为准:
|
||||
|
||||
### 计划
|
||||
|
||||
| 版本 | 发布日期 | 长期支持结束 | 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 |
|
||||
| 版本 | 发布日期 | 长期支持结束 | 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>
|
||||
|
||||
@ -44,11 +44,11 @@ Fastify 长期支持计划 (LTS) 以本文档为准:
|
||||
|
||||
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 |
|
||||
| 系统 | 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`。
|
||||
使用 [yarn](https://yarnpkg.com/) 命令需添加 `--ignore-engines`。
|
||||
|
@ -1,8 +1,9 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
## 生命周期
|
||||
|
||||
下图展示了 Fastify 的内部生命周期。<br>
|
||||
每个节点右边的分支为生命周期的下一阶段,左边的则是上一个生命周期抛出错误时产生的错误码 *(请注意 Fastify 会自动处理所有的错误)*。
|
||||
每个节点右边的分支为生命周期的下一阶段,左边的则是上一个生命周期抛出错误时产生的错误码 _(请注意 Fastify 会自动处理所有的错误)_。
|
||||
|
||||
```
|
||||
Incoming Request
|
||||
@ -37,10 +38,11 @@ Incoming Request
|
||||
```
|
||||
|
||||
在`用户编写的处理函数`执行前或执行时,你可以调用 `reply.hijack()` 以使得 Fastify:
|
||||
|
||||
- 终止运行所有钩子及用户的处理函数
|
||||
- 不再自动发送响应
|
||||
|
||||
特别注意 (*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。
|
||||
特别注意 (\*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。
|
||||
|
||||
## 响应生命周期
|
||||
|
||||
@ -75,4 +77,4 @@ Incoming Request
|
||||
|
||||
- 通过[响应序列化方法](Server.md#setreplyserializer) (如有设置)
|
||||
- 或当为返回的 HTTP 状态码设置了 JSON schema 时,通过[序列化函数生成器](Server.md#setserializercompiler)
|
||||
- 或通过默认的 `JSON.stringify` 函数
|
||||
- 或通过默认的 `JSON.stringify` 函数
|
||||
|
@ -10,107 +10,111 @@ Fastify 专注于性能,因此使用了 [pino](https://github.com/pinojs/pino)
|
||||
开启日志相当简单:
|
||||
|
||||
```js
|
||||
const fastify = require('fastify')({
|
||||
logger: true
|
||||
})
|
||||
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.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.log.info("Something important happened!");
|
||||
```
|
||||
|
||||
如果你想为日志配置选项,直接将选项传递给 Fastify 实例就可以了。
|
||||
你可以在 [Pino 的文档](https://github.com/pinojs/pino/blob/master/docs/api.md#pinooptions-stream)中找到全部选项。如果你想指定文件地址,可以:
|
||||
|
||||
```js
|
||||
const fastify = require('fastify')({
|
||||
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' })
|
||||
})
|
||||
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 split = require("split2");
|
||||
const stream = split(JSON.parse);
|
||||
|
||||
const fastify = require('fastify')({
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
level: 'info',
|
||||
stream: stream
|
||||
}
|
||||
})
|
||||
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')({
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
serializers: {
|
||||
req (request) {
|
||||
return { url: request.url }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
req(request) {
|
||||
return { url: request.url };
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
响应的 payload 与 header 可以按如下方式记录日志 (即便这是*不推荐*的做法):
|
||||
|
||||
```js
|
||||
const fastify = require('fastify')({
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
prettyPrint: true,
|
||||
serializers: {
|
||||
res (reply) {
|
||||
res(reply) {
|
||||
// 默认
|
||||
return {
|
||||
statusCode: reply.statusCode
|
||||
}
|
||||
statusCode: reply.statusCode,
|
||||
};
|
||||
},
|
||||
req (request) {
|
||||
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
|
||||
headers: request.headers,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**注**:在 `req` 方法中,body 无法被序列化。因为请求是在创建子日志时就序列化了,而此时 body 尚未被解析。
|
||||
|
||||
以下是记录 `req.body` 的一个方法
|
||||
|
||||
```js
|
||||
app.addHook('preHandler', function (req, reply, done) {
|
||||
app.addHook("preHandler", function (req, reply, done) {
|
||||
if (req.body) {
|
||||
req.log.info({ body: req.body }, 'parsed body')
|
||||
req.log.info({ body: req.body }, "parsed body");
|
||||
}
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
|
||||
*Pino 之外的日志工具会忽略该选项。*
|
||||
_Pino 之外的日志工具会忽略该选项。_
|
||||
|
||||
你还可以提供自定义的日志实例。将实例传入,取代配置选项即可。提供的示例必须实现 Pino 的接口,换句话说,便是拥有下列方法:
|
||||
`info`、`error`、`debug`、`fatal`、`warn`、`trace`、`child`。
|
||||
@ -118,21 +122,23 @@ app.addHook('preHandler', function (req, reply, done) {
|
||||
示例:
|
||||
|
||||
```js
|
||||
const log = require('pino')({ level: 'info' })
|
||||
const fastify = require('fastify')({ logger: log })
|
||||
const log = require("pino")({ level: "info" });
|
||||
const fastify = require("fastify")({ logger: log });
|
||||
|
||||
log.info('does not have request information')
|
||||
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' })
|
||||
})
|
||||
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)的各部分均可使用。*
|
||||
_当前请求的日志实例在[生命周期](Lifecycle.md)的各部分均可使用。_
|
||||
|
||||
## 日志修订
|
||||
|
||||
|
||||
[Pino](https://getpino.io) 支持低开销的日志修订,以隐藏特定内容。
|
||||
举例来说,出于安全方面的考虑,我们也许想在 HTTP header 的日志中隐藏 `Authorization` 这一个 header:
|
||||
|
||||
@ -140,22 +146,22 @@ fastify.get('/', function (request, reply) {
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
stream: stream,
|
||||
redact: ['req.headers.authorization'],
|
||||
level: 'info',
|
||||
redact: ["req.headers.authorization"],
|
||||
level: "info",
|
||||
serializers: {
|
||||
req (request) {
|
||||
req(request) {
|
||||
return {
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
headers: request.headers,
|
||||
hostname: request.hostname,
|
||||
remoteAddress: request.ip,
|
||||
remotePort: request.socket.remotePort
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
remotePort: request.socket.remotePort,
|
||||
};
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
更多信息请看 https://getpino.io/#/docs/redaction。
|
||||
|
@ -7,22 +7,22 @@
|
||||
以下是通过 [`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')())
|
||||
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')())
|
||||
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)。
|
||||
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)。
|
||||
|
@ -16,15 +16,15 @@
|
||||
|
||||
```js
|
||||
// 在 Fastify v2 中使用 Express 的 `cors` 中间件。
|
||||
fastify.use(require('cors')());
|
||||
fastify.use(require("cors")());
|
||||
```
|
||||
|
||||
**v3:**
|
||||
|
||||
```js
|
||||
// 在 Fastify v3 中使用 Express 的 `cors` 中间件。
|
||||
await fastify.register(require('fastify-express'));
|
||||
fastify.use(require('cors')());
|
||||
await fastify.register(require("fastify-express"));
|
||||
fastify.use(require("cors")());
|
||||
```
|
||||
|
||||
### 日志序列化 ([#2017](https://github.com/fastify/fastify/pull/2017))
|
||||
@ -36,34 +36,34 @@ fastify.use(require('cors')());
|
||||
**v2:**
|
||||
|
||||
```js
|
||||
const fastify = require('fastify')({
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
serializers: {
|
||||
res(res) {
|
||||
return {
|
||||
statusCode: res.statusCode,
|
||||
customProp: res.customProp
|
||||
customProp: res.customProp,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**v3:**
|
||||
|
||||
```js
|
||||
const fastify = require('fastify')({
|
||||
const fastify = require("fastify")({
|
||||
logger: {
|
||||
serializers: {
|
||||
res(reply) {
|
||||
return {
|
||||
statusCode: reply.statusCode, // 无需更改
|
||||
customProp: reply.raw.customProp // 从 res 对象 (译注:即 Node.js 原生的响应对象,此处为 raw) 中记录属性
|
||||
customProp: reply.raw.customProp, // 从 res 对象 (译注:即 Node.js 原生的响应对象,此处为 raw) 中记录属性
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
@ -75,7 +75,7 @@ const fastify = require('fastify')({
|
||||
|
||||
```js
|
||||
const schema = {
|
||||
body: 'schemaId#'
|
||||
body: "schemaId#",
|
||||
};
|
||||
fastify.route({ method, url, schema, handler });
|
||||
```
|
||||
@ -85,8 +85,8 @@ fastify.route({ method, url, schema, handler });
|
||||
```js
|
||||
const schema = {
|
||||
body: {
|
||||
$ref: 'schemaId#'
|
||||
}
|
||||
$ref: "schemaId#",
|
||||
},
|
||||
};
|
||||
fastify.route({ method, url, schema, handler });
|
||||
```
|
||||
@ -103,8 +103,8 @@ const ajv = new AJV();
|
||||
ajv.addSchema(schemaA);
|
||||
ajv.addSchema(schemaB);
|
||||
|
||||
fastify.setSchemaCompiler(schema => ajv.compile(schema));
|
||||
fastify.setSchemaResolver(ref => ajv.getSchema(ref).schema);
|
||||
fastify.setSchemaCompiler((schema) => ajv.compile(schema));
|
||||
fastify.setSchemaResolver((ref) => ajv.getSchema(ref).schema);
|
||||
```
|
||||
|
||||
**v3:**
|
||||
@ -116,7 +116,7 @@ ajv.addSchema(schemaA);
|
||||
ajv.addSchema(schemaB);
|
||||
|
||||
fastify.setValidatorCompiler(({ schema, method, url, httpPart }) =>
|
||||
ajv.compile(schema)
|
||||
ajv.compile(schema),
|
||||
);
|
||||
```
|
||||
|
||||
@ -173,14 +173,14 @@ interface PingBody {
|
||||
}
|
||||
|
||||
server.get<PingQuerystring, PingParams, PingHeaders, PingBody>(
|
||||
'/ping/:bar',
|
||||
"/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`
|
||||
}
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@ -192,7 +192,7 @@ server.get<{
|
||||
Params: PingParams;
|
||||
Headers: PingHeaders;
|
||||
Body: PingBody;
|
||||
}>('/ping/:bar', opts, async (request, reply) => {
|
||||
}>("/ping/:bar", opts, async (request, reply) => {
|
||||
console.log(request.query); // 类型为 `PingQuerystring`
|
||||
console.log(request.params); // 类型为 `PingParams`
|
||||
console.log(request.headers); // 类型为 `PingHeaders`
|
||||
@ -209,12 +209,12 @@ server.get<{
|
||||
```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,同时服务器崩溃
|
||||
})
|
||||
reply.send(error);
|
||||
});
|
||||
fastify.get("/", (request, reply) => {
|
||||
const maybeAnArray = request.body.something ? [] : "I am a string";
|
||||
maybeAnArray.substr(); // 抛错:[].substr is not a function,同时服务器崩溃
|
||||
});
|
||||
```
|
||||
|
||||
**v3:**
|
||||
@ -222,12 +222,12 @@ fastify.get('/', (request, reply) => {
|
||||
```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,但错误得到控制
|
||||
})
|
||||
reply.send(error);
|
||||
});
|
||||
fastify.get("/", (request, reply) => {
|
||||
const maybeAnArray = request.body.something ? [] : "I am a string";
|
||||
maybeAnArray.substr(); // 抛错:[].substr is not a function,但错误得到控制
|
||||
});
|
||||
```
|
||||
|
||||
## 更多特性与改善
|
||||
@ -238,4 +238,4 @@ fastify.get('/', (request, reply) => {
|
||||
- 添加 [`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))
|
||||
- 支持将对象作为错误抛出 ([#2134](https://github.com/fastify/fastify/pull/2134))
|
||||
|
@ -1,11 +1,13 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
# 插件漫游指南
|
||||
|
||||
首先, `不要恐慌`!
|
||||
|
||||
Fastify 从一开始就搭建成非常模块化的系统. 我们搭建了非常强健的 API 来允许你创建命名空间, 来添加工具方法. Fastify 创建的封装模型可以让你在任何时候将你的应用分割成不同的微服务, 而无需重构整个应用.
|
||||
|
||||
**内容清单**
|
||||
|
||||
- [注册器](#register)
|
||||
- [装饰器](#decorators)
|
||||
- [钩子方法](#hooks)
|
||||
@ -17,310 +19,351 @@ Fastify 从一开始就搭建成非常模块化的系统. 我们搭建了非常
|
||||
- [开始!](#start)
|
||||
|
||||
<a name="register"></a>
|
||||
|
||||
## 注册器
|
||||
|
||||
就像在 JavaScript 万物都是对象, 在 Fastify 万物都是插件.<br>
|
||||
你的路由, 你的工具方法等等都是插件. 无论添加什么功能的插件, 你都可以使用 Fastify 优秀又独一无二的 API: [`register`](Plugins.md).
|
||||
|
||||
```js
|
||||
fastify.register(
|
||||
require('./my-plugin'),
|
||||
{ options }
|
||||
)
|
||||
fastify.register(require("./my-plugin"), { options });
|
||||
```
|
||||
|
||||
`register` 创建一个新的 Fastify 上下文, 这意味着如果你对 Fastify 的实例做任何改动, 这些改动不会反映到上下文的父级上. 换句话说, 封装!
|
||||
|
||||
*为什么封装这么重要?*<br>
|
||||
_为什么封装这么重要?_<br>
|
||||
那么, 假设你创建了一个具有开创性的初创公司, 你会怎么做? 你创建了一个包含所有东西的 API 服务器, 所有东西都在同一个地方, 一个庞然大物!<br>
|
||||
现在, 你增长得非常迅速, 想要改变架构去尝试微服务. 通常这意味着非常多的工作, 因为交叉依赖和缺少关注点的分离.<br>
|
||||
Fastify 在这个层面上可以帮助你很多, 多亏了封装模型, 它完全避免了交叉依赖, 并且帮助你将组织成高聚合的代码块.
|
||||
|
||||
*让我们回到如何正确地使用 `register`.*<br>
|
||||
_让我们回到如何正确地使用 `register`._<br>
|
||||
插件必须输出一个有以下参数的方法
|
||||
|
||||
```js
|
||||
module.exports = function (fastify, options, done) {}
|
||||
module.exports = function (fastify, options, done) {};
|
||||
```
|
||||
|
||||
`fastify` 就是封装的 Fastify 实例, `options` 就是选项对象, 而 `done` 是一个在插件准备好了之后**必须**要调用的方法.
|
||||
|
||||
Fastify 的插件模型是完全可重入的和基于图(数据结构)的, 它能够处理任何异步代码并且保证插件的加载顺序, 甚至是关闭顺序! *如何做到的?* 很高兴你发问了, 查看下 [`avvio`](https://github.com/mcollina/avvio)! Fastify 在 `.listen()`, `.inject()` 或者 `.ready()` 被调用了之后开始加载插件.
|
||||
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' })
|
||||
})
|
||||
fastify.get("/plugin", (request, reply) => {
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
|
||||
done()
|
||||
}
|
||||
done();
|
||||
};
|
||||
```
|
||||
|
||||
那么现在你已经知道了如何使用 `register` API 并且知道它是怎么工作的, 但我们如何给 Fastify 添加新的功能, 并且分享给其他的开发者?
|
||||
|
||||
<a name="decorators"></a>
|
||||
|
||||
## 装饰器
|
||||
|
||||
好了, 假设你写了一个非常好的工具方法, 因此你决定在你所有的代码里都能够用这个方法. 你改怎么做? 可能是像以下代码一样:
|
||||
|
||||
```js
|
||||
// your-awesome-utility.js
|
||||
module.exports = function (a, b) {
|
||||
return a + b
|
||||
}
|
||||
return a + b;
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
const util = require('./your-awesome-utility')
|
||||
console.log(util('that is ', 'awesome'))
|
||||
const util = require("./your-awesome-utility");
|
||||
console.log(util("that is ", "awesome"));
|
||||
```
|
||||
|
||||
现在你需要在所有需要这个方法的文件中引入它. (别忘了你可能在测试中也需要它).
|
||||
|
||||
Fastify 提供了一个更优雅的方法, *装饰器*.
|
||||
Fastify 提供了一个更优雅的方法, _装饰器_.
|
||||
创建一个装饰器非常简单, 只要使用 [`decorate`](Decorators.md) API:
|
||||
|
||||
```js
|
||||
fastify.decorate('util', (a, b) => a + b)
|
||||
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'))
|
||||
instance.decorate("util", (a, b) => a + b);
|
||||
console.log(instance.util("that is ", "awesome"));
|
||||
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register((instance, opts, done) => {
|
||||
console.log(instance.util('that is ', 'awesome')) // 这里会抛错
|
||||
console.log(instance.util("that is ", "awesome")); // 这里会抛错
|
||||
|
||||
done()
|
||||
})
|
||||
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'))
|
||||
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()
|
||||
})
|
||||
console.log(instance.util("that is ", "awesome")); // 这里不会抛错
|
||||
done();
|
||||
});
|
||||
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.register((instance, opts, done) => {
|
||||
console.log(instance.util('that is ', 'awesome')) // 这里会抛错
|
||||
console.log(instance.util("that is ", "awesome")); // 这里会抛错
|
||||
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
```
|
||||
*PS: 如果你需要全局的工具方法, 请注意要声明在应用根作用域上. 或者你可以使用 `fastify-plugin` 工具, [参考](#distribution).*
|
||||
|
||||
_PS: 如果你需要全局的工具方法, 请注意要声明在应用根作用域上. 或者你可以使用 `fastify-plugin` 工具, [参考](#distribution)._
|
||||
|
||||
`decorate` 不是唯一可以用来扩展服务器的功能的 API, 你还可以使用 `decorateRequest` 和 `decorateReply`.
|
||||
|
||||
*`decorateRequest` 和 `decorateReply`? 为什么我们已经有了 `decorate` 还需要它们?*<br>
|
||||
_`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.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' })
|
||||
})
|
||||
这个可行, 但可以变得更好!
|
||||
|
||||
```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.decorate("getHeader", (req, header) => {
|
||||
return req.headers[header];
|
||||
});
|
||||
|
||||
fastify.addHook('preHandler', (request, reply, done) => {
|
||||
request.isHappy = fastify.getHeader(request.raw, 'happy')
|
||||
done()
|
||||
})
|
||||
fastify.addHook("preHandler", (request, reply, done) => {
|
||||
request.isHappy = fastify.getHeader(request.raw, "happy");
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.get('/happiness', (request, reply) => {
|
||||
reply.send({ happy: request.isHappy })
|
||||
})
|
||||
fastify.get("/happiness", (request, reply) => {
|
||||
reply.send({ happy: request.isHappy });
|
||||
});
|
||||
```
|
||||
|
||||
这个也可行, 但可以变得更好!
|
||||
|
||||
```js
|
||||
fastify.decorateRequest('setHeader', function (header) {
|
||||
this.isHappy = this.headers[header]
|
||||
})
|
||||
fastify.decorateRequest("setHeader", function (header) {
|
||||
this.isHappy = this.headers[header];
|
||||
});
|
||||
|
||||
fastify.decorateRequest('isHappy', false) // 这会添加到 Request 对象的原型中, 好快!
|
||||
fastify.decorateRequest("isHappy", false); // 这会添加到 Request 对象的原型中, 好快!
|
||||
|
||||
fastify.addHook('preHandler', (request, reply, done) => {
|
||||
request.setHeader('happy')
|
||||
done()
|
||||
})
|
||||
fastify.addHook("preHandler", (request, reply, done) => {
|
||||
request.setHeader("happy");
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.get('/happiness', (request, reply) => {
|
||||
reply.send({ happy: request.isHappy })
|
||||
})
|
||||
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.decorate("util", (request, key, value) => {
|
||||
request[key] = value;
|
||||
});
|
||||
|
||||
fastify.get('/plugin1', (request, reply) => {
|
||||
fastify.util(request, 'timestamp', new Date())
|
||||
reply.send(request)
|
||||
})
|
||||
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)
|
||||
})
|
||||
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.decorate("util", (request, key, value) => {
|
||||
request[key] = value;
|
||||
});
|
||||
|
||||
fastify.addHook('preHandler', (request, reply, done) => {
|
||||
fastify.util(request, 'timestamp', new Date())
|
||||
done()
|
||||
})
|
||||
fastify.addHook("preHandler", (request, reply, done) => {
|
||||
fastify.util(request, "timestamp", new Date());
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.get('/plugin1', (request, reply) => {
|
||||
reply.send(request)
|
||||
})
|
||||
fastify.get("/plugin1", (request, reply) => {
|
||||
reply.send(request);
|
||||
});
|
||||
|
||||
fastify.get('/plugin2', (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.decorate("util", (request, key, value) => {
|
||||
request[key] = value;
|
||||
});
|
||||
|
||||
instance.addHook('preHandler', (request, reply, done) => {
|
||||
instance.util(request, 'timestamp', new Date())
|
||||
done()
|
||||
})
|
||||
instance.addHook("preHandler", (request, reply, done) => {
|
||||
instance.util(request, "timestamp", new Date());
|
||||
done();
|
||||
});
|
||||
|
||||
instance.get('/plugin1', (request, reply) => {
|
||||
reply.send(request)
|
||||
})
|
||||
instance.get("/plugin1", (request, reply) => {
|
||||
reply.send(request);
|
||||
});
|
||||
|
||||
done()
|
||||
})
|
||||
done();
|
||||
});
|
||||
|
||||
fastify.get('/plugin2', (request, reply) => {
|
||||
reply.send(request)
|
||||
})
|
||||
fastify.get("/plugin2", (request, reply) => {
|
||||
reply.send(request);
|
||||
});
|
||||
```
|
||||
|
||||
现在你的钩子方法只会在第一个路由中运行!
|
||||
|
||||
你可能已经注意到, `request` and `reply` 不是标准的 Nodejs *request* 和 *response* 对象, 而是 Fastify 对象.<br>
|
||||
你可能已经注意到, `request` and `reply` 不是标准的 Nodejs _request_ 和 _response_ 对象, 而是 Fastify 对象.<br>
|
||||
|
||||
<a name="distribution"></a>
|
||||
|
||||
## 如何处理封装与分发
|
||||
|
||||
完美, 现在你知道了(几乎)所有的扩展 Fastify 的工具. 但可能你遇到了一个大问题: 如何分发你的代码?
|
||||
|
||||
我们推荐将所有代码包裹在一个`注册器`中分发, 这样你的插件可以支持异步启动 *(`decorate` 是一个同步 API)*, 例如建立数据库链接.
|
||||
我们推荐将所有代码包裹在一个`注册器`中分发, 这样你的插件可以支持异步启动 _(`decorate` 是一个同步 API)_, 例如建立数据库链接.
|
||||
|
||||
*等等? 你不是告诉我 `register` 会创建封装的上下文, 那么我创建的不是就外层不可见了?*<br>
|
||||
_等等? 你不是告诉我 `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) {
|
||||
```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()
|
||||
})
|
||||
fastify.decorate("db", conn);
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = fp(dbPlugin)
|
||||
module.exports = fp(dbPlugin);
|
||||
```
|
||||
|
||||
你还可以告诉 `fastify-plugin` 去检查安装的 Fastify 版本, 万一你需要特定的 API.
|
||||
|
||||
正如前面所述,Fastify 在 `.listen()`、`.inject()` 以及 `.ready()` 被调用,也即插件被声明 __之后__ 才开始加载插件。这么一来,即使插件通过 [`decorate`](Decorators.md) 向外部的 Fastify 实例注入了变量,在调用 `.listen()`、`.inject()` 和 `.ready()` 之前,这些变量是获取不到的。
|
||||
正如前面所述,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) {
|
||||
```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.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' }
|
||||
})
|
||||
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' }
|
||||
})
|
||||
async function plugin(fastify, opts) {
|
||||
fastify.get("/", async (req, reply) => {
|
||||
return { hello: "world" };
|
||||
});
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default plugin;
|
||||
```
|
||||
|
||||
__注意__:Fastify 不支持具名导入 ESM 模块,但支持 `default` 导入。
|
||||
**注意**:Fastify 不支持具名导入 ESM 模块,但支持 `default` 导入。
|
||||
|
||||
```js
|
||||
// server.mjs
|
||||
import Fastify from 'fastify'
|
||||
import Fastify from "fastify";
|
||||
|
||||
const fastify = Fastify()
|
||||
const fastify = Fastify();
|
||||
|
||||
///...
|
||||
|
||||
fastify.listen(3000, (err, address) => {
|
||||
if (err) {
|
||||
fastify.log.error(err)
|
||||
process.exit(1)
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
<a name="handle-errors"></a>
|
||||
|
||||
## 错误处理
|
||||
|
||||
你的插件也可能在启动的时候失败. 或许你预料到这个并且在这种情况下有特定的处理逻辑. 你该怎么实现呢?
|
||||
`after` API 就是你需要的. `after` 注册一个回调, 在注册之后就会调用这个回调, 它可以有三个参数.<br>
|
||||
回调会基于不同的参数而变化:
|
||||
@ -331,46 +374,51 @@ fastify.listen(3000, (err, address) => {
|
||||
1. 如果有三个参数, 第一个是错误对象, 第二个是顶级上下文(除非你同时指定了服务器和复写, 在这个情况下将会是那个复写的返回), 第三个是完成回调.
|
||||
|
||||
让我们看看如何使用它:
|
||||
|
||||
```js
|
||||
fastify
|
||||
.register(require('./database-connector'))
|
||||
.after(err => {
|
||||
if (err) throw err
|
||||
})
|
||||
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())
|
||||
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')
|
||||
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) 章节中!
|
||||
|
||||
太棒了, 现在你已经知道了所有创建插件需要的关于 Fastify 和它的插件系统的知识, 如果你写了插件请告诉我们! 我们会将它加入到 [_生态_](https://github.com/fastify/fastify#ecosystem) 章节中!
|
||||
|
||||
如果你想要看看真正的插件例子, 查看:
|
||||
|
||||
- [`point-of-view`](https://github.com/fastify/point-of-view)
|
||||
给 Fastify 提供模版 (*ejs, pug, handlebars, marko*) 支持.
|
||||
给 Fastify 提供模版 (_ejs, pug, handlebars, marko_) 支持.
|
||||
- [`fastify-mongodb`](https://github.com/fastify/fastify-mongodb)
|
||||
Fastify MongoDB 连接插件, 可以在全局使用同一 MongoDb 连接池.
|
||||
Fastify MongoDB 连接插件, 可以在全局使用同一 MongoDb 连接池.
|
||||
- [`fastify-multipart`](https://github.com/fastify/fastify-multipart)
|
||||
Multipart 支持
|
||||
Multipart 支持
|
||||
- [`fastify-helmet`](https://github.com/fastify/fastify-helmet) 重要的安全头部支持
|
||||
|
||||
|
||||
*如果感觉还差什么? 告诉我们! :)*
|
||||
_如果感觉还差什么? 告诉我们! :)_
|
||||
|
@ -1,61 +1,67 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
## 插件
|
||||
|
||||
Fastify 允许用户通过插件的方式扩展自身的功能。
|
||||
一个插件可以是一组路由,一个服务器[装饰器](Decorators.md)或者其他任意的东西。 在使用一个或者许多插件时,只需要一个 API `register`。<br>
|
||||
|
||||
默认, `register` 会创建一个 *新的作用域( Scope )*, 这意味着你能够改变 Fastify 实例(通过`decorate`), 这个改变不会反映到当前作用域, 只会影响到子作用域。 这样可以做到插件的*封装*和*继承*, 我们创建了一个*无回路有向图*(DAG), 因此不会有交叉依赖的问题。
|
||||
默认, `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)
|
||||
- [`日志级别`](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',
|
||||
fastify.register(require("fastify-foo"), {
|
||||
prefix: "/foo",
|
||||
foo: {
|
||||
fooOption1: 'value',
|
||||
fooOption2: 'value'
|
||||
}
|
||||
})
|
||||
fooOption1: "value",
|
||||
fooOption2: "value",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
如果不考虑冲突, 插件可以简化成直接接收对象参数:
|
||||
|
||||
```js
|
||||
fastify.register(require('fastify-foo'), {
|
||||
prefix: '/foo',
|
||||
fooOption1: 'value',
|
||||
fooOption2: 'value'
|
||||
})
|
||||
fastify.register(require("fastify-foo"), {
|
||||
prefix: "/foo",
|
||||
fooOption1: "value",
|
||||
fooOption2: "value",
|
||||
});
|
||||
```
|
||||
|
||||
`options` 参数还可以是一个在插件注册时确定的 `函数`,这个函数的第一位参数是 Fastify 实例:
|
||||
|
||||
```js
|
||||
const fp = require('fastify-plugin')
|
||||
const fp = require("fastify-plugin");
|
||||
|
||||
fastify.register(fp((fastify, opts, done) => {
|
||||
fastify.decorate('foo_bar', { hello: 'world' })
|
||||
fastify.register(
|
||||
fp((fastify, opts, done) => {
|
||||
fastify.decorate("foo_bar", { hello: "world" });
|
||||
|
||||
done()
|
||||
}))
|
||||
done();
|
||||
}),
|
||||
);
|
||||
|
||||
// fastify-foo 的 options 参数会是 { hello: 'world' }
|
||||
fastify.register(require('fastify-foo'), parent => parent.foo_bar)
|
||||
fastify.register(require("fastify-foo"), (parent) => parent.foo_bar);
|
||||
```
|
||||
|
||||
传给函数的 Fastify 实例是插件声明时**外部 Fastify 实例**的最新状态,允许你访问**注册顺序**在前的插件通过 [`decorate`](Decorators.md) 注入的变量。这在需要依赖前置插件对于 Fastify 实例的改动时派得上用场,比如,使用已存在的数据库连接来包装你的插件。
|
||||
@ -63,124 +69,142 @@ fastify.register(require('fastify-foo'), parent => parent.foo_bar)
|
||||
请记住,传给函数的 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'))
|
||||
fastify.register(require("my-plugin"));
|
||||
|
||||
// `after` 将在上一个 `register` 结束后执行
|
||||
fastify.after(err => console.log(err))
|
||||
fastify.after((err) => console.log(err));
|
||||
|
||||
// `ready` 将在所有 `register` 结束后执行
|
||||
fastify.ready(err => console.log(err))
|
||||
fastify.ready((err) => console.log(err));
|
||||
|
||||
// `listen` 是一个特殊的 `ready`,
|
||||
// 因此它的执行时机与 `ready` 一致
|
||||
fastify.listen(3000, (err, address) => {
|
||||
if (err) console.log(err)
|
||||
})
|
||||
if (err) console.log(err);
|
||||
});
|
||||
```
|
||||
|
||||
<a name="async-await"></a>
|
||||
|
||||
### async/await
|
||||
|
||||
`after`、`ready` 与 `listen` 支持 *async/await*,同时 `fastify` 也是一个 [Thenable](https://promisesaplus.com/) 对象。
|
||||
`after`、`ready` 与 `listen` 支持 _async/await_,同时 `fastify` 也是一个 [Thenable](https://promisesaplus.com/) 对象。
|
||||
|
||||
```js
|
||||
await fastify.register(require('my-plugin'))
|
||||
await fastify.register(require("my-plugin"));
|
||||
|
||||
await fastify.after()
|
||||
await fastify.after();
|
||||
|
||||
await fastify.ready()
|
||||
await fastify.ready();
|
||||
|
||||
await fastify.listen(3000)
|
||||
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()
|
||||
import Fastify from "fastify";
|
||||
const fastify = Fastify();
|
||||
|
||||
fastify.register(import('./plugin.mjs'))
|
||||
fastify.register(import("./plugin.mjs"));
|
||||
|
||||
fastify.listen(3000, console.log)
|
||||
fastify.listen(3000, console.log);
|
||||
|
||||
// plugin.mjs
|
||||
async function plugin (fastify, opts) {
|
||||
fastify.get('/', async (req, reply) => {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
async function plugin(fastify, opts) {
|
||||
fastify.get("/", async (req, reply) => {
|
||||
return { hello: "world" };
|
||||
});
|
||||
}
|
||||
|
||||
export default plugin
|
||||
export default plugin;
|
||||
```
|
||||
|
||||
<a name="create-plugin"></a>
|
||||
|
||||
### 创建插件
|
||||
|
||||
创建插件非常简单, 你只需要创建一个方法, 这个方法接收三个参数: `fastify` 实例、`options` 选项和 `done` 回调。<br>
|
||||
例子:
|
||||
|
||||
```js
|
||||
module.exports = function (fastify, opts, done) {
|
||||
fastify.decorate('utility', () => {})
|
||||
fastify.decorate("utility", () => {});
|
||||
|
||||
fastify.get('/', handler)
|
||||
fastify.get("/", handler);
|
||||
|
||||
done()
|
||||
}
|
||||
done();
|
||||
};
|
||||
```
|
||||
|
||||
你也可以在一个 `register` 内部添加其他 `register`:
|
||||
|
||||
```js
|
||||
module.exports = function (fastify, opts, done) {
|
||||
fastify.decorate('utility', () => {})
|
||||
fastify.decorate("utility", () => {});
|
||||
|
||||
fastify.get('/', handler)
|
||||
fastify.get("/", handler);
|
||||
|
||||
fastify.register(require('./other-plugin'))
|
||||
fastify.register(require("./other-plugin"));
|
||||
|
||||
done()
|
||||
}
|
||||
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) 模块
|
||||
|
||||
- 使用 [`fastify-plugin`](https://github.com/fastify/fastify-plugin) 模块
|
||||
- 使用 `'skip-override'` 隐藏属性
|
||||
|
||||
我们建议使用 `fastify-plugin` 模块, 因为它是专门用来为你解决这个问题, 并且你可以传一个能够支持的 Fastify 版本范围的参数。
|
||||
|
||||
```js
|
||||
const fp = require('fastify-plugin')
|
||||
const fp = require("fastify-plugin");
|
||||
|
||||
module.exports = fp(function (fastify, opts, done) {
|
||||
fastify.decorate('utility', () => {})
|
||||
done()
|
||||
}, '0.x')
|
||||
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()
|
||||
function yourPlugin(fastify, opts, done) {
|
||||
fastify.decorate("utility", () => {});
|
||||
done();
|
||||
}
|
||||
yourPlugin[Symbol.for('skip-override')] = true
|
||||
module.exports = yourPlugin
|
||||
yourPlugin[Symbol.for("skip-override")] = true;
|
||||
module.exports = yourPlugin;
|
||||
```
|
||||
|
@ -4,10 +4,11 @@
|
||||
|
||||
本文涵盖了使用 Fastify 的推荐方案及最佳实践。
|
||||
|
||||
* [使用反向代理](#reverseproxy)
|
||||
* [Kubernetes](#kubernetes)
|
||||
- [使用反向代理](#reverseproxy)
|
||||
- [Kubernetes](#kubernetes)
|
||||
|
||||
## 使用反向代理
|
||||
|
||||
<a id="reverseproxy"></a>
|
||||
|
||||
Node.js 作为各框架中的先行者,在标准库中提供了易用的 web 服务器。在它现世前,PHP、Python 等语言的使用者,要么需要一个支持该语言的 web 服务器,要么需要一个搭配该语言的 [CGI 网关](cgi)。而 Node.js 能让用户专注于 _直接_ 处理 HTTP 请求的应用本身,这样一来,新的诱惑点变成了处理多个域名的请求、监听多端口 (如 HTTP _和_ HTTPS),接着将应用直接暴露于 Internet 来处理请求。
|
||||
@ -150,7 +151,7 @@ server {
|
||||
# 默认服务器
|
||||
listen 80 default_server;
|
||||
listen [::]:80 default_server;
|
||||
|
||||
|
||||
# 指定端口
|
||||
# listen 80;
|
||||
# listen [::]:80;
|
||||
@ -165,7 +166,7 @@ server {
|
||||
# 默认服务器
|
||||
listen 443 ssl http2 default_server;
|
||||
listen [::]:443 ssl http2 default_server;
|
||||
|
||||
|
||||
# 指定端口
|
||||
# listen 443 ssl http2;
|
||||
# listen [::]:443 ssl http2;
|
||||
@ -180,21 +181,21 @@ server {
|
||||
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;
|
||||
@ -205,7 +206,7 @@ server {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -214,17 +215,19 @@ server {
|
||||
[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
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 4000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 3
|
||||
successThreshold: 1
|
||||
failureThreshold: 5
|
||||
```
|
||||
|
@ -1,6 +1,7 @@
|
||||
<h1 align="center">Fastify</h1>
|
||||
|
||||
## 回复
|
||||
|
||||
- [回复](#reply)
|
||||
- [简介](#introduction)
|
||||
- [.code(statusCode)](#codestatuscode)
|
||||
@ -29,7 +30,9 @@
|
||||
- [.then](#then)
|
||||
|
||||
<a name="introduction"></a>
|
||||
|
||||
### 简介
|
||||
|
||||
处理函数的第二个参数为 `Reply`。
|
||||
Reply 是 Fastify 的一个核心对象。它暴露了以下函数及属性:
|
||||
|
||||
@ -49,170 +52,205 @@ Reply 是 Fastify 的一个核心对象。它暴露了以下函数及属性:
|
||||
- `.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) 对象。
|
||||
- `.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) {
|
||||
fastify.get("/", options, function (request, reply) {
|
||||
// 你的代码
|
||||
reply
|
||||
.code(200)
|
||||
.header('Content-Type', 'application/json; charset=utf-8')
|
||||
.send({ hello: 'world' })
|
||||
})
|
||||
.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)
|
||||
})
|
||||
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
|
||||
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
|
||||
})
|
||||
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'
|
||||
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
|
||||
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.redirect("/home");
|
||||
```
|
||||
|
||||
示例 (不调用 `reply.code()`):状态码 `303`,重定向至 `/home`
|
||||
|
||||
```js
|
||||
reply.redirect(303, '/home')
|
||||
reply.redirect(303, "/home");
|
||||
```
|
||||
|
||||
示例 (调用 `reply.code()`):状态码 `303`,重定向至 `/home`
|
||||
|
||||
```js
|
||||
reply.code(303).redirect('/home')
|
||||
reply.code(303).redirect("/home");
|
||||
```
|
||||
|
||||
示例 (调用 `reply.code()`):状态码 `302`,重定向至 `/home`
|
||||
|
||||
```js
|
||||
reply.code(303).redirect(302, '/home')
|
||||
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()
|
||||
reply.callNotFound();
|
||||
```
|
||||
|
||||
<a name="getResponseTime"></a>
|
||||
|
||||
### .getResponseTime()
|
||||
|
||||
调用自定义响应时间获取函数,来计算自收到请求起的时间。
|
||||
|
||||
```js
|
||||
const milliseconds = reply.getResponseTime()
|
||||
const milliseconds = reply.getResponseTime();
|
||||
```
|
||||
|
||||
<a name="type"></a>
|
||||
|
||||
### .type(contentType, type)
|
||||
|
||||
设置响应的 content type。
|
||||
这是 `reply.header('Content-Type', 'the/type')` 的简写。
|
||||
|
||||
```js
|
||||
reply.type('text/html')
|
||||
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)
|
||||
.header("Content-Type", "application/x-protobuf")
|
||||
.serializer(protoBuf.serialize);
|
||||
```
|
||||
|
||||
注意,你并不需要在一个 `handler` 内部使用这一工具,因为 Buffers、streams 以及字符串 (除非已经设置了序列化工具) 被认为是已序列化过的。
|
||||
|
||||
```js
|
||||
reply
|
||||
.header('Content-Type', 'application/x-protobuf')
|
||||
.send(protoBuf.serialize(data))
|
||||
.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 }) // 这行不会应用
|
||||
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.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()` 发送了一个响应。
|
||||
@ -223,91 +261,109 @@ true`,程序能完全掌控底层的请求,且相关钩子不会被触发。
|
||||
请看范例:
|
||||
|
||||
```js
|
||||
app.get('/', (req, reply) => {
|
||||
reply.sent = true
|
||||
reply.raw.end('hello world')
|
||||
app.get("/", (req, reply) => {
|
||||
reply.sent = true;
|
||||
reply.raw.end("hello world");
|
||||
|
||||
return Promise.resolve('this will be skipped') // 译注:该处会被跳过
|
||||
})
|
||||
return Promise.resolve("this will be skipped"); // 译注:该处会被跳过
|
||||
});
|
||||
```
|
||||
|
||||
如果处理函数 reject,将会记录一个错误。
|
||||
|
||||
<a name="hijack"></a>
|
||||
|
||||
### .hijack()
|
||||
|
||||
有时你需要终止请求生命周期的执行,并手动发送响应。
|
||||
|
||||
Fastify 提供了 `reply.hijack()` 方法来完成此任务。在 `reply.send()` 之前的任意节点调用该方法,能阻止 Fastify 自动发送响应,并不再执行之后的生命周期函数 (包括用户编写的处理函数)。
|
||||
|
||||
特别注意 (*):假如使用了 `reply.raw` 来发送响应,则 `onResponse` 依旧会执行。
|
||||
特别注意 (\*):假如使用了 `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' })
|
||||
})
|
||||
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')
|
||||
})
|
||||
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'`。
|
||||
|
||||
_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)
|
||||
})
|
||||
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'`。
|
||||
|
||||
未设置 `'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)
|
||||
})
|
||||
})
|
||||
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 会自动创建一个如下的错误结构:
|
||||
|
||||
若使用 _send_ 发送一个 _Error_ 的实例,Fastify 会自动创建一个如下的错误结构:
|
||||
|
||||
```js
|
||||
{
|
||||
error: String // HTTP 错误信息
|
||||
code: String // Fastify 的错误代码
|
||||
message: String // 用户错误信息
|
||||
statusCode: Number // HTTP 状态码
|
||||
error: String; // HTTP 错误信息
|
||||
code: String; // Fastify 的错误代码
|
||||
message: String; // 用户错误信息
|
||||
statusCode: Number; // HTTP 状态码
|
||||
}
|
||||
```
|
||||
|
||||
你可以向 Error 对象添加自定义属性,例如 `headers`,这可以用来增强 HTTP 响应。<br>
|
||||
*注意:如果 `send` 一个错误,但状态码小于 400,Fastify 会自动将其设为 500。*
|
||||
_注意:如果 `send` 一个错误,但状态码小于 400,Fastify 会自动将其设为 500。_
|
||||
|
||||
贴士:你可以通过 [`http-errors`](https://npm.im/http-errors) 或 [`fastify-sensible`](https://github.com/fastify/fastify-sensible) 来简化生成的错误:
|
||||
|
||||
```js
|
||||
fastify.get('/', function (request, reply) {
|
||||
reply.send(httpErrors.Gone())
|
||||
})
|
||||
fastify.get("/", function (request, reply) {
|
||||
reply.send(httpErrors.Gone());
|
||||
});
|
||||
```
|
||||
|
||||
你可以通过如下方式自定义 JSON 错误的输出:
|
||||
@ -318,42 +374,46 @@ fastify.get('/', function (request, reply) {
|
||||
请注意,如果返回的状态码不在响应 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)
|
||||
})
|
||||
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
|
||||
```js
|
||||
fastify.setErrorHandler(function (error, request, reply) {
|
||||
request.log.warn(error)
|
||||
var statusCode = error.statusCode >= 400 ? error.statusCode : 500
|
||||
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)
|
||||
})
|
||||
.type("text/plain")
|
||||
.send(statusCode >= 500 ? "Internal server error" : error.message);
|
||||
});
|
||||
```
|
||||
|
||||
路由生成的 not found 错误会使用 [`setNotFoundHandler`](Server.md#setnotfoundhandler)。
|
||||
@ -361,15 +421,14 @@ API:
|
||||
|
||||
```js
|
||||
fastify.setNotFoundHandler(function (request, reply) {
|
||||
reply
|
||||
.code(404)
|
||||
.type('text/plain')
|
||||
.send('a custom not found')
|
||||
})
|
||||
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`
|
||||
@ -379,41 +438,47 @@ fastify.setNotFoundHandler(function (request, reply) {
|
||||
- `null`
|
||||
|
||||
<a name="async-await-promise"></a>
|
||||
|
||||
#### Async-Await 与 Promise
|
||||
|
||||
Fastify 原生地处理 promise 并支持 async-await。<br>
|
||||
*请注意,在下面的例子中我们没有使用 reply.send。*
|
||||
_请注意,在下面的例子中我们没有使用 reply.send。_
|
||||
|
||||
```js
|
||||
const delay = promisify(setTimeout)
|
||||
const delay = promisify(setTimeout);
|
||||
|
||||
fastify.get('/promises', options, function (request, reply) {
|
||||
return delay(200).then(() => { return { hello: 'world' }})
|
||||
})
|
||||
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' }
|
||||
})
|
||||
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("/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' }
|
||||
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` 将会等待,直到回复被发送。
|
||||
|
@ -1,14 +1,16 @@
|
||||
<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 请求
|
||||
- `req` _(不推荐,请使用 `.raw`)_ - Node 原生的 HTTP 请求
|
||||
- `server` - Fastify 服务器的实例,以当前的[封装上下文](Encapsulation.md)为作用域。
|
||||
- `id` - 请求 ID
|
||||
- `log` - 请求的日志实例
|
||||
@ -32,29 +34,29 @@ Request 是 Fastify 的核心对象,包含了以下字段:
|
||||
|
||||
```js
|
||||
request.headers = {
|
||||
'foo': 'bar',
|
||||
'baz': 'qux'
|
||||
}
|
||||
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')
|
||||
})
|
||||
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");
|
||||
});
|
||||
```
|
||||
|
@ -19,47 +19,51 @@
|
||||
- [路由约束](#constraints)
|
||||
|
||||
<a name="full-declaration"></a>
|
||||
|
||||
### 完整定义
|
||||
|
||||
```js
|
||||
fastify.route(options)
|
||||
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)了解更多信息。
|
||||
- `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`。
|
||||
- `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)一文。
|
||||
|
||||
@ -68,33 +72,36 @@ fastify.route(options)
|
||||
**注意:** 在[钩子](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: '/',
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
querystring: {
|
||||
name: { type: 'string' },
|
||||
excitement: { type: 'integer' }
|
||||
name: { type: "string" },
|
||||
excitement: { type: "integer" },
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
hello: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: function (request, reply) {
|
||||
reply.send({ hello: 'world' })
|
||||
}
|
||||
})
|
||||
reply.send({ hello: "world" });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
<a name="shorthand-declaration"></a>
|
||||
|
||||
### 简写定义
|
||||
上文的路由定义带有 *Hapi* 的风格。要是偏好 *Express/Restify* 的写法,Fastify 也是支持的:<br>
|
||||
|
||||
上文的路由定义带有 _Hapi_ 的风格。要是偏好 _Express/Restify_ 的写法,Fastify 也是支持的:<br>
|
||||
`fastify.get(path, [options], handler)`<br>
|
||||
`fastify.head(path, [options], handler)`<br>
|
||||
`fastify.post(path, [options], handler)`<br>
|
||||
@ -104,186 +111,209 @@ fastify.route({
|
||||
`fastify.patch(path, [options], handler)`
|
||||
|
||||
示例:
|
||||
|
||||
```js
|
||||
const opts = {
|
||||
schema: {
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fastify.get('/', opts, (request, reply) => {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
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',
|
||||
type: "object",
|
||||
properties: {
|
||||
hello: { type: 'string' }
|
||||
}
|
||||
}
|
||||
}
|
||||
hello: { type: "string" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
handler: function (request, reply) {
|
||||
reply.send({ hello: 'world' })
|
||||
}
|
||||
}
|
||||
fastify.get('/', opts)
|
||||
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/:userId", (request, reply) => {});
|
||||
fastify.get("/example/:userId/:secretToken", (request, reply) => {});
|
||||
|
||||
// 通配符
|
||||
fastify.get('/example/*', (request, reply) => {})
|
||||
fastify.get("/example/*", (request, reply) => {});
|
||||
```
|
||||
|
||||
正则表达式路由亦被支持。但要注意,正则表达式会严重拖累性能!
|
||||
|
||||
```js
|
||||
// 正则表达的参数路由
|
||||
fastify.get('/example/:file(^\\d+).png', (request, reply) => {})
|
||||
fastify.get("/example/:file(^\\d+).png", (request, reply) => {});
|
||||
```
|
||||
|
||||
你还可以在同一组斜杠 ("/") 里定义多个参数。就像这样:
|
||||
|
||||
```js
|
||||
fastify.get('/example/near/:lat-:lng/radius/:r', (request, reply) => {})
|
||||
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) => {})
|
||||
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
|
||||
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
|
||||
})
|
||||
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)
|
||||
})
|
||||
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) {
|
||||
fastify.get("/", options, async function (request, reply) {
|
||||
setImmediate(() => {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
await reply
|
||||
})
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
await reply;
|
||||
});
|
||||
```
|
||||
|
||||
返回回复也是可行的:
|
||||
|
||||
```js
|
||||
fastify.get('/', options, async function (request, reply) {
|
||||
fastify.get("/", options, async function (request, reply) {
|
||||
setImmediate(() => {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
return reply
|
||||
})
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
return reply;
|
||||
});
|
||||
```
|
||||
|
||||
**警告:**
|
||||
* 如果你同时使用 `return value` 与 `reply.send(value)`,那么只会发送第一次,同时还会触发警告日志,因为你试图发送两次响应。
|
||||
* 不能返回 `undefined`。更多细节请看 [promise 取舍](#promise-resolution)。
|
||||
|
||||
- 如果你同时使用 `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`。
|
||||
- **别** `return` 任何值。
|
||||
- **别**忘了 `reply.send`。
|
||||
2. 如果你想使用 `async/await` 或 promise:
|
||||
- **别**使用 `reply.send`。
|
||||
- **别**返回 `undefined`。
|
||||
- **别**使用 `reply.send`。
|
||||
- **别**返回 `undefined`。
|
||||
|
||||
通过这一方法,我们便可以最小代价同时支持 `回调函数风格` 以及 `async-await`。尽管这么做十分自由,我们还是强烈建议仅使用其中的一种,因为应用的错误处理方式应当保持一致。
|
||||
|
||||
**注意**:每个 async 函数各自返回一个 promise 对象。
|
||||
|
||||
<a name="route-prefixing"></a>
|
||||
|
||||
### 路由前缀
|
||||
|
||||
有时你需要维护同一 API 的多个不同版本。一般的做法是在所有的路由之前加上版本号,例如 `/v1/user`。
|
||||
Fastify 提供了一个快捷且智能的方法来解决上述问题,无需手动更改全部路由。这就是*路由前缀*。让我们来看下吧:
|
||||
|
||||
```js
|
||||
// server.js
|
||||
const fastify = require('fastify')()
|
||||
const fastify = require("fastify")();
|
||||
|
||||
fastify.register(require('./routes/v1/users'), { prefix: '/v1' })
|
||||
fastify.register(require('./routes/v2/users'), { prefix: '/v2' })
|
||||
fastify.register(require("./routes/v1/users"), { prefix: "/v1" });
|
||||
fastify.register(require("./routes/v2/users"), { prefix: "/v2" });
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
```js
|
||||
// routes/v1/users.js
|
||||
module.exports = function (fastify, opts, done) {
|
||||
fastify.get('/user', handler_v1)
|
||||
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.get("/user", handler_v2);
|
||||
done();
|
||||
};
|
||||
```
|
||||
在编译时 Fastify 自动处理了前缀,因此两个不同路由使用相同的路径名并不会产生问题。*(这也意味着性能一点儿也不受影响!)*。
|
||||
|
||||
在编译时 Fastify 自动处理了前缀,因此两个不同路由使用相同的路径名并不会产生问题。_(这也意味着性能一点儿也不受影响!)_。
|
||||
|
||||
现在,你的客户端就可以访问下列路由了:
|
||||
|
||||
- `/v1/user`
|
||||
- `/v2/user`
|
||||
|
||||
@ -297,7 +327,9 @@ module.exports = function (fastify, opts, done) {
|
||||
要改变这一行为,请见上文 `prefixTrailingSlash` 选项。
|
||||
|
||||
<a name="custom-log-level"></a>
|
||||
|
||||
### 自定义日志级别
|
||||
|
||||
在 Fastify 中为路由里设置不同的日志级别是十分容易的。<br/>
|
||||
你只需在插件或路由的选项里设置 `logLevel` 为相应的[值](https://github.com/pinojs/pino/blob/master/docs/api.md#level-string)即可。
|
||||
|
||||
@ -305,96 +337,102 @@ module.exports = function (fastify, opts, done) {
|
||||
|
||||
```js
|
||||
// server.js
|
||||
const fastify = require('fastify')({ logger: true })
|
||||
const fastify = require("fastify")({ logger: true });
|
||||
|
||||
fastify.register(require('./routes/user'), { logLevel: 'warn' })
|
||||
fastify.register(require('./routes/events'), { logLevel: 'debug' })
|
||||
fastify.register(require("./routes/user"), { logLevel: "warn" });
|
||||
fastify.register(require("./routes/events"), { logLevel: "debug" });
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
你也可以直接将其传给路由:
|
||||
|
||||
```js
|
||||
fastify.get('/', { logLevel: 'warn' }, (request, reply) => {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
fastify.get("/", { logLevel: "warn" }, (request, reply) => {
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
```
|
||||
*自定义的日志级别仅对路由生效,通过 `fastify.log` 访问的全局日志并不会受到影响。*
|
||||
|
||||
_自定义的日志级别仅对路由生效,通过 `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'), {
|
||||
const fastify = require("fastify")({ logger: true });
|
||||
fastify.register(require("./routes/user"), {
|
||||
logSerializers: {
|
||||
user: (value) => `My serializer one - ${value.name}`
|
||||
}
|
||||
})
|
||||
fastify.register(require('./routes/events'), {
|
||||
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)
|
||||
user: (value) => `My serializer two - ${value.name} ${value.surname}`,
|
||||
},
|
||||
});
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
你可以通过上下文来继承序列化器:
|
||||
|
||||
```js
|
||||
const fastify = Fastify({
|
||||
const fastify = Fastify({
|
||||
logger: {
|
||||
level: 'info',
|
||||
level: "info",
|
||||
serializers: {
|
||||
user (req) {
|
||||
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, {
|
||||
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: (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({})
|
||||
})
|
||||
reply.send({});
|
||||
});
|
||||
}
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
<a name="routes-config"></a>
|
||||
|
||||
### 配置
|
||||
|
||||
注册一个新的处理函数,你可以向其传递一个配置对象,并在其中使用它。
|
||||
|
||||
```js
|
||||
// server.js
|
||||
const fastify = require('fastify')()
|
||||
const fastify = require("fastify")();
|
||||
|
||||
function handler (req, reply) {
|
||||
reply.send(reply.context.config.output)
|
||||
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.get("/en", { config: { output: "hello world!" } }, handler);
|
||||
fastify.get("/it", { config: { output: "ciao mondo!" } }, handler);
|
||||
|
||||
fastify.listen(3000)
|
||||
fastify.listen(3000);
|
||||
```
|
||||
|
||||
<a name="constraints"></a>
|
||||
|
||||
### 约束
|
||||
|
||||
Fastify 允许你基于请求的某些属性,例如 `Host` header 或 [`find-my-way`](https://github.com/delvedor/find-my-way) 指定的其他值,来限制路由仅匹配特定的请求。路由选项里的 `constraints` 属性便是用于这一特性。Fastify 有两个内建的约束属性:`version` 及 `host`。你可以自定义约束策略,来判断某路由是否处理一个请求。
|
||||
@ -403,45 +441,51 @@ Fastify 允许你基于请求的某些属性,例如 `Host` header 或 [`find-m
|
||||
|
||||
你可以在路由的 `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' },
|
||||
method: "GET",
|
||||
url: "/",
|
||||
constraints: { version: "1.2.0" },
|
||||
handler: function (request, reply) {
|
||||
reply.send({ hello: 'world' })
|
||||
}
|
||||
})
|
||||
reply.send({ hello: "world" });
|
||||
},
|
||||
});
|
||||
|
||||
fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
'Accept-Version': '1.x' // 也可以是 '1.2.0' 或 '1.2.x'
|
||||
}
|
||||
}, (err, res) => {
|
||||
// { 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)
|
||||
> 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/>
|
||||
@ -455,44 +499,50 @@ fastify.inject({
|
||||
|
||||
```js
|
||||
fastify.route({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
constraints: { host: 'auth.fastify.io' },
|
||||
method: "GET",
|
||||
url: "/",
|
||||
constraints: { host: "auth.fastify.io" },
|
||||
handler: function (request, reply) {
|
||||
reply.send('hello world from auth.fastify.io')
|
||||
}
|
||||
})
|
||||
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: "example.com",
|
||||
},
|
||||
},
|
||||
(err, res) => {
|
||||
// 返回 404,因为 host 不匹配
|
||||
},
|
||||
);
|
||||
|
||||
fastify.inject({
|
||||
method: 'GET',
|
||||
url: '/',
|
||||
headers: {
|
||||
'Host': 'auth.fastify.io'
|
||||
}
|
||||
}, (err, res) => {
|
||||
// => 'hello world from auth.fastify.io'
|
||||
})
|
||||
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: '/',
|
||||
method: "GET",
|
||||
url: "/",
|
||||
constraints: { host: /.*\.fastify\.io/ }, // 匹配 fastify.io 的任意子域
|
||||
handler: function (request, reply) {
|
||||
reply.send('hello world from ' + request.headers.host)
|
||||
}
|
||||
})
|
||||
```
|
||||
reply.send("hello world from " + request.headers.host);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -21,16 +21,16 @@ Fastify 无法直接运行在无服务器平台上,需要做一点修改。本
|
||||
|
||||
以下是使用 Fastify 在 AWS Lambda 和 Amazon API Gateway 架构上构建无服务器 web 应用/服务的示例。
|
||||
|
||||
*注:使用 [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) 仅是一种可行方案。*
|
||||
_注:使用 [aws-lambda-fastify](https://github.com/fastify/aws-lambda-fastify) 仅是一种可行方案。_
|
||||
|
||||
### app.js
|
||||
|
||||
```js
|
||||
const fastify = require('fastify');
|
||||
const fastify = require("fastify");
|
||||
|
||||
function init() {
|
||||
const app = fastify();
|
||||
app.get('/', (request, reply) => reply.send({ hello: 'world' }));
|
||||
app.get("/", (request, reply) => reply.send({ hello: "world" }));
|
||||
return app;
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ if (require.main === module) {
|
||||
// 直接调用,即执行 "node app"
|
||||
init().listen(3000, (err) => {
|
||||
if (err) console.error(err);
|
||||
console.log('server listening on 3000');
|
||||
console.log("server listening on 3000");
|
||||
});
|
||||
} else {
|
||||
// 作为模块引入 => 用于 aws lambda
|
||||
@ -52,16 +52,16 @@ if (require.main === module) {
|
||||
在 [`lambda.js`](https://www.fastify.io/docs/latest/Serverless/#lambda-js) 里,我们会用到它。
|
||||
|
||||
当像往常一样运行 Fastify 应用,
|
||||
比如执行 `node app.js` 时 *(可以用 `require.main === module` 来判断)*,
|
||||
比如执行 `node app.js` 时 _(可以用 `require.main === module` 来判断)_,
|
||||
你可以监听某个端口,如此便能本地运行应用了。
|
||||
|
||||
### lambda.js
|
||||
|
||||
```js
|
||||
const awsLambdaFastify = require('aws-lambda-fastify')
|
||||
const init = require('./app');
|
||||
const awsLambdaFastify = require("aws-lambda-fastify");
|
||||
const init = require("./app");
|
||||
|
||||
const proxy = awsLambdaFastify(init())
|
||||
const proxy = awsLambdaFastify(init());
|
||||
// 或
|
||||
// const proxy = awsLambdaFastify(init(), { binaryMimeTypes: ['application/octet-stream'] })
|
||||
|
||||
@ -82,7 +82,6 @@ exports.handler = 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 还不支持它。
|
||||
@ -92,7 +91,7 @@ exports.handler = proxy;
|
||||
|
||||
与 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)*。
|
||||
_参照以下步骤部署 Google Cloud Run。如果你对 gcloud 还不熟悉,请看其[入门文档](https://cloud.google.com/run/docs/quickstarts/build-and-deploy)_。
|
||||
|
||||
### 调整 Fastify 服务器
|
||||
|
||||
@ -100,35 +99,35 @@ exports.handler = proxy;
|
||||
|
||||
```js
|
||||
function build() {
|
||||
const fastify = Fastify({ trustProxy: true })
|
||||
return fastify
|
||||
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
|
||||
const IS_GOOGLE_CLOUD_RUN = process.env.K_SERVICE !== undefined;
|
||||
|
||||
// 监听 Cloud Run 提供的端口
|
||||
const port = process.env.PORT || 3000
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
// 监听 Cloud Run 中所有的 IPV4 地址
|
||||
const address = IS_GOOGLE_CLOUD_RUN ? "0.0.0.0" : undefined
|
||||
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}`)
|
||||
const server = build();
|
||||
const address = await server.listen(port, address);
|
||||
console.log(`Listening on ${address}`);
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = build
|
||||
module.exports = build;
|
||||
|
||||
if (require.main === module) {
|
||||
start()
|
||||
start();
|
||||
}
|
||||
```
|
||||
|
||||
@ -197,7 +196,7 @@ gcloud beta run deploy --image gcr.io/PROJECT-ID/APP-NAME --platform managed
|
||||
### functions/server.js
|
||||
|
||||
```js
|
||||
export { handler } from '../lambda.js'; // 记得将路径修改为你的应用中对应的 `lambda.js` 的路径
|
||||
export { handler } from "../lambda.js"; // 记得将路径修改为你的应用中对应的 `lambda.js` 的路径
|
||||
```
|
||||
|
||||
### netlify.toml
|
||||
@ -218,12 +217,12 @@ export { handler } from '../lambda.js'; // 记得将路径修改为你的应用
|
||||
**别忘记添加这个文件,否则会有不少问题**
|
||||
|
||||
```js
|
||||
const nodeExternals = require('webpack-node-externals');
|
||||
const dotenv = require('dotenv-safe');
|
||||
const webpack = require('webpack');
|
||||
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';
|
||||
const env = process.env.NODE_ENV || "production";
|
||||
const dev = env === "development";
|
||||
|
||||
if (dev) {
|
||||
dotenv.config({ allowEmptyValues: true });
|
||||
@ -231,32 +230,32 @@ if (dev) {
|
||||
|
||||
module.exports = {
|
||||
mode: env,
|
||||
devtool: dev ? 'eval-source-map' : 'none',
|
||||
devtool: dev ? "eval-source-map" : "none",
|
||||
externals: [nodeExternals()],
|
||||
devServer: {
|
||||
proxy: {
|
||||
'/.netlify': {
|
||||
target: 'http://localhost:9000',
|
||||
pathRewrite: { '^/.netlify/functions': '' }
|
||||
}
|
||||
}
|
||||
"/.netlify": {
|
||||
target: "http://localhost:9000",
|
||||
pathRewrite: { "^/.netlify/functions": "" },
|
||||
},
|
||||
},
|
||||
},
|
||||
module: {
|
||||
rules: []
|
||||
rules: [],
|
||||
},
|
||||
plugins: [
|
||||
new webpack.DefinePlugin({
|
||||
'process.env.APP_ROOT_PATH': JSON.stringify('/'),
|
||||
'process.env.NETLIFY_ENV': true,
|
||||
'process.env.CONTEXT': env
|
||||
})
|
||||
]
|
||||
"process.env.APP_ROOT_PATH": JSON.stringify("/"),
|
||||
"process.env.NETLIFY_ENV": true,
|
||||
"process.env.CONTEXT": env,
|
||||
}),
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
在 `package.json` 的 *scripts* 里加上这一命令
|
||||
在 `package.json` 的 _scripts_ 里加上这一命令
|
||||
|
||||
```json
|
||||
"scripts": {
|
||||
@ -305,6 +304,6 @@ app.register(import("../src/app"));
|
||||
|
||||
export default async (req, res) => {
|
||||
await app.ready();
|
||||
app.server.emit('request', req, res);
|
||||
}
|
||||
```
|
||||
app.server.emit("request", req, res);
|
||||
};
|
||||
```
|
||||
|
@ -15,40 +15,40 @@
|
||||
**app.js**:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const fastify = require('fastify')
|
||||
const fastify = require("fastify");
|
||||
|
||||
function build(opts={}) {
|
||||
const app = fastify(opts)
|
||||
app.get('/', async function (request, reply) {
|
||||
return { hello: 'world' }
|
||||
})
|
||||
function build(opts = {}) {
|
||||
const app = fastify(opts);
|
||||
app.get("/", async function (request, reply) {
|
||||
return { hello: "world" };
|
||||
});
|
||||
|
||||
return app
|
||||
return app;
|
||||
}
|
||||
|
||||
module.exports = build
|
||||
module.exports = build;
|
||||
```
|
||||
|
||||
**server.js**:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const server = require('./app')({
|
||||
const server = require("./app")({
|
||||
logger: {
|
||||
level: 'info',
|
||||
prettyPrint: true
|
||||
}
|
||||
})
|
||||
level: "info",
|
||||
prettyPrint: true,
|
||||
},
|
||||
});
|
||||
|
||||
server.listen(3000, (err, address) => {
|
||||
if (err) {
|
||||
console.log(err)
|
||||
process.exit(1)
|
||||
console.log(err);
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
### 使用 fastify.inject() 的好处
|
||||
@ -60,22 +60,22 @@ server.listen(3000, (err, address) => {
|
||||
**app.test.js**:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const build = require('./app')
|
||||
const build = require("./app");
|
||||
|
||||
const test = async () => {
|
||||
const app = build()
|
||||
const app = build();
|
||||
|
||||
const response = await app.inject({
|
||||
method: 'GET',
|
||||
url: '/'
|
||||
})
|
||||
method: "GET",
|
||||
url: "/",
|
||||
});
|
||||
|
||||
console.log('status code: ', response.statusCode)
|
||||
console.log('body: ', response.body)
|
||||
}
|
||||
test()
|
||||
console.log("status code: ", response.statusCode);
|
||||
console.log("body: ", response.body);
|
||||
};
|
||||
test();
|
||||
```
|
||||
|
||||
我们的代码运行在异步函数里,因此可以使用 async/await。
|
||||
@ -100,36 +100,40 @@ body: {"hello":"world"}
|
||||
**app.test.js**:
|
||||
|
||||
```js
|
||||
'use strict'
|
||||
"use strict";
|
||||
|
||||
const { test } = require('tap')
|
||||
const build = require('./app')
|
||||
const { test } = require("tap");
|
||||
const build = require("./app");
|
||||
|
||||
test('requests the "/" route', async t => {
|
||||
const app = build()
|
||||
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')
|
||||
})
|
||||
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) => {
|
||||
// 你的测试
|
||||
})
|
||||
fastify.inject(
|
||||
{
|
||||
method: String,
|
||||
url: String,
|
||||
query: Object,
|
||||
payload: Object,
|
||||
headers: Object,
|
||||
cookies: Object,
|
||||
},
|
||||
(error, response) => {
|
||||
// 你的测试
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
忽略回调函数,可以链式调用 `.inject` 提供的方法:
|
||||
@ -137,12 +141,13 @@ fastify.inject({
|
||||
```js
|
||||
fastify
|
||||
.inject()
|
||||
.get('/')
|
||||
.headers({ foo: 'bar' })
|
||||
.query({ foo: 'bar' })
|
||||
.end((err, res) => { // 调用 .end 触发请求
|
||||
console.log(res.payload)
|
||||
})
|
||||
.get("/")
|
||||
.headers({ foo: "bar" })
|
||||
.query({ foo: "bar" })
|
||||
.end((err, res) => {
|
||||
// 调用 .end 触发请求
|
||||
console.log(res.payload);
|
||||
});
|
||||
```
|
||||
|
||||
或是用 promise 的版本
|
||||
@ -155,20 +160,26 @@ fastify
|
||||
query: Object,
|
||||
payload: Object,
|
||||
headers: Object,
|
||||
cookies: Object
|
||||
cookies: Object,
|
||||
})
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
// 你的测试
|
||||
})
|
||||
.catch(err => {
|
||||
.catch((err) => {
|
||||
// 处理错误
|
||||
})
|
||||
});
|
||||
```
|
||||
|
||||
Async await 也是支持的!
|
||||
|
||||
```js
|
||||
try {
|
||||
const res = await fastify.inject({ method: String, url: String, payload: Object, headers: Object })
|
||||
const res = await fastify.inject({
|
||||
method: String,
|
||||
url: String,
|
||||
payload: Object,
|
||||
headers: Object,
|
||||
});
|
||||
// 你的测试
|
||||
} catch (err) {
|
||||
// 处理错误
|
||||
@ -178,49 +189,58 @@ try {
|
||||
#### 另一个例子:
|
||||
|
||||
**app.js**
|
||||
|
||||
```js
|
||||
const Fastify = require('fastify')
|
||||
const Fastify = require("fastify");
|
||||
|
||||
function buildFastify () {
|
||||
const fastify = Fastify()
|
||||
function buildFastify() {
|
||||
const fastify = Fastify();
|
||||
|
||||
fastify.get('/', function (request, reply) {
|
||||
reply.send({ hello: 'world' })
|
||||
})
|
||||
|
||||
return fastify
|
||||
fastify.get("/", function (request, reply) {
|
||||
reply.send({ hello: "world" });
|
||||
});
|
||||
|
||||
return fastify;
|
||||
}
|
||||
|
||||
module.exports = buildFastify
|
||||
module.exports = buildFastify;
|
||||
```
|
||||
|
||||
**test.js**
|
||||
|
||||
```js
|
||||
const tap = require('tap')
|
||||
const buildFastify = require('./app')
|
||||
const tap = require("tap");
|
||||
const buildFastify = require("./app");
|
||||
|
||||
tap.test('GET `/` route', t => {
|
||||
t.plan(4)
|
||||
tap.test("GET `/` route", (t) => {
|
||||
t.plan(4);
|
||||
|
||||
const fastify = buildFastify()
|
||||
const fastify = buildFastify();
|
||||
|
||||
// 在测试的最后,我们强烈建议你调用 `.close()`
|
||||
// 方法来确保所有与外部服务的连接被关闭。
|
||||
t.teardown(() => fastify.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.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 的测试。
|
||||
|
||||
#### 举例:
|
||||
@ -228,67 +248,81 @@ tap.test('GET `/` route', t => {
|
||||
使用之前例子的 **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')
|
||||
const tap = require("tap");
|
||||
const request = require("request");
|
||||
const buildFastify = require("./app");
|
||||
|
||||
tap.test('GET `/` route', t => {
|
||||
t.plan(5)
|
||||
tap.test("GET `/` route", (t) => {
|
||||
t.plan(5);
|
||||
|
||||
const fastify = buildFastify()
|
||||
const fastify = buildFastify();
|
||||
|
||||
t.teardown(() => fastify.close())
|
||||
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' })
|
||||
})
|
||||
})
|
||||
})
|
||||
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')
|
||||
const tap = require("tap");
|
||||
const supertest = require("supertest");
|
||||
const buildFastify = require("./app");
|
||||
|
||||
tap.test('GET `/` route', async (t) => {
|
||||
const fastify = buildFastify()
|
||||
tap.test("GET `/` route", async (t) => {
|
||||
const fastify = buildFastify();
|
||||
|
||||
t.teardown(() => fastify.close())
|
||||
t.teardown(() => fastify.close());
|
||||
|
||||
await fastify.ready()
|
||||
await fastify.ready();
|
||||
|
||||
const response = await supertest(fastify.server)
|
||||
.get('/')
|
||||
.get("/")
|
||||
.expect(200)
|
||||
.expect('Content-Type', 'application/json; charset=utf-8')
|
||||
t.same(response.body, { hello: 'world' })
|
||||
})
|
||||
.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` 的其他部分) 了。
|
||||
现在你便可以在编辑器中检测你的测试文件 (以及 `Fastify` 的其他部分) 了。
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,12 +1,14 @@
|
||||
<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) 标签!*
|
||||
_需要一些灵感?你可以在 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 即可!
|
||||
@ -14,8 +16,10 @@ Fastify 运用了不同的技术来优化代码,其大部分都被写入了文
|
||||
一旦你向我们的 [生态列表](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)
|
||||
@ -23,37 +27,44 @@ Fastify 运用了不同的技术来优化代码,其大部分都被写入了文
|
||||
- [`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 的模板渲染 (_ejs, pug, handlebars, marko_) 插件。
|
||||
- [`fastify-mongodb`](https://github.com/fastify/fastify-mongodb)
|
||||
Fastify 的 MongoDB 连接插件,通过它你可以在你服务器的每个部分共享同一个 MongoDB 连接池。
|
||||
Fastify 的 MongoDB 连接插件,通过它你可以在你服务器的每个部分共享同一个 MongoDB 连接池。
|
||||
- [`fastify-multipart`](https://github.com/fastify/fastify-multipart)
|
||||
为 Fastify 提供 mltipart 支持。
|
||||
为 Fastify 提供 mltipart 支持。
|
||||
- [`fastify-helmet`](https://github.com/fastify/fastify-helmet)
|
||||
重要的请求头安全插件。
|
||||
重要的请求头安全插件。
|
||||
|
22
drizzle.config.js
Normal file
22
drizzle.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
import dotenv from 'dotenv';
|
||||
|
||||
dotenv.config({ path: '.env' });
|
||||
|
||||
export default {
|
||||
schema: './src/entities/schema.js',
|
||||
out: './SQL',
|
||||
dialect: 'mysql',
|
||||
dbCredentials: {
|
||||
host: process.env.DB_HOST,
|
||||
port: Number(process.env.DB_PORT),
|
||||
user: process.env.DB_USER,
|
||||
password: process.env.DB_PASSWORD,
|
||||
database: process.env.DB_NAME,
|
||||
},
|
||||
verbose: true,
|
||||
strict: true,
|
||||
introspect: {
|
||||
// 启用驼峰命名
|
||||
casing: 'camel',
|
||||
},
|
||||
};
|
35
eslint.config.js
Normal file
35
eslint.config.js
Normal file
@ -0,0 +1,35 @@
|
||||
import configPrettier from 'eslint-config-prettier';
|
||||
import pluginImport from 'eslint-plugin-import';
|
||||
|
||||
export default [
|
||||
{
|
||||
// 将 settings 提升到顶层
|
||||
settings: {
|
||||
'import/resolver': {
|
||||
'custom-alias': {
|
||||
extensions: ['.js'],
|
||||
alias: {
|
||||
'#config': './config',
|
||||
'#src': './src',
|
||||
'#start': './src/utils/start.js',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
semi: ['error', 'always'],
|
||||
'comma-dangle': ['error', 'always-multiline'],
|
||||
},
|
||||
},
|
||||
configPrettier,
|
||||
{
|
||||
plugins: { import: pluginImport },
|
||||
rules: pluginImport.configs.recommended.rules,
|
||||
},
|
||||
];
|
7
migrations/0000_wandering_marvel_boy.sql
Normal file
7
migrations/0000_wandering_marvel_boy.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE `users` (
|
||||
`id` int NOT NULL,
|
||||
`name` varchar(255),
|
||||
`email` varchar(255),
|
||||
CONSTRAINT `users_id` PRIMARY KEY(`id`),
|
||||
CONSTRAINT `users_email_unique` UNIQUE(`email`)
|
||||
);
|
59
migrations/meta/0000_snapshot.json
Normal file
59
migrations/meta/0000_snapshot.json
Normal file
@ -0,0 +1,59 @@
|
||||
{
|
||||
"version": "5",
|
||||
"dialect": "mysql",
|
||||
"id": "08e581e2-3826-4de5-b172-057ee6a72da0",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"users": {
|
||||
"name": "users",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "int",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"autoincrement": false
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "varchar(255)",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"autoincrement": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {
|
||||
"users_id": {
|
||||
"name": "users_id",
|
||||
"columns": ["id"]
|
||||
}
|
||||
},
|
||||
"uniqueConstraints": {
|
||||
"users_email_unique": {
|
||||
"name": "users_email_unique",
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"checkConstraint": {}
|
||||
}
|
||||
},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
},
|
||||
"internal": {
|
||||
"tables": {},
|
||||
"indexes": {}
|
||||
}
|
||||
}
|
13
migrations/meta/_journal.json
Normal file
13
migrations/meta/_journal.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "mysql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1742369871003,
|
||||
"tag": "0000_wandering_marvel_boy",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
6665
package-lock.json
generated
6665
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
85
package.json
85
package.json
@ -1,37 +1,52 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
"name": "yuheng",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/application.js",
|
||||
"type": "module",
|
||||
"imports": {
|
||||
"#/*": "./*",
|
||||
"#common/*": "./src/common/*",
|
||||
"#config/*": "./config/*.js",
|
||||
"#start": "./src/utils/start.js",
|
||||
"#plugins/*": "./src/plugins/*.js",
|
||||
"#src/*": "./src/*.js"
|
||||
},
|
||||
"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 src --ext .js",
|
||||
"format": "prettier --write . --config ./.prettier.config.cjs --ignore-path .prettierignore",
|
||||
"lint:format": "prettier --check . --config ./.prettier.config.cjs --ignore-path .prettierignore",
|
||||
"makeSQL": "drizzle-kit generate",
|
||||
"makeEntity": "drizzle-kit introspect",
|
||||
"syncDB": "drizzle-kit migrate",
|
||||
"sqlV": "drizzle-kit studio"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"dotenv": "^16.4.7",
|
||||
"drizzle-kit": "^0.30.5",
|
||||
"drizzle-orm": "^0.40.1",
|
||||
"fastify": "^5.2.1",
|
||||
"fastify-plugin": "^5.0.1",
|
||||
"mysql2": "^3.13.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-multi-stream": "^6.0.0",
|
||||
"pino-pretty": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-config-prettier": "^10.0.2",
|
||||
"eslint-import-resolver-custom-alias": "^1.3.2",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"nodemon": "^3.1.9",
|
||||
"prettier": "^3.5.3"
|
||||
}
|
||||
}
|
||||
|
@ -1,23 +1,19 @@
|
||||
// ESM
|
||||
import "#start"
|
||||
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';
|
||||
import config from '#config/index';
|
||||
import plugin from '#src/plugins/index';
|
||||
import routes from '#src/routes/index';
|
||||
import logger from '#src/utils/logger';
|
||||
|
||||
async function start() {
|
||||
// 创建Fastify实例
|
||||
const fastify = new Fastify({
|
||||
logger
|
||||
})
|
||||
const fastify = new Fastify({ logger });
|
||||
// 加载插件
|
||||
await fastify.register(plugin);
|
||||
await fastify.register(plugin, { config });
|
||||
// 加载路由
|
||||
fastify.register(routes);
|
||||
|
||||
await fastify.listen(config.server); // 使用配置中的服务器参数
|
||||
await fastify.register(routes);
|
||||
// 启动服务器
|
||||
await fastify.listen(config.server); // 使用配置中的服务器参数
|
||||
}
|
||||
|
||||
|
||||
start();
|
||||
start();
|
||||
|
17
src/entities/schema.js
Normal file
17
src/entities/schema.js
Normal file
@ -0,0 +1,17 @@
|
||||
/**
|
||||
* @typedef {import('drizzle-orm/mysql-core').MySqlTable} MySqlTable
|
||||
* @typedef {import('drizzle-orm/mysql-core').varchar} varchar
|
||||
* @typedef {import('drizzle-orm/mysql-core').int} int
|
||||
*/
|
||||
|
||||
import { mysqlTable, varchar, int } from 'drizzle-orm/mysql-core';
|
||||
|
||||
/**
|
||||
* 用户模型
|
||||
* @type {MySqlTable}
|
||||
*/
|
||||
export const users = mysqlTable('users', {
|
||||
id: int('id').primaryKey(),
|
||||
name: varchar('name', { length: 255 }),
|
||||
email: varchar('email', { length: 255 }).unique(),
|
||||
});
|
28
src/plugins/database/index.js
Normal file
28
src/plugins/database/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
import { drizzle } from 'drizzle-orm/mysql2';
|
||||
import { sql } from 'drizzle-orm';
|
||||
import mysql from 'mysql2/promise';
|
||||
import fastifyPlugin from 'fastify-plugin';
|
||||
async function database(fastify, options) {
|
||||
fastify.log.warn('Register Database Plugin!');
|
||||
const config = fastify.config;
|
||||
// 配置数据库
|
||||
const pool = await mysql.createPool({
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
user: config.db.user,
|
||||
password: config.db.password,
|
||||
database: config.db.database,
|
||||
ssl: config.db.ssl,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
queueLimit: 0,
|
||||
});
|
||||
// 暴露数据库
|
||||
const db = drizzle(pool);
|
||||
// 新增获取所有表名方法
|
||||
const [result] = await db.execute(sql`SHOW TABLES`);
|
||||
const tableList = result.map(row => Object.values(row)[0]);
|
||||
db.tableList = tableList;
|
||||
fastify.decorate('db', db);
|
||||
}
|
||||
export default fastifyPlugin(database);
|
@ -1,13 +1,14 @@
|
||||
export default async function plugin(fastify, opts) {
|
||||
import fp from 'fastify-plugin';
|
||||
import database from '#plugins/database/index';
|
||||
|
||||
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);
|
||||
// 注册数据库
|
||||
await fastify.register(database);
|
||||
fastify.log.warn(fastify.db.tableList);
|
||||
// 读取数据库基本信息
|
||||
fastify.log.warn('Register Global Plugin complete!');
|
||||
}
|
||||
}
|
||||
export default fp(plugin);
|
||||
|
@ -1,13 +1,7 @@
|
||||
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' };
|
||||
// });
|
||||
// 定义一个GET请求的路由,路径为根路径
|
||||
fastify.get('/', async (request, reply) => {
|
||||
fastify.log.info(fastify.config);
|
||||
return fastify.config;
|
||||
});
|
||||
}
|
||||
|
||||
|
35
src/services/user.service.js
Normal file
35
src/services/user.service.js
Normal file
@ -0,0 +1,35 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { users } from '#db/schema';
|
||||
|
||||
/**
|
||||
* 用户服务模块
|
||||
* @param {object} fastify - Fastify实例
|
||||
*/
|
||||
export default async function userService(fastify) {
|
||||
const { db } = fastify;
|
||||
|
||||
return {
|
||||
/**
|
||||
* 根据ID获取用户
|
||||
* @param {number} userId - 用户ID
|
||||
* @returns {Promise<object>} 用户对象
|
||||
*/
|
||||
async getUserById(userId) {
|
||||
return db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.id, userId))
|
||||
.limit(1)
|
||||
.then(res => res[0]);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建新用户
|
||||
* @param {object} userData - 用户数据
|
||||
* @returns {Promise<object>} 创建的用户记录
|
||||
*/
|
||||
async createUser(userData) {
|
||||
return db.insert(users).values(userData).returning();
|
||||
},
|
||||
};
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import config from "#config/index.js"
|
||||
export default{
|
||||
import config from '#config/index';
|
||||
export default {
|
||||
// 日志级别顺序(从高到低):
|
||||
// fatal(致命) > error(错误) > warn(警告) > info(信息) > debug(调试) > trace(追踪)
|
||||
// 高级别日志会包含低级别日志(设置warn会包含error和fatal)
|
||||
@ -27,16 +27,16 @@ export default{
|
||||
// - req: 请求对象
|
||||
// - responseTime: 响应时间
|
||||
// - 支持通配符如 'req*'(忽略所有 req 开头字段)
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
target: 'pino/file',
|
||||
options: {
|
||||
destination: config.logger.filePath, // 文件路径从配置读取
|
||||
destination: config.logger.filePath, // 文件路径从配置读取
|
||||
mkdir: true,
|
||||
ignore: 'pid,hostname',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
@ -1,12 +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字符宽度
|
||||
|
||||
/* eslint-disable no-console */
|
||||
console.log(
|
||||
'\x1B[48;5;0m%s\x1B[0m', // 灰色背景
|
||||
'\x1B[32;5;12m\x1B[1m ' + // 白色加粗
|
||||
'-'.repeat(padding) +
|
||||
text +
|
||||
'-'.repeat(padding)+
|
||||
' \x1B[0m' // 重置样式
|
||||
);
|
||||
'-'.repeat(padding) +
|
||||
text +
|
||||
'-'.repeat(padding) +
|
||||
' \x1B[0m', // 重置样式
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user