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