「非同期ローカル ストレージ」というフレーズを聞くと、何を思い浮かべますか?最初は、ブラウザベースのローカル ストレージの魔法のような実装を指していると思うかもしれません。しかし、この仮定は正しくありません。非同期ローカル ストレージは、ブラウザー関連でも一般的なストレージ メカニズムでもありません。おそらく、あなたが使用した 1 つまたは 2 つのライブラリが内部で使用されていると思われます。多くの場合、この機能により、煩雑なコードを扱う必要がなくなります。
非同期ローカル ストレージは Node.js で導入された機能で、最初はバージョン v13.10.0 と v12.17.0 で追加され、その後 v16.4.0 で安定しました。これは async_hooks モジュールの一部であり、Node.js アプリケーションで非同期リソースを追跡する方法を提供します。この機能により、複数の非同期関数が明示的に渡さずにアクセスできる共有コンテキストを作成できます。コンテキストは、AsyncLocalStorage インスタンスの run() メソッドに渡されるコールバック内で実行されるすべての (および唯一の) 操作で使用できます。
例に入る前に、使用するパターンについて説明しましょう。
初期化
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
上記のモジュールでは、AsyncLocalStorage のインスタンスを初期化し、それを変数としてエクスポートします。
使用法
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
run() メソッドは 2 つの引数を取ります。共有するデータを含むストレージと、ロジックを配置するコールバックです。その結果、コールバック内のすべての関数呼び出しでストレージにアクセスできるようになり、非同期操作全体でシームレスなデータ共有が可能になります。
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
コンテキストにアクセスするには、インスタンスをインポートし、asyncLocalStorage.getStore() メソッドを呼び出します。素晴らしい点は、初期化中に Context タイプを AsyncLocalStorage に渡したため、 getStore() から取得したストレージが型指定されていることです: new AsyncLocalStorage
認証システムのない Web アプリケーションは存在しません。認証トークンを検証し、ユーザー情報を抽出する必要があります。ユーザー ID を取得したら、それをルート ハンドラーで利用できるようにして、それぞれのハンドラーでコードが重複するのを避けたいと考えています。 AsyncLocalStorage を利用して、コードをクリーンに保ちながら認証コンテキストを実装する方法を見てみましょう。
この例では fastify を選択しました。
ドキュメントによると、fastify は次のとおりです。
Node.js 用の高速でオーバーヘッドの低い Web フレームワーク
わかりました、始めましょう:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
npm install fastify
ここからが非常に重要な部分です。 authAsyncLocalStorage.run() メソッドでハンドラーをラップするための onRequest フックを追加します。
type Context = Map<"userId", string>;
検証が成功した後、authAsyncLocalStorage から run() メソッドを呼び出します。ストレージ引数として、トークンから取得した userId を含む認証コンテキストを渡します。コールバックでは、done 関数を呼び出して Fastify ライフサイクルを続行します。
非同期操作を必要とする認証チェックがある場合は、それらをコールバックに追加する必要があります。ドキュメントによると、これは次の理由によるものです。
done コールバックは、async/await を使用する場合、または Promise を返す場合には使用できません。この状況で完了コールバックを呼び出すと、予期しない動作が発生する可能性があります。ハンドラーの重複呼び出し
これがどのように見えるかの例を次に示します:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
この例では、保護されたルートが 1 つだけあります。より複雑なシナリオでは、特定のルートのみを認証コンテキストでラップする必要がある場合があります。そのような場合は、次のいずれかを行うことができます:
よし、コンテキストが設定されたので、保護されたルートを定義できるようになりました。
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
コードは非常に簡単です。 authAsyncLocalStorage をインポートし、userId を取得し、UserRepository を初期化し、データを取得します。このアプローチにより、ルート ハンドラーがクリーンかつ集中的に保たれます。
この例では、Next.js から Cookie ヘルパーを再実装します。でもちょっと待ってください。これは AsyncLocalStorage に関する投稿ですよね?では、なぜクッキーについて話しているのでしょうか?答えは簡単です。Next.js は AsyncLocalStorage を使用してサーバー上の Cookie を管理します。そのため、サーバー コンポーネント内の Cookie を読み取るのは次のように簡単です。
import Fastify from "fastify"; import { authAsyncLocalStorage } from "./context"; import { getUserIdFromToken, validateToken } from "./utils"; /* other code... */ app.addHook( "onRequest", (request: FastifyRequest, reply: FastifyReply, done: () => void) => { const accessToken = request.headers.authorization?.split(" ")[1]; const isTokenValid = validateToken(accessToken); if (!isTokenValid) { sendUnauthorized(reply, "Access token is invalid"); } const userId = accessToken ? getUserIdFromToken(accessToken) : null; if (!userId) { sendUnauthorized(reply, "Invalid or expired token"); } authAsyncLocalStorage.run(new Map([["userId", userId]]), async () => { await new Promise((resolve) => setTimeout(resolve, 2000)); sendUnauthorized(reply, "Invalid or expired token"); done(); }); }, ); /* other code... */
私たちは next/headers からエクスポートされた cookie 関数を使用します。この関数は、cookie を管理するためのいくつかの方法を提供します。しかし、これは技術的にどのようにして可能なのでしょうか?
まず、この例は、Lee Robinson による素晴らしいビデオと Next.js リポジトリから得た知識に基づいていることを述べておきたいと思います。
この例では、サーバー フレームワークとして Hono を使用します。これを選んだ理由は 2 つあります:
まず Hono をインストールします:
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
次に、Hono を初期化し、ミドルウェアを追加します。
asyncLocalStorage.run({ userId }, async () => { const usersData: UserData = await collectUsersData(); console.log("usersData", usersData); }); // (method) AsyncLocalStorage<unknown>.run<Promise<void>>(store: unknown, callback: () => Promise<void>): Promise<void> (+1 overload)
このコードは、Fastify サンプルのミドルウェアに似ていますね。コンテキストを設定するには、cookie モジュール (cookie 関数のカスタムの単純な実装) からインポートされる setCookieContext を利用します。 setCookieContext 関数に従って、インポート元のモジュールに移動しましょう:
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
setCookieContext 関数 (その戻り値を Hono ミドルウェアの cookieAsyncLocalStorage.run() に渡しました) は、hono コンテキストを表す c パラメーターから Cookie を抽出し、Cookie を管理するためのユーティリティ関数を提供するクロージャーとそれらをバンドルします。
当社の Cookie 機能は、next/headers の Cookie の機能を複製します。 cookieAsyncLocalStorage.getStore() メソッドを利用して、呼び出し時に cookieAsyncLocalStorage.run() に渡されるのと同じコンテキストにアクセスします。
Next.js 実装の動作を模倣するという約束で、cookie 関数の戻り値をラップしました。バージョン 15 より前では、この関数は同期でした。現在の Next.js コードでは、次の簡略化された例に示すように、Cookie によって返されるメソッドが Promise オブジェクトにアタッチされます。
npm install fastify
もう 1 つ言及する価値がある点は、この場合、cookies.setCookie と cookies.deleteCookie を使用すると、サーバー コンポーネントに Cookie を設定するときに Next.js で観察される動作と同様に、常にエラーがスローされることです。元の実装では、setCookie または deleteCookie を使用できるかどうかは、RequestStore と呼ばれるストレージ (これは AsyncLocalStorage の実装であり、Cookie も保存されます) に保存されているフェーズ (WorkUnitPhase) プロパティに依存するため、このロジックをハードコードしました。ただし、このトピックについては別の投稿に適しています。この例を単純にするために、WorkUnitPhase.
のシミュレーションを省略しましょう。次に、React コードを追加する必要があります。
type Context = Map<"userId", string>;
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
Cookie の使用法は、Next.js React サーバー コンポーネントでの使用方法と似ています。
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
私たちのテンプレートは、hono コンテキストから html メソッドによってレンダリングされます。ここで重要な点は、ルート ハンドラーが cookieContext を受け取る asyncLocalStorage.run() メソッド内で実行されるということです。その結果、Cookie 関数を通じて DisplayCookies コンポーネントのこのコンテキストにアクセスできるようになります。
React サーバー コンポーネント内に Cookie を設定することはできないため、手動で行う必要があります:
ページを更新しましょう:
これで、Cookie が正常に取得され、表示されました。
asyncLocalStorage には他にも多くの使用例があります。この機能を使用すると、ほぼすべてのサーバー フレームワークでカスタム コンテキストを構築できます。 asyncLocalStorage コンテキストは run() メソッドの実行内にカプセル化されるため、管理が容易になります。リクエストベースのシナリオを処理するのに最適です。 API はシンプルかつ柔軟で、状態ごとにインスタンスを作成することでスケーラビリティを実現します。認証、ロギング、機能フラグなどの個別のコンテキストをシームレスに維持できます。
その利点にもかかわらず、留意すべき考慮事項がいくつかあります。 asyncLocalStorage はコードに「魔法」を導入しすぎているという意見を聞いたことがあります。正直に言うと、この機能を初めて使用したとき、その概念を完全に理解するまでに時間がかかりました。もう 1 つ考慮すべき点は、コンテキストをモジュールにインポートすると、管理する必要がある新しい依存関係が作成されることです。ただし、最終的には、深くネストされた関数呼び出しを介して値を渡すことははるかに悪いことです。
読んでいただきありがとうございます。また次の投稿でお会いしましょう?
追記: ここで例 (プラス 1 つのボーナス) を見つけることができます
Bog Post ソース: https://www.aboutjs.dev/en/async-local-storage-is-here-to-help-you
以上が非同期ローカル ストレージが役に立ちますの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。