路由
路由
路由方法将配置应用程序的端点。您可以使用两种方法在 Fastify 中声明路由:简写方法和完整声明。
完整声明
fastify.route(options)
路由选项
method
: 目前支持GET
、HEAD
、TRACE
、DELETE
、OPTIONS
、PATCH
、PUT
和POST
。要接受更多方法,必须使用addHttpMethod
。它也可以是方法数组。url
: 与此路由匹配的 URL 路径(别名:path
)。schema
: 包含请求和响应模式的对象。它们需要采用 JSON Schema 格式,请查看 此处 获取更多信息。body
: 如果是 POST、PUT、PATCH、TRACE、SEARCH、PROPFIND、PROPPATCH 或 LOCK 方法,则验证请求的主体。querystring
或query
: 验证查询字符串。这可以是一个完整的 JSON Schema 对象,其type
属性为object
,properties
对象为参数,或者只是如下所示的properties
对象中包含的内容的值。params
: 验证参数。response
: 过滤并为响应生成模式,设置模式可以使我们获得 10-20% 的更高吞吐量。
exposeHeadRoute
: 为任何GET
路由创建一个同级HEAD
路由。默认为exposeHeadRoutes
实例选项的值。如果您希望自定义HEAD
处理程序而不禁用此选项,请确保在GET
路由之前定义它。attachValidation
: 将validationError
附加到请求,如果存在模式验证错误,则不将其发送到错误处理程序。默认的 错误格式 是 Ajv 格式。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)
: 发送响应后调用的 函数,因此您将无法向客户端发送更多数据。它也可以是函数数组。onTimeout(request, reply, done)
: 请求超时且 HTTP 套接字已挂断时调用的 函数。onError(request, reply, error, done)
: 路由处理程序抛出或发送到客户端的错误时调用的 函数。handler(request, reply)
: 将处理此请求的函数。调用处理程序时,Fastify 服务器 将绑定到this
。注意:使用箭头函数将破坏this
的绑定。errorHandler(error, request, reply)
: 请求范围内的自定义错误处理程序。覆盖默认的全局错误处理程序,以及setErrorHandler
为路由请求设置的任何内容。要访问默认处理程序,您可以访问instance.errorHandler
。请注意,只有在插件尚未覆盖它时,它才会指向 fastify 的默认errorHandler
。childLoggerFactory(logger, binding, opts, rawReq)
: 用于为每个请求生成子日志记录器实例的自定义工厂函数。有关更多信息,请参阅childLoggerFactory
。覆盖默认的日志记录器工厂,以及setChildLoggerFactory
为路由请求设置的任何内容。要访问默认工厂,您可以访问instance.childLoggerFactory
。请注意,只有在插件尚未覆盖它时,它才会指向 Fastify 的默认childLoggerFactory
。validatorCompiler({ schema, method, url, httpPart })
: 用于构建请求验证模式的函数。请参阅 验证和序列化 文档。serializerCompiler({ { schema, method, url, httpStatus, contentType } })
: 用于构建响应序列化的模式的函数。请参阅 验证和序列化 文档。schemaErrorFormatter(errors, dataVar)
: 用于格式化验证编译器错误的函数。请参阅 验证和序列化 文档。覆盖全局模式错误格式化程序处理程序,以及setSchemaErrorFormatter
为路由请求设置的任何内容。bodyLimit
: 防止默认 JSON 主体解析器解析大于此字节数的请求主体。必须是整数。您也可以在使用fastify(options)
首次创建 Fastify 实例时全局设置此选项。默认为1048576
(1 MiB)。logLevel
: 设置此路由的日志级别。请参见下文。logSerializers
: 设置要为此路由记录的序列化器。config
: 用于存储自定义配置的对象。constraints
: 基于请求属性或值定义路由限制,使用 find-my-way 约束启用自定义匹配。包括内置的version
和host
约束,以及对自定义约束策略的支持。prefixTrailingSlash
: 用于确定如何处理将/
作为具有前缀的路由传递的字符串。both
(默认): 将注册/prefix
和/prefix/
。slash
: 将仅注册/prefix/
。no-slash
: 将仅注册/prefix
。
注意:此选项不会覆盖 服务器 配置中的
ignoreTrailingSlash
。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.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)
将向所有支持的方法添加相同的处理程序。
也可以通过 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。
要注册参数化路径,请在参数名称之前使用冒号。对于通配符,请使用星号。请记住,静态路由始终在参数化路由和通配符路由之前检查。
// parametric
fastify.get('/example/:userId', function (request, reply) {
// curl ${app-url}/example/12345
// userId === '12345'
const { userId } = request.params;
// your code here
})
fastify.get('/example/:userId/:secretToken', function (request, reply) {
// curl ${app-url}/example/12345/abc.zHi
// userId === '12345'
// secretToken === 'abc.zHi'
const { userId, secretToken } = request.params;
// your code here
})
// wildcard
fastify.get('/example/*', function (request, reply) {})
正则表达式路由也受支持,但请注意,您必须转义斜杠。请注意,RegExp 在性能方面也非常昂贵!
// parametric with regexp
fastify.get('/example/:file(^\\d+).png', function (request, reply) {
// curl ${app-url}/example/12345.png
// file === '12345'
const { file } = request.params;
// your code here
})
可以在同一对斜杠 ("/") 内定义多个参数。例如
fastify.get('/example/near/:lat-:lng/radius/:r', function (request, reply) {
// curl ${app-url}/example/near/15°N-30°E/radius/20
// lat === "15°N"
// lng === "30°E"
// r ==="20"
const { lat, lng, r } = request.params;
// your code here
})
在这种情况下,请记住使用连字符 ("-") 作为参数分隔符。
最后,可以使用多个带有 RegExp 的参数。
fastify.get('/example/at/:hour(^\\d{2})h:minute(^\\d{2})m', function (request, reply) {
// curl ${app-url}/example/at/08h24m
// hour === "08"
// minute === "24"
const { hour, minute } = request.params;
// your code here
})
在这种情况下,可以使用正则表达式不匹配的任何字符作为参数分隔符。
如果在参数名称末尾添加问号 ("?"),则最后一个参数可以设置为可选。
fastify.get('/example/posts/:id?', function (request, reply) {
const { id } = request.params;
// your code here
})
在这种情况下,您可以请求 /example/posts
以及 /example/posts/1
。如果未指定,则可选参数将为未定义。
具有多个参数的路由可能会对性能产生负面影响,因此尽可能优先使用单个参数方法,尤其是在应用程序热路径上的路由。如果您对我们如何处理路由感兴趣,请查看 find-my-way。
如果希望路径包含冒号而不声明参数,请使用双冒号。例如
fastify.post('/name::verb') // will be interpreted as /name:verb
异步等待
您是 async/await
用户吗?我们已为您准备好了!
fastify.get('/', options, async function (request, reply) {
var data = await getData()
var processed = await processData(data)
return processed
})
如您所见,我们没有调用 reply.send
将数据发送回用户。您只需返回主体即可!
如果需要,您还可以使用reply.send
将数据发送回用户。在这种情况下,请不要忘记在您的async
处理程序中return reply
或await reply
,否则在某些情况下会导致竞争条件。
fastify.get('/', options, async function (request, reply) {
var data = await getData()
var processed = await processData(data)
return reply.send(processed)
})
如果路由包装了一个基于回调的API,该API将在 promise 链之外调用reply.send()
,则可以await reply
。
fastify.get('/', options, async function (request, reply) {
setImmediate(() => {
reply.send({ hello: 'world' })
})
await reply
})
返回 reply 也可行。
fastify.get('/', options, async function (request, reply) {
setImmediate(() => {
reply.send({ hello: 'world' })
})
return reply
})
警告
- 当同时使用
return value
和reply.send(value)
时,先发生的将优先,第二个值将被丢弃,并且还会发出警告日志,因为您尝试发送两次响应。 - 在 promise 外部调用
reply.send()
是可能的,但需要特别注意。有关更多详细信息,请阅读promise-resolution。 - 您不能返回
undefined
。有关更多详细信息,请阅读promise-resolution。
Promise 解析
如果您的处理程序是async
函数或返回一个 promise,则应注意支持回调和 promise 控制流所需的特殊行为。当处理程序的 promise 被解析时,回复将自动发送其值,除非您在处理程序中显式地等待或返回reply
。
- 如果您想使用
async/await
或 promise,但使用reply.send
响应一个值- 请执行
return reply
/await reply
。 - 请勿忘记调用
reply.send
。
- 请执行
- 如果您想使用
async/await
或 promise- 请勿使用
reply.send
。 - 请执行返回您要发送的值。
- 请勿使用
通过这种方式,我们可以以最小的折衷支持callback-style
和async-await
。尽管有如此多的自由,我们还是强烈建议只使用一种风格,因为错误处理应该在您的应用程序中以一致的方式进行处理。
注意:每个异步函数本身都会返回一个 promise。
路由前缀
有时您需要维护相同 API 的两个或更多不同版本;一种经典的方法是使用 API 版本号作为所有路由的前缀,例如/v1/user
。Fastify 提供了一种快速且智能的方式来创建相同 API 的不同版本,而无需手动更改所有路由名称,即路由前缀。让我们看看它是如何工作的。
// server.js
const fastify = require('fastify')()
fastify.register(require('./routes/v1/users'), { prefix: '/v1' })
fastify.register(require('./routes/v2/users'), { prefix: '/v2' })
fastify.listen({ port: 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
,并且也支持路由参数。
如果您希望对所有路由使用前缀,可以将它们放在插件中。
const fastify = require('fastify')()
const route = {
method: 'POST',
url: '/login',
handler: () => {},
schema: {},
}
fastify.register(function (app, _, done) {
app.get('/users', () => {})
app.route(route)
done()
}, { prefix: '/v1' }) // global route prefix
await fastify.listen({ port: 3000 })
路由前缀和 fastify-plugin
请注意,如果您使用fastify-plugin
包装您的路由,则此选项将不起作用。您仍然可以通过将插件包装在插件中来使其工作,例如:
const fp = require('fastify-plugin')
const routes = require('./lib/routes')
module.exports = fp(async function (app, opts) {
app.register(routes, {
prefix: '/v1',
})
}, {
name: 'my-routes'
})
处理带前缀插件内的 / 路由
/
路由的行为取决于前缀是否以/
结尾。例如,如果我们考虑前缀/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({ port: 3000 })
或者您可以直接将其传递给路由
fastify.get('/', { logLevel: 'warn' }, (request, reply) => {
reply.send({ hello: 'world' })
})
请记住,自定义日志级别仅应用于路由,而不应用于全局 Fastify Logger(可以通过fastify.log
访问)。
自定义日志序列化器
在某些情况下,您可能需要记录一个大型对象,但对于某些路由来说,这可能是资源浪费。在这种情况下,您可以定义自定义serializers
并在正确的上下文中附加它们!
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({ port: 3000 })
您可以按上下文继承序列化器。
const fastify = Fastify({
logger: {
level: 'info',
serializers: {
user (req) {
return {
method: req.method,
url: req.url,
headers: req.headers,
host: req.host,
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' })
// shows: { user: 'My serializer father - call father serializer', key: 'another key' }
reply.send({})
})
}
fastify.listen({ port: 3000 })
配置
注册新的处理程序时,您可以向其传递一个配置对象并在处理程序中检索它。
// server.js
const fastify = require('fastify')()
function handler (req, reply) {
reply.send(reply.routeOptions.config.output)
}
fastify.get('/en', { config: { output: 'hello world!' } }, handler)
fastify.get('/it', { config: { output: 'ciao mondo!' } }, handler)
fastify.listen({ port: 3000 })
约束
Fastify 支持根据请求的某些属性(如Host
标头或通过find-my-way
约束的任何其他值)来约束路由以仅匹配某些请求。约束在路由选项的constraints
属性中指定。Fastify 有两个内置约束可供使用:version
约束和host
约束,您可以添加自己的自定义约束策略来检查请求的其他部分,以决定是否应为请求执行路由。
版本约束
您可以在路由的constraints
选项中提供version
键。版本化路由允许您为相同的 HTTP 路由路径声明多个处理程序,然后将根据每个请求的Accept-Version
标头进行匹配。Accept-Version
标头值应遵循semver规范,并且应使用精确的 semver 版本声明路由以进行匹配。
如果路由设置了版本,则 Fastify 将要求设置请求Accept-Version
标头,并且对于相同路径,将优先选择版本化路由而不是非版本化路由。目前不支持高级版本范围和预发布版本。
请注意,使用此功能会导致路由器整体性能下降。
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' // it could also be '1.2.0' or '1.2.x'
}
}, (err, res) => {
// { hello: 'world' }
})
⚠ 安全提示
请记住在您的响应中使用您用于定义版本控制的值(例如:
'Accept-Version'
)设置Vary
标头,以防止缓存中毒攻击。您也可以将其配置为代理/CDN 的一部分。const append = require('vary').append
fastify.addHook('onSend', (req, reply, payload, done) => {
if (req.headers['accept-version']) { // or the custom header you are using
let value = reply.getHeader('Vary') || ''
const header = Array.isArray(value) ? value.join(', ') : String(value)
if ((value = append(header, 'Accept-Version'))) { // or the custom header you are using
reply.header('Vary', value)
}
}
done()
})
如果您声明了具有相同主版本或次版本的多版本,Fastify 将始终选择与Accept-Version
标头值最兼容的版本。
如果请求没有Accept-Version
标头,则将返回 404 错误。
可以定义自定义版本匹配逻辑。这可以通过创建 Fastify 服务器实例时的constraints
配置来完成。
主机约束
您可以在constraints
路由选项中提供host
键,以将该路由限制为仅匹配请求Host
标头的某些特定值。host
约束值可以指定为字符串以进行精确匹配,或指定为正则表达式以进行任意主机匹配。
fastify.route({
method: 'GET',
url: '/',
constraints: { host: 'auth.fastify.dev' },
handler: function (request, reply) {
reply.send('hello world from auth.fastify.dev')
}
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'Host': 'example.com'
}
}, (err, res) => {
// 404 because the host doesn't match the constraint
})
fastify.inject({
method: 'GET',
url: '/',
headers: {
'Host': 'auth.fastify.dev'
}
}, (err, res) => {
// => 'hello world from auth.fastify.dev'
})
正则表达式host
约束也可以指定,允许约束到匹配通配符子域(或任何其他模式)的主机。
fastify.route({
method: 'GET',
url: '/',
constraints: { host: /.*\.fastify\.dev/ }, // will match any subdomain of fastify.dev
handler: function (request, reply) {
reply.send('hello world from ' + request.headers.host)
}
})
异步自定义约束
可以提供自定义约束,并且可以从其他来源(如数据库
)获取constraint
条件。异步自定义约束的使用应作为最后的手段,因为它会影响路由器性能。
function databaseOperation(field, done) {
done(null, field)
}
const secret = {
// strategy name for referencing in the route handler `constraints` options
name: 'secret',
// storage factory for storing routes in the find-my-way route tree
storage: function () {
let handlers = {}
return {
get: (type) => { return handlers[type] || null },
set: (type, store) => { handlers[type] = store }
}
},
// function to get the value of the constraint from each incoming request
deriveConstraint: (req, ctx, done) => {
databaseOperation(req.headers['secret'], done)
},
// optional flag marking if handlers without constraints can match requests that have a value for this constraint
mustMatchWhenDerived: true
}
⚠ 安全提示
在使用异步约束时。强烈建议不要在回调内部返回错误。如果错误不可避免,建议提供自定义
frameworkErrors
处理程序来处理它。否则,您的路由选择可能会中断或向攻击者公开敏感信息。const Fastify = require('fastify')
const fastify = Fastify({
frameworkErrors: function (err, res, res) {
if (err instanceof Fastify.errorCodes.FST_ERR_ASYNC_CONSTRAINT) {
res.code(400)
return res.send("Invalid header provided")
} else {
res.send(err)
}
}
})