JavaScripts callback-modell var den enda mekanismen för icke-blockerande I/O när Node.js dök upp 2009. När Promises kom i ES6 och async/await följde i ES2017, hade de flesta produktionskodbaser åratal av callback-baserad logik redan i bruk: kapslade fel-först-hanterare, delat muterbart tillstånd som är gängat genom stängningar, återförsökslogik inbäddad i anonyma funktioner tre nivåer djupt. Syntaxen ändrades. Den körande koden gjorde det inte. Idag ärver de flesta ingenjörsteam exakt denna situation: en kodbas där de nya mönstren och de gamla samexisterar, där teamet vill migrera men systemet inte kan stoppa medan de gör det.
Migrera din asynkrona kodbas säkert
SMART TS XL och identifierar migrationsrisk över hela din kodbas innan du ändrar en rad.
Utforska nuDen goda nyheten är att migreringen inte kräver omskrivning. Återanrop, löften och async/await är kompatibla vid väldefinierade gränser. Node.js levereras util.promisify just för att överbrygga klyftan mellan fel-först-anrop och Promise-returning-funktioner. Wrapper-lager låter gammal och ny kod samexistera under övergången. Stegvis migrering, en modul i taget, håller produktionen igång medan kodbasen går framåt. Utmaningen är inte själva transformationen utan att göra den systematiskt: att förstå vilka anrop som är säkra att konvertera först, vilka antimönster som kommer att orsaka buggar om de naivt skrivs om, och hur man validerar att varje konverterad funktion beter sig identiskt med den den ersatte.
Förstå återuppringningsmodellen och varför den går sönder i stor skala
Node.js återanropskonvention är enkel: funktioner som utför asynkront arbete accepterar en återanropskonvention som sitt sista argument. Återanropskonventionen får ett fel som sitt första argument och resultatet som sitt andra. Varje standardbiblioteksfunktion i Node.js följer denna konvention: fs.readFile, http.get, child_process.exec, och hundratals till.
JavaScript
// Standard Node.js error-first callback pattern
const fs = require('fs');
fs.readFile('config.json', 'utf8', (err, data) => {
if (err) {
console.error('Failed to read config:', err);
return;
}
const config = JSON.parse(data);
console.log('Config loaded:', config);
});
Detta mönster är enkelt för en enskild operation. Det bryts ner när operationer måste kedjas, eftersom varje efterföljande operation måste initieras inuti föregående återanrop. En trestegssekvens av läsning, transformering och skrivning producerar tre kapslade nivåer av återanrop. En femstegssekvens producerar fem. Det här är den struktur som utvecklarna kallar "återanropshelvetet" eller "dömets pyramid", och det är inte bara ett estetiskt problem. Djupt kapslade återanrop gör felhanteringen inkonsekvent (varje nivå måste oberoende kontrollera sitt felargument), gör exekveringsordningen svår att resonera kring och gör omfaktorering farlig eftersom dataflödet är implicit snarare än explicit.
Det mer kritiska problemet för produktionssystem är att återanrop inte har någon inbyggd mekanism för koordinering. Att köra två asynkrona operationer och vänta på att båda ska slutföras kräver manuell räknarspårning. Att köra en sekvens av operationer över en array kräver rekursiva mönster eller tredjepartsbibliotek som asyncInget av denna komplexitet syns i funktionssignaturerna: ett callback-baserat API och en samtidig orkestrering som byggts ovanpå det ser identiska ut från utsidan, tills de misslyckas.
Hur Callback Hell faktiskt ser ut
En verklig återanropspyramid från en Node.js-tjänst som läser användardata, validerar behörigheter och loggar åtkomsten:
JavaScript
// Three-level callback pyramid -- representative of real legacy code
function getUserReport(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, user) => {
if (err) return callback(err);
if (!user) return callback(new Error('User not found'));
permissions.check(userId, 'read:reports', (err, allowed) => {
if (err) return callback(err);
if (!allowed) return callback(new Error('Permission denied'));
auditLog.write({ userId, action: 'read:reports' }, (err) => {
if (err) return callback(err);
// Finally, the actual work
callback(null, buildReport(user));
});
});
});
}
Felhantering upprepningar på varje nivå. Indraget gör dataflödet visuellt komplext. Att lägga till ett fjärde steg kräver ytterligare en kapslad nivå. Testning av den här funktionen kräver att alla tre beroenden simuleras i ordning, och simulering av ett fel på den tredje nivån kräver att de två första simuleras för att lyckas. Var och en av dessa egenskaper försämras allt eftersom kedjan växer.
Använda util.promisify för att överbrygga återanrop och löften
Node.js 8.0 introducerad util.promisify, som konverterar alla funktioner som följer standardkonventionen för fel-först-återuppringning till en funktion som returnerar ett löfte. Detta är rätt utgångspunkt för alla async/await-migreringar i en Node.js-kodbas.
JavaScript
const { promisify } = require('util');
const fs = require('fs');
// Convert Node.js built-ins
const readFile = promisify(fs.readFile);
const writeFile = promisify(fs.writeFile);
// Now usable with async/await
async function processConfig(path) {
const data = await readFile(path, 'utf8');
const config = JSON.parse(data);
config.lastLoaded = Date.now();
await writeFile(path, JSON.stringify(config, null, 2), 'utf8');
return config;
}
util.promisify hanterar fel-först-konventionen automatiskt: om återanropet får ett icke-null-första argument, avvisas det returnerade Promise med det felet. Om återanropet får ett null-första argument och ett resultat, löses Promise med resultatet. För de allra flesta Node.js core API:er och tredjepartsbibliotek som följer denna konvention behövs ingen anpassad omslagning.
Lova anpassade återuppringningsfunktioner
För anpassade funktioner som följer fel-först-konventionen men inte är inbyggda i Node.js, util.promisify fungerar identiskt:
JavaScript
const { promisify } = require('util');
// Your existing callback-based function
function fetchUserFromDB(userId, callback) {
db.query('SELECT * FROM users WHERE id = ?', [userId], (err, rows) => {
if (err) return callback(err);
callback(null, rows[0] || null);
});
}
// Promisified version -- no changes to the original function needed
const fetchUser = promisify(fetchUserFromDB);
// Use in async context
async function getUser(userId) {
const user = await fetchUser(userId);
if (!user) throw new Error(`User ${userId} not found`);
return user;
}
Denna metod är viktig: util.promisify slår om den ursprungliga funktionen utan att ändra den. Den ursprungliga återanropsversionen fortsätter att fungera. Anropare som ännu inte har migrerats fortsätter att använda återanropsversionen. Anropare som har migrerats använder den utlovade versionen. Denna samexistens är det som möjliggör stegvis migrering.
Hantera icke-standardiserade återuppringningssignaturer
Vissa äldre bibliotek skickar flera resultatvärden till sina återanrop, vilket util.promisify upplöses endast till det första värdet. I dessa fall, util.promisify.custom symbolen tillåter att definiera en anpassad löftesifiering:
JavaScript
const { promisify } = require('util');
// A function that passes two results to its callback
function parseData(input, callback) {
// callback(err, parsedData, metadata)
callback(null, { value: input.trim() }, { length: input.length });
}
// Custom promisification that returns both results
parseData[promisify.custom] = (input) => {
return new Promise((resolve, reject) => {
parseData(input, (err, data, meta) => {
if (err) reject(err);
else resolve({ data, meta });
});
});
};
const parseDataAsync = promisify(parseData);
const result = await parseDataAsync(' hello ');
// result === { data: { value: 'hello' }, meta: { length: 9 } }
Konvertera återuppringningar till löften: Steg-för-steg-mönstret
För kod som inte kan hanteras av util.promisify direkt, den manuella Promise-omslagsplattan är migreringsvägen. Mönstret är konsekvent:
JavaScript
// Step 1: Original callback-based function
function checkPermission(userId, resource, callback) {
acl.check({ userId, resource }, (err, result) => {
if (err) return callback(err);
callback(null, result.allowed);
});
}
// Step 2: Promise wrapper (coexists with the original)
function checkPermissionAsync(userId, resource) {
return new Promise((resolve, reject) => {
checkPermission(userId, resource, (err, allowed) => {
if (err) reject(err);
else resolve(allowed);
});
});
}
// Step 3: async/await consumer
async function authorizeRequest(userId, resource) {
const allowed = await checkPermissionAsync(userId, resource);
if (!allowed) {
throw new Error(`${userId} does not have access to ${resource}`);
}
}
Trenivå-återanropspyramiden från tidigare, omskriven med async/await:
JavaScript
// After migration: same logic, linear structure
async function getUserReport(userId) {
const user = await db.queryAsync('SELECT * FROM users WHERE id = ?', [userId]);
if (!user) throw new Error('User not found');
const allowed = await permissions.checkAsync(userId, 'read:reports');
if (!allowed) throw new Error('Permission denied');
await auditLog.writeAsync({ userId, action: 'read:reports' });
return buildReport(user);
}
Felhanteringen hanteras nu av ett enda försök/fångst på anropsplatsen istället för att upprepas på varje nivå. Indraget är platt. Att lägga till ett fjärde steg kräver ytterligare ett. await rad. Testning kräver att varje beroende simuleras oberoende av varandra, i valfri ordning.
Parallell exekvering med Promise.all och Promise.allSettled
Ett av de vanligaste misstagen vid migrering från callbacks till async/await är sekventiell exekvering av operationer som kan köras parallellt. Callbacks gjorde parallell exekvering så komplex att många utvecklare som standard använde sekventiella kedjor. Async/await gör att parallellitet ser sekventiell ut, vilket kan vilseleda utvecklare att skriva kod som är långsammare än dess föregångare i callback-tekniken.
JavaScript
// WRONG: sequential execution -- each awaits the previous result
async function loadDashboardData(userId) {
const profile = await fetchProfile(userId); // 100ms
const orders = await fetchOrders(userId); // 150ms
const reviews = await fetchReviews(userId); // 80ms
return { profile, orders, reviews }; // Total: ~330ms
}
// RIGHT: parallel execution with Promise.all
async function loadDashboardData(userId) {
const [profile, orders, reviews] = await Promise.all([
fetchProfile(userId),
fetchOrders(userId),
fetchReviews(userId),
]);
return { profile, orders, reviews }; // Total: ~150ms
}
Promise.all avvisar om något Promise i arrayen avvisas. När oberoende operationer kan misslyckas utan att påverka varandra och anroparen behöver veta om alla fel, Promise.allSettled är rätt verktyg:
JavaScript
// Promise.allSettled: runs all, reports success or failure per operation
async function syncAllSources(userId) {
const results = await Promise.allSettled([
syncFromGitHub(userId),
syncFromJira(userId),
syncFromSlack(userId),
]);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason.message);
if (failed.length > 0) {
console.warn('Some syncs failed:', failed);
}
return results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
}
Detta mönster har ingen naturlig motsvarighet i callback-baserad kod utan en manuell räknare eller ett bibliotek som async.parallelMigrerar till Promise.allSettled är ofta en av de mest hävstångseffektsförändringarna i en äldre asynkron kodbas.
Undvika antimönstret "await-in-loop"
Felet med sekventiellt istället för parallellt uppträder oftast inuti loopar:
JavaScript
// WRONG: sequential -- processes items one at a time
async function processOrders(orderIds) {
const results = [];
for (const id of orderIds) {
const result = await processOrder(id); // blocks until each completes
results.push(result);
}
return results;
}
// RIGHT: parallel -- all orders processed concurrently
async function processOrders(orderIds) {
return Promise.all(orderIds.map(id => processOrder(id)));
}
// RIGHT (with concurrency limit): parallel but bounded
const pLimit = require('p-limit');
const limit = pLimit(5); // max 5 concurrent
async function processOrders(orderIds) {
return Promise.all(
orderIds.map(id => limit(() => processOrder(id)))
);
}
Await-in-loop-mönstret är en av de vanligaste regressionsmönstren som introduceras under async/await-migrering av callback-baserad kod, eftersom callbacks tvingade utvecklare att tänka explicit på samtidighet medan async/await döljer den.
Felhantering i Async/Await: Ersätta spridning av återuppringningsfel
Återanrop sprider fel enligt konvention: det första argumentet i varje återanrop är ett fel eller null. Detta fungerar men kräver att varje anropare manuellt kontrollerar felargumentet. Async/await sprider fel genom Promise-avvisningsmekanismen, som integreras med JavaScripts inbyggda try/catch.
JavaScript
// Callback error propagation: repeated at every level
function processPayment(orderId, callback) {
validateOrder(orderId, (err, order) => {
if (err) return callback(err); // propagate
chargeCard(order.amount, (err, charge) => {
if (err) return callback(err); // propagate again
updateInventory(orderId, (err) => {
if (err) return callback(err); // propagate again
callback(null, charge.id);
});
});
});
}
// Async/await: error propagation is automatic
async function processPayment(orderId) {
const order = await validateOrder(orderId); // throws on error
const charge = await chargeCard(order.amount); // throws on error
await updateInventory(orderId); // throws on error
return charge.id;
}
En viktig faktor att beakta vid migrering är att bevara felkontexten. Återanropsbaserad kod skickar ofta fel som har utökats med kontextuell information på varje nivå. Vid migrering till async/await, se till att felomslag bevarar denna kontext:
JavaScript
// Preserving error context during async migration
async function processPayment(orderId) {
let order;
try {
order = await validateOrder(orderId);
} catch (err) {
throw new Error(`Payment validation failed for order ${orderId}: ${err.message}`);
}
try {
const charge = await chargeCard(order.amount);
await updateInventory(orderId);
return charge.id;
} catch (err) {
// Attempt rollback, then rethrow with context
await refundCharge(order.amount).catch(console.error);
throw new Error(`Payment processing failed for order ${orderId}: ${err.message}`);
}
}
Obehandlade avslag på löften
Återanropsbaserad kod sväljer fel i tysthet när argumentet fel ignoreras. Async/await producerar ohanterade Promise-avslag, vilket i Node.js 15+ orsakar processavslutning som standard. Detta är en avgörande förändring vid migrering: kod som tidigare misslyckades i tysthet kraschar nu.
JavaScript
// This produces an unhandled rejection in Node.js 15+
async function riskyOperation() {
throw new Error('Something failed');
}
riskyOperation(); // Promise rejected, but rejection is not caught
// Fix: always await or chain .catch()
await riskyOperation(); // throws, caller handles it
riskyOperation().catch(console.error); // handles inline
Granska alla anrop av typen "fire-and-forget async" under migreringen. Alla anrop till en async-funktion vars returvärde inte väntas eller kedjas med .catch() är ett potentiellt tyst fel i återuppringningsvärlden men en ohanterad avvisningskrasch i async/await-världen.
Migrera EventEmitter-mönster till Promises och asynkrona iteratorer
Node.js EventEmitters är en form av återanropsmönster där flera återanrop registreras för namngivna händelser. De är vanliga i strömmar, nätverksanslutningar och anpassade händelsebussar. Direkt migrering till async/await kräver att det händelsebaserade API:et omsluts.
JavaScript
const { EventEmitter } = require('events');
// Original EventEmitter-based pattern
function fetchDataLegacy(source) {
const emitter = new EventEmitter();
setTimeout(() => {
emitter.emit('data', { records: [1, 2, 3] });
emitter.emit('end');
}, 100);
return emitter;
}
// Usage: callback registration
const stream = fetchDataLegacy('api');
stream.on('data', chunk => console.log('received', chunk));
stream.on('error', err => console.error('error', err));
stream.on('end', () => console.log('done'));
Att omvandla en engångshändelse till ett löfte är enkelt:
JavaScript
// Converting a single-event completion to Promise
function waitForEvent(emitter, successEvent, errorEvent = 'error') {
return new Promise((resolve, reject) => {
emitter.once(successEvent, resolve);
emitter.once(errorEvent, reject);
});
}
async function fetchData(source) {
const emitter = fetchDataLegacy(source);
const data = await waitForEvent(emitter, 'data');
await waitForEvent(emitter, 'end');
return data;
}
För strömmar som genererar flera datahändelser tillhandahåller Node.js events.on vilket returnerar en asynkron iterator, vilket gör att hela strömmen kan konsumeras med for await...of:
JavaScript
const { on } = require('events');
async function processStream(readable) {
for await (const chunk of on(readable, 'data')) {
await processChunk(chunk);
}
}
Node.js-läsbara strömmar är också direkt itererbara som asynkrona itererbara strömmar sedan Node 10:
JavaScript
const fs = require('fs');
async function countLines(filePath) {
let lines = 0;
const stream = fs.createReadStream(filePath, { encoding: 'utf8' });
for await (const chunk of stream) {
lines += chunk.split('\n').length - 1;
}
return lines;
}
TypeScript Async/Await-migreringsmönster
TypeScript-kodbaser har ytterligare överväganden vid migrering till async/await. Returtyper måste uppdateras från återanropssignaturer till Promise<T>, och kompilatorn framtvingar att await endast används inuti async-funktioner.
skrivmaskin
// Before: callback signature
function fetchUser(
id: string,
callback: (err: Error | null, user: User | null) => void
): void {
db.findOne({ id }, callback);
}
// After: async/await signature with proper return type
async function fetchUser(id: string): Promise<User> {
const user = await db.findOneAsync<User>({ id });
if (!user) throw new Error(`User ${id} not found`);
return user;
}
TypeScripts strikta nullkontroller interagerar med asynkron kod på ett sätt som fångar vanliga migreringsfel. Om en funktion tidigare returnerades User | null genom en återuppringning och migreringen ändrar det till Promise<User> (kastar istället för att returnera null), TypeScript kommer att fånga anropare som kontrollerade efter null men inte längre behöver, och anropare som inte kontrollerar efter fel som de nu behöver hantera.
För äldre TypeScript-kod som använder @types/node återuppringningssignaturer, util.promisify är fullständigt typad och härleder automatiskt rätt Promise-returtyp för inbyggda Node.js-funktioner.
Stegvis migreringsstrategi för produktionssystem
Ett produktionssystem kan inte migreras på en gång. Den stegvisa metoden konverterar en modul i taget, validerar den och går sedan vidare till nästa. Nyckeln är att upprätthålla en tydlig gräns mellan konverterad och okonverterad kod i varje steg.
Migreringsordningen bör följa beroendens riktning: konvertera de djupaste beroendena först, arbeta sedan uppåt mot anroparna. Detta säkerställer att när en funktion på högre nivå konverteras returnerar dess beroenden redan Promises, och omslagslagret behövs inte längre.
JavaScript
// Stage 1: Promisify the data layer (deepest dependency)
const db = {
queryAsync: promisify(db.query.bind(db)),
insertAsync: promisify(db.insert.bind(db)),
};
// Stage 2: Convert the service layer (depends on db)
class UserService {
async getUser(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
async createUser(data) {
return db.insertAsync('users', data);
}
}
// Stage 3: Convert the controller layer (depends on service)
// -- only after Stage 2 is validated and deployed
async function handleGetUser(req, res) {
try {
const user = await userService.getUser(req.params.id);
res.json(user);
} catch (err) {
res.status(500).json({ error: err.message });
}
}
Denna etappvisa metod stöds direkt av analysverktyg. Som diskuterats i data- och kontrollflödesanalysAtt förstå hur data flödar genom asynkrona lager innan man modifierar dem är en förutsättning för säker stegvis omstrukturering. Beroenderiktningen avgör vilka moduler som är säkra att konvertera först och vilka som måste vänta tills deras beroenden redan har migrerats.
Omslagslager för bakåtkompatibilitet
Under migreringen kommer vissa anropare fortfarande att förvänta sig återuppringningsbaserade API:er. util.callbackify funktionen är inversen av util.promisify: den konverterar en async-funktion tillbaka till ett fel-först callback-gränssnitt:
JavaScript
const { callbackify } = require('util');
// New async implementation
async function fetchUserAsync(id) {
return db.queryAsync('SELECT * FROM users WHERE id = ?', [id]);
}
// Backward-compatible callback version for unconverted callers
const fetchUser = callbackify(fetchUserAsync);
// Old callers continue to work unchanged
fetchUser(userId, (err, user) => {
if (err) return handleError(err);
render(user);
});
// New callers use the async version directly
const user = await fetchUserAsync(userId);
Denna dubbelriktade kompatibilitet innebär att konvertering inte är en händelse på en dag. Enskilda moduler kan konverteras i vilken sprint som helst utan att koordineras med varje anropare samtidigt.
Vanliga fallgropar med asynkron/väntande under migrering
Saknar väntan vid asynkrona funktionsanrop
Det vanligaste migreringsfelet är att anropa en async-funktion utan att vänta på den. Detta är osynligt för JavaScript-körningen om inte funktionen avvisar, och avvisandet blir ett ohanterat Promise-avvisande snarare än ett utlöst fel.
JavaScript
// Bug: missing await -- function runs but result is a Promise, not the user
async function updateUserName(id, name) {
const user = fetchUser(id); // BUG: forgot await, user is a Promise object
user.name = name; // setting .name on a Promise, not a user
await saveUser(user); // saves the Promise object
}
// Fix
async function updateUserName(id, name) {
const user = await fetchUser(id);
user.name = name;
await saveUser(user);
}
TypeScript och ESLint no-floating-promises regeln fångar upp detta mönster automatiskt. Det rekommenderas starkt att lägga till denna lint-regel under migreringen.
Asynkrona funktioner i arraymetoder
Array.prototype.forEach väntar inte på asynkrona återanrop. Detta producerar samma sekventiella-men-felaktiga beteende som await-in-loop, förutom att koden verkar fungera samtidigt som alla återanrop körs tyst samtidigt utan att vänta på någon:
JavaScript
// Bug: forEach does not await async callbacks
async function processAll(ids) {
ids.forEach(async (id) => {
await processItem(id); // these run concurrently, forEach completes immediately
});
// function returns before any processItem completes
}
// Fix: use Promise.all with map
async function processAll(ids) {
await Promise.all(ids.map(id => processItem(id)));
}
Try/Catch fångar inte asynkrona fel utanför Await
Ett try/catch-block fångar bara fel från väntade uttryck. Ett async-funktionsanrop utan väntan kommer inte att få sitt avvisande fångat upp av ett omgivande try/catch-block:
JavaScript
// Bug: the rejection from riskyOp() is not caught
async function run() {
try {
riskyOp(); // not awaited -- rejection escapes the try/catch
} catch (err) {
console.error(err); // never reached
}
}
// Fix: await inside the try block
async function run() {
try {
await riskyOp();
} catch (err) {
console.error(err);
}
}
Testa asynkron kod efter migrering
Migrerade asynkrona funktioner kräver asynkrona testfall. Moderna testramverk har stöd för detta.
JavaScript
// Jest async test patterns
describe('UserService', () => {
// Pattern 1: async/await in test
test('fetches user by id', async () => {
const user = await userService.getUser('user-123');
expect(user.id).toBe('user-123');
});
// Pattern 2: testing rejection
test('throws when user not found', async () => {
await expect(userService.getUser('nonexistent'))
.rejects.toThrow('User nonexistent not found');
});
// Pattern 3: parallel setup
beforeAll(async () => {
await db.connect();
await db.seed(testData);
});
afterAll(async () => {
await db.cleanup();
await db.disconnect();
});
});
När man validerar att en migrerad funktion producerar identisk utdata som sin föregångare i återanropsfunktionen är jämförelsetestning effektiv:
JavaScript
// Comparison test: callback version vs. async version must agree
test('async version matches callback version output', async () => {
const callbackResult = await promisify(fetchUserLegacy)('user-123');
const asyncResult = await fetchUserAsync('user-123');
expect(asyncResult).toEqual(callbackResult);
});
Detta testmönster är särskilt användbart under övergångsperioden när båda versionerna körs parallellt. Som undersökts i samband med konsekvensanalys innan kodändringar görs, att validera att en omfaktorerad funktion producerar identiska utdata som sin föregångare är den evidensbaserade bekräftelsen på att migreringen är korrekt innan den gamla versionen tas bort.
Hur SMART TS XL Stöder säker asynkron migrering i stor skala
För företagskodbaser där återanropskedjor sträcker sig över flera filer, tjänster och team är det första kravet för säker migrering en komplett karta över de befintliga asynkrona beroendena: vilka funktioner anropar vilka, vilka data som passerar mellan dem, vilka kedjor som är kritiska vägar för applikationens kärnverksamhet och vilka som är isolerade verktyg som kan migreras oberoende.
SMART TS XL konstruerar denna beroendekarta genom att analysera hela kodbasen, inte bara enskilda filer. Den löser hur återuppringningsbaserade funktioner refererar till varandra över modulgränser, identifierar vilket delat tillstånd som är kopplat genom stängningar och visualiserar exekveringskedjorna som måste förbli intakta under migreringen. Denna strukturella analys ger input för den etappvisa migreringsmetod som beskrivs i den här guiden: vilka moduler finns längst ner i beroendegrafen och kan konverteras först, och vilka moduler måste vänta tills deras beroenden redan har migrerats.
Plattformen konsekvensanalys Funktionen utökar detta till att omfatta ändringsbedömning. Innan en callback-baserad modul konverteras för att returnera Promises, identifierar konsekvensanalysen alla andra moduler i kodbasen som anropar den med ett callback-gränssnitt. Dessa anropare är migreringsomfånget för nästa steg: de måste antingen konverteras samtidigt, eller en bakåtkompatibel omslagsmapp som använder util.callbackify måste bibehållas tills de konverteras. Utan denna uppräkning fortsätter migreringar med okänt omfång och producerar oväntade avbrott när anropare som inte identifierades stöter på ett löfte där de förväntade sig en återanropning.
För kodbaser som blandar JavaScript med TypeScript, eller som anropar backend-tjänster på andra språk, SMART TS XLÄr analys av beroenden mellan språk ger insyn i hela exekveringsvägen, inte bara JavaScript-lagret. En återanropskedja som avslutas med ett anrop till en extern tjänst skriven i Java eller Python har beroenden som enspråkiga verktyg inte kan se, och migreringsplanering som ignorerar dessa beroenden är ofullständig. visualisering av beroenden den där SMART TS XL provides gör dessa gränsöverskridande relationer synliga innan några migreringsändringar görs.
Upprätthålla migreringen: Från återanrop till asynkron/vänta över hela kodbasen
Övergången från återanrop till async/await är inte slutförd när den första modulen konverteras. Den är slutförd när det sista omslagslagret tas bort och kodbasen inte har några kvarvarande kodfiler. callback konvention i sin kärnlogik. Att nå dit kräver disciplin under hela migreringsperioden: ny kod måste skrivas i async/await, omslagslager måste behandlas som temporära, och ESLint-regler måste säkerställa att callback-liknande funktioner inte introduceras i konverterade moduler.
De praktiska markörerna för en avslutad migration är: nej util.promisify anrop i applikationskod (de behövdes endast under övergångsperioden), nej (err, result) => mönster i kärnverksamhetens logik (de har ersatts av try/catch i asynkrona funktioner), inga manuella Promise-konstruktörer där async/await skulle räcka, och Promise.all på alla platser där oberoende verksamheter tidigare bedrevs sekventiellt.
Var och en av dessa är mätbara genom statisk analys, vilket innebär att framsteg kan spåras och rapporteras objektivt snarare än uppskattas. För team som arbetar i stor skala är kombinationen av automatiserad statisk analys för att hitta återanropsmönster och beroendeanalys för att avgränsa varje migreringssteg det som gör skillnaden mellan en migrering som slutförs inom en definierad tidslinje och en som kvarstår på obestämd tid eftersom omfattningen aldrig var helt känd.