Začněte používat ECMAScript moduly v Node.js už dnes
Publikováno: 27.1.2021
Proč jsou ECMAScript moduly lepší než CommonJS? Už nastal ten správný čas přepsat všechny své NPM balíčky na ECMAScript moduly?
Node.js verze 10.x pomalu míří do křemíkového nebe, na konci dubna končí dlouhodobá podpora pro tuto verzi. Jedná se o poslední verzi Node.js, která nepodporovala ECMAScript modules, zkráceně ESM.
ESM jsou experimentálně podporovány již od verze Node.js 12.x a v poslední verzi Node.js 15.x se ES moduly zařadily mezi stabilní součásti Node.js. Tímto se otevírají dveře pro opuštění CommonJS, zkráceně CJS.
Takže musím všechno přepsat?
Nemusíte přepisovat vůbec nic, protože Node.js dál považuje CJS za defaultní a umožňuje souběžné použití jak CJS tak ESM. Jenže ECMAScript modules je onen standard, se kterým lze počítat na straně severu do budoucna. ESM se už stihl slušně zabydlet ve webových prohlížečích. Navíc jsem si jistý, že hodně z vás už import a export ve svých Node.js kódech používá, jen jej překlápíte do CJS pomocí babel, respektive babel-node.
Rozdíly
Jaký je tedy hlavní rozdíl mezi ESM a CJS? CommonJS require()
je synchronní funkce a nevrací promis, ale přímo výsledek obsažený v module.export
.
Proti tomu ESM loader běží asynchronně, což je velká výhoda. Parser v první fází pouze analyzuje kód a detekuje volání import
a export
bez spouštění samotného kódu. Překladač je také schopen v této fázi detekovat chyby importu a případně vyhodit výjimku. Teprve poté ESM zavaděč začne kódy porcovat a vytvářet „modulový graf“ závislostí a spouštět paralelně jednotlivé části kódu.
Další neméně významnou výhodou ESM je výchozí striktní režim "use strict";
– takže jej už nemusíte zapínat v každém souboru zvlášť.
Píšeme svůj první ES Modul
Pojďme se společně podívat na to, jak ES modul napsat. Standardní zápis CJS jistě všichni znáte:
// something.jsmodule.exports =functionMontyPython() {
console.log('And Now for Something Completely Different');
}
Hlavní soubor index.js
pak může vypadat například takto:
const MontyPython = require('./something.js');
MontyPython();
Když tento kód přepíšeme do ESM, bude vypadat následovně:
// something.jsexportfunctionMontyPython() {
console.log('And Now for Something Completely Different')
}
Soubor index.js
pak bude vypadat takto:
import {MontyPython} from'./something.js';
MontyPython();
Tento kód si můžete zkusit spustit pomocí příkazu node index.js
. Pokud tak učiníte, vypíše vám Node.js chybu:
nodeindex.js
(node:9467) Warning: To load an ES module, set "type": "module"in the package.json or use the .mjs extension.
(Use `node--trace-warnings ...` to show where the warning was created)
/Volumes/Work/example/index.js:1
import {MontyPython} from './something.js';
^^^^^^
SyntaxError: Cannot use import statement outside a module
at wrapSafe (node:internal/modules/cjs/loader:1024:16)
at Module._compile (node:internal/modules/cjs/loader:1072:27)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1137:10)
at Module.load (node:internal/modules/cjs/loader:973:32)
at Function.Module._load (node:internal/modules/cjs/loader:813:14)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:76:12)
at node:internal/main/run_main_module:17:47
Touhle chybou vám Node.js oznamuje, že import
je možné používat pouze v ES modulech. Máte dvě možnosti, jak svůj kód skutečně proměnit na ES modul:
- První, že upravíte
package.json
, což vám ostatně Node.js navrhuje. - Druhá, že změníte koncovku souboru na
*.mjs
.
Osobně bych zvolil spíš ten první způsob a nejsem v tom sám. Přidejte tedy vedle svých dvou souborů následující package.json
:
{
"type": "module",
"engines": { "node": ">=12" }
}
Zároveň pomocí "engines"
označíme minimální požadovanou verzi Node.js. Ještě je zde nutné upozornit, že verze 12.x ESM podporuje, ale musíte je explicitně povolit pomocí parametru --experimental-modules
– v této verzi se totiž ještě jedná o experimentální funkcionalitu. Bez tohoto parametru se obejdete až u poslední verze Node.js. Svůj kód spustíte pomocí příkazu:
$ node--experimental-modules index.js
And Now for Something Completely Different
Pokud ještě používáte výše zmínění babel-node, kvůli překladu moderního JavaScriptu do CJS, můžete jej nyní odebrat – už ho totiž nebudete potřebovat. Spouštění a načítání jednotlivých části kódu bude mít na starosti ESM loader. Než vypustíte svůj balíček do světa, doporučuji přidat do package.json
definici vstupního souboru pomocí exports.
ESM a zpětná kompatibilita s CJS
ES modul dokáže snadno vložit CJS balíček pomocí import název from "balíček"
, jedná se zkrácený zápis import {default as název} from "balíček";
. ESM loader v případě CJS kódu totiž bere module.export = ...
stejně jako zápis export default ...
.
import {defaultas fsExtra } from"fs-extra";
import fs from'fs-extra';
console.log(fs.existsSync('./package.json'));
console.log(fsExtra.existsSync('./package.json'));
Co však nelze, je vložit pojmenované exporty. Kód import {shuffle} from './lodash.js
nebude fungovat, ale tohle lze to celkem elegantně vyřešit:
import_ from './lodash.js';
const {shuffle} = _;
Z druhé strany je situace poněkud komplikovanější. CJS si nedokáže pomocí funkce require()
poradit s navráceným promise. Kód ES module je v CJS potřeba obalit do asynchronní funkce:
(async () => {
const {foo} = await import('./foo.mjs');
})();
Jak nakombinovat JSX a ESM dohromady?
Závěrem přidám ještě jednu drobnost. Server-side renderování komponent je čím dál populárnější, ale pokud jej potřebujete nakombinovat JSX s ESM narazíte. JSX syntaxe totiž není standardním zápisem javascriptového kódu a Node.js tomuto zápisu nerozumí.
// Page.jsimport React from'react';
exportconst Page = () => (
<p>React komponenta</p>
);
Spuštění tohoto modulu momentálně skončí syntaktickou chybou:
file:///Volumes/Work/example/index.js:91
<Page />,
^
SyntaxError: Unexpected token '<'
at Loader.moduleStrategy (node:internal/modules/esm/translators:147:18)
at async link (node:internal/modules/esm/module_job:48:21)
Pokud se chcete vyhnout přepisování svých JSX komponent na React.createElement()
musíte JSX kód před spuštěním nejprve prohnat transpilerem a přeložit jej do čístého Javascriptu. Přidejte si do svého mini projektu následující balíčky:
$ yarn add react react-dom @babel/core @babel/preset-react @node-loader/babel
Poslední balíček @node-loader/babel je experimentální ESM loader, který s využitím babel provede transpilaci JSX kódu do čistého JavaScriptu a připraví tak kód pro spuštění v Node.js.
Nezapomeňte si ještě vytvořit konfigurační soubor .babelrc
s následujícím obsahem:
{
"presets": ["@babel/react"]
}
Váš index.js
pak může vypadat následovně:
import React from'react';
import {renderToStaticMarkup} from"react-dom/server.js";
import {Page} from'./Page.js'console.log(renderToStaticMarkup(<Page />));
Protože se opět jedná o experimentální funkcionalitu, dokonce i ve verzi Node.js 15.x, musíte si jí vyžádat parametrem --experimental-loader
a přidat ještě název vybraného loaderu:
$ node--experimental-modules --no-warnings \
--experimental-loader @node-loader/babel index.js
<p>React komponenta</p>
Mějte na paměti, že API experimentálního loaderu je v současnosti předmětem aktivního vývoje a dozajista se bude měnit, takže tento postup může přestat fungovat.