回调钩子
回调钩子通过 fastify.addHook
方法注册,并允许你在应用程序或请求/响应生命周期的特定事件上进行监听。你需要在事件触发之前注册一个钩子,否则该事件将丢失。
使用钩子可以直接与Fastify的生命周期交互。有请求/回复钩子和应用钩子:
ℹ️ 注意:当使用
async
/await
或返回一个Promise
时,done
回调不可用。在这种情况下调用done
回调可能会导致意外行为,例如处理程序的重复调用。
请求/回复钩子
done
是继续执行生命周期的函数。
通过查看lifecycle页面,很容易理解每个钩子在何处被执行。
钩子受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) => {
// 某些代码
done(null, newPayload)
})
或者使用 async/await
:
fastify.addHook('preParsing', async (request, reply, payload) => {
// 某些代码
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 }
})
ℹ️ 注意:如果负载是字符串、Buffer、流或 null,则不会调用该钩子。
onError
fastify.addHook('onError', (request, reply, error, done) => {
// Some code
done()
})
或者使用 async/await
:
fastify.addHook('onError', async (request, reply, 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 套接字已断开连接。因此,您将无法向客户端发送数据。
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()
,否则请求将继续执行。当 reply.send()
被调用时,如果不在 Promise 链中,则需要返回 reply
否则请求将被执行两次。
重要的是不要 混合使用回调和 async
/Promise
,否则钩子链将被执行两次。
如果您正在使用 onRequest
或 preHandler
,请使用 reply.send
。
fastify.addHook('onRequest', (request, reply, done) => {
reply.send('Early response')
})
// 也适用于异步函数
fastify.addHook('preHandler', async (request, reply) => {
setTimeout(() => {
reply.send({ hello: 'from prehandler' })
})
return reply // 必须返回,以防止请求继续执行
// 注释掉上面的行将允许钩子继续并因 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
的情况下发送响应,请确保始终返回 reply
:
fastify.addHook('preHandler', async (request, reply) => {
setImmediate(() => { reply.send('hello') })
// 这里需要信号处理程序等待响应,
// 即使响应是在 promise 链之外发送的
return reply
})
fastify.addHook('preHandler', async (request, reply) => {
// @fastify/static 插件将异步发送文件,
// 因此我们需要返回 reply
reply.sendFile('myfile')
return reply
})
应用程序钩子
您也可以在应用程序生命周期中进行挂钩。
onReady
在服务器开始监听请求之前以及调用 .ready()
时触发。它不能更改路由或添加新的钩子。注册的钩子函数按顺序执行。只有当所有 onReady
钩子函数完成之后,服务器才会开始监听请求。钩子函数接受一个参数:一个回调函数 done
,在钩子函数完成后调用。钩子函数被绑定到关联的 Fastify 实例上。
// 回调风格
fastify.addHook('onReady', function (done) {
// 某些代码
const err = null;
done(err)
})
// 或者使用 async/await 风格
fastify.addHook('onReady', async function () {
// 某些异步代码
await loadCacheFromDatabase()
})
onListen
当服务器开始监听请求时触发。钩子函数按顺序执行,如果一个钩子函数引发错误,则该错误会被记录并忽略,允许其他钩子继续运行。钩子函数接受一个参数:一个回调函数 done
,在钩子函数完成后调用。钩子函数被绑定到关联的 Fastify 实例上。
这是 fastify.server.on('listening', () => {})
的替代方案。
// 回调风格
fastify.addHook('onListen', function (done) {
// 某些代码
const err = null;
done(err)
})
// 或者使用 async/await 风格
fastify.addHook('onListen', async function () {
// 某些异步代码
})
ℹ️ 注意:当服务器通过
fastify.inject()
或fastify.ready()
启动时,此钩子不会运行。
onClose
当调用 fastify.close()
停止服务器,并且所有正在进行的 HTTP 请求已完成时触发。
这对于需要“关闭”事件的 插件 非常有用,例如关闭与数据库的连接。
钩子函数接收 Fastify 实例作为第一个参数,
以及一个用于同步钩子函数的 done
回调。
// 回调风格
fastify.addHook('onClose', (instance, done) => {
// 某些代码
done()
})
// 或者 async/await 风格
fastify.addHook('onClose', async (instance) => {
// 某些异步代码
await closeDatabaseConnections()
})
preClose
当调用 fastify.close()
停止服务器,并且所有正在进行的 HTTP 请求尚未完成时触发。
这对于已经设置了一些与 HTTP 服务器关联的状态的 插件 非常有用,这些状态会阻止服务器关闭。
你不太可能需要使用此钩子,
对于最常见的场景,请使用 onClose
。
// 回调风格
fastify.addHook('preClose', (done) => {
// 某些代码
done()
})
// 或者 async/await 风格
fastify.addHook('preClose', async () => {
// 某些异步代码
await removeSomeServerState()
})
onRoute
当注册新的路由时触发。监听器接收一个包含routeOptions
对象作为唯一参数。接口是同步的,因此不会传递回调函数。此钩子被封装。
fastify.addHook('onRoute', (routeOptions) => {
//Some code
routeOptions.method
routeOptions.schema
routeOptions.url // 路由的完整URL,包括前缀(如果有)
routeOptions.path // `url` 的别名
routeOptions.routePath // 不包含前缀的路由 URL
routeOptions.bodyLimit
routeOptions.logLevel
routeOptions.logSerializers
routeOptions.prefix
})
如果你正在编写一个插件并且需要自定义应用程序路由(例如修改选项或添加新的路由钩子),这是正确的地方。
fastify.addHook('onRoute', (routeOptions) => {
function onPreSerialization(request, reply, payload, done) {
// Your code
done(null, payload)
}
// preSerialization 可以是一个数组或 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) => {
// 从旧数组创建一个新的数组
// 不保留引用,允许用户拥有封装的
// `data` 属性实例
instance.data = instance.data.slice()
// 新注册实例的选项
console.log(opts.prefix)
})
范围
除了onClose,所有钩子都被封装。这意味着你可以通过使用register
(如在插件指南中所述的plugins guide)来决定你的钩子应在何处运行。如果你传递一个函数,该函数将绑定到正确的Fastify上下文,并且从此处你将完全访问Fastify API。
fastify.addHook('onRequest', function (request, reply, done) {
const self = this // Fastify 上下文
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,而是当前作用域中的一个。
路由级别钩子
您可以为路由声明一个或多个自定义生命周期钩子(onRequest、onResponse、preParsing、preValidation、preHandler、preSerialization、onSend、onTimeout 和 onError),这些钩子将唯一适用于该路由。如果您这样做,这些钩子始终作为其类别中的最后一个钩子执行。
这在需要实现身份验证时非常有用,在这种情况下,preParsing 或 preValidation 钩子正是您所需要的。也可以将多个路由级别钩子指定为数组形式。
fastify.addHook('onRequest', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('onResponse', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('preParsing', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('preValidation', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('preHandler', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('preSerialization', (request, reply, payload, done) => {
// 您的代码
done(null, payload)
})
fastify.addHook('onSend', (request, reply, payload, done) => {
// 您的代码
done(null, payload)
})
fastify.addHook('onTimeout', (request, reply, done) => {
// 您的代码
done()
})
fastify.addHook('onError', (request, reply, error, done) => {
// 您的代码
done()
})
fastify.addHook(‘onError’, (request, reply, error, done) => { // 您的代码 done() })
fastify.route({
method: ‘GET’,
url: ’/’,
schema: { … },
onRequest: function (request, reply, done) {
// 此钩子将在共享 onRequest
钩子之后始终执行
done()
},
// 示例使用异步钩子。所有钩子都支持此语法
//
// onRequest: async function (request, reply) {
// // 此钩子将在共享 onRequest
钩子之后始终执行
// await …
// }
onResponse: function (request, reply, done) {
// 此钩子将在共享 onResponse
钩子之后始终执行
done()
},
preParsing: function (request, reply, done) {
// 此钩子将在共享 preParsing
钩子之后始终执行
done()
},
preValidation: function (request, reply, done) {
// 此钩子将在共享 preValidation
钩子之后始终执行
done()
},
preHandler: function (request, reply, done) {
// 此钩子将在共享 preHandler
钩子之后始终执行
done()
},
// 示例使用数组。所有钩子都支持此语法。
//
// preHandler: [function (request, reply, done) {
// // 此钩子将在共享 preHandler
钩子之后始终执行
// done()
// }],
preSerialization: (request, reply, payload, done) => {
// 此钩子将在共享 preSerialization
钩子之后始终执行
done(null, payload)
},
onSend: (request, reply, payload, done) => {
// 此钩子将在共享 onSend
钩子之后始终执行
done(null, payload)
},
onTimeout: (request, reply, done) => {
// 此钩子将在共享 onTimeout
钩子之后始终执行
done()
},
onError: (request, reply, error, done) => {
// 此钩子将在共享 onError
钩子之后始终执行
done()
},
handler: function (request, reply) {
reply.send({ hello: ‘world’ })
}
})
> ℹ️ 注意:这两个选项也接受函数数组。
使用钩子注入自定义属性
您可以使用钩子将自定义属性注入到传入的请求中。 这在控制器中重用从钩子处理的数据时非常有用。
一个常见的应用场景是,例如,根据用户的令牌检查用户身份验证,
然后将其恢复的数据存储到 Request 实例中。这样,您的控制器可以轻松地使用 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 = /* 从包中的其他地方获取 */
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
)和 HTTP 方法(例如 GET
)的对象。:start
和 :end
事件始终为请求触发。如果请求处理程序是异步函数或返回 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)
})