Martin Michálek Martin Michálek  – 30. 3. 2020

ES modul (občas budu název zjednodušovat jako javascriptový modul) je soubor obsahující javascriptový kód, který se vkládá do jiného souboru, obsahujícího javascriptový kód.

Definici máme hotovou, jdeme domů? Ještě počkejte, slibuji, že v článku půjdu více do hloubky.

Moduly různých typů, jako CommonJS nebo AMD, se v praxi používají už dlouho, hlavně díky sestavovačům jako je Webpack, protože nativní podpora v prohlížečích byla až doteď nejistá a různorodá.

Po přechodu MS Edge na jádro Chrome podporují ECMAScript moduly všechny prohlížeče kromě Internet Exploreru 11, takže je možné tuhle legraci používat skoro všude.

S alternativním řešením pro IE11 nám znovu pomohou sestavovací nástroje. K tomu se dostaneme na konci článku. Teď si pojďme říct něco o samotných modulech.

Úplné základy: export a import části kódu

Klíčovým slovem export se označuje část kódu, která má být veřejně dostupná, importovatelná zvenčí.

Vezměme, že si vytvoříme soubor module.js:

export function foo() {
  console.log('foo');
}

function bar() {
  console.log('bar');
}

Veškerý kód v souboru platí standardně jen na úrovni modulu a nelze jej volat zvenčí. Pomocí klíčového slova export se pak část kódu „vystaví na veřejnost“. Dělá se tím jakési API tohoto modulu.

Exportovat můžete všechny možné části kódu – function, class, let, nebo const – ale jen na nejvyšší úrovni zanoření.

Import v jiném souboru

Předpokládejme, že module.js bude knihovna, kterou používáme v našem hlavním souboru. Pojmenujme jej script.js:

// Importujeme:
import { foo } from './module.js';

// Používáme:
foo();

Vysvětleme:

  • Na druhém řádku (import …) importujeme funkci foo ze souboru module.js.
  • Na čtvrtém řádku importovanou funkci voláme. Konzole prohlížeče nám tedy slavnostně vypíše řetězec foo.

Pokud bychom ale například importovali funkci bar() (import { bar } …), prohlížeč by s námi nesouhlasil a v konzoli hlásil: „The requested module './module.js' does not provide an export named 'bar'.“ No jistě, vždyť téhle funkci jsme pomocí klíčového slova exportnedovolili, aby byla veřejně dostupná.

Voláme moduly z HTML

Aby to ale celé fungovalo, musíme ještě vložit script.js do nějakého HTML kódu:

<script type="module" src="script.js"></script>

Všimněte si onoho type="module". Říkáme tím prohlížeči, aby s kódem v JS zacházel jako s modulem. Pokud bychom to neuvedli, prohlížeč by se na nás zlobil a hlásil „Uncaught SyntaxError: Cannot use import statement outside a module“.

Druhý soubor, module.js, na který se odkazujeme uvnitř script.js, si prohlížeč stáhne a vykoná sám, to už je jeho práce.

Související: Značka SCRIPT: Vložení JavaScriptu do HTML

Další možnosti importů a exportů

Zpravidla chceme exportovat více než jednu část kódu modulu. Náš module.js tedy rozšíříme o konstantu:

export const hello = 'Hello!';

Importovat pak do script.js můžeme konstantu i funkci tak, že je prostě oddělíme čárkou…

import { foo, hello } from './module.js';

Možností je ale více:

  1. Seznamy exportů
    V modulu (module.js) není potřeba klíčové slovo export uvádět vícekrát. Stačí vypsat seznam toho, co exportujeme: export { foo, hello };
  2. Přejmenování
    Klíčovým slovem as je možné původní objekty přejmenovat a přidělit jim jmenný prostor. Importujeme pomocí import { foo as myFoo } from './module.js' a dále používáme např. jako module.foo().
  3. Hromadný import přejmenovaných
    Pomocí znaku * je možné importovat všechny exportované prvky modulu: import { * as myFoo } from './module.js'.
  4. Výchozí exporty
    Náš module.js může mít nějaký výchozí výstup, označíme jej klíčovým slovem default. Například takto: export default function() { … }. Při importování je pak možné prostě jen uvést jméno pro importovaný modul: import myModule from './module.js'.

Více příkladů hledejte v článcích, na které odkazuji na konci textu. Příklad se základním kódem najdete v Gistu.

V čem se liší modul od klasického JavaScriptu?

Pár rozdílů v chování <script><script type="module">bychom našli:

  1. Striktní režim je zapnutý
    Moduly se spouští ve striktním režimu. Deklaraci 'use strict' není potřeba uvádět.
  2. Proměnné nejsou globální
    Tohle je asi zřejmé z předchozích odstavců – pokud v modulu deklarujete proměnnou constahoj, platnost bude mít jen na úrovni onoho modulu a nedostanete se k ní z kořenového objektu – window.ahoj.
  3. Moduly se vyhodnocují jen jednou
    Pokaždé když do DOMu přidáte klasický <script>, byť s odkazem na stejný soubor, musí se v prohlížečích znovu vyhodnotit. <script type="module"> toto nedělá.
  4. Moduly se stahují se s CORS
    Pokud moduly taháte z jiné domény, musejí být doručeny podle Cross-origin resource sharing (CORS) se správnými hlavičkami, jako je Access-Control-Allow-Origin: *.
  5. Inline moduly mohou být „async“
    Atribut async nefunguje pro klasické <script> s inline kódem (vloženým přímo v HTML zdroji), ale funguje pro inline kód uvnitř <script async type="module">.
  6. Výchozí servírování modulů je „defer“
    Vykonání skriptů modulu je ve výchozím nastavení odloženo až po rozparsování stránky. Není tedy nutné přidávat atribut defer ke značce <script type="module">.

→ Dále také čtěte: Vkládání JS jako async, defer, module a vliv na rychlost webu.

Tipy a doporučení

Přípona souboru .mjs: Ano nebo ne?

Sám to v ukázkách nepoužívám, ale některé texty o javascriptových modulech zmiňují dva rozumné důvody, proč koncovkou odlišovat moduly od běžných JS souborů:

  1. Vaši kolegové a kolegyně, upravující kód, by měli vědět, že jde o modul. S moduly se zachází jinak než s klasickými skripty, takže rozdíl je důležitý a není vidět ze samotného kódu. Viz předchozí sekce o odlišnostech modulů.
  2. Nástroje které moduly zpracovávají - jako Node.js, Babel.js nebo Webpack díky koncovce souboru poznají, že jde o modul a patřičně k němu pak přistupují.

Dává mi to smysl, ale dle Robina Pokorného je to kontroverzní. Dovolím si jej citovat:

„ES moduly jsou součástí JS, vlastně to jsou jediné javascriptové moduly o kterých specifikace mluví. Pokud se tedy nepoužívá jiný typ modulů (např. .cjs pro moduly podle CommonJS), není je třeba odlišovat. U přípony .mjs je také problematická podpora v různých nástrojích, včetně např. TypeScriptu. Koncovka .mjs je tedy dle mého důležitá pouze pro běh mimo prohlížeč, v Node.js, ale i tam jsou jiné možnosti.“

Pokud koncovku .mjs použijete, je pak potřeba na serveru nastavit správnou hlavičku Content-Type: text/javascript.

Dynamický import()

Je dobré vědět, že kromě uvedených statických importů existují také jejich dynamické varianty. Například pro situace, kdy uživatel klikne na odkaz nebo tlačítko:

const moduleSpecifier = './utils.mjs';
const module = import(moduleSpecifier)

Na rozdíl od statického importu lze dynamický import() použít z běžných skriptů. Je to snadný způsob, jak moduly začít postupně používat v existujícím kódu.

Více o dynamických importech je na v8.dev.

Máte hodně modulů? Skládejte kód do větších celků

O tom, zda je po nasazení HTTP/2 na web potřeba balíčkovat („bundlovat“) soubory do větších celků je možné vést dlouhé diskuze.

Na jedné straně je při zvažování situace vždy počet dotazů z prohlížeče na server, které mohou jít po pomalých mobilních připojeních. Na druhé straně pak leží výhoda dlouhého ukládání v prohlížečové cache v případě rozdělení do menších kousků.

Analýza načítání 300 nebundlovaných modulů ukázala, že je v takovém množství lepší balíčkovat. Pochopitelně. Pokud máte vyšší desítky až stovky modulů, rozhodně o nějaké formě balíčkování do větších souborů uvažujte.

Bundlery jako Webpack navíc optimalizují váš kód odstraněním nevyužitých volání import. Pokud balíčkovač umí posílat jen potřebný kód pro daný stav, obecně se doporučuje posílat bundlovanou verzi na produkci vždy.

Pokud těch modulů máte spíše jednotky, a nebo připravujete distribuci pro lokální prostředí, balíčkování vám zase tak moc nepřinese.

Zvažte přednačtení modulů

Pokud kód dělíte do modulů i na produkci, zvažte přidání instrukce k přednačtení do HTML.

Zápis prohlížeči umožní objevit důležité soubory v podobě modulů ještě předtím než stáhne hlavní JavaScript a také optimalizovat přednačtení právě pro specifický kód modulů:

<link rel="modulepreload" href="lib.mjs">
<link rel="modulepreload" href="main.mjs">
<script type="module" src="main.mjs"></script>

Podpora a fallback

Aktuálně podporují moduly z ECMAScript všechny prohlížeče kromě Internet Exploreru 11. Viz údaje na CanIUse.com.

To může někomu vadit natolik, že se do jejich používání nepustí. My ostatní tady máme tooling, nástrojařinu. Ve Webpacku je psaní modulů podle ECMAScript jednou z možností práce, dokonce doporučovanou. Parcel tuto syntaxi podporuje také.

Můžeme psát kód moderním způsobem a nástroje nám exportují balíček kódu, který zvládnou staré prohlížeče. Nebo dva balíčky, jak teď uvidíte.

Návrhový vzor module/nomodule

Ve specifikaci na mechanismus pro náhradní řešení mysleli a vznikl atribut nomodule pro značku <script>, který se nestáhne a neprovede v prohlížečích, které moduly zvládají. Vezměme tento kód:

<!-- Moderní prohlížeče: -->
<script type="module" src="main.js"></script>
<!-- MSIE a podobní staříci: -->
<script nomodule src="fallback.js"></script>

Vysvětlíme:

  • Starší prohlížeče jako Internet Explorer neznají atribut type="module", proto soubor main.js nestáhnou a neprovedou.
  • Moderní prohlížeče se zase vyhýbají kódu s atributem nomodule, takže ty zase nestáhnou soubor fallback.js.

Prohlížeče, které neznají javascriptové moduly obvykle neumějí ani novější funkce jazyka jako jsou například „arrow“ funkce nebo async-await. Nic proti nim, ale tohle je jedna z jejich lepších vlastností.

Této jejich (ne)schopnosti jde využít a nastavit si nástroje tak, aby produkovaly moderní (a datově méně objemný) kód pro moderní prohlížeče (v našem případě do main.js) a pak kód opačné charakteristiky pro MSIE a spol. (do fallback.js).

Vzor module/nomodule má své nevýhody:

  • Původní Microsoft Edge ve verzích 16-18 vykoná skript v module, takže kód pro moderní prohlížeče tam umístěný s tím musí počítat. (Původní Edge je sice moderní prohlížeč, ale své mouchy má…)
  • Starší prohlížeče jako právě původní Edge (do verze 18) nebo Safari 10 stáhnou (ale naštěstí nevykonají) soubor v modulenomodule.

Dle mého je potřeba tyto problémy znát, ale nemusí to bránit v použití. Dvě stažení ve starém Edge a Safari nemusí zase tak vadit, když nám to umožní spouštět v moderních prohlížečích, tedy u většiny uživatelů, výrazně jednodušší kód.

Více o nevýhodách vzoru module/nomodule najdete v textu Will it double-fetch? nebo Differential Serving na CSS-Tricks.

Tímto bych text o ECMAScript modulech s dovolením ukončil. Pokud vás téma zajímá více, mrkněte se na následující zdroje.

Autor děkuje Robinovi PokornémuMichalovi Matuškovi za cenné připomínky k textu.