理解 Webpack 的工作原理

Webpack 可以把模块打包到一个文件里面去的,但具体它是怎么实现的呢?

让我们一起来通过 Webpack 输出的代码和 Chrome DevTools 来理解 Webpack 实现的原理吧。

准备代码

关于 Webpack 的基础使用,请看我的另一篇博客文章

下面我们准备一个简单的程序。这里我们使用 CommonJS 的语法,ES6 的我们之后再看。

/src/index.js 代码

const hello = require("./component/hello");

hello()

/src/component/hello.js 代码

function hello() {
    console.log('hello');
}

module.exports = hello;

webpack.config.js 需要配置模式和 Source Map 格式,这样生成出来的代码最容易方便看懂。

module.exports = {
    mode: 'development',
    devtool: 'inline-source-map',
    /* 省略 */
};

运行 npx webpack 打包,然后查看 /dist/main.js 的内容。

分析 Webpack 生成的代码

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/

/* 省略 */
/******/ ({

/***/ "./src/component/hello.js":
/*!********************************!*\
  !*** ./src/component/hello.js ***!
  \********************************/
/*! no static exports found */
/***/ (function(module, exports) {


function hello() {
    console.log('hello');
}

module.exports = hello;

/***/ }),
/* 省略 */

生成出来的代码,上面一部分是 Webpack 的代码,下面就是我们写的模块了。

这么一大串,其实它就是一个立即函数(IIFE)

立即函数相关的资料可以看上面的文档,它最主要的好处就是内部的变量不会污染到全局环境。

Webpack 生成的代码其实就可以简化为:

(function(modules) {
    /* 初始化 */
    /* 定义一个 __webpack_require__ 函数 */
    function __webpack_require__(moduleId) {
        /* 省略 */
    }

    return __webpack_require__("./src/index.js");
})(
    {
        "./src/index.js": {
            /* index.js */
        },
        "./src/component/hello.js": {
            /* hello.js */
        }
    }
)

它传入了一个模块的 Object,其中 Key 是文件名, Value 就是代码部分。

我们把它的代码从上面一步一步看。

/******/ (function(modules) { // webpackBootstrap
/******/     // The module cache
/******/     var installedModules = {};
/******/

这个 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;
/******/     }

这里定义了 require 的函数。

/******/         // Check if module is in cache
/******/         if(installedModules[moduleId]) {
/******/             return installedModules[moduleId].exports;
/******/         }

它首先检查了 installedModules 里面有没有这个模块,有就直接返回

/******/         // Create a new module (and put it into the cache)
/******/         var module = installedModules[moduleId] = {
/******/             i: moduleId,
/******/             l: false,
/******/             exports: {}
/******/         };

这里创建一个新的模块,然后同时放到 installedModules 里面。

其中三个属性:

  • i 应该是 ID,是传进来的 moduleId。
  • l 是一个 flag,指示这个模块是否已经加载,现在设为了 false。
  • exports 目前是空的,继续往下看。
/******/         // Execute the module function
/******/         modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

这里调用了 Function.prototype.call ,第一个参数是执行的环境(即 this ),后面的都是传入的参数。

继续往后看。

/******/         // Flag the module as loaded
/******/         module.l = true;
/******/
/******/         // Return the exports of the module
/******/         return module.exports;
/******/     }

之后 loaded flag 设置为了 true,然后返回了 module.exports 。为什么要返回 exports 呢?继续往后看。

后面一部分都是扩展 __webpack_require__ 方法,暂时省略。

/******/     // Load entry module and return exports
/******/     return __webpack_require__(__webpack_require__.s = "./src/index.js");
/******/ })

这里返回了入口模块的路径。也就是把 "./src/index.js" 当 ID 调用了一下 __webpack_require__ 方法。

看了一轮之后,好像也没能理解到底做了什么,实际运行一下看看吧。

分析 Webpack 的运行过程

把它嵌入一个网页上运行一下,可以使用 live-server

webpack-dev-server 会插入一些其他的东西在 main.js 里,会干扰我们看代码。

把脚本放在一个 html 里就可以了,下面是一个例子。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

打开 Chrome DevTools,切换到 Sources 标签页,然后在第三行 installedModules 的行号左边空白处点一下,添加一个断点。

界面如图,Chrome 还会自动识别模块,所以它会创建一个新的标签页。

前面都是在配置 __webpack_require__ 方法,可以一路步过。

到了后面,点一下 Step Into 来看看它是怎么初始化入口的。

首先它以 "./src/index.js" 为 moduleId,检查了缓存,然后一路创建新模块。

此时 exports 都是空的,到了最关键的 call 这一步,我们 Step Into 看看。

Chrome 又为这个模块开了个新标签页。这样子我们看不到调用的函数,所以我们切回 main.js 的代码

先看这个函数传入的参数

function(module, exports, __webpack_require__)

和刚才的 call 函数相对应,原来我们代码里写的 require 会调用 __webpack_require__ 方法。

modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

那前面两个参数有什么用呢?继续往下看。

我们再 Step Into 一次,看看 __webpack_require__("./src/component/hello.js"); 又帮我们做了什么。

这次以 "./src/component/hello.js" 为 ID 了,再进去 call 看看。

咦?这里是我们调用的 module.exports,我们回去看一下 Webpack 的代码。

var module = installedModules[moduleId] = {

    i: moduleId,

    l: false,

    exports: {}
};

modules[moduleId].call(module.exports, module /* 这里 */, module.exports, __webpack_require__);

之前给 moduleexports 属性设了个空的对象,然后我们用 module.exports 给它赋了值,也就是说现在 exports 是我们导出的函数了。

我们过下一步看看。

果然,现在 moduleexports 是我们的函数了。

然后整个函数返回了 module.export,我们再想想刚才这个函数是从哪里调用的。

原来就是我们的 require 函数,也就是这个时候返回的是模块里的 hello() 函数。

到这里,你应该对 Webpack 的 import / export 有点概念了。

有时间的话务必动手一步一步调试看看,比如 ES6 的模块, Webpack 会怎么处理呢?导出多个模块,default 导出,打包后都会变成什么样子呢?这就作为下次博客的题材了。