本文已参与「新人创作礼」活动,一起开启掘金创作之路
本篇文章对应源码:github.com/Plasticine-…
在你写项目的时候,你是否有好奇过为什么可以在webpack
项目中同时使用import
和require
导入其他的包?也就是说webpack
是同时支持ES Module
和CommonJS
这两种模块化规范的(其他的模块化规范由于已经很少使用,因此不在本篇文章讨论范围内)
那么webpack
中是如何处理ES Module
和CommonJS
模块同时使用的问题的呢?这就是这篇文章要探讨的问题
1. 环境搭建
首先我们需要了解一下ESM(ES Module)
和CJS(CommonJS Module)
在webpack
中是如何被处理的,一个很自然的猜想:既然webpack
能够同时支持两种模块化规范,那肯定是它自己内部实现了一个模块化的方案,并且能够把ESM
和CJS
的模块化代码转成自己实现的模块化方案的代码
当然,这只是目前的猜想,那么webpack
内部到底是不是这样做的呢?为了探究清楚这个问题,我们先来搭建一下项目的目录结构
项目的入口在src/index.js
中,其代码如下
// ESM 方式加载 CJS 模块
import moduleEsCommon from './js/common-module';
moduleEsCommon.hello();
// ESM 方式加载 ESM 模块
import moduleEsEs from './js/es-module';
moduleEsEs.hello();
// CJS 方式加载 CJS 模块
const moduleCommonCommon = require('./js/common-module');
moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
const moduleCommonEs = require('./js/es-module');
moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
我们需要探讨四种情况:
- 在
CJS
中导入CJS
模块 - 在
ESM
中导入ESM
模块 - 在
CJS
中导入ESM
模块 - 在
ESM
中导入CJS
模块
src/js
目录下有两个js
文件,一个是CJS
模块,另一个是ESM
模块,然后我们在main.js
中导入它们,而其他没用到的模块则注释掉即可
// src/js/common-module.js
module.exports = {
hello() {
console.log(`I'm from common module.`);
},
};
// src/js/es-module.js
export default {
hello() {
console.log(`I'm from es module.`);
},
};
export const esmVar = 'hello esm';
export function esmFn() {
console.log('hello esm fn');
}
2. 修改mode为development
默认情况下,webpack
打包的mode
是production
,这样一来打包后的代码会被压缩和丑化
- 压缩:将所有空格换行去掉
- 丑化:将所有变量名替换成简短的无意义的变量名
这样的话不方便我们观察打包后的结果,因此可以修改mode
为development
,尽量让index.js
中的代码保持原样
除此之外,development
模式下,我们的业务代码是会被放进eval
中的,也不方便观察,这是因为development
模式下默认的devtool
是eval
,因此还需要将devtool
改成source-map
,关于webpack
的mode
和devtool
中的source-map
后面还会出文章详细讲,这里只是为了本篇文章的需要先配置环境,了解即可
因此目前的webpack.config.js
配置如下
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanPlugin } = require('webpack');
/**
* @type { import('webpack').Configuration }
*/
module.exports = {
mode: 'development',
devtool: 'source-map',
plugins: [new HtmlWebpackPlugin(), new CleanPlugin()],
};
3. CJS方式加载CJS模块
首先将index.js
中的其他模块注释掉,只保留CJS
加载CJS
模块的代码
// src/index.js
// ESM 方式加载 CJS 模块
// import moduleEsCommon from './js/common-module';
// moduleEsCommon.hello();
// ESM 方式加载 ESM 模块
// import moduleEsEs from './js/es-module';
// moduleEsEs.hello();
// CJS 方式加载 CJS 模块
const moduleCommonCommon = require('./js/common-module');
moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
// const moduleCommonEs = require('./js/es-module');
// moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
之后的情况也是类似的处理,就不赘述了
然后运行webpack
打包查看构建结果
// dist/main.js
(() => {
// index.js 中用到的所有模块会被放到 __webpack_modules__ 中
var __webpack_modules__ = {
// 每个模块都不是直接拿到的 而是放到一个函数中 执行后会将模块的内容挂载到 module.exports 中
'./src/js/common-module.js': (module) => {
module.exports = {
hello() {
console.log(`I'm from common module.`);
},
};
},
};
// 用于缓存
var __webpack_module_cache__ = {};
// 替代原生的 CommonJS 的 require
function __webpack_require__(moduleId) {
// 到缓存中去取模块
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
// 缓存中有模块则直接返回
return cachedModule.exports;
}
// 缓存中没有则添加到缓存中再返回
var module = (__webpack_module_cache__[moduleId] = {
// 双重赋值 module 和 缓存项 都指向 { exports: {} } 这一对象
exports: {},
});
// 在 __webpack_modules__ 中找到对应的模块后执行它得到模块的内容
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// 替代原生的 CommonJS 的 module.exports
var __webpack_exports__ = {};
// 入口文件中的代码
(() => {
// ESM 方式加载 CJS 模块
// import moduleEsCommon from './js/common-module';
// moduleEsCommon.hello();
// ESM 方式加载 ESM 模块
// import moduleEsEs from './js/es-module';
// moduleEsEs.hello();
// CJS 方式加载 CJS 模块
const moduleCommonCommon = __webpack_require__('./src/js/common-module.js');
moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
// const moduleCommonEs = require('./js/es-module');
// moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
})();
})();
打包的结果中是有webpack
的注释的,为了方便观察,我将这些注释都删除了
所有代码都被放到一个立即执行函数中执行,形成一个单独的作用域,防止变量污染
打包的结果中主要有一下几个部分:
__webpack_modules__
:在入口文件中用到的所有模块都会被放到这个对象中,key
是模块所在路径,value
是一个函数,模块中导出的内容并不是直接作为对象的value
,而是通过一个函数,执行该函数时会将导出的内容挂载到函数的参数module
中__webpack_module_cache__
:用作缓存,缓存存放的是对模块导出对象的引用,因此导出对象修改时,缓存中的相应部分也会修改__webpack_require__
:实现了类似CJS
的require
的功能,能够加载一个模块中导出的内容,首先会查找缓存,缓存中有则直接返回,缓存中没有时,则先创建一个有一个exports
属性的对象,然后根据moduleId
到__webpack_modules__
中找到相应的模块挂载函数后,执行挂载函数将模块的内容挂载到exports
属性中再返回__webpack_exports__
:实现类似CJS
的module.exports
的功能- 最后是一个立即执行函数,里面就是入口文件中的代码了,只是把其中的
require
替换成了webpack
实现的__webpack_require__
4. ESM方式加载ESM模块
(() => {
'use strict';
var __webpack_modules__ = ({
'./src/js/es-module.js': ((
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
// default 将会被放到 exports 对象中
default: () => __WEBPACK_DEFAULT_EXPORT__,
"esmFn": () => (/* binding */ esmFn),
"esmVar": () => (/* binding */ esmVar)
});
// 模块中的内容会作为 default 函数的返回值返回 因此要使用模块的内容需要这样:
// const foo = __webpack_require__('module-name');
// foo.default.hello(); -- default 现在是 foo 的 getter 会被自动调用,因此不需要我们手动调用
const __WEBPACK_DEFAULT_EXPORT__ = ({
hello() {
console.log(`I'm from es module.`);
},
});
const esmVar = 'hello esm';
function esmFn() {
console.log('hello esm fn');
}
}),
});
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
/**
* 遍历 definition 对象的每一个属性 如果在 exports 中不存在该属性时则将其添加进去
* @param {any} exports 存放模块导出内容的对象
* @param {any} definition
*/
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
// key 是属于 definition 且 不是 exports 的属性时
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
// 将 key 添加到 exports 中
Object.defineProperty(exports, key, {
enumerable: true,
// 设置 getter 代理 调用 definition 中对 key 的 getter 代理
get: definition[key],
});
}
}
};
})();
(() => {
/**
* 判断 prop 是否是属于 obj 自身的属性而不是原型链上的属性
* @param {any} obj 对象
* @param {string | Symbol} prop 属性名
*/
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
// 给 __webpack_require__ 函数对象添加一个 r 方法
/**
* 给 exports 对象添加一个 __esModule 属性 并且在支持 Symbol 的环境下还会添加一个额外的属性
* @param {any} exports 存放模块中导出内容的对象
*/
__webpack_require__.r = (exports) => {
// 判断是否支持 Symbol
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
// 支持 Symbol 时给 exports 对象添加一个属性 [Symbol.toStringTag]: 'Module'
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
// 添加一个属性 __esModule: true
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _js_es_module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
'./src/js/es-module.js'
);
// ESM 方式加载 CJS 模块
// import moduleEsCommon from './js/common-module';
// moduleEsCommon.hello();
// ESM 方式加载 ESM 模块
// 默认导出的内容被挂载到 default 属性中
_js_es_module__WEBPACK_IMPORTED_MODULE_0__['default'].hello();
console.log(_js_es_module__WEBPACK_IMPORTED_MODULE_0__.esmVar);
(0,_js_es_module__WEBPACK_IMPORTED_MODULE_0__.esmFn)();
// CJS 方式加载 CJS 模块
// const moduleCommonCommon = require('./js/common-module');
// moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
// const moduleCommonEs = require('./js/es-module');
// moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
})();
})();
相比于CJS
加载CJS
模块,多出了以下几点:
- 给
__webpack_require__
函数对象添加了三个方法r
、d
、o
,这三个方法的作用已在上面的代码中写明了注释 - 对于默认导出对象,会给
exports
添加一个default
属性,并且通过Object.defineProperty
的方式给default
属性设置了getter
,将模块中的默认导出的容挂载到default getter
的返回值中,这样在使用的时候需要对__webpack_require__
得到的模块再额外调用一个default
属性后才能访问到模块中的内容 - 普通导出的内容也是有相应的
getter
函数,但是没有挂载到default
属性中,因此还是可以直接调用
打包的结果中,对于esm
中的esmFn
的调用是这样调用的
(0,_js_es_module__WEBPACK_IMPORTED_MODULE_0__.esmFn)()
这种方式等价于直接调用esmFn
函数
5. CJS方式加载ESM模块
(() => {
var __webpack_modules__ = {
'./src/js/es-module.js': (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
'use strict';
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
const __WEBPACK_DEFAULT_EXPORT__ = {
hello() {
console.log(`I'm from es module.`);
},
};
},
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
// define __esModule on exports
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
// ESM 方式加载 CJS 模块
// import moduleEsCommon from './js/common-module';
// moduleEsCommon.hello();
// ESM 方式加载 ESM 模块
// import moduleEsEs from './js/es-module';
// moduleEsEs.hello();
// CJS 方式加载 CJS 模块
// const moduleCommonCommon = require('./js/common-module');
// moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
const moduleCommonEs = __webpack_require__('./src/js/es-module.js');
moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
})();
})();
可以看到,结果和ESM
方式加载ESM
模块是一样的,但是由于ESM
中的默认导出的内容是被挂载到default
中的,因此CJS
的方式加载到的ESM
模块要想使用默认导出的内容时还是需要先调用default
属性才能访问ESM
中默认导出的内容
6. ESM方式加载CJS模块
(() => {
var __webpack_modules__ = {
'./src/js/common-module.js': (module) => {
module.exports = {
hello() {
console.log(`I'm from common module.`);
},
};
},
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
// 多了一个判断是 module 是否是 esm 的特殊处理
// esm 模块的默认导出的内容会放到 default 属性中 为了在外部都能够直接统一调用模块中的内容而不需要加上
// default 属性去调用 因此需要进行一下判断再去处理
(() => {
__webpack_require__.n = (module) => {
var getter =
module && module.__esModule ? () => module['default'] : () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();
(() => {
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
'use strict';
__webpack_require__.r(__webpack_exports__);
var _js_common_module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
'./src/js/common-module.js'
);
// 调用 __webpack_require__.n() 使得无论是 cjs 还是 esm 中的默认导出内容都能够直接被调用
// 而不需要添加 default 属性去调用
var _js_common_module__WEBPACK_IMPORTED_MODULE_0___default =
__webpack_require__.n(_js_common_module__WEBPACK_IMPORTED_MODULE_0__);
// ESM 方式加载 CJS 模块
_js_common_module__WEBPACK_IMPORTED_MODULE_0___default().hello();
// ESM 方式加载 ESM 模块
// import moduleEsEs from './js/es-module';
// moduleEsEs.hello();
// CJS 方式加载 CJS 模块
// const moduleCommonCommon = require('./js/common-module');
// moduleCommonCommon.hello();
// CJS 方式加载 ESM 模块
// const moduleCommonEs = require('./js/es-module');
// moduleCommonEs.default.hello(); // CJS 方式导入 ESM 模块时要加上 default
})();
})();
相比于前面的方式中,在__webpack_require__
函数对象上新增了一个n
方法,用于特殊处理esm
模块的默认导出内容,使得在外部调用的时候可以不需要带上.default
去调用
这也就能够理解为什么ESM
方式使用ESM
模块的默认导出内容时可以不需要先调用default
属性,而CJS
方式使用ESM
模块的默认导出内容时却需要了