Ou como controlar o comportamento das importações de JavaScript
<script>
<base>
import.meta.resolve()
Esta proposta permite controlar quais URLs são obtidos por instruções import
JavaScript e expressões import()
. Isso permite que "especificadores de importação simples", como import moment from "moment"
, funcionem.
O mecanismo para fazer isso é através de um mapa de importação que pode ser usado para controlar a resolução dos especificadores de módulo em geral. Como exemplo introdutório, considere o código
import moment from "moment" ;
import { partition } from "lodash" ;
Hoje, isso acontece, já que esses especificadores básicos são explicitamente reservados. Fornecendo ao navegador o seguinte mapa de importação
< script type =" importmap " >
{
"imports" : {
"moment" : "/node_modules/moment/src/moment.js" ,
"lodash" : "/node_modules/lodash-es/lodash.js"
}
}
</ script >
o acima funcionaria como se você tivesse escrito
import moment from "/node_modules/moment/src/moment.js" ;
import { partition } from "/node_modules/lodash-es/lodash.js" ;
Para obter mais informações sobre o novo valor "importmap"
para o atributo type=""
de <script>
, consulte a seção de instalação. Por enquanto, nos concentraremos na semântica do mapeamento, adiando a discussão sobre instalação.
Desenvolvedores Web com experiência com sistemas de módulos anteriores ao ES2015, como CommonJS (em Node ou empacotado usando webpack/browserify para o navegador), estão acostumados a poder importar módulos usando uma sintaxe simples:
const $ = require ( "jquery" ) ;
const { pluck } = require ( "lodash" ) ;
Traduzido para a linguagem do sistema de módulos integrado do JavaScript, estes seriam
import $ from "jquery" ;
import { pluck } from "lodash" ;
Em tais sistemas, esses especificadores de importação simples de "jquery"
ou "lodash"
são mapeados para nomes de arquivos completos ou URLs. Mais detalhadamente, esses especificadores representam pacotes , geralmente distribuídos em npm; ao especificar apenas o nome do pacote, eles estão solicitando implicitamente o módulo principal desse pacote.
O principal benefício deste sistema é que ele permite uma fácil coordenação em todo o ecossistema. Qualquer um pode escrever um módulo e incluir uma instrução de importação usando o nome conhecido de um pacote e deixar o tempo de execução do Node.js ou suas ferramentas de tempo de construção cuidarem de traduzi-lo em um arquivo real no disco (incluindo descobrir considerações sobre controle de versão).
Hoje, muitos desenvolvedores web estão até usando a sintaxe de módulo nativo do JavaScript, mas combinando-a com especificadores de importação simples, tornando assim seu código incapaz de ser executado na web sem modificação antecipada por aplicativo. Gostaríamos de resolver isso e trazer esses benefícios para a web.
Explicamos as características do mapa de importação através de uma série de exemplos.
Como mencionado na introdução,
{
"imports" : {
"moment" : " /node_modules/moment/src/moment.js " ,
"lodash" : " /node_modules/lodash-es/lodash.js "
}
}
fornece suporte básico ao especificador de importação no código JavaScript:
import moment from "moment" ;
import ( "lodash" ) . then ( _ => ... ) ;
Observe que o lado direito do mapeamento (conhecido como "endereço") deve começar com /
, ../
ou ./
, ou ser analisável como um URL absoluto, para identificar um URL. No caso de endereços semelhantes a URLs relativos, eles são resolvidos em relação ao URL base do mapa de importação, ou seja, o URL base da página para mapas de importação in-line e o URL do recurso do mapa de importação para mapas de importação externos.
Em particular, URLs relativos "vazios" como node_modules/moment/src/moment.js
não funcionarão nessas posições, por enquanto. Isto é feito como um padrão conservador, já que no futuro poderemos querer permitir múltiplos mapas de importação, o que poderá alterar o significado do lado direito de maneiras que afetem especialmente esses casos básicos.
É comum no ecossistema JavaScript ter um pacote (no sentido de npm) contendo vários módulos ou outros arquivos. Para tais casos, queremos mapear um prefixo no espaço do especificador de módulo para outro prefixo no espaço de URL buscável.
Os mapas de importação fazem isso dando um significado especial às chaves especificadoras que terminam com uma barra final. Assim, um mapa como
{
"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/ "
}
}
permitiria não apenas importar os módulos principais como
import moment from "moment" ;
import _ from "lodash" ;
mas também módulos não principais, por exemplo
import localeData from "moment/locale/zh-cn.js" ;
import fp from "lodash/fp.js" ;
Também é comum no ecossistema Node.js importar arquivos sem incluir a extensão. Não podemos nos dar ao luxo de tentar várias extensões de arquivo até encontrarmos uma boa correspondência. No entanto, podemos emular algo semelhante usando um mapa de importação. Por exemplo,
{
"imports" : {
"lodash" : " /node_modules/lodash-es/lodash.js " ,
"lodash/" : " /node_modules/lodash-es/ " ,
"lodash/fp" : " /node_modules/lodash-es/fp.js " ,
}
}
permitiria não apenas import fp from "lodash/fp.js"
, mas também permitir import fp from "lodash/fp"
.
Embora este exemplo mostre como é possível permitir importações sem extensão com mapas de importação, isso não é necessariamente desejável . Fazer isso incha o mapa de importação e torna a interface do pacote menos simples – tanto para humanos quanto para ferramentas.
Esse inchaço é especialmente problemático se você precisar permitir importações sem extensão dentro de um pacote. Nesse caso, você precisará de uma entrada de mapa de importação para cada arquivo do pacote, não apenas para os pontos de entrada de nível superior. Por exemplo, para permitir import "./fp"
de dentro do arquivo /node_modules/lodash-es/lodash.js
, você precisaria de um mapeamento de entrada de importação /node_modules/lodash-es/fp
para /node_modules/lodash-es/fp.js
. Agora imagine repetir isso para cada arquivo referenciado sem extensão.
Como tal, recomendamos cautela ao empregar padrões como este em seus mapas de importação ou escrever módulos. Será mais simples para o ecossistema se não dependermos de mapas de importação para corrigir incompatibilidades relacionadas a extensões de arquivos.
Como parte da permissão do remapeamento geral de especificadores, os mapas de importação permitem especificamente o remapeamento de especificadores semelhantes a URL, como "https://example.com/foo.mjs"
ou "./bar.mjs"
. Um uso prático para isso é mapear hashes, mas aqui demonstramos alguns básicos para comunicar o conceito:
{
"imports" : {
"https://www.unpkg.com/vue/dist/vue.runtime.esm.js" : " /node_modules/vue/dist/vue.runtime.esm.js "
}
}
Este remapeamento garante que qualquer importação da versão unpkg.com do Vue (pelo menos nessa URL) pegue aquela do servidor local.
{
"imports" : {
"/app/helpers.mjs" : " /app/helpers/index.mjs "
}
}
Este remapeamento garante que quaisquer importações semelhantes a URL que sejam resolvidas para /app/helpers.mjs
, incluindo, por exemplo, uma import "./helpers.mjs"
de arquivos dentro de /app/
ou uma import "../helpers.mjs"
de arquivos dentro de /app/models
, será resolvido para /app/helpers/index.mjs
. Provavelmente não é uma boa ideia; em vez de criar uma indireção que ofusque seu código, você deve apenas atualizar seus arquivos de origem para importar os arquivos corretos. Porém, é um exemplo útil para demonstrar as capacidades dos mapas de importação.
Esse remapeamento também pode ser feito com base na correspondência de prefixo, finalizando a chave do especificador com uma barra final:
{
"imports" : {
"https://www.unpkg.com/vue/" : " /node_modules/vue/ "
}
}
Esta versão garante que as instruções de importação para especificadores que começam com a substring "https://www.unpkg.com/vue/"
serão mapeadas para o URL correspondente abaixo de /node_modules/vue/
.
Em geral, a questão é que o remapeamento funciona da mesma forma para importações semelhantes a URL e para importações simples. Nossos exemplos anteriores alteraram a resolução de especificadores como "lodash"
e, portanto, alteraram o significado de import "lodash"
. Aqui estamos alterando a resolução de especificadores como "/app/helpers.mjs"
e, assim, alterando o significado de import "/app/helpers.mjs"
.
Observe que esta variante de barra final do mapeamento do especificador semelhante a URL só funciona se o especificador semelhante a URL tiver um esquema especial: por exemplo, um mapeamento de "data:text/": "/foo"
não afetará o significado de import "data:text/javascript,console.log('test')"
, mas afetará apenas import "data:text/"
.
Os arquivos de script geralmente recebem um hash exclusivo em seu nome de arquivo, para melhorar a capacidade de armazenamento em cache. Veja esta discussão geral da técnica ou esta discussão mais focada em JavaScript e webpack.
Com gráficos de módulo, esta técnica pode ser problemática:
Considere um gráfico de módulo simples, com app.mjs
dependendo dep.mjs
que depende de sub-dep.mjs
. Normalmente, se você atualizar ou alterar sub-dep.mjs
, app.mjs
e dep.mjs
poderão permanecer em cache, exigindo apenas a transferência do novo sub-dep.mjs
pela rede.
Agora considere o mesmo gráfico de módulo, usando nomes de arquivos com hash para produção. Lá temos nosso processo de construção gerando app-8e0d62a03.mjs
, dep-16f9d819a.mjs
e sub-dep-7be2aa47f.mjs
a partir dos três arquivos originais.
Se atualizarmos ou alterarmos sub-dep.mjs
, nosso processo de construção irá gerar novamente um novo nome de arquivo para a versão de produção, digamos sub-dep-5f47101dc.mjs
. Mas isso significa que precisamos alterar a instrução import
na versão de produção de dep.mjs
. Isso altera seu conteúdo, o que significa que a própria versão de produção do dep.mjs
precisa de um novo nome de arquivo. Mas isso significa que precisamos atualizar a instrução import
na versão de produção do app.mjs
...
Ou seja, com gráficos de módulo e instruções import
contendo arquivos de script com nome de arquivo com hash, as atualizações em qualquer parte do gráfico tornam-se virais para todas as suas dependências, perdendo todos os benefícios de capacidade de armazenamento em cache.
Os mapas de importação fornecem uma saída para esse dilema, desacoplando os especificadores de módulo que aparecem nas instruções import
das URLs no servidor. Por exemplo, nosso site poderia começar com um mapa de importação como
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-7be2aa47f.mjs "
}
}
e com instruções de importação no formato import "./sub-dep.mjs"
em vez de import "./sub-dep-7be2aa47f.mjs"
. Agora, se alterarmos sub-dep.mjs
, simplesmente atualizaremos nosso mapa de importação:
{
"imports" : {
"/js/app.mjs" : " /js/app-8e0d62a03.mjs " ,
"/js/dep.mjs" : " /js/dep-16f9d819a.mjs " ,
"/js/sub-dep.mjs" : " /js/sub-dep-5f47101dc.mjs "
}
}
e deixe a instrução import "./sub-dep.mjs"
em paz. Isso significa que o conteúdo de dep.mjs
não muda e, portanto, permanece em cache; o mesmo para app.mjs
.
<script>
Uma observação importante sobre o uso de mapas de importação para alterar o significado dos especificadores de importação é que isso não altera o significado de URLs brutos, como aqueles que aparecem em <script src="">
ou <link rel="modulepreload">
. Isto é, dado o exemplo acima, enquanto
import "./app.mjs" ;
estaria remapeando corretamente para sua versão com hash em navegadores que suportam mapas de importação,
< script type =" module " src =" ./app.mjs " > </ script >
não: em todas as classes de navegadores, ele tentaria buscar app.mjs
diretamente, resultando em um 404. O que funcionaria , em navegadores que suportam mapas de importação, seria
< script type =" module " > import "./app.mjs" ; </ script >
Muitas vezes você deseja usar o mesmo especificador de importação para se referir a diversas versões de uma única biblioteca, dependendo de quem as está importando. Isso encapsula as versões de cada dependência em uso e evita o inferno de dependências (postagem mais longa no blog).
Apoiamos esse caso de uso em mapas de importação, permitindo alterar o significado de um especificador dentro de um determinado escopo :
{
"imports" : {
"querystringify" : " /node_modules/querystringify/index.js "
},
"scopes" : {
"/node_modules/socksjs-client/" : {
"querystringify" : " /node_modules/socksjs-client/querystringify/index.js "
}
}
}
(Este é um dos vários exemplos de múltiplas versões por aplicativo fornecidos por @zkat. Obrigado, @zkat!)
Com esse mapeamento, dentro de quaisquer módulos cujos URLs comecem com /node_modules/socksjs-client/
, o especificador "querystringify"
se referirá a /node_modules/socksjs-client/querystringify/index.js
. Caso contrário, o mapeamento de nível superior garantirá que "querystringify"
se refira a /node_modules/querystringify/index.js
.
Observe que estar em um escopo não altera a forma como um endereço é resolvido; o URL base do mapa de importação ainda é usado, em vez de, por exemplo, o prefixo do URL do escopo.
Os escopos "herdam" uns dos outros de maneira intencionalmente simples, fundindo-se, mas substituindo-os à medida que avançam. Por exemplo, o seguinte mapa de importação:
{
"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 "
}
}
}
daria as seguintes resoluções:
Especificador | Referenciador | URL resultante |
---|---|---|
um | /scope1/foo.mjs | /a-1.mjs |
b | /scope1/foo.mjs | /b-1.mjs |
c | /scope1/foo.mjs | /c-1.mjs |
um | /scope2/foo.mjs | /a-2.mjs |
b | /scope2/foo.mjs | /b-1.mjs |
c | /scope2/foo.mjs | /c-1.mjs |
um | /scope2/scope3/foo.mjs | /a-2.mjs |
b | /scope2/scope3/foo.mjs | /b-3.mjs |
c | /scope2/scope3/foo.mjs | /c-1.mjs |
Você pode instalar um mapa de importação para seu aplicativo usando um elemento <script>
, seja inline ou com um atributo src=""
:
< script type =" importmap " >
{
"imports" : { ... } ,
"scopes" : { ... }
}
</ script >
< script type =" importmap " src =" import-map.importmap " > </ script >
Quando o atributo src=""
é usado, a resposta HTTP resultante deve ter o tipo MIME application/importmap+json
. (Por que não reutilizar application/json
? Fazer isso pode ativar desvios de CSP.) Assim como os scripts de módulo, a solicitação é feita com o CORS habilitado e a resposta é sempre interpretada como UTF-8.
Como afetam todas as importações, quaisquer mapas de importação devem estar presentes e obtidos com sucesso antes de qualquer resolução de módulo ser feita. Isso significa que a busca do gráfico do módulo está bloqueada na busca do mapa de importação.
Isto significa que a forma inline de mapas de importação é fortemente recomendada para obter melhor desempenho. Isso é semelhante à prática recomendada de inlining CSS crítico; ambos os tipos de recursos impedem que seu aplicativo execute trabalhos importantes até que sejam processados, portanto, introduzir uma segunda viagem de ida e volta de rede (ou mesmo de ida e volta de cache de disco) é uma má ideia. Se você deseja usar mapas de importação externos, você pode tentar mitigar essa penalidade de ida e volta com tecnologias como HTTP/2 Push ou trocas HTTP agrupadas.
Como outra consequência de como os mapas de importação afetam todas as importações, tentar adicionar um novo <script type="importmap">
após o início de qualquer busca de gráfico de módulo é um erro. O mapa de importação será ignorado e o elemento <script>
disparará um evento error
.
Por enquanto, apenas um <script type="importmap">
é permitido na página. Planejamos estender isso no futuro, assim que descobrirmos a semântica correta para combinar vários mapas de importação. Veja a discussão em #14, #137 e #167.
O que fazemos nos trabalhadores? Provavelmente new Worker(someURL, { type: "module", importMap: ... })
? Ou você deve configurá-lo de dentro do trabalhador? Os trabalhadores dedicados devem usar o mapa do seu documento de controle, por padrão ou sempre? Discuta em #2.
As regras acima significam que você pode gerar mapas de importação dinamicamente, desde que faça isso antes de realizar qualquer importação. Por exemplo:
< 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 >
Um exemplo mais realista poderia usar esse recurso para montar o mapa de importação com base na detecção de feições:
< 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 >
Observe que (como outros elementos <script>
) modificar o conteúdo de um <script type="importmap">
depois de ele já estar inserido no documento não funcionará. É por isso que escrevemos o exemplo acima montando o conteúdo do mapa de importação antes de criar e inserir o <script type="importmap">
.
Os mapas de importação são algo no nível do aplicativo, semelhante aos service workers. (Mais formalmente, eles seriam mapas por módulo e, portanto, por domínio.) Eles não foram feitos para serem compostos, mas produzidos por um ser humano ou ferramenta com uma visão holística de seu aplicativo da web. Por exemplo, não faria sentido que uma biblioteca incluísse um mapa de importação; as bibliotecas podem simplesmente referenciar módulos por especificador e deixar o aplicativo decidir para quais URLs esses especificadores serão mapeados.
Isto, além da simplicidade geral, é em parte o que motiva as restrições acima em <script type="importmap">
.
Como o mapa de importação de um aplicativo altera o algoritmo de resolução para cada módulo no mapa de módulo, eles não são afetados pelo fato de o texto de origem de um módulo ser originalmente de uma URL de origem cruzada. Se você carregar um módulo de uma CDN que usa especificadores de importação simples, precisará saber antecipadamente quais especificadores de importação básicos esse módulo adiciona ao seu aplicativo e incluí-los no mapa de importação do seu aplicativo. (Ou seja, você precisa saber quais são todas as dependências transitivas do seu aplicativo.) É importante que o controle de quais URLs são usadas para cada pacote fique com o autor do aplicativo, para que ele possa gerenciar holisticamente o controle de versão e o compartilhamento de módulos.
A maioria dos navegadores possui um analisador HTML especulativo que tenta descobrir recursos declarados na marcação HTML enquanto o analisador HTML aguarda que os scripts de bloqueio sejam buscados e executados. Isso ainda não está especificado, embora haja esforços contínuos para fazê-lo em whatwg/html#5959. Esta seção discute algumas das possíveis interações que você deve conhecer.
Primeiro, observe que, embora até onde sabemos nenhum navegador faça isso atualmente, seria possível para um analisador especulativo buscar https://example.com/foo.mjs
no exemplo a seguir, enquanto espera pelo script de bloqueio 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 >
Da mesma forma, um navegador poderia buscar especulativamente https://example.com/foo.mjs
e https://example.com/bar.mjs
no exemplo a seguir, analisando o mapa de importação como parte do processo de análise especulativa:
<!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 >
Uma interação a ser observada aqui é que os navegadores que analisam especulativamente módulos JS in-line, mas não suportam mapas de importação, provavelmente especulariam incorretamente para este exemplo: eles poderiam buscar especulativamente https://other.example/bar.mjs
, em vez do https://example.com/bar.mjs
para o qual está mapeado.
De forma mais geral, as especulações baseadas em mapas de importação podem estar sujeitas ao mesmo tipo de erros que outras especulações. Por exemplo, se o conteúdo blocking-1.js
fosse
const el = document . createElement ( "base" ) ;
el . href = "/subdirectory/" ;
document . currentScript . after ( el ) ;
então a busca especulativa de https://example.com/foo.mjs
no exemplo do mapa sem importação seria desperdiçada, pois quando chegasse a hora de realizar a avaliação real do módulo, recalcularíamos o especificador relativo "./foo.mjs"
e perceba que o que realmente é solicitado é https://example.com/subdirectory/foo.mjs
.
Da mesma forma para o caso do mapa de importação, se o conteúdo de blocking-2.js
fosse
document . write ( `<script type="importmap">
{
"imports": {
"foo": "./other-foo.mjs",
"https://other.example/bar.mjs": "./other-bar.mjs"
}
}
</script>` ) ;
então as buscas especulativas de https://example.com/foo.mjs
e https://example.com/bar.mjs
seriam desperdiçadas, pois o mapa de importação recém-escrito estaria em vigor em vez daquele que foi visto embutido no HTML.
<base>
Quando o elemento <base>
está presente no documento, todos os URLs e especificadores semelhantes a URL no mapa de importação são convertidos em URLs absolutos usando o href
de <base>
.
< 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 >
Se o navegador suportar o método support(type) de HTMLScriptElement, HTMLScriptElement.supports('importmap')
deverá retornar true.
if ( HTMLScriptElement . supports && HTMLScriptElement . supports ( 'importmap' ) ) {
console . log ( 'Your browser supports import maps.' ) ;
}
Ao contrário do Node.js, no navegador não temos o luxo de um sistema de arquivos razoavelmente rápido que possamos rastrear em busca de módulos. Assim, não podemos implementar o algoritmo de resolução do módulo Node diretamente; seria necessário realizar várias viagens de ida e volta ao servidor para cada instrução import
, desperdiçando largura de banda e tempo à medida que continuamos a obter erros 404. Precisamos garantir que cada instrução import
cause apenas uma solicitação HTTP; isso requer alguma medida de pré-computação.
Alguns sugeriram personalizar o algoritmo de resolução de módulo do navegador usando um gancho JavaScript para interpretar cada especificador de módulo.
Infelizmente, isso é fatal para o desempenho; entrar e sair do JavaScript para cada borda de um gráfico de módulo retarda drasticamente a inicialização do aplicativo. (Aplicativos web típicos têm da ordem de milhares de módulos, com 3-4× esse número de instruções de importação.) Você pode imaginar várias mitigações, como restringir as chamadas apenas a especificadores de importação simples ou exigir que o gancho receba lotes de especificadores e retorna lotes de URLs, mas no final nada supera a pré-computação.
Outro problema é que é difícil imaginar um algoritmo de mapeamento útil que um desenvolvedor web possa escrever, mesmo que receba esse gancho. O Node.js tem um, mas é baseado no rastreamento repetido do sistema de arquivos e na verificação se os arquivos existem; conforme discutido acima, isso é inviável na web. A única situação em que um algoritmo geral seria viável seria se (a) você nunca precisasse de personalização por subgráfico, ou seja, existisse apenas uma versão de cada módulo em sua aplicação; (b) as ferramentas conseguiram organizar seus módulos antecipadamente de maneira uniforme e previsível, de modo que, por exemplo, o algoritmo se torne "return /js/${specifier}.js
". Mas se estivermos neste mundo de qualquer maneira, uma solução declarativa seria mais simples.
Uma solução em uso hoje (por exemplo, no CDN unpkg via babel-plugin-unpkg) é reescrever todos os especificadores de importação básicos em seus URLs absolutos apropriados com antecedência, usando ferramentas de construção. Isso também pode ser feito no momento da instalação, de modo que quando você instala um pacote usando npm, ele reescreve automaticamente o conteúdo do pacote para usar URLs absolutos ou relativos em vez de especificadores de importação simples.
O problema dessa abordagem é que ela não funciona com import()
dinâmico, pois é impossível analisar estaticamente as strings passadas para essa função. Você pode injetar uma correção que, por exemplo, altere todas as instâncias de import(x)
para import(specifierToURL(x, import.meta.url))
, onde specifierToURL
é outra função gerada pela ferramenta de construção. Mas no final, esta é uma abstração bastante vazada, e a função specifierToURL
duplica em grande parte o trabalho desta proposta de qualquer maneira.
À primeira vista, os prestadores de serviços parecem ser o lugar certo para fazer esse tipo de tradução de recursos. Já falamos sobre como encontrar uma maneira de passar o especificador junto com o evento fetch de um service worker, permitindo assim que ele retorne um Response
apropriado.
No entanto, os service workers não estão disponíveis no primeiro carregamento . Assim, eles não podem realmente fazer parte da infraestrutura crítica usada para carregar módulos. Eles só podem ser usados como um aprimoramento progressivo além de buscas que geralmente funcionariam.
Se você tiver aplicativos simples, sem necessidade de resolução de dependências com escopo definido, e tiver uma ferramenta de instalação de pacotes que seja confortável para reescrever caminhos no disco dentro do pacote (ao contrário das versões atuais do npm), você poderá obter um mapeamento muito mais simples. Por exemplo, se sua ferramenta de instalação criou uma listagem simples no formato
node_modules_flattened/
lodash/
index.js
core.js
fp.js
moment/
index.js
html-to-dom/
index.js
então a única informação que você precisa é
/node_modules_flattened/
)index.js
)Você poderia imaginar um formato de configuração de importação de módulo que especificasse apenas essas coisas, ou mesmo apenas alguns subconjuntos (se fizermos suposições para os outros).
Esta ideia não funciona para aplicações mais complexas que necessitam de resolução com escopo definido, por isso acreditamos que a proposta do mapa de importação completo é necessária. Mas continua atraente para aplicações simples, e nos perguntamos se há uma maneira de fazer com que a proposta também tenha um modo fácil que não exija a listagem de todos os módulos, mas que dependa de convenções e ferramentas para garantir que o mapeamento mínimo seja necessário. Discuta no item 7.
Várias vezes já aconteceu que as pessoas desejam fornecer metadados para cada módulo; por exemplo, metadados de integridade ou opções de busca. Embora alguns tenham proposto fazer isso com uma instrução import, a consideração cuidadosa das opções leva à preferência por um arquivo de manifesto fora de banda.
O mapa de importação pode ser esse arquivo de manifesto. No entanto, pode não ser a melhor opção, por alguns motivos:
Conforme previsto atualmente, a maioria dos módulos de uma aplicação não teria entradas no mapa de importação. O principal caso de uso é para módulos aos quais você precisa se referir por meio de especificadores básicos ou módulos onde você precisa fazer algo complicado como polyfilling ou virtualização. Se imaginássemos cada módulo no mapa, não incluiríamos recursos de conveniência, como pacotes por meio de barras finais.
Todos os metadados propostos até agora são aplicáveis a qualquer tipo de recurso, não apenas a módulos JavaScript. Uma solução provavelmente deveria funcionar num nível mais geral.
É natural que vários <script type="importmap">
s apareçam em uma página, assim como vários <script>
s de outros tipos podem. Gostaríamos de permitir isso no futuro.
O maior desafio aqui é decidir como os múltiplos mapas de importação são compostos. Ou seja, dados dois mapas de importação que remapeiam o mesmo URL ou duas definições de escopo que cobrem o mesmo espaço de prefixo de URL, qual deve ser o efeito na página? O principal candidato atual é a resolução em cascata, que reformula os mapas de importação de especificador de importação → mapeamentos de URL, para ser uma série em cascata de especificador de importação → mapeamentos de especificador de importação, eventualmente chegando ao fundo de um "especificador de importação buscável" (essencialmente um URL).
Veja essas questões em aberto para mais discussão.
Alguns casos de uso desejam uma maneira de ler ou manipular o mapa de importação de um domínio a partir do script, em vez de inserir elementos <script type="importmap">
declarativos. Considere-o um "modelo de objeto de mapa de importação", semelhante ao modelo de objeto CSS que permite manipular as regras CSS geralmente declarativas da página.
Os desafios aqui giram em torno de como reconciliar os mapas de importação declarativos com quaisquer alterações programáticas, bem como quando, no ciclo de vida da página, tal API pode operar. Em geral, os designs mais simples são menos poderosos e podem atender a menos casos de uso.
Consulte estas questões em aberto para obter mais discussões e casos de uso em que uma API programática pode ajudar.
import.meta.resolve()
A função import.meta.resolve(specifier)
proposta permite que os scripts do módulo resolvam especificadores de importação para URLs a qualquer momento. Veja whatwg/html#5572 para mais. Isso está relacionado à importação de mapas, pois permite resolver recursos "relativos ao pacote", por exemplo
const url = import . meta . resolve ( "somepackage/resource.json" ) ;
forneceria a localização mapeada apropriadamente de resource.json
dentro do namespace somepackage/
controlado pelo mapa de importação da página.
Vários membros da comunidade têm trabalhado em polyfills e ferramentas relacionadas à importação de mapas. Aqui estão os que conhecemos:
package.json
e node_modules/
.package.json
.<script type="systemjs-importmap">
.Sinta-se à vontade para enviar uma solicitação pull com mais! Além disso, você pode usar o número 146 no rastreador de problemas para discussão sobre este espaço.
Este documento originou-se de um sprint de um dia envolvendo @domenic, @hiroshige-g, @justinfagnani, @MylesBorins e @nyaxt. Desde então, @guybedford tem sido fundamental na prototipagem e no avanço da discussão sobre esta proposta.
Obrigado também a todos os colaboradores do Issue Tracker pela ajuda na evolução da proposta!