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 funkcifoo
ze souborumodule.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 export
nedovolili, 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:
- Seznamy exportů
V modulu (module.js
) není potřeba klíčové slovoexport
uvádět vícekrát. Stačí vypsat seznam toho, co exportujeme:export { foo, hello }
; - Přejmenování
Klíčovým slovemas
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ř. jakomodule.foo()
. - Hromadný import přejmenovaných
Pomocí znaku*
je možné importovat všechny exportované prvky modulu:import { * as myFoo } from './module.js'
. - Výchozí exporty
Nášmodule.js
může mít nějaký výchozí výstup, označíme jej klíčovým slovemdefault
. 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>
a <script type="module">
bychom našli:
- Striktní režim je zapnutý
Moduly se spouští ve striktním režimu. Deklaraci'use strict'
není potřeba uvádět. - Proměnné nejsou globální
Tohle je asi zřejmé z předchozích odstavců – pokud v modulu deklarujete proměnnouconstahoj
, platnost bude mít jen na úrovni onoho modulu a nedostanete se k ní z kořenového objektu –window.ahoj
. - 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á. - 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 jeAccess-Control-Allow-Origin: *
. - Inline moduly mohou být „async“
Atributasync
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">
. - 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 atributdefer
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ů:
- 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ů.
- 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 soubormain.js
nestáhnou a neprovedou. - Moderní prohlížeče se zase vyhýbají kódu s atributem
nomodule
, takže ty zase nestáhnou souborfallback.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
module
inomodule
.
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.
- ECMAScript modules in browsers – rychlý úvod od Jake Archibalda.
- ES modules: A cartoon deep-dive (zábavné!) a ES6 In Depth: Modules – Mozilla Hacks.
- JavaScript modules na v8.dev – hlavně odtud jsem při psaní vycházel.
- ES6 v kostce – obecnější, ale český text.
Autor děkuje Robinovi Pokornému a Michalovi Matuškovi za cenné připomínky k textu.
Komentáře
Máte doplnění, komentář nebo jste našli chybu?
Pro přidání názoru se prosím
přihlaste nebo si zřiďte účet.