钩子
钩子
钩子使用 fastify.addHook
方法注册,允许您监听应用程序或请求/响应生命周期中的特定事件。您必须在事件触发之前注册钩子,否则事件将丢失。
通过使用钩子,您可以直接与 Fastify 的生命周期交互。有请求/回复钩子和应用程序钩子。
注意:当使用 async
/await
或返回 Promise
时,done
回调不可用。如果您在这种情况下调用 done
回调,可能会发生意外行为,例如处理程序的重复调用。
请求/回复钩子
done
是继续执行 生命周期 的函数。
通过查看 生命周期页面,可以很容易地理解每个钩子在何处执行。
钩子受 Fastify 的封装影响,因此可以应用于选定的路由。有关更多信息,请参阅 作用域 部分。
您可以使用八个不同的请求/回复钩子(按执行顺序)
onRequest
fastify.addHook('onRequest', (request, reply, done) => {
// Some code
done()
})
或 async/await
fastify.addHook('onRequest', async (request, reply) => {
// Some code
await asyncMethod()
})
注意:在 onRequest 钩子中,request.body
将始终为 undefined
,因为主体解析发生在 preValidation 钩子之前。
preParsing
如果您使用 preParsing
钩子,则可以在解析请求有效负载流之前对其进行转换。它接收请求和回复对象,就像其他钩子一样,以及包含当前请求有效负载的流。
如果它返回值(通过 return
或通过回调函数),则必须返回一个流。
例如,您可以解压缩请求主体。
fastify.addHook('preParsing', (request, reply, payload, done) => {
// Some code
done(null, newPayload)
})
或 async/await
fastify.addHook('preParsing', async (request, reply, payload) => {
// Some code
await asyncMethod()
return newPayload
})
注意:在 preParsing 钩子中,request.body
将始终为 undefined
,因为主体解析发生在 preValidation 钩子之前。
注意:您还应该向返回的流添加一个 receivedEncodedLength
属性。此属性用于将请求有效负载与 Content-Length
标头值正确匹配。理想情况下,应在每个接收到的块上更新此属性。
注意:返回流的大小将被检查,以确保不超过 bodyLimit
选项中设置的限制。
preValidation
如果您使用 preValidation
钩子,则可以在验证之前更改有效负载。例如
fastify.addHook('preValidation', (request, reply, done) => {
request.body = { ...request.body, importantKey: 'randomString' }
done()
})
或 async/await
fastify.addHook('preValidation', async (request, reply) => {
const importantKey = await generateRandomString()
request.body = { ...request.body, importantKey }
})
preHandler
preHandler
钩子允许您指定一个在路由的处理程序之前执行的函数。
fastify.addHook('preHandler', (request, reply, done) => {
// some code
done()
})
或 async/await
fastify.addHook('preHandler', async (request, reply) => {
// Some code
await asyncMethod()
})
preSerialization
如果您使用 preSerialization
钩子,则可以在序列化之前更改(或替换)有效负载。例如
fastify.addHook('preSerialization', (request, reply, payload, done) => {
const err = null
const newPayload = { wrapped: payload }
done(err, newPayload)
})
或 async/await
fastify.addHook('preSerialization', async (request, reply, payload) => {
return { wrapped: payload }
})
注意:如果有效负载是 string
、Buffer
、stream
或 null
,则不会调用该钩子。
onError
fastify.addHook('onError', (request, reply, error, done) => {
// Some code
done()
})
或 async/await
fastify.addHook('onError', async (request, reply, error) => {
// Useful for custom error logging
// You should not use this hook to update the error
})
如果您需要执行一些自定义错误日志记录或在发生错误时添加一些特定标头,此钩子很有用。
它并非旨在更改错误,并且调用 reply.send
将引发异常。
此钩子仅在 由 setErrorHandler
设置的自定义错误处理程序 执行后以及仅当自定义错误处理程序将错误发送回用户时才执行(请注意,默认错误处理程序始终将错误发送回用户)。
注意:与其他钩子不同,不支持将错误传递给 done
函数。
onSend
如果您使用 onSend
钩子,则可以更改有效负载。例如
fastify.addHook('onSend', (request, reply, payload, done) => {
const err = null;
const newPayload = payload.replace('some-text', 'some-new-text')
done(err, newPayload)
})
或 async/await
fastify.addHook('onSend', async (request, reply, payload) => {
const newPayload = payload.replace('some-text', 'some-new-text')
return newPayload
})
您还可以通过用 null
替换有效负载来清除有效负载以发送具有空主体的响应。
fastify.addHook('onSend', (request, reply, payload, done) => {
reply.code(304)
const newPayload = null
done(null, newPayload)
})
您还可以通过用空字符串
''
替换有效负载来发送空主体,但请注意,这将导致Content-Length
标头设置为0
,而如果有效负载为null
,则不会设置Content-Length
标头。
注意:如果您更改有效负载,则只能将其更改为 string
、Buffer
、stream
、ReadableStream
、Response
或 null
。
onResponse
fastify.addHook('onResponse', (request, reply, done) => {
// Some code
done()
})
或 async/await
fastify.addHook('onResponse', async (request, reply) => {
// Some code
await asyncMethod()
})
onResponse
钩子在发送响应时执行,因此您将无法向客户端发送更多数据。但是,它对于向外部服务发送数据(例如,收集统计信息)很有用。
注意:将 disableRequestLogging
设置为 true
将禁用 onResponse
钩子中的任何错误日志。在这种情况下,使用 try - catch
记录错误。
onTimeout
fastify.addHook('onTimeout', (request, reply, done) => {
// Some code
done()
})
或 async/await
fastify.addHook('onTimeout', async (request, reply) => {
// Some code
await asyncMethod()
})
onTimeout
在您需要监控服务中请求超时(如果在 Fastify 实例上设置了 connectionTimeout
属性)时很有用。当请求超时且 HTTP 套接字已挂起时,会执行 onTimeout
钩子。因此,您将无法向客户端发送数据。
onRequestAbort
fastify.addHook('onRequestAbort', (request, done) => {
// Some code
done()
})
或 async/await
fastify.addHook('onRequestAbort', async (request) => {
// Some code
await asyncMethod()
})
当客户端在整个请求处理完成之前关闭连接时,会执行 onRequestAbort
钩子。因此,您将无法向客户端发送数据。
注意:客户端中止检测并非完全可靠。请参阅:Detecting-When-Clients-Abort.md
管理钩子中的错误
如果您在执行钩子期间遇到错误,只需将其传递给 done()
,Fastify 将自动关闭请求并将相应的错误代码发送给用户。
fastify.addHook('onRequest', (request, reply, done) => {
done(new Error('Some error'))
})
如果您想向用户传递自定义错误代码,只需使用 reply.code()
。
fastify.addHook('preHandler', (request, reply, done) => {
reply.code(400)
done(new Error('Some error'))
})
错误将由 Reply
处理。
或者,如果您使用的是 async/await
,您只需抛出错误即可。
fastify.addHook('onRequest', async (request, reply) => {
throw new Error('Some error')
})
从钩子响应请求
如果需要,您可以在到达路由处理程序之前响应请求,例如在实现身份验证钩子时。从钩子回复意味着钩子链已停止,并且不会执行其余的钩子和处理程序。如果钩子使用回调方法,即它不是 async
函数或它返回 Promise
,则只需调用 reply.send()
并避免调用回调即可。如果钩子是 async
,则必须在函数返回或 promise 解析之前调用 reply.send()
,否则请求将继续进行。当在 promise 链之外调用 reply.send()
时,重要的是 return reply
,否则请求将执行两次。
重要的是不要混合回调和 async
/Promise
,否则钩子链将执行两次。
如果您使用的是 onRequest
或 preHandler
,请使用 reply.send
。
fastify.addHook('onRequest', (request, reply, done) => {
reply.send('Early response')
})
// Works with async functions too
fastify.addHook('preHandler', async (request, reply) => {
setTimeout(() => {
reply.send({ hello: 'from prehandler' })
})
return reply // mandatory, so the request is not executed further
// Commenting the line above will allow the hooks to continue and fail with FST_ERR_REP_ALREADY_SENT
})
如果您想使用流进行响应,则应避免对钩子使用 async
函数。如果您必须使用 async
函数,则您的代码需要遵循 test/hooks-async.js 中的模式。
fastify.addHook('onRequest', (request, reply, done) => {
const stream = fs.createReadStream('some-file', 'utf8')
reply.send(stream)
})
如果您在没有 await
的情况下发送响应,请确保始终 return reply
fastify.addHook('preHandler', async (request, reply) => {
setImmediate(() => { reply.send('hello') })
// This is needed to signal the handler to wait for a response
// to be sent outside of the promise chain
return reply
})
fastify.addHook('preHandler', async (request, reply) => {
// the @fastify/static plugin will send a file asynchronously,
// so we should return reply
reply.sendFile('myfile')
return reply
})
应用程序钩子
您也可以钩入应用程序生命周期。
onReady
在服务器开始监听请求之前以及调用 .ready()
时触发。它无法更改路由或添加新的钩子。已注册的钩子函数按顺序执行。只有在所有 onReady
钩子函数都完成后,服务器才会开始监听请求。钩子函数接受一个参数:一个回调 done
,在钩子函数完成后调用。钩子函数以与关联的 Fastify 实例绑定的 this
调用。
// callback style
fastify.addHook('onReady', function (done) {
// Some code
const err = null;
done(err)
})
// or async/await style
fastify.addHook('onReady', async function () {
// Some async code
await loadCacheFromDatabase()
})
onListen
当服务器开始监听请求时触发。钩子一个接一个地运行。如果钩子函数导致错误,则会记录并忽略该错误,允许钩子队列继续。钩子函数接受一个参数:一个回调 done
,在钩子函数完成后调用。钩子函数以与关联的 Fastify 实例绑定的 this
调用。
这是 fastify.server.on('listening', () => {})
的替代方案。
// callback style
fastify.addHook('onListen', function (done) {
// Some code
const err = null;
done(err)
})
// or async/await style
fastify.addHook('onListen', async function () {
// Some async code
})
注意
当使用fastify.inject()
或fastify.ready()
启动服务器时,此钩子将不会运行。
onClose
当调用 fastify.close()
停止服务器时触发,在所有进行中的 HTTP 请求都完成后。当 插件 需要“关闭”事件时(例如,关闭与数据库的打开连接)它很有用。
钩子函数将 Fastify 实例作为第一个参数,以及用于同步钩子函数的 done
回调。
// callback style
fastify.addHook('onClose', (instance, done) => {
// Some code
done()
})
// or async/await style
fastify.addHook('onClose', async (instance) => {
// Some async code
await closeDatabaseConnections()
})
preClose
当调用 fastify.close()
停止服务器时触发,此时所有正在进行的 HTTP 请求尚未完成。当 插件 设置了一些附加到 HTTP 服务器的状态,这些状态会阻止服务器关闭时,此钩子很有用。您不太可能需要使用此钩子,对于最常见的情况,请使用 onClose
。
// callback style
fastify.addHook('preClose', (done) => {
// Some code
done()
})
// or async/await style
fastify.addHook('preClose', async () => {
// Some async code
await removeSomeServerState()
})
onRoute
注册新路由时触发。监听器将 routeOptions
对象作为唯一参数传递。接口是同步的,因此监听器不会传递回调函数。此钩子是封装的。
fastify.addHook('onRoute', (routeOptions) => {
//Some code
routeOptions.method
routeOptions.schema
routeOptions.url // the complete URL of the route, it will include the prefix if any
routeOptions.path // `url` alias
routeOptions.routePath // the URL of the route without the prefix
routeOptions.bodyLimit
routeOptions.logLevel
routeOptions.logSerializers
routeOptions.prefix
})
如果您正在编写插件并且需要自定义应用程序路由,例如修改选项或添加新的路由钩子,那么这是正确的位置。
fastify.addHook('onRoute', (routeOptions) => {
function onPreSerialization(request, reply, payload, done) {
// Your code
done(null, payload)
}
// preSerialization can be an array or undefined
routeOptions.preSerialization = [...(routeOptions.preSerialization || []), onPreSerialization]
})
要在 onRoute 钩子中添加更多路由,必须正确标记路由。如果没有标记,钩子将陷入无限循环。推荐的方法如下所示。
const kRouteAlreadyProcessed = Symbol('route-already-processed')
fastify.addHook('onRoute', function (routeOptions) {
const { url, method } = routeOptions
const isAlreadyProcessed = (routeOptions.custom && routeOptions.custom[kRouteAlreadyProcessed]) || false
if (!isAlreadyProcessed) {
this.route({
url,
method,
custom: {
[kRouteAlreadyProcessed]: true
},
handler: () => {}
})
}
})
有关更多详细信息,请参阅此 问题。
onRegister
注册新插件并创建新的封装上下文时触发。此钩子将在注册代码之前执行。
如果您正在开发一个需要知道何时形成插件上下文并希望在该特定上下文中操作的插件,此钩子会很有用,因此此钩子是封装的。
注意:如果插件包装在 fastify-plugin
中,则不会调用此钩子。
fastify.decorate('data', [])
fastify.register(async (instance, opts) => {
instance.data.push('hello')
console.log(instance.data) // ['hello']
instance.register(async (instance, opts) => {
instance.data.push('world')
console.log(instance.data) // ['hello', 'world']
}, { prefix: '/hola' })
}, { prefix: '/ciao' })
fastify.register(async (instance, opts) => {
console.log(instance.data) // []
}, { prefix: '/hello' })
fastify.addHook('onRegister', (instance, opts) => {
// Create a new array from the old one
// but without keeping the reference
// allowing the user to have encapsulated
// instances of the `data` property
instance.data = instance.data.slice()
// the options of the new registered instance
console.log(opts.prefix)
})
作用域
除了 onClose 之外,所有钩子都是封装的。这意味着您可以使用 register
决定钩子应在何处运行,如 插件指南 中所述。如果您传递一个函数,则该函数将绑定到正确的 Fastify 上下文,并且从那里您可以完全访问 Fastify API。
fastify.addHook('onRequest', function (request, reply, done) {
const self = this // Fastify context
done()
})
请注意,每个钩子中的 Fastify 上下文与注册路由的插件相同,例如
fastify.addHook('onRequest', async function (req, reply) {
if (req.raw.url === '/nested') {
assert.strictEqual(this.foo, 'bar')
} else {
assert.strictEqual(this.foo, undefined)
}
})
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.get('/nested', async function (req, reply) {
assert.strictEqual(this.foo, 'bar')
return { hello: 'world' }
})
})
警告:如果您使用 箭头函数 声明函数,则 this
将不是 Fastify,而是当前作用域的 this
。
路由级钩子
您可以声明一个或多个自定义生命周期钩子(onRequest、onResponse、preParsing、preValidation、preHandler、preSerialization、onSend、onTimeout 和 onError)钩子,这些钩子对于路由来说是唯一的。如果您这样做,这些钩子将始终作为其类别中的最后一个钩子执行。
如果您需要实现身份验证,并且 preParsing 或 preValidation 钩子正是您需要的,这将非常有用。还可以将多个路由级钩子指定为数组。
fastify.addHook('onRequest', (request, reply, done) => {
// Your code
done()
})
fastify.addHook('onResponse', (request, reply, done) => {
// your code
done()
})
fastify.addHook('preParsing', (request, reply, done) => {
// Your code
done()
})
fastify.addHook('preValidation', (request, reply, done) => {
// Your code
done()
})
fastify.addHook('preHandler', (request, reply, done) => {
// Your code
done()
})
fastify.addHook('preSerialization', (request, reply, payload, done) => {
// Your code
done(null, payload)
})
fastify.addHook('onSend', (request, reply, payload, done) => {
// Your code
done(null, payload)
})
fastify.addHook('onTimeout', (request, reply, done) => {
// Your code
done()
})
fastify.addHook('onError', (request, reply, error, done) => {
// Your code
done()
})
fastify.route({
method: 'GET',
url: '/',
schema: { ... },
onRequest: function (request, reply, done) {
// This hook will always be executed after the shared `onRequest` hooks
done()
},
// // Example with an async hook. All hooks support this syntax
//
// onRequest: async function (request, reply) {
// // This hook will always be executed after the shared `onRequest` hooks
// await ...
// }
onResponse: function (request, reply, done) {
// this hook will always be executed after the shared `onResponse` hooks
done()
},
preParsing: function (request, reply, done) {
// This hook will always be executed after the shared `preParsing` hooks
done()
},
preValidation: function (request, reply, done) {
// This hook will always be executed after the shared `preValidation` hooks
done()
},
preHandler: function (request, reply, done) {
// This hook will always be executed after the shared `preHandler` hooks
done()
},
// // Example with an array. All hooks support this syntax.
//
// preHandler: [function (request, reply, done) {
// // This hook will always be executed after the shared `preHandler` hooks
// done()
// }],
preSerialization: (request, reply, payload, done) => {
// This hook will always be executed after the shared `preSerialization` hooks
done(null, payload)
},
onSend: (request, reply, payload, done) => {
// This hook will always be executed after the shared `onSend` hooks
done(null, payload)
},
onTimeout: (request, reply, done) => {
// This hook will always be executed after the shared `onTimeout` hooks
done()
},
onError: (request, reply, error, done) => {
// This hook will always be executed after the shared `onError` hooks
done()
},
handler: function (request, reply) {
reply.send({ hello: 'world' })
}
})
注意:这两个选项也接受函数数组。
使用钩子注入自定义属性
您可以使用钩子将自定义属性注入传入的请求中。这对于在控制器中重用从钩子处理的数据很有用。
一个非常常见的用例是,例如,根据用户的令牌检查用户身份验证,然后将其恢复的数据存储到 请求 实例中。这样,您的控制器就可以使用 request.authenticatedUser
或您想命名的任何名称轻松读取它。它可能看起来像这样
fastify.addHook('preParsing', async (request) => {
request.authenticatedUser = {
id: 42,
name: 'Jane Doe',
role: 'admin'
}
})
fastify.get('/me/is-admin', async function (req, reply) {
return { isAdmin: req.authenticatedUser?.role === 'admin' || false }
})
请注意,.authenticatedUser
实际上可以是您自己选择的任何属性名称。使用您自己的自定义属性可以防止您修改现有属性,这将是一个危险且具有破坏性的操作。因此,请小心并确保您的属性是全新的,并且仅在像此示例这样的非常具体的小用例中使用此方法。
关于此示例中的 TypeScript,您需要更新 FastifyRequest
核心接口以包含您的新属性类型(有关更多信息,请参阅 TypeScript 页面),例如
interface AuthenticatedUser { /* ... */ }
declare module 'fastify' {
export interface FastifyRequest {
authenticatedUser?: AuthenticatedUser;
}
}
虽然这是一种非常实用的方法,但如果您尝试执行更改这些核心对象(例如修改请求对象)的更复杂的操作,则应考虑创建一个自定义 插件。
诊断通道钩子
一个 diagnostics_channel
发布事件 'fastify.initialization'
会在初始化时发生。Fastify 实例作为传递的对象的属性传递到钩子中。此时,可以与实例进行交互以添加钩子、插件、路由或任何其他修改。
例如,跟踪包可能会执行以下操作(当然,这是一个简化)。这将位于跟踪包初始化中加载的文件中,以典型的“首先加载仪器工具”的方式。
const tracer = /* retrieved from elsewhere in the package */
const dc = require('node:diagnostics_channel')
const channel = dc.channel('fastify.initialization')
const spans = new WeakMap()
channel.subscribe(function ({ fastify }) {
fastify.addHook('onRequest', (request, reply, done) => {
const span = tracer.startSpan('fastify.request.handler')
spans.set(request, span)
done()
})
fastify.addHook('onResponse', (request, reply, done) => {
const span = spans.get(request)
span.finish()
done()
})
})
注意:TracingChannel 类 API 目前处于实验阶段,即使在 Node.js 的语义版本修补程序版本中也可能会发生重大更改。
其他五个事件在每个请求的基础上发布,遵循 跟踪通道 命名法。通道名称列表及其接收的事件为
tracing:fastify.request.handler:start
:始终触发{ request: Request, reply: Reply, route: { url, method } }
tracing:fastify.request.handler:end
:始终触发{ request: Request, reply: Reply, route: { url, method }, async: Bool }
tracing:fastify.request.handler:asyncStart
:对于 promise/async 处理程序触发{ request: Request, reply: Reply, route: { url, method } }
tracing:fastify.request.handler:asyncEnd
:对于 promise/async 处理程序触发{ request: Request, reply: Reply, route: { url, method } }
tracing:fastify.request.handler:error
:发生错误时触发{ request: Request, reply: Reply, route: { url, method }, error: Error }
对于与给定请求关联的所有事件,对象实例保持不变。所有有效负载都包含一个 request
和 reply
属性,它们是 Fastify 的 Request
和 Reply
实例。它们还包含一个 route
属性,该属性是一个包含匹配的 url
模式(例如 /collection/:id
)和 method
HTTP 方法(例如 GET
)的对象。:start
和 :end
事件始终为请求触发。如果请求处理程序是 async
函数或返回 Promise
的函数,则 :asyncStart
和 :asyncEnd
事件也会触发。最后,:error
事件包含与请求失败关联的 error
属性。
这些事件可以像这样接收
const dc = require('node:diagnostics_channel')
const channel = dc.channel('tracing:fastify.request.handler:start')
channel.subscribe((msg) => {
console.log(msg.request, msg.reply)
})