Když se naÅ¡e aplikace rozrůstá, chceme ji rozdÄlit do vÃce souborů, tzv. âmodulůâ. Modul může obsahovat tÅÃdu nebo knihovnu funkcà sloužÃcà specifickému úÄelu.
Dlouhou dobu JavaScript existoval bez modulové syntaxe na jazykové úrovni. To nebyl problém, protože skripty byly původnÄ malé a jednoduché, takže to nebylo zapotÅebÃ.
Äasem se vÅ¡ak skripty stávaly stále složitÄjÅ¡Ãmi a složitÄjÅ¡Ãmi, takže komunita vynalezla rozliÄné způsoby, jak zorganizovat kód do modulů, a speciálnà knihovny, které naÄtou moduly na požádánÃ.
Abychom vyjmenovali nÄkteré z nich (z historických důvodů):
- AMD â jeden z nejstarÅ¡Ãch modulových systémů, původnÄ implementován knihovnou require.js.
- CommonJS â modulový systém vytvoÅený pro server Node.js.
- UMD â dalšà modulový systém, navržený jako univerzálnÃ, kompatibilnà s AMD a CommonJS.
Nynà se vÅ¡echny pomalu odebÃrajà do dÄjin, ale ve starých skriptech je poÅád můžeme najÃt.
Modulový systém na jazykové úrovni se ve standardu objevil v roce 2015, od té doby se postupnÄ vyvÃjel a nynà je podporován vÅ¡emi významnými prohlÞeÄi a v Node.js. Od nynÄjÅ¡ka tedy budeme studovat modernà JavaScriptové moduly.
Co je modul?
Modul je prostÄ soubor. Jeden skript je jeden modul. Tak jednoduché to je.
Moduly se mohou navzájem naÄÃtat a použÃvat speciálnà direktivy export a import, aby si vzájemnÄ vymÄÅovaly funkcionalitu a jeden modul mohl volat funkce druhého:
- Kl̀ov̩ slovo
exportoznaÄuje promÄnné a funkce, které by mÄly být dostupné zvenÄà aktuálnÃho modulu. - KlÃÄové slovo
importumožÅuje import funkcionality z jiných modulů.
NapÅÃklad máme-li soubor ÅekniAhoj.js, který exportuje funkci:
// ð ÅekniAhoj.js
export function ÅekniAhoj(uživatel) {
alert(`Ahoj, ${uživatel}!`);
}
â¦Pak ji jiný soubor může importovat a použÃvat:
// ð main.js
import {ÅekniAhoj} from './ÅekniAhoj.js';
alert(ÅekniAhoj); // function...
ÅekniAhoj('Jan'); // Ahoj, Jan!
Direktiva import nahrává modul z cesty ./ÅekniAhoj.js, která je relativnà vůÄi aktuálnÃmu souboru, a pÅiÅadà exportovanou funkci ÅekniAhoj do odpovÃdajÃcà promÄnné.
SpusÅ¥me si tento pÅÃklad v prohlÞeÄi.
Protože moduly podporujà speciálnà klÃÄová slova a jazykové prvky, musÃme prohlÞeÄi sdÄlit, že se skriptem má zacházet jako s modulem. UÄinÃme to použitÃm atributu <script type="module">.
NapÅÃklad:
export function ÅekniAhoj(uživatel) {
return `Ahoj, ${uživatel}!`;
}<!doctype html>
<script type="module">
import {ÅekniAhoj} from './say.js';
document.body.innerHTML = ÅekniAhoj('Jan');
</script>ProhlÞeÄ automaticky stáhne a vyhodnotà importovaný modul (a jeho importy, jsou-li zapotÅebÃ) a pak spustà skript.
Pokud se pokusÃte otevÅÃt webovou stránku lokálnÄ protokolem file://, zjistÃte, že direktivy import/export nefungujÃ. K testovánà modulů použÃvejte lokálnà webový server, napÅ. statický server, nebo využijte schopnost âživého serveruâ ve vaÅ¡em editoru, napÅ. VS Code Live Server Extension.
Základnà vlastnosti modulů
ÄÃm se lišà moduly od âobyÄejnýchâ skriptů?
Majà základnà vlastnosti, které platà pro JavaScript na prohlÞeÄové i serverové stranÄ.
Vždy použÃvajà âuse strictâ
Moduly vždy fungujà ve striktnÃm režimu. NapÅÃklad pÅiÅazenà hodnoty nedeklarované promÄnné ohlásà chybu.
<script type="module">
a = 5; // chyba
</script>
Rozsah platnosti na úrovni modulu
Každý modul má na nejvyššà úrovni svůj vlastnà rozsah platnosti. Jinými slovy, promÄnné a funkce na nejvyššà úrovni modulu nejsou vidÄt v jiných skriptech.
V následujÃcÃm pÅÃkladu jsou importovány dva skripty a hello.js se pokusà použÃt promÄnnou uživatel, deklarovanou v user.js. Selže to, protože to je oddÄlený modul (chybu uvidÃte v konzoli):
alert(uživatel); // taková promÄnná neexistuje (každý modul má nezávislé promÄnné)let uživatel = "Jan";<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>Moduly by mÄly exportovat direktivou export to, co chtÄjà zpÅÃstupnit zvenÄÃ, a importovat direktivou import to, co potÅebujÃ.
user.jsby mÄl exportovat promÄnnouuživatel.hello.jsby ji mÄl importovat z moduluuser.js.
Jinými slovy, s moduly použÃváme import a export mÃsto závislosti na globálnÃch promÄnných.
Toto je správná varianta:
import {uživatel} from './user.js';
document.body.innerHTML = uživatel; // Janexport let uživatel = "Jan";<!doctype html>
<script type="module" src="hello.js"></script>HovoÅÃme-li o HTML stránkách, v prohlÞeÄi existuje nezávislý rozsah platnosti na nejvyššà úrovni také pro každý <script type="module"> zvlášť.
Zde jsou dva skripty na stejné stránce, oba majà type="module". Navzájem nevidà svoje promÄnné na nejvyššà úrovni:
<script type="module">
// PromÄnná je viditelná jen v tomto modulovém skriptu
let uživatel = "Jan";
</script>
<script type="module">
alert(uživatel); // Chyba: uživatel nenà definován
</script>
V prohlÞeÄi můžeme uÄinit promÄnnou globálnà na úrovni okna tak, že ji výslovnÄ pÅiÅadÃme do vlastnosti objektu window, napÅ. window.uživatel = "Jan".
Pak ji uvidà vÅ¡echny skripty, s type="module" i bez nÄj.
PÅesto je vÅ¡ak vytváÅenà takových globálnÃch promÄnných nehezké. ProsÃme, snažte se tomu vyhnout.
Modulový kód je vyhodnocen jen poprvé, když je importován
Je-li tentýž modul importován do nÄkolika jiných modulů, jeho kód se spustà pouze jednou, pÅi prvnÃm importu. Pak jsou jeho exporty pÅedány vÅ¡em budoucÃm importérům.
Jednorázové vyhodnocenà má důležité důsledky, kterých bychom si mÄli být vÄdomi.
PodÃvejme se na nÄkolik pÅÃkladů.
PÅedevÅ¡Ãm jestliže spuÅ¡tÄnà kódu modulu způsobà vedlejšà efekty, napÅ. zobrazenà zprávy, pak se pÅi vÃcenásobném importu spustà pouze jednou â poprvé:
// ð alert.js
alert("Modul je vyhodnocen!");
// Importujeme stejný modul v různých souborech
// ð 1.js
import `./alert.js`; // Modul je vyhodnocen!
// ð 2.js
import `./alert.js`; // (nezobrazà nic)
Druhý import nic nezobrazÃ, protože modul již byl vyhodnocen.
Existuje pravidlo, že kód na nejvyššà úrovni modulu by mÄl být použit pro inicializaci, vytvoÅenà vnitÅnÃch datových struktur specifických pro modul. Jestliže potÅebujeme umožnit nÄco volat vÃcekrát, mÄli bychom to exportovat jako funkci, tak jak jsme to udÄlali s výše uvedenou funkcà ÅekniAhoj.
Nynà uvažujme hlubšà pÅÃklad.
ÅeknÄme, že modul exportuje objekt:
// ð admin.js
export let správce = {
jméno: "Jan"
};
Je-li tento modul importován z vÃce souborů, je vyhodnocen pouze poprvé, vytvoÅà se objekt správce a ten je pak pÅedán vÅ¡em dalÅ¡Ãm importérům.
VÅ¡ichni importéÅi dostanou pÅesnÄ jeden a tentýž objekt správce:
// ð 1.js
import {správce} from './admin.js';
správce.jméno = "Petr";
// ð 2.js
import {správce} from './admin.js';
alert(správce.jméno); // Petr
// Jak 1.js, tak 2.js se odkazujà na tentýž objekt správce
// ZmÄny uÄinÄné v 1.js jsou viditelné ve 2.js
Jak vidÃte, když 1.js v importovaném objektu správce zmÄnà vlastnost jméno, pak 2.js uvidà nové správce.jméno.
To je právÄ proto, že modul je spuÅ¡tÄn pouze jednou. Vygenerujà se exporty a ty jsou pak sdÃleny mezi importéry, takže jestliže nÄco zmÄnà objekt správce, ostatnà importéÅi to uvidÃ.
Takové chovánà je ve skuteÄnosti velmi užiteÄné, protože nám umožÅuje konfigurovat moduly.
Jinými slovy, modul může poskytovat obecnou funkcionalitu, která potÅebuje nÄjaké nastavenÃ. NapÅÃklad autentifikace potÅebuje osobnà údaje. Pak modul může exportovat konfiguraÄnà objekt a oÄekávat, že do nÄj vnÄjšà kód nÄco vložÃ.
Zde je klasický vzor:
- Modul exportuje konfiguraci nÄjakého druhu, napÅ. konfiguraÄnà objekt.
- PÅi prvnÃm importu jej inicializujeme, zapÃÅ¡eme do jeho vlastnostÃ. To může udÄlat skript na nejvyššà úrovni aplikace.
- Dalšà importy budou tento modul využÃvat.
NapÅÃklad modul admin.js může poskytovat urÄitou funkcionalitu (napÅ. autentifikaci), ale oÄekává, že osobnà údaje pÅijdou do objektu konfigurace zvenÄÃ:
// ð admin.js
export let konfigurace = { };
export function ÅekniAhoj() {
alert(`Jsem pÅipraven posloužit, ${konfigurace.uživatel}!`);
}
Zde modul admin.js exportuje objekt konfigurace (na zaÄátku je prázdný, ale může mÃt i nÄjaké standardnà vlastnosti).
Pak z nÄj v init.js, prvnÃm skriptu našà aplikace, objekt konfigurace importujeme a nastavÃme konfigurace.uživatel:
// ð init.js
import {konfigurace} from './admin.js';
konfigurace.uživatel = "Petr";
â¦Nynà je modul admin.js nakonfigurován.
Dalšà importéÅi jej mohou volat a modul správnÄ zobrazà aktuálnÃho uživatele:
// ð jiný.js
import {ÅekniAhoj} from './admin.js';
ÅekniAhoj(); // Jsem pÅipraven posloužit, Petr!
import.meta
Informace o aktuálnÃm modulu obsahuje objekt import.meta.
Jeho obsah závisà na prostÅedÃ. V prohlÞeÄi obsahuje URL skriptu nebo URL aktuálnà webové stránky, je-li uvnitÅ HTML:
<script type="module">
alert(import.meta.url); // URL skriptu
// pro vložený skript URL aktuálnà HTML stránky
</script>
V modulu nenà definováno âthisâ
Nenà to významná vlastnost, ale pro úplnost bychom ji mÄli uvést.
V modulu je this na nejvyššà úrovni nedefinované.
Porovnejme si to s nemodulovými skripty, v nichž je this globálnà objekt:
<script>
alert(this); // window
</script>
<script type="module">
alert(this); // undefined
</script>
Vlastnosti specifické pro prohlÞeÄe
Mezi skripty s type="module" a bÄžnými skripty existuje také nÄkolik rozdÃlů, které jsou specifické pro prohlÞeÄe.
Jestliže tento Älánek Ätete poprvé nebo jestliže nepoužÃváte JavaScript v prohlÞeÄi, můžete tuto podkapitolu prozatÃm pÅeskoÄit.
Modulové skripty jsou deferované
Modulové skripty jsou vždy deferované. Je to stejný efekt jako u atributu defer (popsaného v kapitole Skripty: async, defer), a to pro externà i vložené skripty.
Jinými slovy:
- stahovánà externÃch modulových skriptů
<script type="module" src="...">neblokuje zpracovánà HTML, naÄtou se paralelnÄ s ostatnÃmi zdroji. - modulové skripty ÄekajÃ, dokud nenà celý HTML dokument pÅipraven (i pokud jsou malé a naÄtou se rychleji než HTML), a až pak se spustÃ.
- zachovává se relativnà poÅadà skriptů: skripty, které jsou uvedeny jako prvnà v dokumentu, se jako prvnà spustÃ.
VedlejÅ¡Ãm efektem je, že modulové skripty vždy âvidÃâ celou naÄtenou HTML stránku vÄetnÄ HTML prvků, které jsou až pod nimi.
NapÅÃklad:
<script type="module">
alert(typeof tlaÄÃtko); // object: skript âvidÃâ tlaÄÃtko pod sebou
// protože moduly jsou deferované, skript se spustà až po naÄtenà celé stránky
</script>
Porovnejme si to s bÄžným skriptem:
<script>
alert(typeof tlaÄÃtko); // tlaÄÃtko je undefined, protože skript nevidà prvky pod sebou
// bÄžné skripty se spustà okamžitÄ, jeÅ¡tÄ pÅed zpracovánÃm zbytku stránky
</script>
<button id="tlaÄÃtko">TlaÄÃtko</button>
ProsÃme vÅ¡imnÄte si, že druhý skript se ve skuteÄnosti spustà pÅed prvnÃm! Nejprve tedy uvidÃme undefined, až pak object.
Je to proto, že moduly jsou deferované, takže ÄekajÃ, až se zpracuje celý dokument. BÄžný skript se spustà okamžitÄ, proto jeho výstup uvidÃme jako prvnÃ.
PÅi použÃvánà modulů bychom si mÄli být vÄdomi toho, že HTML stránka se zobrazuje průbÄžnÄ, když se naÄÃtá, ale JavaScriptové moduly se spustà až potom. Uživatel tedy může vidÄt stránku jeÅ¡tÄ pÅedtÃm, než je JavaScriptová aplikace pÅipravena. NÄkterá funkcionalita tedy jeÅ¡tÄ nemusà fungovat. MÄli bychom tedy na stránku umÃstit âukazatele naÄÃtánÃâ nebo nÄjak jinak zajistit, aby to návÅ¡tÄvnÃka nezmátlo.
Na vložených skriptech funguje async
Pro nemodulové skripty funguje atribut async pouze na externÃch skriptech. Asynchronnà skripty se spustà okamžitÄ, když jsou pÅipraveny, nezávisle na ostatnÃch skriptech nebo HTML dokumentu.
Pro modulové skripty funguje i na vložených skriptech.
NapÅÃklad následujÃcà vložený skript má async, takže nebude na nic Äekat.
Provede import (stáhne si ./analytics.js) a spustà se, jakmile bude pÅipraven, i když HTML dokument jeÅ¡tÄ nebude dokonÄen nebo jiné skripty budou stále Äekat na vyÅÃzenÃ.
To je dobré pro funkcionalitu, která na niÄem nezávisÃ, napÅ. ÄÃtaÄe, reklamy, posluchaÄe událostà na dokumentové úrovni.
<!-- jsou staženy všechny závislosti (analytics.js) a skript se spustà -->
<!-- neÄeká na dokument nebo jiné znaÄky <script> -->
<script async type="module">
import {counter} from './analytics.js';
counter.count();
</script>
Externà skripty
Externà skripty, které majà type="module", se lišà ve dvou aspektech:
-
Externà skripty se stejným
srcse spustà pouze jednou:<!-- skript můj.js se naÄte a spustà pouze jednou --> <script type="module" src="můj.js"></script> <script type="module" src="můj.js"></script> -
Externà skripty, které jsou staženy z jiného zdroje (napÅ. z jiné stránky), vyžadujà hlaviÄku CORS, jak je popsáno v kapitole Fetch: požadavky jiného původu. Jinými slovy, je-li modulový skript stažen z jiného zdroje, vzdálený server musà poskytnout hlaviÄku
Access-Control-Allow-Origin, která staženà umožnÃ.<!-- jina-stranka.com musà poskytovat Access-Control-Allow-Origin --> <!-- jinak se skript nespustà --> <script type="module" src="http://jina-stranka.com/jejich-modul.js"></script>To standardnÄ zajišťuje vÄtšà bezpeÄnost.
âHoléâ moduly nejsou povoleny
V prohlÞeÄi musà import obdržet relativnà nebo absolutnà URL. Moduly bez cesty se nazývajà âholéâ moduly. Takové moduly nejsou v import povoleny.
NapÅÃklad tento import je chybný:
import {ÅekniAhoj} from 'ÅekniAhoj'; // Chyba, âholýâ modul
// modul musà obsahovat cestu, napÅ. './ÅekniAhoj.js' nebo jinou, kde tento modul je
UrÄitá prostÅedÃ, napÅ. Node.js nebo spojovacà nástroje, holé moduly bez uvedené cesty dovolujÃ, protože majà své vlastnà způsoby nalezenà modulů a páky, jak je vyladit. ProhlÞeÄe vÅ¡ak holé moduly zatÃm nepodporujÃ.
Kompatibilita, ânomoduleâ
Staré prohlÞeÄe nerozumÄjà type="module". Skripty neznámého typu prostÄ ignorujÃ. Je možné jim poskytnout nouzové ÅeÅ¡enà pomocà atributu nomodule:
<script type="module">
alert("BÄžà v modernÃch prohlÞeÄÃch");
</script>
<script nomodule>
alert("Modernà prohlÞeÄe znajà type=module i nomodule, takže toto pÅeskoÄÃ.")
alert("Staré prohlÞeÄe ignorujà skript s neznámým type=module, ale toto spustÃ.");
</script>
Sestavovacà nástroje
V reálném životÄ se prohlÞeÄové moduly jen zÅÃdka použÃvajà ve své âsurovéâ podobÄ. Obvykle je spojujeme dohromady (anglicky âbundleâ) pomocà speciálnÃho nástroje zvaného bundler, napÅ. Webpack, a nahráváme na produkÄnà server.
Jednou z výhod použÃvánà bundlerů je, že nám poskytujà vÃce kontroly nad tÃm, jak jsou moduly vyhodnocovány, umožÅujà holé moduly a mnoho dalÅ¡Ãch, napÅ. CSS/HTML moduly.
Sestavovacà nástroje provádÄjà následujÃcÃ:
- Vezmou âhlavnÃâ modul, ten, který je urÄen k vloženà do
<script type="module">v HTML. - Analyzujà jeho závislosti: importy, pak importy importů atd.
- Sestavà jediný soubor se vÅ¡emi moduly (nebo vÃce souborů, to lze nastavit) a nahradà nativnà volánÃ
importspojovacÃmi funkcemi, takže to bude fungovat. Podporovány jsou i âspeciálnÃâ typy modulů, napÅ. HTML/CSS. - V tomto procesu mohou být aplikovány i jiné transformace a optimalizace:
- OdstranÄnà nedosažitelného kódu.
- OdstranÄnà nepoužitých exportů (âtÅesenà stromemâ).
- OdstranÄnà pÅÃkazů specifických pro vývoj, napÅ.
consoleadebugger. - Modernà syntaxe JavaScriptu může být transformována na staršàs podobnou funkcionalitou pomocà Babelu.
- Výsledný soubor je minifikován (mezery odstranÄny, názvy promÄnných nahrazeny kratÅ¡Ãmi, atd.).
PoužÃváme-li bundlery, pak když jsou skripty spojeny dohromady do jediného souboru (nebo nÄkolika málo souborů), pÅÃkazy import/export uvnitÅ tÄchto skriptů jsou nahrazeny speciálnÃmi spojovacÃmi funkcemi. Výsledný âspojenýâ skript tedy neobsahuje žádný import/export, nevyžaduje type="module" a můžeme jej vložit do obvyklého skriptu:
<!-- PÅedpokládáme, že jsme z nástroje, napÅ. Webpacku, zÃskali bundle.js -->
<script src="bundle.js"></script>
I pÅesto jsou ovÅ¡em použitelné i nativnà moduly. Zde tedy Webpack použÃvat nebudeme: můžete si jej nakonfigurovat pozdÄji.
ShrnutÃ
Abychom to shrnuli, základnà koncepty jsou:
- Modul je soubor. Aby fungoval
import/export, prohlÞeÄe potÅebujÃ<script type="module">. Moduly majà nÄkolik odliÅ¡nostÃ:- StandardnÄ jsou deferované.
- Ve vložených skriptech funguje async.
- K naÄtenà externÃch skriptů z jiného zdroje (domény/protokolu/portu) jsou nezbytné hlaviÄky CORS.
- Duplicitnà externà skripty jsou ignorovány.
- Moduly majà svůj vlastnÃ, lokálnà rozsah platnosti na nejvyššà úrovni a vymÄÅujà si funkcionalitu pomocÃ
import/export. - Moduly vždy majÃ
use strict. - Kód modulu se spustà pouze jednou. Exporty se vytvoÅà jednou a mezi importéry jsou sdÃleny.
Když použÃváme moduly, každý modul implementuje funkcionalitu a exportuje ji. Pak použijeme import k jejÃmu pÅÃmému importu tam, kde je zapotÅebÃ. ProhlÞeÄ tyto skripty automaticky naÄte a vyhodnotÃ.
PÅi produkci se Äasto použÃvajà bundlery, napÅ. Webpack, ke spojenà modulů dohromady kvůli zlepÅ¡enà výkonnosti i z jiných důvodů.
V pÅÃÅ¡tà kapitole uvidÃme dalšà pÅÃklady modulů a toho, jak lze různé vÄci exportovat nebo importovat.
KomentáÅe
<code>, pro nÄkolik Åádků je obalte znaÄkou<pre>, pro vÃce než 10 Åádků vložte odkaz na pÃskoviÅ¡tÄ (plnkr, jsbin, codepenâ¦)