Bootcamp i backendudvikling med Node.js · Lektion

Kontekstvideregivelse med AsyncLocalStorage

Videregiv request-scoped state på tværs af asynkrone grænser uden prop drilling med AsyncLocalStorage.

Lektion 4 af 413 trin

Kontekstvideregivelse med AsyncLocalStorage er en gratis Bootcamp i backendudvikling med Node.js-lektion på CoddyKit. Dette er lektion 4 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Bootcamp i backendudvikling med Node.js, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.

Problemet med prop-drilling

I en backend-tjeneste er data knyttet til forespørgslen, f.eks. et forespørgsels-id, den godkendte bruger eller et tenant-id, nødvendige dybt nede i dit kaldestak: i repositories, loggere og udgående HTTP-klienter.

Den naive løsning er prop-drilling — at føre et ctx-argument gennem hver funktion:

  • Hver funktionssignatur bliver fyldt med en ctx-parameter.
  • Én glemt videregivelse, og et efterfølgende kald mister konteksten.
  • Bibliotekskode, som du ikke ejer, kan slet ikke modtage din ctx.

Vi har brug for en måde at bære tilstand pr. forespørgsel implicit på, så den overlever hvert await og hver callback. Det er præcis det, AsyncLocalStorage leverer.

async function handler(ctx, req) {
  const user = await loadUser(ctx, req.userId);
  return await renderPage(ctx, user);
}

async function loadUser(ctx, id) {
  log(ctx, 'loading user');         // ctx passed again
  return await db.find(ctx, id);    // and again...
}

// Every layer must accept and forward ctx by hand.

Hvad AsyncLocalStorage er

AsyncLocalStorage findes i det indbyggede modul node:async_hooks. Tænk på det som trådlokal lagring til den asynkrone verden: et lager, der forbliver knyttet til en logisk kæde af asynkrone handlinger.

Du kalder als.run(store, callback) for at etablere et lager, og derefter returnerer als.getStore() det samme lager overalt inde i callbacken — uanset hvor mange awaits, Promise-kæder, setTimeouts eller hændelsesafsendere der ligger imellem.

  • Hver samtidig forespørgsel får sit eget isolerede lager.
  • Ingen globale variabler og ingen kapløbsbetingelser mellem forespørgsler.
  • Drevet af Nodes sporing af asynkron kontekst i baggrunden.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

als.run({ requestId: 'abc-123' }, () => {
  setTimeout(() => {
    const store = als.getStore();
    console.log(store.requestId); // 'abc-123'
  }, 10);
});

run() etablerer en kontekst

Den centrale API er als.run(store, fn, ...args). Den udfører fn synkront, men knytter store til hele det asynkrone træ, der udspringer af den.

  • store kan være en hvilken som helst værdi — oftest et almindeligt objekt eller en Map.
  • run() returnerer det, som fn returnerer (herunder et Promise).
  • Uden for callbacken returnerer getStore() undefined.

Dette kodestykke viser, at lageret overlever et await på en rigtig timer.

import { AsyncLocalStorage } from 'node:async_hooks';
import { setTimeout as sleep } from 'node:timers/promises';

const als = new AsyncLocalStorage();

async function deep() {
  await sleep(5);
  return als.getStore()?.requestId;
}

await als.run({ requestId: 'r-42' }, async () => {
  const id = await deep();
  console.log('inside run:', id);   // 'r-42'
});

console.log('outside run:', als.getStore()); // undefined

Integration i en HTTP-server

Mønsteret i en rigtig server er at pakke arbejdet pr. forespørgsel ind i als.run() helt yderst og initialisere lageret med et nyt forespørgsels-id. Alt længere nede kan derefter læse det.

Nedenfor bruges kun det indbyggede node:http-modul, så der er ingen afhængighed af et framework. Hver indgående forespørgsel får sit eget lager, isoleret fra samtidige forespørgsler.

import http from 'node:http';
import { randomUUID } from 'node:crypto';
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function currentRequestId() {
  return als.getStore()?.requestId ?? 'no-context';
}

const server = http.createServer((req, res) => {
  als.run({ requestId: randomUUID() }, async () => {
    // deep call needs no ctx argument
    await Promise.resolve();
    res.end('request id: ' + currentRequestId());
  });
});

server.listen(3000, () => console.log('listening on 3000'));

Mønster for Express-middleware

I Express er det idiomatiske sted at kalde als.run() en middleware, der monteres først. Den skal kalde next() inde i callbacken, så resten af kæden nedarver lageret.

  • Læs en indgående x-request-id-header, hvis en proxy eller en upstream-tjeneste har leveret en; ellers genererer du et nyt UUID.
  • Alle efterfølgende behandlere, tjenester og loggere kan læse lageret uden at modtage det som et argument.
import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

export const als = new AsyncLocalStorage();

export function contextMiddleware(req, res, next) {
  const store = {
    requestId: req.headers['x-request-id'] || randomUUID(),
    startedAt: Date.now(),
  };
  als.run(store, () => next()); // next() runs inside the context
}

export function getStore() {
  const store = als.getStore();
  if (!store) throw new Error('No request context');
  return store;
}

Kontekstbevidst struktureret logning

Den største gevinst: en logger, der automatisk sætter forespørgsels-id'et på hver linje — ingen kaldende kode sender det nogensinde med.

Loggeren læser selv lageret. Hvis der ikke er en aktiv kontekst (f.eks. i opstartskode), håndterer den det uden problemer.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function log(level, msg, extra = {}) {
  const store = als.getStore();
  const line = {
    ts: new Date().toISOString(),
    level,
    msg,
    requestId: store?.requestId ?? null,
    ...extra,
  };
  console.log(JSON.stringify(line));
}

als.run({ requestId: 'req-7' }, () => {
  log('info', 'user fetched', { userId: 99 });
});

log('warn', 'no request context here');

Ændring af lageret under en forespørgsel

Fordi lageret er en reference (et objekt eller en Map), kan du berige det, efter godkendelsen er gennemført. Senere loglinjer får automatisk de nye felter med.

  • Initialisér med minimale data ved kanten (forespørgsels-id).
  • Efter godkendelses-middleware er kørt, føjes userId og tenantId til det samme lagerobjekt.
  • Foretræk en Map, hvis du vil have en tydelig nøglerelateret API; et almindeligt objekt er fint og en smule hurtigere.
import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

function set(key, value) {
  const store = als.getStore();
  if (store) store.set(key, value);
}
function get(key) {
  return als.getStore()?.get(key);
}

als.run(new Map([['requestId', 'r-1']]), () => {
  // ... later, after authenticating:
  set('userId', 42);
  set('tenantId', 'acme');
  console.log(get('requestId'), get('userId'), get('tenantId'));
});

enterWith sammenlignet med run

Der er to måder at angive et lager på:

  • als.run(store, fn) — afgrænser lageret til fn og dets asynkrone efterkommere. Når fn er afsluttet, forsvinder konteksten. Foretræk denne.
  • als.enterWith(store) — angiver lageret for den aktuelle synkrone udførelse og alt, der følger efter i den samme asynkrone ressource, uden automatisk afslutning.

enterWith kræver forsigtighed: hvis du kalder den i en asynkron ressource med lang levetid, kan den lække ind i senere, uvedkommende arbejde. Brug run, medmindre du har en specifik grund (f.eks. at du ikke kan omgive et callback).

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();

// Scoped and safe — context ends with the callback:
als.run({ id: 'A' }, () => {
  console.log(als.getStore().id); // 'A'
});
console.log(als.getStore());      // undefined

// enterWith persists with no clear boundary — easy to leak:
als.enterWith({ id: 'B' });
console.log(als.getStore().id);   // 'B' (and stays set!)

Hvor konteksten kan gå tabt

AsyncLocalStorage følger native promises, async/await, timere og de fleste hændelsesudsendere. Men konteksten kan gå tabt i nogle få situationer:

  • Arbejde, der planlægges før run() — f.eks. en forbindelsespool eller kø, der oprettes ved opstart, og hvis callbacks kører uden for et forespørgselslager.
  • Nogle ældre biblioteker, der samler eller genbruger ressourcer på tværs af forespørgsler, kan medføre et forældet lager.
  • Manuelt frakoblede callbacks, der gemmes i et globalt array og kaldes senere.

Løsningen ved integration med sådan kode er als.bind(fn) (eller AsyncResource.bind), som tager et øjebliksbillede af den aktuelle kontekst og anvender den igen, hver gang funktionen kaldes senere.

import { AsyncLocalStorage } from 'node:async_hooks';

const als = new AsyncLocalStorage();
const queue = [];

als.run({ requestId: 'r-9' }, () => {
  // bind captures the current store for later execution
  queue.push(als.bind(() => {
    console.log('later:', als.getStore()?.requestId);
  }));
});

// Runs outside the run() callback, but context is preserved:
queue.forEach((fn) => fn()); // later: r-9

Et genanvendeligt kontekstmodul

I praksis centraliserer du lageret i ét lille modul, så resten af kodebasen kun importerer hjælpefunktioner — den tilgår aldrig AsyncLocalStorage-instansen direkte.

Det holder API'et overskueligt: runWithContext() ved kanten, requestId() / getUser() alle andre steder.

import { AsyncLocalStorage } from 'node:async_hooks';
import { randomUUID } from 'node:crypto';

const als = new AsyncLocalStorage();

export function runWithContext(seed, fn) {
  const store = { requestId: randomUUID(), ...seed };
  return als.run(store, fn);
}

export function context() {
  return als.getStore() ?? null;
}

export function requestId() {
  return context()?.requestId ?? null;
}

export function setUser(user) {
  const store = als.getStore();
  if (store) store.user = user;
}

Ydeevne og god praksis

AsyncLocalStorage i moderne Node (v16+) bygger på en effektiv native implementering, og omkostningen er lille — acceptabel for stort set alle webarbejdsbelastninger. Følg dog nogle gode praksisser:

  • Brug én AsyncLocalStorage-instans med lang levetid pr. formål, ikke én pr. forespørgsel.
  • Hold lageret lille; det er tilstand afgrænset til en forespørgsel, ikke en cache.
  • Gem ikke hemmeligheder, som du ikke ønsker skal vises i logfiler, der læser lageret.
  • Foretræk run() frem for enterWith() for at få automatisk oprydning.
  • Håndtér altid situationen, hvor lageret er undefined, for kode der kan køre uden for en forespørgsel.

Hurtigt tjek

Test din forståelse af, hvordan du opretter en forespørgselskontekst på sikker vis.

Opsummering

Du har lært, hvordan du viderefører tilstand, der er afgrænset til en forespørgsel, på tværs af asynkrone grænser uden at sende den gennem hvert kald:

  • AsyncLocalStorage fra node:async_hooks er trådlokal lagring til den asynkrone verden.
  • als.run(store, fn) knytter et lager til fn og alle dets asynkrone efterkommere; als.getStore() læser det overalt; uden for run er det undefined.
  • Tilslut det én gang ved kanten (HTTP-serveren eller den første middleware), initialisér et forespørgsels-id, og berig derefter lageret efter godkendelsen.
  • En kontekstbevidst logger kan automatisk sætte forespørgsels-id'et på hver linje.
  • Foretræk run() frem for enterWith() for automatisk oprydning, og brug als.bind() / AsyncResource.bind() til at bevare konteksten på tværs af frakoblede callbacks.
  • Hold lageret lille, håndtér situationen uden kontekst, og centralisér adgangen i ét modul.
Gratis at komme i gang

Lær JavaScript med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
22
Lektioner
92

Ofte stillede spørgsmål

Er lektionen “Kontekstvideregivelse med AsyncLocalStorage” gratis?

Ja — hele teksten til “Kontekstvideregivelse med AsyncLocalStorage” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Bootcamp i backendudvikling med Node.js-kurset, skal du opgradere til CoddyKit PRO. Bootcamp i backendudvikling med Node.js-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Kontekstvideregivelse med AsyncLocalStorage”?

Videregiv request-scoped state på tværs af asynkrone grænser uden prop drilling med AsyncLocalStorage. Du øver dig i Bootcamp i backendudvikling med Node.js med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Bootcamp i backendudvikling med Node.js?

Der kræves ingen tidligere erfaring. Bootcamp i backendudvikling med Node.js på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 4 af 4.

Hvor lang tid tager lektionen “Kontekstvideregivelse med AsyncLocalStorage”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Bootcamp i backendudvikling med Node.js-lektion?

Ja. Alle Bootcamp i backendudvikling med Node.js-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Struktureret logging med korrelations-id'er
  2. Distribueret tracing med OpenTelemetry-spans
  3. Eksponering af applikationsmetrikker og RED-metoden
  4. Kontekstvideregivelse med AsyncLocalStorage
← Tilbage til Bootcamp i backendudvikling med Node.js