for Node.js入门
#node #fastify

您以前与Express合作过的机会很高,因为自2010年发行以来,它一直是Node.js开发人员的首选网络框架。但是,近年来,出现了更新的Web框架,Express的开发已经大大减慢了。

Fastify是现场相对较新的球员,但是由于其速度和独特的功能,它很快就会受欢迎。

如果您仍在使用Express,您可能会想知道是否值得切换来快速进行。我们创建了这个三部分系列,以探索从Express转换为快速发展的好处,以及您可能遇到的潜在挑战。它还将提供一个动手指南,用于迁移您现有的明确应用程序以快速迁移。

在这一部分中,我们将重点介绍为什么您可以考虑在下一个node.js项目中快速浏览并详细探讨一些Fastify的核心概念。

所以让我们潜入其中,看看快点提供了什么!

先决条件

本教程假设您在计算机上安装了node.js的最新版本,例如最新的LTS版本(撰写时v18.x)。它的代码样本使用top-level await和ES模块语法而不是CONCORJS。我们还假设您使用Express或其他Web框架具有构建Node.js支持的API的基本知识。

为什么要从express切换到node.js?

在深入研究Fastify的内部工作之前,重要的是要了解为什么您可能要考虑将其用于下一个node.js项目。本节涵盖了与Express相比,在撰写本文时,Fastify提供的一些好处:

1.更快的性能

快速设计是从头开始快速使用的系统资源,因此它处理每秒的更多请求。

尽管Express是一个更成熟的框架,具有更大的社区和更全面的第三方套件生态系统,但它的优化并不像Fastify一样优化。

2.插件体系结构

快速设有功能强大的插件系统,可以轻松地为您的应用程序添加自定义功能。许多官方插件处理诸如身份验证,验证,安全性,数据库连接,无服务器兼容性和限制速率的事情。还有一个不断增长的第三方插件生态系统,可以集成到您的应用程序中,您可以轻松地创建自己的插件。

通过注入请求/响应处理管道中的中间件功能可扩展,以修改应用程序的行为。尽管如此,它们还是紧密地耦合到框架中,并且比Fastify的方法更灵活或模块化。 Fastify中的插件也被默认封装,因此您不会遇到交叉依赖性引起的问题。

3.默认情况下更安全

安全性是构建Web应用程序时的一个关键考虑,并且Fastify提供了许多内置的安全功能,例如:

  • 自动逃脱输出
  • 输入验证
  • 防止内容嗅探 攻击
  • 防止恶意标头注射

它的插件系统还可以轻松地在您的应用程序中采用其他安全措施。

另一方面,Express更加依赖中间件来处理安全问题。尽管可以将几个第三方软件包用于此目的。

,它没有内置验证或基于模式的请求处理。

4.现代JavaScript功能

Fastify对现代JavaScript功能(例如async/await and Promises)具有内置支持。它还自动捕获了在路线处理程序中发生的拒绝的承诺,从而更容易编写安全的异步代码。

Express不支持async/await处理程序,尽管您可以使用express-async-errors之类的软件包添加它们。请注意,此功能将在Express 5中本地支持。

app.get("/", async function (request, reply) {
  var data = await getData();
  var processed = await processData(data);
  return processed;
});

5.内置JSON模式验证

在Fastify中,JSON Schema验证是一个内置功能,可允许您在执行处理程序功能之前验证传入请求的有效载荷。这确保了传入数据的预期格式,并符合您的业务逻辑所需的标准。 Fastify的JSON Schema验证由Ajv library提供动力,这是一个快速有效的JSON Schema验证器。

const bodySchema = {
  type: "object",
  properties: {
    first_name: { type: "string" },
    last_name: { type: "string" },
    email: { type: "string", format: "email" },
    phone: { type: "number" },
  },
  required: ["first_name", "last_name", "email"],
};

fastify.post(
  "/create-user",
  {
    schema: {
      body: bodySchema,
    },
  },
  async (request, reply) => {
    const { first_name, last_name, age, email } = request.body;
    reply.send({ first_name, last_name, age, email });
  }
);

相比之下,Express不能为JSON模式验证提供内置支持。但是,您可以使用Joi(例如Joi)或上述AJV软件包等第三方库来验证Express应用程序中的JSON有效载荷(这需要其他设置和配置)。

6.打字稿支持

Fastify具有出色的TypeScript support,并考虑到了打字稿。它的类型定义包含在软件包中,并且它支持使用TypeScript为路由处理程序,模式和插件定义类型。

来自v3.x,快速类型系统在很大程度上依赖通用属性来进行准确的类型检查。

express还支持打字稿(通过@types/express软件包),但其支持不如快速fastify。

7.内置的记录仪

Fastify提供了基于Pino的内置记录机制,可让您捕获应用程序中的各种事件。启用后,将所有传入的请求快速登录到服务器以及处理所述请求时发生的错误。它还提供了一种通过fastify实例或请求对象上的log()方法来记录自定义消息的方便方法。

const app = require("fastify")({
  logger: true,
});

app.get("/", function (request, reply) {
  request.log.info("something happened");
  reply.send("Hello, world!");
});

Express不提供内置的记录器。取而代之的是,您需要使用Morgan,Pino或Winston等第三方记录库来记录HTTP请求和响应。尽管这些库是高度可配置的,但它们需要其他设置和配置。

快速提供的提供不仅仅是明确的

您可以看到,Fastify提供了比Express的许多优势,这使其成为构建Node.js Web应用程序的引人注目的选择。在以下各节中,我们将深入研究Fastify的核心功能,并演示如何创建Web服务器和路由。

我们还将探索快速的可扩展性和插件系统在应用程序开发方面具有更大的灵活性。

最后,您将更好地理解为什么Fastify是构建高性能和可扩展节点的绝佳选择。

开始使用Node.js应用程序

在您可以在项目中使用快点之前,您需要先安装它:

npm install fastify

安装后,您可以将其导入项目并实例化新的Fastify服务器实例,如下所示:

import Fastify from "fastify";

const fastify = Fastify();

Fastify函数接受自定义服务器行为的options object。例如,您可以启用其内置日志记录功能,并通过下面的摘要为传入客户端请求指定超时值:

. . .
const fastify = Fastify({
  logger: true,
  requestTimeout: 30000, // 30 seconds
});

配置您的首选选项后,您可以添加第一个路线,如下所示:

. . .
fastify.get('/', function (request, reply) {
  reply.send("Hello world!")
})

此路线接受对服务器根的GET请求,并以“ Hello World!”响应。然后,您可以通过在首选的Localhost端口上聆听来启动服务器:

. . .
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}`);
});

通过执行条目文件启动服务器。您将在控制台中观察一些JSON日志输出:

{"level":30,"time":1675958171939,"pid":391638,"hostname":"fedora","msg":"Server listening at http://127.0.0.1:3000"}
{"level":30,"time":1675958171940,"pid":391638,"hostname":"fedora","msg":"Server listening at http://[::1]:3000"}
{"level":30,"time":1675958171940,"pid":391638,"hostname":"fedora","msg":"Fastify is listening on port: http://127.0.0.1:3000"}

您可以使用pino-pretty包来使您的应用程序日志更易于阅读。

安装软件包后,将程序的输出输出到CLI,如下所示:

node server.js | pino-pretty

您将获得看起来像这样的彩色输出:

pino-pretty in action

启用记录器特别有用,因为它以以下方式将所有传入的请求记录到服务器:

curl "http://localhost:3000"
{
  "level": 30,
  "time": 1675961032671,
  "pid": 450514,
  "hostname": "fedora",
  "reqId": "req-1",
  "res": { "statusCode": 200 },
  "responseTime": 3.1204520016908646,
  "msg": "request completed"
}

注意日志中如何获得时间戳,请求ID,响应状态代码和响应时间(以毫秒为单位)。这是从Express迈出的一步,直到您自己整合Pino(或其他记录框架),您才能获得此类功能。

在快速建立路线

在Fastify中创建端点很容易使用框架中的几种辅助方法。 fastify.get()方法(在上一节中看到)创建了接受HTTP获取请求的端点。头部,帖子,put,删除,补丁和选项请求也存在类似的方法:

fastify.get(path, [options], handler);
fastify.head(path, [options], handler);
fastify.post(path, [options], handler);
fastify.put(path, [options], handler);
fastify.delete(path, [options], handler);
fastify.options(path, [options], handler);
fastify.patch(path, [options], handler);

如果您想将同一处理程序用于特定路由上所有受支持的HTTP请求方法,则可以使用fastify.all()方法:

fastify.all(path, [options], handler);

从上面的签名中可以看到,options参数是可选的,但是您可以使用它来指定每个路由的无数配置设置。 See the Fastify docs for the full list of available options

path参数可以是静态的(例如/about/settings/profile)或动态(例如/article/:id/foo*/:userID/repos/:projectID)。动态URL中的URL参数可通过request.params对象在handler函数中访问:

fastify.get("/:userID/repos/:projectID", function (request, reply) {
  const { userID, projectID } = request.params;
  // rest of your code``
});

快速管理员具有以下签名,其中request代表服务器接收的HTTP请求,而reply表示来自HTTP请求的响应。

function (request, reply) {}

如果路由的处理程序函数是异步的,则可以通过从功能返回来发送响应:

fastify.get("/", async function (request, reply) {
  // do some work
  return { body: true };
});

如果您在async处理程序中使用replyawait replyreturn reply来避免种族条件:

fastify.get("/", async function (request, reply) {
  // do some work
  return reply.send({ body: true });
});

快速路线的一个整洁方面是,它们会自动捕获未被发现的例外或承诺拒绝。当发生这种例外时,执行默认错误处理程序以提供通用的“ 500内部服务器错误”响应。

fastify.get("/", function (request, reply) {
  throw new Error("Uncaught exception");
});

这是您使用curl将请求发送到上面的端点时获得的JSON响应:

{"statusCode":500,"error":"Internal Server Error","message":"Uncaught exception"}

您可以通过
修改默认错误处理行为 koude25 function.

快速插件

快速设计可通过插件扩展。插件本质上是独立的,封装和可重复使用的模块,可将自定义逻辑和行为添加到快速服务器实例中。它们可用于各种目的,例如与协议,框架,数据库或API,处理身份验证等集成。

在写作时,有over 250 plugins available for Fastify。有些由核心团队维护,但社区提供了大多数。当您找到要使用的插件时,必须先安装它,然后在Fastify实例上注册。

例如,让我们使用@fastify/formbody插件来添加对x-www-form-urlencoded身体的支持:

npm install @fasitfy/formbody
// import the plugin
import formBody from '@fastify/formbody';

. . .

// register it on your Fastify instance
await fastify.register(formBody);

fastify.post('/form', function (request, reply) {
  reply.send(request.body);
});

. . .

使用此插件安装和注册,您将能够作为对象访问x-www-form-urlencoded形式的身体。例如,以下请求:

curl -d "param1=value1&param2=value2" -X POST 'http://localhost:3000/form'

应产生以下输出:

{"param1":"value1","param2":"value2"}

您还可以很容易地创建一个自定义快速插件。您需要做的就是导出具有以下签名的函数:

function (fastify, opts, next) {}

第一个参数是插件正在注册的快速实例,第二个是一个选项对象,第三个是回调函数,当插件准备就绪时必须调用。

下面是一个简单的快速插件的示例,该插件向服务器添加了新的健康检查路由:

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

  done();
}

export default health;
// server.js
import health from "./plugin.js";

const fastify = Fastify({ logger: true });
await fastify.register(health);

此时,向http://localhost:3000/health的请求将产生以下答复:

{ "status": "up" }

快速工作中的插件如何

fastify中的插件创建一个从应用程序中所有其他上下文中隔离的新的encapsulation context。这使您可以在插件上下文中修改fastify实例,而不会影响任何其他上下文的状态。在快速应用程序中总是只有一个根上下文,但是您可以拥有想要的儿童上下文。

这是一个代码段,说明了上下文如何在fastify中工作:

// `root` is the root context.
const root = Fastify({
  logger: true,
});

root.register(function pluginA(contextA, opts, done) {
  // `contextA` is a child of the `root` context.
  contextA.register(function pluginB(contextB, opts, done) {
    // `contextB` is a child of `contextA`
    done();
  });

  done();
});

root.register(function pluginC(contextC, opts, done) {
  // `contextC` is a child of the `root` context.
  contextC.register(function pluginD(contextD, opts, done) {
    // `contextD` is a child of `contextC`
    done();
  });

  done();
});

上面的摘要描述了具有五个不同封装上下文的快速应用程序。 root是两个上下文的父母(contextAcontextC),每个上下文都有自己的子上下文(分别为contextBcontextD)。

Fastify中的每个上下文(或插件)都有自己的状态,其中包括装饰器,钩子,路线或插件。虽然儿童上下文可以访问父的状态,但反向并非如此(至少默认为默认情况下)。

这是一个使用装饰器的示例(我们将在本系列的第二部分中进一步讨论装饰器)将一些自定义属性附加到每个上下文:

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

root.decorate("answerToLifeTheUniverseAndEverything", 42);

await root.register(async function pluginA(contextA, opts, done) {
  contextA.decorate("speedOfLight", "299,792,458 m/s");

  console.log(
    "contextA -> root =",
    contextA.answerToLifeTheUniverseAndEverything
  );
  await contextA.register(function pluginB(contextB, opts, done) {
    contextB.decorate("someAPIKey", "3493203890");

    console.log(
      "contextB -> root =",
      contextB.answerToLifeTheUniverseAndEverything
    );
    console.log("contextB -> contextA =", contextB.speedOfLight);
    done();
  });

  console.log("contextA -> contextB =", contextA.someAPIKey);

  done();
});

console.log("root -> contextA =", root.speedOfLight);
console.log("root -> contextB =", root.someAPIKey);

在上面的摘要中,root上下文装饰有自定义属性answerToLifeTheUniverseAndEverything,其产生42.同样,contextAcontextB都装饰有自己的自定义属性。执行代码时,您将在控制台中观察以下结果:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = undefined
root -> contextA = undefined
root -> contextB = undefined

请注意,由于它们都是root上下文的后代,因此可以直接在contextAcontextB对象上访问该根的自定义属性。同样,出于相同的原因,contextB可以访问speedOfLight属性。

但是,由于父母无法访问其嵌套上下文的状态,因此访问contextA.someAPIKeyroot.speedOfLightroot.someAPIKey会产生undefined

在node.js中共享上下文

有一种方法可以破坏Fastify的封装机制,以便父母也可以访问其子女环境的状态。这是通过用fastify-plugin module包装插件函数来完成的:

// import the plugin
import fp from "fastify-plugin";

await root.register(
  // wrap the plugin function
  fp(async function pluginA(contextA, opts, done) {
    // . . .
  })
);

将其安装到位,您将观察到以下输出:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = undefined
root -> contextA = 299,792,458 m/s
root -> contextB = undefined

请注意,contextA上装饰的speedOfLight属性现在可以在root上下文中访问。但是,someAPIKey仍然无法访问,因为pluginB函数未用fastify-plugin模块包裹。

如果您还打算在父上下文中访问contextB的状态,则是解决方案:

// import the plugin
import fp from "fastify-plugin";

await root.register(
  // wrap the plugin function
  fp(async function pluginA(contextA, opts, done) {
    // . . .

    await contextA.register(
      fp(function pluginB(contextB, opts, done) {
        // . . .
      })
    );
  })
);

contextB的状态现在也可以在其所有祖先中访问:

contextA -> root = 42
contextB -> root = 42
contextB -> contextA = 299,792,458 m/s
contextA -> contextB = 3493203890
root -> contextA = 299,792,458 m/s
root -> contextB = 3493203890

您将在本系列的下一部分中看到一个更实际的插件封装上下文示例,我们将在其中处理钩子和中间件。

下一步:钩子,中间件,装饰器和验证

在本文中,我们介绍了Fastify Web框架,探索了它的便利性,速度和低开销,这使其成为构建高性能和可扩展的Web应用程序的流行选择。我们比较了快速表达并突出显示您为什么要考虑切换的原因。

我们还讨论了Fastify的可扩展性和插件系统,该系统允许您自定义和扩展其功能。

在本系列的第二部分中,我们将深入研究一些更高级的快速概念,例如钩子,中间件,装饰器和验证。

直到下次,感谢您的阅读!

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

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