Menu

CommonJS vs ES Modules: require czy import w Node

Dwa systemy modułów, z którymi żyje JavaScript: skąd się wzięły oba i jak wybrać między require a import w projektach Node.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

Dwa systemy modułów, jeden język

Na początku JavaScript nie miał żadnego systemu modułów. Node wypełnił tę lukę w 2009 roku za pomocą CommonJS (require, module.exports) i przez lata tak właśnie wyglądał kod w Node. Potem, w 2015 roku, sam język dostał standardowy system modułów, czyli ES Modules (import, export), który dziś obsługują zarówno przeglądarki, jak i Node.

Dlatego w praktyce spotkasz oba. Oto ten sam mały moduł napisany na dwa sposoby:

Ta sama funkcja, dwie różne koperty. Reszta tej strony wyjaśnia, kiedy koperta ma znaczenie i po którą sięgać.

Różnice w składni

Codzienne różnice zmieszczą się na pocztówce:

// CommonJS
const fs = require("fs");
const { readFile } = require("fs/promises");

module.exports = something;
module.exports.name = value;
exports.name = value;
// ES Modules
import fs from "fs";
import { readFile } from "fs/promises";

export default something;
export const name = value;
export { name };

require to zwykłe wywołanie funkcji. import to instrukcja: może pojawić się tylko na najwyższym poziomie modułu, a ścieżka musi być literałem tekstowym. To ograniczenie nie jest przypadkowe: właśnie ono pozwala ESM robić rzeczy, których CommonJS nie potrafi.

Prawdziwa różnica: statyczne vs dynamiczne

CommonJS wykonuje require() w chwili, gdy dochodzi do tej linii. Możesz go wstawić do if, wyliczyć ścieżkę w trakcie działania programu i ładować moduł warunkowo:

ES Modules są statyczne. Silnik parsuje wszystkie instrukcje import, zanim uruchomi jakikolwiek kod, buduje graf zależności i rozwiązuje wszystko z góry. Dlatego ścieżka musi być literałem i dlatego import pojawia się tylko na najwyższym poziomie.

Zysk: narzędzia widzą cały graf modułów bez wykonywania czegokolwiek. Dzięki temu bundlery robią tree-shaking (usuwają nieużywane eksporty), edytory dają trafne podpowiedzi, a przeglądarka może pobierać moduły równolegle.

Gdy naprawdę potrzebujesz dynamicznego ładowania w ESM, użyj import(): wyrażenia wyglądającego jak funkcja, które zwraca Promise:

Jak Node decyduje, którego systemu używa plik

Jeden projekt Node może zawierać pliki obu rodzajów. Node ustala system dla każdego pliku na podstawie dwóch rzeczy:

  • Rozszerzenie pliku: .mjs to zawsze ESM, .cjs to zawsze CommonJS.
  • Pole "type" w najbliższym package.json: "module" oznacza, że pliki .js to ESM, a "commonjs" (wartość domyślna, gdy pola brak) oznacza, że to CJS.
// package.json
{
    "name": "my-app",
    "type": "module"
}

Przy "type": "module" zwykły hello.js w tej samej paczce używa import/export. Wrzuć obok hello.cjs, a ten jeden plik będzie używał require. Tak projekt może migrować stopniowo, a biblioteka publikować obie wersje obok siebie.

Pułapka dla początkujących: w pliku ESM require i module.exports po prostu nie istnieją. Jeśli sięgniesz po nie z przyzwyczajenia, dostaniesz ReferenceError.

Współpraca: mieszanie obu systemów

Często plik ESM musi użyć paczki CommonJS albo odwrotnie. Zasady nie są symetryczne.

ESM importujący CommonJS działa bezpośrednio. Obiekt module.exports z CJS staje się eksportem domyślnym:

// app.mjs
import greet from "./greet.cjs";
console.log(greet("Rosa"));

Importy nazwane z CommonJS czasem działają (Node próbuje statycznie wykryć nazwane eksporty), ale dla pewności weź eksport domyślny i go zdestrukturyzuj:

import pkg from "./utils.cjs";
const { parse, stringify } = pkg;

CommonJS importujący ESM to bolesny kierunek. Nie możesz zrobić require() modułu ES: rzuci ERR_REQUIRE_ESM. Wyjściem awaryjnym jest dynamiczny import(), który działa także w CJS i zwraca Promise:

Nowoczesny Node (22+) dodał w pewnych warunkach synchroniczny require() dla ESM, ale przenośnym rozwiązaniem pozostaje dynamiczny import().

Inne różnice w działaniu, które warto znać

Poza składnią oba systemy różnią się kilkoma szczegółami, które czasem potrafią ugryźć:

Kilka kolejnych:

  • this na najwyższym poziomie. W CJS this to module.exports. W ESM to undefined. ESM zawsze działa w trybie ścisłym.
  • __dirname i __filename. CJS daje je za darmo. W ESM wyprowadzasz je z import.meta.url:
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
  • Rozszerzenia plików w importach. ESM wymaga rozszerzenia ("./utils.js", a nie "./utils") w ścieżkach względnych. CJS jest pobłażliwy.
  • Żywe powiązania vs migawki. Importy ESM to żywe referencje do zmiennych modułu eksportującego. CJS daje kopię tego, co było przypisane do module.exports w chwili ładowania. Większość kodu nigdy tego nie zauważa, ale ma to znaczenie przy zależnościach cyklicznych.

Którego używać?

W nowym projekcie: ES Modules. Ustaw "type": "module" w package.json i nie oglądaj się za siebie. ESM to standard języka, działa tak samo w przeglądarkach i w Node, obsługuje await na najwyższym poziomie, a narzędzia powstają z myślą o nim.

Zostań przy CommonJS, gdy:

  • Utrzymujesz istniejący kod w CJS, a migracja jeszcze się nie opłaca.
  • Publikujesz bibliotekę, która musi wspierać bardzo stare wersje Node albo odbiorców, którzy nie mogą używać ESM.
  • Kluczowa zależność jest dostępna tylko w CJS, a jej współpraca z ESM jest kłopotliwa. (Dziś rzadko, ale wciąż się zdarza.)

Nawet wtedy będziesz ciągle czytać kod ESM: wszystko, co trafiło na npm w ostatnich latach, zmierza w tę stronę. Znajomość obu nie jest opcjonalna; biegłość w idiomach tego, w którym faktycznie piszesz, również.

Szybka lista kontrolna

Gdy otwierasz nowy plik, zapytaj siebie:

  • Czy ten plik używa import/export, czy require/module.exports? Nie mieszaj ich.
  • Co mówi najbliższy package.json o polu "type"?
  • Jeśli importujesz paczkę, sprawdź jej package.json: czy dostarcza ESM, CJS, czy oba?
  • Jeśli trafisz na ERR_REQUIRE_ESM, jesteś w CJS i próbujesz załadować ESM. Przejdź na dynamiczny import() albo przenieś wywołujący plik do ESM.

Dziewięćdziesiąt procent zamieszania z modułami w Node to jeden z tych czterech punktów.

Dalej: podstawy npm

Moduły pozwalają dzielić twój kod na pliki. Następny krok to korzystanie z kodu napisanego przez innych, i do tego służy npm. Omówimy instalowanie paczek, zakresy semver i te części pracy z npm, których naprawdę używasz na co dzień.

Najczęściej zadawane pytania

Czym różni się require od import w JavaScript?

require to sposób ładowania modułów w CommonJS: działa synchronicznie, wykonuje się w miejscu wywołania i zwraca to, co moduł przypisał do module.exports. import to składnia ES Modules: jest statyczny, przenoszony na początek pliku i analizowany, zanim kod się uruchomi. Oba systemy różnią się też tym, czym jest this, jak rozwiązują zależności cykliczne i czy pozwalają na await na najwyższym poziomie (pozwala tylko ESM).

CommonJS czy ES Modules w nowym projekcie Node?

Wybierz ES Modules. Ustaw "type": "module" w package.json i pisz import/export. ESM to oficjalny standard, działa w przeglądarkach i w Node oraz obsługuje await na najwyższym poziomie. CommonJS wciąż pojawia się w starszych paczkach i narzędziach, więc będziesz go czytać, nawet jeśli sam go nie piszesz.

Czy można mieszać require i import w jednym projekcie?

Tak, ale według zasad. Plik .mjs albo paczka z "type": "module" używa ESM; .cjs albo "type": "commonjs" używa CJS. ESM może zrobić import modułu CommonJS (jego module.exports staje się eksportem domyślnym). CommonJS nie może bezpośrednio zrobić require() modułu ESM: trzeba użyć dynamicznego import(), który zwraca Promise.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ