Webpack 4.x CommonJS 模块化原理与打包流程解析
本文深入解析 Webpack 4.x 对 CommonJS 模块的处理机制。通过实际案例,展示从项目初始化、依赖安装到使用配置文件进行打包的完整过程。重点分析打包后 bundle.js 的自执行函数结构,详解 __webpack_require 函数如何递归加载模块、处理依赖关系及利用缓存优化性能,揭示 Webpack 模块化打包的核心原理。
Webpack 模块化概念与核心作用
首先参考 webpack 官方文档中关于模块的定义与描述。
在模块化编程体系中,开发者将程序拆解为离散的功能块(discrete chunks of functionality),这些功能块即被称为模块。
每个模块拥有比完整程序更小的接触面,从而使得校验、调试和测试变得轻而易举。精心编写的模块提供了可靠的抽象和封装界限,确保应用程序中的每个模块都具备条理清晰的设计与明确的目的。
“一切皆模块”是 webpack 的核心概念之一。webpack 在项目中的主要作用是分析项目结构,识别 JavaScript 模块以及其他浏览器无法直接运行的拓展语言(如 less、scss、typescript),并将它们打包为浏览器可识别的格式。它从项目的主文件开始,根据引用路径查找所有依赖文件,通过 loader 进行处理(解析、编译浏览器不支持的文件),最终打包为一个或多个浏览器可识别的 JavaScript 文件。
本文不赘述 webpack 的详细构建流程,因为官网已有详尽说明。重点在于分析 webpack 打包后的文件形态及其模块使用方式。webpack 最早支持 CommonJS 模块化,随后逐步支持 ES6、AMD 等规范。无论使用何种方式,webpack 均能解析并打包。本文示例采用 CommonJS 规范,更多规范介绍可查阅官方文档。
CommonJS 模块化示例搭建
示例
为了便于后续说明,首先创建一个项目。即先建立一个名为 webpack-test 的文件夹(名称可自拟),并在其中新建 package.json 文件以进行 npm 说明。在 webpack-test 文件夹中执行以下命令:
npm init
执行命令后会提示一些问题,一路回车确认即可。随后安装 webpack 依赖包,命令如下:
npm install --save-dev webpack
接着新建几个必要的文件:
1、在项目根目录下新建 app 文件夹用于存放业务代码,public 文件夹用于存放打包后的文件; 2、在 app 中新建入口文件 main.js; 3、在 app 中新建功能模块 hello.js、bye.js 和 to.js; 4、在项目根目录下建立 index.html 文件;
接下来依次为这些文件填充内容:
// webpack-test/app/hello.js
const to = require('./to.js');
module.exports = function() {
var hello = document.createElement('div');
hello.textContent = "Say Hello to " + to.name;
return hello;
};
// webpack-test/app/bye.js
const to = require('./to.js');
module.exports = function() {
var bye = document.createElement('div');
bye.textContent = "Say Bye to " + to.name;
return bye;
};
// webpack-test/app/to.js
module.exports = {name: "小明"};
// webpack-test/app/main.js
const hello = require('./hello.js');
const bye = require('./bye.js');
document.querySelector("#root").appendChild(hello()).appendChild(bye());;
// webpack-test/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Webpack Test Project</title>
</head>
<body>
<div id='root'></div>
// bundle.js 文件就是一会儿我们要打包app中的文件后生成的结果文件
<script src="public/bundle.js"></script>
</body>
</html>
业务模块 hello.js 和 bye.js 分别执行各自的操作,同时引用了公共文件 to.js;主文件 main.js 中引用并执行了模块 hello.js 和 bye.js;index.html 文件引入了 main.js 打包后的最终文件 bundle.js。
Webpack 4.x 打包命令与配置
打包
接下来进行打包操作。首先确保 webpack 已全局安装,否则执行时需指定 webpack 路径。例如在 4.0 以下版本中,使用 node_modules/.bin/webpack ./app/main.js ./public/bundle.js; 若使用 webpack 4.0+ 版本,执行命令 webpack ./app/main.js ./public/bundle.js 可能会报错:
WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/
ERROR in multi ./app/main.js ./public/bundle.js Module not found: Error: Can't resolve './public/bundle.js' in '/Users/zhaohaipeng/soyoung-project/webpack-test' @ multi ./app/main.js ./public/bundle.js main[1]
webpack 4.0+ 之后,针对第一个报错,需要指定环境参数 --mode development;第二个报错是因为未使用配置文件方式打包,而是直接通过命令指定输出位置,因此需要声明输出文件。综上,正确的命令如下:
webpack app/main.js --output public/bundle.js --mode development
执行结果:
➜ webpack-test webpack app/main.js --output public/bundle.js --mode development Hash: a4e2f9ecc51b64891624 Version: webpack 4.25.1 Time: 90ms Built at: 2018-11-08 17:11:01 Asset Size Chunks Chunk Names bundle.js 5.16 KiB main [emitted] main Entrypoint main = bundle.js [./app/bye.js] 165 bytes {main} [built] [./app/hello.js] 173 bytes {main} [built] [./app/main.js] 144 bytes {main} [built] [./app/to.js] 30 bytes {main} [built] ➜ webpack-test
在浏览器中打开 index.html 文件,即可看到运行结果。
Say Hello to 小明 Say Bye to 小明
然而,webpack 作为简化开发难度和提升便捷性的工具,显然通过输入大量命令进行打包并不方便。因此,下面采用配置文件的方式重新进行打包:
在根目录创建 webpack.config.js 文件,并配置打包入口和出口:
// webpack-test/webpack.config.js
module.exports = {
mode: "development",//webpack.0之后需要声明环境
entry: __dirname + "/app/main.js",//唯一入口文件
output: {
path: __dirname + "/public",//打包后的文件存放目录
filename: "bundle.js"//打包后输出文件名
}
}
再次打包时,只需使用命令 webpack 即可,webpack 默认读取当前路径下的 webpack.config.js 文件。
最终打包生成的 bundle.js 文件去除了多余注释并调整了代码格式,内容如下:
// 自执行函数,参数为所有模块组成的,形势为key:value,key是模块名
(function(modules) { // webpackBootstrap
// 已加载模块的缓存,记录模块的加载情况,也是为了避免重复打包,节省资源
var installedModules = {};
// webpack 使用 require 方式加载模块的方法(模拟ConmmonJS reqiure()),作用为根据传进来的模块id来处理对应的模块,加入已加载缓存,执行,标记,返回exports
function __webpack_require__(moduleId) {
// moduleId 为模块路径
// 检测模块是否已加载,已加载的话直接返回该模块
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 当前模块未加载的话,新建,并存于缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 在当前模块的 exports 下,也就是模块的内部执行模块代码,突出作用域
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块已经加载
module.l = true;
// 返回模块的导出内容
return module.exports;
}
// 挂载属性,该模块 (__webpack_modules__)
__webpack_require__.m = modules;
// 挂载属性,模块加载缓存
__webpack_require__.c = installedModules;
// 本代码中未执行,暂时不分析
// 在 exports 中定义 getter 方法
__webpack_require__.d = function(exports, name, getter) {
// 当 name 属性不是定义在对象本身,而是继承自原型链,则在在 exports 中定义 getter 方法
if(!__webpack_require__.o(exports, name)) {
Object.defineProperty(exports, name, { enumerable: true, get: getter });
}
};
// 本代码中未执行,暂时不分析
// 在 exports 中定义 __esModule,定义key为Symbol的属性(在__webpack_require__.t中被调用)
// 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 = "";
// 加载入口模块 main.js ,返回 exports,从而从入口文件开始执行,以递归的方式,将所有依赖执行并返回
return __webpack_require__(__webpack_require__.s = "./app/main.js");
})({
"./app/bye.js": (function(module, exports, __webpack_require__) {
eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");
}),
"./app/hello.js": (function(module, exports) {
eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");
}),
"./app/main.js": (function(module, exports, __webpack_require__) {
eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");
}),
"./app/to.js": (function(module, exports) {
eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");
})
});
打包后文件结构与执行流程分析
分析
webpack 的运行过程大致可分为:读取配置参数、实例化插件、模块解析处理(loader)、输出打包文件。在上述示例中,仅涉及 JavaScript 引用,未使用插件及需 loader 处理的 CSS、less、图片等模块,因此过程仅包括读取配置、识别入口及其引用模块、打包几步,最终生成 bundle.js 文件。
简要描述 webpack 在此过程中的执行流程:在配置文件中读取入口,若配置了 plugins 参数,则在此时进行插件实例化和钩子函数绑定;模块解析即 loader 加入的时刻,从入口文件开始,根据入口文件对其他模块的依赖,结合配置文件中对不同种类文件所使用的 loader 说明,逐级对这些模块进行解析处理(如压缩、转义),生成浏览器可直接识别的内容;最后将所有模块打包,输出打包后的文件。上述代码中已对 bundle.js 内容进行了注释,下面分析 bundle.js 的执行过程:
1、自执行函数
最终输出的 bundle.js 是一个 JavaScript 文件,其本质是一个自执行函数。
(function(参数){})(参数)。
2、参数
自执行方法的参数为所有模块组成的对象,key 为各模块路径,值为各模块内部的执行代码。观察参数内部代码,对比打包前源码,可发现所有 require 均变为 __webpack_require__(webpack 自定义的模块调用方法),且源码中的相对路径也变为最终执行位置文件的相对路径。
{
"./app/bye.js": (function(module, exports, __webpack_require__) {
eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");
}),
"./app/hello.js": (function(module, exports) {
eval("module.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello!\";\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");
}),
"./app/main.js": (function(module, exports, __webpack_require__) {
eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");
}),
"./app/to.js": (function(module, exports) {
eval("module.exports = {name: \"小明\"};\n\n//# sourceURL=webpack:///./app/to.js?");
})
}
3、执行
(1)自执行文件开始执行后,至自执行函数最底部,首先从入口文件开始加载。
return __webpack_require__(__webpack_require__.s = "./app/main.js");
(2)调用 __webpack_require__ 函数,传入参数 ./app/main.js。
function __webpack_require__(moduleId) {
// moduleId 为 ./app/main.js
// 首次进来,未加载,模块还没有缓存
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 新建 ./app/main.js 模块,并存于缓存
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块已经加载
module.l = true;
// 输出模块的内容
return module.exports;
}
此时方法中执行 modules[moduleId].call(module.exports, module, module.exports,__webpack_require__);,相当于在名为 ./app/main.js 的模块中执行如下代码:
(function(module, exports, __webpack_require__) {
eval("const hello = __webpack_require__(/*! ./hello.js */ \"./app/hello.js\");\nconst bye = __webpack_require__(/*! ./bye.js */ \"./app/bye.js\");\n\ndocument.querySelector(\"#root\").appendChild(hello()).appendChild(bye());;\n\n//# sourceURL=webpack:///./app/main.js?");
})()
由于引用关系,接下来会再次执行两次 __webpack_require__ 方法,分别传参模块路径 ./app/hello.js 和 ./app/bye.js;
(3)执行第一个 __webpack_require__ 过程,除传参不同、执行的模块不同外,与第二步基本一致,再次找到依赖模块 to.js,再次调用 __webpack_require__。
"./app/hello.js": (function(module, exports, __webpack_require__) {
eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var hello = document.createElement('div');\n hello.textContent = \"Say Hello to \" + to.name;\n return hello;\n};\n\n//# sourceURL=webpack:///./app/hello.js?");
}),
(4)执行第二个 __webpack_require__ 时,在 bye.js 中找到对 to.js 的依赖,因此将继续调用 __webpack_require__ 方法,只是传参变为 ./app/to.js,达到终点。
"./app/bye.js": (function(module, exports, __webpack_require__) {
eval("const to = __webpack_require__(/*! ./to.js */ \"./app/to.js\");\nmodule.exports = function() {\n var bye = document.createElement('div');\n bye.textContent = \"Say Bye to \" + to.name;\n return bye;\n};\n\n//# sourceURL=webpack:///./app/bye.js?");
})
(5)至此,整个从入口文件开始的针对所依赖模块的解析已完成,所有 js 代码也已引用完毕并放入 bundle.js 中。
总结与展望
总结
由此可见,webpack 对 js 的打包,即是将代码封装为一个个单独的方法,通过引用这些方法实现模块化;而打包过程,则是查找、解析、封装这些方法的过程。整个执行路径类似于一棵树,从主干出发,沿树枝递归式执行“require”方法,直到这根树枝走到尽头才回头寻找其他方法。由于 node 的单线程特性,当项目庞大或模块间依赖错综复杂时,webpack 打包会耗费更多时间。
以上为对 webpack 4.x 中针对 js 模块处理的简单理解,主要基于官方文档介绍和打包后文件分析。源码阅读难度较大,暂不敢妄加评论。对于 ES6、AMD 的模块化方式及代码分割等内容,后续将补充。希望对大家的学习有所帮助,也希望大家多多支持本站。
UmiJS 项目中 Jest 单元测试与 Playwright E2E 测试配置
2026-11-05 10:22:24
JavaScript Ajax请求遇到CORS跨域报错怎么解决
2026-11-04 18:25:04
Webpack HMR 热更新原理与配置详解
2026-11-01 02:44:12
Webpack 3 与 Vue 2 集成 dotenv 实现多环境配置
2026-10-31 08:51:44
解决Webpack配置热更新后页面全刷新的问题
2026-10-31 08:39:50
