# 背景
早期 js
是没有模块化的概念的,直到 nodejs 诞生,才把模块系统引入 js
。 nodejs
使用的是 CJS(Commonjs)
规范。而 js 语言标准的模块规范是 ESM(Ecmascript Module)
。
# CJS
# CJS 导出
// util.cjs | |
function add(a, b) { | |
return a + b; | |
} | |
module.exports = { add }; |
# CJS 导入
//index.cjs | |
const { add } = require("./util.cjs"); | |
async function foo() { | |
console.log(add(1, 2)); | |
} | |
foo(); |
关键词:
module,exports,global,require
特点:
- 模块加载
require
就是代码执行 - 所有代码运行在模块作用域中,不污染全局
- 模块可以多次加载,但是只在第一次加载运行,后面是缓存
适用场景:
Nodejs
,浏览器端需要用其他打包工具支持
注意事项:
export.xxx
不能和 module.exports
一起使用
# ESM
# ESM 导出
// util.mjs | |
export function add(a, b) { | |
return a + b; | |
} |
# ESM 导入
//index.mjs | |
import { add } from "./util.mjs"; | |
export function foo() { | |
console.log(add(1, 2)); | |
} | |
foo(); |
关键词:
export import
特点:
- 异步加载,利用了浏览器原生的解析能力,代码体积更小
- 模块内自动采用严格模式
- 模块中的
this
指向并不是window
或global
,而是undefined
适用场景:
结合其他打包工具 (webpack)
使用或浏览器 <script type="module">
标签包裹
# ESM 和 CJS 的区别
# 模块依赖关系引入时间
可以理解为引入的模块内的代码执行的时间, ESM
发生在编译阶段,模块的引入必须生命在文件头部,且路径名不能动态制定,
CJS
发生在执行阶段,且模块路径可以通过字符串拼接等运算动态指定
// module.js | |
console.log('done') | |
//cjs 导出 | |
exports.name = 'cjs' | |
//esm 导出 | |
export const name ='esm' | |
// index.js | |
//esm 写法 | |
import {name} from './module' | |
document.querySelector('#btn').addEventListener('click', () => { | |
console.log(name, '>>>>') | |
}) | |
//cjs 写法 | |
document.querySelector('#btn').addEventListener('click', () => { | |
const name = require('./module.js') | |
console.log(name, '>>>>') | |
}) |
如上,当我们使用 ESM
写法时候,打印的 done
会在没做任何操作的时候就输出,而 CJS
写法只有在点击按钮的时候才会输出
# 值复制与映射
在 CJS
中我们导入的值是一个导出值的副本,而在 ESM
中我们获取到的是一个映射
// module.js
let name = 'cjs'
module.exports = {
name,
changeName(value) {
name = value;
return name
}
}
// index.js
let { name, changeName } = require('./module')
console.log(name) // 'cjs'
changeName('changed')
console.log(name) // 'cjs'
name = 'changed'
console.log(name) // 'changed'
而当我们用 ems 的时候情况就会有所变化
// module.js | |
let name = 'cjs' | |
function changeName(value) { | |
name = value | |
} | |
export { | |
name, | |
changeName | |
} | |
// index.js | |
import { name, changeName } from './module' | |
console.log(name) // 'esm' | |
changeName('changed') | |
console.log(name) // 'changed' | |
name = 'esm' | |
console.log(name) // 'esm' |
# 互相导入
ESM
模块导入可以直接引入ESM
模块CJS
模块导入可以直接引入CJS
模块CJS
模块导入可以直接引入ESM
模块。ESM
模块导入不可以直接引入CJS
模块
// index.cjs | |
const { add } = require("./util.mjs"); | |
async function foo() { | |
console.log(add(1, 2)); | |
} | |
foo(); | |
// index.mjs | |
import { add } from "./util.cjs"; | |
export function foo() { | |
console.log(add(1, 2)); | |
} | |
foo(); | |
// util.cjs | |
function add(a, b) { | |
return a + b; | |
} | |
module.exports = { add }; | |
// util.mjs | |
export function add(a, b) { | |
return a + b; | |
} |
上面例子的结果, index.cjs
可以正常运行, index.mjs
报错:
node:internal/modules/cjs/loader:1072 | |
throw new ERR_REQUIRE_ESM(filename, true); | |
^ | |
Error [ERR_REQUIRE_ESM]: require() of ES Module C:\Users\suyu_\Desktop\test\utils.mjs not supported. | |
Instead change the require of C:\Users\suyu_\Desktop\test\utils.mjs to a dynamic import() which is available in all CommonJS modules. | |
at Object.<anonymous> (C:\Users\suyu_\Desktop\test\index.cjs:2:17) { | |
code: 'ERR_REQUIRE_ESM' | |
} |
一种解决方案是:
async function foo() { | |
const { add } = await import("./util.mjs"); | |
console.log(add(1, 2)); | |
} | |
foo(); |
前提条件是在 CJS 中导入 ESM 的时候,必须要有异步的执行环境,否则是无法导入