分析 webpack 打包后的代码

Posted symind

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了分析 webpack 打包后的代码相关的知识,希望对你有一定的参考价值。

写在前面的

1. 版本说明

使用的 webpack 版本 3.0.0。

2. webpack 的核心思想

  • 一切皆“模块(module)”
  • 按需加载

一、开始

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 = "";

        在函数的最后,结果是导入 moduleId = 0 的模块,就是执行了入口文件。

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 参数中,随后的调用与同步的步骤的相同的。
























以上是关于分析 webpack 打包后的代码的主要内容,如果未能解决你的问题,请参考以下文章

webpack模块机制浅析

webpack

webpack打包后的文件

webpack打包js代码

webpack打包原理

减少 Webpack 打包后的文件体积