You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
hutao/docs/book/hooks.md

7.7 KiB

Fastify 提供了多种钩子(hooks),允许您在不同的生命周期阶段介入服务器的行为。它们在处理请求和响应时非常有用,例如验证请求、日志记录、执行清理或统计任务等。

下面是 Fastify 所有的生命周期钩子列表,按照它们在请求生命周期中的执行顺序排列:

应用层面的钩子

  1. onRequest(req, reply, done)

    • 在路由解析之前执行(但在请求被解析之后,例如 HTTP 方法、URL 和头部)。
    • 可以用来执行验证、解密等任务。
  2. preParsing(req, reply, payload, done)

    • 在解析 HTTP 请求之前执行(例如,解析请求体之前)。
    • 允许您操作原始请求对象和有效载荷。
  3. preValidation(req, reply, done)

    • 在执行路由处理程序之前、请求校验之前执行。
    • 适合执行权限校验等任务。
  4. preHandler(req, reply, done)

    • 在执行路由处理程序之前执行。
    • 可用于修改请求或回复,或在实际处理程序运行前做准备工作。

路由层面的钩子

在路由声明时,可以指定以下钩子,它们仅在应用于该路由的请求中执行:

  1. preValidation(req, reply, done)

    • 类似于应用层面的 preValidation 钩子,但只作用于特定路由。
  2. preHandler(req, reply, done)

    • 类似于应用层面的 preHandler 钩子,但只作用于特定路由。

异常处理钩子

  1. onError(req, reply, error, done)
    • 当处理程序或钩子抛出异常时执行。
    • 用于自定义错误处理逻辑。

回复生命周期钩子

  1. onSend(req, reply, payload, done)

    • 在将回复发送给客户端之前执行。
    • 可以用来修改回复的有效载荷。
  2. onResponse(req, reply, done)

    • 当响应完全发送给客户端后执行。
    • 适用于清理资源或记录日志。

关闭钩子

  1. onClose(instance, done)
    • 在服务器关闭时执行。
    • 通常用于关闭数据库连接、清理资源等。

这些钩子都遵循 fastify 的 "错误优先回调" 约定,即 done 回调函数的第一个参数是一个错误对象,如果有错误的话。如果一切正常,您只需调用 done(),没有任何参数或者传入 null

要使用这些钩子,通常您会将它们注册为 Fastify 插件,或者直接应用在 Fastify 实例上。例如:

// 注册一个 onRequest 钩子
fastify.addHook('onRequest', (req, reply, done) => {
  // 钩子逻辑
  done();
});

对于路由特定的钩子,您将它们添加到路由声明中,如下所示:

fastify.route({
  method: 'GET',
  url: '/',
  preHandler: (req, reply, done) => {
    // 路由特定的钩子逻辑
    done();
  },
  handler: (req, reply) => {
    // 路由处理程序
    reply.send({ hello: 'world' });
  }
});

钩子可以是非常强大的工具,它们提供了对 Fastify 应用程序的细粒度控制,允许您优雅地集成自定义逻辑和中间件。

在 Fastify 中,您可以通过在路由声明中直接添加钩子来将它们作用于特定的路由。当您声明路由时,Fastify 允许您指定 preValidationpreHandlerpreSerializationonErroronSendonResponse 这些生命周期钩子。

这里有一个例子,展示了如何给特定路由添加钩子:

fastify.route({
  method: 'GET',
  url: '/special',
  preHandler: (request, reply, done) => {
    // ... 在这个特定路由之前执行一些逻辑
    done();
  },
  handler: (request, reply) => {
    // ... 处理路由请求
    reply.send({ hello: 'world' });
  },
  onSend: (request, reply, payload, done) => {
    // ... 在回复发送到客户端之前修改回复内容
    done();
  }
});

在这个例子中,preHandler 钩子函数在处理 GET 请求到 /special 路由之前运行,而 onSend 钩子在响应发送之前运行,可以用于修改响应 payload。

每个钩子函数都会接受不同的参数,根据所执行的任务,这些参数提供了对请求 (request)、回复 (reply)、有效载载荷 (payload) 等的访问权。钩子函数最后需要调用 done 函数来通知 Fastify 钩子已完成执行,如果出现错误,可以将错误作为 done 函数的第一个参数传递。如果一切顺利,那么只需调用 done()done(null) 即可。

如果您需要在一个特定的路由上应用多个同类型的钩子,您可以传递一个钩子函数的数组:

fastify.route({
  method: 'GET',
  url: '/special',
  preHandler: [
    (request, reply, done) => {
      // 第一个 preHandler 钩子
      done();
    },
    (request, reply, done) => {
      // 第二个 preHandler 钩子
      done();
    }
  ],
  handler: (request, reply) => {
    // ...处理路由请求
    reply.send({ hello: 'world' });
  }
});

在这个例子中,当请求 /special 路由时,将按照数组中的顺序执行两个 preHandler 钩子函数。

通过将钩子绑定到特定路由上,您可以为每个路由精细地控制流程,并按需集成权限检查、请求数据修改、响应的自定义处理等逻辑。

在 Fastify 中,如果多个路由有部分共同的路径,并且你想对这些具有共同路径的路由应用特定的钩子,你可以使用两种主要方法:

  1. 通过插件和前缀注册钩子:你可以创建一个插件,为这个插件设置一个路径前缀(路由的共同部分),然后在这个插件内注册全局钩子(这将仅应用于插件范围内的路由)。

    以下是使用插件来注册一个钩子的示例:

    const fastify = require('fastify')({ logger: true });
    
    // 插件定义
    fastify.register(function (instance, options, done) {
      // 这里的钩子将仅作用于 '/api' 前缀路径下的路由
      instance.addHook('preHandler', async (request, reply) => {
        // ... 在该插件下所有路由的处理器之前执行逻辑
      });
    
      instance.get('/feature1', async (request, reply) => {
        // ... 处理该路由的请求
        return { feature: '1' };
      });
    
      instance.get('/feature2', async (request, reply) => {
        // ... 处理该路由的请求
        return { feature: '2' };
      });
    
      done();
    }, { prefix: '/api' });
    
    // 启动服务器
    fastify.listen(3000, err => {
      if (err) throw err;
      console.log(`Server listening on ${fastify.server.address().port}`);
    });
    

    在这个例子中,所有前缀为 /api 的路由都将执行在 addHook 中定义的 preHandler 钩子。

  2. 条件性地在钩子中检查路由路径:你还可以在一个全局钩子中检查请求的路径,根据请求的路由路径来做出条件性的反应,这种方法没有上面那种结构化,但在某些情况下可能有用。

    以下是使用条件性钩子的示例:

    // 全局钩子
    fastify.addHook('preHandler', async (request, reply) => {
      // 查看请求路径是否匹配特定模式
      if (request.raw.url.startsWith('/api')) {
        // ... 在所有 '/api' 路径下的路由之前执行逻辑
      }
    });
    
    // 非`/api`前缀的路由
    fastify.get('/other', async (request, reply) => {
      // 这个路由不会受到上述全局钩子影响
      // 除非在上面的条件检查中同时包括此路径
      return { hello: 'world' };
    });
    

    在这个例子中,全局 preHandler 钩子对所有路由生效,但是它内部检查请求的 URL 是不是以 /api 开头,仅对符合该条件的路径执行额外的逻辑。

通常,对于维护性和可读性而言,使用插件和前缀注册钩子的方法更为推荐,因为它将相关的路由和钩子逻辑组织在一起,并避免了全局状态的潜在冲突。