高級 Fastify:鉤子、中間件和裝飾器(譯文-來自: AppSignal)

閃念基因 發佈 2023-06-11T18:09:50.387277+00:00

在這一部分,我們將深入探討 Fastify 的一些更高級的概念。具體來說,我們將演示如何使用掛鈎、中間件、裝飾器和驗證來構建更健壯的 Web 應用程式。我們還將簡要介紹一些將現有 Express 應用程式遷移到 Fastify 的可行策略。讓我們開始吧!

在這一部分,我們將深入探討 Fastify 的一些更高級的概念。具體來說,我們將演示如何使用掛鈎、中間件、裝飾器和驗證來構建更健壯的 Web 應用程式。我們還將簡要介紹一些將現有 Express 應用程式遷移到 Fastify 的可行策略。

讓我們開始吧!

Fastify 中的鉤子和中間件

Fastify 中的鉤子允許您監聽應用程式生命周期中的特定事件,並在這些事件發生時執行特定任務。Hooks 類似於 Express 風格的中間件功能,但性能更高,允許您執行身份驗證、日誌記錄、路由、數據解析、錯誤處理等任務。Fastify 中有兩種類型的鉤子: 請求/回復鉤子 和 應用程式鉤子。

Fastify 中的請求/回復鉤子

請求/回復掛鈎在伺服器的請求/回復周期中執行,允許您在處理傳入請求或傳出響應之前或之後執行各種任務。這些掛鈎可以應用於所有路線或選擇的路線。它們通常用於實現以下功能:

  • 訪問控制
  • 數據驗證
  • 請求記錄

和更多。

請求/回復掛鈎的示例包括prerequestonSendonTimeout和其他。

下面是一個使用鉤子為從伺服器發送的所有響應onSend添加標頭的示例:Server

javascript

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

和對象大家應該很熟悉了,參數 request就是響應體。您可以在此處修改響應有效負載,或者通過在掛鈎函數中將其設置為來將其完全清除。最後,應該執行回調以表示掛鈎結束,以便請求/回復生命周期繼續。它最多可以接受兩個參數:一個錯誤(如果有的話),或,以及更新後的有效負載。replypayloadnulldone()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如果你想為路由的子集實現鉤子,你需要創建一個新的封裝上下文,然後在插件中的實例上註冊鉤子。例如,您可以在本教程前面演示的插件onSend中創建另一個掛鈎:health

javascript

// 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;

這一次,onSend鉤子用於修改插件上下文中所有路由的響應主體(/health在本例中只是 ),方法是更改up​為 down,並且它還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。如果你想在特定路由而不是插件上下文中的所有路由上註冊一個鉤子,你可以將它添加到路由選項中,如下所示:

javascript

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如下所示。儘管它們在具有相同處理程序的相同插件上下文中,但它們響應的內容是不同的。

獲取/健康

文本

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"}

獲取/健康/始終向上

文本

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實例發出的事件執行,可用於執行一般伺服器或插件任務,例如:

  • 連接到資料庫和其他資源
  • 加載或卸載配置
  • 關閉連接
  • 持久化數據
  • 刷新日誌

和別的。

下面是一個使用 Fastify 的應用程式鉤子模擬正常關閉的示例onClose 。如果您正在部署更新或您的伺服器因其他原因需要重啟,這樣的設置是理想的。

javascript

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回調函數,回調函數應該在鉤子完成時調用。

↓文章在下面繼續

您的應用程式損壞或運行緩慢嗎?AppSignal 讓您知道。

通過 AppSignal 監控 →

此外,該process.on()函數還監聽在您按下或系統關閉SIGINT 時發送給進程的信號。CTRL+C收到信號後,fastify.close()將調用該函數關閉伺服器,並將伺服器關閉的記錄記錄到控制台。

將上述代碼添加到您的程序後,啟動伺服器並按Ctrl-C 關閉進程。您將在控制台中觀察到以下日誌:

JSON

{"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 中的中間件

Fastify 也支持 Express 風格的中間件,但它需要你安裝一個外部插件,比如 @fastify/express或 @fastify/middie。這簡化了從 Express 到 Fastify 的遷移,但不應該在新項目中使用它來支持 hooks。請注意,在許多情況下,您可以找到提供與 Express 中間件相同功能的原生 Fastify 插件。

下面的示例演示了如何在 Fastify 中使用標準的 Express 中間件,例如 cookie-parser ,但您應該儘可能使用原生替代品——例如 @fastify/cookie——因為它們針對 Fastify 的使用進行了更好的優化。

javascript

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開始使用 Express 中間件 ,它應該如上所示工作。請注意,以這種方式應用的每個中間件都將在掛鈎階段執行。use()FastifyonRequest

Fastify 中的裝飾器

裝飾器是一種允許您使用任何類型的 JavaScript 屬性(例如函數、對象或任何原始類型)自定義核心對象的功能。例如,您可以使用裝飾器來存儲自定義數據或分別通過FastifyRequest、和方法在、或Reply對象中註冊一個新方法。decorate()decorateRequest()decorateReply()

這個例子演示了如何使用 Fastify 的裝飾器來為 Web 應用程式添加功能:

javascript

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 無效或丟失,則會拋出錯誤。否則,cookie 值用於通過函數檢索相應的用戶getUser(),並將結果存儲在 user請求對象的屬性中。如果在檢索用戶時發生錯誤,則會記錄並重新拋出錯誤。

雖然您可以向 、 或 對象添加任何屬性FastifyRequestReply您需要提前使用裝飾器聲明它們。這有助於底層 JavaScript 引擎優化對這些對象的處理。

curl --cookie "user_token=yes.Y7pzW5FUVuoPD5yXLV8joDdR35gNiZJzITWeURHF5Tg" http://127.0.0.1:3000/

文本

Hello John Doe!

Fastify 中的數據驗證

數據驗證是任何依賴客戶端數據的 Web 應用程式的基本功能,因為它有助於防止惡意負載引起的安全漏洞並提高應用程式的可靠性和健壯性。

Fastify 使用JSON schema為每個路由的輸入負載定義驗證規則,包括請求主體、查詢字符串、參數和標頭。JSON schema 是定義 JSON 數據結構和約束的標準格式,Fastify 使用 Ajv,這是可用的最快、最高效的 JSON schema 驗證器之一。

要在 Fastify 中使用 JSON 驗證,您需要為每個需要負載的路由定義一個模式。您可以使用標準的 JSON 模式格式或 Fastify JSON 模式格式指定模式,這是表達模式的更簡潔和表達方式。

下面是一個如何在 Fastify 中為路由定義 JSON schema 的例子:

javascript

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,
    });
  }
);

在此示例中,我們定義了一個模式,該模式需要一個具有三個屬性的對象:nameageemail。該name屬性應為字符串,age屬性應為數字,email屬性應為電子郵件格式的字符串。我們還指定 和nameemail必需的屬性。

當客戶端發送/users帶有無效負載的 POST 請求時,Fastify 會自動返回一個「400 Bad Request」響應,其中包含一條錯誤消息,指示哪個屬性未通過驗證。但是,如果有效載荷符合模式,則路由處理函數將使用解析後的有效載荷作為request.body.

這是一個帶有無效負載的請求示例(email密鑰格式錯誤):

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

這是 Fastify 產生的響應:

JSON

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

請參閱文檔以了解有關 Fastify 中的驗證以及如何自定義其行為以滿足您的需求的更多信息。

計劃從 Express 到 Fastify 的增量遷移

對於那些想要切換到新框架但又無力一次性進行所有更改的人來說,增量遷移是一種極好的策略。通過採用增量方法,您可以在仍然使用 Express 的同時逐步將 Fastify 引入您的項目,從而讓您有時間進行任何必要的更改並確保平穩過渡。

第一步是確定項目中最能從 Fastify 的功能中獲益的部分,例如它對驗證和日誌記錄的內置支持,以及比 express 更高的性能。一旦你確定了這些區域,你就可以在現有的 Express 代碼中引入 Fastify。

這可能涉及設置一個單獨的伺服器實例,該實例使用 Fastify 處理某些路由或端點,同時仍然為應用程式的其餘部分使用 Express。但是你可能會發現使用 @fastify/express插件為 Fastify 添加完整的 Express 兼容性更容易,這樣你就可以像使用 Fastify 插件一樣使用 Express 中間件和應用程式。

要使用該@fastify/express插件,您可以通過以下方式安裝它npm並在您的 Fastify 實例中註冊它:

javascript

import Fastify from "fastify";
import expressPlugin from "@fastify/express";
 
const fastify = Fastify({
  logger: true,
});
 
await fastify.register(expressPlugin);

然後,您可以像在 Express 應用程式中一樣使用 Express 中間件或應用程式。例如:

javascript

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

JSON

{ "body": "hello from express" }

隨著您對 Fastify 越來越熟悉,您可以開始遷移越來越多的代碼,最終用 Fastify 替換所有特定於 Express 的代碼。通過花時間計劃和執行深思熟慮的遷移策略,您可以確保順利過渡並最終獲得更高效、高性能的應用程式。

下一步:從 Express 遷移到 Fastify

我們在本教程中介紹了很多內容。我們希望您已經更深入地了解 Fastify 的新穎功能(如鉤子和裝飾器)如何比 Express 更能幫助您定製和擴展您的應用程式。

在本系列的下一部分和最後一部分中,我們將提供從 Express 遷移到 Fastify 的實用指南。我們將涵蓋常見的遷移場景,提供優化性能和提高安全性的技巧,並在此過程中分享一些最佳實踐。

感謝閱讀,我們下次再見!

作者:Damilola Olatunji

Damilola is a freelance technical writer and software developer based in Lagos, Nigeria. He specializes in JavaScript and Node.js, and aims to deliver concise and practical articles for developers. When not writing or coding, he enjoys reading, playing games, and traveling.

出處:https://blog.appsignal.com/2023/05/24/advanced-fastify-hooks-middleware-and-decorators.html

關鍵字: