Išsamus „JavaScript Import Maps“ vadovas, apžvelgiantis „scopes“ funkciją, paveldėjimą ir modulio sprendimo hierarchiją šiuolaikinei žiniatinklio kūrybai.
Atveriame naują žiniatinklio kūrimo erą: išsamus žvilgsnis į JavaScript importų žemėlapių aprėpties paveldėjimą
JavaScript modulių kelias buvo ilgas ir vingiuotas. Nuo ankstyvojo žiniatinklio globaliosios vardų erdvės chaoso iki sudėtingų modelių, tokių kaip CommonJS, skirtas Node.js, ir AMD naršyklėms, kūrėjai nuolat ieškojo geresnių būdų organizuoti ir bendrinti kodą. Nativių ES modulių (ESM) atsiradimas žymi didžiulį poslinkį, standartizuojant modulio sistemą tiesiogiai JavaScript kalboje ir naršyklėse.
Tačiau šis naujas standartas sukėlė didelę kliūtį naršyklėmis pagrįstai plėtrai. Paprasti, elegantiški importo teiginiai, prie kurių pripratome Node.js, pavyzdžiui, import _ from 'lodash';
, naršyklėje sukeltų klaidą. Taip yra todėl, kad naršyklės, skirtingai nei Node.js su savo `node_modules` algoritmu, neturi natyvaus mechanizmo, skirto šių "pirminių modulio specifikatorių" pavertimui galiojančiu URL.
Daugelį metų sprendimas buvo privalomas kūrimo žingsnis. Įrankiai, tokie kaip Webpack, Rollup ir Parcel, surinktų mūsų kodą, paversdami šiuos pirminius specifikatorius į naršyklei suprantamus kelius. Nors galingi, šie įrankiai padidino sudėtingumą, konfigūracijos sąnaudas ir sulėtino grįžtamojo ryšio ciklus kūrimo procese. Ką daryti, jei būtų natyvus, be kūrimo įrankių, būdas tai išspręsti? Pristatome JavaScript importų žemėlapius.
Importų žemėlapiai yra W3C standartas, suteikiantis natyvų mechanizmą, skirtą valdyti JavaScript importų elgesį. Jie veikia kaip paieškos lentelė, tiksliai nurodanti naršyklei, kaip modulio specifikatorius paversti konkrečiais URL. Tačiau jų galia gerokai viršija paprastą aliasingą. Tikrasis žaidimo keitiklis slypi mažiau žinomoje, bet neįtikėtinai galingoje funkcijoje: `scopes` (aprėptyse). Aprėptys leidžia kontekstinį modulio sprendimą, leidžiant skirtingoms jūsų programos dalims importuoti tą patį specifikatorių, bet susieti jį su skirtingais moduliais. Tai atveria naujas architektūrines galimybes mikro-frontendams, A/B testavimui ir sudėtingam priklausomybių valdymui, nereikalaujant nė vienos eilučių konfigūracijos.
Šis išsamus vadovas leis jums giliai pasinerti į importų žemėlapių pasaulį, ypatingą dėmesį skiriant `scopes` valdomos modulio sprendimo hierarchijos paaiškinimui. Ištirsime, kaip veikia aprėpties paveldėjimas (arba, tiksliau, atsarginio kopijavimo mechanizmas), išanalizuosime sprendimo algoritmą ir atskleisime praktinius modelius, kurie pakeis jūsų šiuolaikinį žiniatinklio kūrimo procesą.
Kas yra JavaScript importų žemėlapiai? Pagrindinė apžvalga
Iš esmės importų žemėlapis yra JSON objektas, kuris nurodo sąsają tarp modulio pavadinimo, kurį kūrėjas nori importuoti, ir atitinkamo modulio failo URL. Jis leidžia naudoti švarius, pirminius modulio specifikatorius jūsų kode, lygiai taip pat kaip Node.js aplinkoje, ir leidžia naršyklei tvarkyti sprendimą.
Pagrindinė sintaksė
Importų žemėlapį deklaruojate naudodami <script>
žymą su atributu type="importmap"
. Ši žyma turi būti patalpinta HTML dokumente prieš bet kokias <script type="module">
žymas, kurios naudoja susietus importus.
Štai paprastas pavyzdys:
<!DOCTYPE html>
<html>
<head>
<!-- The Import Map -->
<script type="importmap">
{
"imports": {
"moment": "https://cdn.skypack.dev/moment",
"lodash": "/js/vendor/lodash-4.17.21.min.js",
"app/": "/js/app/"
}
}
</script>
<!-- Your Application Code -->
<script type="module" src="/js/main.js"></script>
</head>
<body>
<h1>Welcome to Import Maps!</h1>
</body>
</html>
Mūsų /js/main.js
faile dabar galime rašyti kodą taip:
// This works because "moment" is mapped in the import map.
import moment from 'moment';
// This works because "lodash" is mapped.
import { debounce } from 'lodash';
// This is a package-like import for your own code.
// It resolves to /js/app/utils.js because of the "app/" mapping.
import { helper } from 'app/utils.js';
console.log('Today is:', moment().format('MMMM Do YYYY'));
Išskaidykime `imports` objektą:
"moment": "https://cdn.skypack.dev/moment"
: Tai yra tiesioginis susiejimas. Kaskart, kai naršyklė matoimport ... from 'moment'
, ji paims modulį iš nurodyto CDN URL."lodash": "/js/vendor/lodash-4.17.21.min.js"
: Tai susieja `lodash` specifikatorių su lokaliai talpinamu failu."app/": "/js/app/"
: Tai yra keliais pagrįstas susiejimas. Atkreipkite dėmesį į brūkšnelį gale tiek rakto, tiek reikšmės. Tai nurodo naršyklei, kad bet kuris importo specifikatorius, prasidedantis `app/`, turėtų būti sprendžiamas atsižvelgiant į `/js/app/`. Pavyzdžiui, `import ... from 'app/auth/user.js'` būtų sprendžiamas kaip `/js/app/auth/user.js`. Tai nepaprastai naudinga struktūrizuojant savo programos kodą, nenaudojant netvarkingų santykinių kelių, tokių kaip `../../`.
Pagrindiniai privalumai
Net ir naudojant šį paprastą būdą, privalumai yra akivaizdūs:
- Kūrimas be surinkimo: Galite rašyti modernų, modulinį JavaScript ir paleisti jį tiesiai naršyklėje be surinkėjo. Tai leidžia greičiau atnaujinti ir paprasčiau nustatyti kūrimo aplinką.
- Atsietos priklausomybės: Jūsų programos kodas nurodo abstrakčius specifikatorius (`'moment'`) vietoj griežtai nurodytų URL. Tai leidžia lengvai keisti versijas, CDN teikėjus arba perkelti iš vietinio failo į CDN, pakeičiant tik importų žemėlapio JSON.
- Pagerintas talpinimas: Kadangi moduliai įkeliami kaip atskiri failai, naršyklė gali juos talpinti nepriklausomai. Pakeitus vieną mažą modulį, nereikia iš naujo atsisiųsti didelio paketo.
Už pagrindų ribų: pristatome „scopes“ (aprėptis) smulkiam valdymui
Aukščiausio lygio `imports` raktas suteikia globalų susiejimą visai jūsų programai. Tačiau kas nutinka, kai jūsų programa tampa sudėtingesnė? Apsvarstykite scenarijų, kai kuriate didelę žiniatinklio programą, integruojančią trečiosios šalies pokalbių valdiklį. Pagrindinė programa naudoja 5-ąją diagramų bibliotekos versiją, tačiau senasis pokalbių valdiklis suderinamas tik su 4-ąja versija.
Be „scopes“ (aprėpčių) susidurtumėte su sunkiu pasirinkimu: pabandyti pertvarkyti valdiklį, rasti kitą valdiklį arba susitaikyti, kad negalite naudoti naujesnės diagramų bibliotekos. Būtent šią problemą buvo sukurta išspręsti „scopes“ funkcija.
Importų žemėlapio `scopes` raktas leidžia apibrėžti skirtingus to paties specifikatoriaus susiejimus, atsižvelgiant į tai, iš kur importuojama. Jis suteikia kontekstinį arba aprėpties modulio sprendimą.
„Scopes“ (aprėpčių) struktūra
„Scopes“ reikšmė yra objektas, kuriame kiekvienas raktas yra URL priešdėlis, atstovaujantis „aprėpties keliui“. Kiekvieno aprėpties kelio reikšmė yra `imports` panašus objektas, apibrėžiantis susiejimus, kurie taikomi konkrečiai toje aprėptyje.
Išspręskime mūsų diagramų bibliotekos problemą su pavyzdžiu:
<script type="importmap">
{
"imports": {
"charting-lib": "/libs/charting-lib/v5/main.js",
"api-client": "/js/api/v2/client.js"
},
"scopes": {
"/widgets/chat/": {
"charting-lib": "/libs/charting-lib/v4/legacy.js"
}
}
}
</script>
<script type="module" src="/js/app.js"></script>
<script type="module" src="/widgets/chat/init.js"></script>
Štai kaip naršyklė tai interpretuoja:
- Scenarijus, esantis `/js/app.js`, nori importuoti `charting-lib`. Naršyklė patikrina, ar scenarijaus kelias (`/js/app.js`) atitinka bet kurį iš aprėpties kelių. Jis neatitinka `/widgets/chat/`. Todėl naršyklė naudoja aukščiausio lygio `imports` susiejimą, o `charting-lib` išsprendžiama kaip `/libs/charting-lib/v5/main.js`.
- Scenarijus, esantis `/widgets/chat/init.js`, taip pat nori importuoti `charting-lib`. Naršyklė mato, kad šio scenarijaus kelias (`/widgets/chat/init.js`) patenka į `/widgets/chat/` aprėptį. Ji ieško šioje aprėptyje `charting-lib` susiejimo ir jį randa. Taigi, šiam scenarijui ir visiems moduliai, kuriuos jis importuoja iš to kelio, `charting-lib` išsprendžiama kaip `/libs/charting-lib/v4/legacy.js`.
Naudodami „scopes“ (aprėptis), sėkmingai leidome dviems mūsų programos dalims naudoti skirtingas tos pačios priklausomybės versijas, taikiai egzistuojančias be konfliktų. Tai yra valdymo lygis, kuris anksčiau buvo pasiekiamas tik naudojant sudėtingas surinkimo įrankio konfigūracijas arba „iframe“ pagrįstą izoliaciją.
Pagrindinė koncepcija: aprėpties paveldėjimo ir modulio sprendimo hierarchijos supratimas
Dabar pasiekiame esmę. Kaip naršyklė nusprendžia, kurią aprėptį naudoti, kai keli aprėptys gali atitikti failo kelią? Ir kas nutinka susiejimams aukščiausio lygio `imports` objekte? Tai valdoma aiškia ir nuspėjama hierarchija.
Auksinė taisyklė: specifiškiausia aprėptis laimi
Pagrindinis aprėpties sprendimo principas yra specifiškumas. Kai modulis tam tikru URL prašo kito modulio, naršyklė peržiūri visus `scopes` objekto raktus. Ji randa ilgiausią raktą, kuris yra prašančio modulio URL priešdėlis. Ši "specifiškiausia" atitinkanti aprėptis yra vienintelė, kuri bus naudojama importo sprendimui. Visi kiti aprėptys ignoruojamos šiam konkrečiam sprendimui.
Iliustruokime tai sudėtingesne failų struktūra ir importų žemėlapiu.
Failų struktūra:
- `/index.html` (yra importų žemėlapis)
- `/js/main.js`
- `/js/feature-a/index.js`
- `/js/feature-a/core/logic.js`
Importų žemėlapis `index.html` faile:
{
"imports": {
"api": "/js/api/v1/api.js",
"ui-kit": "/js/ui/v2/kit.js"
},
"scopes": {
"/js/feature-a/": {
"api": "/js/api/v2-beta/api.js"
},
"/js/feature-a/core/": {
"api": "/js/api/v3-experimental/api.js",
"ui-kit": "/js/ui/v1/legacy-kit.js"
}
}
}
Dabar panagrinėkime `import api from 'api';` ir `import ui from 'ui-kit';` sprendimą iš skirtingų failų:
-
Faile `/js/main.js`:
- Kelias `/js/main.js` neatitinka `/js/feature-a/` ar `/js/feature-a/core/`.
- Jokia aprėptis neatitinka. Sprendimas grįžta prie aukščiausio lygio `imports`.
- `api` išsprendžiama kaip `/js/api/v1/api.js`.
- `ui-kit` išsprendžiama kaip `/js/ui/v2/kit.js`.
-
Faile `/js/feature-a/index.js`:
- Kelias `/js/feature-a/index.js` yra priešdėlis `/js/feature-a/`. Jis nėra priešdėlis `/js/feature-a/core/`.
- Specifiškiausia atitinkanti aprėptis yra `/js/feature-a/`.
- Ši aprėptis turi `api` susiejimą. Todėl `api` išsprendžiama kaip `/js/api/v2-beta/api.js`.
- Ši aprėptis neturi `ui-kit` susiejimo. Šio specifikatoriaus sprendimas grįžta prie aukščiausio lygio `imports`. `ui-kit` išsprendžiama kaip `/js/ui/v2/kit.js`.
-
Faile `/js/feature-a/core/logic.js`:
- Kelias `/js/feature-a/core/logic.js` yra priešdėlis tiek `/js/feature-a/`, tiek `/js/feature-a/core/`.
- Kadangi `/js/feature-a/core/` yra ilgesnis ir todėl specifiškesnis, jis pasirenkamas kaip laiminti aprėptis. Aprėptis `/js/feature-a/` visiškai ignoruojama šiam failui.
- Ši aprėptis turi `api` susiejimą. `api` išsprendžiama kaip `/js/api/v3-experimental/api.js`.
- Ši aprėptis taip pat turi `ui-kit` susiejimą. `ui-kit` išsprendžiama kaip `/js/ui/v1/legacy-kit.js`.
Tiesa apie „paveldėjimą“: tai atsarginis variantas, o ne suliejimas
Labai svarbu suprasti dažną painiavos tašką. Terminas „aprėpties paveldėjimas“ gali būti klaidinantis. Specifiškesnė aprėptis nepaveldi ir nesusilieja su mažiau specifine (tėvine) aprėptimi. Sprendimo procesas yra paprastesnis ir tiesioginis:
- Raskite vieną specifiškiausią atitinkančią aprėptį importuojamo scenarijaus URL.
- Jei ta aprėptis turi nurodyto specifikatoriaus susiejimą, naudokite jį. Procesas čia baigiasi.
- Jei laimėjusi aprėptis neturi specifikatoriaus susiejimo, naršyklė nedelsdama patikrina aukščiausio lygio `imports` objekte, ar nėra susiejimo. Ji nežiūri į jokias kitas, mažiau specifines aprėptis.
- Jei susiejimas randamas aukščiausio lygio `imports`, jis naudojamas.
- Jei susiejimas nerandamas nei laimėjusioje aprėptyje, nei aukščiausio lygio `imports`, išmetama `TypeError` klaida.
Grįžkime prie paskutinio pavyzdžio, kad tai sustiprintume. Kai sprendžiama `ui-kit` iš `/js/feature-a/index.js`, laimėjusi aprėptis buvo `/js/feature-a/`. Ši aprėptis neapibrėžė `ui-kit`, todėl naršyklė netikrino `/` aprėpties (kurios nėra kaip rakto) ar jokio kito tėvo. Ji iškart perėjo prie globalaus `imports` ir ten rado susiejimą. Tai yra atsarginio kopijavimo mechanizmas, o ne kaskadinis ar susiliejantis paveldėjimas, kaip CSS.
Praktiniai pritaikymai ir pažangūs scenarijai
Aprėpties importų žemėlapių galia išties atsiskleidžia sudėtingose, realaus pasaulio programose. Štai keletas architektūrinių modelių, kuriuos jie įgalina.
Mikro-frontendai
Tai neabejotinai yra pagrindinis importų žemėlapių aprėpčių naudojimo atvejis. Įsivaizduokite el. prekybos svetainę, kurioje produktų paieška, pirkinių krepšelis ir apmokėjimas yra atskiros programos (mikro-frontendai), sukurtos skirtingų komandų. Jos visos yra integruotos į vieną pagrindinį puslapį.
- Paieškos komanda gali naudoti naujausią React versiją.
- Krepšelio komanda gali naudoti senesnę, stabilią React versiją dėl senos priklausomybės.
- Pagrindinė programa gali naudoti Preact savo apvalkalui, kad būtų lengva.
Importų žemėlapis gali tai sklandžiai koordinuoti:
{
"imports": {
"react": "/libs/preact/v10/preact.js",
"react-dom": "/libs/preact/v10/preact-dom.js",
"shared-state": "/js/state-manager.js"
},
"scopes": {
"/apps/search/": {
"react": "/libs/react/v18/react.js",
"react-dom": "/libs/react/v18/react-dom.js"
},
"/apps/cart/": {
"react": "/libs/react/v17/react.js",
"react-dom": "/libs/react/v17/react-dom.js"
}
}
}
Čia kiekvienas mikro-frontendas, identifikuojamas pagal savo URL kelią, gauna savo izoliuotą React versiją. Jie vis dar gali importuoti `shared-state` modulį iš aukščiausio lygio `imports`, kad bendrautų tarpusavyje. Tai užtikrina stiprų kapsuliavimą, tuo pačiu leidžiant kontroliuojamą sąveiką, visa tai be sudėtingų surinkimo įrankio federacijos nustatymų.
A/B testavimas ir funkcijų žymėjimas
Norite išbandyti naują atsiskaitymo srauto versiją tam tikrai daliai vartotojų? Galite pateikti šiek tiek kitokį `index.html` testuojamai grupei su pakeistu importų žemėlapiu.
Kontrolinės grupės importų žemėlapis:
{
"imports": {
"checkout-flow": "/js/checkout/v1/flow.js"
}
}
Bandomosios grupės importų žemėlapis:
{
"imports": {
"checkout-flow": "/js/checkout/v2-beta/flow.js"
}
}
Jūsų programos kodas lieka identiškas: `import start from 'checkout-flow';`. Kuris modulis įkeliamas, sprendžiama visiškai importų žemėlapio lygiu, kuris gali būti dinamiškai generuojamas serveryje, atsižvelgiant į vartotojo slapukus ar kitus kriterijus.
Monorepo valdymas
Dideliame monorepo galite turėti daug vidinių paketų, kurie priklauso vienas nuo kito. Aprėptys gali padėti tvarkingai valdyti šias priklausomybes. Kiekvieno paketo pavadinimą galite susieti su jo išeities kodu kūrimo metu.
{
"imports": {
"@my-corp/design-system": "/packages/design-system/src/index.js",
"@my-corp/utils": "/packages/utils/src/index.js"
},
"scopes": {
"/packages/design-system/": {
"@my-corp/utils": "/packages/design-system/src/vendor/utils-shim.js"
}
}
}
Šiame pavyzdyje dauguma paketų gauna pagrindinę `utils` biblioteką. Tačiau `design-system` paketas, galbūt dėl tam tikros priežasties, gauna „shimmed“ arba skirtingą `utils` versiją, apibrėžtą savo aprėptyje.
Naršyklės palaikymas, įrankiai ir diegimo aspektai
Naršyklės palaikymas
Nuo 2023 m. pabaigos natyvus importų žemėlapių palaikymas yra prieinamas visose pagrindinėse šiuolaikinėse naršyklėse, įskaitant Chrome, Edge, Safari ir Firefox. Tai reiškia, kad galite pradėti juos naudoti gamyboje daugumai savo vartotojų be jokių polifilų.
Atsarginiai variantai senesnėms naršyklėms
Programoms, kurios turi palaikyti senesnes naršykles, kurioms trūksta natyvaus importų žemėlapių palaikymo, bendruomenė turi tvirtą sprendimą: `es-module-shims.js` polifilą. Šis vienintelis scenarijus, įtrauktas prieš jūsų importų žemėlapį, grąžina importų žemėlapių ir kitų šiuolaikinių modulio funkcijų (pvz., dinaminio `import()`) palaikymą senesnėms aplinkoms. Jis yra lengvas, išbandytas ir rekomenduojamas būdas užtikrinti platų suderinamumą.
<!-- Polyfill for older browsers -->
<script async src="https://ga.jspm.io/npm:es-module-shims@1.8.2/dist/es-module-shims.js"></script>
<!-- Your import map -->
<script type="importmap">
...
</script>
Dinaminiai, serveryje generuojami žemėlapiai
Vienas iš galingiausių diegimo modelių yra visiškai neturėti statinio importų žemėlapio jūsų HTML faile. Vietoj to, jūsų serveris gali dinamiškai generuoti JSON, atsižvelgdamas į užklausą. Tai leidžia:
- Aplinkos keitimas: „Development“ aplinkoje pateikti nesumažintus, šaltinio žemėlapiuotus modulius, o „production“ – sumažintus, gamybai paruoštus modulius.
- Vartotojo vaidmenimis pagrįsti moduliai: Administratoriaus vartotojas gali gauti importų žemėlapį, kuriame yra susiejimai tik administratoriaus įrankiams.
- Lokalizavimas: Susieti `translations` modulį su skirtingais failais pagal vartotojo `Accept-Language` antraštę.
Geriausios praktikos ir galimi spąstai
Kaip ir su bet kokiu galingu įrankiu, yra geriausios praktikos, kurių reikia laikytis, ir spąstai, kurių reikia vengti.
- Išsaugokite skaitomumą: Nors galite sukurti labai gilias ir sudėtingas aprėpties hierarchijas, jas gali būti sunku derinti. Siekite paprasčiausios aprėpties struktūros, atitinkančios jūsų poreikius. Komentuokite savo importų žemėlapio JSON, jei jis tampa sudėtingas.
- Visada naudokite pasvirusius brūkšnius pabaigoje keliams: Kai susiejate kelio priešdėlį (pvz., katalogą), užtikrinkite, kad tiek raktas importų žemėlapyje, tiek URL reikšmė baigtųsi `/`. Tai labai svarbu, kad atitikimo algoritmas tinkamai veiktų visiems failams tame kataloge. To pamiršimas yra dažna klaidų priežastis.
- Spąstai: ne paveldėjimo spąstai: Atminkite, kad konkreti aprėptis nepaveldi iš mažiau specifinės. Ji grįžta *tik* į globalų `imports`. Jei derinant sprendimo problemą, visada pirmiausia nustatykite vieną laiminčią aprėptį.
- Spąstai: importų žemėlapio talpinimas: Jūsų importų žemėlapis yra įėjimo taškas visam modulio grafui. Jei atnaujinate modulio URL žemėlapyje, turite užtikrinti, kad vartotojai gautų naują žemėlapį. Dažna strategija yra stipriai netalpinti pagrindinio `index.html` failo arba dinamiškai įkelti importų žemėlapį iš URL, kuriame yra turinio maiša, nors pastarasis yra dažnesnis.
- Derinimas yra jūsų draugas: Šiuolaikinės naršyklės kūrėjo įrankiai puikiai tinka modulių problemų derinimui. Tinklo skirtuke galite tiksliai matyti, kuris URL buvo prašomas kiekvienam moduliui. Konsolėje sprendimo klaidos aiškiai nurodys, kuris specifikatorius nepavyko išspręsti iš kurio importuojamo scenarijaus.
Išvada: žiniatinklio kūrimo be surinkimo ateitis
JavaScript importų žemėlapiai, ir ypač jų „scopes“ funkcija, žymi paradigmos pokytį front-end kūrimo srityje. Jie perkelia reikšmingą logikos dalį – modulio sprendimą – iš išankstinio kompiliavimo kūrimo žingsnio tiesiai į naršyklės natyvinį standartą. Tai ne tik patogumas; tai apie lankstesnių, dinamiškesnių ir atsparesnių žiniatinklio programų kūrimą.
Matėme, kaip veikia modulio sprendimo hierarchija: visada laimi specifiškiausias aprėpties kelias, ir jis grįžta prie globalaus `imports` objekto, o ne prie tėvinių aprėpčių. Ši paprasta, bet galinga taisyklė leidžia kurti sudėtingas programų architektūras, tokias kaip mikro-frontendai, ir įgalina dinaminį elgesį, pvz., A/B testavimą, su stebėtinu lengvumu.
Kadangi žiniatinklio platforma toliau bręsta, priklausomybė nuo sunkių, sudėtingų kūrimo įrankių plėtrai mažėja. Importų žemėlapiai yra šios „be surinkimo“ ateities kertinis akmuo, siūlantis paprastesnį, greitesnį ir labiau standartizuotą būdą valdyti priklausomybes. Įsisavinę aprėpčių ir sprendimo hierarchijos sąvokas, jūs ne tik išmokstate naują naršyklės API; jūs aprūpinate save įrankiais, skirtais kurti naujos kartos programas globaliam žiniatinkliui.