前言

最近运用Rollup打包输出CJS模块的代码,时不时会提示导入的模块是ESM标准,需求运用动态导入的方法引进这个模块,这个问题引起了我的留意,所以决议深入研究一下。

下面我会围绕这两个点讲解一波。

  • 简略介绍ESMCJS的原理
  • ESMCJS的相互引证问题

CJS和ESM的历史演化

JavaScript一开始诞生是在浏览器上作为脚本语言运用的,其时在浏览器上并不存在模块的概念。它就像日本动漫里的男主角,刚转生到异世界时没有任何的兵器和装备(不是龙傲天),所以说一开始的JavaScript也是不完善的,需求时刻的历练,打怪晋级学技能。

直到Node.JS的诞生,让JavaScript能在服务端运转,一起Node.js社区的发展给JavaScript带来了模块化的概念,运用module.exports导出和require引进模块,这个标准叫做CommonJS。它的呈现使得服务端的JavaScript应用程序的开发更加标准化和标准化。

因为CommonJS的引进模块原理是同步加载的方法,要是在浏览器端运用这个标准,则需求等待所有的模块加载完才干履行后边的代码。因为浏览器中的JavaScript引擎是单线程的,假如加载JavaScript模块的过程中,代码履行的时刻过长,就会导致其他代码无法履行,从而影响整个页面的渲染和交互,最终影响了用户体验。因而,浏览器需求一种异步加载模块方法。

在2015年的6月,ES6标准正式推出,在这个标准里带来了JavaScript异步加载模块的ESM标准。

ESM,也叫做 ECMAScript modules或许ES modules,是来自ES6标准里的模块化概念,运用importexport关键字,来完成js的模块化导入和导出。

ESModule和CommonJS相互引证

彻底不同的两个标准,相互导入的话会导致什么情况呢?

在持续之前,咱们需求了解Node.js支撑运转ESM标准的最低版别是:v13.2.0,在之前的版别中,想要在node中运用ESM,需求增加--experimental-module参数,在v13.2.0版别之后可以直接运用。

我将会运用Node.jsv16.18.0版别进行下面的试验,话不多说,立马进入。

CJS导入ESM模块

  1. 先新建一个ESM模块,文件名为: esm.js,代码如下:
// esm.js
export const a = 1;
export const b = 2;
export function foo () {
  return 3;
}
  1. 另外新建一个CJS模块,文件名为:cjs.js,代码如下:
// cjs.js
const { a, b, foo } = require('./esm.js')
console.log(a);
console.log(b);
console.log(foo());
  1. 运转cjs.js文件,成果提示以下的过错信息:
export const a = 1;
^^^^^^
SyntaxError: Unexpected token 'export'

经过来说,假如在CJS上经过相对路径方法引进.js结束的ESM模块,就会遇到上面的问题。根本原因Node.js底层在加载这个esm.js文件时,把它辨认成了CJS标准,最后运用cjs的loader进行处理时抛出的反常:

ESModule与CommonJS互相引用的问题

那么问题来了,Node.js是如何辨认引进的文件是ESM仍是CJS标准的?其实Node.js有一个判断的机制叫做:Determining module system。用图例简略解析这个机制的判断逻辑:

ESModule与CommonJS互相引用的问题

为了Determining module system可以把esm.js正确辨认成ESM模块,咱们有2种计划:

第一种:修正文件名后缀的方法。

过程如下:

  1. 咱们修正一下esm.js文件名为esm.mjs,然后调整一下cjs.js文件的内容如下:
// cjs.js
const { a, b, foo } = require('./esm.mjs');
console.log(a);
console.log(b);
console.log(foo());
  1. 履行指令node cjs.js,又抛出反常了,不过这次的报错略微不太一样:
node:internal/modules/cjs/loader:1031
    throw new ERR_REQUIRE_ESM(filename, true);
    ^
Error [ERR_REQUIRE_ESM]: require() of ES Module esm.mjs not supported.
Instead change the require of esm.mjs to a dynamic import() which is available in all CommonJS modules.
  1. 大致的意思是ESM模块不支撑经过require()函数引进,需求替换成经过import()函数动态引进ESM模块,那咱们就改一下cjs.js文件中代码,经过动态import的方法引进:
async function main() {
  const mod = await import('./esm.mjs');
  const { a, b, foo } = mod;
  console.log(a);
  console.log(b);
  console.log(foo());
}
main();
  1. 成功输出:
1
2
3

第二种:在文件同级或许父级目录加一个package.json文件,并且加上”type”字段,值为”module”。

为了让cjs.jsesm.js别离辨认成正确的标准。咱们改形成下图的结构:

ESModule与CommonJS互相引用的问题

其中lib/package.json的文件内容如下,这样esm.js就会被辨认成ESM模块。

// lib/package.json
{
  "type": "module"
}

然后cjs.js文件导入esm.js咱们可以改写成:

// cjs.js
async function main() {
  // 这儿改成导入./lib/esm.js 留意看后缀是 .js
  const mod = await import('./lib/esm.js');
  const { a, b, foo } = mod;
  console.log(a);
  console.log(b);
  console.log(foo());
}
main();

最后运转指令:node ./cjs.js,成功!!!

1
2
3

经过以上的试验,咱们了解到Node.js的底层机制现已支撑CJS模块导入ESM模块,不过需求留意以下的几个点:

  • 根据Determining Module System机制,需求修正ESM模块的文件后缀名为.mjs或许经过同级或许最近父级的package.json加上"type": "module"
  • CJS模块只能经过import()函数异步导入ESM模块。

ESM导入CJS模块

在持续之前,咱们需求了解Node.js支撑运转ESM标准的最低版别是:v13.2.0,所以首要保证本机的Node.js版别大于等于v13.2.0

首要新建一个文件夹,结构如下:

ESModule与CommonJS互相引用的问题

  1. 编辑根目录下的package.json文件,加上type字段,值为”module”,如下所示:
// package.json
{
  ...,
  "type": "module",
  ...,
}

这样esm.js就会被作为ESM模块处理。

  1. 编辑lib/package.json,内容如下:
// lib/package.json
{
   // 或许可以加上"type": "commonjs"
}
  1. esm.jscjs.js文件内容别离为:
// ./lib/cjs.js
module.exports = {
  a: 1,
  b: 2,
  foo: function () {
    return 3
  }
}
// esm.js
import pkg from './lib/cjs.js'
const { a, b, foo} = pkg;
console.log(a);
console.log(b);
console.log(foo());
  1. 运转node esm.js,输出了以下信息:
1
2
3

ESM成功引进CJS模块!!!

在Node.js中ESM模块是支撑引进CJS模块的,可是留意以下几点:

  • 为了让Node.js正确辨认这是一个CJS模块,根据Determining Module System机制,需求修正ESM模块的文件后缀名为.cjs,或许经过同级或许最近父级文件夹下的package.json加上"type": "commonjs"字段。
  • 转化时,CJS模块的module.exportsESM模块的export default对应。

总结

经过以上的章节,咱们了解到CJSESM模块各自的特色,也知道了Node.js的Determining module system机制时是如何辨认代码属于哪种模块,再经过不同的loader去转化代码。

当你需求封装一些类库,引进不同标准依赖的时分也可以挥洒自如地处理,也可以合作一些打包东西例如Rollup的运用。

本来两种模块标准是别离用在不同的端,可是因为ESM标准的异步加载优势,天然生成支撑Tree Shaking,以及各种JavaScript打包东西的盛行,例如WebpackRollup等等,越来越多的开发者在封装各类东西库时运用ESM标准。

后边我将会写一篇文章讲解运用Rollup东西的简略运用,来解决打包成CJS标准时处理ESM。不过或许在不远的将来,Node.js默认运用ESM标准履行js代码也说不定。

感谢你阅读到这儿,如有发现文章中存在过错的当地也请纠正。