Skip to content
On this page

ESM 高阶特性

import map

  • 绝对路径,如 https://cdn.skypack.dev/react
  • 相对路径,如./module-a
  • bare import即直接写一个第三方包名,如reactlodash 浏览器默认不支持 bare import import map 就是来解决这个问题的
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <div id="root"></div>
  <script type="importmap">
  {
    "imports": {
      "react": "https://cdn.skypack.dev/react"
    }
  }
  </script>

  <script type="module">
    import React from 'react';
    console.log(React)
  </script>
</body>

</html>

兼容性

它只能兼容市面上 68% 左右的浏览器份额,而反观type="module"的兼容性(兼容 95% 以上的浏览器),import map的兼容性实属不太乐观

相应的 polyfill

es-module-shims

  1. dynamic import。即动态导入,部分老版本的 Firefox 和 Edge 不支持。
  2. import.metaimport.meta.url。当前模块的元信息,类似 Node.js 中的 __dirname__filename
  3. modulepreload。以前我们会在 link 标签中加上 rel="preload" 来进行资源预加载,即在浏览器解析 HTML 之前就开始加载资源,现在对于 ESM 也有对应的modulepreload来支持这个行为。
  4. JSON ModulesCSS Modules,即通过如下方式来引入json或者css:

Nodejs 包导出策略

在 Node.js 中(>=12.20 版本)有一般如下几种方式可以使用原生 ES Module:

  • 文件以 .mjs 结尾;
  • package.json 中声明type: "module"

main 字段

"main": "./dist/index.js"

exports 字段

// package.json
{
  "name": "package-a",
  "type": "module",
  "exports": {
    // 默认导出,使用方式: import a from 'package-a'
    ".": "./dist/index.js",
    // 子路径导出,使用方式: import d from 'package-a/dist'
    "./dist": "./dist/index.js",
    "./dist/*": "./dist/*", // 这里可以使用 `*` 导出目录下所有的文件
    // 条件导出,区分 ESM 和 CommonJS 引入的情况
    "./main": {
      "import": "./main.js",
      "require": "./main.cjs"
    },
  }
}

条件导出常见属性

  • node: 在 Node.js 环境下适用,可以定义为嵌套条件导出,如:
{
  "exports": {
    {
      ".": {
       "node": {
         "import": "./main.js",
         "require": "./main.cjs"
        }
      }
    }
  },
}
  • import: 用于 import 方式导入的情况,如import("package-a");
  • require: 用于 require 方式导入的情况,如require("package-a");
  • default,兜底方案,如果前面的条件都没命中,则使用 default 导出的路径。

    当然,条件导出还包含 typesbrowserdevelomentproduction 等属性,大家可以参考 Node.js 的详情文档

Nodejs 包导入策略

import 字段

{
  "imports": {
    // key 一般以 # 开头
    // 也可以直接赋值为一个字符串: "#dep": "lodash-es"
    "#dep": {
      "node": "lodash-es",
      "default": "./dep-polyfill.js"
    },
  },
  "dependencies": {
    "lodash-es": "^4.17.21"
  }
}

使用

// index.js
import { cloneDeep } from "#dep";

const obj = { a: 1 };

// { a: 1 }
console.log(cloneDeep(obj));

Node.js 在执行的时候会将#dep定位到lodash-es这个第三方包,当然,你也可以将其定位到某个内部文件。这样相当于实现了路径别名的功能,不过与构建工具中的 alias 功能不同的是,"imports" 中声明的别名必须全量匹配,否则 Node.js 会直接抛错。

Pure ESM

概念

首先,什么是 Pure ESM ? Pure ESM 最初是在 Github 上的一个帖子中被提出来的,其中有两层含义,一个是让 npm 包都提供 ESM 格式的产物,另一个是仅留下 ESM 产物,抛弃 CommonJS 等其它格式产物。 同时也有一部分的 npm 包做得更加激进,直接采取Pure ESM模式,如大名鼎鼎的chalkimagemin

对于没有上层封装需求的大型框架,如 Nuxt、Umi,在保证能上 Pure ESM的情况下,直接上不会有什么问题;但如果是一个底层基础库,最好提供好 ESM 和 CommonJS 两种格式的产物。

ESM 和 Commonjs 的兼容性

ESM<-CommonJS

在 ESM 中,我们可以直接导入 CommonJS 模块,如:

// react 仅有 CommonJS 产物
import React from 'react';
console.log(React)

CommonJS<-ES

Node.js 执行以上的原生 ESM 代码并没有问题,但反过来,如果你想在 CommonJS 中 require 一个 ES 模块,就行不通了:

图片.png

可以通过动态导入在 commonjs 引入 ES 模块

图片.png

    1. 如果执行环境不支持异步,CommonJS 将无法导入 ES 模块;
    1. jest 中不支持导入 ES 模块,测试会比较困难;
    1. 在 tsc 中,对于 await import()语法会强制编译成 require的语法(详情),只能靠eval('await import()')绕过去。

总结

总而言之,CommonJS 中导入 ES 模块比较困难。因此,如果一个基础底层库使用 Pure ESM,那么潜台词相当于你依赖这个库时(可能是直接依赖,也有可能是间接依赖),你自己的库/应用的产物最好为 ESM 格式。也就是说,Pure ESM是具有传染性的,底层的库出现了 Pure ESM 产物,那么上层的使用方也最好是 Pure ESM,否则会有上述的种种限制。

但从另一个角度来看,对于大型框架(如 Nuxt)而言,基本没有二次封装的需求,框架本身如果能够使用 Pure ESM ,那么也能带动社区更多的包(比如框架插件)走向 Pure ESM,同时也没有上游调用方的限制,反而对社区 ESM 规范的推动是一件好事情。

新一代的基础库打包器

当然,上述的结论也带来了一个潜在的问题: 大型框架毕竟很有限,npm 上大部分的包还是属于基础库的范畴,那对于大部分包,我们采用导出 ESM/CommonJS 两种产物的方案,会不会对项目的语法产生限制呢? 我们知道,在 ESM 中无法使用 CommonJS 中的 __dirname__filenamerequire.resolve 等全局变量和方法,同样的,在 CommonJS 中我们也没办法使用 ESM 专有的 import.meta对象,那么如果要提供两种产物格式,这些模块规范相关的语法怎么处理呢?

在传统的编译构建工具中,我们很难逃开这个问题,但新一代的基础库打包器tsup给了我们解决方案。

  • ESM 和 CommonJS 双格式的产物,并且可以任意使用与模块格式强相关的一些全局变量或者 API
export interface Options {
  data: string;
}

export function init(options: Options) {
  console.log(options);
  console.log(import.meta.url);
}

将会转换成

var getImportMetaUrl = () =>
  typeof document === "undefined"
    ? new URL("file:" + __filename).href
    : (document.currentScript && document.currentScript.src) ||
      new URL("main.js", document.baseURI).href;
var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();

// src/index.ts
function init(options) {
  console.log(options);
  console.log(importMetaUrl);
}