路由
路由方法配置应用程序的端点。可以通过简写方法或完整声明来定义路由。
完整声明
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 对象,具有类型属性为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)
:一个在序列化之前调用的 函数,也可以是函数数组。 -
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
设置的工厂,对于路由请求也是如此。要访问默认工厂,请使用
覆盖默认的日志工厂,并且任何由
setChildLoggerFactory
设置的内容,对于路由的请求。要访问默认工厂,可以使用 instance.childLoggerFactory
访问。请注意,这将指向 Fastify 的默认 childLoggerFactory
,前提是插件尚未覆盖它。
-
validatorCompiler({ schema, method, url, httpPart })
: 用于构建请求验证模式的函数。请参阅 验证和序列化 文档。 -
serializerCompiler({ { schema, method, url, httpStatus, contentType } })
: 用于构建响应序列化的模式的函数。请参阅 验证和序列化 文档。 -
schemaErrorFormatter(errors, dataVar)
: 格式化验证编译器中的错误信息的函数。请参阅 验证和序列化 文档。覆盖全局模式错误格式化处理器,并且任何由setSchemaErrorFormatter
设置的内容,对于路由的请求。 -
bodyLimit
: 防止默认 JSON 请求体解析器解析超过此字节数的请求体。必须是整数。您也可以在创建 Fastify 实例时使用fastify(options)
全局设置此选项,默认值为1048576
(1 MiB)。 -
logLevel
: 设置该路由的日志级别。请参阅下方说明。 -
logSerializers
: 为此路由设置日志序列化器。 -
config
: 存储自定义配置的对象。 -
constraints
: 根据请求属性或值定义路由限制,使用 find-my-way 约束实现自定义匹配。包括 -
constraints
: 定义基于请求属性或值的路由限制,使用 find-my-way 约束实现自定义匹配。包括内置的version
和host
约束,并支持自定义约束策略。 -
prefixTrailingSlash
: 用于确定如何处理以/
作为前缀的路由传递的字符串。both
(默认):将同时注册/prefix
和/prefix/
。slash
:仅注册/prefix/
。no-slash
:仅注册/prefix
。
注意:此选项不会覆盖 Server 配置中的 ignoreTrailingSlash
。
ℹ️ 注意:关于
onRequest
,preParsing
,preValidation
,preHandler
,preSerialization
,onSend
, 和onResponse
的文档在 Hooks 中详细说明。要在请求由handler
处理之前发送响应,请参阅 从钩子响应请求。
示例:
fastify.route({
method: 'GET',
url: '/',
schema: {
querystring: {
type: 'object',
properties: {
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。
要注册一个参数化路径,请在参数名称前使用冒号。对于通配符,请使用星号。静态路由始终会在参数化和通配符路由之前进行检查。
// 参数化
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
})
// 通配符
fastify.get('/example/*', function (request, reply) {})
正则表达式路由是受支持的,但斜杠必须转义。 请注意,正则表达式的性能开销非常大!
// 带正则表达式的参数化路径
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;
// 在这里编写代码
})
请注意,在这种情况下,必须使用破折号(”-“)作为参数分隔符。
最后,可以使用正则表达式定义多个参数:
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;
// 在这里编写代码
})
在这种情况下,可以使用正则表达式中未匹配的任何字符作为参数分隔符。
最后一个参数可以通过在参数名末尾添加问号(”?”)来使其变为可选。
fastify.get('/example/posts/:id?', function (request, reply) {
const { id } = request.params;
// 在这里编写代码
})
在这种情况下,/example/posts
和 /example/posts/1
都是有效的。如果未指定,则可选参数将为 undefined
。
具有多个参数的路由可能会对性能产生负面影响。 建议使用单个参数的方法,特别是在应用程序热点路径上的路由上。有关更多详细信息,请参阅 find-my-way 。
要在路径中包含冒号而不声明参数时,可以使用双冒号。 例如:
fastify.post('/name::verb') // 将被解释为 /name:verb
异步/等待
你是 async/await
用户吗?我们为你提供支持!
fastify.get('/', options, async function (request, reply) {
const data = await getData()
const processed = await processData(data)
return processed
})
``
如上所示,不需要调用 `reply.send` 将数据发送回用户。只需直接返回响应体即可!
如果需要的话,你也可以使用 `reply.send` 发送数据。在这种情况下,请不要忘记在异步处理程序中返回 `return reply` 或者 `await reply` 以避免竞态条件。
```js
fastify.get('/', options, async function (request, reply) {
const data = await getData()
const processed = await processData(data)
return reply.send(processed)
})
``
如果路由封装了一个基于回调的 API,并且该 API 在 promise 链之外调用 `reply.send()`,则可以使用 `await reply`:
```js
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)
时,前者优先级更高,后者会被丢弃,并且会发出一个警告日志。- 在 promise 链之外调用
reply.send()
是可能的,但需要特别注意。请参阅 promise-resolution。- 不可以返回
undefined
。请参阅 promise-resolution。
Promise 解决
如果处理器是一个 async
函数或返回一个承诺,请注意支持回调和承诺控制流的特殊行为。当处理器的承诺解析时,除非您在处理器中显式地等待或返回 reply
,否则回复将自动发送其值。
- 如果使用
async/await
或承诺但用reply.send
响应:- 做:
return reply
/await reply
。 - 不做:不要忘记调用
reply.send
。
- 做:
- 如果使用
async/await
或承诺:- 不做:不要使用
reply.send
。 - 做:返回要发送的值。
- 不做:不要使用
这种方法支持 callback-style
和 async-await
,并且折衷最小。然而,建议在应用程序中仅使用一种风格以保持一致的错误处理方式。
ℹ️ 注意:每个异步函数本身都会返回一个承诺。
路由前缀
有时需要维护同一 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' }) // 全局路由前缀
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
选项传递给插件或具有所需值的路由来实现:
value 。
请注意,在插件级别设置 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 日志器,可以通过 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' })
// 显示:{ 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 会优先选择带版本号的路由而不是不带版本号的路由。目前尚不支持高级版本范围和预发布版本。
请注意使用此功能会导致路由器的整体性能下降。
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'
),以防止缓存中毒攻击。 这也可以在代理或 CDN 中进行配置。
const append = require('vary').append
fastify.addHook('onSend', (req, reply, payload, done) => {
if (req.headers['accept-version']) { // 或使用的自定义头
let value = reply.getHeader('Vary') || ''
const header = Array.isArray(value) ? value.join(', ') : String(value)
if ((value = append(header, 'Accept-Version'))) { // 或使用的自定义头
reply.header('Vary', value)
}
}
done()
})
如果声明了具有相同主版本或次版本的多个版本,Fastify 将始终选择与 Accept-Version
请求头值兼容的最高版本。
如果没有请求头 Accept-Version
,将返回 404 错误。
可以通过在创建 Fastify 服务器实例时配置 constraints
来定义自定义版本匹配逻辑。
主机限制
在路由选项的 constraints
中提供一个 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
})
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/ }, // 匹配 fastify.dev 的任意子域
handler: function (request, reply) {
reply.send('hello world from ' + request.headers.host)
}
})
自定义异步约束
可以提供自定义约束,并从其他来源(如数据库)获取 constraint
条件。将异步自定义约束作为最后的手段,因为它们会影响路由器性能。
function databaseOperation(field, done) {
done(null, field)
}
const secret = {
// 路由处理程序中引用策略名称的名称
name: 'secret',
// 存储工厂用于在 find-my-way 路由树中存储路由
storage: function () {
let handlers = {}
return {
get: (type) => { return handlers[type] || null },
set: (type, store) => { handlers[type] = store }
}
},
// 从每个传入请求获取约束值的函数
deriveConstraint: (req, ctx, done) => {
databaseOperation(req.headers['secret'], done)
},
// 可选标志,标记没有约束的处理器是否可以匹配具有此约束值的请求
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("提供的标头无效") } else { res.send(err) } } })