Przygotowanie do rozmów rekrutacyjnych z Node.js i Express: 70 pytań z dokładnymi odpowiedziami
Strukturalny przegląd 70 pytań z rozmów rekrutacyjnych dotyczących Node.js i Express, obejmujący tematy od pętli zdarzeń i strumieni po middleware, bezpieczeństwo oraz skalowanie, wraz z korektami często popełnianych błędów.
Rozmowy kwalifikacyjne dotyczące stanowisk w Node.js zazwyczaj krążą wokół tych samych tematów: jak silnik jednowątkowy obsługuje tysiące połączeń, co tak naprawdę robi pętla zdarzeń oraz jak Express przekształca surowy serwer HTTP w aplikację. Wypowiedzi zapamiętane na pamięć zawodzą przy dodatkowych pytaniach, dlatego ta przeglądka grupuje 70 najczęściej zadawanych pytań według tematów i podaje do każdej odpowiedzi wystarczająco dużo uzasadnienia, aby można ją było obronić. Gdzie popularna odpowiedź jest przestarzała lub nieprecyzyjna, zaznaczono poprawkę, ponieważ właśnie dostrzeganie tych szczegółów często odróżnia silnego kandydata od tego, który tylko się przygotował.
Zasady Node.js
Q1. Co to jest Node.js?
Jest to środowisko wykonawcze, które uruchamia JavaScript poza przeglądarką. Zawiera silnik V8 firmy Google oraz dodatkowe API do obsługi plików, sieci i procesów. Jego model opiera się na zdarzeniach z nieblokującym wejściem/wyjściem, co sprawia, że jest lekki i doskonale nadaje się do usług wymagających dużej ilości danych oraz działających w czasie rzeczywistym.
Q2. Czy Node.js jest jednowątkowy?
Twój JavaScript działa na jednym głównym wątku, który steruje pętlą zdarzeń. W tle libuv utrzymuje zbiór wątków do obsługi zadań, które nie mają odpowiedniej asynchronicznej interfejsu systemowego, takich jak wywołania systemu plików, dns.lookup, niektóre funkcje crypto oraz zlib. Zatem dokładna odpowiedź brzmi: jednowątkowy dla twojego kodu, wielowątkowy na poziomie systemu.
Q3. Dlaczego Node.js jest szybki?
- V8 kompiluje JavaScript do kodu maszynowego zamiast go interpretować.
- Wejście i wyjście są asynchroniczne i nieblokujące.
Q4. Kod synchroniczny versus asynchroniczny
Operacja synchroniczna kończy się przed uruchomieniem następnej linii kodu, blokując wszystko, co następuje po niej. Operacja asynchroniczna, taka jak odczyt pliku lub zapytanie do bazy danych, jest rozpoczynana i przekazywana dalej; jej wykonywanie trwa dalej, a wynik jest dostarczany później za pomocą funkcji zwrotnej lub obietnicy. W poniższej wersji synchronicznej log czeka na zakończenie całego odczytu (i wyświetla Buffer, ponieważ nie podano żadnego kodowania):
// Synchronous
const data = fs.readFileSync('file.txt');
console.log(data);
W wersji asynchronicznej ostatnia linia logu jest wyświetlana przed treścią pliku, ponieważ odczyt kończy się w późniejszym momencie podczas kolejnego cyklu pętli zdarzeń:
// Asynchronous
fs.readFile('file.txt', (err, data) => {
console.log(data);
});
console.log("This runs first!");
Q5. Co to jest npm?
npm to menedżer pakietów dostępny wraz z Node.js. Używa się go do instalowania, publikowania i zarządzania zależnościami, a także do ich rejestrowania wraz ze skryptami w pliku package.json.
Q6. package.json versus package-lock.json
package.json opisuje projekt: jego nazwę, wersję, punkt wejścia, skrypty oraz zakresy zależności, które są akceptowane. package-lock.json określa dokładną wersję każdego zainstalowanego pakietu, włączając te transywne, tak aby na każdym komputerze i podczas wykonywania testów CI instalowano identyczną strukturę. Zapisz oba pliki.
Q7. Które moduły podstawowe powinieneś znać?
Powszechnie wymieniane wbudowane moduły to fs, path, http i https, os, events, stream, buffer, crypto, util, url, querystring (obecnie uważany za przestarzały na rzecz URLSearchParams), child_process, cluster oraz zlib.
Q8. require() versus import
Różnice pomiędzy CommonJS a modułami ES pojawiają się ciągle:
requireładuje dane synchronicznie; moduły ES są ładowane asynchronicznie i oferują również dynamiczneimport(), które zwraca obietnicę.- CommonJS jest standardem dla plików
.js; moduły ES wymagają pola"type": "module"w plikupackage.jsonlub rozszerzenia.mjs.
module.exports; moduły ES używają export oraz export default.require można wywołać w dowolnym miejscu, włącznie z wnętrzem warunków; statyczne deklaracje import są przenoszone na górę i muszą znajdować się na najwyższym poziomie.Q9. module.exports versus exports
exports początkowo odnosi się do tego samego obiektu co module.exports. Dodawanie do niego właściwości działa, ale przypisanie nowej wartości (exports = {...}) jedynie ponownie powiązuje zmienną lokalną, a moduł nadal eksportuje oryginalny obiekt. Aby zastąpić cały eksport, należy przypisać wartość do module.exports.
Q10. Co to jest REPL?
REPL to skrót od Read, Eval, Print, Loop. Uruchomienie node bez argumentów otwiera interaktywną shell, która ewaluje każdą wpisaną przez użytkownika wyrażenie i wyświetla wynik, co jest przydatne do szybkich eksperymentów.
Pętla zdarzeń i wzorce asynchroniczne
Q11. Co to jest pętla zdarzeń?
To mechanizm umożliwiający pojedynczemu wątkowi JavaScript wykonywanie operacji wejścia/wyjścia w trybie nieblokującym. Gdy stos wywołań jest pusty, pętla bierze następny gotowy callback z kolejek i go uruchamia. Szerszy opis sposobu, w jaki pętla ta współpracuje z libuv, znajduje się w naszym artykule o libuv, pętli zdarzeń i puli wątków.
Q12. Jakie są fazy pętli zdarzeń?
- Czasy wyznaczone: callbacki z
setTimeoutisetInterval, których czas już nadszedł.
setImmediate().socket.on('close').Kolejki mikrozadań (process.nextTick najpierw, a potem reakcje na obietnice) są opróżniane po każdym pojedynczym callbacku, a nie tylko pomiędzy fazami. Takie zachowanie występuje od wersji Node 11 i jest często zadawane pytanie.
Q13. nextTick, obietnice, setTimeout i setImmediate
- Callbacki z
process.nextTick()są uruchamiane zaraz po zakończeniu bieżącej operacji, przed mikrozadaniami opartymi na obietnicach.
.then, są wykonywane po tym, jak kolejka nextTick zostanie opróżniona, ale jeszcze przed kontynuacją pętli.setTimeout(fn, 0) jest uruchamiany w fazie timerów po upływie minimalnego opóźnienia.setImmediate() jest uruchamiany w fazie sprawdzania, tuż po fazie przeglądania tej samej iteracji.To skrypt wykorzystuje wszystkie cztery z nich:
console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('nextTick'));
Promise.resolve().then(() => console.log('promise'));
console.log('end');
Oczekiwany wynik pokazuje najpierw logi synchroniczne, a następnie mikrozadania. Dwa makrozadania mogą pojawić się w dowolnej kolejności po zaplanowaniu z modułu głównego, ponieważ zależy to od tego, czy upłynęła już granica czasowa timera w momencie rozpoczęcia pętli; wewnątrz funkcji zwrotnej pliku lub socketu setImmediate zawsze ma pierwszeństwo:
// Output:
// start
// end
// nextTick
// promise
// setTimeout (or setImmediate — order can vary if not inside I/O)
// setImmediate (or setTimeout)
Q14. Funkcje zwrotne i piekło funkcji zwrotnych
Callback to funkcja, którą przekazujemy operacji, aby mogła zostać wywołana po jej zakończeniu. Gdy każdy krok zależy od poprzedniego, callbacki układają się coraz głębiej, tworząc strukturę przypominającą piramidę znaną jako „callback hell”, co utrudnia czytanie kodu oraz obsługę błędów w:
getUser(id, (user) => {
getPosts(user, (posts) => {
getComments(posts, (comments) => {
// deeply nested - hard to manage
});
});
});
Promises oraz async/await uproszczają tę strukturę.
Q15. Co to jest Promise?
Promise to obiekt reprezentujący przyszły wynik operacji asynchronicznej. Może być w stanie oczekiwania, spełniony z określoną wartością lub odrzucony z powodem; po ustaleniu się jego stanu nie zmienia się już więcej. Do obsługi zdarzeń używamy metod .then i .catch:
const promise = new Promise((resolve, reject) => {
if (success) resolve(data);
else reject(error);
});
promise.then(data => console.log(data)).catch(err => console.error(err));
Q16. Co to jest async/await?
Jest to składnia oparta na obietnicach, która umożliwia asynchronicznemu kodowi przetwarzanie danych od góry do dołu. Funkcja async zawsze zwraca obietnicę, a await wstrzymuje jej wykonywanie do chwili, aż oczekiwana obietnica zostanie zrealizowana lub odrzucona, przy czym odrzucenia pojawiają się jako wyjątki, które można przechwycić:
async function getUser() {
try {
const res = await fetch('/api/user');
const data = await res.json();
return data;
} catch (err) {
console.error(err);
}
}
Należy zauważyć, że ten przykład loguje błąd i domyślnie zwraca undefined, więc wywołujący nie mogą odróżnić niepowodzenia od pustego wyniku. Często lepszym rozwiązaniem jest ponowne rzucenie wyjątku.
Q17. Kombinatory obietnic
Promise.all()zwraca wartość, gdy wszystkie wejścia zostaną zrealizowane, a odrzuca wartość natychmiast po tym, jak jakiekolwiek wejście zostanie odrzucone.Promise.race()zwraca wartość pierwszego wejścia, które zostanie zrealizowane lub odrzucone.Promise.allSettled()czeka na wszystkie wejścia i raportuje każdy wynik.
Promise.any() realizuje się przy pierwszym zrealizowanym elemencie wejściowym i odrzuca (z błędem AggregateError) tylko wtedy, gdy wszystkie zostaną odrzucone.Q18. Kod blokujący versus nieblokujący
Kod blokujący zatrzymuje wszystko inne, dopóki nie zostanie zakończony, tak jak robi to fs.readFileSync. Kod nieblokujący rozpoczyna pracę i pozwala programowi kontynuować działanie, tak jak robi to fs.readFile. Na serwerze jedna wywołanie blokujące zatrzymuje wszystkie trwające żądania.
Strumienie, bufory i zdarzenia
Q19. Co to są strumienie?
Strumienie przetwarzają dane stopniowo, po fragmentach, zamiast przechowywać je wszystkie w pamięci. Są odpowiednim narzędziem do dużych plików, mediów i ruchu sieciowego. Istnieją cztery rodzaje:
- Dostępne do odczytu, takie jak
fs.createReadStream - Dostępne do zapisu, takie jak
fs.createWriteStream - Duplex – możliwość jednoczesnego odczytu i zapisu, jak w przypadku gniazda TCP
- Transform – strumień typu duplex, który modyfikuje dane w trakcie ich przepływu, np.
zlib.createGzip
Q20. Co to jest bufor?
Bufer przechowuje surowe dane binarne w pamięci przydzielonej poza pulą V8. Spotykamy go tam, gdzie dane są reprezentowane jako bajty, a nie łańcuchy lub obiekty: w strumieniach, plikach, gniazdach. Stworzenie bufora z łańcucha pokazuje podstawowe wartości bajtów w formacie szesnastkowym:
const buf = Buffer.from('Hello');
console.log(buf); // <Buffer 48 65 6c 6c 6f>
Q21. Co to jest EventEmitter?
EventEmitter z modułu events implementuje wzorzec obserwatora (pub/sub), na którym opiera się duża część API Node.js. Tworzy się instancję tego obiektu:
const EventEmitter = require('events');
const emitter = new EventEmitter();
Następnie rejestruje się słuchacze za pomocą metody on, a potem uruchamia je za pomocą emit, przekazując przy tym wszelkie argumenty:
emitter.on('greet', (name) => console.log(`Hello, ${name}`));
emitter.emit('greet', 'John');
Słuchacze są wykonywane synchronicznie, w kolejności rejestracji, gdy zostanie wywołana funkcja emit.
Q22. Jak przetwarzać duże ilości danych bez wyczerpania pamięci?
Należy używać strumieni: czytać małe fragmenty i przekierowywać strumień do odczytu do strumienia do zapisu, dzięki czemu zużycie pamięci pozostaje mniej więcej stałe, niezależnie od rozmiaru pliku:
fs.createReadStream('input.txt').pipe(fs.createWriteStream('output.txt'));
Dla kodu produkcyjnego stream.pipeline jest lepszy od .pipe(), ponieważ przenosi błędy i usuwa wszystkie strumienie z łańcucha.
Głębsze koncepcje związane z czasem wykonywania
Q23. process.nextTick() kontra setImmediate(), szczegółowo
nextTick – funkcje zwrotne są wykonywane przed kontynuacją pętli, nawet przed mikrozadaniami typu promise. Funkcje zwrotne setImmediate czekają na fazę sprawdzania, po fazie przeglądania. Ponieważ każdy nextTick może zaplanować kolejny, łańcuch rekurencyjny może „zahamować” pętlę, przez co oczekujące operacje odczytu i zapisu nigdy nie zostaną obsłużone. Ten tryb awarii jest omówiony w artykule jak process.nextTick w tajemnicy „zahamowuje” pętlę zdarzeń w Node.js.
Q24. Co to jest klastrowanie i dlaczego je używać?
Jeden proces Node wykorzystuje jeden rdzeń do obsługi JavaScripta. Moduł cluster tworzy kilka procesów roboczych, z których każdy ma własną pętlę zdarzeń i pamięć, a jednocześnie umożliwia im współdzielenie jednego portu serwerowego, dzięki czemu wszystkie rdzenie maszyny mogą obsługiwać ruch. Moduły te importuje się w następujący sposób:
const cluster = require('cluster');
const os = require('os');
Następnie z procesu głównego tworzy się po jednym pracowniku na każdy CPU, podczas gdy pracownicy ładowają serwer:
if (cluster.isMaster) {
os.cpus().forEach(() => cluster.fork());
} else {
require('./server');
}
cluster.isMaster nadal funkcjonuje, ale został uznany za przestarzały na rzecz cluster.isPrimary; w nowym kodzie należy używać tego drugiego.
Q25. cluster versus worker_threads
clustertworzy oddzielne procesy z izolowaną pamięcią, co jest odpowiednie do skalowania serwerów HTTP.worker_threadstworzy wątki w obrębie jednego procesu. Mogą one dzielić się pamięcią za pomocąSharedArrayBufferlub przekazywać bufory, co nadaje się do zadań wymagających dużych zasobów CPU, takich jak przetwarzanie obrazów.
Q26. Jak radzi sobie z zadaniami wymagającymi dużych zasobów CPU?
Kod obciążający CPU blokuje pętlę zdarzeń, dlatego należy przenieść go w inne miejsce: do worker_threads, do procesu potomnego lub całkowicie poza usługę za pomocą kolejki i pracowników w tle. Szukaj wzorów w artykule offloading CPU work with worker threads and pools.
Q27. Czym jest middleware w ogólności?
Middleware to kod znajdujący się pomiędzy otrzymaniem żądania a wysłaniem odpowiedzi, gdzie może on sprawdzać, modyfikować, walidować lub rejestrować dane. Cała architektura Express opiera się na tej koncepcji, jak pokazują poniższe sekcje dotyczące Express.
Q28. Czym są procesy potomne?
Moduł child_process uruchamia zewnętrzne programy lub inne skrypty Node w oddzielnych procesach. Istotne są cztery funkcje:
spawn()przekazuje wyniki w formie strumieni, dzięki czemu dobrze radzi sobie z dużymi ilościami danych.
exec() uruchamia shell i buforuje wyniki, co jest odpowiednie dla małych ilości danych.execFile() uruchamia plik bezpośrednio, bez użycia shella, co jest bezpieczniejsze przy niepewnych argumentach.fork() to specjalizowana wersja spawn dla modułów Node, która tworzy kanał IPC do przekazywania wiadomości.Q29. Co to jest libuv?
libuv to biblioteka w języku C, która dostarcza pętlę zdarzeń Node, asynchroniczną obsługę wejścia/wyjścia oraz zbiór wątków, ukrywając różnice pomiędzy mechanizmami powiadamiania o zdarzeniach w systemach Linux, macOS i Windows.
Q30. setTimeout versus setInterval
setTimeout(fn, delay) uruchamia funkcję fn raz po upływie co najmniej delay milisekund. setInterval(fn, delay) uruchamia ją wielokrotnie w tym interwale, dopóki nie zostanie wywołana funkcja clearInterval().
Q31. Obsługa nieprzechwyconych wyjątków i nierozpatrzonych odrzuceń
Istnieją obsługi na poziomie procesu dla obu przypadków. W przypadku synchronicznych wyjątków, których nikt nie przechwycił, należy zalogować błąd i zakończyć działanie procesu, ponieważ może on znajdować się w niespójnym stanie:
process.on('uncaughtException', (err) => {
console.error('Uncaught Exception:', err);
process.exit(1); // recommended: exit after cleanup
});
W przypadku obietnic odrzuconych bez obsługi:
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled Rejection:', reason);
});
Od wersji Node 15 nierozpatrzone odrzucenie domyślnie powoduje awarię procesu, więc rejestracja tego słuchacza zmienia to zachowanie. Traktuj oba przypadki jako rozwiązanie ostateczne w postaci logowania, a nie jako element przepływu sterowania; używaj try/catch oraz .catch() bezpośrednio w kodzie.
Q32. Co to jest wyciek pamięci i jak go znaleźć?
Ucieczka pamięci to taka pamięć, która pozostaje dostępna po tym, jak przestała być potrzebna: rosnące globalne bufory, timerzy lub słuchacze, których nigdy nie usunięto, oraz zamknięcia przechowujące duże obiekty. Diagnozuj to za pomocą flagi --inspect i zrzutów pamięci w Chrome DevTools, lub narzędzi takich jak heapdump i clinic.js, porównując zrzuty w czasie.
Q33. __dirname versus __filename
__dirname to absolutna ścieżka do folderu zawierającego bieżący moduł; __filename to absolutna ścieżka do samego pliku. Obie istnieją tylko w CommonJS. W modułach ES uzyskuj je z import.meta.url, lub użyj import.meta.dirname i import.meta.filename w nowszych wersjach Node.
Q34. Dlaczego używać modułu path?
path umożliwia tworzenie i analizę ścieżek plików w sposób uniwersalny za pomocą funkcji takich jak path.join, path.resolve, path.basename oraz path.extname, dzięki czemu nie musisz ręcznie łączyć ciągów znaków i mierzyć się z różnicami między ukośnikami Windows a Unix.
Q35. fs.readFile versus fs.createReadStream
fs.readFile ładuje cały plik do pamięci przed zwróceniem wyniku, co stanowi ryzyko przy dużych plikach. fs.createReadStream dostarcza plik w fragmentach, utrzymując niski zużycie pamięci i umożliwiając natychmiastowe rozpoczęcie przetwarzania.
Podstawy Express
Q36. Co to jest Express?
Express to minimalistyczna, bezstronna ramka robocza dla stron internetowych oparta na module http w Node. Dodaje funkcje routingu, pipeline middleware oraz integrację z silnikiem szablonów, eliminując większość standardowych elementów związanych z obsługą żądań.
Q37. Podstawowy serwer Express
Stwórz obiekt aplikacji:
const express = require('express');
const app = express();
Następnie zarejestruj trasę i rozpocznij odsłuchiwanie:
app.get('/', (req, res) => {
res.send('Hello World');
});app.listen(3000, () => console.log('Server running on port 3000'));
Q38. Co to jest middleware w Express?
Middleware w Express to funkcja, która otrzymuje parametry req, res oraz next. Może wykonywać dowolny kod, modyfikować żądanie lub odpowiedź, zakończyć proces odpowiedzią lub wywołać next(), aby przekazać kontrolę następnej funkcji. Klasycznym przykładem jest logger żądań:
app.use((req, res, next) => {
console.log(`${req.method} ${req.url}`);
next();
});
Jeśli middleware nie odpowiada ani nie wywołuje next(), żądanie utyka.
Q39. Rodzaje middleware w Express
- Na poziomie aplikacji, przytwierdzane za pomocą
app.use()lubapp.METHOD(). - Na poziomie routera, przytwierdzane do instancji
express.Router().
(err, req, res, next).express.json() i express.static().cors, morgan i helmet.Q40. Jak działa routowanie?
Trasa łączy metodę HTTP z ścieżką oraz obsługującym ją skryptem. Typowa zasoba typu CRUD wygląda w ten sposób:
app.get('/users', (req, res) => { /* get all users */ });
app.post('/users', (req, res) => { /* create user */ });
app.put('/users/:id', (req, res) => { /* update user */ });
app.delete('/users/:id', (req, res) => { /* delete user */ });
Q41. Co to jest express.Router()?
Router to samodzielna mini-aplikacja z własnymi trasami i middleware, którą montuje się na określonej ścieżce. Umożliwia uporządkowanie dużych aplikacji według funkcjonalności. Trasy definiuje się w osobnym module:
// userRoutes.js
const router = require('express').Router();
router.get('/', (req, res) => res.send('All users'));
module.exports = router;
a następnie montuje się go w głównej aplikacji, gdzie ścieżka / staje się /users:
// app.js
app.use('/users', require('./userRoutes'));
Q42. app.use() w porównaniu z app.get() i podobnymi funkcjami
app.use() pasuje do każdej metody HTTP oraz do dowolnej ścieżki zaczynającej się od podanego prefiksu, dlatego właśnie montuje middleware i routery. app.get(), app.post() oraz podobne funkcje pasują do jednej konkretnej metody i pełnego wzoru ścieżki.
Q43. req.params, req.query i req.body
req.paramsprzechowuje nazwane segmenty ścieżki:/users/:iddajereq.params.id.req.queryprzechowuje wartości ciągu zapytania:/users?age=25dajereq.query.age, zawsze w formie ciągu znaków.req.bodyprzechowuje zinterpretowany treść żądania dla metod takich jak POST i PUT; pozostaje nieskończony, chyba że najpierw zostanie uruchomiony middleware do analizy treści, np.express.json().
Q44. Analiza JSON i danych formularza
Wbudowane parserzy obejmują większość przypadków – jeden do treści JSON, a drugi do form HTML z kodowaniem URL:
app.use(express.json()); // parses application/json
app.use(express.urlencoded({ extended: true })); // parses form data
Q45. Co to jest express.static()?
Jest to wbudowany middleware, który serwuje pliki takie jak obrazy, arkusze stylów, skrypty i HTML z określonego katalogu:
app.use(express.static('public'));
Średni poziom Express
Q46. Obsługa błędów
Błędy są obsługiwane za pomocą middleware’u z czterema parametrami, zarejestrowanego po wszystkich trasach:
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ message: 'Something went wrong' });
});
Wewnątrz tras i middleware’ów przekazuj błędy za pomocą next(err):
app.get('/user/:id', async (req, res, next) => {
try {
const user = await getUser(req.params.id);
res.json(user);
} catch (err) {
next(err); // passed to error-handling middleware
}
});
Unikaj wysyłania err.stack lub informacji wewnętrznych do klientów w środowisku produkcyjnym.
Q47. Obsługa błędów 404
Express nie posiada wbudowanej obsługi błędów 404 w formacie JSON. Dodaj middleware ogólny po każdej trasie; jeśli żądanie trafi do niego, nic innego się nie dopasowało:
app.use((req, res) => {
res.status(404).json({ message: 'Route not found' });
});
Q48. CORS i pakiet cors
Cross-Origin Resource Sharing to mechanizm przeglądarki, który decyduje, czy strona z jednego źródła może odczytywać odpowiedzi z innego. Przeglądarka to egzekwuje; serwer decyduje o tym poprzez nagłówki odpowiedzi, które pakiet cors ustawia za Ciebie:
const cors = require('cors');
app.use(cors()); // allow all origins
app.use(cors({ origin: 'https://example.com' })); // restrict origin
Ukazano tu dwie alternatywy: możliwość korzystania przez wszystkie źródła lub tylko jedno; należy wybrać jedną z nich, a nie obie, ponieważ pierwsza opcja będzie miała priorytet.
Q49. Sesje i pliki cookie
Włóż środowisko pośredniczące do obsługi plików cookie i sesji:
const cookieParser = require('cookie-parser');
const session = require('express-session');
następnie zarejestruj je przy użyciu tajemniczego klucza podpisywania oraz odpowiednich opcji:
app.use(cookieParser());
app.use(session({
secret: 'mySecretKey',
resave: false,
saveUninitialized: true
}));
Zachowaj tę tajemnicę w zmiennej środowiskowej, a nie w kodzie źródłowym, i rozważ ustawienie saveUninitialized: false, aby uniknąć tworzenia sesji dla anonimowych odwiedzających. Standardowe przechowywanie w pamięci nie jest przeznaczone do użycia w środowisku produkcyjnym.
Q50. Autoryzacja JWT
Importuj bibliotekę:
const jwt = require('jsonwebtoken');
Poniższy kod podpisuje token zawierający identyfikator użytkownika przy logowaniu, chroni ścieżki za pomocą middleware, który wyodrębnia token nośny z nagłówka Authorization i sprawdza go, a następnie przypisuje zdekodowany payload do req.user:
// Generate token on login
const token = jwt.sign({ id: user.id }, 'secretKey', { expiresIn: '1h' });// Middleware to protect routes
function auth(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
if (!token) return res.status(401).json({ message: 'No token' });
try {
req.user = jwt.verify(token, 'secretKey');
next();
} catch (err) {
res.status(403).json({ message: 'Invalid token' });
}
}app.get('/protected', auth, (req, res) => res.json({ data: 'secret data' }));
Tak jak w przypadku sesji, stała wartość 'secretKey' powinna pochodzić z konfiguracji. Odpowiedź 401 w przypadku braku tokena oraz 403 w przypadku nieprawidłowego tokena to powszechna konwencja, chociaż wiele API zwraca 401 w obu przypadkach.
Q51. Walidacja żądania
Biblioteki takie jak express-validator lub joi definiują reguły i zbierają błędy. Z express-validator importuje się pomocniki:
const { body, validationResult } = require('express-validator');
Następnie łączy się middleware z regułami przed obsługą żądania i sprawdza wynik:
app.post('/user',
body('email').isEmail(),
body('password').isLength({ min: 6 }),
(req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
// proceed
}
);
Aby dokonać bardziej szczegółowego porównania metod walidacji, zobacz Zod versus express-validator.
Q52. Wgrzewanie plików
multer parsuje dane typu multipart/form-data. Konfiguruj miejsce przechowywania plików:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
i zastosuj to dla każdej trasy; upload.single('file') oczekuje jednego pliku w polu o nazwie file i udostępnia go jako req.file:
app.post('/upload', upload.single('file'), (req, res) => {
res.json({ file: req.file });
});
W rzeczywistych systemach należy również ograniczyć rozmiar i typ plików, a także nigdy nie ufać nazwie pliku dostarczonej przez klienta.
Q53. Połączenie z MongoDB za pomocą Mongoose
Nawiąż połączenie:
const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost:27017/mydb');
następnie zdefiniuj model i użyj go w danej trasie:
const User = mongoose.model('User', new mongoose.Schema({
name: String,
email: String
}));app.post('/users', async (req, res) => {
const user = await User.create(req.body);
res.status(201).json(user);
});
Przekazywanie bezpośrednio req.body do funkcji create pozwala klientom ustawiać dowolne pola schematu, dlatego należy najpierw zweryfikować dane lub wybrać tylko dozwolone pola.
Q54. Kolejność wykonywania middleware
Express uruchamia middleware w kolejności, w jakiej zostały one zarejestrowane, od góry do dołu, a łańcuch zatrzymuje się przy pierwszej funkcji, która nie wywołuje next(). To właśnie z tego powodu obsługa błędów 404 i inne mechanizmy obsługi błędów znajdują się na końcu.
Q55. Silniki szablonów
Silniki szablonów renderują HTML na podstawie danych po stronie serwera. Popularnymi opcjami są EJS, Pug (wcześniej Jade) oraz Handlebars. Aby je uruchomić, potrzebna jest konfiguracja silnika szablonów oraz wywołanie funkcji res.render:
app.set('view engine', 'ejs');
app.get('/', (req, res) => res.render('index', { name: 'John' }));
Zaawansowane zastosowania Express i Node
Q56. Błędy asynchroniczne w Express 4 w porównaniu z Express 5
W Express 4 odrzucona obietnica z obsługiwanego elementu async nie jest przechwytywana przez framework; bez bloków try/catch w połączeniu z wywołaniem next(err) lub odpowiedniego otulacza, staje się odrzuceniem nierozwiązанныm. Express 5 automatycznie przekazuje odrzucone obietnice z elementów obsługujących i middleware do mechanizmu obsługi błędów. Typowym rozwiązaniem w Express 4 jest mała funkcja wyższego rzędu:
// Express 4 workaround
const asyncHandler = fn => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);
Otulenie elementu obsługującego taką funkcją kieruje każde odrzucenie do wywołania next:
app.get('/data', asyncHandler(async (req, res) => {
const data = await fetchData(); // if this throws, asyncHandler catches it
res.json(data);
}));
Ten wzorzec jest szczegółowo opisany w artykule usuwanie standardowego kodu try/catch za pomocą asyncHandler.
Q57. Strukturyzacja dużej aplikacji Express
Częsty układ w stylu MVC dzieli różne aspekty aplikacji na osobne foldery:
project/
├── controllers/ # business logic
├── routes/ # route definitions
├── models/ # DB schemas
├── middlewares/ # custom middleware
├── config/ # DB, env configs
├── utils/ # helper functions
├── app.js
└── server.js
Trasy deklarują punkty końcowe i przekazują je do kontrolerów, które zawierają logikę obsługi żądań, natomiast modele odpowiadają za zabezpieczenie dostępu do danych. Rozdzielenie pliku app.js (konfiguracja aplikacji) od pliku server.js (uruchamianie jej) ułatwia również importowanie aplikacji w testach.
Q58. Ograniczanie szybkości
express-rate-limit liczy żądania od danego klienta w określonym czasie. Należy go zaimportować:
const rateLimit = require('express-rate-limit');
i zastosować ogranicznik – w tym przypadku 100 żądań na IP co 15 minut:
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100 // limit each IP to 100 requests per window
});app.use(limiter);
Standardowy magazyn danych znajduje się w pamięci, więc każdy proces liczy żądania oddzielnie; przy skalowaniu należy użyć wspólnego magazynu, takiego jak Redis. Za proxyem konieczna jest konfiguracja opcji trust proxy, aby użyto rzeczywistego IP klienta.
Q59. Zabezpieczanie aplikacji Express
- Ustaw headery HTTP o charakterze ochronnym za pomocą biblioteki
helmet.
dotenv, zamiast przechowywać je w kodzie.npm audit.Q60. res.send() versus res.json() versus res.end()
res.send()przyjmuje łańcuchy, obiekty lub buforzy i wybiera odpowiedniContent-Type.res.json()serializuje swój argument jako JSON i ustawiaContent-Type: application/json.res.end()zakończa odpowiedź bez dodatkowych danych – jest to uproszczona warstwa otaczająca wbudowaną metodę Node.
Q61. Co robi next('route')?
Pomija pozostałe funkcje powrotnicze bieżącej trasy i przechodzi do następnej pasującej trasy. Dotyczy to wyłącznie obsługowników zarejestrowanych za pomocą app.METHOD() lub router.METHOD(), które mają kilka funkcji powrotniczych.
Q62. Poprawa wydajności
- Zastosuj klastrowanie lub PM2, aby uruchamiać po jednym procesie na każdym rdzeniu.
- Zachowuj odpowiedzi w pamięci podręcznej Redis lub w pamięci komputera.
- Skompresuj odpowiedzi za pomocą middleware’u
compressionlub zleć to proxyowi. - Umieść przed aplikacją Nginx lub inny proxy odwrotny lub balanser obciążeń.
- Unikaj wykonywania zadań wymagających dużo mocy obliczeniowej CPU w pętli zdarzeń.
- Wykorzystaj pulę połączeń do bazy danych.
- Rozważ użycie protokołu HTTP/2.
- Zastosuj asynchroniczną obsługę wejścia/wyjścia we wszystkich miejscach i unikaj wariantów
Syncw ścieżkach żądań.
Q63. REST kontra GraphQL z Express
REST udostępnia wiele punktów końcowych o ustalonych strukturach odpowiedzi, dlatego klienty często pobierają zbyt dużo lub zbyt mało danych. GraphQL udostępnia jeden punkt końcowy, w którym klient precyzyjnie określa dane, które chce otrzymać. Zazwyczaj jest on integrowany z Express za pomocą express-graphql lub apollo-server-express; oba te pakiety zostały już uznane za przestarzałe na rzecz nowszych rozwiązań, dlatego należy sprawdzić aktualną dokumentację Apollo lub GraphQL Yoga.
Q64. Jak Express radzi sobie z równoczesnymi żądaniami?
Każda prośba to po prostu seria funkcji zwrotnych w pętli zdarzeń. Ponieważ operacje na bazie danych i sieci nie są blokujące, jeden wątek może obsługiwać wiele żądań jednocześnie: ruch sieciowy jest przekazywany do mechanizmów asynchronicznych systemu operacyjnego, praca z plikami do puli wątków libuv, a funkcje zwrotne są wykonywane po otrzymaniu wyników. W przeciwieństwie do klasycznych serwerów wielowątkowych, nie ma jednego wątku na żądanie, i to dlatego jeden wolny, synchroniczny obsługa wpływa na wszystkich.
Q65. PM2 versus moduł cluster
cluster to wbudowana funkcja do skalowania wieloprocesowego. PM2 to menedżer procesów oparty na tej samej koncepcji, który dodaje automatyczne restarty po awariach, równoważenie obciążenia, obsługę logów, ładowanie bez przerwy oraz monitorowanie, dlatego jest używany w tak wielu produkcjach.
Q66. Dlaczego umieszczać proxy odwrotny przed Node?
Proxy odwrotny, takie jak Nginx, kończy proces TLS, równomiernie rozkłada obciążenie między instancje, cacheuje dane, kompresuje pliki i serwuje pliki statyczne, co uwalnia Node od tych zadań, aby proces aplikacji mógł skupić się na logice biznesowej.
Q67. Konfiguracja specyficzna dla środowiska
Pakiet dotenv ładuje zmienne z pliku .env do process.env, a aplikacja korzysta z wartości domyślnych, gdy dana wartość jest brakująca:
require('dotenv').config();
const PORT = process.env.PORT || 3000;
Nowsze wersje Node mogą również ładować pliki env natywnie za pomocą flagi --env-file. Nigdy nie przechowuj w repozytorium plików .env zawierających dane poufne.
Q68. app.all() versus app.use()
app.all() pasuje do każdej metody HTTP na określonej ścieżce. app.use() również pasuje do każdej metody, ale do całej ścieżki i wszystkiego poniżej niej, dlatego jest narzędziem do montowania middleware.
Q69. Paginacja z Express i MongoDB
Przeczytaj wartości page i limit z ciągu zapytania, używając rozsądnych domyślnych wartości, a następnie połącz je z skip:
app.get('/posts', async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const posts = await Post.find()
.skip((page - 1) * limit)
.limit(limit);
res.json(posts);
});
Zabezpiecz to przed wdrożeniem: przekaż radix do parseInt, ogranicz page do co najmniej 1, a limit do maksymalnej wartości, oraz dodaj stabilną opcję sort. W przypadku bardzo dużych kolekcji szybkość operacji skip spada wraz ze wzrostem wartości offsetu, natomiast paginacja oparta na kursorze w polu zindeksowanym radzi sobie lepiej.
Q70. Co to jest idempotencja?
Operacja jest idempotentna, gdy jej powtórzenie pozostawia serwer w tym samym stanie co wykonanie jej raz. Operacje GET, PUT i DELETE są definiowane jako idempotentne; POST zazwyczaj nie jest, ponieważ każde wezwanie może stworzyć nowy zasób. Ponawianie prób po wygaśnięciu czasu oczekiwania jest bezpieczne tylko w przypadku zapytań idempotentnych, dlatego API płatności często dodają klucze idempotencji do zapytań typu POST.
Główne wnioski
- Większość pytań dotyczących Node sprowadza się do jednego modelu: pojedynczej wątku JavaScript, pętli zdarzeń z określonymi fazami, mikrozadań usuwanych po każdym callbacku oraz biblioteki libuv zajmującej się resztą.
- Większość pytań dotyczących Express sprowadza się do innego modelu: uporządkowanej łańcuchowej struktury middleware, w której każda funkcja albo odpowiada, albo wywołuje
next(), a błędy trafiają do obsługujących je funkcji przyjmujących cztery argumenty.
cluster.isPrimary, błędy spowodowane nieobsłużonymi odrzuceniami, asynchroniczne błędy w Express 5 oraz narzędzia do obsługi ścieżek w ESM.