Moduly, úvod

Jak 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í pro konkrétní účel.

Po dlouhou dobu existoval JavaScript bez syntaxe modulu na jazykové úrovni. To nebyl problém, protože zpočátku byly skripty malé a jednoduché, takže to nebylo potřeba.

Ale nakonec byly skripty stále složitější, takže komunita vynalezla různé způsoby, jak organizovat kód do modulů, speciálních knihoven pro načítání modulů na vyžádání.

Abych některé jmenoval (z historických důvodů):

  • AMD – jeden z nejstarších modulových systémů, původně implementovaný 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 tyto pomalu stávají součástí historie, ale stále je můžeme najít ve starých skriptech.

Systém modulů na jazykové úrovni se ve standardu objevil v roce 2015, od té doby se postupně vyvíjel a nyní je podporován všemi hlavními prohlížeči a v Node.js. Od této chvíle budeme studovat moderní moduly JavaScriptu.

Co je modul?

Modul je pouze soubor. Jeden skript je jeden modul. Tak jednoduché.

Moduly se mohou navzájem načítat a používat speciální direktivy export a import pro výměnu funkcí zavolejte funkce jednoho modulu z jiného:

  • export klíčová slova označují proměnné a funkce, které by měly být přístupné mimo aktuální modul.
  • import umožňuje import funkcí z jiných modulů.

Například, pokud máme soubor sayHi.js exportování funkce:

// 📁 sayHi.js
export function sayHi(user) {
 alert(`Hello, ${user}!`);
}

…Pak jej může importovat a používat jiný soubor:

// 📁 main.js
import {sayHi} from './sayHi.js';

alert(sayHi); // function...
sayHi('John'); // Hello, John!

import direktiva načte modul cestou ./sayHi.js vzhledem k aktuálnímu souboru a přiřadí exportovanou funkci sayHi na odpovídající proměnnou.

Spusťte příklad v prohlížeči.

Protože moduly podporují speciální klíčová slova a funkce, musíme prohlížeči sdělit, že se skriptem má být zacházeno jako s modulem, pomocí atributu <script type="module"> .

Takhle:

Resultsay.jsindex.html
export function sayHi(user) {
 return `Hello, ${user}!`;
}
<!doctype html>
<script type="module">
 import {sayHi} from './say.js';

 document.body.innerHTML = sayHi('John');
</script>

Prohlížeč automaticky načte a vyhodnotí importovaný modul (a jeho importy v případě potřeby) a poté spustí skript.

Moduly fungují pouze přes HTTP(y), nikoli lokálně

Pokud se pokusíte otevřít webovou stránku lokálně, pomocí file:// protokol, zjistíte, že import/export direktivy nefungují. K testování modulů použijte místní webový server, jako je statický server, nebo použijte funkci „živého serveru“ vašeho editoru, jako je VS Code Live Server Extension.

Základní funkce modulu

V čem se moduly liší od „běžných“ skriptů?

Existují základní funkce platné pro JavaScript na straně prohlížeče i serveru.

Vždy „použijte přísné“

Moduly vždy fungují v přísném režimu. Např. přiřazení k nedeklarované proměnné způsobí chybu.

<script type="module">
 a = 5; // error
</script>

Rozsah na úrovni modulu

Každý modul má svůj vlastní rozsah nejvyšší úrovně. Jinými slovy, proměnné a funkce nejvyšší úrovně z modulu nejsou v jiných skriptech vidět.

V níže uvedeném příkladu jsou importovány dva skripty a to hello.js se pokusí použít user proměnná deklarovaná v user.js . Selže, protože se jedná o samostatný modul (chybu uvidíte v konzole):

Resulthello.jsuser.jsindex.html
alert(user); // no such variable (each module has independent variables)
let user = "John";
<!doctype html>
<script type="module" src="user.js"></script>
<script type="module" src="hello.js"></script>

Moduly by měly export co chtějí mít přístupné zvenčí a import co potřebují.

  • user.js by měl exportovat user proměnná.
  • hello.js měli byste jej importovat z user.js modul.

Jinými slovy, u modulů používáme import/export namísto spoléhání se na globální proměnné.

Toto je správná varianta:

Resulthello.jsuser.jsindex.html
import {user} from './user.js';

document.body.innerHTML = user; // John
export let user = "John";
<!doctype html>
<script type="module" src="hello.js"></script>

V prohlížeči, pokud mluvíme o HTML stránkách, existuje také nezávislý rozsah nejvyšší úrovně pro každý <script type="module"> .

Zde jsou dva skripty na stejné stránce, oba type="module" . Nevidí navzájem své proměnné nejvyšší úrovně:

<script type="module">
 // The variable is only visible in this module script
 let user = "John";
</script>

<script type="module">
 alert(user); // Error: user is not defined
</script>
Poznámka:

V prohlížeči můžeme vytvořit proměnnou na úrovni okna globální tak, že ji explicitně přiřadíme k window majetek, např. window.user = "John" .

Pak to uvidí všechny skripty, oba s type="module" a bez něj.

To znamená, že vytváření takových globálních proměnných je odsuzováno. Zkuste se jim prosím vyhnout.

Kód modulu je vyhodnocen pouze při prvním importu

Pokud je stejný modul importován do více dalších modulů, jeho kód se provede pouze jednou, při prvním importu. Poté jsou jeho exporty předány všem dalším dovozcům.

Jednorázové hodnocení má důležité důsledky, kterých bychom si měli být vědomi.

Podívejme se na několik příkladů.

Za prvé, pokud spuštění kódu modulu přináší vedlejší efekty, jako je zobrazení zprávy, pak jej vícenásobný import spustí pouze jednou – poprvé:

// 📁 alert.js
alert("Module is evaluated!");
// Import the same module from different files

// 📁 1.js
import `./alert.js`; // Module is evaluated!

// 📁 2.js
import `./alert.js`; // (shows nothing)

Druhý import neukazuje nic, protože modul již byl vyhodnocen.

Existuje pravidlo:kód modulu nejvyšší úrovně by měl být použit pro inicializaci, vytváření interních datových struktur specifických pro modul. Pokud potřebujeme udělat něco volatelného vícekrát – měli bychom to exportovat jako funkci, jako jsme to udělali s sayHi výše.

Nyní se podívejme na hlubší příklad.

Řekněme, že modul exportuje objekt:

// 📁 admin.js
export let admin = {
 name: "John"
};

Pokud je tento modul importován z více souborů, modul se vyhodnotí pouze poprvé, admin objekt je vytvořen a poté předán všem dalším importérům.

Všichni dovozci dostanou přesně ten jediný admin objekt:

// 📁 1.js
import {admin} from './admin.js';
admin.name = "Pete";

// 📁 2.js
import {admin} from './admin.js';
alert(admin.name); // Pete

// Both 1.js and 2.js reference the same admin object
// Changes made in 1.js are visible in 2.js

Jak můžete vidět, když 1.js změní name vlastnost v importovaném admin a poté 2.js můžete vidět nový admin.name .

To je přesně proto, že modul je spuštěn pouze jednou. Exporty jsou generovány a poté jsou sdíleny mezi dovozci, takže pokud se něco změní, admin objekt, ostatní dovozci to uvidí.

Takové chování je ve skutečnosti velmi pohodlné, protože nám umožňuje konfigurovat moduly.

Jinými slovy, modul může poskytovat obecnou funkcionalitu, která vyžaduje nastavení. Např. autentizace vyžaduje přihlašovací údaje. Poté může exportovat konfigurační objekt a očekává, že mu bude přiřazen vnější kód.

Zde je klasický vzor:

  1. Modul exportuje některé prostředky konfigurace, např. konfigurační objekt.
  2. Při prvním importu jej inicializujeme, zapíšeme do jeho vlastností. To může udělat skript aplikace nejvyšší úrovně.
  3. Další importy využívají modul.

Například admin.js modul může poskytovat určité funkce (např. ověřování), ale očekávejte, že přihlašovací údaje přijdou do config objekt zvenčí:

// 📁 admin.js
export let config = { };

export function sayHi() {
 alert(`Ready to serve, ${config.user}!`);
}

Zde admin.js exportuje config objekt (zpočátku prázdný, ale může mít také výchozí vlastnosti).

Poté v init.js , první skript naší aplikace, importujeme config z něj a nastavte config.user :

// 📁 init.js
import {config} from './admin.js';
config.user = "Pete";

…Nyní modul admin.js je nakonfigurován.

Další importéři jej mohou volat a správně zobrazuje aktuálního uživatele:

// 📁 another.js
import {sayHi} from './admin.js';

sayHi(); // Ready to serve, Pete!

import.meta

Objekt import.meta obsahuje informace o aktuálním modulu.

Jeho obsah závisí na prostředí. V prohlížeči obsahuje adresu URL skriptu nebo adresu URL aktuální webové stránky, pokud je uvnitř HTML:

<script type="module">
 alert(import.meta.url); // script URL
 // for an inline script - the URL of the current HTML-page
</script>

V modulu není „toto“ definováno

To je druh menší funkce, ale pro úplnost bychom ji měli zmínit.

V modulu nejvyšší úrovně this není definováno.

Porovnejte to s nemodulovými skripty, kde this je globální objekt:

<script>
 alert(this); // window
</script>

<script type="module">
 alert(this); // undefined
</script>

Funkce specifické pro prohlížeč

Existuje také několik rozdílů ve skriptech specifických pro prohlížeč s type="module" ve srovnání s běžnými.

Pokud čtete poprvé nebo pokud v prohlížeči nepoužíváte JavaScript, můžete tuto část prozatím přeskočit.

Skripty modulu jsou odloženy

Skripty modulu jsou vždy odloženo, stejný účinek jako defer atribut (popsaný v kapitole Skripty:async, defer), pro externí i vložené skripty.

Jinými slovy:

  • stažení skriptů externího modulu <script type="module" src="..."> neblokuje zpracování HTML, načítají se paralelně s jinými zdroji.
  • Skripty modulu čekají, dokud nebude dokument HTML zcela připraven (i když jsou malé a načítají se rychleji než HTML), a pak se spustí.
  • Je zachováno relativní pořadí skriptů:skripty, které jsou v dokumentu první, se spouštějí jako první.

Vedlejším efektem je, že skripty modulů vždy „vidí“ plně načtenou stránku HTML, včetně prvků HTML pod nimi.

Například:

<script type="module">
 alert(typeof button); // object: the script can 'see' the button below
 // as modules are deferred, the script runs after the whole page is loaded
</script>

Compare to regular script below:

<script>
 alert(typeof button); // button is undefined, the script can't see elements below
 // regular scripts run immediately, before the rest of the page is processed
</script>

<button id="button">Button</button>

Vezměte prosím na vědomí:druhý skript ve skutečnosti běží před prvním! Takže uvidíme undefined nejprve a poté object .

Je to proto, že moduly jsou odloženy, takže čekáme na zpracování dokumentu. Běžný skript běží okamžitě, takže jeho výstup vidíme jako první.

Při používání modulů bychom si měli uvědomit, že stránka HTML se zobrazuje při načítání a moduly JavaScriptu se spouštějí až poté, takže uživatel může stránku vidět dříve, než bude aplikace JavaScript připravena. Některé funkce ještě nemusí fungovat. Měli bychom umístit „indikátory načítání“ nebo jinak zajistit, aby tím návštěvník nebyl zmaten.

Async funguje na vložených skriptech

U nemodulových skriptů async atribut funguje pouze na externích skriptech. Asynchronní skripty se spouštějí okamžitě, když jsou připraveny, nezávisle na jiných skriptech nebo dokumentu HTML.

V případě modulových skriptů to funguje také s vloženými skripty.

Například níže uvedený vložený skript má async , takže na nic nečeká.

Provede import (načte ./analytics.js ) a spustí se, když je připraven, i když dokument HTML ještě není dokončen nebo pokud další skripty stále čekají na zpracování.

To je dobré pro funkce, které na ničem nezávisí, jako jsou počítadla, reklamy, posluchače událostí na úrovni dokumentu.

<!-- all dependencies are fetched (analytics.js), and the script runs -->
<!-- doesn't wait for the document or other <script> tags -->
<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:

  1. Externí skripty se stejným src spustit pouze jednou:

    <!-- the script my.js is fetched and executed only once -->
    <script type="module" src="my.js"></script>
    <script type="module" src="my.js"></script>
  2. Externí skripty, které jsou načítány z jiného zdroje (např. z jiného webu), vyžadují záhlaví CORS, jak je popsáno v kapitole Načítání:Požadavky napříč zdrojem. Jinými slovy, pokud je skript modulu načten z jiného zdroje, vzdálený server musí dodat hlavičku Access-Control-Allow-Origin povolení načtení.

    <!-- another-site.com must supply Access-Control-Allow-Origin -->
    <!-- otherwise, the script won't execute -->
    <script type="module" src="http://another-site.com/their.js"></script>

    To ve výchozím nastavení zajišťuje lepší zabezpečení.

Žádné „holé“ moduly nejsou povoleny

V prohlížeči import musí získat buď relativní nebo absolutní adresu URL. Moduly bez jakékoli cesty se nazývají „holé“ moduly. Takové moduly nejsou v import povoleny .

Například tento import je neplatné:

import {sayHi} from 'sayHi'; // Error, "bare" module
// the module must have a path, e.g. './sayHi.js' or wherever the module is

Některá prostředí, jako je Node.js nebo nástroje balíků, umožňují holé moduly bez jakékoli cesty, protože mají své vlastní způsoby, jak najít moduly a háčky k jejich jemnému vyladění. Prohlížeče však holé moduly zatím nepodporují.

Kompatibilita, „nomodule“

Staré prohlížeče nerozumí type="module" . Skripty neznámého typu jsou prostě ignorovány. Pro ně je možné poskytnout záložní řešení pomocí nomodule atribut:

<script type="module">
 alert("Runs in modern browsers");
</script>

<script nomodule>
 alert("Modern browsers know both type=module and nomodule, so skip this")
 alert("Old browsers ignore script with unknown type=module, but execute this.");
</script>

Nástroje pro vytváření

V reálném životě se moduly prohlížeče zřídka používají ve své „surové“ podobě. Obvykle je spojujeme se speciálním nástrojem, jako je Webpack, a nasazujeme na produkční server.

Jedna z výhod používání bundlerů – poskytují větší kontrolu nad tím, jak jsou moduly vyřešeny, umožňují holé moduly a mnoho dalšího, jako jsou moduly CSS/HTML.

Nástroje pro sestavení provedou následující:

  1. Vezměte si „hlavní“ modul, který má být vložen do <script type="module"> v HTML.
  2. Analyzujte jeho závislosti:importy a poté importy importů atd.
  3. Vytvořte jeden soubor se všemi moduly (nebo více souborů, které lze ladit), nahraďte nativní import volání s funkcemi bundleru, aby to fungovalo. Podporovány jsou také „speciální“ typy modulů, jako jsou moduly HTML/CSS.
  4. V tomto procesu mohou být použity další transformace a optimalizace:
    • Nedostupný kód byl odstraněn.
    • Nepoužité exporty byly odstraněny („třesení stromů“).
    • Prohlášení specifická pro vývoj jako console a debugger odstraněno.
    • Moderní, nedokonalá syntaxe JavaScriptu může být pomocí Babel transformována na starší s podobnou funkčností.
    • Výsledný soubor je minifikován (odstraněny mezery, proměnné nahrazeny kratšími názvy atd.).

Pokud použijeme nástroje pro balíčky, pak jsou skripty sdruženy do jednoho souboru (nebo několika souborů), import/export příkazy uvnitř těchto skriptů jsou nahrazeny speciálními bundlerovými funkcemi. Výsledný „sbalený“ skript tedy neobsahuje žádný import/export , nevyžaduje type="module" , a můžeme to vložit do běžného skriptu:

<!-- Assuming we got bundle.js from a tool like Webpack -->
<script src="bundle.js"></script>

To znamená, že nativní moduly jsou také použitelné. Webpack zde tedy používat nebudeme:můžete jej nakonfigurovat později.

Shrnutí

Abychom to shrnuli, základní pojmy jsou:

  1. Modul je soubor. Chcete-li vytvořit import/export fungují, prohlížeče potřebují <script type="module"> . Moduly mají několik rozdílů:
    • Ve výchozím nastavení odloženo.
    • Async funguje na vložených skriptech.
    • Pro načtení externích skriptů z jiného zdroje (domény/protokolu/portu) jsou zapotřebí záhlaví CORS.
    • Duplicitní externí skripty jsou ignorovány.
  2. Moduly mají svůj vlastní, místní nejvyšší rozsah a funkce výměny prostřednictvím import/export .
  3. Moduly vždy use strict .
  4. Kód modulu se spustí pouze jednou. Exporty jsou vytvořeny jednou a sdíleny mezi dovozci.

Když používáme moduly, každý modul implementuje funkcionalitu a exportuje ji. Pak použijeme import přímo importovat tam, kde je potřeba. Prohlížeč načte a vyhodnotí skripty automaticky.

Ve výrobě lidé často používají balíčky, jako je Webpack, ke sbalení modulů z důvodu výkonu a dalších důvodů.

V další kapitole uvidíme více příkladů modulů a toho, jak lze věci exportovat/importovat.