提升 Node.js 服务端性能:Fastify 框架

微信搜索"好朋友乐平"关注公众号。


1. fastify

Fastify 是一个高效且快速的 Node.js web 框架,专为提供最佳的性能而设计。它是相对较新的,但已经因其高性能和低开销而受到许多开发者的欢迎。Fastify 提供了一个简洁的开发体验,同时支持快速的路由和插件架构,使得开发者可以快速构建和扩展他们的应用。

Fastify 可以被视为 Node.js 中的一个高效、现代化的 web 框架,是构建快速 web 应用的一个优秀选择。

主要特性和原则:

  • 高性能:据我们所知,Fastify 是最快的 web 框架之一,根据代码复杂度,我们可以处理每秒高达 30,000 个请求。
  • 可扩展:Fastify 通过其钩子、插件和装饰器完全可扩展。
  • 基于模式:虽然不是强制的,但我们建议使用 JSON Schema 来验证你的路由并序列化你的输出,内部地,Fastify 将模式编译成一个高性能的函数。
  • 日志记录:日志非常重要但成本高昂;我们选择了最佳的日志记录器 Pino 来几乎消除这一成本!
  • 开发者友好:这个框架旨在非常表达性,并在不牺牲性能和安全性的情况下,帮助开发者日常使用。
  • TypeScript 准备就绪:我们努力维护 TypeScript 类型声明文件,以支持不断增长的 TypeScript 社区。

2. 诞生背景

Fastify 的诞生背景主要源于对现有 Node.js web 框架的性能和开发效率的需求。在 Fastify 出现之前,Express 和 Koa 已经是 Node.js 领域广泛使用的两个框架。虽然它们非常流行且易于使用,但在性能方面,尤其是在处理大量请求时,可能不会提供最优的性能。

Fastify 旨在解决以下几个关键问题:

  1. 性能:Fastify 的核心目标之一是提供比现有框架更高的性能。它通过减少每个请求的开销来实现这一点,使其能够处理更多的请求,这对于构建高性能应用程序至关重要。

  2. 开发效率:尽管性能至关重要,但开发效率同样重要。Fastify 通过其插件架构和开箱即用的功能(如模式验证、日志记录等)提供了一个高效且易于使用的开发环境,这有助于加速开发过程并减少潜在的错误。

  3. 模式验证:Fastify 内置对 JSON Schema 的支持,这不仅有助于验证客户端输入,还能确保应用程序的数据一致性和可靠性。这是许多框架在核心层面没有集成的功能。

  4. 可扩展性:通过其插件架构,Fastify 允许开发者轻松地添加新功能或集成第三方库,而不会对应用程序的整体性能产生重大影响。

通过解决这些问题,Fastify 为开发高性能、可维护且易于开发的 Node.js 应用程序提供了一个强大的平台。这些特性让 Fastify 在 JavaScript 社区中迅速获得了关注和使用。

3. 高性能实现

Fastify 实现高性能的关键在于它的设计和架构选择,这些选择帮助最小化每个请求的开销。下面是 Fastify 实现高性能的几个关键方面:

  1. 高效的路由分发:Fastify 使用的是一个非常快的路由分发机制,这意味着当请求进来时,Fastify 能够迅速决定哪个处理函数应该被调用。这种快速的路由决策极大地减少了请求处理时间。

  2. 预编译的序列化:Fastify 在发送响应之前不是在运行时动态序列化对象,而是使用预编译的序列化函数。这种方法减少了每个请求的处理时间,因为响应可以更快地被序列化并发送给客户端。

  3. 模式驱动的开发:Fastify 强烈推荐(并在某些情况下要求)使用 JSON Schema 来定义路由的输入和输出。这不仅有助于验证和文档化 API,而且允许 Fastify 预编译验证逻辑,从而提高运行时效率。

  4. 优化的插件系统:Fastify 的插件架构旨在尽可能高效。插件系统允许代码重用和最佳实践,同时保持核心框架的轻量级。这种设计支持了高度的模块化和灵活性,而不牺牲性能。

  5. 高效的日志记录:内置的日志记录工具 Pino 是为速度而设计的,它提供了极低的开销日志记录功能,这对于保持应用程序的性能至关重要。

  6. 零成本抽象:Fastify 的设计哲学是尽量减少抽象层的性能开销。这意味着即使在使用各种抽象和便利功能时,Fastify 也能保持高性能。

通过这些设计决策和优化,Fastify 能够提供出色的性能,这使得它成为构建高效且响应迅速的 Node.js 应用的优秀选择。

4. 安装使用

复制代码
npm install fastify
js 复制代码
// Import the framework and instantiate it
import Fastify from "fastify";
const fastify = Fastify({
  logger: true,
});

// Declare a route
fastify.get("/", async function handler(request, reply) {
  return { hello: "world" };
});

// Run the server!
try {
  await fastify.listen({ port: 3000 });
} catch (err) {
  fastify.log.error(err);
  process.exit(1);
}

或者使用 cli 生成骨架:

复制代码
npm install --global fastify-cli
fastify generate myproject

JSON Schema 和 hooks 处理请求示例:

js 复制代码
import Fastify from "fastify";
const fastify = Fastify({
  logger: true,
});

fastify.route({
  method: "GET",
  url: "/",
  schema: {
    // request needs to have a querystring with a `name` parameter
    querystring: {
      type: "object",
      properties: {
        name: { type: "string" },
      },
      required: ["name"],
    },
    // the response needs to be an object with an `hello` property of type 'string'
    response: {
      200: {
        type: "object",
        properties: {
          hello: { type: "string" },
        },
      },
    },
  },
  // this function is executed for every request before the handler is executed
  preHandler: async (request, reply) => {
    // E.g. check authentication
  },
  handler: async (request, reply) => {
    return { hello: "world" };
  },
});

try {
  await fastify.listen({ port: 3000 });
} catch (err) {
  fastify.log.error(err);
  process.exit(1);
}

5. 插件、装饰器、中间件、钩子

在 Fastify 中,插件、装饰器、中间件和钩子是框架核心概念,各自承担不同的角色:

  1. 插件(Plugins)

    • 插件是 Fastify 应用中用于添加功能、共享代码或封装逻辑的主要方式。
    • 一个插件可以是一个函数,该函数接受 Fastify 实例、选项和回调函数作为参数。
    • 插件可以注册路由、添加装饰器、声明新的钩子,甚至可以封装其他插件,用于构建模块化的应用。
    • 插件允许开发者构建可重用的逻辑块,可以轻松地在不同的 Fastify 应用或同一个应用的不同部分之间共享。
  2. 装饰器(Decorators)

    • 装饰器在 Fastify 中用于扩展 Fastify 实例、请求(Request)和回复(Reply)对象,通过添加新的方法或属性。
    • 开发者可以使用装饰器来添加自定义的功能或数据,使其在整个应用的不同部分可用。
    • 例如,可以添加一个装饰器来添加一个方法,该方法在每个请求中都可用,用于访问共享的配置数据或服务。
  3. 中间件(Middleware)

    • 尽管 Fastify 设计上不依赖于中间件,但它支持使用 Express/Connect 风格的中间件,主要用于兼容性或特定功能的集成。
    • 中间件可以访问请求和响应对象,可以执行代码、修改请求和响应对象、终止请求处理链、调用堆栈中的下一个中间件等。
    • Fastify 中间件应谨慎使用,因为不当使用可能会绕过 Fastify 的一些优化,影响性能。
  4. 钩子(Hooks)

    • 钩子是 Fastify 中的一种机制,允许开发者在请求生命周期的不同阶段介入执行逻辑(例如,在请求接收之后、路由解析之前、发送响应之前等)。
    • 钩子可以用于执行一些预处理或后处理逻辑,如权限检查、请求日志记录、修改响应等。
    • Fastify 提供了多种类型的钩子(如 onRequest, preHandler, onSend, 等),使开发者能够精细控制请求处理的不同阶段。

这些构件共同工作,为 Fastify 提供了极大的灵活性和扩展性,同时保持了框架的高性能特性。

5.1 声明周期权重

Fastify 中的组件(插件、装饰器、中间件和钩子)遵循一定的执行顺序和优先级,这些顺序和优先级定义了它们在请求处理流程中的作用时机。理解这个执行流程对于设计高效和可靠的 Fastify 应用至关重要。

  1. 插件

    • 插件在应用启动时加载,按照注册的顺序执行。一旦应用启动完成,插件的设置就被固化,对于后续的每个请求,插件中定义的功能(如路由、钩子、装饰器)都是可用的。
  2. 装饰器

    • 装饰器本身没有明确的执行时机,它们提供的方法或属性在被装饰的对象(Fastify 实例、请求、回复)上即刻可用,并在对象的生命周期内持续有效。
  3. 中间件

    • 中间件在每个请求处理流程的早期执行,具体是在路由匹配之前。中间件可以修改请求和回复对象,或者决定是否将请求传递给下一个处理器。
  4. 钩子

    • 钩子遵循特定的执行顺序,反映在请求的处理流程中:
      • onRequest:在请求被接收后立即执行,但在任何其他处理之前。
      • preParsing:在请求体解析之前执行。
      • preValidation:在路由级别的验证之前执行。
      • preHandler:在路由处理函数之前执行。
      • preSerialization:在响应被序列化之前执行,发送给客户端之前。
      • onSend:在响应发送给客户端之前,但在序列化之后执行。
      • onResponse:在响应完全发送给客户端后执行。

针对中断的能力:

  • 插件:不直接参与请求处理的中断,但可以注册会影响流程的钩子或中间件。
  • 装饰器:不控制流程,因此不参与中断。
  • 中间件 :可以中断请求处理流程,例如,不调用 next() 或发送响应可以停止后续处理。
  • 钩子 :特定的钩子(如 preHandler)可以决定是否继续处理请求,或者直接发送响应,从而中断后续流程。

理解这些组件的执行顺序和中断能力对于构建符合预期行为的 Fastify 应用至关重要。

6. 生命周期

复制代码
Incoming Request
  │
  └─▶ Routing
        │
        └─▶ Instance Logger
             │
   4**/5** ◀─┴─▶ onRequest Hook
                  │
        4**/5** ◀─┴─▶ preParsing Hook
                        │
              4**/5** ◀─┴─▶ Parsing
                             │
                   4**/5** ◀─┴─▶ preValidation Hook
                                  │
                            400 ◀─┴─▶ Validation
                                        │
                              4**/5** ◀─┴─▶ preHandler Hook
                                              │
                                    4**/5** ◀─┴─▶ User Handler
                                                    │
                                                    └─▶ Reply
                                                          │
                                                4**/5** ◀─┴─▶ preSerialization Hook
                                                                │
                                                                └─▶ onSend Hook
                                                                      │
                                                            4**/5** ◀─┴─▶ Outgoing Response
                                                                            │
                                                                            └─▶ onResponse Hook

当响应被劫持时 (即调用了 reply.hijack()) 会跳过之后的步骤,否则,响应被提交后的数据流向如下:

复制代码
                        ★ schema validation Error
                                    │
                                    └─▶ schemaErrorFormatter
                                               │
                          reply sent ◀── JSON ─┴─ Error instance
                                                      │
                                                      │         ★ throw an Error
                     ★ send or return                │                  │
                            │                         │                  │
                            │                         ▼                  │
       reply sent ◀── JSON ─┴─ Error instance ──▶ setErrorHandler ◀─────┘
                                                      │
                                 reply sent ◀── JSON ─┴─ Error instance ──▶ onError Hook
                                                                                │
                                                                                └─▶ reply sent

7. 封装

Fastify 的一个基本特性是"封装上下文"。封装上下文决定了哪些装饰器、注册的钩子和插件可用于路由。

注意,每个上下文仅从父上下文继承。父上下文不能访问其后代上下文中的任何实体。这种默认情况有时不是所需的。在这种情况下,可以通过使用 fastify-plugin 破坏封装上下文,使得在后代上下文中注册的任何东西都可用于包含的父上下文。

8. JSON Schema 验证路由

JSON Schema 在 Fastify 中与路由结合使用,主要是用于验证客户端请求的数据和格式化响应数据。通过定义 JSON Schema,可以确保传入的请求数据满足特定格式和类型要求,同时还可以控制响应数据的结构。这样做有助于提高 API 的健壮性和安全性。

以下是一个如何在 Fastify 路由中使用 JSON Schema 来验证请求和响应的示例:

javascript 复制代码
const fastify = require("fastify")();

// 定义 JSON Schema
const userSchema = {
  type: "object",
  required: ["username", "email", "age"],
  properties: {
    username: { type: "string" },
    email: { type: "string", format: "email" },
    age: { type: "number", minimum: 1 },
  },
};

fastify.route({
  method: "POST",
  url: "/create-user",
  schema: {
    body: userSchema, // 使用 JSON Schema 验证请求体
    response: {
      200: {
        type: "object",
        properties: {
          success: { type: "boolean" },
          id: { type: "string" },
        },
      },
    },
  },
  handler: (request, reply) => {
    // 处理请求逻辑
    // 假设创建用户成功,返回用户 ID
    reply.send({ success: true, id: "12345" });
  },
});

fastify.listen(3000, (err) => {
  if (err) throw err;
  console.log("Server is running on http://localhost:3000");
});

在这个例子中:

  1. 定义了一个 userSchema,描述了预期的请求体格式,包括必需字段和各字段的类型。
  2. 在路由配置中,通过 schema 属性将 userSchema 应用于请求体(body),这样 Fastify 会自动验证传入的请求数据是否符合定义的 schema。
  3. 同样,也定义了响应的 schema,确保响应数据结构符合预期。

通过这种方式,可以确保所有进入的请求都经过严格的验证,并且所有的响应都符合预定的格式,从而增强了 API 的健壮性和用户的可预测性。

9. 示例

9.1 Hooks 处理登录态问题

js 复制代码
const fastify = require("fastify")({ logger: true });
const secureSession = require("fastify-secure-session");

// 注册 session 插件
fastify.register(secureSession, {
  secret: "averylognsecretkey", // 应使用一个长而复杂的密钥
  cookie: {
    path: "/",
    // 配置其他 cookie 选项根据需要
  },
});

// 仅对/api/a 和/api/c 应用 preHandler 钩子
fastify.addHook("preHandler", (request, reply, done) => {
  if (
    (request.routerPath === "/api/a" || request.routerPath === "/api/c") &&
    !request.session.user
  ) {
    request.session.redirectTo = request.raw.url;
    reply.redirect(302, "/login");
    done();
  } else {
    done();
  }
});

// 不需要 session 验证的路由
fastify.get("/api/b", (req, reply) => {
  reply.send({ message: "Access granted for /api/b" });
});

// 需要 session 验证的路由
fastify.get("/api/a", (req, reply) => {
  reply.send({ message: "Access granted for /api/a" });
});

fastify.get("/api/c", (req, reply) => {
  reply.send({ message: "Access granted for /api/c" });
});

// 登录页面的路由
fastify.get("/login", (req, reply) => {
  reply.send(`Login form goes here. POST to /login to authenticate.`);
});

// 登录逻辑
fastify.post("/login", (req, reply) => {
  const { username, password } = req.body;
  if (username === "user" && password === "password") {
    req.session.user = username;
    const redirectTo = req.session.redirectTo || "/";
    delete req.session.redirectTo;
    reply.redirect(302, redirectTo);
  } else {
    reply.code(401).send({ error: "Unauthorized" });
  }
});

// 启动服务器
fastify.listen(3000, (err) => {
  if (err) {
    fastify.log.error(err);
    process.exit(1);
  }
  console.log(`Server listening on http://localhost:3000`);
});

9.2 限定参数

下面是 post 必须传 ab 参数:

js 复制代码
const fastify = require("fastify")();

const postSchema = {
  schema: {
    body: {
      type: "object",
      required: ["a", "b"],
      properties: {
        a: { type: "string" },
        b: { type: "string" },
      },
    },
    response: {
      // 如果缺失,默认返回 400 错误,可在此处修改 400 的返回类型
      400: {
        type: "object",
        properties: {
          statusCode: { type: "number" },
          error: { type: "string" },
          message: { type: "string" },
        },
      },
    },
  },

  // 此处可以拦截上面的默认拦截策略
  preValidation: (request, reply, done) => {
    if (!request.body || !request.body.a || !request.body.b) {
      reply
        .code(400)
        .send({ error: "Bad Request", message: "Missing required parameters" });
      return;
    }
    done();
  },
};

fastify.post("/api/data", postSchema, (request, reply) => {
  // 如果到这里,说明 a 和 b 参数都已经通过验证
  reply.send({ message: "Success" });
});

fastify.listen(3000, (err) => {
  if (err) {
    console.error(err);
    process.exit(1);
  }
  console.log("Server listening on port 3000");
});

如果不在路由中定义 preValidation 钩子和自定义的 response schema,当 POST 请求的 body 不符合定义的 JSON Schema 时,Fastify 默认会返回一个 400 错误(Bad Request)。这个错误的响应体会包含关于哪些字段不符合要求的信息,但这个信息是由 Fastify 自动生成的,可能不会像自定义错误消息那样具体或清晰。

默认情况下,如果只传了 a 参数而缺失了 b 参数,Fastify 会返回一个包含错误详情的响应,类似于:

json 复制代码
{
  "statusCode": 400,
  "error": "Bad Request",
  "message": "body should have required property 'b'"
}

微信搜索"好朋友乐平"关注公众号。

github原文地址

相关推荐
万少13 分钟前
第五款 HarmonyOS 上架作品 奇趣故事匣 来了
前端·harmonyos·客户端
爱奥尼欧16 分钟前
【Linux 系统】基础IO——Linux中对文件的理解
linux·服务器·microsoft
OpenGL19 分钟前
Android targetSdkVersion升级至35(Android15)相关问题
前端
rzl0234 分钟前
java web5(黑马)
java·开发语言·前端
Amy.Wang36 分钟前
前端如何实现电子签名
前端·javascript·html5
海天胜景38 分钟前
vue3 el-table 行筛选 设置为单选
javascript·vue.js·elementui
今天又在摸鱼38 分钟前
Vue3-组件化-Vue核心思想之一
前端·javascript·vue.js
蓝婷儿40 分钟前
每天一个前端小知识 Day 21 - 浏览器兼容性与 Polyfill 策略
前端
百锦再43 分钟前
Vue中对象赋值问题:对象引用被保留,仅部分属性被覆盖
前端·javascript·vue.js·vue·web·reactive·ref
戒不掉的伤怀43 分钟前
【Navicat 连接MySQL时出现错误1251:客户端不支持服务器请求的身份验证协议;请考虑升级MySQL客户端】
服务器·数据库·mysql