nodejs模块化cjs/esm区别与双模块机制详解
知识点
-
适合人群:对模块化规范已有一定对了解
- 对nodejs双模块感兴趣
- 对双包机制感兴趣
- 希望开发第三方库
-
nodejs模块化,ejs(commonJS模块化规范)和esm(ES Module模块化规范)的区别对比
- 这两种模块化规范网上充斥着大量教程,本文只做简单的使用
- 两种模块化规范对比
-
nodejs作为第三方包时,同时包含双模块
- 双模块使用
- 双模块的问题
- 双模块的解决方案
cjs(CommonJS)
- commonjs模块化规范:运行时加载,简单来说就是在js运行阶段(v8执行js时)通过同步的方法进行加载
- 1-cjs.cjs
module.exports = {
title: "博客",
author: {
uname: "xqv",
},
};
// 等价于上面的exports
/* exports.title = "博客";
exports.author = {
uname: "xqv",
}; */
- 1-main.cjs
const cjs = require("./1-cjs.cjs");
console.log(cjs); // { title: '博客', author: { uname: 'xqv' } }
// console.log(module.exports === exports); // true
// console.log(module.exports === this); // true
这里大家需要清楚
module.export和exports以及this实际上是一个引用,在nodejsrequire()源码中使用vm模块对引入的文件(1-cjs.cjs)进行同步加载后,根据导出内容作为返回结果,并将该内容放入缓存中;注意要点:
- require是同步操作
- require内部源码利用
vm原生模块的runInThisContext()方法执行(类似eval,只是具有明确上下文,不污染全局)并最终会拿到导出的内容- require导入过的内容都会进行缓存
- 缓存作用一:便于二次加载
- 缓存作用二:解决cjs循环引用问题
- 如果对require源码感兴趣,可以看看本菜鸡的这篇博客:nodejs cjs模块化加载源码
- 代码:上述内容GitHub代码
esm(ES Module)
-
ECMA TC39模块化官方规范
-
基本使用
- 2-mjs.mjs
// 命名导出 export default { title: "标题", author: { uname: "xqv", }, }; // 具名导出 /* export const title = "标题"; export const author = { author: { uname: "xqv", }, }; */- 2-main.mjs
// 命名导出 import mjs from "./2-mjs.mjs"; console.log(mjs); // { title: '标题', author: { uname: 'xqv' } } // 具名导出 // import { title, author } from "./2-mjs.mjs"; // console.log(title, author); // 标题 { author: { uname: 'xqv' } }
-
更多导入导出规则查看MDN
-
代码:上述内容GitHub代码
cjs与esm区别
结论
- esm可以引入cjs模块,但cjs模块无法直接通过require esm模块(直接导入会报错)
- esm下的
export才是引用,而cjs和esm的export default都是值拷贝 - esm和cjs运行时期不一致,esm在编译期(当然也有
import()运行时导入,这里不做讨论),cjs在运行时且同步 - esm的this是undefined,cjs的this是exports是module.exports(默认{})
- esm编译器执行所以import导入有置顶效果
- esm可以做到tree sharking(清理未使用的多余代码,特别是对前端优化很有用,基本上webpack等打包工具利用了该特性)
在nodejs中还有更多的差异,这里只做主要的区别讲解,详细参考官方文档:nodejs官方文档:esm与cjs在nodejs中差异
cjs与esm之间互导
- esm导入cjs
- 1-module.cjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
module.exports = {
title,
author,
update,
};
- 1-main.mjs
import m from "./1-module.cjs";
console.log(m);
/*
{
title: '博客',
author: { uname: 'xiaoqinvar.' },
update: [Function: update]
}
*/
-
其中官网有一句话要注意:
- When importing CommonJS modules, the
module.exportsobject is provided as the default export - 当导入cjs模块时,module.exports是以export default形式导出的
- 🥕这正是export default是值拷贝,export是值引用的原因
- When importing CommonJS modules, the
esm可以直接导入cjs模块,但是是作为export default导出
代码:Github代码片段
- cjs导入esm
- 2-module.mjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
export default {
title,
author,
update,
};
- 2-main.cjs
// ❌ ERR_REQUIRE_ESM
// const m = require("./2-module.mjs");
// ✅
import("./2-module.mjs").then((m) => {
console.log(m);
/*
[Module: null prototype] {
default: {
title: '博客',
author: { uname: 'xiaoqinvar.' },
update: [Function: update]
}
}
*/
});
无法使用默认require()导入,可以使用import()动态导入
代码:Github代码片段
cjs值拷贝与esm值引用
- esm的
export是值引用(注意是export)
- 3-export.mjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
export { title, author, update };
- 3-import.mjs
import { title, author, update } from "./3-export.mjs";
console.log(title, author); // 博客 { uname: 'xiaoqinvar.' }
update();
console.log(title, author); // update title. { uname: 'aka. xqv yo.' }
- cjs值拷贝
- 3-module.export.cjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
module.exports = { title, author, update };
- 3-require.cjs
const m = require("./3-module.export.cjs");
console.log(m.title, m.author); // 博客 { uname: 'xiaoqinvar.' }
m.update();
console.log(m.title, m.author); // 博客 { uname: 'xiaoqinvar.' }
- cjs值拷贝内存图分析
- esm的export default是值的拷贝等价于module.export效果
- 3-export-default.mjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
export default { title, author, update };
- 3-import.mjs
import m from "./3-export-default.mjs";
const { title, author, update } = m;
console.log(title, author); // 博客 { uname: 'xiaoqinvar.' }
update();
console.log(title, author); // 博客 { uname: 'aka. xqv yo.' }
注意这里
title字段是值拷贝,所以没有变,走的cjs那一套
js后缀mjs和cjs的含义
-
以文件名后缀确定模块环境(权重最高🚀)
cjs:遵循cjs模块化规范mjs:遵循esm模块化规范
-
项目package.json确定模块环境:最近的
package.json文件包含值为"module": "type"字段时,扩展名为.js的文件为mjs规范,反之cjs规范
{
"type": "module"
}
确定为一种模块化后,对于另一种模块化就不可以乱用了;比如:
main.cjs你就不能import x from "..."
双包机制: "exports"字段
- 在node 12-,之前都是"main"字段决定了唯一的入口,如果是一个前后端都可以的运行时,如
loadsh库,就需要解决cjs规范和esm的双包导出问题;之前采用的是"main"作为cjs入口,"module"作为esm入口,因为package.json根本没有"module"字段,所以该字段会被node忽略,而打包工具如webpack会依赖该字段作为项目的入口
- 问题点:当时nodejs没办法一个包同时提供多个模块规范的入口,这正是"exports"要解决的问题,双包问题!
- 基本使用:这里只做简单讲解,这玩意儿规则还是蛮多的,有兴趣或者碰到了查看官方文档即可
// package.json
{
"type": "module", // 该项目默认js走esm规范
"exports": {
"import": "./index-module.js", // esm导入这个入口文件
"require": "./index-require.cjs", // cjs...
"default": "./index-require.cjs", // 兜底用
// "node": "..." 匹配任何 Node.js 环境
// 规则还是比较多的,有兴趣可以查看官方文档
}
}
双包机制的危害与注意事项
双包危害:引入cjs规范和esm规范返回的是不同的东西,简单来讲如果我们写的包会创建一个对象的话,cjs创建了一个对象,esm也会创建一个对象,这样就是多例问题了
官方文档:nodejs官方文档:双包危害
- 场景一:此包使用cjs编写,但是不想用esm重构,但希望能提供给esm用户使用
- 优点:简单、兼容、单例
- 缺点:cjs规则值拷贝问题(这里可以导出一个构造函数,通过创建堆内存对象来解决值拷贝的问题)
- 4-core.cjs
let title = "博客";
const author = {
uname: "xiaoqinvar.",
};
function update() {
title = "update title.";
author.uname = "aka. xqv yo.";
}
module.exports = { title, author, update };
- 4-esm.mjs
import core from "./4-core.cjs"; // esm拿cjs走
export default core;
- 4-main.mjs
import cjs from "./4-core.cjs";
import esm from "./4-esm.mjs";
console.log(cjs === esm); // true
console.log("cjs", cjs.title, esm.author); // cjs 博客 { uname: 'xiaoqinvar.' }
console.log("esm", esm.title, esm.author); // esm 博客 { uname: 'xiaoqinvar.' }
cjs.update();
console.log("cjs", cjs.title, esm.author); // cjs 博客 { uname: 'aka. xqv yo.' }
console.log("esm", esm.title, esm.author); // esm 博客 { uname: 'aka. xqv yo.' }
这里因为源码是cjs规则,所以整条链路下来都是走cjs规则的,cjs本身就是值拷贝所以
title字段没变,而author引用类型自然会变单例模式导出在Github仓库代码:看"5-*"开头的代码文件
官方减少双包危害(我觉得它这里说的特别稀里糊涂,还是要自己多思考一下):nodejs官方文档:减小双包危害