梳理 webpack打包的逻辑过程
一、前置知识了解
- eval
简单理解就是执行字符串里面的JavaScript程序。例如:
let str = "console.log(1 + 1)"
eval(str);

- Object.defineProperty
Object.defineProperty()的作用就是直接在一个对象上定义一个新属性,或者修改一个已经存在的属性,使用语法如下:
Object.defineProperty(obj, prop, desc);
obj 需要定义属性的当前对象
prop 当前需要定义的属性名
desc 属性描述符

- 给一个对象设置一个值或者改写这个值
var obj = {};
Object.defineProperty(obj,"sex",{
value:"女"
});
console.log(obj.sex);//女
- 属性是否可写
上面的例子我们改写如下:
var obj = {};
Object.defineProperty(obj,"sex",{
value:"女"
});
obj.sex = "男";
console.log(obj.sex);//女
输出的还是女,这是因为 writeable 默认为 false ,表示这个属性不能更改。我们把这个属性设置为 true 就行了。
var obj = {};
Object.defineProperty(obj,"sex",{
value:"女",
writable:true
});
obj.sex = "男";
console.log(obj.sex);//男
- get 和 set
var obj = {sex:"233"};
Object.defineProperty(obj,"sex",{
get:()=>{
return "男"
},
set:(v)=>{
console.log(v);//男
return "不男不女";
}
});
console.log(obj.sex);//男
obj.sex = "男";
读取的时候触发 get 函数,返回值作为读取的值,更改 obj 属性的时候,返回值作为读取的值,其中接收的第一个参数是 oldValue。
- 属性是否可以遍历
var obj = {
name:"Condor Hero",
age : 18,
sex : 233
};
Object.defineProperty(obj,"sex",{
get:()=>{
return "男"
},
set:(v)=>{
console.log(v);//男
return "不男不女";
},
enumerable:false
});
for( let k in obj){
console.log(`我的key是${k},我的value是${obj[k]}`);
}
//运算结果
//我的key是name,我的value是Condor Hero
//我的key是age,我的value是18
sex 并没有给遍历出来。要想便利出来 enumerable:false 改为 true 或者不写都行。
二、认识项目目录和项目文件
新建一个 app 文件作为项目文件,项目目录下的文件如下:
┣✈mian.js //主函数
┣✈index.html //在浏览器运行 bundle.js
┣✈circle.js //求圆的面积
┣✈rectangle.js //求长方形的面积函数
┣✈bundle.js //打包之后的文件
┣✈webpack.config.js //webpack编译依赖文件
来看看项目文件的内容:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>webpack执行机理</title>
<style>
html,body{
width: 100%;
height: 100%;
overflow: hidden;
background-color: #cfc;
}
</style>
</head>
<body>
<script src="./bundle.js"></script>
</body>
</html>
rectangle.js
// 求长方形的面积
function rect(a , b){
return a * b;
};
export default rect;
circle.js
// 求圆的面积
function circle(r){
return Math.PI * r ** 2;
};
export default circle;
main.js
import rect from "./rectangle.js";
import circle from "./circle.js";
function main(){
// 长方形的面积 5 X 10 = 50;
const r = rect(5 , 10);
// 圆的面积 πX10X10≈314
const c = circle(10);
// 浏览器弹出结果
alert(r);
alert(c);
};
main();
webpack.config.js
const path = require("path");
module.exports = {
mode: "development",
entry: "./main.js",
output: {
path: __dirname,
filename: "bundle.js"
}
}
这时候在项目目录运行 CMD 窗口,使用 webpack 进行编译:
webpack main.js -o bundle.js
C:\Users\Administrator\Desktop\app>webpack -v
4.41.5
C:\Users\Administrator\Desktop\app>webpack
Hash: c1528ceb89195bae839e
Version: webpack 4.41.5
Time: 225ms
Built at: 2019-12-29 12:19:08 AM
Asset Size Chunks Chunk Name
bundle.js 5.37 KiB main [emitted] main
Entrypoint main = bundle.js
[./circle.js] 96 bytes {main} [built]
[./main.js] 279 bytes {main} [built]
[./rectangle.js] 91 bytes {main} [built]
项目文件夹,这时候会自动生成一个 bundle.js 文件,这时候直接在浏览器打开 index.html。就会看到下面的情况。

三、bundle.js 文件的执行逻辑分析
因为在 webpack.config.js 中设置 mode 为 开发环境,所以 bundle.js 没有被压缩。
/******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = "./main.js");
/******/ })
/************************************************************************/
/******/ ({
/***/ "./circle.js":
/*!*******************!*\
!*** ./circle.js ***!
\*******************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n// 求圆的面积\r\nfunction circle(r){\r\n\treturn Math.PI * r ** 2;\r\n};\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (circle);\n\n//# sourceURL=webpack:///./circle.js?");
/***/ }),
/***/ "./main.js":
/*!*****************!*\
!*** ./main.js ***!
\*****************/
/*! no exports provided */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _rectangle_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./rectangle.js */ \"./rectangle.js\");\n/* harmony import */ var _circle_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./circle.js */ \"./circle.js\");\n\r\n\r\n\r\nfunction main(){\r\n\t// 长方形的面积 5 X 10 = 50;\r\n\tconst r = Object(_rectangle_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(5 , 10);\r\n\r\n\t// 圆的面积 πX10X10≈314\r\n\tconst c = Object(_circle_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(10);\r\n\r\n\t// 浏览器弹出结果\r\n\talert(r);\r\n\talert(c);\r\n};\r\n\r\nmain();\n\n//# sourceURL=webpack:///./main.js?");
/***/ }),
/***/ "./rectangle.js":
/*!**********************!*\
!*** ./rectangle.js ***!
\**********************/
/*! exports provided: default */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n// 求长方形的面积\r\nfunction rect(a , b){\r\n\treturn a * b;\r\n};\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (rect);\n\n//# sourceURL=webpack:///./rectangle.js?");
/***/ })
/******/ });
连代码加注释是一百二十五行,文件里面是个大的 IIFE
。这逻辑是通的webpack是为了模块化开发,解决浏览器不认识 export import ES6 的语法,但是又不能丧失模块开发功能所以 IIFE 是个最好的选择。
去掉一些注释大致文件如下:
(function(modules) { // webpackBootstrap
// The module cache
var installedModules = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// Execute the module function
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// Flag the module as loaded
module.l = true;
// Return the exports of the module
return module.exports;
}
// expose the modules object (__webpack_modules__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// define getter function for harmony exports
__webpack_require__.d = function(exports, name, getter) {
if (!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// define __esModule on exports
__webpack_require__.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// create a fake namespace object
// mode & 1: value is a module id, require it
// mode & 2: merge all properties of value into the ns
// mode & 4: return value when already ns object
// mode & 8|1: behave like require
__webpack_require__.t = function(value, mode) {
if (mode & 1) value = __webpack_require__(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
__webpack_require__.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string')
for (var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// getDefaultExport function for compatibility with non-harmony modules
__webpack_require__.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
__webpack_require__.d(getter, 'a', getter);
return getter;
};
// Object.prototype.hasOwnProperty.call
__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
// __webpack_public_path__
__webpack_require__.p = "";
// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./main.js");
})({
"./circle.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n// 求圆的面积\r\nfunction circle(r){\r\n\treturn Math.PI * r ** 2;\r\n};\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (circle);\n\n//# sourceURL=webpack:///./circle.js?");
}),
"./main.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _rectangle_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./rectangle.js */ \"./rectangle.js\");\n/* harmony import */ var _circle_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./circle.js */ \"./circle.js\");\n\r\n\r\n\r\nfunction main(){\r\n\t// 长方形的面积 5 X 10 = 50;\r\n\tconst r = Object(_rectangle_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"])(5 , 10);\r\n\r\n\t// 圆的面积 πX10X10≈314\r\n\tconst c = Object(_circle_js__WEBPACK_IMPORTED_MODULE_1__[\"default\"])(10);\r\n\r\n\t// 浏览器弹出结果\r\n\talert(r);\r\n\talert(c);\r\n};\r\n\r\nmain();\n\n//# sourceURL=webpack:///./main.js?");
}),
"./rectangle.js": (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n// 求长方形的面积\r\nfunction rect(a , b){\r\n\treturn a * b;\r\n};\r\n\r\n/* harmony default export */ __webpack_exports__[\"default\"] = (rect);\n\n//# sourceURL=webpack:///./rectangle.js?");
})
});
来看程序大致的执行思路:
- 首先是一个大大的
IIFE
,IIFE
传入一个对象,对象的 key ,就是我们模块文件的路径名,value 是个函数,这个函数使用"use strict"
严选模式。通过 eval 来执行字符串里面的 JavaScript 代码。为什么要用 eval 来执行代码?看看eval里面的内容就知道里面有你写的注释,甚至连换行都给标注出来了,webpack对你写的代码也没怎么处理。这是一种比较懒得方式。 - 然后来到
IIFE
这个函数,installedModules
用来作为模块缓存,然后IIFE
里面又有了一个函数,__webpack_require__
简称 wr,wr 函数身上还绑定了九个属性值,wr 函数接收一个形参moduleId
,根据函数的调用(__webpack_require__.s = "./main.js"
先把值绑定到 wr 函数身上在传入)情况知道moduleId
就是用来先执行主函数main.js
的。 - 继续往下读 installedModules 用来做缓存逻辑,本来是空的,如果
installedModules[moduleId]
为 true 说明已经有函数执行了,如果执行就不往下走了,说明这个文件执行过,如果installedModules[moduleId]
为 false。函数没执行也就是没缓存,继续执行同时创建一个新模型。模型里面放入:
i: moduleId,//函数的ID
l: false,//是否已经执行loading
exports: {}//创建一个空对象
-
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__)
函数执行,第一个执行大对象里面的"./main.js"
对应的函数。call 用来绑定函数的上下文,不过被调用的函数没用到 this,所以这里没用。同时传入两个参数。module.l = true;
标记这个函数已经执行,进入缓存。最后函数返回一个值。 - 剩下的就是函数属性身上的值了,只要懂 API 就基本能看懂到底是干啥的。
我们在改造下代码,不动主要逻辑,只是让我们更容易看懂。
(function(modules) {
// 缓存
var installedModules = {};
// wr函数
function wr(moduleId) {
// 是否函数已经执行
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
//创建一个对象,并放入缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,//未缓存loading
exports: {}
};
// 函数执行并绑定上下文
modules[moduleId].call(module.exports, module, module.exports, wr);
// 已经缓存
module.l = true;
// 暴露出去一个对象
return module.exports;
}
// 没用到
wr.m = modules;
// 没用到
wr.c = installedModules;
// 没用到
wr.d = function(exports, name, getter) {
if (!wr.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 用到了
wr.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 没用到
wr.t = function(value, mode) {
if (mode & 1) value = wr(value);
if (mode & 8) return value;
if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
var ns = Object.create(null);
wr.r(ns);
Object.defineProperty(ns, 'default', { enumerable: true, value: value });
if (mode & 2 && typeof value != 'string')
for (var key in value) wr.d(ns, key, function(key) { return value[key]; }.bind(null, key));
return ns;
};
// 没用到
wr.n = function(module) {
var getter = module && module.__esModule ?
function getDefault() { return module['default']; } :
function getModuleExports() { return module; };
wr.d(getter, 'a', getter);
return getter;
};
// 没用到
wr.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
//没用
wr.p = "";
// 先执行入口函数wr.s可以不绑定
return wr(wr.s = "./main.js");
})({
"./circle.js": function(m, we, wr) {
wr.r(we);
function circle(r){
return Math.PI * r ** 2;
};
we["default"] = circle;
},
"./main.js": function(m, we, wr) {
wr.r(we);
var rwim = wr("./rectangle.js");
var cwim = wr("./circle.js");
console.log(rwim)
function main() {
const r = Object(rwim["default"])(5, 10);
const c = Object(cwim["default"])(10);
alert(r);
alert(c);
};
main();
},
"./rectangle.js": function(m, we, wr) {
wr.r(we);
function rect(a , b){
return a * b;
};
we["default"] = rect;
}
});
这样看就顺畅多了,标注没用到只是现在没用到,不是代表webpack之后没用到,我们去掉没用的和处理缓存的避免视线干扰再来看看代码。
(function(modules) {
// wr函数
function wr(moduleId) {
//创建一个对象,并放入缓存
var module = {
i: moduleId,
l: false, //未缓存loading
exports: {}
};
// 函数执行并绑定上下文
modules[moduleId](module.exports, wr);
// 暴露出去一个对象
return module.exports;
}
// 用到了
wr.r = function(exports) {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
}
Object.defineProperty(exports, '__esModule', { value: true });
};
// 先执行入口函数wr.s可以不绑定
return wr("./main.js");
})({
"./circle.js": function(we, wr) {
wr.r(we);
function circle(r) {
return Math.PI * r ** 2;
};
we["default"] = circle;
},
"./main.js": function(we, wr) {
wr.r(we);
var rwim = wr("./rectangle.js");
var cwim = wr("./circle.js");
function main() {
const r = Object(rwim["default"])(5, 10);
const c = Object(cwim["default"])(10);
alert(r);
alert(c);
};
main();
},
"./rectangle.js": function(we, wr) {
wr.r(we);
function rect(a, b) {
return a * b;
};
we["default"] = rect;
}
});
现在就剩下这些代码了。这看着多舒服~~~~。 wr.r
是用来标记此模块是否是 es 模块。这里也用不到~删。
(function(modules) {
// wr函数
function wr(moduleId) {
//创建一个对象
var module = {};
// 函数执行并绑定上下文
modules[moduleId](module, wr);
// 暴露出去一个对象
return module;
};
return wr("./main.js");
})({
"./circle.js": function(we, wr) {
function circle(r) {
return Math.PI * r ** 2;
};
we["default"] = circle;
},
"./main.js": function(we, wr) {
var rwim = wr("./rectangle.js");
var cwim = wr("./circle.js");
function main() {
const r = Object(rwim["default"])(5, 10);
const c = Object(cwim["default"])(10);
alert(r);
alert(c);
};
main();
},
"./rectangle.js": function(we, wr) {
function rect(a, b) {
return a * b;
};
we["default"] = rect;
}
});
代码就剩这点了,一个IIFE,结合递归回调来执行你的代码。然后没了,讲完了~~~
四、第三方 npm 如何打包的
以数字转人民币大写这个为例,npm 包名为 rmb-x 。你一定好奇为什么选择这个,没错就是因为这个包纯碎,没有依赖第三方包。
更改 main.js:
import rect from "./rectangle.js";
import circle from "./circle.js";
import rmb_x from "rmb-x";
function main(){
// 长方形的面积 5 X 10 = 50;
const r = rect(5 , 10);
// 圆的面积 πX10X10≈314
const c = circle(10);
const rmb = rmb_x(1873947);
// 浏览器弹出结果
alert(rmb);
alert(r);
alert(c);
};
main();
打包之后的效果:

来看看打包之后的
bundle.js
文件,文件没啥变换,只是作为参数传进 IIFE
的大对象增加了一个字段。
说白了,这个第三方包你可以想象就是你自己写的程序,事实上这个 npm 包也不是很难,难道你看不懂,我们顺着引用路径去看看这个我们现在的 npm 包。

看看这个包不是很难,就是封装一个函数默认暴露出去,和我们写求圆的面积差不多。所以经过 webpack 编译也没啥难度,只不过是路径可能有点不同。