深入解析Webpack模块打包与运行时加载原理

作者:脚本小铺

时间:2026-10-22

浏览:522

本文深入解析Webpack打包JS模块的核心原理,涵盖从入口文件分析依赖、生成模块映射表,到通过webpack_require实现运行时动态加载的完整流程。重点剖析CommonJS与ES6 import/export的转换机制,特别是__esModule标记在默认导出处理中的作用,揭示Webpack如何将源码转化为可执行的bundle文件。

文章目录

Webpack打包概述

目前,前端开发中利用Webpack进行JavaScript及其他资源的打包已成为行业标准。随着Node.js的普及,前端工程化模式正逐渐趋近于后端开发,实现了全面的模块化与统一编译。尽管Webpack版本迭代迅速且配置选项繁多,常导致令人困惑的错误,但理解其模块组织与编译生成的代码执行机制至关重要,否则Webpack将始终是一个无法掌控的黑盒。本文旨在记录对Webpack原理的初步研究,帮助读者打破这一黑盒。

编译模块原理

编译模块

“编译”一词常因生成代码的复杂性而显得高深莫测,但实际上其核心原理并不复杂。Webpack的编译过程本质上是分析源代码后对其进行修改,并将所有源码整合到一个文件中,最终生成一个名为bundle.js的大文件。该文件由浏览器或其他JavaScript引擎执行并返回结果。

为了说明Webpack打包模块的原理,我们构建一个简单案例。假设存在一个模块文件mA.js。


var aa = 1;

function getDate() {
 return new Date();
}

module.exports = {
 aa: aa,
 getDate: getDate
}

在mA.js中,我们定义了一个变量aa和一个函数getDate,并使用CommonJS语法将其导出。

接着定义主文件app.js,同样采用CommonJS风格。


var mA = require('./mA.js');

console.log('mA.aa =' + mA.aa);
mA.getDate();

现在拥有两个模块,以app.js为入口文件,依赖mA.js。Webpack在执行打包时需完成以下任务:

  1. 从入口模块app.js出发,分析所有模块的依赖关系,并读取所有涉及的模块。
  2. 将每个模块的源代码封装在一个立即执行函数(IIFE)中。
  3. 改写模块代码中与require和export相关的语法及其对应的引用变量。
  4. 在生成的bundle文件中建立一套模块管理系统,支持在运行时动态加载所需模块。

观察上述示例的打包结果,最终的bundle文件是一个复杂的立即执行函数,命名晦涩。为便于理解,此处对代码进行了简化处理。

首先,Webpack列出所有用到的模块,以文件名(通常为完整路径)作为ID,建立一张映射表。


var modules = {
 './mA.js': generated_mA,
 './app.js': generated_app
}

关键在于上述代码中的generated_xxx是什么?它是一个函数,将每个模块的源代码包裹其中,形成局部作用域,从而隐藏内部变量。这使得每个模块都变成了一个执行函数,其定义通常如下。


function generated_module(module, exports, webpack_require) {
  // 模块的具体代码。
  // ...
}

这里的“模块具体代码”指的是生成后的代码,Webpack称之为generated code。以mA为例,经过改写后结果如下。


function generated_mA(module, exports, webpack_require) {
 var aa = 1;
 
 function getDate() {
  return new Date();
 }

 module.exports = {
  aa: aa,
  getDate: getDate
 }
}

乍看之下,生成代码似乎与源代码无异。确实,由于mA未引入其他模块且使用传统CommonJS导出,生成代码未做额外改动。值得注意的是最后的module.exports = ...,这里的module是外部传入的参数。这表明运行该函数即执行mA的源代码,并将导出内容保存至外部,标志着mA加载完成。这个外部对象即为后文所述的模块管理系统。

接下来查看app.js的生成代码。


function generated_app(module, exports, webpack_require) {
 var mA_imported_module = webpack_require('./mA.js');
 
 console.log('mA.aa =' + mA_imported_module['aa']);
 mA_imported_module['getDate']();
}

可以看到,app.js中引入mA的部分已被修改。由于JavaScript解释器无法直接执行require/exports或ES6的import/export,必须依赖模块管理系统将这些抽象关键词具体化。webpack_require即为require的具体实现,它能动态载入模块mA并将结果返回给app。

此时,你可能已初步构建出模块管理系统的概念,让我们查看webpack_require的具体实现。


// 加载完毕的所有模块。
var installedModules = {};

function webpack_require(moduleId) {
 // 如果模块已经加载过了,直接从Cache中读取。
 if (installedModules[moduleId]) {
  return installedModules[moduleId].exports;
 }

 // 创建新模块并添加到installedModules。
 var module = installedModules[moduleId] = {
  id: moduleId,
  exports: {}
 };
 
 // 加载模块,即运行模块的生成代码,
 modules[moduleId].call(
  module.exports, module, module.exports, webpack_require);
 
 return module.exports;
}

注意倒数第二句中的modules,即我们之前定义的所有模块的generated code。


var modules = {
 './mA.js': generated_mA,
 './app.js': generated_app
}

webpack_require的逻辑清晰:首先检查模块是否已加载,若已加载则直接从缓存中返回exports结果。若是新模块,则建立相应的module数据结构并运行该模块的generated code。此函数传入的是我们建立的module对象及其exports域,这正是CommonJS中exports和module的来源。运行完该函数后,模块加载完成,导出结果保存至module对象中。

因此,所谓的模块管理系统原理非常简单,只需耐心梳理即可发现其并不深奥。它主要由以下三部分组成:


// 所有模块的生成代码
var modules;
// 所有已经加载的模块,作为缓存表
var installedModules;
// 加载模块的函数
function webpack_require(moduleId);

当然,上述所有代码在最终的bundle文件中均被包裹在一个大的立即执行匿名函数中,最后返回语句如下。


return webpack_require(‘./app.js');

即加载入口模块app.js,后续所有依赖将在运行时动态、递归地加载。虽然Webpack实际生成的代码略有不同,但其结构大致如下。


(function(modules) {
 var installedModules = {};
 
 function webpack_require(moduleId) {
   // ...
 }

 return webpack_require('./app.js');
}) ({
 './mA.js': generated_mA,
 './app.js': generated_app
});

可以看到,Webpack直接将modules作为立即执行函数的参数传入,而非单独定义。这与上述写法本质相同,此处改写旨在更清晰地解释原理。

ES6模块处理机制

ES6的import和export

上述示例均使用传统的CommonJS写法。目前更通用的ES6风格使用import和export关键词,使用上略有不同。但对于Webpack或其他模块管理系统而言,这些新特性仅被视为语法糖,本质上与require/exports相同。例如export。


export aa
// 等价于:
module.exports['aa'] = aa

export default bb
// 等价于:
module.exports['default'] = bb

而对于import。


import {aa} from './mA.js'
// 等价于
var aa = require('./mA.js')['aa']

比较特殊的情况如下。


import m from './m.js'

这种情况稍显复杂,它需要载入模块m的default export。而模块m可能并非由ES6的export编写,甚至可能没有export default。因此,Webpack在为模块生成generated code时,会判断其是否为ES6风格的export。例如定义模块mB.js。


let x = 3;

let printX = () => {
 console.log('x = ' + x);
}

export {printX}
export default x

若使用了ES6的export,Webpack在mB的generated code中会添加一句话。


function generated_mB(module, exports, webpack_require) {
 Object.defineProperty(module.exports, '__esModule', {value: true});
 // mB的具体代码
 // ....
}

也就是说,它给mB的export标注了一个__esModule,表明这是ES6风格的export。这样,当其他模块以类似import m from './m.js'的方式加载时,会首先判断得到的是否为ES6 export出的模块。若是,则返回其default;若不是,则返回整个export对象。例如,上述mA是传统CommonJS,mB是ES6风格。


// mA is CommonJS module
import mA from './mA.js'
console.log(mA);

// mB is ES6 module
import mB from './mB.js'
console.log(mB);

我们定义get_export_default函数。


function get_export_default(module) {
 return module && module.__esModule? module['default'] : module;
}

这样,generated code运行后在mA和mB上会得到不同的结果。


var mA_imported_module = webpack_require('./mA.js');
// 打印完整的 mA_imported_module
console.log(get_export_default(mA_imported_module));

var mB_imported_module = webpack_require('./mB.js');
// 打印 mB_imported_module['default']
console.log(get_export_default(mB_imported_module));

这就是Webpack在ES6 import上需要特殊处理的地方。总体而言,ES6的import/export本质上与CommonJS无区别,且Webpack最终生成的generated code仍基于CommonJS的module/exports机制来实现模块加载。

模块管理系统总结

模块管理系统

以上解读了Webpack如何打包组织模块并实现运行时加载。其原理并不复杂,核心思想是建立模块管理系统。这种做法具有普遍性,若阅读Node.js的Module部分源代码,会发现其使用了类似方法。此处有一篇文章可供参考。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章