在上一节中我们初步了解了webpack
可以利用内置静态模块类型(asset module type
)来处理资源文件,我们所知道的本地服务,资源的压缩,代码分割,在webpack
构建的工程中有一个比较显著的特征是,模块化,要么commonjs
要么esModule
,在开发环境我们都是基于这两种,那么通过webpack
打包后,如何让其支持浏览器能正常的加载两种不同的模式呢?
接下来我们一起来探讨下webpack
中打包后代码的原理
正文开始...
初始化基础项目
新建一个文件夹webpack-05-module
,
npm init -y
我们安装项目一些基础支持的插件
npm i webpack webpack-cli webpack-dev-server html-webpack-plugin babel-loader @babel
l/core -D
在根目录新建webpack.config.js
,配置相关参数,为了测试 webpack 打包cjs
与esModule
我在entry
写了两个入口文件,并且设置mode:development
与devtool: 'source-map'
,设置source-map
是为了更好的查看源代码
code
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
entry: {
cjs: './src/commonjs_index.js',
esjs: './src/esmodule_index.js'
},
devtool: 'source-map',
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[name][ext]'
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
options: {
presets: ['@babel/env']
}
},
{
test: /\.(png|jpg)$/i,
type: 'asset/resource'
// generator: {
// // filename: 'images/[name][ext]',
// publicPath: '/assets/images/'
// }
}
]
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
template: './public/index.html'
})
]
};
在src
目录下新建commonjs_index.js
, esmodule_index.js
文件
commonjs_index.js
// commonjs_index.js
const { twoSum } = require('./utils/common.js');
import imgSrc from './assets/images/1.png';
console.log('cm_sum=' + twoSum(1, 2));
const domApp = document.getElementById('app');
var img = new Image();
img.src = imgSrc;
domApp.appendChild(img);
引入的common.js
// utils/common.js
function twoSum(a, b) {
return a + b;
}
module.exports = {
twoSum
};
esmodule_index.js
// esmodule_index.js
import twoSumMul from './utils/esmodule.js';
console.log('es_sum=' + twoSumMul(2, 2));
引入的esmodule.js
// utils/esmodule.js
function twoSumMul(a, b) {
return a * b;
}
// esModule
export default twoSumMul;
当我们运行npm run build
命令,会在根目录dist/js
文件夹下打包入口指定的两个文件
webpack 打包 cjs 最终代码
我把对应注释去掉后就是下面这样的
code
// cjs.js
(() => {
var __webpack_modules__ = {
'./src/utils/common.js': (module) => {
function twoSum(a, b) {
return a + b;
}
module.exports = {
twoSum: twoSum
};
},
'./src/assets/images/1.png': (module, __unused_webpack_exports, __webpack_require__) => {
'use strict';
module.exports = __webpack_require__.p + 'images/1.png';
}
};
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__ "moduleId");
return module.exports;
}
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName('script');
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}
if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
scriptUrl = scriptUrl
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\/[^\/]+$/, '/');
__webpack_require__.p = scriptUrl + '../';
})();
var __webpack_exports__ = {};
(() => {
'use strict';
__webpack_require__.r(__webpack_exports__);
var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
twoSum = _require.twoSum;
console.log('cm_sum=' + twoSum(1, 2));
var domApp = document.getElementById('app');
var img = new Image();
img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
domApp.appendChild(img);
})();
})();
初次看,感觉webpack
打包cjs
的代码太长了,但是删除掉注释后,我们仔细分析发现,并没有那么复杂
首先是该模块采用 IFEE 模式,一个匿名的自定义自行函数内包裹了几大块区域
1、初始化定义了 webpack 依赖的模块
code
var __webpack_modules__ = {
'./src/utils/common.js': (module) => {
function twoSum(a, b) {
return a + b;
}
// 当在执行时,返回这个具体函数体内容
module.exports = {
twoSum: twoSum
};
},
'./src/assets/images/1.png': (module, __unused_webpack_exports, __webpack_require__) => {
'use strict';
// 每一个对应的模块对应的内容
module.exports = __webpack_require__.p + 'images/1.png';
}
};
我们发现webpack
是用模块引入的路径当成key
,然后value
就是一个函数,函数体内就是引入的具体代码内容,并且内部传入了一个形参module
,实际上这个module
就是为{exports: {}}
定义的对象,把内部函数twoSum
绑定了在对象上
2、调用模块优先从缓存对象模块取值
code
var __webpack_module_cache__ = {};
// moduleId 就是引入的路径
function __webpack_require__(moduleId) {
// 根据moduleId优先从缓存中获取__webpack_modules__中绑定的值 {twoSum: TwoSum}
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 传入__webpack_modules__内部value的形参 module
var module = (__webpack_module_cache__[moduleId] = {
exports: {}
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
// 根据moduleId依次返回 {twoSum: twoSum}、__webpack_require__.p + 'images/1.png‘图片路径
return module.exports;
}
__webpack_require__.p
图片地址
3、绑定全局对象,引入图片的资源路径,主要是code
(() => {
__webpack_require__.g = (function () {
if (typeof globalThis === 'object') return globalThis;
try {
return this || new Function('return this')();
} catch (e) {
if (typeof window === 'object') return window;
}
})();
})();
(() => {
var scriptUrl;
if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + '';
var document = __webpack_require__.g.document;
if (!scriptUrl && document) {
if (document.currentScript) scriptUrl = document.currentScript.src;
if (!scriptUrl) {
var scripts = document.getElementsByTagName('script');
if (scripts.length) scriptUrl = scripts[scripts.length - 1].src;
}
}
if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser');
scriptUrl = scriptUrl
.replace(/#.*$/, '')
.replace(/\?.*$/, '')
.replace(/\/[^\/]+$/, '/');
// 获取图片路径
__webpack_require__.p = scriptUrl + '../';
})();
esModule
转换,用Object.defineProperty
拦截exports
(module.exports)对象添加__esModule
属性
4、将(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
})();
__webpack_require__(moduleId)
执行获取对应的内容
5、code
var __webpack_exports__ = {};
(() => {
'use strict';
// 在步骤4中做对象拦截,添加__esMoules属性
__webpack_require__.r(__webpack_exports__);
//根据路径获取对应module.exports的内容也就是__webpack_require__中的module.exports对象的数据
var _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./assets/images/1.png */ './src/assets/images/1.png');
var _require = __webpack_require__(/*! ./utils/common.js */ './src/utils/common.js'),
twoSum = _require.twoSum;
console.log('cm_sum=' + twoSum(1, 2));
var domApp = document.getElementById('app');
var img = new Image();
img.src = _assets_images_1_png__WEBPACK_IMPORTED_MODULE_0__;
domApp.appendChild(img);
})();
})();
webpack 打包 esModule 最终代码
我们看下具体代码
code
// esjs.js
(() => {
// webpackBootstrap
'use strict';
var __webpack_modules__ = {
'./src/utils/esmodule.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
function twoSumMul(a, b) {
return a * b;
}
const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__
});
}
};
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
});
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
// Return the exports of the module
return module.exports;
}
(() => {
// define getter functions for harmony 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/runtime/hasOwnProperty shorthand */
(() => {
__webpack_require__.o = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
})();
/* webpack/runtime/make namespace object */
(() => {
// 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__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils/esmodule.js */ './src/utils/esmodule.js');
console.log('es_sum=' + (0, _utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__['default'])(2, 2));
})();
})();
看着代码似乎与cjs
大体差不多,事实上有些不一样
当我们执行_utils_esmodule_js__WEBPACK_IMPORTED_MODULE_0__
这个方法时,实际会在__webpack_modules__
方法会根据moduleId
执行 value 值的函数体,而函数体会被__webpack_require__.d
这个方法进行拦截,会执行 Object.defineProperty
的get
方法,返回绑定在__webpack_exports__
对象的值上
主要看以下两段代码
var __webpack_modules__ = {
'./src/utils/esmodule.js': (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 这里定义模块时就已经先进行了拦截,这里与cjs有很大的区别
__webpack_require__.r(__webpack_exports__);
function twoSumMul(a, b) {
return a * b;
}
const __WEBPACK_DEFAULT_EXPORT__ = twoSumMul;
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__
});
}
};
...
(() => {
// define getter functions for harmony 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 转换esModule
代码中,同样会是有优先从缓存对象中获取,通过调用 __webpack_modules__[moduleId](module, module.exports, __webpack_require__ "moduleId");
这个方法,改变module.exports
根据moduleId
获取函数体内的值twoSumMul
函数
最后画了一张简易的图,文字理解还是有点多,纸上得来终学浅,绝知此事要躬行,还是得写个简单的demo
自己深深体会下,具体参考文末的code example
总结
webpack 打包
cjs
与esModule
的区别,本质上就是为了在浏览器支持 webpack 中使用export default {}
与module.exports
在浏览器定义了一个全局变量__webpack_modules__
根据引入的模块路径变成key
,value
就是在webpack
中的cjs
或者esModule
中函数体。当我们在
cjs
使用require('/path')
、或者在esModule
中使用import xx from '/path'
时,实际上webpack
把require
orimport
转变成了一个定义的函数__webpack_require__('moduleId')
的可执行函数。cjs
是在执行__webpack_require__.r(__webpack_exports__)
是就已经预先将__webpack_require__
返回的函数体内容进行了绑定,只有在执行_webpack_require__(/*! ./utils/common.js */ './src/utils/common.js')
返回函数体,本质上就是在运行时执行esMoule
实际上是在定义时就已经进行了绑定,在定义__webpack_exports__
时,执行了__webpack_require__.r(__webpack_exports__);
动态添加__esModule
属性,根据moduleId
定义模块时,执行了__webpack_require__.d(__webpack_exports__, { default: () => __WEBPACK_DEFAULT_EXPORT__});
,将对应模块函数体会直接用对象拦截执行Object.defineProperty
的get
方法,执行definition[key]
从而返回函数体。本质上就是在编译前执行,而不是像cjs
一样在函数体执行阶段直接输出对应内容。他们相同点就是优先会从缓存
__webpack_module_cache__
对象中根据moduleId
直接获取对应的可执行函数体