Import Map详解

作为「Web 新特性」专栏的第一篇文章,准备介绍一下目前被大家接受且支持比较好的 import map,关于 import map,上一篇文章已经介绍了它的基本功能和用法,本篇将对其进行详细的介绍。

基本功能

前面的文章提到了关于 Import Map 的内容,其设计初衷用一句话归结就是控制 JavaScript 的模块加载行为,具体表现为:

  1. 允许 import 语句或者import()函数时使用简写来代替 URL 引入模块
  2. 允许对内置模块进行降级映射(Chrome)

举个例子,在浏览器中你可以这样写了:

JavaScript
1
2
3
4
import React from "react";
import("lodash").then((Module) => {
// ...
});

在日常工作开发中这种写法是很常见的,但浏览器中直接运行会出错,因为浏览器将字符串当作 URL 请求,以致于找不到对应的文件模块,现在 import map 特性支持我们在浏览器中这样做,只需要在 HTML 中插入以下内容:

JavaScript
1
2
3
4
5
6
7
8
<script type="importmap">
{
"imports": {
"react": "./public/assets/react.js",
"lodash": "./public/assets/lodash.js"
}
}
</script>

这样一来,import React from "react" 相当于变成了import react from "./public/assets/react.js",请求了 react.js,模块就能正确加载了。

斜杠/的秘密

基本使用

除了这个基本能力以外,import map 提案还考虑了诸多场景,比如需要用到相同的文件路径(可以理解为同一文件夹这种简单场景)下的多个文件时,不需要每个文件都被显式的写出来,只需在映射的两端加上斜杠/即可,这样在引用的时候更符合直觉(看起来像是使用某个模块下的文件),此时冒号左边的模块名就相当于模块前缀:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
<script type="importmap">
{
"imports": {
"lodash": "./lodash/main.js",
"lodash/": "./lodash/"
}
}
</script>
<script type="module">
import lodash from "lodash";
import debounce from "lodash/debounce.js";
</script>

如果使用了斜杠,引入的时候是需要明确指定文件后缀的,有一种方法可以避免这样写:

示例 1
1
2
3
4
5
6
7
8
9
10
<script type="importmap">
{
"imports": {
"lodash/debounce": "./lodash/debounce.js"
}
}
</script>
<script type="module">
import debounce from "lodash/debounce";
</script>

这样写起来使导入具有语义性,说明后者是前者的子模块或者子函数,并没有要求lodash/debounce指向的文件一定在lodash/指代的文件夹下。可以做一个实验验证一下:

示例 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script type="importmap">
{
"imports": {
"ex/": "http://127.0.0.1:5500/",
"ex/foo": "https://esm.sh/react",
"ex/bar": "http://127.0.0.1:5500/bar.js"
}
}
</script>
<script type="module">
import foo from "ex/foo";
import bar from "ex/bar";
console.log(foo.version); // 打印 '17.0.2'
bar();
</script>

上面这段代码是没有问题的,即使ex/指向本地文件,而ex/foo指向公网链接,只是显得没有语义性,推荐示例 1那样用。

只区分最末尾的那个斜杠/

import map 对于斜杠的使用只有一条规则,不管中间写多少个斜杠,浏览器只会当作一个字符串整体来对待,浏览器只会区别对待末尾的那个斜杠,如果冒号两边末尾为斜杠/,说明模块名将被当作模块前缀使用,所以有了一些神奇的用法,比如“移花接木”,用 URL 字符串代替另一个真实链接:

importmap
1
2
3
4
5
{
"imports": {
"https://reactjs.org": "https://esm.sh/react"
}
}

那么引用 react 的时候直接import react from "https://reactjs.org"

也可以实现在引用本地模块文件,使之看起来像某网站上的文件,如下:

importmap
1
2
3
4
5
{
"imports": {
"https://www.unpkg.com/vue/": "/node_modules/vue/"
}
}

左右两侧必须都是斜杠结尾

使用末尾斜杠这种写法时,map 的左右两侧必须都是斜杠结尾,如果右侧不是斜杠结尾,那么即使左侧是斜杠结尾,在使用引用时不能被当作前缀使用,否则会引起错误。

作用域 scope

在编程语言中,作用域主要用于隔离变量,比如有两个同名变量的时候,通过作用域进行区分。import map 的作用域也是一样,当我们在 map 中指定了重复名字时,用于帮我们确定到底使用哪一个。import map 的作用域只能区分不同文件路径前缀,就是说当目标文件在不同文件夹下时才能使用 import map 的作用域,使用方式如下:

importmap
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"imports": {
"axios": "./axios-0.24.0"
},
"scopes": {
"/v1/": {
"axios": "./axios-0.10.0"
},
"/v2/": {
"axios": "./axios-0.20.0"
}
}
}

在顶层添加「scopes」字段,对应一个「key-value」对象,每一个 key 都是 scope,scope 其实是/开始的文件夹路径,当有文件在这个路径下引用了模块,此时就会优先寻找该路径下的指定模块。比如/v1文件夹下的index.jsimport lodash from "lodash",那么将会去找/v1scope 下的lodash,即会解析为./lodash-0.10.0,而不是./lodash-0.24.0./lodash-0.20.0

奇怪的作用域写法

import map 的 scope 写法是有点奇怪的,从直觉上,作用域应该是可以嵌套的,但是 import map 的写法是扁平化的,请看以下文件结构:

1
2
└───v1
└───v1-1

在 import map 的书写中/v1//v1-1/都是直接写在scopes下的同一级中。

importmap
1
2
3
4
5
6
{
"scopes": {
"/v1/": {},
"/v1/v1-1/": {}
}
}

当文件夹v1-1中有文件引用了模块,会先找/v1/v1-1/scope 中是否有映射,其次会看/v1/scope 中有没有映射,看起来浏览器对映射的查找是有层级关系的,但是扁平化的写法违反了开发者的直觉。这一点需要注意,因为scope实际上是文件夹路径前缀,文件夹存在嵌套关系,那么scope也是有嵌套(继承)关系的,浏览器对映射的查找也是自底向上的。

引入方式

外部链接引入

除了之前的示例的写在 HTML 中,也可以通过链接来引入,方法如下:

index.html
1
<script type="importmap" src="./importmap.importmap"></script>

虽然文件扩展名是importmap,但其实不管什么扩展名都行,只需要保证 HTTP 请求返回的 MIME 类型为application/importmap+json

动态生成引入

另一种引入方法是使用 JavaScript 动态生成 import map,构造成 script 标签,将其插入到 DOM 中:

JavaScript
1
2
3
4
5
6
7
8
const el = document.createElement('script')
el.type = 'importmap'
el.textContent = JSON.stringify({
imports: {
'ployfill': isIE ? './ie-polyfill.js' : './polyfill.js'
}
})
document.currentScript.afterel)

这种方式看起来有点花里胡哨,但是在需要区别引入不同的模块的时候非常有用。

注意:无论用什么方式引入,必须保证 import map 在模块解析之前加载,并且不要在 import map 生效后修改其内容

import map 映射降级

import map 并不在 W3C 标准中,只是 WICG 的一个提案,所以对其支持的浏览器比较少,目前只有 Chrome 实现,在 Chrome 74 中作为实验性功能出现,直到 Chrome 89 版本才成为正式功能。在 Chrome 74 中和 import map 一同出现的功能还有内置模块 kv-storage,于是在 Chrome 的 import map 设计中,实现了一个原始提案中没有提到的能力:内置模块降级。即 import map 中可以尝试映射 Chrome 提供的内置模块,如果失败则会映射到降级的内容。

importmap
1
2
3
4
5
{
"imports": {
"kv-storage": ["@std/kv-storage", "/lib/kv-storage/index.js"]
}
}

可以看到,右边的映射路径不再是一个字符串,而是字符串数组,Chrome 会尝试映射到第一个字符串,如果失败的话会尝试映射到下一个字符串地址。

注意:降级是 Chrome 自行实现的,并非提案标准,而且只能降级内置模块

谁在使用?

上边讲的 import map 全是浏览器中的实现,而且只是 Chrome 中,介绍 import map 的一个原因是其实已经被被很多开发者接受使用,在 Deno 中,所有模块的导入都写完整的 http 链接是非常烦人的,尤其是经常引用这些模块的时候,因此可以使用 import map 来映射到一个较短的标识符。

importmap
1
2
3
4
5
{
"imports": {
"io.files": "https://deno.land/std@0.116.0/io/files.ts"
}
}
javascript
1
2
- import {readRange} from "https://deno.land/std@0.116.0/io/files.ts“
+ import {readRange} from "io.files"

如果在模块出现的时候就出现了 import map,会不会从一开始就是 bundleless,不用经历这么多年的打包坎坷了?

更多资源

  1. import map 提案仓库:GitHub
  2. import map WICG 提案:wicg.github.io
  3. Chrome import map 实现:docs.google.com
  4. Chrome import map 实现进度:chromestatus.com
  5. Chrome kv-storange 介绍:developers.google.com
作者

KylinLee

发布于

2021-11-17

更新于

2021-12-02

许可协议

CC BY-NC-SA 4.0

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×