高级快速化:钩子,中间件和装饰器
#node #fastify

在本系列的第一篇文章中,我们介绍了快速的表达并进行了比较,突出了切换到快速效果Web应用程序的好处。我们还详细探讨了Fastify的插件系统,向您展示了如何使用可重复使用的模块扩展和自定义Web应用程序。

在这一部分中,我们将更深入地研究Fastify更高级的概念。具体来说,我们将演示如何使用钩子,中间件,装饰器和验证来构建更强大的Web应用程序。我们还将简要介绍一些可行的策略,以迁移现有的快速应用程序以快速迁移。

让我们开始!

钩子和中间件

fastify中的钩子可让您聆听应用程序生命周期中的特定事件,并在发生这些事件时执行特定的任务。钩子类似于Express Style中间件功能,但性能更高,使您可以执行诸如身份验证,日志记录,路由,数据解析,错误处理等任务。 Fastify中有两种类型的钩子:request/reply hooksapplication hooks

fastify请求/回复钩子

请求/回复挂钩在服务器的请求/回复周期期间执行,使您可以在处理或之后处理传入请求或发出响应之前执行各种任务。这些钩可以应用于所有路由或选择路线。它们通常用于实现以下功能:

  • 访问控制
  • 数据验证
  • 请求记录

和更多。

请求/回复钩的示例包括preRequestonSendonTimeout等。

这是一个使用onSend钩添加Server标头的示例

fastify.addHook("onSend", (request, reply, payload, done) => {
  reply.headers({
    Server: "fastify",
  });

  done();
});

现在应该熟悉requestreply对象,而payload参数代表响应主体。您可以在此处修改响应有效载荷,或通过将其设置为挂钩函数中的null来完全清除。最后,应执行done()回调以表示挂钩的末端,以便请求/回复生命周期继续。它最多可能需要两个参数:一个错误(如果有),或null和更新的有效载荷。

使用上述代码,每个响应现在都包含指定的标题:

curl -I http://localhost:3000
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
server: fastify
content-length: 12
Date: Sun, 12 Feb 2023 18:23:40 GMT
Connection: keep-alive
Keep-Alive: timeout=72

如果要为路由子集实现挂钩,则需要创建一个新的封装上下文,然后在插件中的fastify实例上注册钩子。例如,您可以在本教程前面演示的health插件中创建另一个onSend钩:

// plugin.js
function health(fastify, options, next) {
  fastify.get("/health", (request, reply) => {
    reply.send({ status: "up" });
  });

  fastify.addHook("onSend", (request, reply, payload, done) => {
    const newPlayload = payload.replace("up", "down");
    reply.headers({
      "Cache-Control": "no-store",
      Server: "nodejs",
    });

    done(null, newPlayload);
  });

  next();
}

export default health;

这次,通过将up更改为down,使用onSend挂钩来修改插件上下文中所有路由的响应主体(只是/health),它还覆盖了Server响应标头,同时添加新的Cache-Control标头。因此,到/health路线的请求现在将产生以下答复:

curl -i http://localhost:3000/health
HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
cache-control: no-store
server: nodejs
content-length: 17
Date: Sun, 12 Feb 2023 18:54:21 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"down"}⏎

请注意,在所有共享的onSend钩子之后,health上下文中的onSend钩将运行。如果要在特定路由上注册钩子,而不是插件上下文中的所有路由,则可以将其添加到路由选项中,如下所示:

function health(fastify, options, next) {
  fastify.get(
    "/health",
    {
      onSend: function (request, reply, payload, done) {
        const newPlayload = payload.replace("up", "down");
        reply.headers({
          "Cache-Control": "no-store",
          Server: "nodejs",
        });

        done(null, newPlayload);
      },
    },
    (request, reply) => {
      reply.send({ status: "up" });
    }
  );

  fastify.get("/health/alwaysUp", (request, reply) => {
    reply.send({ status: "up" });
  });

  next();
}

export default health;

onSend钩已移至/health路线的路由选项,因此它仅影响此路线的请求/响应周期。您可以通过向/health/health/alwaysUp提出请求来观察差异,如下所示。即使他们与相同的处理程序处于相同的插件上下文中,他们的响应内容也不同。

get /health < /strong>:

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
server: nodejs
cache-control: no-store
content-length: 17
Date: Sun, 12 Feb 2023 19:08:37 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"down"}

get/health/ewansway

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
server: fastify
content-length: 15
Date: Sun, 12 Feb 2023 19:09:18 GMT
Connection: keep-alive
Keep-Alive: timeout=72

{"status":"up"}

快速应用程序挂钩

另一方面,

应用程序挂钩在请求/回复生命周期之外执行。它们是在Fastify实例发出的事件上执行的,可用于执行一般服务器或插件任务,例如:

  • 连接到数据库和其他资源
  • 加载或卸载配置
  • 关闭连接
  • 持续数据
  • 冲洗日志

和其他。

这是一个示例,它使用Fastify的onClose应用程序挂钩模拟了优雅的关闭。如果您部署更新或您的服务器需要重新启动,则这样的设置是理想的选择。

fastify.addHook("onClose", (instance, done) => {
  instance.log.info("Server is shutting down...");
  // Perform any necessary cleanup tasks here
  done();
});

process.on("SIGINT", () => {
  fastify.close(() => {
    fastify.log.info("Server has been shut down");
    process.exit(0);
  });
});

在此示例中,onClose钩在服务器的根上下文上注册,并且在关闭服务器之前执行回调。挂钩函数可以访问当前的Fastify实例和done回调,该回调应在挂钩完成后调用。

此外,process.on()函数会听取SIGINT信号的声音,当您按CTRL+C或系统关闭时,该函数将发送到该过程。接收信号后,fastify.close()函数被调用以关闭服务器,并将服务器闭合记录记录到控制台。

将上述代码添加到程序后,启动服务器,然后按Ctrl-C关闭该过程。您将在控制台中观察以下日志:

{"level":30,"time":1676240333903,"pid":615734,"hostname":"fedora","msg":"Server is shutting down..."}
{"level":30,"time":1676240333903,"pid":615734,"hostname":"fedora","msg":"Server has been shut down"}

快速中的中间件

快速fastify还支持Express Style中间件,但它要求您安装外部插件,例如@fastify/express@fastify/middie。这简化了从Express迁移到快速迁移,但不应在Greenfield项目中使用它,而不是挂钩。请注意,在许多情况下,您可以找到一个提供与Express Middleware相同功能的本机快速插件。

下面的示例演示了如何在fastify中使用标准的快速中间件,例如cookie-parser,但是您应该在可能的情况下更喜欢天然替代方案,因为它们可以更好地优化用于fastify。<

import Fastify from "fastify";
import middie from "@fastify/middie";
import cookieParser from "cookie-parser";

const fastify = Fastify({
  logger: true,
});

await fastify.register(middie);

fastify.use(cookieParser());

fastify.get("/", function (request, reply) {
  console.log("Cookies: ", request.raw.cookies);
  reply.send("Hello world!");
});

const port = process.env.PORT || 3000;

fastify.listen({ port }, function (err, address) {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }

  fastify.log.info(`Fastify is listening on port: ${address}`);
});

一旦导入和注册了@fastify/middie插件,您就可以通过Fastify实例上提供的use()方法开始使用Express Mifdreware,并且它应如上所述工作。请注意,以这种方式应用的每个中间件将在onRequest钩阶段执行。

快速装饰者

装饰器是一项功能,可允许您使用任何类型的JavaScript属性自定义核心对象,例如功能,对象或任何原始类型。例如,您可以使用装饰器存储自定义数据或在FastifyRequestReply中注册新方法,分别通过decorate()decorateRequest()decorateReply()方法。

此示例演示了使用Fastify的装饰器添加
Web应用程序的功能:

import Fastify from 'fastify';
import fastifyCookie from '@fastify/cookie';

const fastify = Fastify({
  logger: true,
});

await fastify.register(fastifyCookie, {
  secret: 'secret',
});

fastify.decorate('authorize', authorize);
fastify.decorate('getUser', getUser);
fastify.decorateRequest('user', null);

async function getUser(token) {
  // imagine the token is used to retrieve a user
  return {
    id: 1234,
    name: 'John Doe',
    email: 'john@example.com',
  };
}

async function authorize(request, reply) {
  const { user_token } = request.cookies;
  if (!user_token) {
    throw new Error('unauthorized: missing session cookie');
  }

  const cookie = request.unsignCookie(user_token);
  if (!cookie.valid) {
    throw new Error('unauthorized: invalid cookie signature');
  }

  let user;
  try {
    user = await fastify.getUser(cookie.value);
  } catch (err) {
    request.log.warn(err);
    throw err;
  }

  request.user = user;
}

fastify.get('/', async function (request, reply) {
  await this.authorize(request, reply);

  reply.send(`Hello ${request.user.name}!`);
});

. . .

上述代码片段在fastify实例上装饰了两个功能。第一个是getUser(),它将令牌作为参数并返回用户对象(在此示例中进行了硬编码)。

接下来定义了authorize()功能。它检查了请求中是否存在user_token cookie并验证它。如果cookie无效或丢失,则会丢弃错误。否则,使用getUser()函数将cookie值用于检索相应的用户,并且结果存储在请求对象上的user属性中。如果在检索用户时发生错误,则记录错误并重新填写。

虽然您可以将任何属性添加到FastifyRequestReply对象中,但您需要先用装饰器声明它们。这有助于基础JavaScript引擎优化这些对象的处理。

curl --cookie "user_token=yes.Y7pzW5FUVuoPD5yXLV8joDdR35gNiZJzITWeURHF5Tg" http://127.0.0.1:3000/
Hello John Doe!

快速验证数据验证

数据验证是依赖客户数据的任何Web应用程序的重要功能,因为它有助于防止恶意有效载荷引起的安全漏洞并提高应用程序的可靠性和鲁棒性。

快速使用JSON schema来定义每个路由输入有效载荷的验证规则,其中包括请求主体,查询字符串,参数和标题。 JSON模式是定义JSON数据的结构和约束的标准格式,Fastify使用Ajv,这是可用的最快,最有效的JSON Schema验证器之一。

要在Fastify中使用JSON验证,您需要为每条期望有效负载的每个路线定义一个模式。您可以使用标准JSON架构格式或Fastify JSON模式格式指定模式,这是表达模式的一种更简洁和表达的方式。

这是如何为fastify中的路线定义JSON模式的一个示例:

const bodySchema = {
  type: "object",
  properties: {
    name: { type: "string" },
    age: { type: "number" },
    email: { type: "string", format: "email" },
  },
  required: ["name", "email"],
};

fastify.post(
  "/users",
  {
    schema: {
      body: bodySchema,
    },
  },
  async (request, reply) => {
    const { name, age, email } = request.body;
    reply.send({
      name,
      age,
      email,
    });
  }
);

在此示例中,我们定义了一个期望具有三个属性的对象:nameageemailname属性应该是字符串,age属性应该是一个数字,并且email属性应为电子邮件格式的字符串。我们还指定nameemail是必需的属性。

当客户端以无效的有效载荷向/users发送邮政请求时,Fastify会自动返回“ 400不良请求”响应,并带有错误消息,指示哪个属性失败了验证。但是,如果有效载荷遵守架构,则将用解析的有效载荷执行路由处理程序功能为request.body

这是带有无效有效载荷请求的示例(email密钥的格式错误):

curl --header "Content-Type: application/json" \
  --request POST \
  --data '{"name":"John","age": 44,"email":"john@example"}' \
  http://localhost:3000/users

这是Fastify产生的响应:

{"statusCode":400,"error":"Bad Request","message":"body/email must match format \"email\""}

See the docs to learn more about validation in Fastify以及如何自定义其行为以适应您的需求。

规划从Express的增量迁移到快速

增量迁移是那些想要切换到新框架但无法一次进行全部更改的人来说,这是一个绝佳的策略。通过采用增量方法,您可以在仍在使用Express的同时逐渐将快速介绍到您的项目中,这使您有时间进行任何必要的更改并确保平稳的过渡。

第一步是确定项目的各个部分,这些部分将从Fastify的功能中受益,例如其内置支持和记录的内置支持,并改善了Express的性能。一旦确定了这些领域,就可以与现有的Express代码一起介绍Fastify。

这可能涉及设置一个单独的服务器实例,该实例在仍在使用Express的其余应用程序中,使用快速fastify处理某些路由或端点。但是您可能会发现,使用@fastify/express插件更容易添加完整的Express兼容性来快速添加,以便您可以使用Express Mifferware和应用程序,就像它们是快速插件一样。

要使用@fastify/express插件,您可以通过npm安装它,并在您的快速实例中注册:

import Fastify from "fastify";
import expressPlugin from "@fastify/express";

const fastify = Fastify({
  logger: true,
});

await fastify.register(expressPlugin);

您可以像在Express应用程序中一样使用Express中间件或应用程序。例如:

import express from "express";
const expressApp = express();

expressApp.use((req, res, next) => {
  console.log("This is an Express middleware");
  next();
});

expressApp.get("/express", (req, res) => {
  res.json({ body: "hello from express" });
});

fastify.use(expressApp);
curl http://localhost:3000/express
{ "body": "hello from express" }

随着您对Fastify感到更加自在,您可以开始越来越多地迁移您的代码,最终用Fastify替换所有特定于特定的代码。通过抽出时间计划和执行周到的迁移策略,您可以确保平稳的过渡,并最终获得更高效,高性能的应用程序。

下一步:从Express迁移

我们在本教程中涵盖了很多基础。我们希望您对Fastify的新颖功能(如钩子和装饰者)如何帮助您自定义和扩展应用程序更深入了解。

在本系列的下一个也是最后一部分中,我们将提供一个实用的指南,以迁移从Express快速进行。我们将介绍常见的迁移方案,提供优化性能和提高安全性的技巧,并在此过程中分享一些最佳实践。

感谢您的阅读,下次见!

P.S。如果您喜欢这篇文章,subscribe to our JavaScript Sorcery list每月深入研究更神奇的JavaScript技巧和技巧。

P.P.S。如果您需要node.js应用程序的apm,请go和check out the AppSignal APM for Node.js