Lorsque vous entendez l'expression « Stockage local asynchrone », qu'est-ce qui vous vient à l'esprit ? Au début, vous pourriez penser qu’il s’agit d’une implémentation magique du stockage local basé sur un navigateur. Cependant, cette hypothèse est incorrecte. Le stockage local asynchrone n'est ni lié au navigateur ni un mécanisme de stockage typique. Il est probable qu'une ou deux bibliothèques que vous avez utilisées l'utilisent sous le capot. Dans de nombreux cas, cette fonctionnalité peut vous éviter de gérer un code compliqué.
Async Local Storage est une fonctionnalité introduite dans Node.js, initialement ajoutée dans les versions v13.10.0 et v12.17.0, puis stabilisée dans la v16.4.0. Il fait partie du module async_hooks, qui fournit un moyen de suivre les ressources asynchrones dans les applications Node.js. La fonctionnalité permet la création d'un contexte partagé auquel plusieurs fonctions asynchrones peuvent accéder sans le transmettre explicitement. Le contexte est disponible dans chaque (et unique) opération exécutée dans le rappel transmis à la méthode run() de l'instance AsyncLocalStorage.
Avant de plonger dans les exemples, expliquons le modèle que nous allons utiliser.
Initialisation
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
Dans le module ci-dessus, nous initialisons une instance de AsyncLocalStorage et l'exportons sous forme de variable.
Utilisation
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)
La méthode run() prend deux arguments : storage, qui contient les données que nous souhaitons partager, et callback, où nous plaçons notre logique. En conséquence, le stockage devient accessible à chaque appel de fonction dans le rappel, permettant un partage transparent des données entre les opérations asynchrones.
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
Pour accéder au contexte, nous importons notre instance et appelons la méthode asyncLocalStorage.getStore(). Ce qui est bien, c'est que le stockage récupéré depuis getStore() est typé car nous avons transmis le type Context à AsyncLocalStorage lors de l'initialisation : new AsyncLocalStorage
Il n'y a pas d'application web sans système d'authentification. Nous devons valider les jetons d'authentification et extraire les informations utilisateur. Une fois que nous avons obtenu l'identité de l'utilisateur, nous souhaitons la rendre disponible dans les gestionnaires de routes et éviter de dupliquer le code dans chacun d'eux. Voyons comment nous pouvons utiliser AsyncLocalStorage pour implémenter un contexte d'authentification tout en gardant notre code propre.
J'ai choisi fastify pour cet exemple.
Selon la documentation, fastify est :
Framework Web rapide et faible, pour Node.js
Ok, commençons :
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
Vient maintenant la partie très importante. Nous allons ajouter un hook onRequest pour envelopper les gestionnaires avec la méthode authAsyncLocalStorage.run().
type Context = Map<"userId", string>;
Après une validation réussie, nous appelons la méthode run() depuis notre authAsyncLocalStorage. Comme argument de stockage, nous transmettons le contexte d'authentification avec l'ID utilisateur récupéré du jeton. Dans le rappel, nous appelons la fonction done pour continuer le cycle de vie Fastify.
Si nous avons des contrôles d'authentification qui nécessitent des opérations asynchrones, nous devons les ajouter au rappel. En effet, selon la documentation :
le rappel terminé n'est pas disponible lors de l'utilisation de async/await ou du renvoi d'une promesse. Si vous invoquez un rappel effectué dans cette situation, un comportement inattendu peut se produire, par ex. appel en double des gestionnaires
Voici un exemple de ce à quoi cela pourrait ressembler :
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
Notre exemple ne comporte qu'un seul itinéraire protégé. Dans des scénarios plus complexes, vous devrez peut-être encapsuler uniquement des routes spécifiques avec le contexte d'authentification. Dans de tels cas, vous pouvez soit :
Très bien, notre contexte est posé et nous pouvons désormais définir un itinéraire protégé :
import Fastify from "fastify"; /* other code... */ const app = Fastify(); function sendUnauthorized(reply: FastifyReply, message: string) { reply.code(401).send({ error: `Unauthorized: ${message}` }); } /* other code... */
Le code est assez simple. Nous importons authAsyncLocalStorage, récupérons l'userId, initialisons UserRepository et récupérons les données. Cette approche permet au gestionnaire d'itinéraire de rester propre et concentré.
Dans cet exemple, nous allons réimplémenter l'assistant de cookies de Next.js. Mais attendez, c'est un article sur AsyncLocalStorage, n'est-ce pas ? Alors pourquoi parle-t-on de cookies ? La réponse est simple : Next.js utilise AsyncLocalStorage pour gérer les cookies sur le serveur. C'est pourquoi lire un cookie dans un composant serveur est aussi simple que :
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... */
Nous utilisons la fonction cookies exportée depuis next/headers, qui propose plusieurs méthodes de gestion des cookies. Mais comment est-ce techniquement possible ?
Tout d'abord, je tiens à mentionner que cet exemple est basé sur les connaissances que j'ai acquises grâce à une superbe vidéo, de Lee Robinson et en plongeant dans le référentiel Next.js.
Dans cet exemple, nous utiliserons Hono comme framework de serveur. Je l'ai choisi pour deux raisons :
Première installation de Hono :
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const asyncLocalStorage = new AsyncLocalStorage<Context>(); // export const authAsyncLocalStorage = new AuthAsyncLocalStorage<AuthContext>()
Maintenant, initialisez Hono et ajoutez un middleware :
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)
Le code ressemble au middleware de l'exemple Fastify, n'est-ce pas ? Pour définir le contexte, nous utilisons setCookieContext, qui est importé du module cookies - notre implémentation simple et personnalisée de la fonction cookies. Suivons la fonction setCookieContext et naviguons jusqu'au module à partir duquel elle a été importée :
async function collectUsersData() { const context = asyncLocalStorage.getStore(); }
La fonction setCookieContext (dont nous avons transmis la valeur de retour à cookieAsyncLocalStorage.run() dans le middleware Hono) extrait les cookies du paramètre c, qui représente le contexte hono, et les regroupe avec des fermetures qui fournissent des fonctions utilitaires pour gérer les cookies.
Notre fonction cookies reproduit la fonctionnalité des cookies de next/headers. Il utilise la méthode cookieAsyncLocalStorage.getStore() pour accéder au même contexte qui est transmis à cookieAsyncLocalStorage.run() lorsqu'il est appelé.
Nous avons enveloppé le retour de notre fonction cookies dans une promesse d'imiter le comportement de l'implémentation Next.js. Avant la version 15, cette fonction était synchrone. Désormais, dans le code Next.js actuel, les méthodes renvoyées par les cookies sont attachées à un objet de promesse, comme le montre l'exemple simplifié suivant :
npm install fastify
Un autre point à mentionner est que dans notre cas, l'utilisation de cookies.setCookie et cookies.deleteCookie génère toujours une erreur, similaire au comportement observé dans Next.js lors de la définition de cookies dans un composant serveur. Nous avons codé en dur cette logique car, dans l'implémentation d'origine, la possibilité d'utiliser setCookie ou deleteCookie dépend de la propriété phase(WorkUnitPhase) stockée dans le stockage appelé RequestStore (il s'agit de l'implémentation d'AsyncLocalStorage et stocke également les cookies). Cependant, ce sujet conviendrait mieux à un autre article. Pour garder cet exemple simple, omettons la simulation de WorkUnitPhase.
Nous devons maintenant ajouter notre code React.
type Context = Map<"userId", string>;
import { AsyncLocalStorage } from "async_hooks"; import { Context } from "./types"; export const authAsyncLocalStorage = new AsyncLocalStorage<Context>();
L'utilisation des cookies est similaire à celle utilisée dans les composants du serveur 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... */
Notre modèle est rendu par la méthode html à partir du contexte hono. Le point clé ici est que le gestionnaire de route s'exécute dans la méthode asyncLocalStorage.run(), qui prend cookieContext. De ce fait, nous pouvons accéder à ce contexte dans le composant DisplayCookies via la fonction cookies.
Il n'est pas possible de définir des cookies dans les composants du serveur React, nous devons donc le faire manuellement :
Actualisons une page :
Et voilà, nos cookies sont récupérés et affichés avec succès.
Il existe de nombreux autres cas d'utilisation pour asyncLocalStorage. Cette fonctionnalité vous permet de créer des contextes personnalisés dans presque n'importe quelle infrastructure de serveur. Le contexte asyncLocalStorage est encapsulé dans l'exécution de la méthode run(), ce qui facilite sa gestion. Il est parfait pour gérer des scénarios basés sur des requêtes. L'API est simple et flexible, permettant l'évolutivité en créant des instances pour chaque état. Il est possible de gérer de manière transparente des contextes séparés pour des éléments tels que l'authentification, la journalisation et les indicateurs de fonctionnalités.
Malgré ses avantages, il y a quelques considérations à garder à l’esprit. J'ai entendu des opinions selon lesquelles asyncLocalStorage introduit trop de « magie » dans le code. J'avoue que lorsque j'ai utilisé cette fonctionnalité pour la première fois, il m'a fallu un certain temps pour bien comprendre le concept. Une autre chose à considérer est que l’importation du contexte dans un module crée une nouvelle dépendance que vous devrez gérer. Cependant, en fin de compte, transmettre des valeurs via des appels de fonctions profondément imbriqués est bien pire.
Merci d'avoir lu et à bientôt dans le prochain post !?
PS : Vous pouvez retrouver les exemples (plus un bonus) ici
Source du message Bog : https://www.aboutjs.dev/en/async-local-storage-is-here-to-help-you
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!