iis服务器助手广告广告
返回顶部
首页 > 资讯 > 精选 >ESM与CJS互相转换怎么实现
  • 503
分享到

ESM与CJS互相转换怎么实现

2023-07-05 08:07:09 503人浏览 薄情痞子
摘要

本篇内容介绍了“ESM与Cjs互相转换怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!正文ESM 和 CJS 是我们常用的模块格式,两

本篇内容介绍了“ESM与Cjs互相转换怎么实现”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

正文

ESM 和 CJS 是我们常用的模块格式,两种模块系统具有不同的语法和加载机制。在项目中,我们可能会遇到 ESM 和 CJS 转换的场景:

  • ESM 引入只支持 CJS 的库

  • 开发 npm 库的时候,写 ESM 然后编译成 CJS。

ESM 转 CJS

ESM 转 CJS 的使用场景非常常见,例如:

  • npm 库,需要同时提供 ESM 和 CJS,供开发者自行选择使用。一般是用 ESM 开发,然后同时输出 ESM 和 CJS

  • 使用 ESM 进行开发,但最后由于兼容性、性能等原因,编译成 CJS 在线上运行。例如:利用 Vite、webpack 等构建工具进行开发 开发

各大工具,如 TSC、Babel、Vite、WEBpack、Rollup 等,都自带了 ESM 转 CJS 的能力。

export 的转换

  • 情况一,只有默认导出:

export default 666

Rollup 会转换成

modules.exports = 666

很好理解,modules.exports 导出的整个东西就是默认导出嘛

用 CJS 引用该模块的方式:

const lib = require('lib')console.log(lib)// 666
  • 情况二,只有命名导出:

export const a = 123export const b = 234

转换成

module.exports.a = 123module.exports.b = 234

命名导出用 module.exports.xxx 一个个导出就行

用 CJS 引用该模块的方式:

const {a, b} = require('lib')console.log(a, b)// 123 234
  • 情况三:默认导出和命名导出同时存在

export default 666export const a = 123export const b = 234

这时候会发现,前面两种情况的转换思路不能用了,你不能这样转换

modules.exports = 666module.exports.a = 123module.exports.b = 234

毕竟 modules.exports 不是对象,因此设置不了属性。

那莫得办法了,只能这样表示了:

  • module.exports.default 为默认导出

  • module.exports.xxx 其他为命名导出

为了跟前两种情况做区分,因此还要新增一个标记__esModule

于是就会编译成这样的代码:

+ Object.defineProperty(exports, '__esModule', { value: true })+ module.exports.default = 666- module.exports = 666module.exports.a = 123module.exports.b = 234

用 CJS 引用该模块的方式:

const lib = require('lib')console.log(lib.default, lib.a, lib.b)// 666 123 234

在这种情况下,必须要用 .default 访问默认导出

但这样子看起来非常的别扭,但是没有办法,混用默认导出和命名导出是有代价的。

为什么我们项目中,从来就遇到过该问题?

一般情况下,我们使用 ESM 写项目,然后编译成 CJS

假如,我们写的代码引用了上述的代码(默认导出和命名导出混用):

// foo.jsimport lib from 'lib'import {a, b} from 'lib'console.log(lib, a, b)

这段代码,会被转换成:

'use strict';var lib = require('lib');function _interopDefault (e) {   return e && e.__esModule ? e : { default: e }; }var lib__default = _interopDefault(lib);console.log(lib__default.default, lib.a, lib.b);

_interopDefault 函数会自动根据 __esModule,将导出对象标准化,使 .default 一定为默认导出

  • 如果有 __esModule,那就不用处理

  • 没有 __esModule,就将其放到 default 属性中,作为默认导出

ESM与CJS互相转换怎么实现

工具在转译 lib.js 的同时,也会转译引入它的 foo.js,会加上标准化 require 对象的逻辑。

我们的项目,在编译的时候,全部 ESM 模块都转为 CJS(不是只转换一个,不转另外一个) ,在这个过程中它自动屏蔽了模块默认导出的差异,由于编译工具已经帮我们处理好,因此我们没有任何感知。

如果我们直接写 CJS,去引入 ESM 转换后的 CJS,就需要自行处理该问题

要想尽量避免这种情况,建议全部都使用命名导出,由于没有默认导出,就不需要担心默认导出是 module.exports 还是 module.exports.default,都用以下方式进行引入即可:

const {a, b} = require('lib')

这样开发者在任何情况下都没有心智负担。

import 的转换

其实上一小节已经讲了

import lib from 'lib'import {a, b} from 'lib'console.log(lib, a, b)

会被转换成

'use strict';var lib = require('lib');function _interopDefault (e) {   return e && e.__esModule ? e : { default: e }; }var lib__default = _interopDefault(lib);console.log(lib__default.default, lib.a, lib.b);

加上 _interopDefault,屏蔽了不同情况下默认导出的差异,因此如果所有代码都是从 ESM 转 CJS,就不用担心默认导出的差异问题。

小结

其实 ESM 转 CJS,不同的工具的输出会稍微有些不同。以上是 Rollup 的的转换方式,个人认为这种更为简洁,而 TSC 的转换则更复杂。

不过这些工具的思路都是相同的,都遵守 __esModule 的约定,标记 __esModule 的模块默认导出是 .default

ESM 转 CJS 有哪些局限性?

存在以下情况可能无法进行转换:

  • 存在循环依赖

  • import.meta,这个特性只能在 ESM 中使用

CJS 转 ESM

CJS 转 ESM 的场景不多,一般不会用 CJS 写 npm 库然后输出 ESM;用 CJS 写的库,当时不会输出 ESM。新写的 npm 库,一般来说也是用 ESM 写。

因此一般只有写 ESM 项目,引入了一个只有 CJS 的库时,且编译出 ESM 时,才会用到 CJS 转 ESM。

为什么我们用 webpack 写 ESM,然后引入 CJS 的时候,基本上没遇到什么问题?

要运行 ESM 引入 CJS 的代码,有两种方式:

  • 把 ESM 转 CJS,然后运行 CJS

  • 把 CJS 转成 ESM,然后运行 ESM

因为 webpack 是前者,ESM 转 CJS 能够很好地进行转换。

CJS 转 ESM,没有一种统一的转换标准(相对来说,ESM 转 CJS 有 __esModule 约定),不同的工具和库,可能转换出来的结果是不一样的,可能会导致代码不兼容。

export 的转换

场景一:

module.exports = {    a: 3,    b: 4}

Rollup 会转换成

var lib = {    a: 3,    b: 4};export { lib as default };

module.exports 会被当做默认导出

而 esbuild 会这样转换

var __getOwnPropNames = Object.getOwnPropertyNames;var __commonJS = (cb, mod) => function __require() {  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;};var require_lib = __commonJS({  "src/cjs/lib.js"(exports, module) {    module.exports = {      a: 3,      b: 4    };  }});export default require_lib();

esbuild 会给代码包一层辅助函数,然后将代码搬过去就好了。好处是,这样编译工具就不需要考虑代码的真正意义,直接简单包一层即可

这种情况下,虽然 Rollup 和 esbuild 转换的代码不太相同,但代码的运行结果是相同的

场景二:

exports.c =123

Rollup 会转换成:

var lib = {};var c = lib.c =123;export { c, lib as default };

Rollup 会转换成默认导出和命名导出。

esbuild 则转换成:

var __getOwnPropNames = Object.getOwnPropertyNames;var __commonJS = (cb, mod) => function __require() {  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;};var require_lib = __commonJS({  "src/cjs/lib.js"(exports) {    exports.d = 666;  }});export default require_lib();

仍然是包一层辅助函数,但 esbuild 全部都当做默认导出

在这种情况下,Rollup 和 esbuild 转换的代码,其运行结果是不同的

场景三:

exports.d = 123module.exports = {    a: 3,    b: 4}exports.c =123

exports.d = 123 其实是无效的

Rollup 会编译成这样:

var libExports = {};var lib$1 = {  get exports(){ return libExports; },  set exports(v){ libExports = v; },};(function (module, exports) {exports.d =123;module.exports = {    a: 3,    b: 4};exports.c =123;} (lib$1, libExports));var lib = libExports;export { lib as default };

此时 Rollup 也会加上一层辅助函数

而 esbuild 仍然是加一层辅助函数

var __getOwnPropNames = Object.getOwnPropertyNames;var __commonJS = (cb, mod) => function __require() {  return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;};var require_lib = __commonJS({  "src/cjs/lib.js"(exports, module) {    exports.d = 666;    module.exports = {      a: 3,      b: 4    };    exports.c = 666;  }});export default require_lib();

辅助函数的好处之前也说了,不需要关注代码逻辑,可以看到,即使 exports.d = 666; 是一行无效语句,照样执行也是没有问题的,不需要先分析出代码的语义。

总体对比下来,esbuild 的处理还是相对简单的

require 的转换

const lib = require('./lib')const {c} = require('./lib')console.info(lib,c)

Rollup 转换成:

import require$$0 from './lib';const lib = require$$0;const {c} = require$$0;console.info(lib,c);

require 的转换比较简单,不管你解不解构,反正我就只有默认引入

而 esbuild。。。还不支持,干脆就报错了

ESM与CJS互相转换怎么实现

小结

为什么工具的转换结果是不同的?

CJS 转换成 ESM 是有歧义的

module.export.a = 123module.export.b = 345

等价于

module.export = {    a: 123,    b: 345,}

那么它是默认导出,还是命名导出呢?都行

本质上,是因为 CJS 只有一个导出方式,不确定它对应的是 ESM 的命名导出还是默认导出。

用一个形象点的例子就是,女朋友回了一句哦,但是你不知道女朋友是想说肯定的意思,还是表示无语的意思、还是其他别的意思。。。

对于 require

const {c} = require('./lib')

你说这个是默认导入呢?还是命名导入?好像也都行。。。

正是由于这个歧义,且没有一个标准去规范这个转换行为,因此不同工具的转换结果是不同的

CJS 转换成 ESM 有哪些局限性?

  • 不同工具的转换结果不同

  • CJS 模块可以使用 require.resolve 方法查找模块的路径,而 ESM 模块不可以

  • CJS 模块可以导入和导出非 javascript 文件,例如 JSON

  • CJS 在运行时导入导出,支持运行时改变导入导出的内容,以下代码是合法的:

module.exports.a = 123if( Date.now() % 2){    module.exports.b = 234}

由于没有统一的标准,CJS 转 ESM 的工具,相对来说少了很多,目前仅有少量工具能够进行转换,esbuildbabel-plugin-transfORM-commonjs@rollup/commonjs

有时候 Vite 使用一些 CJS 包不兼容,也是因为有些 CJS 转不了 ESM。但幸运的是,目前大部分常见的 npm 包,都已经支持 ESM,或者能够比较好的被转换成 ESM,因此也不需要太担心 Vite 的问题。

“ESM与CJS互相转换怎么实现”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!

--结束END--

本文标题: ESM与CJS互相转换怎么实现

本文链接: https://www.lsjlt.com/news/350675.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++ 生态系统中流行库和框架的贡献指南
    作为 c++++ 开发人员,通过遵循以下步骤即可为流行库和框架做出贡献:选择一个项目并熟悉其代码库。在 issue 跟踪器中寻找适合初学者的问题。创建一个新分支,实现修复并添加测试。提交...
    99+
    2024-05-15
    框架 c++ 流行库 git
  • C++ 生态系统中流行库和框架的社区支持情况
    c++++生态系统中流行库和框架的社区支持情况:boost:活跃的社区提供广泛的文档、教程和讨论区,确保持续的维护和更新。qt:庞大的社区提供丰富的文档、示例和论坛,积极参与开发和维护。...
    99+
    2024-05-15
    生态系统 社区支持 c++ overflow 标准库
  • c++中if elseif使用规则
    c++ 中 if-else if 语句的使用规则为:语法:if (条件1) { // 执行代码块 1} else if (条件 2) { // 执行代码块 2}// ...else ...
    99+
    2024-05-15
    c++
  • c++中的继承怎么写
    继承是一种允许类从现有类派生并访问其成员的强大机制。在 c++ 中,继承类型包括:单继承:一个子类从一个基类继承。多继承:一个子类从多个基类继承。层次继承:多个子类从同一个基类继承。多层...
    99+
    2024-05-15
    c++
  • c++中如何使用类和对象掌握目标
    在 c++ 中创建类和对象:使用 class 关键字定义类,包含数据成员和方法。使用对象名称和类名称创建对象。访问权限包括:公有、受保护和私有。数据成员是类的变量,每个对象拥有自己的副本...
    99+
    2024-05-15
    c++
  • c++中优先级是什么意思
    c++ 中的优先级规则:优先级高的操作符先执行,相同优先级的从左到右执行,括号可改变执行顺序。操作符优先级表包含从最高到最低的优先级列表,其中赋值运算符具有最低优先级。通过了解优先级,可...
    99+
    2024-05-15
    c++
  • c++中a+是什么意思
    c++ 中的 a+ 运算符表示自增运算符,用于将变量递增 1 并将结果存储在同一变量中。语法为 a++,用法包括循环和计数器。它可与后置递增运算符 ++a 交换使用,后者在表达式求值后递...
    99+
    2024-05-15
    c++
  • c++中a.b什么意思
    c++kquote>“a.b”表示对象“a”的成员“b”,用于访问对象成员,可用“对象名.成员名”的语法。它还可以用于访问嵌套成员,如“对象名.嵌套成员名.成员名”的语法。 c++...
    99+
    2024-05-15
    c++
  • C++ 并发编程库的优缺点
    c++++ 提供了多种并发编程库,满足不同场景下的需求。线程库 (std::thread) 易于使用但开销大;异步库 (std::async) 可异步执行任务,但 api 复杂;协程库 ...
    99+
    2024-05-15
    c++ 并发编程
  • 如何在 Golang 中备份数据库?
    在 golang 中备份数据库对于保护数据至关重要。可以使用标准库中的 database/sql 包,或第三方包如 github.com/go-sql-driver/mysql。具体步骤...
    99+
    2024-05-15
    golang 数据库备份 mysql git 标准库
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作