关于JavaScript Import Maps的一切

本来是译文,原文见Everything You Need to Know About JavaScript Import Maps。就像它霸气的标题一样,本文讲了关于import maps的一切问题。本文详细的讲述了Import Maps的用法,工作原理,以及polyfill。

ES modules作为标准的JavaScript模块系统在ECMAScript 2015介绍之后,import便开始根据规范来引入相对路径或者绝对路径。

1
2
import dayjs from "https://cdn.skypack.dev/dayjs@1.10.7"; // ES modules
console.log(dayjs("2019-01-25").format("YYYY-MM-DDTHH:mm:ssZ[Z]"));

这和其它的模块系统多少有点差别,比如CommonJS或者使用模块系统进行打包的webpack,它们都有相对简单的语法。

1
2
3
const dayjs = require('dayjs') // CommonJS

import dayjs from 'dayjs'; // webpack

在这些系统中,当处于运行时的node.js查询时import标识符被指向了具体的文件。用户在使用import引入时只需要使用纯粹的模块标识符(通常是包的名字),模块相关的功能就会被自动解析。

因为开发者对这种从npm上下载包,然后后引入的方式已经很熟悉了,因此需要有一种方式能够确保用这种方式写的代码在打包后能够在浏览器上运行。这个问题被import maps解决了。基本上import标识符既可以映射相对位置,也可以映射绝对位置。import maps帮助我们引入模块而不需要进行打包。

Import Maps是怎么工作的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script type="importmap">
{
"import": {
"dayjs": "https://cdn.skypack.dev/dayjs@1.10.7",
}
}
</script>

<script type="module">
import dayjs from 'dayjs';

console.log(dayjs('2019-01-25').format('YYYY-MM-DDTHH:mm:ssZ[Z]'));
</script>

import map配置在html文档的<script type="importmap"></script>标签当中,它必须配置在第一个<script type="module"></script>标签前面,因此它在模块被解析之前执行。另外,一个html文档中,当前只允许出现一个<script type="importmap"></script>标签,虽然这个规定未来有可能被移除掉。

在script标签中,通过一个json对象来配置所有需要在当前html文档中需要引入的模块。格式如下

1
2
3
4
5
6
7
8
9
10
11
<script type="importmap">
{
"imports": {
"react": "https://cdn.skypack.dev/react@17.0.1",
"react-dom": "https://cdn.skypack.dev/react-dom",
"square": "./modules/square.js",
"lodash": "/node_modules/lodash-es/lodash.js"
}
}
</script>

在上面的imports对象中,映射的每一项基本都是一样的。映射的左边是需要导入模块的标识符,右边每一个模块标识副对应的相对路径或者绝对路径。如果配置相对路径,请确保用/,../或者/开头。在imports对象中存在的包,并不一定会被浏览器加载。任何在页面的script没有被用到模块,将不会被浏览器加载,即使它出现在imports对象中。

1
<script type="importmap" src="importmap.json"></script>

可以像上面一样将映射放在其它文件中,然后通过src属性去链接它。如果使用这种方式,需要确保文件头的Content-Type属性值为application/importmap+json。需要注意的是,出于性能考虑,更加推荐使用内联样式。而且本文中下面的例子也都是通过内联样式实现的。

配置好映射关系后,就可以像下面这样使用import标识符引入模块了:

1
2
3
4
5
6
7
8
<script type="module">
import { cloneDeep } from 'lodash';

const objects = [{ a: 1 }, { b: 2 }];

const deep = cloneDeep(objects);
console.log(deep[0] === objects[0]);
</script>

需要注意的是配置好的映射关系并不会影响script标签上的src属性,因此如果像下面这样<script src="/app.js">写代码,浏览器将会尝试下载app.js对应的文件,而忽略标签内部需要import的内容。

将标识符映射到整个包上

除了将标识符映射到一个模块上,我们照样可以将标识符映射到一个包含多个模块的包上面。这可以通过标识符加斜杠结尾的路径来实现。如下:

1
2
3
4
5
6
7
<script type="importmap">
{
"imports": {
"lodash/": "/node_modules/lodash-es/"
}
}
</script>

通过这个技术你可以让浏览器下载某个包中的某一个模块,而不是将整个包都下载下来。

动态构建Import Maps映射

Import Maps映射可以根据任意条件动态配置,这项能力可以用来根据能力侦探的方式来决定引入某一个模块。下面的案例是通过APIIntersectionObserver来动态的决定加载哪一个懒加载文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<script>
const importMap = {
imports: {
lazyload: 'IntersectionObserver' in window
? './lazyload.js'
: './lazyload-fallback.js',
},
};

const im = document.createElement('script');
im.type = 'importmap';
im.textContent = JSON.stringify(importMap);
document.currentScript.after(im);
</script>

如果想这样做,请一定要在插入映射标签前完成这样工作,像上面的案例中一样,因为配置一个已经存在的Import Maps映射将不会起作用。

通过Import Maps引入同一个包的不同版本

通过Import Maps引入同一个包的不同版本是一件非常简单的事情,你只需要把不同版本的标识符在映射中写出来就行。就像下面这样。

1
2
3
4
5
6
7
8
9
<script type="importmap">
{
"imports": {
"lodash@3/": "https://unpkg.com/lodash-es@3.10.1/",
"lodash@4/": "https://unpkg.com/lodash-es@4.17.21/"
}
}
</script>

同时,也可以通过作用域的形式用同一个标识符引入同一个包的不同版本。如下:

1
2
3
4
5
6
7
8
9
10
11
12
<script type="importmap">
{
"imports": {
"lodash/": "https://unpkg.com/lodash-es@4.17.21/"
},
"scopes": {
"/static/js": {
"lodash/": "https://unpkg.com/lodash-es@3.10.1/"
}
}
}
</script>

现在,通过/static/js路径引入的lodash将会走https://unpkg.com/lodash-es@3.10.1/,其它的走https://unpkg.com/lodash-es@4.17.21/

通过Import Maps使用npm下的包

在我开始写这篇的时候,任何稳定的使用ES Modules 的npm包都可以通过被import maps通过cdn的形式引入,比如ESM,Unpkg,Skypack。即使一些npm上的包不是为了ES Modules系统设计的,也可以通过Skypack and ESM的服务将它转换后使用。你可以在Skypack主页上搜索针对浏览器引入优化的版本,而无需自己去转化打包。

编程式检测是否支持Import Map

可以通过HTMLScriptElement.supports())来检测是否支持Import Map。下面代码是检测用的案例:

1
2
3
4
if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
// import maps is supported
}

支持兼容老旧浏览器

import-maps支持图

impirt maps 让我们可以直接通过标识符使用包,而不需要进行打包,但是有些老旧的浏览器是不支持的。这些可以通过polyfill来解决。

目前有一个解决方案是( ES Module Shims)[https://github.com/guybedford/es-module-shims]可以支持ES modules。只需要添加下面代码到页面中即可。

1
<script async src="https://unpkg.com/es-module-shims@1.3.0/dist/es-module-shims.js"></script>

在页面的console中你可能还会看到下面的错误提示,忽略它就行了。

1
Uncaught TypeError: Error resolving module specifier “lodash/toUpper.js”. Relative

其它的import-maps polyfill 可以在下面的页面中 GitHub repository找到。

总结

Import map 提供了在浏览器中使用ES modules 更健壮的方式,不再限制于绝对路径还是相对路径。它使我们可以轻易的调整代码而不去考虑调整引入语句。并且使模块更新更加的顺滑,而不会影响依赖于这些模块的缓存。总之一句话,import maps 让我们在服务端和客户端使用es modules感觉差不多。

感谢阅读。大家可以和作者在Twitter互动。