迁移您的快速申请以快速迁移
#node #express #fastify

欢迎来到我们Express的最后一部分,以快速浏览系列。在上一期中,我们探索了Fastify Over Express的独特功能和优势。现在,您将通过迁移现有的Express应用程序快速迁移到迄今为止所学到的知识。

您将避免通过逐渐过渡到快速进行从头开始重写整个应用程序。阅读本文后,您将拥有自信地迁移明确应用程序并享受快速框架的好处的技能。

让我们开始!

先决条件

在进行文章之前,请确保您在计算机上安装了node.js和NPM的最新版本。您还需要install MongoDB并确保它在端口27017上运行。

设置演示项目

为了说明从Express迁移到快速迁移的过程,我们已经准备了demo application。该应用程序利用Express,MongoosePug创建一个URL Shortener应用程序,如下所示:

URL Shortener Application

通过表格提交URL后,应用程序会生成可复制并在其他地方使用的缩短URL。

继续使用以下命令将存储库克隆到计算机:

git clone https://github.com/damilolaolatunji/node-url-shortener

之后,cd进入项目目录,并使用npm
安装应用程序依赖项

npm install

该过程完成后,将.env.sample文件重命名为.env,并按如下编辑:

NODE_ENV=development
LOG_LEVEL=debug
PORT=3000
MONGODB_URL=mongodb://127.0.0.1:27017/url_shortener
APPSIGNAL_PUSH_API_KEY=<your_appsignal_push_api_key>

该应用程序被配置为使用AppSignal进行日志管理和错误跟踪,因此您可以轻松监视和故障排除可能发生的任何问题。 AppSignal提供实时警报和详细的错误报告,使您可以在成为主要问题之前快速识别并修复错误的根本原因。

如果您想在行动中看到此操作,sign up for an AppSignal account(您可以进行30天免费试用,无需信用卡),则是create a new application并复制 push api键 push &在应用程序设置中部署 页。

完成此操作后,替换.env文件中的<your_appsignal_push_api_key>占位符。请注意,不需要此步骤完成迁移以快速进行。

您可以通过执行以下命令来启动开发服务器:

npm run start:dev

以下输出表明服务器成功启动:

[nodemon] starting `node src/index.js`
{"level":"info","message":"Connected to MongoDB","timestamp":"2023-05-03T12:26:07.746Z"}
{"level":"info","message":"URL Shortener is running in development mode → PORT 3000","timestamp":"2023-05-03T12:26:07.753Z"}

您还会观察到your application's logs将开始出现在AppSignal中的日志仪表板中:

Application logs in AppSignal

从这里,您可以执行各种操作来监视和对应用程序进行故障。

现在前往http://localhost:3000查看应用程序中的应用程序!

检查项目结构

URL Shortener应用程序由几个文件和目录组成,如下所示:

.
├── package.json
├── package-lock.json
├── README.md
└── src
    ├── app.js
    ├── appsignal.js
    ├── config
    │   ├── config.js
    │   └── logger.js
    ├── controllers
    │   ├── root.controller.js
    │   └── url.controller.js
    ├── index.js
    ├── middleware
    │   ├── error.js
    │   ├── requestLogger.js
    │   └── validator.js
    ├── models
    │   ├── plugins
    │   │   └── toJSON.plugin.js
    │   └── url.model.js
    ├── public
    │   ├── main.js
    │   └── style.css
    ├── routes
    │   └── routes.js
    ├── schemas
    │   └── url.schema.js
    └── views
        ├── default.pug
        └── home.pug

这是对最重要的简要说明:

  • src/index.js:应用程序的入口点。
  • src/app.js:配置Express应用的位置。
  • src/appsignal.js:初始化应用程序信号集成。
  • src/public/:包含静态CSS和JavaScript文件。
  • src/views/:包含应用程序的哈巴狗模板。
  • src/routes/routes.js:配置应用程序路由。
  • src/config/:包含Winston Logger和环境变量的配置。
  • src/models/:具有词汇的架构定义。
  • src/controllers/:包括应用程序路由的业务逻辑。
  • src/middleware/:包含一些有用的中间件功能。

当我们迁移应用程序以快速迁移时,我们将检查大多数这些文件的内容。

在下一部分中,我们将在项目中安装快速fastify,并创建一个新的快速应用程序,该应用程序嵌入了原始Express应用以启动我们的增量迁移。

从Express迁移到快速

正如本系列的part 2中提到的那样,使用@fastify/express插件是使现有Express应用程序与Fastify一起使用的最快方法。该插件添加了完整的Express兼容性,以便您可以轻松地使用任何Express中间件 - 甚至在您的Fastify实例中使用整个Express应用程序,并且它将不需要任何更改。

继续,将fastify@fastify/express软件包安装到您的项目中:

npm install fastify @fastify/express

一旦安装了它们,请在app.js文件的开头导入它们:

// app.js
import Fastify from "fastify";
import fastifyExpress from "@fastify/express";

之后,在文件底部附近创建一个新的Fastify应用程序实例,然后注册fastifyExpress插件。另外,在fastify实例上注册expressApp,然后将文件导出从expressApp更改为fastifyApp

// app.js
. . .
expressApp.use(expressErrorHandler());
expressApp.use(errorHandler);

const fastifyApp = Fastify();

await fastifyApp.register(fastifyExpress);
fastifyApp.use(expressApp);

export default fastifyApp;

此时,app.js公开了一个快速应用程序,该应用程序嵌入了具有自己的路由,中间件和插件的功能齐全的Express应用程序。

在测试更改之前,您还需要修改index.js文件,如下所示。您只需要更改在配置端口上聆听连接的代码的一部分:

//app.js
. . .

  logger.info('Connected to MongoDB');

  const address = await app.listen({ port: config.port });
  logger.info(
    `URL Shortener is running in ${config.env} mode → PORT ${address}`
  );

. . .

有了这些更改,您的申请应像以前一样继续工作。

fastify现在被用于启动服务器,但是整个服务功能仍保留在Express应用程序中。在下一部分中,我们将开始迁移我们的路线以快速进行。

迁移快速路线

Express Router当前执行我们的应用程序路由,但是我们将开始迁移以在本节中快速进行的路由。打开您的routes/routes.js文件并添加以下代码:

// routes/routes.js
. . .
export async function fastifyRoutes(fastify) {
}

export default router;

fastifyRoutes函数代表一种新的插件上下文,该上下文将包含我们的应用程序路由。要迁移快速的快速路线,您需要:

  1. express.Router实例中删除路线。
  2. 在插件函数中的fastify实例上注册路由。
  3. 更新路线本身,以使其符合快速API。

让我们按照上述步骤进行/路线,看看我们走了多远。

首先,在routes.js文件中删除以下代码:

// routes/routes.js
router.get("/", rootController.render);

接下来,在fastifyRoutes中注册/路线如下:

// routes/routes.js
export async function fastifyRoutes(fastify) {
  fastify.get("/", rootController.render);
}

之后,打开controllers/root.controller.js并检查其内容:

// controller/root.controller.js
function render(req, res) {
  res.render("home");
}

export default { render };

此路由渲染src/views/home.pug文件,并将结果的HTML字符串返回到客户端。在app.js文件中,Express配置为通过以下代码行与PUG模板一起使用:

// app.js
expressApp.set("view engine", "pug");
expressApp.set("views", path.join(__dirname, "views"));

由于我们已经迁移了呈现模板的唯一应用程序路由,因此不再需要上述行,并且可以安全删除。但是您现在需要配置Fastify,以通过@fastify/view插件渲染PUG模板。它用一种方法将Reply接口装饰,然后将其用于渲染模板。

继续安装插件:

npm install @fastify/view

然后将其导入下面显示的app.js文件。您还需要导入pug

// app.js
. . .
import fastifyView from '@fastify/view';
import pug from 'pug';

fastifyApp声明下方,添加以下行以注册fastifyView插件:

// app.js
. . .
const fastifyApp = Fastify();

await fastifyApp.register(fastifyView, {
  engine: {
    pug,
  },
  root: path.join(__dirname, 'views'),
  propertyName: 'render',
  viewExt: 'pug',
});
. . .

此时,您的快速应用程序现在可以使用PUG模板。您只需要在fastifyApp实例中注册fastifyRoutes插件,如下所示:

// app.js
import routes, { fastifyRoutes } from './routes/routes.js';

. . .

await fastifyApp.register(fastifyRoutes);

您还可以如下更新rootController.render方法,以使其符合Fastify的命名约定:

function render(req, reply) {
  reply.render("home");
}

export default { render };

随着上述更改,应用程序应
继续像以前一样工作。 /路线是通过Fastify处理的,而其他两条路线仍通过Express路线。通过这种方式,您最终可以迁移所有快速路线以快速进行。

迁移其余的快速路线

在本节中,我们将迁移剩余的两个快速路由以快速进行。首先更新routes.js文件如下:

// routes/routes.js
import urlSchema from "../schemas/url.schema.js";
import urlController from "../controllers/url.controller.js";
import rootController from "../controllers/root.controller.js";

export default async function fastifyRoutes(fastify) {
  fastify.get("/", rootController.render);

  fastify.post(
    "/shorten",
    {
      schema: {
        body: urlSchema,
      },
    },
    urlController.shorten
  );

  fastify.get("/:shortID", urlController.redirect);
}

由于Fastify支持Ajv的模式验证,因此在/shorten路线上不再需要validate模块,我们可以直接在路线上指定JSON模式。这两种路线的控制器都将在很大程度上保持不变,除了res参数被重命名为reply

// controllers/url.controller.js
async function shorten(req, reply) {
  const { shortID, destination } = await Url.findOrCreate(req.body.destination);

  logger.debug(`${destination} shortened to ${shortID}`, {
    shortID,
    destination,
  });

  reply.send({ shortID, destination });
}

async function redirect(req, reply) {
  const { shortID } = req.params;

  const { destination } = await Url.findOne({ shortID });

  logger.debug(`redirecting /${shortID} to ${destination}`, {
    shortID,
    destination,
  });

  reply.redirect(destination);
}

export default { shorten, redirect };

最后,回到app.js文件并修复routes导入,因为fastifyRoutes现在是默认导出:

// app.js
import routes from "./routes/routes.js";

现在,我们所有的快速路线都已迁移以快速化。您可以安全删除这些行:

// app.js
expressApp.use(express.json());
expressApp.use(express.urlencoded({ extended: true }));

. . .
expressApp.use(routes);

然后用routes替换对fastifyRoutes的引用,如下所示:

// app.js
await fastifyApp.register(routes);

在此阶段,您可以再次测试应用程序,以确认其工作正常。

现在,我们已经成功地移动了所有路线以毫无麻烦。在下一节中,我们将把静态文件的处理从Express转移到快速。

使用快速提供静态文件

目前,根据app.js中的这一行,express.static中间件正在处理静态JavaScript和CSS文件(位于/src/public):

// app.js
expressApp.use("/static", express.static(path.join(__dirname, "public")));

我们可以用@fastify/static插件替换此中间件,您可以通过下面的命令安装:

npm install @fastify/static

安装后,将其导入到文件中,然后在fastifyApp实例上注册:

// app.js
import fastifyStatic from '@fastify/static';

. . .

await fastifyApp.register(fastifyStatic, {
  root: path.join(__dirname, 'public'),
  prefix: '/static/',
});

然后删除对express.static的引用,一切都应该保持与以前相同的工作(现在快速处理静态文件除外)。

用快速插件代替快速中间件

我们几乎已经完成了迁移以快速进行,但是在删除@fastify/express兼容层之前,我们仍然有一些快速的位数要替换。在本节中,我们将用相应的Fastify alternatives替换以下中间件:

通过以下命令安装插件:

npm install @fastify/helmet @fastify/cors @fastify/compress

在您的app.js文件中,删除以下行:

// app.js
import helmet from 'helmet';
import cors from 'cors';
import compression from 'compression';

. . .

expressApp.use(helmet());
expressApp.use(cors());
expressApp.use(compression());

然后在适当位置添加以下行:

// app.js
. . .
import fastifyCors from '@fastify/cors';
import fastifyHelmet from '@fastify/helmet';
import fastifyCompress from '@fastify/compress';

. . .

await fastifyApp.register(fastifyCors);
await fastifyApp.register(fastifyHelmet);
await fastifyApp.register(fastifyCompress);
. . .

您可以通过向应用程序根请求并通过浏览器DevTools检查响应标题来确认这些工作。

Confirming that Fastify Plugins work

在这一点上,我们已经用快速插件替换了大多数Express中间件,而不会影响应用程序功能。如果您无法找到快速中间件的快速替代品怎么办?您可以使用techniques discussed in part 2 of this series继续使用
中间件即使应用程序的其余部分迁移以快速进行。

在下一部分中,我们将注意力转向错误处理中间件。

fastify中的错误处理

在Express中,通过添加到应用程序中间件堆栈中的中间件功能来完成错误处理。这些函数具有附加的err参数,表示正在处理的错误,当中间软件堆栈中的任何位置发生错误或将其扔到路由处理程序中时,它们就会被调用。我们的Express应用程序的错误处理程序位于middleware/error.js中:

// middleware/error.js
import { ValidationError } from "express-json-validator-middleware";
import logger from "../config/logger.js";

const errorHandler = (err, req, res, next) => {
  let statusCode = 500;
  let message = "internal server error";

  if (err instanceof ValidationError) {
    statusCode = 400;
    message = "validation error";
  } else {
    logger.error(err);
  }

  const response = {
    code: statusCode,
    message,
  };

  res.status(statusCode).send(response);
};

export default errorHandler;

我们还安装并导入express-async-errors软件包,以便该中间件捕获异步错误而不是崩溃程序。

最后,我们使用AppSignal的expressErrorHandler中间件自动跟踪意外错误:

// app.js
. . .
import 'express-async-errors';
import { expressErrorHandler } from '@appsignal/nodejs';
import errorHandler from './middleware/error.js';

. . .

expressApp.use(expressErrorHandler());
expressApp.use(errorHandler);

让我们开始迁移我们的错误处理以通过删除app.js文件中的上述行进行快速进行操作。默认情况下,Fastify会自动从同步路由和异步路由中捕获错误,因此不需要替换express-async-errors的插件。您可以通过在Fastify实例上的setErrorHandler()方法提供自定义错误处理程序,从而自定义Fastify中的默认错误处理行为。提供的处理程序需要具有以下签名:

function (error, request, reply) {}

让我们修改我们现有的Express错误处理程序中间件,以便可以处理快速错误:

// middleware/error.js
import logger from "../config/logger.js";
import { sendError } from "@appsignal/nodejs";

function errorHandler(err, req, reply) {
  let statusCode = 500;
  let message = "internal server error";

  if (err.code === "FST_ERR_VALIDATION") {
    statusCode = 400;
    message = "validation error";
    logger.info(err);
  } else {
    sendError(err);
    logger.error(err);
  }

  const response = {
    code: statusCode,
    message,
  };

  reply.code(statusCode).send(response);
}

export default errorHandler;

代码几乎保持不变,只是修改了函数签名,而现在对架构验证错误的检测到不同。另外,sendError()方法替换了expressErrorHandler作为跟踪Appsignal中意外应用错误的方法。

您现在可以使用setErrorHandler()方法将此功能设置为路由的错误处理程序:

// routes/routes.js
. . .
import errorHandler from '../middleware/error.js';

export default async function fastifyRoutes(fastify) {
  . . .

  fastify.setErrorHandler(errorHandler);
}

在这一点上,您提供的自定义errorHandler将捕获和处理路线中发生的任何错误。它们还将在AppSignal中跟踪,以便您可以快速通知您的Node.js应用程序中发生的任何问题。

Node.js errors in AppSignal

您可以了解有关error handling in Fastify's documentationAppSignal's error tracking page的更多信息。

用Pino代替Winston Logging

我们的Express应用程序使用WinstonMorgan来记录HTTP请求。

fastify默认情况下随附Pino,但必须启用记录才能工作:

// app.js
const fastifyApp = Fastify({
  logger: true,
});

启用快速记录时,您会注意到将请求发送到服务器时,Pino日志与Winston日志一起出现:

{"level":30,"time":1683531253228,"pid":782352,"hostname":"fedora","reqId":"req-2","res":{"statusCode":304},"responseTime":6.965402990579605,"msg":"request completed"}
{"level":30,"time":1683531253229,"pid":782352,"hostname":"fedora","reqId":"req-3","res":{"statusCode":304},"responseTime":4.585820972919464,"msg":"request completed"}
{"content_length":"0","level":"info","message":"GET /static/main.js 304 0 - 2.775 ms","method":"GET","path":"/static/main.js","response_time_ms":"2.775","status_code":304,"timestamp":"2023-05-08T07:34:13.229Z","user_agent":"Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0"}

此时,您可以从Express应用程序中删除requestLogger,以便仅保留快速日志。继续并从app.js文件中删除以下代码行:

// app.js
import requestLogger from "./middleware/requestLogger.js";

expressApp.use(requestLogger);

在此阶段,我们已经将Express应用程序完全迁移到Fastify,因此您也可以删除express@fastify/express软件包:

// app.js

import express from 'express';
import fastifyExpress from '@fastify/express';
. . .

const expressApp = express();

. . .
await fastifyApp.register(fastifyExpress);
fastifyApp.use(expressApp);

前往您的config/logger.js文件,然后用pino替换为pino,如下所示:

// config/logger.js
import pino from "pino";
import config from "./config.js";

const logger = pino({
  level: config.logLevel,
  formatters: {
    bindings: (bindings) => {
      return { pid: bindings.pid, host: bindings.hostname };
    },

    level: (label) => {
      return { level: label };
    },
  },
  timestamp: pino.stdTimeFunctions.isoTime,
});

export default logger;

之后,用自定义Pino Logger替换Fastify的默认记录器实例:

// app.js
import logger from './config/logger.js';

. . .

const fastifyApp = Fastify({
  logger: logger,
});

此时,所有应用程序日志现在都是通过同一Pino实例生成的,因此日志格式应保持一致:

{"level":"info","time":"2023-05-08T08:09:43.047Z","pid":920874,"host":"fedora","reqId":"req-1","req":{"method":"POST","url":"/shorten","hostname":"localhost:3000","remoteAddress":"127.0.0.1","remotePort":60582},"msg":"incoming request"}
{"level":"debug","time":"2023-05-08T08:09:43.058Z","pid":920874,"host":"fedora","msg":"https://github.com/nvim-telescope/telescope.nvim#themes shortened to mB5Ke86"}
{"level":"info","time":"2023-05-08T08:09:43.060Z","pid":920874,"host":"fedora","reqId":"req-1","res":{"statusCode":200},"responseTime":12.570792019367218,"msg":"request completed"}

请注意,Fastify使其Logger在ServerRequest类型上可用,因此您可以直接访问它而无需从config/logger.js导入。例如:

// controllers/url.controller.js
import Url from "../models/url.model.js";

async function shorten(req, reply) {
  const { shortID, destination } = await Url.findOrCreate(req.body.destination);

  req.log.debug(`${destination} shortened to ${shortID}`);

  reply.send({ shortID, destination });
}

async function redirect(req, reply) {
  const { shortID } = req.params;

  const { destination } = await Url.findOne({ shortID });

  req.log.debug(`redirecting /${shortID} to ${destination}`);

  reply.redirect(destination);
}

export default { shorten, redirect };

了解有关logging in Fastifyhow to customize the Pino logger的更多信息。

测试您的应用程序

我们已经成功地完成了从Express到Fastify的迁移!现在,您可以通过下面的命令从项目中删除所有与明确相关的软件包:

npm uninstall express body-parser express-async-errors helmet winston cors @fastify/express express-json-validator-middleware ajv-formats compression morgan

您还可以删除不再需要的一些文件:

rm src/middleware/validator.js src/middleware/requestLogger.js

之后,请确保您彻底测试应用程序,以确认其正常工作,理想情况下是通过先前设置的自动测试。但是,由于我们的应用程序很小,而且我们没有任何自动测试,因此手动测试就足够了:

Fastify application

包起来

感谢您与本系列有关从Express迁移到快速迁移的系列。我们已经涵盖了很多基础,希望您发现它有助于确定是否要切换到快速进行迁移过程。

Fastify提供了我们在本系列中无法涵盖的许多其他功能,因此请查看Fastify official documentation以了解更多信息。另外,您可以找到我们在this GitHub repository中使用的所有代码样本。

我希望这个系列为您提供了知识和技能,以开始使用快速构建表演和可扩展的Web应用程序。如果您有任何疑问或反馈,请不要犹豫。

直到下一次,愉快的编码!

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

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