ESM 和 CJS的互操作性

293 阅读3分钟

ESM 和 CJS 模块在前端模块引用中存在互操作性,总结一下ESM 和CJS 模块如何相互引用。

1. ESM模块 引入CJS

  1. 具名引入 默认引入 import
  2. 动态引入 import('xxx').then((res)=>res.default)

import 引入 cjs 模块

// cjs.cjs commonjs 模块
module.exports.bb = "hh";
module.exports = {
  test: "ooooooooo",
  world: function () {
    console.log("world");
  },
};

这里后面的module.exports 导出 会默认覆盖之前的 module.exports 导出 导致之前导出的bb 为undefined 。

使用es module 导入 cjs 模块时 导入的是默认最后一个module.exports 导出的值

import cjs,{bb} from './cjs.cjs'
// const bb = require('./cjs.js')
console.log(cjs);
console.log(bb);

image.png

使用esmodule 默认导入的是 module.exports 导出的值 且无法实现具名导入cjs 模块 只能使用默认导入的方式导入cjs模块。

如果想要实现 具名导出 和默认导出都可以访问到的情况 使用exports.xxx 属性和exports.default 属性 可以实现

exports.bb = "hh";
exports.default = {
  test: "ooooooooo",
  world: function () {
    console.log("world");
  },
};

esm 模块引入上述cjs模块

import cjs from './cjs.cjs'
console.log(cjs);

result:

image.png

该处的esm模块 默认引入 可以同时访问到 cjs模块的具名导出 和默认导出。 也可以使用具名引入 引入cjs 模块的具体某一个属性

import cjs,{bb} from './cjs.cjs'
// console.log(cjs);
console.log(bb);

image.png

也可以使用动态import的方式导入cjs 模块

import('./cjs.cjs').then((res)=>{
    console.log(res,'cjs模块');
})

image.png 可以看到 通过这种方式引入进来的cjs 模块 默认的所有属性都绑定在default属性上

import('./cjs.cjs').then((res)=>{
    console.log(res.default,'cjs模块');
})

image.png

注意:

exportsmodule.exports的别名 在cjs模块的导出中 以 module.exports的导出为准

eg: exports 的覆盖导出:

exports = {
    xxx
}

eg: module.exports 的覆盖导出


module.exports  = {
    xxx
}

module.exports的覆盖导出可以覆盖exports的导出 ,但是exports的覆盖导出无法覆盖module.exports的导出

具体以module.exports的导出为准

eg:

// cjs.cjs
exports.bb = "hh";
exports.default = {
  test: "ooooooooo",
  world: function () {
    console.log("world");
  },
};

module.exports = {
  b:400
}

esm导入:

import('./cjs.cjs').then((res)=>{
    // console.log(res,'cjs模块');
    console.log(res.default,'cjs模块2');
})

image.png

eg2: exports 的覆盖导出 无法覆盖module.exports.xxx 的导出。

exports = {
  aa:8888
}

module.exports.bb = "module hh";
module.exports.ff = {
  test: "module ooooooooo",
  moduleworld: function () {
    console.log("module world");
  },
};

esm导入:

import('./cjs.cjs').then((res)=>{
    // console.log(res,'cjs模块');
    console.log(res.default,'cjs模块2');
})

image.png

因此实际业务中尽量使用module.exports导出

2. CJS中如何导入ESM模块

  • 动态import的方式 import('xxxx').then((xxx)=>xxx)
  • 或者 await的方式引入

由于require只能引入同步模块 cjs模块 不能引入esm 异步模块 因此使用如下方式引用 必须修改文件名为.mjs 后缀结尾的 标志这个文件是一个esmodule文件 或者 单独开一个文件夹 初始化package.json文件 标识 type:'module' 这样也可以实现 将以.js后缀为结尾的文件识别为esmodule格式。

async function main() {
  const mod = await import('./esm.mjs');
  const { a, b, foo } = mod;
  console.log(a);
  console.log(b);
  console.log(foo());
}

main();

  • 使用IIFE 立即执行函数引入esm模块

type = 'module' 这里可以使用.js 后缀结尾文件标识

(async function main() {
  const mod = await import('./esm.js');
  const { a, b, foo } = mod;
  console.log(a);
  console.log(b);
  console.log(foo());
})()

最新Node23 版本 新增支持require esm 模块

// demo.mjs 模块
export const a  = '5555555555555555'

export default function TestEsm() {
    console.log('999999999999999999');
}
// test.cjs模块
const TestEsm = require('./demo.mjs')
console.log(TestEsm);

最终结果:

image.png

参考:

exports 和module.exports的导出差异

cjs和esm互相引入