Фокусиран набор от въпроси за интервю по Node.js за бекенд разработчици. Отговорите са структурирани за устна подготовка: обяснете архитектурата, дайте пример и споменете предимствата и недостатъците в продукционна среда.
Node.js е среда за изпълнение на JavaScript извън браузъра, изградена върху V8 и разширена с програмни интерфейси (API) за мрежова комуникация, файлове, процеси, потоци и достъп до операционната система. Често се използва за REST API интерфейси, GraphQL услуги, WebSocket сървъри в реално време, микроуслуги, CLI инструменти, прокси сървъри и API шлюзове. Основното му предимство е неблокиращият вход/изход (I/O): един процес може ефективно да обработва множество мрежови връзки. Недостатъкът е, че дългите и натоварващи процесора изчисления могат да блокират цикъла на събитията, освен ако не бъдат преместени в работни нишки, отделни процеси или друга услуга.
JavaScript се изпълнява във V8, докато програмните интерфейси на Node.js делегират асинхронната работа на операционната система или на пула от нишки на `libuv`. Главната JavaScript нишка не чака завършването на входно-изходните (I/O) операции; тя продължава да изпълнява друг код. Когато операцията приключи, нейната обратна функция или `Promise` продължение стават готови за цикъла на събитията. По този начин Node.js постига висока паралелност, въпреки че потребителският JavaScript код обикновено се изпълнява в една главна нишка.
3Какво представляват V8, `libuv` и цикълът на събитията?
V8 компилира и изпълнява JavaScript и управлява JavaScript обектите и паметта. `libuv` предоставя междуплатформен цикъл на събитията, интеграция на асинхронен вход/изход (I/O) и пул от работни нишки за операции като работа с файловата система, DNS заявки и криптография. Цикълът на събитията решава кога трябва да се изпълнят обратните функции (callbacks) на завършените операции. Самото Node.js свързва JavaScript API интерфейсите с нативни възможности на операционната система; V8 сам по себе си не предоставя HTTP сървър, достъп до файловата система или API интерфейси за управление на процеси.
Един процес на Node.js обикновено изпълнява потребителския JavaScript код в една главна нишка, поради което два стандартни обработчика на заявки не изпълняват JavaScript код едновременно в тази нишка. Въпреки това, платформата не се състои буквално от една нишка: libuv разполага с пул от нишки, операционната система извършва асинхронни входно-изходни операции, V8 изпълнява задачи по почистване на паметта, а Node.js също така поддържа `worker_threads` и дъщерни процеси. Този израз описва главно стандартния модел на изпълнение на JavaScript.
5Как Node.js може да обработва много заявки едновременно?
Node.js стартира входно-изходните операции и след това се връща към цикъла на събитията, вместо да блокира по една нишка за всяка заявка. Например, 1000 извиквания към базата данни могат да бъдат в процес на изпълнение, докато главната нишка продължава да приема връзки и да обработва готовите обратни извиквания. Това работи добре за натоварвания, зависими от входно-изходни операции, като HTTP извиквания, заявки към бази данни, файлове и сокети. Работи лошо, когато обработчиците на заявки извършват тежка синхронна работа на процесора, защото това блокира цикъла на събитията и забавя всички останали заявки.
6Кои са основните фази на цикъла на събитията в Node.js?
Node.js използва фазите на libuv, включително pending callbacks, idle/prepare, poll, check и close callbacks, заедно с обработката на таймери. poll обработва много входно-изходни обратни извиквания, check изпълнява setImmediate, а таймерите изпълняват setTimeout/setInterval след минималните им закъснения, когато планирането позволява. При libuv 1.45 и Node.js 20+ таймерите се обработват след poll във всяка итерация, с начален проход преди цикъла за съвместимост. process.nextTick има отделна опашка на Node; реакциите на Promise и queueMicrotask използват опашката за микрозадачи на V8. Нито една не е фаза на libuv. Тези опашки се изпълняват в контролни точки между обратните извиквания. Рекурсивното им попълване може да лиши останалата работа от време за изпълнение.
7Как се различават синхронните, асинхронните и неблокиращите операции?
Синхронността описва управлението: извикването връща резултат или хвърля изключение, преди извикващият код да продължи. Асинхронността описва резултат, доставен по-късно чрез обратно извикване, Promise или друго уведомяване. Блокирането е чакане за работа или ресурс, докато текущата нишка не може да върши друго. Неблокиращите интерфейси избягват това, например като връщат „операцията би блокирала“ или организират уведомяване. Това са различни характеристики: синхронност сама по себе си не означава чакане за входно-изходна готовност, а връщането на Promise не прави тежките синхронни изчисления неблокиращи. fs.readFileSync е блокиращ синхронен интерфейс; fs.promises.readFile доставя резултата асинхронно, докато главната нишка може да обработва друга работа. В обработката на заявки предпочитайте подходящи асинхронни входно-изходни интерфейси.
8Какво представлява гладуването на цикъла на събитията?
То възниква, когато синхронен JavaScript или непрекъснато попълвани опашки в контролните точки пречат на навременната обработка на входно-изходни обратни извиквания, таймери и заявки. Причини са тежки изчисления, големи JSON операции, рекурсивно планирани микрозадачи и прекомерна употреба на process.nextTick. Ограничете входните размери, профилирайте скъпата работа и преместете тежките изчисления в worker_threads или друга услуга. При разделяне на работата отстъпвайте управлението до следващ оборот на цикъла, например с setImmediate; многократно очакване на вече разрешени Promise или планиране на queueMicrotask все още може да лишава входа/изхода от изпълнение. Избягвайте блокиращи синхронни интерфейси в често изпълняваните пътища на заявките.
9Какво е обратно извикване и какво означава ад от обратни извиквания?
Обратното извикване е функция, подадена на друга операция, за да бъде извикана от нея. Може да се изпълнява синхронно, както при Array.map, или асинхронно, както при fs.readFile. Класическите асинхронни извиквания на Node.js обикновено поставят грешката като първи аргумент: callback(error, result). Адът от обратни извиквания е дълбоко влагане на зависими извиквания, което затруднява проследяването на управлението и грешките. Именувани функции, по-малки операции, Promise и async/await подобряват четимостта. Стартирайте независими операции заедно, когато е подходящо, например чрез Promise.all, като спазвате ограниченията за конкурентност и обработвате грешките.
import fs from 'node:fs';
fs.readFile('file.txt', 'utf8', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
});
Promise представлява бъдещия резултат от асинхронна операция и може да бъде в състояние на изчакване (pending), изпълнен (fulfilled) или отхвърлен (rejected). async функция винаги връща Promise. await спира временно само текущата async функция, докато Promise не се установи (settles); това не блокира целия процес на Node.js. Добрият отговор трябва да спомене обработката на грешки с try/catch, избягването на висящи Promise обекти и използването на Promise.all за независими задачи, за да не се изпълняват операциите ненужно последователно.
11Как се различават process.nextTick, queueMicrotask, setImmediate и setTimeout?
process.nextTick планира работа в next-tick опашката на Node; queueMicrotask използва опашката за микрозадачи на V8, както и реакциите на Promise. В нормална контролна точка на най-горното ниво на CommonJS nextTick се изпълнява преди чакащите Promise/queueMicrotask извиквания. На най-горното ниво на ES модул изпълнението вече е в контекст на микрозадача, така че чакащите микрозадачи могат да предхождат nextTick. Това зависи от контекста и не е универсален приоритет, прекъсващ текущия JavaScript. setImmediate се изпълнява в check. setTimeout(fn, 0) чака минимален времеви праг и не е незабавен. Взаимният им ред зависи от мястото на планиране; ако и двете са планирани в едно входно-изходно обратно извикване, setImmediate е първи. За преносимо отлагане като микрозадача предпочитайте queueMicrotask, освен ако е нужно специфичното поведение на nextTick.
// CommonJS file (.cjs): nextTick precedes promise at this top level.
// ES module file (.mjs): promise precedes nextTick at this top level.
setTimeout(() => console.log('timeout'), 0);
setImmediate(() => console.log('immediate'));
Promise.resolve().then(() => console.log('promise'));
process.nextTick(() => console.log('nextTick'));
// timeout vs immediate is context-dependent here.
Използвайте try/catch за синхронни изключения. При операции с Promise използвайте await в try/catch или добавете обработчик за отхвърляне; външен синхронен try/catch не прихваща изключение от по-късно обратно извикване. Обратните извиквания с грешка като първи аргумент трябва да го проверяват, а събитията error на EventEmitter изискват подходящи обработчици. Сървърите трябва да централизират обработката на грешки, да записват полезен контекст и да не разкриват вътрешни подробности на клиентите. След uncaughtException не приемайте, че процесът може безопасно да продължи: извършете само необходимото синхронно аварийно почистване, прекратете процеса и оставете надзорен процес да го рестартира. Планираното плавно спиране е полезно за нормално прекратяване, но произволно асинхронно възстановяване след неприхванато изключение не е надеждна стратегия.
Модулът е изолирана единица код с изрично експортиран интерфейс. Node.js поддържа вградени модули като node:fs и node:http, локални модули на проекта, npm пакети, CommonJS модули и ECMAScript модули. Модулите правят границите на отговорност по-ясни и позволяват преизползване. Компромисът в продукционна среда е свързаността: ако модулите се импортират взаимно твърде дълбоко или скриват странични ефекти по време на импортиране, тестването и поведението при стартиране стават по-трудни за анализиране.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from './math.js';
14Как се различават require в CommonJS и import в ES модулите?
CommonJS използва require и module.exports, а ES модулите — import и export. require обикновено зарежда синхронно и може да бъде извикан условно. Статичните import декларации разкриват графа на зависимостите за анализ; динамичният import() връща Promise. Съвременният Node.js може с require да зареди и граф от ES модули без await на най-горното ниво според поддържаните правила за съвместна работа; ES модулите не означават винаги асинхронно изпълнение. Режимът се определя от .cjs/.mjs, type в package.json и приложимите правила за разпознаване на синтаксиса. ES модулите поддържат await на най-горното ниво. CommonJS предоставя __dirname/__filename; актуалният Node.js предоставя import.meta.dirname/filename за файлови ES модули. import.meta.url е полезен за разрешаване чрез URL и подходи, съвместими с по-стари версии.
// File: math.cjs (CommonJS)
function calculate(a, b) { return a + b; }
module.exports = { calculate };
// Separate file: math.mjs (ES module)
export function calculate(a, b) { return a + b; }
// These are two separate files, not one combined script.
npm е интерфейсът на командния ред за управление на пакети. package.json описва метаданни, скриптове и изисквания към преките зависимости, често чрез диапазони от версии. package-lock.json записва разрешено дърво на зависимостите с версии и данни за целостта. Включвайте заключващия файл на приложението в контрола на версиите. npm ci е полезен при непрекъсната интеграция: инсталира от заключващия файл, премахва съществуващия node_modules и отказва при несъответствие с package.json. Заключващият файл намалява отклоненията в зависимостите; не уеднаквява операционните системи, нативните компилации, скриптовете от жизнения цикъл, версиите на Node или опциите за инсталиране. Възпроизводимите компилации изискват и контролирани инструменти и среда.
16Как трябва да се управляват конфигурацията и променливите на средата?
Четете променливите чрез process.env: наличните стойности са низове, а липсваща стойност е undefined. Проверявайте задължителните стойности и ги преобразувайте целенасочено при стартиране; само Number за PORT не отхвърля празен низ, NaN или порт извън допустимия диапазон. Прекратявайте с ясна грешка при невалидна задължителна конфигурация. Не съхранявайте истински тайни в контрола на версиите; подавайте производствените тайни чрез платформата за внедряване или мениджър на тайни. Примерна конфигурация без тайни може да се включи в хранилището. NODE_ENV е приложна конвенция, а не надеждна граница на сигурност.
const rawPort = process.env.PORT ?? '3000';
if (!/^\d+$/.test(rawPort)) throw new Error('PORT must be an integer');
const port = Number(rawPort);
if (!Number.isSafeInteger(port) || port < 1 || port > 65535) {
throw new Error('PORT must be between 1 and 65535');
}
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl?.trim()) throw new Error('DATABASE_URL is required');
EventEmitter реализира шаблона издател-абонат в рамките на един Node.js процес. on регистрира слушател, once регистрира слушател само за едно събитие, off премахва слушател, а emit извиква слушателите синхронно в реда на тяхната регистрация. Синхронната същност е важна: бавен слушател блокира излъчвателя. Събитието error е специално, тъй като необработено събитие за грешка може да срине процеса. Системите с дълъг живот трябва също така да премахват неизползваните слушатели, за да предотвратят течове на памет.
import { EventEmitter } from 'node:events';
const emitter = new EventEmitter();
emitter.on('orderCreated', order => {
console.log(`Order ${order.id} created`);
});
emitter.emit('orderCreated', { id: 42 });
`Buffer` представлява последователност от байтове с фиксиран размер. Използва се за файлове, TCP сокети, тела на HTTP съобщения, изображения, криптография и двоични протоколи. `Buffer.from` създава буфер от съществуващи данни, `Buffer.alloc` създава запълнена с нули памет, а `Buffer.allocUnsafe` е по-бърз, но може да съдържа стара памет, докато не бъде презаписан. Това прави `allocUnsafe` подходящ само когато кодът незабавно запълва буфера и никога не разкрива стари данни.
const buffer = Buffer.from('Hello', 'utf8');
console.log(buffer.toString('utf8'));
const safe = Buffer.alloc(1024);
// Prefer alloc for general use. Never expose allocUnsafe bytes before filling them.
const fast = Buffer.allocUnsafe(1024);
fast.fill(0); // All bytes initialized before use.
19Какво представляват потоците и какви видове съществуват?
Потоците обработват данните на порции, вместо да зареждат всичко в паметта. Основните видове са Readable, Writable, Duplex и Transform. Файловете, обектите за HTTP заявки и отговори, TCP сокетите и gzip потоците са често срещани примери. Потоците са изключително полезни при работа с големи файлове и мрежов трафик, защото намаляват използването на паметта и латентността. Един отговор на ниво продукционна среда трябва да спомене, че грешките в потоците трябва да се обработват правилно по цялата верига.
20Какво е обратно налягане (backpressure) и защо функцията pipeline е полезна?
Обратното налягане възниква, когато източник на данни произвежда порции данни по-бързо, отколкото местоназначението може да ги обработи. Без управление на обратното налягане, използваната памет може да нарасне до степен, в която процесът се забавя или срива. Потоците и `pipe` могат да координират потока от данни, но функцията `pipeline` от `node:stream/promises` е по-безопасна за многостъпкови вериги, защото разпространява грешките, затваря свързаните потоци и връща `Promise`. Тя е добър избор по подразбиране за компресиране на файлове, качване, изтегляне и трансформации на потоци.
import fs from 'node:fs';
import zlib from 'node:zlib';
import { pipeline } from 'node:stream/promises';
await pipeline(
fs.createReadStream('input.txt'),
zlib.createGzip(),
fs.createWriteStream('input.txt.gz')
);