钩子 (hooks) 让你能够监听应用或请求/响应生命周期之上的特定事件。使用 fastify.addHook
可以注册钩子。你必须在事件被触发之前注册相应的钩子,否则,事件将得不到处理。
通过钩子方法,你可以与 Fastify 的生命周期直接进行交互。有用于请求/响应的钩子,也有应用级钩子:
注意:使用 async
/await
或返回一个 Promise
时,done
回调不可用。在这种情况下,仍然使用 done
可能会导致难以预料的行为,例如,处理函数的重复调用。
Request 与 Reply 是 Fastify 核心的对象。
done
是调用生命周期下一阶段的函数。
生命周期一文清晰地展示了各个钩子执行的位置。
钩子可被封装,因此可以运用在特定的路由上。更多信息请看作用域一节。
在请求/响应中,有八个可用的钩子 (按执行顺序排序):
fastify.addHook('onRequest', (request, reply, done) => {
// 其他代码
done()
})
或使用 async/await
:
fastify.addHook('onRequest', async (request, reply) => {
// 其他代码
await asyncMethod()
})
注意:在 onRequest 钩子中,request.body
的值总是 null
,这是因为 body 的解析发生在 preValidation 钩子之前。
preParsing
钩子让你能在解析请求之前转换它们。它的参数除了和其他钩子一样的请求与响应对象外,还多了一个当前请求 payload 的 stream。
需要通过 return
或回调函数返回值的话,必须返回一个 stream。
例如,你可以解压请求的 body:
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
的值总是 null
,这是因为 body 的解析发生在 preValidation 钩子之前。
注意:你应当给返回的 stream 添加 receivedEncodedLength
属性。这是为了通过比对请求头的 Content-Length
,来精确匹配请求的 payload。理想情况下,每收到一块数据都应该更新该属性。
注意:早先的写法 function(request, reply, done)
与 function(request, reply)
仍被支持,但不推荐使用。
使用 preValidation
钩子时,你可以在校验前修改 payload。示例如下:
fastify.addHook('preValidation', (request, reply, done) => {
req.body = { ...req.body, importantKey: 'randomString' }
done()
})
或使用 async/await
:
fastify.addHook('preValidation', async (request, reply) => {
const importantKey = await generateRandomString()
req.body = { ...req.body, importantKey }
})
fastify.addHook('preHandler', (request, reply, done) => {
// 其他代码
done()
})
或使用 async/await
:
fastify.addHook('preHandler', async (request, reply) => {
// 其他代码
await asyncMethod()
})
preSerialization
钩子让你可以在 payload 被序列化之前改动 (或替换) 它。举个例子:
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 }
})
注:payload 为 string
、Buffer
、stream
或 null
时,该钩子不会被调用。
fastify.addHook('onError', (request, reply, error, done) => {
// 其他代码
done()
})
或使用 async/await
:
fastify.addHook('onError', async (request, reply, error) => {
// 当自定义错误日志时有用处
// 你不应该使用这个钩子去更新错误
})
onError
钩子可用于自定义错误日志,或当发生错误时添加特定的 header。
该钩子并不是为了变更错误而设计的,且调用 reply.send
会抛出一个异常。
它只会在 customErrorHandler
向用户发送错误之后被执行 (要注意的是,默认的 customErrorHandler
总是会发送错误)。
注意:与其他钩子不同,onError
不支持向 done
函数传递错误。
使用 onSend
钩子可以改变 payload。例如:
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
})
你也可以通过将 payload 置为 null
,发送一个空消息主体的响应:
fastify.addHook('onSend', (request, reply, payload, done) => {
reply.code(304)
const newPayload = null
done(null, newPayload)
})
将 payload 设为空字符串
''
也可以发送空的消息主体。但要小心的是,这么做会造成Content-Length
header 的值为0
。而 payload 为null
则不会设置Content-Length
header。
注:你只能将 payload 修改为 string
、Buffer
、stream
或 null
。
fastify.addHook('onResponse', (request, reply, done) => {
// 其他代码
done()
})
或使用 async/await
:
fastify.addHook('onResponse', async (request, reply) => {
// 其他代码
await asyncMethod()
})
onResponse
钩子在响应发出后被执行,因此在该钩子中你无法再向客户端发送数据了。但是你可以在此向外部服务发送数据,比如收集数据。
fastify.addHook('onTimeout', (request, reply, done) => {
// 其他代码
done()
})
Or async/await
:
fastify.addHook('onTimeout', async (request, reply) => {
// 其他代码
await asyncMethod()
})
onTimeout
用于监测请求超时,需要在 Fastify 实例上设置 connectionTimeout
属性。当请求超时,socket 挂起 (hang up) 时,该钩子执行。因此,在这个钩子里不能再向客户端发送数据了。
在钩子的执行过程中如果发生了错误,只需将错误传递给 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('onResponse', async (request, reply) => {
throw new Error('Some error')
})
需要的话,你可以在路由函数执行前响应一个请求,例如进行身份验证。在钩子中响应暗示着钩子的调用链被 终止,剩余的钩子将不会执行。假如钩子使用回调的方式,意即不是 async
函数,也没有返回 Promise
,那么只需要调用 reply.send()
,并且避免触发回调便可。假如钩子是 async
函数,那么 reply.send()
必须 发生在函数返回或 promise resolve 之前,否则请求将会继续下去。当 reply.send()
在 promise 调用链之外被调用时,需要 return reply
,不然请求将被执行两次。
不应当混用回调与 async
/Promise
,否则钩子的调用链会被执行两次。
如果你在 onRequest
或 preHandler
中发出响应,请使用 reply.send
。
fastify.addHook('onRequest', (request, reply, done) => {
reply.send('Early response')
})
// 也可使用 async 函数
fastify.addHook('preHandler', async (request, reply) => {
await something()
reply.send({ hello: 'world' })
return reply // 在这里是可选的,但这是好的实践
})
如果你想要使用流 (stream) 来响应请求,你应该避免使用 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') })
// 让处理函数等待 promise 链之外发出的响应
return reply
})
fastify.addHook('preHandler', async (request, reply) => {
// fastify-static 插件异步地发送文件,因此需要 return reply
reply.sendFile('myfile')
return reply
})
你也可以在应用的生命周期里使用钩子方法。要格外注意的是,这些钩子并未被完全封装。钩子中的 this
得到了封装,但处理函数可以响应封装界线外的事件。
在服务器开始监听请求之前,或调用 .ready()
方法时触发。在此你无法更改路由,或添加新的钩子。注册的 onReady
钩子函数串行执行,只有全部执行完毕时,服务器才会开始监听请求。钩子接受一个回调函数作为参数:done
,在钩子函数完成后调用。钩子的 this
为 Fastify 实例。
// 回调写法
fastify.addHook('onReady', function (done) {
// 其他代码
const err = null;
done(err)
})
// 或 async/await
fastify.addHook('onReady', async function () {
// 异步代码
await loadCacheFromDatabase()
})
使用 fastify.close()
停止服务器时被触发。当插件需要一个 "shutdown" 事件时有用,例如关闭一个数据库连接。
该钩子的第一个参数是 Fastify 实例,第二个为 done
回调函数。
fastify.addHook('onClose', (instance, done) => {
// 其他代码
done()
})
当注册一个新的路由时被触发。它的监听函数拥有一个唯一的参数:routeOptions
对象。该函数是同步的,其本身并不接受回调作为参数。
fastify.addHook('onRoute', (routeOptions) => {
// 其他代码
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) {
// 其他代码
done(null, payload)
}
// preSerialization 可以是数组或 undefined
routeOptions.preSerialization = [...(routeOptions.preSerialization || []), onPreSerialization]
})
当注册一个新的插件,或创建了新的封装好的上下文后被触发。该钩子在注册的代码之前被执行。
当你的插件需要知晓上下文何时创建完毕,并操作它们时,可以使用这一钩子。
注意:被 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)
})
除了应用钩子,所有的钩子都是封装好的。这意味着你可以通过 register
来决定在何处运行它们,正如插件指南所述。如果你传递一个函数,那么该函数会获得 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.route({
method: 'GET',
url: '/',
schema: { ... },
onRequest: function (request, reply, done) {
// 该钩子总是在共享的 `onRequest` 钩子后被执行
done()
},
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' })
}
})
注:两个选项都接受一个函数数组作为参数。
注:
诊断通道
(diagnostics_channel) 是当前 Node.js 的试验特性, 因此,其 API 即便在补丁版本中也可能会发生变动。 对于 Fastify 支持的 Node.js 版本,不兼容诊断通道
的, 将会使用 polyfill。 而 polyfill 都不支持的版本将无法使用该特性。
当前版本在初始化时,会有一个诊断通道
发布 'fastify.initialization'
事件。此时,Fastify 的实例将会作为回调函数参数的一个属性,该实例可以添加钩子、插件、路由及其他任意内容。
举例来说,某个监控工具可以如下使用(当然这是一个简化的例子)。在典型的“探测工具优先加载 (require instrumentation tools first)”风格下,这段代码会在被监控的应用初始化时加载。
const tracer = /* 某个监控工具 */
const dc = require('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')
spans.set(request, span)
done()
})
fastify.addHook('onResponse', (request, reply, done) => {
const span = spans.get(request)
span.finish()
done()
})
})