ESM 与 CommonJS

1,324 阅读3分钟

使用差异

image.png

Commonjs 与 ESM 差异

  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
  • CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

差异一

CommonJS 模块输出的是一个值的拷贝

内存在中间,一个导出通用 JS 模块指向一个内存位置,然后将值复制到另一个,导入 JS 模块指向新位置

// a.js
let counter = 1
exports.counter = 1
counter++
// b.js
const { counter } = require('./a.js')
console.log(counter) // 输出 1

ES6 模块输出的是值的引用

与上图相同,但 main.js 的模块环境记录现在将其导入链接到其他两个模块的导出。

// a.js
export let counter = 1
counter++
// b.js
import {counter} from './a.js'
console.log(counter) // 输出 2

差异二

CommonJs 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而ES6模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

差异三

ESM 异步加载模块

  1. 找到程序的入口文件

    1. 在html中<script src='main.js' type='module'>
    2. 在node中package.json中通过"main": "./main.js"
  2. 根据入口文件main.js的导入语句分析入口文件的依赖模块

    img

  1. 然后再分析counter.js的依赖,就这样一层一层的遍历,找出依赖并加载

img

  1. 如果浏览器主线程等待这些文件一个个都下载完成,那许多其它任务将堆积在主线程队列中

image-20211109092052113

  1. 像这样阻塞主线程会使使用模块的应用程序使用速度太慢。所以ESM规范将算法拆分成三个阶段

img

  1. 构建--查找、下载所有文件并将其解析为模块记录(Module Record)并生成模块映射(Module Map) img 用模块记录填充的模块映射图中的“获取”占位符

  2. 实例化--在内存中找到 内存地址(Memory Location) 来存放所有导出的值,然后让导出和导入都指向该内存地址,这称为链接

  3. 评估--运行代码以使用变量的实际值填充

CommonJS 同步加载

因为CommonJS是从文件系统加载文件,这比通过Internet下载花费的时间少得多。这意味着Node可以在加载文件时阻塞主线程。同时也意味着在返回模块实例之前,你要遍历整个树,加载、实例化和评估任何依赖项。 img

Node.js的模块加载方法

  • 采用ESM的情况

    • .mjs后缀的js文件内可以使用exportimport
    • package.json中指定typemodule
  • 采用CommonJS的情况

    • .cjs后缀的js文件内可以使用exportsrequire
    • package.json中指定typecommonjs

注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

package.json的main字段

// ./node_modules/A/package.json
{
  "type": "module",
  "main": "./src/index.js"
}

上面代码指定项目的入口脚本为./src/index.js,它的格式为 ES6 模块。如果没有type字段,index.js就会被解释为 CommonJS 模块。

然后,其它项目就可以通过import命令就可以加载这个模块。

import { something } from 'A'

上面代码中,运行该脚本以后,Node.js 就会到./node_modules目录下面,寻找A模块,然后根据该模块package.jsonmain字段去执行入口文件。

package.json的exports字段

main的别名

exports字段的别名如果是., 就代表模块的主入口,优先级高于main字段,并且可以直接简写成exports字段

{
  "exports": {
    ".": "./main.js"
  }
  // 等同于
  "exports": "./main.js"
}

子目录别名

package.json文件的exports字段可以指定脚本或子目录的别名

// ./node_modules/es-module-package/package.json
{
  "exports": {
    "./submodule": "./src/submodule.js"
  }
}

上面的代码指定src/submodule.js别名为submodule,然后就可以从别名加载这个文件。

import submodule from 'es-module-package/submodule';
// 加载 ./node_modules/es-module-package/src/submodule.js

参考资料