または、JavaScript インポートの動作を制御する方法
<script>
では再マッピングが機能しません<base>
要素import.meta.resolve()
この提案により、JavaScript import
ステートメントとimport()
式によってフェッチされる URL を制御できるようになります。これにより、 import moment from "moment"
などの「裸のインポート指定子」が機能するようになります。
これを行うためのメカニズムは、一般にモジュール指定子の解決を制御するために使用できるインポート マップを介して行われます。入門例として、次のコードを考えてみましょう。
import moment from "moment" ;
import { partition } from "lodash" ;
現在、このような裸の指定子は明示的に予約されているため、これがスローされます。ブラウザに次のインポート マップを提供することにより、
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
上記はあなたが書いたかのように動作します
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
<script>
のtype=""
属性の新しい"importmap"
値の詳細については、インストールのセクションを参照してください。今のところ、インストールの議論は延期して、マッピングのセマンティクスに集中します。
CommonJS (Node 内、またはブラウザーの webpack/browserify を使用してバンドルされたもの) など、ES2015 より前のモジュール システムの経験を持つ Web 開発者は、単純な構文を使用してモジュールをインポートできることに慣れています。
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
JavaScript の組み込みモジュール システムの言語に翻訳すると、次のようになります。
import $ from "jquery" ;
import { pluck } from "lodash" ;
このようなシステムでは、 "jquery"
または"lodash"
の裸のインポート指定子が完全なファイル名または URL にマップされます。さらに詳しく言うと、これらの指定子は通常 npm で配布されるpackageを表します。パッケージの名前を指定するだけで、そのパッケージのメイン モジュールを暗黙的に要求します。
このシステムの主な利点は、エコシステム全体での調整が容易になることです。誰でもモジュールを作成し、パッケージの既知の名前を使用して import ステートメントを含めることができ、Node.js ランタイムまたはビルド時ツールにそれをディスク上の実際のファイルに変換させることができます (バージョン管理の考慮事項の把握を含む)。
現在、多くの Web 開発者は JavaScript のネイティブ モジュール構文を使用していますが、それを裸のインポート指定子と組み合わせているため、アプリケーションごとに事前に変更を加えなければコードを Web 上で実行できなくなります。私たちはそれを解決し、これらの利点を Web にもたらしたいと考えています。
一連の例を使用してインポート マップの機能を説明します。
はじめにでも述べたように、
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
JavaScript コードでベアインポート指定子のサポートを提供します。
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
URL を識別するには、マッピングの右側 (「アドレス」と呼ばれる) が/
、 ../
、または./
で始まるか、絶対 URL として解析可能である必要があることに注意してください。相対 URL のようなアドレスの場合、それらはインポート マップのベース URL、つまりインライン インポート マップのページのベース URL、および外部インポート マップのインポート マップ リソースの URL に対して相対的に解決されます。
特に、 node_modules/moment/src/moment.js
のような「裸の」相対 URL は、現時点ではこれらの位置では機能しません。これは、将来的には複数のインポート マップを許可する可能性があるため、保守的なデフォルトとして行われます。これにより、特にこれらのベア ケースに影響を与える形で右側の意味が変更される可能性があります。
JavaScript エコシステムでは、パッケージ (npm の意味) に複数のモジュールやその他のファイルが含まれることが一般的です。このような場合、モジュール指定子空間のプレフィックスを、フェッチ可能な URL 空間の別のプレフィックスにマップする必要があります。
インポート マップは、末尾のスラッシュで終わる指定子キーに特別な意味を与えることによってこれを行います。したがって、次のような地図
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"moment/" : " /node_modules/moment/src/ " ,
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ "
}
}
次のようなメインモジュールをインポートするだけでなく、
import moment from "moment" ;
import _ from "lodash" ;
メイン以外のモジュールも含まれます。
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
Node.js エコシステムでは、拡張子を含めずにファイルをインポートすることも一般的です。適切な拡張子が見つかるまで、複数のファイル拡張子を試す余裕はありません。ただし、インポート マップを使用すると、同様のものをエミュレートできます。例えば、
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
これによりimport fp from "lodash/fp.js"
できるだけでなく、 import fp from "lodash/fp"
ことも可能になります。
この例は、インポート マップを使用して拡張子なしのインポートを許可する方法を示していますが、これは必ずしも望ましいことではありません。これを行うとインポート マップが肥大化し、人間にとってもツールにとってもパッケージのインターフェイスが単純でなくなります。
この肥大化は、パッケージ内で拡張子なしのインポートを許可する必要がある場合に特に問題になります。その場合、トップレベルのエントリ ポイントだけでなく、パッケージ内のすべてのファイルに対してインポート マップ エントリが必要になります。たとえば、 /node_modules/lodash-es/lodash.js
-es/lodash.js ファイル内からimport "./fp"
できるようにするには、 /node_modules/lodash-es/fp
を/node_modules/lodash-es/fp.js
。次に、拡張子なしで参照されるすべてのファイルに対してこれを繰り返すことを想像してください。
そのため、インポート マップでこのようなパターンを採用したり、モジュールを作成したりする場合は注意することをお勧めします。ファイル拡張子に関連する不一致を修正するためにインポート マップに依存しない方が、エコシステムにとってはより簡単になります。
指定子の一般的な再マッピングを許可する一環として、インポート マップでは特に、 "https://example.com/foo.mjs"
や"./bar.mjs"
などの URL のような指定子の再マッピングを許可します。これの実際的な用途はハッシュをマッピングすることですが、ここでは概念を伝えるためにいくつかの基本的なものを示します。
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
この再マッピングにより、unpkg.com バージョンの Vue (少なくともその URL) のインポートでは、代わりにローカル サーバーからのインポートが確実に行われます。
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
この再マッピングにより、 /app/
内のファイルからのimport "./helpers.mjs"
やファイルからのimport "../helpers.mjs"
など、 /app/helpers.mjs
に解決される URL のようなインポートが確実に行われます。 /app/models
内では、代わりに/app/helpers/index.mjs
に解決されます。これはおそらく良い考えではありません。コードを難読化する間接的なコードを作成するのではなく、ソース ファイルを更新して正しいファイルをインポートする必要があります。ただし、これはインポート マップの機能を示すのに役立つ例です。
このような再マッピングは、指定子キーの末尾にスラッシュを付けることで、接頭辞一致ベースで行うこともできます。
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
このバージョンでは、部分文字列"https://www.unpkg.com/vue/"
で始まる指定子のインポート ステートメントが、 /node_modules/vue/
下の対応する URL にマッピングされるようになります。
一般に、重要なのは、URL に似たインポートでも、再マッピングがベア インポートの場合と同じように機能するということです。前の例では、 "lodash"
のような指定子の解決を変更し、 import "lodash"
の意味を変更しました。ここでは、 "/app/helpers.mjs"
のような指定子の解像度を変更し、 import "/app/helpers.mjs"
の意味を変更しています。
URL のような指定子マッピングのこの末尾のスラッシュの変形は、URL のような指定子に特別なスキームがある場合にのみ機能することに注意してください。たとえば、 "data:text/": "/foo"
のマッピングはimport "data:text/javascript,console.log('test')"
ですが、代わりにimport "data:text/"
のみに影響します。
スクリプト ファイルには、キャッシュ可能性を向上させるために、ファイル名に一意のハッシュが付けられることがよくあります。この手法に関する一般的な説明、または JavaScript と Webpack に焦点を当てたこの説明を参照してください。
モジュール グラフの場合、この手法には問題が生じる可能性があります。
app.mjs
がdep.mjs
に依存し、 dep.mjs がsub-dep.mjs
に依存する、単純なモジュール グラフを考えてみましょう。通常、 sub-dep.mjs
アップグレードまたは変更した場合、 app.mjs
およびdep.mjs
キャッシュされたままであり、必要なのは新しいsub-dep.mjs
ネットワーク経由で転送することだけです。
ここで、実稼働用にハッシュされたファイル名を使用する、同じモジュール グラフについて考えてみましょう。ここには、元の 3 つのファイルからapp-8e0d62a03.mjs
、 dep-16f9d819a.mjs
、およびsub-dep-7be2aa47f.mjs
を生成するビルド プロセスがあります。
sub-dep.mjs
アップグレードまたは変更すると、ビルド プロセスによって製品バージョン用の新しいファイル名 (たとえばsub-dep-5f47101dc.mjs
が再生成されます。ただし、これはdep.mjs
の実稼働バージョンでimport
ステートメントを変更する必要があることを意味します。これにより内容が変更されます。つまり、 dep.mjs
の製品バージョン自体に新しいファイル名が必要になります。ただし、これは、 app.mjs
の実稼働バージョンでimport
ステートメントを更新する必要があることを意味します...
つまり、ハッシュされたファイル名のスクリプト ファイルを含むモジュール グラフとimport
ステートメントを使用すると、グラフの任意の部分の更新がそのすべての依存関係に広まり、キャッシュ可能性の利点がすべて失われます。
インポート マップは、 import
ステートメントに表示されるモジュール指定子をサーバー上の URL から切り離すことで、このジレンマから抜け出す方法を提供します。たとえば、私たちのサイトは次のようなインポート マップから始めることができます。
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
また、 import "./sub-dep.mjs"
の代わりにimport "./sub-dep-7be2aa47f.mjs"
という形式のインポート ステートメントを使用します。ここで、 sub-dep.mjs
変更する場合は、インポート マップを更新するだけです。
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
import "./sub-dep.mjs"
ステートメントはそのままにしておきます。これは、 dep.mjs
の内容が変更されないため、キャッシュされたままになることを意味します。 app.mjs
についても同様です。
<script>
では再マッピングが機能しませんインポート マップを使用してインポート指定子の意味を変更する場合の重要な注意点は、 <script src="">
や<link rel="modulepreload">
に表示されるものなど、生の URL の意味は変更されないことです。つまり、上記の例を考えると、
import "./app.mjs" ;
インポートマップをサポートするブラウザでは、ハッシュされたバージョンに正しく再マッピングされます。
< script type =" module " src =" ./app.mjs " > </ script >
ブラウザのすべてのクラスで、 app.mjs
直接取得しようとするため、404 が発生します。インポートマップをサポートするブラウザでは、次のように動作します。
< script type =" module " > import "./app.mjs" ; </ script >
誰がインポートするかに応じて、同じインポート指定子を使用して 1 つのライブラリの複数のバージョンを参照したいことがよくあります。これにより、使用中の各依存関係のバージョンがカプセル化され、依存関係地獄 (長いブログ投稿) が回避されます。
指定されたスコープ内で指定子の意味を変更できるようにすることで、インポート マップでこの使用例をサポートします。
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(この例は、@zkat によって提供された、アプリケーションごとに複数のバージョンが存在するいくつかの実際の例のうちの 1 つです。ありがとう、@zkat!)
このマッピングでは、URL が/node_modules/socksjs-client/
で始まるモジュール内で、 "querystringify"
指定子は/node_modules/socksjs-client/querystringify/index.js
を参照します。それ以外の場合、トップレベルのマッピングにより、 "querystringify"
が/node_modules/querystringify/index.js
を参照することが保証されます。
スコープ内であってもアドレスの解決方法は変わらないことに注意してください。スコープ URL プレフィックスなどの代わりに、インポート マップのベース URL が引き続き使用されます。
スコープは、意図的に単純な方法で相互に「継承」し、マージしますが、途中でオーバーライドします。たとえば、次のインポート マップは次のとおりです。
{
"imports" : {
"a" : " /a-1.mjs " ,
"b" : " /b-1.mjs " ,
"c" : " /c-1.mjs "
},
"scopes" : {
"/scope2/" : {
"a" : " /a-2.mjs "
},
"/scope2/scope3/" : {
"b" : " /b-3.mjs "
}
}
}
次のような解決策が得られます。
指定子 | 参照元 | 結果の URL |
---|---|---|
ある | /scope1/foo.mjs | /a-1.mjs |
b | /scope1/foo.mjs | /b-1.mjs |
c | /scope1/foo.mjs | /c-1.mjs |
ある | /scope2/foo.mjs | /a-2.mjs |
b | /scope2/foo.mjs | /b-1.mjs |
c | /scope2/foo.mjs | /c-1.mjs |
ある | /スコープ2/スコープ3/foo.mjs | /a-2.mjs |
b | /スコープ2/スコープ3/foo.mjs | /b-3.mjs |
c | /スコープ2/スコープ3/foo.mjs | /c-1.mjs |
<script>
要素をインラインで使用するか、 src=""
属性を使用して、アプリケーションのインポート マップをインストールできます。
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
src=""
属性が使用される場合、結果の HTTP 応答の MIME タイプはapplication/importmap+json
である必要があります。 ( application/json
再利用しないのはなぜですか? そうすると、CSP バイパスが有効になる可能性があります。) モジュール スクリプトと同様に、リクエストは CORS を有効にして行われ、応答は常に UTF-8 として解釈されます。
これらはすべてのインポートに影響するため、モジュールの解決が行われる前にインポート マップが存在し、正常にフェッチされる必要があります。これは、インポート マップのフェッチ時にモジュール グラフのフェッチがブロックされることを意味します。
これは、最高のパフォーマンスを得るには、インポート マップのインライン形式を強く推奨することを意味します。これは、重要な CSS をインライン化するベスト プラクティスに似ています。どちらのタイプのリソースも、アプリケーションが処理されるまで重要な作業を実行できないようにするため、2 回目のネットワーク ラウンドトリップ (またはディスク キャッシュ ラウンド トリップ) を導入することはお勧めできません。外部インポート マップを使用したい場合は、HTTP/2 プッシュやバンドルされた HTTP 交換などのテクノロジを使用して、このラウンドトリップ ペナルティの軽減を試みることができます。
インポート マップがすべてのインポートに与える影響のもう 1 つの結果として、モジュール グラフのフェッチが開始された後に新しい<script type="importmap">
を追加しようとするとエラーが発生します。インポート マップは無視され、 <script>
要素によってerror
イベントが発生します。
現時点では、ページ上で許可される<script type="importmap">
は 1 つだけです。複数のインポート マップを組み合わせるための正しいセマンティクスが判明したら、将来これを拡張する予定です。 #14、#137、および #167 の説明を参照してください。
ワーカーでは何をするのでしょうか?おそらくnew Worker(someURL, { type: "module", importMap: ... })
ですか?それともワーカー内部から設定する必要がありますか?専任のワーカーは、デフォルトで、または常に、制御ドキュメントのマップを使用する必要がありますか? #2で議論してください。
上記のルールは、インポートを実行する前に限り、インポート マップを動的に生成できることを意味します。例えば:
< script >
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( {
imports : {
'my-library' : Math . random ( ) > 0.5 ? '/my-awesome-library.mjs' : '/my-rad-library.mjs'
}
} ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import 'my-library' ; // will fetch the randomly-chosen URL
</ script >
より現実的な例では、この機能を使用して、特徴検出に基づいてインポート マップを組み立てることができます。
< script >
const importMap = {
imports : {
moment : '/moment.mjs' ,
lodash : someFeatureDetection ( ) ?
'/lodash.mjs' :
'/lodash-legacy-browsers.mjs'
}
} ;
const im = document . createElement ( 'script' ) ;
im . type = 'importmap' ;
im . textContent = JSON . stringify ( importMap ) ;
document . currentScript . after ( im ) ;
</ script >
< script type =" module " >
import _ from "lodash" ; // will fetch the right URL for this browser
</ script >
(他の<script>
要素と同様に) <script type="importmap">
がドキュメントに挿入された後でその内容を変更しても機能しないことに注意してください。このため、 <script type="importmap">
を作成して挿入する前にインポート マップの内容を組み立てて上記の例を作成しました。
インポート マップはアプリケーション レベルのもので、Service Worker に似ています。 (より正式には、マップはモジュールごと、したがってレルムごとになります。) これらは構成することを目的としたものではなく、Web アプリケーションの全体的なビューを持って人間またはツールによって作成されます。たとえば、ライブラリにインポート マップを含めることは意味がありません。ライブラリは単に指定子によってモジュールを参照し、それらの指定子がどの URL にマップされるかをアプリケーションに決定させることができます。
これは、一般的な単純さに加えて、 <script type="importmap">
に対する上記の制限の動機の一部となっています。
アプリケーションのインポート マップはモジュール マップ内のすべてのモジュールの解決アルゴリズムを変更するため、モジュールのソース テキストがもともとクロスオリジン URL からのものであるかどうかには影響を受けません。ベア インポート指定子を使用する CDN からモジュールを読み込む場合は、そのモジュールがアプリに追加するベア インポート指定子を事前に把握し、アプリケーションのインポート マップに含める必要があります。 (つまり、アプリケーションのすべての推移的な依存関係が何であるかを知る必要があります。) 各パッケージに使用される URL の制御をアプリケーション作成者が行い、モジュールのバージョン管理と共有を総合的に管理できるようにすることが重要です。
ほとんどのブラウザには、投機的な HTML パーサーが搭載されており、HTML パーサーがブロック スクリプトのフェッチと実行を待機している間に、HTML マークアップで宣言されたリソースの検出を試みます。これはまだ指定されていませんが、whatwg/html#5959 でそのための取り組みが進行中です。このセクションでは、注意すべき潜在的な相互作用のいくつかについて説明します。
まず、現時点でこれを行うブラウザは私たちの知る限りではありませんが、次の例では、投機的パーサーがブロック スクリプトhttps://example.com/blocking-1.js
を待機している間にhttps://example.com/foo.mjs
をフェッチすることが可能であることに注意してください。 https://example.com/blocking-1.js
:
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-1.js " > </ script >
< script type =" module " >
import "./foo.mjs" ;
</ script >
同様に、次の例では、ブラウザーは投機的解析プロセスの一部としてインポート マップを解析することにより、 https://example.com/foo.mjs
とhttps://example.com/bar.mjs
投機的にフェッチできます。
<!DOCTYPE html >
<!-- This file is https://example.com/ -->
< script src =" blocking-2.js " > </ script >
< script type =" importmap " >
{
"imports" : {
"foo" : "./foo.mjs" ,
"https://other.example/bar.mjs" : "./bar.mjs"
}
}
</ script >
< script type =" module " >
import "foo" ;
import "https://other.example/bar.mjs" ;
</ script >
ここで注目すべき対話の 1 つは、インライン JS モジュールを投機的に解析するがインポート マップをサポートしていないブラウザは、おそらくこの例では間違った推測を行う可能性があることです。つまり、 https://other.example/bar.mjs
は、 https://example.com/bar.mjs
にマッピングされます。
より一般的には、インポート マップに基づく推測は、他の推測と同じ種類の間違いを犯す可能性があります。たとえば、 blocking-1.js
の内容が次の場合、
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
その場合、インポートなしマップの例でのhttps://example.com/foo.mjs
の投機的フェッチは無駄になります。モジュールの実際の評価を実行する時までに、相対指定子を再計算することになるからです。 "./foo.mjs"
と入力すると、実際にリクエストされているのはhttps://example.com/subdirectory/foo.mjs
であることがわかります。
インポート マップの場合も同様に、 blocking-2.js
の内容が
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
その場合、 https://example.com/foo.mjs
とhttps://example.com/bar.mjs
の投機的なフェッチは、表示されたマップの代わりに新しく作成されたインポート マップが有効になるため、無駄になります。 HTML内のインライン。
<base>
要素<base>
要素がドキュメント内に存在する場合、インポート マップ内のすべての URL および URL に似た指定子は、 <base>
のhref
を使用して絶対 URL に変換されます。
< base href =" https://www.unpkg.com/vue/dist/ " >
< script type =" importmap " >
{
"imports" : {
"vue" : "./vue.runtime.esm.js" ,
}
}
</ script >
< script >
import ( "vue" ) ; // resolves to https://www.unpkg.com/vue/dist/vue.runtime.esm.js
</ script >
ブラウザが HTMLScriptElement の support(type) メソッドをサポートしている場合、 HTMLScriptElement.supports('importmap')
true を返す必要があります。
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
Node.js とは異なり、ブラウザーでは、モジュールを探してクロールできるほど高速なファイル システムという贅沢はありません。したがって、ノード モジュール解決アルゴリズムを直接実装することはできません。 import
ステートメントごとに複数のサーバーラウンドトリップを実行する必要があり、404 を取得し続けるため帯域幅と時間が無駄になります。すべてのimport
ステートメントで HTTP リクエストが 1 つだけ発生するようにする必要があります。これには、ある程度の事前計算が必要になります。
各モジュール指定子を解釈するために JavaScript フックを使用してブラウザのモジュール解決アルゴリズムをカスタマイズすることを提案する人もいます。
残念ながら、これはパフォーマンスにとって致命的です。モジュール グラフの端ごとに JavaScript に出入りすると、アプリケーションの起動が大幅に遅くなります。 (一般的な Web アプリケーションには数千のオーダーのモジュールがあり、その 3 ~ 4 倍の import ステートメントが含まれます。) 呼び出しをベアの import 指定子のみに制限したり、フックが指定子のバッチを取得することを要求したりするなど、さまざまな緩和策を想像することができます。 URL のバッチを返しますが、最終的には事前計算に勝るものはありません。
これに関するもう 1 つの問題は、たとえこのフックが与えられたとしても、Web 開発者が作成できる有用なマッピング アルゴリズムを想像するのが難しいことです。 Node.js にはこれがありますが、ファイル システムを繰り返しクロールしてファイルが存在するかどうかを確認することに基づいています。上で説明したように、それはウェブ上では不可能です。一般的なアルゴリズムが実行可能な唯一の状況は、(a) サブグラフごとのカスタマイズがまったく必要ない場合、つまり、アプリケーション内にすべてのモジュールのバージョンが 1 つだけ存在する場合です。 (b) ツールは、たとえばアルゴリズムが "return /js/${specifier}.js
" になるように、何らかの均一で予測可能な方法でモジュールを事前に調整することに成功しました。しかし、とにかく私たちがこの世界にいるのであれば、宣言的な解決策の方が簡単でしょう。
現在使用されている解決策の 1 つは (babel-plugin-unpkg を介した unpkg CDN など)、ビルド ツールを使用して、事前にすべてのベア インポート指定子を適切な絶対 URL に書き換えることです。これはインストール時に行うこともできるため、npm を使用してパッケージをインストールすると、裸のインポート指定子の代わりに絶対 URL または相対 URL を使用するようにパッケージのコンテンツが自動的に書き換えられます。
このアプローチの問題は、関数に渡された文字列を静的に分析することが不可能であるため、動的import()
では機能しないことです。たとえば、 import(x)
のすべてのインスタンスをimport(specifierToURL(x, import.meta.url))
に変更する修正を注入できます。 ここで、 specifierToURL
はビルド ツールによって生成される別の関数です。しかし、結局のところ、これはかなり漏洩の多い抽象化であり、 specifierToURL
関数はいずれにしてもこの提案の作業をほぼ複製しています。
一見すると、Service Worker はこの種のリソースの変換を行うのに適した場所のように見えます。以前、Service Worker の fetch イベントとともに指定子を渡し、適切なResponse
返せるようにする方法を見つけることについて話しました。
ただし、 Service Worker は最初のロードでは使用できません。したがって、実際には、モジュールをロードするために使用される重要なインフラストラクチャの一部になることはできません。これらは、通常は機能するフェッチに加えて、プログレッシブ拡張としてのみ使用できます。
スコープ依存関係の解決を必要としない単純なアプリケーションがあり、(現在のバージョンの npm とは異なり) パッケージ内のディスク上のパスを快適に書き換えられるパッケージ インストール ツールがある場合は、はるかに単純なマッピングで済む可能性があります。たとえば、インストール ツールがフォームのフラット リストを作成した場合、
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
必要な情報は次のとおりです
/node_modules_flattened/
)index.js
)これらのものだけ、または一部のサブセットのみを指定するモジュール インポート構成形式を想像することもできます (他のものについての仮定を組み込んだ場合)。
このアイデアは、範囲指定された解決を必要とするより複雑なアプリケーションには機能しないため、完全なインポート マップの提案が必要であると考えられます。しかし、単純なアプリケーションにとっては依然として魅力的であり、すべてのモジュールをリストする必要がなく、代わりに最小限のマッピングが必要であることを保証する規則とツールに依存するイージーモードを提案に組み込む方法はないだろうかと考えています。 #7 で議論します。
各モジュールにメタデータを提供したいという意見が何度か出てきました。たとえば、整合性メタデータ、フェッチ オプションなどです。これを import ステートメントで行うことを提案する人もいますが、オプションを慎重に検討することにより、アウトオブバンド マニフェスト ファイルを使用することを推奨します。
インポート マップはそのマニフェスト ファイルである可能性があります。ただし、次のような理由により、これが最適ではない可能性があります。
現在想定されているように、アプリケーション内のほとんどのモジュールはインポート マップにエントリを持ちません。主な使用例は、裸の指定子によって参照する必要があるモジュール、またはポリフィルや仮想化などの難しいことを行う必要があるモジュールです。すべてのモジュールがマップ内にあることを想定している場合、packages-via-trailing-slashes のような便利な機能は含めないでしょう。
これまでに提案されたすべてのメタデータは、JavaScript モジュールだけでなく、あらゆる種類のリソースに適用できます。おそらく、ソリューションはより一般的なレベルで機能するはずです。
他のタイプの複数の<script>
と同様に、ページ上に複数の<script type="importmap">
が表示されるのは自然なことです。将来的にはこれを有効にしたいと考えています。
ここでの最大の課題は、複数のインポート マップをどのように構成するかを決定することです。つまり、両方とも同じ URL を再マップする 2 つのインポート マップ、または同じ URL プレフィックス スペースをカバーする 2 つのスコープ定義がある場合、ページへの影響はどうなるでしょうか?現在の最有力候補はカスケード解決です。これは、インポート マップをインポート指定子 → URL マッピングから、代わりに一連のインポート指定子 → インポート指定子マッピングのカスケードに再キャストし、最終的には「フェッチ可能なインポート指定子」 (本質的には URL) で底を打ちます。
さらに詳しい議論については、これらの未解決の問題を参照してください。
一部のユースケースでは、宣言的な<script type="importmap">
要素を挿入するのではなく、スクリプトからレルムのインポート マップを読み取りまたは操作する方法が必要です。これは、ページの通常は宣言的な CSS ルールを操作できるようにする CSS オブジェクト モデルに似た「インポート マップ オブジェクト モデル」であると考えてください。
ここでの課題は、宣言的なインポート マップとプログラムによる変更をどのように調整するか、またページのライフサイクルのどの時点でそのような API が動作できるかという点にあります。一般に、単純な設計は性能が低く、対応するユースケースが少なくなる可能性があります。
プログラム API が役立つ詳細な議論とユースケースについては、これらの未解決の問題を参照してください。
import.meta.resolve()
提案されているimport.meta.resolve(specifier)
関数を使用すると、モジュール スクリプトでいつでもインポート指定子を URL に解決できます。詳細については、whatwg/html#5572 を参照してください。これは、「パッケージ相対」リソースを解決できるため、インポート マップに関連しています。
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
これにより、ページのインポート マップによって制御されるsomepackage/
名前空間内のresource.json
の適切にマップされた場所が得られます。
コミュニティの何人かのメンバーが、マップのインポートに関連するポリフィルとツールの開発に取り組んできました。私たちが知っているものは次のとおりです。
package.json
およびnode_modules/
ディレクトリからインポート マップを生成します。package.json
からインポート マップを生成します。<script type="systemjs-importmap">
を使用して古いブラウザでインポート マップを使用するためのポリフィルのようなワークフローを提供します。その他のプル リクエストもお気軽に送信してください。また、このスペースに関するディスカッションには、問題トラッカーの #146 を使用できます。
このドキュメントは、@domenic、@hiroshige-g、@justinfagnani、@MylesBorins、@nyaxt が関与した 1 日にわたるスプリントから生まれました。それ以来、@guybedford はこの提案のプロトタイピングと議論の推進に尽力してきました。
提案の発展に協力してくれた問題トラッカーのすべての貢献者にも感謝します。