跳至主要内容
版本: 最新版本 (v5.0.x)

路由

路由

路由方法将配置应用程序的端点。您可以使用两种方法在 Fastify 中声明路由:简写方法和完整声明。

完整声明

fastify.route(options)

路由选项

  • method: 目前支持 GETHEADTRACEDELETEOPTIONSPATCHPUTPOST。要接受更多方法,必须使用 addHttpMethod。它也可以是方法数组。

  • url: 与此路由匹配的 URL 路径(别名:path)。

  • schema: 包含请求和响应模式的对象。它们需要采用 JSON Schema 格式,请查看 此处 获取更多信息。

    • body: 如果是 POST、PUT、PATCH、TRACE、SEARCH、PROPFIND、PROPPATCH 或 LOCK 方法,则验证请求的主体。
    • querystringquery: 验证查询字符串。这可以是一个完整的 JSON Schema 对象,其 type 属性为 objectproperties 对象为参数,或者只是如下所示的 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: 用于存储自定义配置的对象。

  • version: 定义端点版本的与 semver 兼容的字符串。示例

  • constraints: 基于请求属性或值定义路由限制,使用 find-my-way 约束启用自定义匹配。包括内置的 versionhost 约束,以及对自定义约束策略的支持。

  • prefixTrailingSlash: 用于确定如何处理将 / 作为具有前缀的路由传递的字符串。

    • both (默认): 将注册 /prefix/prefix/
    • slash: 将仅注册 /prefix/
    • no-slash: 将仅注册 /prefix

    注意:此选项不会覆盖 服务器 配置中的 ignoreTrailingSlash

  • request请求 中定义。

  • reply响应 中定义。

注意:onRequestpreParsingpreValidationpreHandlerpreSerializationonSendonResponse 的文档在 钩子 中进行了更详细的描述。此外,要先于 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 replyawait 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 valuereply.send(value)时,先发生的将优先,第二个值将被丢弃,并且还会发出警告日志,因为您尝试发送两次响应。
  • 在 promise 外部调用reply.send()是可能的,但需要特别注意。有关更多详细信息,请阅读promise-resolution
  • 您不能返回undefined。有关更多详细信息,请阅读promise-resolution

Promise 解析

如果您的处理程序是async函数或返回一个 promise,则应注意支持回调和 promise 控制流所需的特殊行为。当处理程序的 promise 被解析时,回复将自动发送其值,除非您在处理程序中显式地等待或返回reply

  1. 如果您想使用async/await或 promise,但使用reply.send响应一个值
    • 请执行return reply / await reply
    • 请勿忘记调用reply.send
  2. 如果您想使用async/await或 promise
    • 请勿使用reply.send
    • 请执行返回您要发送的值。

通过这种方式,我们可以以最小的折衷支持callback-styleasync-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,则setNotFoundHandlersetErrorHandler也将受到影响。

// 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)
}
}
})