목차
一、并不完美的 ESM 支持
二、CJS 和 ESM 的不同点
三、借助工具实现 CJS、ESM 混写
웹 프론트엔드 JS 튜토리얼 Node의 두 가지 모듈 사양인 CJS와 ESM에 대해 이야기해 보겠습니다.

Node의 두 가지 모듈 사양인 CJS와 ESM에 대해 이야기해 보겠습니다.

Mar 03, 2022 pm 07:53 PM
esm node

本篇文章给大家带大家了解一下Node的两种模块规范(难以相容的 CJS 与 ESM),介绍一下CJS 和 ESM 的不同点,怎么实现 CJS、ESM 混写,希望对大家有所帮助!

Node의 두 가지 모듈 사양인 CJS와 ESM에 대해 이야기해 보겠습니다.

13.2.0 版本开始,Nodejs 在保留了 CommonJS(CJS)语法的前提下,新增了对 ES Modules(ESM)语法的支持。

天下苦 CJS 久已,Node 逐渐拥抱新标准的规划当然值得称赞,我们也会展望未来 Node 不再需要借助工具,就能打破两种模块化语法的壁垒……

但实际上,一切没有想象中的那么美好。

一、并不完美的 ESM 支持

1.1 在 Node 中使用 ESM

Node 默认只支持 CJS 语法,这意味着你书写了一个 ESM 语法的 js 文件,将无法被执行。

如果想在 Node 中使用 ESM 语法,有两种可行方式:

  • ⑴ 在 package.json 中新增 "type": "module" 配置项。
  • ⑵ 将希望使用 ESM 的文件改为 .mjs 后缀。

对于第一种方式,Node 会将和 package.json 文件同路径下的模块,全部当作 ESM 来解析。

第二种方式不需要修改 package.json,Node 会自动地把全部 xxx.mjs 文件都作为 ESM 来解析。

同理,如果在 package.json 文件中设置 "type": "commonjs",则表示该路径下模块以 CJS 形式来解析。 如果文件后缀名为 .cjs,Node 会自动地将其作为 CJS 模块来解析(即使在 package.json 中配置为 ESM 模式)。

我们可以通过上述修改 package.json 的方式,来让全部模块都以 ESM 形式执行,然后项目上的模块都统一使用 ESM 语法来书写。

如果存在较多陈旧的 CJS 模块懒得修改,也没关系,把它们全部挪到一个文件夹,在该文件夹路径下新增一个内容为 {"type": "commonjs"}package.json 即可。

Node 在解析某个被引用的模块时(无论它是被 import 还是被 require),会根据被引用模块的后缀名,或对应的 package.json 配置去解析该模块。

1.2 ESM 引用 CJS 模块的问题

ESM 基本可以顺利地 import CJS 模块,但对于具名的 exports(Named exports,即被整体赋值的 module.exports),只能以 default export 的形式引入:

/** @file cjs/a.js **/
// named exports
module.exports = {
    foo: () => {
        console.log("It's a foo function...")
    }
}


/** @file index_err.js **/
import { foo } from './cjs/a.js';  
// SyntaxError: Named export 'foo' not found. The requested module './cjs/a.js' is a CommonJS module, which may not support all module.exports as named exports.
foo();


/** @file index_err.js **/
import pkg from './cjs/a.js';  // 以 default export 的形式引入
pkg.foo();  // 正常执行
로그인 후 복사

到 Github 获取示例代码(test1):

https://github.com/VaJoy/BlogDemo3/tree/main/220220/test1

具体原因我们会在后续提及。

1.3 CJS 引用 ESM 模块的问题

假设你在开发一个供别人使用的开源项目,且使用 ESM 的形式导出模块,那么问题来了 —— 目前 CJS 的 require 函数无法直接引入 ESM 包,会报错:

let { foo } = require('./esm/b.js');
              ^

Error [ERR_REQUIRE_ESM]: require() of ES Module BlogDemo3\220220\test2\esm\b.js from BlogDemo3\220220\test2\require.js not supported.
Instead change the require of b.js in BlogDemo3\220220\test2\require.js to a dynamic import() which is available in all CommonJS modules.
    at Object.<anonymous> (BlogDemo3\220220\test2\require.js:4:15) {
  code: &#39;ERR_REQUIRE_ESM&#39;
}
로그인 후 복사

按照上述错误陈述,我们不能并使用 require 引入 ES 模块(原因会在后续提及),应当改为使用 CJS 模块内置的动态 import 方法:

import(&#39;./esm/b.js&#39;).then(({ foo }) => {
    foo();
});

// or

(async () => { 
    const { foo } = await import(&#39;./esm/b.js&#39;); 
})();
로그인 후 복사

到 Github 获取示例代码(test2):

https://github.com/VaJoy/BlogDemo3/tree/main/220220/test2

查阅 dynamic import 文档

https://v8.dev/features/dynamic-import#dynamic

开源项目当然不能强制要求用户改用这种形式来引入,所以又得借助 rollup 之类的工具将项目编译为 CJS 模块……


由上可见目前 Node.js 对 ESM 语法的支持是有限制的,如果不借助工具处理,这些限制可能会很糟心。

对于想入门前端的新手来说,这些麻烦的规则和限制也会让人困惑。

截至我落笔书写本文时, Node.js LTS 版本为 16.14.0,距离开始支持 ESM 的 13.2.0 版本已过去了两年多的时间。

那么为何 Node.js 到现在还无法打通 CJS 和 ESM?

答案并非 Node.js 敌视 ESM 标准从而迟迟不做优化,而是因为 —— CJS 和 ESM,二者真是太不一样了。

二、CJS 和 ESM 的不同点

2.1 不同的加载逻辑

在 CJS 模块中,require() 是一个同步接口,它会直接从磁盘(或网络)读取依赖模块并立即执行对应的脚本。

ESM 标准的模块加载器则完全不同,它读取到脚本后不会直接执行,而是会先进入编译阶段进行模块解析,检查模块上调用了 importexport 的地方,并顺腾摸瓜把依赖模块一个个异步、并行地下载下来。

在此阶段 ESM 加载器不会执行任何依赖模块代码,只会进行语法检错、确定模块的依赖关系、确定模块输入和输出的变量。

最后 ESM 会进入执行阶段,按顺序执行各模块脚本。

所以我们常常会说,CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

在上方 1.2 小节,我们曾提及到 ESM 中无法通过指定依赖模块属性的形式引入 CJS named exports:

/** @file cjs/a.js **/
// named exports
module.exports = {
    foo: () => {
        console.log("It&#39;s a foo function...")
    }
}

/** @file index_err.js **/
import { foo } from &#39;./cjs/a.js&#39;;  
// SyntaxError: Named export &#39;foo&#39; not found. The requested module &#39;./cjs/a.js&#39; is a CommonJS module, which may not support all module.exports as named exports.
foo();
로그인 후 복사

这是因为 ESM 获取所指定的依赖模块属性(花括号内部的属性),是需要在编译阶段进行静态分析的,而 CJS 的脚本要在执行阶段才能计算出它们的 named exports 的值,会导致 ESM 在编译阶段无法进行分析。

2.2 不同的模式

ESM 默认使用了严格模式(use strict),因此在 ES 模块中的 this 不再指向全局对象(而是 undefined),且变量在声明前无法使用。

这也是为何在浏览器中,<script> 标签如要启用原生引入 ES 模块能力,必须加上 type="module" 告知浏览器应当把它和常规 JS 区分开来处理。

查看 ESM 严格模式的更多限制:

https://es6.ruanyifeng.com/#docs/module#%E4%B8%A5%E6%A0%BC%E6%A8%A1%E5%BC%8F

2.3 ESM 支持“顶级 await”,但 CJS 不行。

ESM 支持顶级 awaittop-level await),即 ES 模块中,无须在 async 函数内部就能直接使用 await

// index.mjs
const { foo } = await import(&#39;./c.js&#39;);
foo();
로그인 후 복사

到 Github 获取示例代码(test3):

https://github.com/VaJoy/BlogDemo3/tree/main/220220/test3

在 CSJ 模块中是没有这种能力的(即使使用了动态的 import 接口),这也是为何 require 无法加载 ESM 的原因之一。

试想一下,一个 CJS 模块里的 require 加载器同步地加载了一个 ES 模块,该 ES 模块里异步地 import 了一个 CJS 模块,该 CJS 模块里又同步地去加载一个 ES 模块…… 这种复杂的嵌套逻辑处理起来会变得十分棘手。

查阅关于更多“如何实现 require 加载 ESM”的讨论:

https://github.com/nodejs/modules/issues/454

2.4 ESM 缺乏 __filename 和 __dirname

在 CJS 中,模块的执行需要用函数包起来,并指定一些常用的值:

  NativeModule.wrapper = [
    &#39;(function (exports, require, module, __filename, __dirname) { &#39;,
    &#39;\n});&#39;
  ];
로그인 후 복사

所以我们才可以在 CJS 模块里直接用 __filename__dirname

而 ESM 的标准中不包含这方面的实现,即无法在 Node 的 ESM 里使用 __filename__dirname

参考:https://github.com/nodejs/node/blob/v4.0.0/src/node.js#L932


从上方几点可以看出,在 Node.js 中,如果要把默认的 CJS 切换到 ESM,会存在巨大的兼容性问题。

这也是 Node.js 目前,甚至未来很长一段时间,都难以解决的一场模块规范持久战。

如果你希望不借助工具和规则,也能放宽心地使用 ESM,可以尝试使用 Deno 替代 Node,它默认采用了 ESM 作为模块规范(当然生态没有 Node 这么完善)。

三、借助工具实现 CJS、ESM 混写

借助构建工具可以实现 CJS 模块、ES 模块的混用,甚至可以在同一个模块同时混写两种规范的 API,让开发不再需要关心 Node.js 上面的限制。另外构建工具还能利用 ESM 在编译阶段静态解析的特性,实现 Tree-shaking 效果,减少冗余代码的输出。

这里我们以 rollup 为例,先做全局安装:

pnpm i -g rollup
로그인 후 복사

接着再安装 rollup-plugin-commonjs 插件,该插件可以让 rollup 支持引入 CJS 模块(rollup 本身是不支持引入 CJS 模块的):

pnpm i --save-dev @rollup/plugin-commonjs
로그인 후 복사

我们在项目根目录新建 rollup 配置文件 rollup.config.js

import commonjs from &#39;rollup-plugin-commonjs&#39;;

export default {
  input: &#39;index.js&#39;,  // 入口文件
  output: {
    file: &#39;bundle.js&#39;,  // 目标文件
    format: &#39;iife&#39;
  },
  plugins: [
    commonjs({
      transformMixedEsModules: true,
      sourceMap: false,
    })
  ]
};
로그인 후 복사

plugin-commonjs 默认会跳过所有含 import/export 的模块,如果要支持如 import + require 的混合写法,需要带 transformMixedEsModules 属性。

接着执行 rollup --config 指令,就能按照 rollup.config.js 进行编译和打包了。

示例

/** @file a.js **/
export let func = () => {
    console.log("It&#39;s an a-func...");
}

export let deadCode = () => {
    console.log("[a.js deadCode] Never been called here");
}


/** @file b.js **/
// named exports
module.exports = {
    func() {
        console.log("It&#39;s a b-func...")
    },
    deadCode() {
        console.log("[b.js deadCode] Never been called here");
    }
}


/** @file c.js **/
module.exports.func = () => {
    console.log("It&#39;s a c-func...")
};

module.exports.deadCode = () => {
    console.log("[c.js deadCode] Never been called here");
}


/** @file index.js **/
let a = require(&#39;./a&#39;);
import { func as bFunc } from &#39;./b.js&#39;;
import { func as cFunc } from &#39;./c.js&#39;;

a.func();
bFunc();
cFunc();
로그인 후 복사

到 Github 获取示例代码(test4):

https://github.com/VaJoy/BlogDemo3/tree/main/220220/test4

打包后的 bundle.js 文件如下:

(function () {
	&#39;use strict&#39;;

	function getAugmentedNamespace(n) {
		if (n.__esModule) return n;
		var a = Object.defineProperty({}, &#39;__esModule&#39;, {value: true});
		Object.keys(n).forEach(function (k) {
			var d = Object.getOwnPropertyDescriptor(n, k);
			Object.defineProperty(a, k, d.get ? d : {
				enumerable: true,
				get: function () {
					return n[k];
				}
			});
		});
		return a;
	}

	let func$1 = () => {
	    console.log("It&#39;s an a-func...");
	};

	let deadCode = () => {
	    console.log("[a.js deadCode] Never been called here");
	};

	var a$1 = /*#__PURE__*/Object.freeze({
		__proto__: null,
		func: func$1,
		deadCode: deadCode
	});

	var require$$0 = /*@__PURE__*/getAugmentedNamespace(a$1);

	var b = {
	    func() {
	        console.log("It&#39;s a b-func...");
	    },
	    deadCode() {
	        console.log("[b.js deadCode] Never been called here");
	    }
	};

	var func = () => {
	    console.log("It&#39;s a c-func...");
	};

	let a = require$$0;

	a.func();
	b.func();
	func();

})();
로그인 후 복사

可以看到,rollup 通过 Tree-shaking 移除掉了从未被调用过的 c 模块的 deadCode 方法,但 a、b 两模块中的 deadCode 代码段未被移除,这是因为我们在引用 a.js 时使用了 require,在 b.js 中使用了 CJS named exports,这些都导致了 rollup 无法利用 ESM 的特性去做静态解析。

常规在开发项目时,还是建议尽量使用 ESM 的语法来书写全部模块,这样可以最大化地利用构建工具来减少最终构建文件的体积。


希望本文能为你提供帮助,共勉~

更多node相关知识,请访问:nodejs 教程

위 내용은 Node의 두 가지 모듈 사양인 CJS와 ESM에 대해 이야기해 보겠습니다.의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

Video Face Swap

Video Face Swap

완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

nvm에서 노드를 삭제하는 방법 nvm에서 노드를 삭제하는 방법 Dec 29, 2022 am 10:07 AM

nvm을 사용하여 노드를 삭제하는 방법: 1. "nvm-setup.zip"을 다운로드하여 C 드라이브에 설치합니다. 2. "nvm -v" 명령을 통해 환경 변수를 구성하고 버전 번호를 확인합니다. install" 명령 노드 설치; 4. "nvm uninstall" 명령을 통해 설치된 노드를 삭제합니다.

Express를 사용하여 노드 프로젝트에서 파일 업로드를 처리하는 방법 Express를 사용하여 노드 프로젝트에서 파일 업로드를 처리하는 방법 Mar 28, 2023 pm 07:28 PM

파일 업로드를 처리하는 방법은 무엇입니까? 다음 글에서는 Express를 사용하여 노드 프로젝트에서 파일 업로드를 처리하는 방법을 소개하겠습니다. 도움이 되길 바랍니다.

Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명 Node 서비스의 Docker 미러링을 수행하는 방법은 무엇입니까? 극한 최적화에 대한 자세한 설명 Oct 19, 2022 pm 07:38 PM

이 기간 동안 저는 Tencent 문서의 모든 카테고리에 공통되는 HTML 동적 서비스를 개발 중입니다. 다양한 카테고리에 대한 액세스 생성 및 배포를 촉진하고 클라우드로 이동하는 추세에 부응하기 위해. Docker를 사용하여 서비스 콘텐츠를 수정하고 제품 버전을 균일하게 관리합니다. 이 글에서는 제가 Docker를 서비스하면서 쌓은 최적화 경험을 여러분의 참고용으로 공유하겠습니다.

PI 노드 교육 : PI 노드 란 무엇입니까? Pi 노드를 설치하고 설정하는 방법은 무엇입니까? PI 노드 교육 : PI 노드 란 무엇입니까? Pi 노드를 설치하고 설정하는 방법은 무엇입니까? Mar 05, 2025 pm 05:57 PM

Pinetwork 노드에 대한 자세한 설명 및 설치 안내서이 기사에서는 Pinetwork Ecosystem을 자세히 소개합니다. Pi 노드, Pinetwork 생태계의 주요 역할을 수행하고 설치 및 구성을위한 전체 단계를 제공합니다. Pinetwork 블록 체인 테스트 네트워크가 출시 된 후, PI 노드는 다가오는 주요 네트워크 릴리스를 준비하여 테스트에 적극적으로 참여하는 많은 개척자들의 중요한 부분이되었습니다. 아직 Pinetwork를 모른다면 Picoin이 무엇인지 참조하십시오. 리스팅 가격은 얼마입니까? PI 사용, 광업 및 보안 분석. Pinetwork 란 무엇입니까? Pinetwork 프로젝트는 2019 년에 시작되었으며 독점적 인 Cryptocurrency Pi Coin을 소유하고 있습니다. 이 프로젝트는 모든 사람이 참여할 수있는 사람을 만드는 것을 목표로합니다.

Node의 프로세스 관리 도구 'pm2”에 대한 심층 분석 Node의 프로세스 관리 도구 'pm2”에 대한 심층 분석 Apr 03, 2023 pm 06:02 PM

이 기사에서는 Node의 프로세스 관리 도구인 "pm2"를 공유하고 pm2가 필요한 이유, pm2 설치 및 사용 방법에 대해 설명합니다. 모두에게 도움이 되기를 바랍니다!

npm node gyp가 실패하는 경우 수행할 작업 npm node gyp가 실패하는 경우 수행할 작업 Dec 29, 2022 pm 02:42 PM

"node-gyp.js"와 "Node.js"의 버전이 일치하지 않아 npm node gyp가 실패했습니다. 해결 방법: 1. "npm 캐시 clean -f"를 통해 노드 캐시를 지웁니다. 2. "npm install - g n" n 모듈을 설치합니다. 3. "n v12.21.0" 명령을 통해 "node v12.21.0" 버전을 설치합니다.

pkg를 사용하여 Node.js 프로젝트를 실행 파일로 패키징하는 방법에 대해 이야기해 보겠습니다. pkg를 사용하여 Node.js 프로젝트를 실행 파일로 패키징하는 방법에 대해 이야기해 보겠습니다. Dec 02, 2022 pm 09:06 PM

nodejs 실행 파일을 pkg로 패키징하는 방법은 무엇입니까? 다음 기사에서는 pkg를 사용하여 Node 프로젝트를 실행 파일로 패키징하는 방법을 소개합니다. 도움이 되기를 바랍니다.

Angular 및 Node를 사용한 토큰 기반 인증 Angular 및 Node를 사용한 토큰 기반 인증 Sep 01, 2023 pm 02:01 PM

인증은 모든 웹 애플리케이션에서 가장 중요한 부분 중 하나입니다. 이 튜토리얼에서는 토큰 기반 인증 시스템과 기존 로그인 시스템과의 차이점에 대해 설명합니다. 이 튜토리얼이 끝나면 Angular와 Node.js로 작성된 완벽하게 작동하는 데모를 볼 수 있습니다. 기존 인증 시스템 토큰 기반 인증 시스템으로 넘어가기 전에 기존 인증 시스템을 살펴보겠습니다. 사용자는 로그인 양식에 사용자 이름과 비밀번호를 입력하고 로그인을 클릭합니다. 요청한 후 데이터베이스를 쿼리하여 백엔드에서 사용자를 인증합니다. 요청이 유효하면 데이터베이스에서 얻은 사용자 정보를 이용하여 세션을 생성하고, 세션 정보를 응답 헤더에 반환하여 브라우저에 세션 ID를 저장한다. 다음과 같은 애플리케이션에 대한 액세스를 제공합니다.

See all articles