分析 webpack 打包后的代码

时间:2024-01-23 11:17:33

写在前面的:使用的 webpack 版本 3.0.0

一、开始

1. 准备

        当前只创建一个文件 index.js,该文件作为入口文件,代码如下。

console.log('hello, world');

        接着使用 webpack 来进行打包,执行的命令如下。

webpack index.js index.bundle.js

2. 分析

        打包文件生成的一大堆代码,实际上就是一个自执行函数,仅传入一个参数为 modules,且该对象为一个数组。

(function (modules) {
    // ...
})([function (module, exports) {
    function sayHello () {
        console.log('hello');
    }
}])

        该函数的作用就是管理模块,它的内部定义了两个主要的对象 installedModules 对象和 __webpack_require__(moduleId) 函数对象。

        installedModules 对象初始化代码如下。

var installedModules = {};

        来看一下函数对象 __webpack_require__ 的定义,它的作用与 require 函数相同。

// require 函数
function
__webapck_require__(moduleId) {
// 如果模块在缓存中
if (installedModules[moduleId]) { return installedModules[moduleId].exports; }
// 创建一个新的模块(并将它放在缓存中)
var module = installedModules[moduleId] = { i: moduleId, l: false,// l是loaded的缩写,指代是否已经加载 exports: {} };
// 执行模块的函数 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标记模块已经加载 module.l
= true;
// 返回模块的 exports 对象
return module.exports; }

        看到这里,我们可以看出 installedModules 对象其实是被当作字典使用,key 是模块的 id,value 是代表模块状态和导出的一个对象,如下。

{
    i: moduleId, // 模块的 id
    l: false, // l是loaded的缩写,指代是否已经加载
    exports: {} // 模块的导出对象
}

        __webpack_require__ 还被定义了许多的静态方法和静态对象。

// 外放 modules 对象(__webpack_modules__)
__webpack_require__.m = modules;

// 外放模块的缓存
__webpack_require__.c = installedModules;

// 定义 getter 函数以便友好的加载模块
__webpack_require__.d = function (exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
        Object.defineProperty(exports, name, {
            configurable: false,
            enumerable: true,
            get: getter
        });
    }
};
__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) { Object.prototype.hasOwnProperty.call(object, property); };

// __webpack_public_path__
__webpack_require__.p = "";

        在函数的最后。

return __webpack_require__(__webpack_require__.s = 0);

        你定义的入口文件中的内容被转换为。

(function (module, __webpack_exports__, __webpack_require__) {
    console.log('hello, world!');
});

        传入立即函数表达式中的参数为一个数组对象,而入口文件转换的函数为数组中的第一个元素。注意 webpack 添加的注释 /* 0 */ 代表该模块的 moduleId 为 0,而 webpack 其实是将每一个资源文件看作一个模块,并将其指定一个标识 moduleId。

[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
    // ...
})
]

 二、添加依赖

1. 准备

        添加一个新的 js 文件,命名为 util.js,代码如下。

export function add (a, b) {
return a + b; }

        入口文件 index.js 代码修改如下。

import util from './util.js'

console.log('1 + 2 = ' + util.add(1, 2));

        执行之前的命令,使用 webpack 构建工具进行打包。

2. 分析

        此时查看 index.bundle.js 下的代码,之后生成的立即执行函数的传入参数发生变化,还记得之前说过该参数为一个数组,此时的数组如下。

[
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1);


    console.log('1 + 2 = ' + __WEBPACK_IMPORTED_MODULE_0__util_js__["a" /* default */].add(1 + 2));

}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony default export */ __webpack_exports__["add"] = add;
function (a, b) { return a + b; } }) ]

        可见,webpack 将每个文件视为一个独立的模块,它分析模块之间的依赖关系,将模块中 import export 相关的内容做一次替换,比如。

import b from './b'

export default {
    name: 'c'  
}

// 最后被转化为
var __WEBPACK_IMPORTED_MODULE_0_b__ = __webpack_require__(0)

// 这里需要特别注意一点,webpack 将 a 属性作为某块的 default 值
__webpack_exports__["a"] = ({
    name: 'c'
})

         再给所有模块外面加一层包装函数,使其成为模块初始化函数,接着把所有模块初始化函数合成一个数组,赋值给 modules 变量。

三、模块的动态导入

1. 准备

        修改入口文件 index.js 的代码如下。

import('./util.js').then(function (util) {
    console.log('1 + 2 = ' + util.add(1 + 2));
})

         执行之前的命令,使用 webpack 构建工具进行打包。

2. 分析

        这次打包后不仅获取了 index.bundle.js 文件,还产生了一个 0.index.bundle.js 文件,接下来先分析 o.index.bundle.js。

webpackJsonp([0],[
/* 0 */,
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {

"use strict";
Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
/* harmony export (immutable) */ __webpack_exports__["add"] = add;
function add (a, b) {
    return a + b;
}

/***/ })
]);

        之后可以看到该块代码以 JSONP 的形式被加载,这里可以看出该代码被加载后立即执行 webpackJsonp 这个方法,这个方法为 index.bundle.js 中新增的方法。在看这个方法之前,先看一下 index.bundle.js 中新增的一个对象。

var installedChunks = {
    1: 0
};

         该对象记录 chunk 的安装状态,而不记录 chunk 相关的具体内容,具体内容仍保存到 installedModules 对象中。installedChunks 也是被当作一个字典使用,key 为 chunk 的 id(此处要注意与 mouleId 的区别,每个 chunk 中至少会拥有一个 module),value 可为 0(已加载)、[resolve, reject](正在加载)、undefined(未加载),可以在 requireEnsure(chunkId) 方法中观察到这种状态的变化。

__webpack_require__.e = function requireEnsure(chunkId) {
    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData === 0) {
        return new Promise(function(resolve) { resolve(); });
    }

    // 一个 Promise 意味“正在加载”
    if (installedChunkData) {
        return installedChunkData[2];
    }
    
    var promise = new Promise(function(resolve, reject) {
        installedChunkData = installedChunks[chunkId] = [resolve, reject];
    });
    installedChunkData[2] = promise;

    var head = document.getElementsByTagName('head')[0];
    var script = document.createElement('script');
    script.type = 'text/javascript';
    script.charset = 'utf-8';
    script.async = true;
    script.timeout = 120000;

    if(__webpack_require__.nc) {
        script.setAttribute("nonce", __webpack_require__.nc);
    }
    script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js";
    var timeout = setTimeout(onScriptComplete, 120000);
    script.onerror = script.onload = onScriptComplete;
    function onScriptComplete() {
        // avoid mem leaks in IE.
        script.onerror = script.onload = null;
        clearTimeout(timeout);
        var chunk = installedChunks[chunkId];
        if(chunk !== 0) {
            if(chunk) {
                chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
            }
            installedChunks[chunkId] = undefined;
        }
    };
    head.appendChild(script);

    return promise;
};

         最后看 webpackJsonp 函数,如下。

var parentJsonpFunction = window["webpackJsonp"];
window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
    // 添加 “moreModules”到 modules 对象上
    // 然后所有的 “chunkIds” 已经加载并调用 callback
    var moduleId, chunkId, i = 0, resolves = [], result;
    for(; i<chunkIds.length; i++) {
        chunkId = chunkIds[i];
        if(installedChunks[chunkId]) {
            resolves.push(installedChunks[chunkId][0]);
        }
        installedChunks[chunkId] = 0;
    }
    for(moduleId in moreModules) {
        if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if(parentJsonFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);
    while(resolves.length) {
        resolves.shift()();
    }
};
            

        该方法有三个参数,chunkIds 为该模块的先行 chunk,moreModules 为此次新添加的模块,executeModules 暂时不知道其作用。该方法先对正在加载的先行 chunk 进行处理,还记的么?正在加载的 chunk 代表其状态的对象的值为 [resolve, reject],这里将它们的 resolve 方法保存到局部定义的 resolves 数组中。还记得 modules 对象么,该对象为 webpack 生成的最外层立即执行函数的参数,这里继续将 moreModules 安装到 modules 对象中。最后执行保存在 resolves 数组中的 resolve 方法。

        可见异步加载模块时主要是使用 Promise 来包装 jsonp 请求需要的 chunk 文件,之后将其添加到 modules 参数中,随后的调用与同步的步骤的相同的。