Fastify

钩子方法

钩子 (hooks) 让你能够监听应用或请求/响应生命周期之上的特定事件。使用 fastify.addHook 可以注册钩子。你必须在事件被触发之前注册相应的钩子,否则,事件将得不到处理。

通过钩子方法,你可以与 Fastify 的生命周期直接进行交互。有用于请求/响应的钩子,也有应用级钩子:

注意:使用 async/await 或返回一个 Promise 时,done 回调不可用。在这种情况下,仍然使用 done 可能会导致难以预料的行为,例如,处理函数的重复调用。

请求/响应钩子

RequestReply 是 Fastify 核心的对象。
done 是调用生命周期下一阶段的函数。

生命周期一文清晰地展示了各个钩子执行的位置。
钩子可被封装,因此可以运用在特定的路由上。更多信息请看作用域一节。

在请求/响应中,有八个可用的钩子 (按执行顺序排序)

onRequest

  1. fastify.addHook('onRequest', (request, reply, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('onRequest', async (request, reply) => {
  2. // 其他代码
  3. await asyncMethod()
  4. return
  5. })

注意:在 onRequest 钩子中,request.body 的值总是 null,这是因为 body 的解析发生在 preValidation 钩子之前。

preParsing

  1. fastify.addHook('preParsing', (request, reply, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('preParsing', async (request, reply) => {
  2. // 其他代码
  3. await asyncMethod()
  4. return
  5. })

注意:在 preParsing 钩子中,request.body 的值总是 null,这是因为 body 的解析发生在 preValidation 钩子之前。

preValidation

  1. fastify.addHook('preValidation', (request, reply, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('preValidation', async (request, reply) => {
  2. // 其他代码
  3. await asyncMethod()
  4. return
  5. })

preHandler

  1. fastify.addHook('preHandler', (request, reply, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('preHandler', async (request, reply) => {
  2. // 其他代码
  3. await asyncMethod()
  4. return
  5. })

preSerialization

preSerialization 钩子让你可以在 payload 被序列化之前改动 (或替换) 它。举个例子:

  1. fastify.addHook('preSerialization', (request, reply, payload, done) => {
  2. const err = null;
  3. const newPayload = { wrapped: payload }
  4. done(err, newPayload)
  5. })

或使用 async/await

  1. fastify.addHook('preSerialization', async (request, reply, payload) => {
  2. return { wrapped: payload }
  3. })

注:payload 为 stringBufferstreamnull 时,该钩子不会被调用。

onError

  1. fastify.addHook('onError', (request, reply, error, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('onError', async (request, reply, error) => {
  2. // 当自定义错误日志时有用处
  3. // 你不应该使用这个钩子去更新错误
  4. })

onError 钩子可用于自定义错误日志,或当发生错误时添加特定的 header。
该钩子并不是为了变更错误而设计的,且调用 reply.send 会抛出一个异常。
它只会在 customErrorHandler 向用户发送错误之后被执行 (要注意的是,默认的 customErrorHandler 总是会发送错误)。 注意:与其他钩子不同,onError 不支持向 done 函数传递错误。

onSend

使用 onSend 钩子可以改变 payload。例如:

  1. fastify.addHook('onSend', (request, reply, payload, done) => {
  2. const err = null;
  3. const newPayload = payload.replace('some-text', 'some-new-text')
  4. done(err, newPayload)
  5. })

或使用 async/await

  1. fastify.addHook('onSend', async (request, reply, payload) => {
  2. const newPayload = payload.replace('some-text', 'some-new-text')
  3. return newPayload
  4. })

你也可以通过将 payload 置为 null,发送一个空消息主体的响应:

  1. fastify.addHook('onSend', (request, reply, payload, done) => {
  2. reply.code(304)
  3. const newPayload = null
  4. done(null, newPayload)
  5. })

将 payload 设为空字符串 '' 也可以发送空的消息主体。但要小心的是,这么做会造成 Content-Length header 的值为 0。而 payload 为 null 则不会设置 Content-Length header。

注:你只能将 payload 修改为 stringBufferstreamnull

onResponse

  1. fastify.addHook('onResponse', (request, reply, done) => {
  2. // 其他代码
  3. done()
  4. })

或使用 async/await

  1. fastify.addHook('onResponse', async (request, reply) => {
  2. // 其他代码
  3. await asyncMethod()
  4. return
  5. })

onResponse 钩子在响应发出后被执行,因此在该钩子中你无法再向客户端发送数据了。但是你可以在此向外部服务发送数据,比如收集数据。

在钩子中管理错误

在钩子的执行过程中如果发生了错误,只需将错误传递给 done(),Fastify 就会自动关闭请求,并发送一个相应的错误码给用户。

  1. fastify.addHook('onRequest', (request, reply, done) => {
  2. done(new Error('Some error'))
  3. })

如果你想自定义发送给用户的错误码,使用 reply.code() 即可:

  1. fastify.addHook('preHandler', (request, reply, done) => {
  2. reply.code(400)
  3. done(new Error('Some error'))
  4. })

错误最终会在 Reply 中得到处理。

或者在 async/await 函数中抛出错误:

  1. fastify.addHook('onResponse', async (request, reply) => {
  2. throw new Error('Some error')
  3. })

在钩子中响应请求

需要的话,你可以在路由函数执行前响应一个请求,例如进行身份验证。在钩子中响应暗示着钩子的调用链被 终止,剩余的钩子将不会执行。假如钩子使用回调的方式,意即不是 async 函数,也没有返回 Promise,那么只需要调用 reply.send(),并且避免触发回调便可。假如钩子是 async 函数,那么 reply.send() 必须 发生在函数返回或 promise resolve 之前,否则请求将会继续下去。当 reply.send() 在 promise 调用链之外被调用时,需要 return reply,不然请求将被执行两次。

不应当混用回调与 async/Promise,否则钩子的调用链会被执行两次。

如果你在 onRequestpreHandler 中发出响应,请使用 reply.send。如果是在中间件中,使用 res.end

  1. fastify.addHook('onRequest', (request, reply, done) => {
  2. reply.send('Early response')
  3. })
  4. // 也可使用 async 函数
  5. fastify.addHook('preHandler', async (request, reply) => {
  6. await something()
  7. reply.send({ hello: 'world' })
  8. return reply // 在这里是可选的,但这是好的实践
  9. })

如果你想要使用流 (stream) 来响应请求,你应该避免使用 async 函数。必须使用 async 函数的话,请参考 test/hooks-async.js 中的示例来编写代码。

  1. fastify.addHook('onRequest', (request, reply, done) => {
  2. const stream = fs.createReadStream('some-file', 'utf8')
  3. reply.send(stream)
  4. })

如果发出响应但没有 await 关键字,请确保总是 return reply

  1. fastify.addHook('preHandler', async (request, reply) => {
  2. setImmediate(() => { reply.send('hello') })
  3. // 让处理函数等待 promise 链之外发出的响应
  4. return reply
  5. })
  6. fastify.addHook('preHandler', async (request, reply) => {
  7. // fastify-static 插件异步地发送文件,因此需要 return reply
  8. reply.sendFile('myfile')
  9. return reply
  10. })

应用钩子

你也可以在应用的生命周期里使用钩子方法。要格外注意的是,这些钩子并未被完全封装。钩子中的 this 得到了封装,但处理函数可以响应封装界线外的事件。

onClose

使用 fastify.close() 停止服务器时被触发。当插件需要一个 “shutdown” 事件时有用,例如关闭一个数据库连接。
该钩子的第一个参数是 Fastify 实例,第二个为 done 回调函数。

  1. fastify.addHook('onClose', (instance, done) => {
  2. // 其他代码
  3. done()
  4. })

onRoute

当注册一个新的路由时被触发。它的监听函数拥有一个唯一的参数:routeOptions 对象。该函数是同步的,其本身并不接受回调作为参数。

  1. fastify.addHook('onRoute', (routeOptions) => {
  2. // 其他代码
  3. routeOptions.method
  4. routeOptions.schema
  5. routeOptions.url
  6. routeOptions.bodyLimit
  7. routeOptions.logLevel
  8. routeOptions.logSerializers
  9. routeOptions.prefix
  10. })

如果在编写插件时,需要自定义程序的路由,比如修改选项或添加新的路由层钩子,你可以在这里添加。

  1. fastify.addHook('onRoute', (routeOptions) => {
  2. function onPreSerialization(request, reply, payload, done) {
  3. // 其他代码
  4. done(null, payload)
  5. }
  6. // preSerialization 可以是数组或 undefined
  7. routeOptions.preSerialization = [...(routeOptions.preSerialization || []), onPreSerialization]
  8. })

onRegister

当注册一个新的插件,或创建了新的封装好的上下文后被触发。该钩子在注册的代码之前被执行。
当你的插件需要知晓上下文何时创建完毕,并操作它们时,可以使用这一钩子。
注意:被 fastify-plugin 所封装的插件不会触发该钩子。

  1. fastify.decorate('data', [])
  2. fastify.register(async (instance, opts) => {
  3. instance.data.push('hello')
  4. console.log(instance.data) // ['hello']
  5. instance.register(async (instance, opts) => {
  6. instance.data.push('world')
  7. console.log(instance.data) // ['hello', 'world']
  8. }), { prefix: '/hola' })
  9. }), { prefix: '/ciao' })
  10. fastify.register(async (instance, opts) => {
  11. console.log(instance.data) // []
  12. }), { prefix: '/hello' })
  13. fastify.addHook('onRegister', (instance, opts) => {
  14. // 从旧数组浅拷贝,
  15. // 生成一个新数组,
  16. // 使用户获得一个
  17. // 封装好的 `data` 的实例
  18. instance.data = instance.data.slice()
  19. // 新注册实例的选项
  20. console.log(opts.prefix)
  21. })

作用域

除了应用钩子,所有的钩子都是封装好的。这意味着你可以通过 register 来决定在何处运行它们,正如插件指南所述。如果你传递一个函数,那么该函数会获得 Fastify 的上下文,如此你便能使用 Fastify 的 API 了。

  1. fastify.addHook('onRequest', function (request, reply, done) {
  2. const self = this // Fastify 上下文
  3. done()
  4. })

注:使用箭头函数会破坏 Fastify 实例对 this 的绑定。

路由层钩子

你可以为单个路由声明一个或多个自定义的 onRequestonReponsepreParsingpreValidationpreHandlerpreSerialization 钩子。 如果你这么做,这些钩子总是会作为同一类钩子中的最后一个被执行。
当你需要进行认证时,这会很有用,而 preParsingpreValidation 钩子正是为此而生。 你也可以通过数组定义多个路由层钩子。

  1. fastify.addHook('onRequest', (request, reply, done) => {
  2. // 你的代码
  3. done()
  4. })
  5. fastify.addHook('onResponse', (request, reply, done) => {
  6. // 你的代码
  7. done()
  8. })
  9. fastify.addHook('preParsing', (request, reply, done) => {
  10. // 你的代码
  11. done()
  12. })
  13. fastify.addHook('preValidation', (request, reply, done) => {
  14. // 你的代码
  15. done()
  16. })
  17. fastify.addHook('preHandler', (request, reply, done) => {
  18. // 你的代码
  19. done()
  20. })
  21. fastify.addHook('preSerialization', (request, reply, payload, done) => {
  22. // 你的代码
  23. done(null, payload)
  24. })
  25. fastify.route({
  26. method: 'GET',
  27. url: '/',
  28. schema: { ... },
  29. onRequest: function (request, reply, done) {
  30. // 该钩子总是在共享的 `onRequest` 钩子后被执行
  31. done()
  32. },
  33. onResponse: function (request, reply, done) {
  34. // 该钩子总是在共享的 `onResponse` 钩子后被执行
  35. done()
  36. },
  37. preParsing: function (request, reply, done) {
  38. // 该钩子总是在共享的 `preParsing` 钩子后被执行
  39. done()
  40. },
  41. preValidation: function (request, reply, done) {
  42. // 该钩子总是在共享的 `preValidation` 钩子后被执行
  43. done()
  44. },
  45. preHandler: function (request, reply, done) {
  46. // 该钩子总是在共享的 `preHandler` 钩子后被执行
  47. done()
  48. },
  49. // // 使用数组的例子。所有钩子都支持这一语法。
  50. //
  51. // preHandler: [function (request, reply, done) {
  52. // // 该钩子总是在共享的 `preHandler` 钩子后被执行
  53. // done()
  54. // }],
  55. preSerialization: (request, reply, payload, done) => {
  56. // 该钩子总是在共享的 `preSerialization` 钩子后被执行
  57. done(null, payload)
  58. },
  59. handler: function (request, reply) {
  60. reply.send({ hello: 'world' })
  61. }
  62. })

:两个选项都接受一个函数数组作为参数。