Javascript Import maps

最新在学习jspm,里面出现了很多关于import maps的概念,想去学习下,鉴于中文文章基本上没有找到关于import maps的介绍,于是翻译了import-maps

如有不正确欢迎指出。

什么是Import maps?

这个提案允许控制 js的 import语句或者import()表达式获取的库的url,并允许在非导入上下文中重用这个映射。这就解决了非常多的问题,比如:

  • 允许直接import标志符,就能在浏览器中运行,比如:import moment from "moment
  • 提供兜底解决方案,比如import $ from "jquery",他先会去尝试去CDN引用这个库,如果CDN挂了可以回退到引用本地版本。
  • 开启对一些内置模块或者其他功能的polyfill。
  • 共享import标识符在Javascript importing 上下文或者传统的url上下文,比如fetch()或者

他的主要机制是通过导入import map(模块和对应url的映射),然后我们就可以在HTML或者CSS中接受使用url导入模块的上下文替换成import: URL scheme来导入模块。

来看下面这个例子:

import moment from "moment";
import { partition } from "lodash";

这样写纯粹的标识符会抛出错误,见explicitly reserved。(简单来说只能允许/ ./ ../开头的标识符)。
但是如果有了import map:


那种纯粹的写法就能被解析为:

import moment from "/node_modules/moment/src/moment.js";
import { partition } from "/node_modules/lodash-es/lodash.js";

import:URL Schema的场景下:


更多关于值为"importmap"的script标签见: installation section.

具体功能

模块标识符映射

模块的纯粹标识符

{
  "imports": {
    "moment": "/node_modules/moment/src/moment.js",
    "lodash": "/node_modules/lodash-es/lodash.js"
  }
}

在设置了这个import map之后,

import moment from "moment";
import("lodash").then(_ => ...);

以上这种写法就能在js里直接支持。

需要注意的是映射的value,必须以/./或者../开头,或者是一个能够识别的url。在这个示例中的是一个类相对路径的地址,它会根据import map的基本路径进行解析,比如:内联的import map,它的基础路径就是页面url,而如果是外部资源的import map(

这样就不能正常运行了,这样会无条件的加载polyfill,而不会使用内建模块。
如果还想上面的功能正常进行,应该这么写:


遗憾的是,这样的写法只能运行在支持import maps的浏览器中,但是在不支持import maps的浏览器中,我们只有这么写:


这样就能像上述我们所说那种运行了。

作用域

相同的模块多版本

这是一个常见的case,比如我们用了socksjs-client这个库,他依赖[email protected],但是我们想使用[email protected]的功能。一种解决方案是我们为我们使用的querystringify改个名字,但这并不是我们想要的解决方案。
Import maps 提供了scope 来解决这样的情况:

{
  "imports": {
    "querystringify": "/node_modules/querystringify/index.js"
  },
  "scopes": {
    "/node_modules/socksjs-client/": {
      "querystringify": "/node_modules/socksjs-client/querystringify/index.js"
    }
  }
}

使用了这个import maps,任何以/node_modules/socksjs-client/开头的库都会去使用/node_modules/socksjs-client/querystringify/index.js,然而顶级的imports确保其他我们使用到的querystringify,都是"/node_modules/querystringify/index.js"这个版本。

作用域继承

作用域以一种简单的方式合并切覆盖例如:

{
  "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"
    }
  }
}

将会按以下的这种方式解析:

Specifier Referrer Resulting URL
a /scope1/foo.mjs /a-1.mjs
b /scope1/foo.mjs /b-1.mjs
c /scope1/foo.mjs /c-1.mjs
a /scope2/foo.mjs /a-2.mjs
b /scope2/foo.mjs /b-1.mjs
c /scope2/foo.mjs /c-1.mjs
a /scope2/scope3/foo.mjs /a-2.mjs
b /scope2/scope3/foo.mjs /b-3.mjs
c /scope2/scope3/foo.mjs /c-1.mjs

虚拟化

有能包装、扩展、删除内建模块的能力是非常重要的,以下举例import maps如何做到这一点。
注意:这同样对第三方模块有效,只是以内建模块为例子

删除内建模块

尽管这是非常极端及很少使用的,但是依然可能出现需要拒绝对某个模块访问的情况,如果是全局模块,我们可以这么做:

delete self.WebSocket;

在import maps中你可以限制对一个内建模块的访问,通过设置其值为空数组:

{
  "imports": {
    "std:kv-storage": []
  }
}

或者设置为null:

{
  "imports": {
    "std:kv-storage": null
  }
}

这样在代码里:

import { Storage } from "std:kv-storage"; // throws

就会抛出错误。

选择性拒绝

可以使用scope的特性,选择性限制对某个内建模块的访问:

{
  "imports": {
    "std:kv-storage": null
  },
  "scopes": {
    "/js/storage-code/": {
      "std:kv-storage": "std:kv-storage"
    }
  }
}

"/js/storage-code/"中就可以访问到内建模块,而在全局访问都会抛出错误。

当然也可以让模块模块访问不到"std:kv-storage",而其他地方都能访问:

{
  "scopes": {
    "/node_modules/untrusted-third-party/": {
      "std:kv-storage": null
    }
  }
}

封装内建模块

有时候我们需要封装一下内建模块,这就需要,封装的这个模块能访问原生的内建模块,而其他地方得访问封装后的模块,可以像以下这么写:

{
  "imports": {
    "std:kv-storage": "/js/als-wrapper.mjs"
  },
  "scopes": {
    "/js/als-wrapper.mjs": {
      "std:kv-storage": "std:kv-storage"
    }
  }
}

其他地方import "std:kv-storage"时都会访问到"/js/als-wrapper.mjs",而这个就是封装后的模块,但是在"/js/als-wrapper.mjs"这个文件自身内访问的是原生的模块:

import instrument from "/js/utils/instrumenter.mjs";
import { storage as orginalStorage, StorageArea as OriginalStorageArea } from "std:kv-storage";

export const storage = instrument(originalStorage);
export const StorageArea = instrument(OriginalStorageArea);

扩展内建模块

这个封装一个内建模块非常相似,比如我们需要在内建模块上再export一个class:SuperAwesomeStorageArea。我们只需要依然使用上例的import maps,然后修改"/js/als-wrapper.mjs"代码:

export { storage, StorageArea } from "std:kv-storage";
export class SuperAwesomeStorageArea { ... };

如果我们是在想原声模块上添加方法,那么我们不需要import maps,而是直接引入polyfill即可,polyfill文件里给StorageArea.prototype添加方法。

import: URLs

作为import maps概念的补充,提供了import: URL scheme。 它使得在HTML、CSS或者一些其他接受URL地方来使用import map。

一个widget库的例子

这个库不仅仅包括js模块,还报错css主题和一些图片,你可以配置import map:

{
  "imports": {
    "widget": "/node_modules/widget/index.mjs",
    "widget/": "/node_modules/widget/"
  }
}

然后就可以使用:



或者:

.back-button {
  background: url('import:widget/assets/back.svg');
}

这使得所有web资源都可以通过库标识符来访问。

数据文件的例子

比如the timezone database这样的json文件:

{
  "imports": {
    "tzdata": "/node_modules/tzdata/timezone-data.json"
  }
}

然后就可以这样访问:

const data = await (await fetch('import:tzdata')).json();

URL解析语义

如何精确的来解析import:仍然是有些模糊的,特别是以下两种情况使用URL时:

  • 解析相对路径标识符,例如import:./foo
  • 在不同地方决定使用哪个作用域时
    第一种情况并不重要,因为相应的用例并不十分重要。但是第二种情况就会产生歧义,比如我们app用了v2的widget,但是有一个第三方库使用了v1的widget,我们就需要配置:

{
  "imports": {
    "widget": "/node_modules/widget-v2/index.mjs",
    "widget/": "/node_modules/widget-v2/"
  },
  "scopes": {
    "/node_modules/gadget/": {
      "widget": "/node_modules/widget-v1/index.mjs",
      "widget/": "/node_modules/widget-v1/"
    }
  }
}

问题在于/node_modules/gadget/styles.css会怎么解析?

.back-button {
  background: url(import:widget/back-button.svg);
}

这里其实和你预期时一样的,使用的是v1相应的url。
当前我们关于import:所提案的url解析方案是使用请求所在的URL,意思是:

  • 默认的,使用页面的基础URL(获取客户端API所基于的URL)
  • 如果请求发生在css里,使用css文件的url( location见 Referrer Policy
  • 如果请求发生在 HTML module里,使用模块的相对路径。

但是默认这种选择就会出现问题,假如在/node_modules/gadget/index.mjs里:

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = 'import:widget/themes/light.css';
document.head.append(link);

因为他最终会成为页面上的一个link元素然后引用import:widget/themes/light.css,而不是js代码里的引用,所以他最终会按照页面的URL去解析,故得到的是v2的版本。但实际上这是在/node_modules/gadget/index.mjs里的代码,我希望他获取的是v1。

一个提案是使用import.meta.resolve()

const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = import.meta.resolve('widget/themes/light.css');
document.head.append(link);

由于兜底这个功能这也会变得有些复杂。讨论:#79。

前一个版本的提案是解析import:相对于,当前执行脚本,但是也会有问题,见:#75

Import map 处理程序

安装


或者:


但是当用src时,http的response的MIME必须是application/importmap+json(为什么不用application/json这将会使内容安全协议
失效),并且就像大多数cdn上的库,都是开启了跨域,且通常都是按UTF-8解析的。

由于import maps影响着所有的引入,所以import maps必须在所有其他模块解析前加载成功。这就代表import maps会阻塞其他任何导入的加载。

这就意味着我们强烈推荐使用内联的import maps,这会带来更好的性能,就类似内联样式一样。直到他们处理完前,他们都会一直阻塞浏览器的运行。如果非要使用外部资源,推荐使用HTTP/2 Push或者bundled HTTP exchanges这样的功能来缓和阻塞造成的影响。

还有另外一种结果就是,如果在import或者import:之后才加载import maps,那么就会抛错。import maps将会被忽略,且script元素也会触发一个错误事件。

一个页面允许多个import maps,它的加载规则和以下等效:

const result = {
  imports: { ...a.imports, ...b.imports },
  scopes: { ...a.scopes, ...b.scopes }
};

the proto-spec有更多关于这个的介绍。

动态生成import maps

你可以在执行任何import之前,执行以下脚本,动态生成import maps




也可以动态覆盖已经导入的import






动态覆盖的script在第二个,因为如果已经有人使用到了import maps再覆盖,就没有用了。

作用域

Import maps是应用级的东西,类似service workers(更确切地说,他是每一个模块的映射,作用在不同的环境里)。所以它不应该被手动的组合,而是应该由控制整个app视角的人或工具生成。比如,一个库里包含import map就是没有意义的, 库应该通过标识符简单的引用,而让整个应用去决定到底映射哪一个url。

也就是这样促使了它是以

你可能感兴趣的:(Javascript Import maps)