Propagacja kontekstu za pomocą AsyncLocalStorage
Przenoś stan związany z żądaniem przez granice asynchroniczności bez przekazywania go przez kolejne warstwy, korzystając z AsyncLocalStorage
Propagacja kontekstu za pomocą AsyncLocalStorage to bezpłatna lekcja Node.js Backend Development Bootcamp na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Node.js Backend Development Bootcamp, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.
Problem przekazywania parametrów przez wiele warstw
W usłudze backendowej dane przypisane do żądania, takie jak identyfikator żądania, uwierzytelniony użytkownik czy identyfikator dzierżawy, są potrzebne głęboko w stosie wywołań: w repozytoriach, loggerach i wychodzących klientach HTTP.
Naiwnym rozwiązaniem jest prop drilling — przekazywanie argumentu ctx przez każdą funkcję:
- Sygnatury wszystkich funkcji zostają zaśmiecone parametrem
ctx. - Jedno pominięte przekazanie powoduje utratę kontekstu przez dalsze wywołanie.
- Kod biblioteczny, którego nie kontroluje użytkownik, w ogóle nie może otrzymać jego
ctx.
Potrzebny jest sposób niejawnego przenoszenia stanu przypisanego do żądania, który przetrwa każde await i każdy callback. Właśnie to zapewnia AsyncLocalStorage.
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.Czym jest AsyncLocalStorage
AsyncLocalStorage znajduje się we wbudowanym module node:async_hooks. Można myśleć o nim jak o pamięci lokalnej wątku dla świata asynchronicznego: magazynie pozostającym przypisanym do logicznego łańcucha operacji asynchronicznych.
Wywołanie als.run(store, callback) ustanawia magazyn, a następnie w dowolnym miejscu wewnątrz tego callbacku — niezależnie od liczby zagłębień await, łańcuchów Promise, wywołań setTimeout czy emiterów zdarzeń — als.getStore() zwraca ten sam magazyn.
- Każde współbieżne żądanie otrzymuje własny izolowany magazyn.
- Brak zmiennych globalnych i wyścigów między żądaniami.
- Pod spodem mechanizm korzysta ze śledzenia kontekstu asynchronicznego Node.
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() ustanawia kontekst
Podstawowym API jest als.run(store, fn, ...args). Wykonuje ono fn synchronicznie, ale wiąże store z całym drzewem asynchronicznym utworzonym w jego ramach.
storemoże być dowolną wartością — najczęściej zwykłym obiektem lub obiektemMap.run()zwraca wszystko, co zwracafn(w tym Promise).- Poza callbackiem
getStore()zwracaundefined.
Ten fragment pokazuje, że magazyn zachowuje się podczas await na rzeczywistym zegarze.
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()); // undefinedPodłączanie do serwera HTTP
Wzorzec stosowany na prawdziwym serwerze polega na opakowaniu obsługi żądania w als.run() na samym brzegu systemu i zainicjalizowaniu magazynu świeżym identyfikatorem żądania. Cały kod znajdujący się dalej może go odczytywać.
Poniższy przykład korzysta wyłącznie z wbudowanego modułu node:http, więc nie ma zależności od frameworka. Każde przychodzące żądanie otrzymuje własny magazyn, odizolowany od żądań współbieżnych.
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'));Wzorzec middleware Express
W Express idiomatycznym miejscem wywołania als.run() jest middleware zamontowane jako pierwsze. Musi ono wywołać next() wewnątrz callbacku, aby reszta łańcucha odziedziczyła magazyn.
- Należy odczytać przychodzący nagłówek
x-request-id, jeśli proxy lub usługa nadrzędna go dostarczyła; w przeciwnym razie trzeba wygenerować nowy UUID. - Każdy kolejny handler, serwis i logger może odczytać magazyn bez otrzymywania go jako argumentu.
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;
}Logowanie strukturalne świadome kontekstu
Największa korzyść: logger, który automatycznie opatruje każdą linię identyfikatorem żądania — osoba wywołująca nigdy go nie przekazuje.
Logger sam odczytuje dane ze store. Jeśli nie ma aktywnego kontekstu (np. podczas uruchamiania), działa bez zakłóceń w trybie awaryjnym.
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');Modyfikowanie Store w trakcie żądania
Ponieważ store jest referencją (obiektem lub Map), można go wzbogacić po zakończeniu uwierzytelniania. Kolejne wpisy w logu automatycznie uwzględnią nowe pola.
- Na brzegu aplikacji zainicjalizować minimalne dane (identyfikator żądania).
- Po wykonaniu middleware uwierzytelniania dodać
userIditenantIddo tego samego obiektu store. - Wybrać
Map, jeśli potrzebny jest przejrzysty interfejs oparty na kluczach; zwykły obiekt również się sprawdzi i jest nieco szybszy.
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 a run
Store można ustawić na dwa sposoby:
als.run(store, fn)— ogranicza store do zakresufni jego asynchronicznych elementów potomnych. Po zakończeniu działaniafnkontekst znika. Preferowane rozwiązanie.als.enterWith(store)— ustawia store dla bieżącego wykonania synchronicznego oraz wszystkiego, co nastąpi później w ramach tego samego zasobu asynchronicznego, bez automatycznego opuszczenia kontekstu.
enterWith jest zdradliwe: wywołanie go w długotrwałym zasobie asynchronicznym może spowodować przeniknięcie kontekstu do niezwiązanych z nim późniejszych operacji. Należy używać run, chyba że istnieje konkretny powód (np. nie można opakować callbacku).
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!)Gdzie kontekst może zostać utracony
AsyncLocalStorage obsługuje natywne promisy, async/await, timery i większość emiterów zdarzeń. Jednak w kilku sytuacjach kontekst może zostać utracony:
- Operacje zaplanowane przed wywołaniem
run()— np. pula połączeń lub kolejka utworzona podczas uruchamiania wykonuje callbacki poza store żądania. - Niektóre starsze biblioteki, które przechowują lub ponownie wykorzystują zasoby między żądaniami, mogą przenosić nieaktualny store.
- Ręcznie odłączone callbacki przechowywane w globalnej tablicy i wywoływane później.
Podczas integrowania takiego kodu należy użyć als.bind(fn) (lub AsyncResource.bind), które zapisuje migawkę bieżącego kontekstu i ponownie go stosuje przy każdym późniejszym wywołaniu funkcji.
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-9Moduł wielokrotnego użytku dla kontekstu
W praktyce store jest centralizowany w jednym niewielkim module, dzięki czemu reszta bazy kodu importuje wyłącznie funkcje pomocnicze — bez bezpośredniego dostępu do instancji AsyncLocalStorage.
Dzięki temu interfejs API pozostaje uporządkowany: runWithContext() na brzegu aplikacji, a requestId() / getUser() w pozostałych miejscach.
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;
}Wydajność i dobre praktyki
AsyncLocalStorage w nowoczesnym Node (v16+) korzysta z wydajnej implementacji natywnej, a narzut jest niewielki — akceptowalny praktycznie dla wszystkich obciążeń aplikacji webowych. Mimo to należy przestrzegać kilku dobrych praktyk:
- Używać jednej długotrwałej instancji
AsyncLocalStoragena każdy zakres odpowiedzialności, a nie jednej na żądanie. - Przechowywać w store niewielką ilość danych; jest to stan przypisany do żądania, a nie pamięć podręczna.
- Nie przechowywać sekretów, których nie chcieliby Państwo zobaczyć w logach odczytujących store.
- Preferować
run()zamiastenterWith(), aby uzyskać automatyczne czyszczenie. - Zawsze obsługiwać przypadek store o wartości
undefinedw kodzie, który może działać poza żądaniem.
Szybkie sprawdzenie
Sprawdź swoją wiedzę na temat bezpiecznego ustanawiania kontekstu żądania.
Podsumowanie
Dowiedział się Pan/Pani, jak propagować stan przypisany do żądania przez granice asynchroniczne bez przekazywania go przez kolejne warstwy:
AsyncLocalStorageznode:async_hooksto magazyn lokalny dla wątków świata asynchronicznego.als.run(store, fn)wiąże store zfni wszystkimi jego asynchronicznymi elementami potomnymi;als.getStore()odczytuje go w dowolnym miejscu; poza zakresem run ma on wartośćundefined.- Należy skonfigurować go raz na brzegu aplikacji (serwerze HTTP lub w pierwszym middleware), inicjalizując identyfikator żądania, a następnie wzbogacić store po uwierzytelnieniu.
- Logger świadomy kontekstu może automatycznie opatrywać każdą linię identyfikatorem żądania.
- Należy preferować
run()zamiastenterWith()ze względu na automatyczne czyszczenie oraz używaćals.bind()/AsyncResource.bind()do zachowania kontekstu w odłączonych callbackach. - Store powinien być niewielki, należy obsługiwać brak kontekstu, a dostęp do niego centralizować w jednym module.
Ucz się JavaScript dzięki korepetycjom AI — za darmo
Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.
- Kursy
- 22
- Lekcje
- 92
Często zadawane pytania
Czy lekcja „Propagacja kontekstu za pomocą AsyncLocalStorage” jest bezpłatna?
Tak — pełny tekst „Propagacja kontekstu za pomocą AsyncLocalStorage” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Node.js Backend Development Bootcamp, przejdź na CoddyKit PRO. Kurs Node.js Backend Development Bootcamp zawiera 4 lekcji w sumie.
Co nauczysz się w „Propagacja kontekstu za pomocą AsyncLocalStorage”?
Przenoś stan związany z żądaniem przez granice asynchroniczności bez przekazywania go przez kolejne warstwy, korzystając z AsyncLocalStorage Ćwiczysz Node.js Backend Development Bootcamp z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.
Czy potrzebuję doświadczenia, aby zacząć Node.js Backend Development Bootcamp?
Nie wymagamy żadnego doświadczenia. Node.js Backend Development Bootcamp w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.
Ile czasu zajmuje lekcja „Propagacja kontekstu za pomocą AsyncLocalStorage”?
Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.
Czy mogę pisać i uruchamiać kod w tej lekcji Node.js Backend Development Bootcamp?
Tak. Każda lekcja Node.js Backend Development Bootcamp zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.
Wszystkie lekcje w tym kursie
- Ustrukturyzowane logowanie z identyfikatorami korelacji
- Śledzenie rozproszone za pomocą spanów OpenTelemetry
- Udostępnianie metryk aplikacji i metoda RED
- Propagacja kontekstu za pomocą AsyncLocalStorage