Includováná v javascriptu:
<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-bad-integrity" crossorigin="anonymous"></script>
<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>
První řádek selže (kvůli špatnému subresource integrity integrity="sha256-bad-integrity"
), ale 2. už projde (žádná kontrola integrity). Běžný javascript je možné načítat opakovaně.
Jiná situace je ale u modulů:
<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-bad-integrity" crossorigin="anonymous"></script>
(povšimněte si type="module"
)
všechny myslitelné pokusy o vložení stejného souboru poté selžou:
<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>
<script src="https://iiic.dev/js/modules/importWithIntegrity.mjs" crossorigin="anonymous"></script>
ať už v head
či v body
… pro tento účel je chování shodné:
<script>
const element = document.createElement( 'SCRIPT' );
element.src = "https://iiic.dev/js/modules/importWithIntegrity.mjs";
document.body.appendChild( element );
</script>
<script type="module">
const element = document.createElement( 'SCRIPT' );
element.src = "https://iiic.dev/js/modules/importWithIntegrity.mjs";
document.body.appendChild( element );
</script>
<script>
import { importWithIntegrity } from 'https://iiic.dev/js/modules/importWithIntegrity.mjs';
</script>
<script type="module">
import { importWithIntegrity } from 'https://iiic.dev/js/modules/importWithIntegrity.mjs';
</script>
… a další možnosti vložení. Žádná neprojde (Failed to find a valid digest in the 'integrity' attribute for resource 'https://iiic.dev/js/modules/importWithIntegrity.mjs' with computed SHA-256 integrity 'cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII='. The resource has been blocked.). Důvodem je speciální chování javascriptových modulů, vkládají se pouze jednou . A to i v případě, že pokus o vložení napoprvé selže z důvodu špatné subresource integrity.
Podobně by mohl sloužit i preload:
<link rel="preload" as="script" href="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-some-integrity-string" crossorigin="anonymous">
ale narazíte na zásadní rozdíl mezi prohlížeči Chrome (testováno na verzi 79) a Firefox (testováno na verzi 71), kde v Chrome se integrita u preloadu kontroluje a pokud nesedí, tak se zablokuje další načítání souboru, ovšem ve Firefoxu ne, tam se při preloadu integrita nekontroluje a další načtení se provede. Z tohoto důvodu je link rel="preload"
k tomuto účelu nepoužitelný.
K preloadu modulů by měl sloužit nový link rel="modulepreload"
ovšem podpora je zatím špatná a pro potřebu kontorly integrity ve Firefoxu selhává zcela stejně jako link rel="preload"
. Prostě horká novinka, zatím až moc horká.
Nejsou 2 načítání toho samého javascriptu problém?
Ne, totiž, jak kdy :) . V případě modulů to problém není, ty se načtou pouze 1x. V případě že moduly nepoužíváte to problém je. Pak by se zbytečně jak přenášely data (pokud by tam nebyl nějaký agresivnější druh cache
ování) tak prováděl obsah javascriptu.
A ostatní prohlížeče?
Opera (verze 64) se chová stejně jako Chrome, tedy plná podpora. Firefox neumí preload
, jak bylo popsáno výše. IE nemá podporu vůbec a Edge (zatím) také ne. Viz sekce tohoto článku podpora modulů.
Co z toho tedy plyne?
Že javascriptové moduly jsou super, používejte javascriptové moduly:
Automaticky silná cache (to samé lze dosáhnout i bez modulů, pomocí Cache-Control: immutable, ale tady máte toto chování ve výchozím stavu)
Module se načítá a provádí pouze 1x opakované includy nejsou problém. Né že by byli problém i bez modulů, to není nic, co by vývojáře trápilo, ale když používáte dynamický import dostáváte se ke zcela novým možnostem, kód:
someHugeArray.forEach(( item ) => { import( 'myGreatModule.mjs' ).then( ( /** @type { Module } */ module ) => { doSomethingWith( module ); } ); });
není problémem, sice je import uvnitř cyklu, ale ke stahování i provádění souboru myGreatModule.mjs
dojde pouze při prvním průběhu cyklu.
Izolace… moduly jsou o něco více izolované než běžné scripty (ne až tak drasticky, jako
service worker
y, naštěstí, ale to by bylo povídání na samostatný článek), ale ukázat si to můžeme klasicky na příkladu:<script> console.log(this); // objekt Window </script> <script type="module"> console.log(this); // 'undefined' </script>
Nemusíte se ale bát, že se nedostanete k Window
případně window.document
a dalším. Až takhle striktní izolace není, jen musíte přistoupit přímo přes objekt (window
), nemůžete použít lokální kontext this
.
Díky izolaci se také kód z modulu vykoná o něco rychleji než javascript bez modulu, ale výkon tím nespasíte (zrychlení je minimální), spíše jen taková zajímavost k dobru modulů.
Dobře, ale jaká je podpora modulů?
Obsáhle vyjádřeno: různá, totiž prohlížeče implementují tuto technologii po částech. Proto například když vkládáte 1 modul uvnitř jiného javascriptu ( třeba uvnitř modulu ), má to jinou podporu, než když vkládáte modul do HTML stránky . Ve zkratce by se dalo najít úzké hrdlo, kterým je nulová (a tohle už se hádám nezlepší) podpora v IE a stejně tak nedostatečná (neumí ty dynamické importy) podpora v Edge, tam se to naštěstí zlepší, už teď funguje v nové Edge betě založené Edge na jádru Chromia.
Zpátky k tématu
No a protože šetříme čase, co takhle si vytvořit funkci pro import s kontrolou integrity?
A neudělal to už někdo, někdo jiný? No nevypadá to, Google by mi to jinak řekl. Respektive je na to knihovna RequireJS, ale není řešení v podobě 1 funkce bez nutnosti přidávat celou knihovnu (2.2 MiB v zipu). RequireJS toho umí spoustu, jenže né všechno z toho potřebuji, prakticky nic. Bylo by to krásné, kdyby knihovny byli rozdělené do modulů a člověk mohl použít jen to, co potřebuje. Snad v budoucnu, já jsem optimista. A jsem optimista i ohledně tématu… import s integritou se připravuje, zatím ve stádiu návrhu. K použití v prohlížeči je ale dlouhá cesta, napřed se zohlední komentáře a připomínky, pak se návrh schválí a pak se objeví v prohlížečích a pak po čase až tyto verze prohlížečů budou mít na svých strojích uživatelé můžeme používat. A samozřejmě nesmíme zapomínat na nadšence v teamech vývojářů prohlížečů. Třeba Chrome už několikrát ukázal, že věc kterou sám chce udělat standardem napřed nasadí do prohlížeče a až pak si projde to legislativní kolečko tvorby standardu.
Takže vlastními silami…
Způsobů jak zapsat import je hodně, krásně to popsali v MDN:
import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { export1 , export2 } from "module-name";
import { foo , bar } from "module-name/path/to/specific/un-exported/file";
import { export1 , export2 as alias2 , [...] } from "module-name";
import defaultExport, { export1 [ , [...] ] } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";
var promise = import("module-name");
zdroj: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#Syntax
Mohl by pomoct preload například:
<link rel="preload" as="script" href="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII=" crossorigin="anonymous">
Jenže jak jsem popsal výše situace s Firefoxem je nešťastná a proto musíte použít prostě javascript:
<script type="module" src="https://iiic.dev/js/modules/importWithIntegrity.mjs" integrity="sha256-cfOxrPKw+HRch/o1HAGTjzeo5g+8Ho2VW5Ki75Y6DII=" crossorigin="anonymous"></script>
následně libovolný způsob importu, to jestli se provede nebo ne bude záležet právě na odpovídající hodnotě atributu integrity
.
Dynamické importy
Jistě by někdo mohl říct že mezi preloadem a kompletním načtením (a provedením) javascriptu je velký rozdíl. Moduly s tím počítají a zavádí dynamický import. Jak ale zajistit ten proti změně zdrojového souboru kontrolou integrity?
Co takhle upravit funkci import()
Ale javascript je úžasně tvárný (někdo kdo z možnosti upravit si libovolnou funkci není tak nadšený by možná použil jiné slůvko než úžasně, ale to je jiný příběh). Takže co upravit standardní funkci import()
tak, aby vyžadovala subresource integrity string? To je opravdu špatný nápad, ale někdo si tu slepou uličku musel projít, a říct "tudy ne, přátelé". Totiž už to že import()
je funkce je samo o sobě chyba. Takže vlastně celý nadpis tohoto odstavce je tak trochu podvod. Vypadá to jako funkce, ale není. V MDN to popsali slovem function-like to je myslím docela přesné. Tahle specialita obnáší mimo jiné to, že ji nelze přetížit. A když vytvoříte funkci pojmenovanou import()
dostanete se do kolizního stavu, který nemá jednoznačné řešení. Prohlížeče si s ní ale poradí tím způsobem že použijí buď vaši novou funkci nebo zmíněné function-like chování podle kontextu. Nicméně nativní function-likeimport()
prostě přetížit nelze. Ono stejně v mnoha příručkách o javascriptových best practice se dočtete že přetěžování nativních funkcí není vhodné. Jinde jsou zase případy kdy to vhodné je. Tak jak tak v tomto případě nemáte na výběr, takže to nyní rozhodně neotvírejme jako otázku.
Nezbývá než tedy vytvořit si funkci novou, která může sama provést víše popsaný postup spočívající ve vytvoření preload
u v hlavičce stránky se subresource integrity kontrolou a následně provede dynamický import javascriptem.
Může vypadat například takto:
export function importWithIntegrity ( /** @type { String } */ path, /** @type { String } */ integrity )
{
const POSSIBLE_HASHES = [ 'sha256', 'sha384', 'sha512' ]; // same length… 6 chars
const INTEGRITY_DIVIDER = '-';
if ( !integrity ) {
integrity = 'is missing!';
}
if (
!POSSIBLE_HASHES.includes( integrity.substring( 0, 6 ).toLowerCase() )
|| integrity.substring( 6, 7 ) !== INTEGRITY_DIVIDER
) {
integrity = POSSIBLE_HASHES[ 0 ] + INTEGRITY_DIVIDER + integrity;
}
/** @type { HTMLScriptElement } */
const element = ( document.createElement( 'SCRIPT' ) ); // link rel="preload" also working, but NOT in Firefox :(
element.type = 'module';
element.src = path;
element.integrity = integrity;
element.setAttribute( 'crossorigin', 'anonymous' );
document.head.appendChild( element );
return new Promise( ( /** @type { Function } */ resolve ) =>
{
import( path ).then( ( /** @type { Module } */ module ) =>
{
resolve( module );
} );
} );
}
( https://iiic.dev/js/modules/importWithIntegrity.mjs )
Pro použití nyní máte 2 možnosti, buď tuto funkci includovat do globálního scope, nebo ji volat až v případě že je skutečně potřeba. To vám sice může způsobit mírné zpomalení scriptu (pokud ještě není modul nacacheovaný v prohlížeči uživatele), ale zase ušetřit nějaká data v případě že do tohoto stavu se javascript dostane jen příležitostně. To už je na zvážení každého programátora a konkrétní situaci:
importWithIntegrity(
'https://iiic.dev/js/modules/url/completeHash.mjs',
'sha256-csBcPTKCf0Z5pJMrAtLx2B4mx25eSqDSgtoAxbVKu0I='
).then( ( /** @type { Module } */ completeHash ) => {
new completeHash.append( URL );
return new URL(window.location.href).completeHash();
} ).then( ( str ) => {
console.log( str );
} );
Samotná nová funkce importWithIntegrity()
umožní načíst libovolný modul a přitom zkontroluje jeho subresource integrity, ale neumí takto načíst sama sebe, takže nezapomeňte na kontrolní součet při vkládání scriptu v hlavičce stránky (nebo kdekoliv jinde, tak jak tak se modul chová jako defer
).