webpack启动代码源码解读【转】

原文地址:https://segmentfault.com/a/1190000016524677?utm_source=sf-similar-article

一、前言

虽然每天都在用webpack,但一直觉得隔着一层神秘的面纱,对它的工作原理一直似懂非懂。它是如何用原生JS实现模块间的依赖管理的呢?对于按需加载的模块,它是通过什么方式动态获取的?打包完成后那一堆/******/开头的代码是用来干什么的?本文将围绕以上3个问题,对照着源码给出解答。

如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结

二、模块管理

先写一个简单的JS文件,看看webpack打包后会是什么样子:

// main.js
console.log('Hello Dickens');

// webpack.config.js
const path = require('path');
module.exports = {
  entry: './main.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist')
  }
};

在当前目录下运行webpack,会在dist目录下面生成打包好的bundle.js文件。去掉不必要的干扰后,核心代码如下:

// webpack启动代码
(function (modules) { 
    // 模块缓存对象
    var installedModules = {};

    // webpack实现的require函数
    function __webpack_require__(moduleId) {
        // 检查缓存对象,看模块是否加载过
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }

        // 创建一个新的模块缓存,再存入缓存对象
        var module = installedModules[moduleId] = {
            i: moduleId,
            l: false,
            exports: {}
        };

        // 执行模块代码
        modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

        // 将模块标识为已加载
        module.l = true;

        // 返回export的内容
        return module.exports;
    }

    ...

    // 加载入口模块
    return __webpack_require__(__webpack_require__.s = 0);
})
([
    /* 0 */
    (function (module, exports) {
        console.log('Hello Dickens');
    })
]);

代码是一个立即执行函数,参数modules是由各个模块组成的数组,本例子只有一个编号为0的模块,由一个函数包裹着,注入了moduleexports2个变量(本例没用到)。

核心代码是__webpack_require__这个函数,它的功能是根据传入的模块id,返回模块export的内容。模块id由webpack根据文件的依赖关系自动生成,是一个从0开始递增的数字,入口文件的id为0。所有的模块都会被webpack用一个函数包裹,按照顺序存入上面提到的数组实参当中。

模块export的内容会被缓存在installedModules中。当获取模块内容的时候,如果已经加载过,则直接从缓存返回,否则根据id从modules形参中取出模块内容并执行,同时将结果保存到缓存对象当中。缓存对象数据结构如下:

[图片上传失败...(image-97e893-1619760017929)]

我们再添加一个文件,在入口文件处导入,再来看看生成的启动文件是怎样的。

// main.js
import logger from './logger';

console.log('Hello Dickens');
logger();

//logger.js
export default function log() {
    console.log('Log from logger');
}

启动文件的模块数组:

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

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

        console.log('Hello Dickens');

        Object(__WEBPACK_IMPORTED_MODULE_0__logger__["a" /* default */ ])();
    }),
    /* 1 */
    (function (module, __webpack_exports__, __webpack_require__) {

        "use strict";
        /* harmony export (immutable) */
        __webpack_exports__["a"] = log;

        function log() {
            console.log('Log from logger');
        }
    })
]

可以看到现在有2个模块,每个模块的包裹函数都传入了module, __webpack_exports__, __webpack_require__三个参数,它们是通过上文提到的__webpack_require__注入的:

// 执行模块代码
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

执行的结果也保存在缓存对象中了。

执行流程如下图所示:
[图片上传失败...(image-f6a59b-1619760017929)]

三、按需加载

再对代码进行改造,来研究webpack是如何实现动态加载的:

// main.js
console.log('Hello Dickens');

import('./logger').then(logger => {
    logger.default();
});

logger文件保持不变,编译后比之前多出了1个chunk。
[图片上传失败...(image-d29fce-1619760017929)]

bundle_asy的内容如下:

(function (modules) {
    // 加载成功后的JSONP回调函数
    var parentJsonpFunction = window["webpackJsonp"];

    // 加载成功后的JSONP回调函数
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
        var moduleId, chunkId, i = 0,
            resolves = [],
            result;

        for (; i < chunkIds.length; i++) {
            chunkId = chunkIds[i];

            // installedChunks[chunkId]不为0且不为undefined,将其放入加载成功数组
            if (installedChunks[chunkId]) {
                // promise的resolve
                resolves.push(installedChunks[chunkId][0]);
            }

            // 标记模块加载完成
            installedChunks[chunkId] = 0;
        }

        // 将动态加载的模块添加到modules数组中,以供后续的require使用
        for (moduleId in moreModules) {
            if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
                modules[moduleId] = moreModules[moduleId];
            }
        }

        if (parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules);

        while (resolves.length) {
            resolves.shift()();
        }
    };

    // 模块缓存对象
    var installedModules = {};

    // 记录正在加载和已经加载的chunk的对象,0表示已经加载成功
    // 1是当前模块的编号,已加载完成
    var installedChunks = {
        1: 0
    };

    // require函数,跟上面的一样
    function __webpack_require__(moduleId) {
        if (installedModules[moduleId]) {
            return installedModules[moduleId].exports;
        }

        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;
    }

    // 按需加载,通过动态添加script标签实现
    __webpack_require__.e = function requireEnsure(chunkId) {
        var installedChunkData = installedChunks[chunkId];

        // chunk已经加载成功
        if (installedChunkData === 0) {
            return new Promise(function (resolve) {
                resolve();
            });
        }

        // 加载中,返回之前创建的promise(数组下标为2)
        if (installedChunkData) {
            return installedChunkData[2];
        }

        // 将promise相关函数保持到installedChunks中方便后续resolve或reject
        var promise = new Promise(function (resolve, reject) {
            installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        installedChunkData[2] = promise;

        // 启动chunk的异步加载
        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 + ".bundle_async.js";
        script.onerror = script.onload = onScriptComplete;
        var timeout = setTimeout(onScriptComplete, 120000);

        function onScriptComplete() {
            script.onerror = script.onload = null;

            clearTimeout(timeout);

            var chunk = installedChunks[chunkId];

            // 正常的流程,模块加载完后会调用webpackJsonp方法,将chunk置为0
            // 如果不为0,则可能是加载失败或者超时
            if (chunk !== 0) {
                if (chunk) {
                    // 调用promise的reject
                    chunk[1](new Error('Loading chunk ' + chunkId + ' failed.'));
                }
                installedChunks[chunkId] = undefined;
            }
        };

        head.appendChild(script);

        return promise;
    };

    ...

    // 加载入口模块
    return __webpack_require__(__webpack_require__.s = 0);
})
([
    /* 0 */
    (function (module, exports, __webpack_require__) {

        console.log('Hello Dickens');

        // promise resolve后,会指定加载哪个模块
        __webpack_require__.e /* import() */(0)
            .then(__webpack_require__.bind(null, 1))
            .then(logger => {
                logger.default();
            });
    })
]);

这里用户记录异步模块加载状态的对象installedChunks的数据结构如下:

[图片上传失败...(image-d3df74-1619760017928)]

当chunk加载完成后,对应的值是0。在加载过程中,对应的值是一个数组,数组内保存了promise的相关信息。

挂在到window下面的webpackJsonp函数是动态加载模块代码下载后的回调,它会通知webpack模块下载完成并将模块加入到modules当中。

__webpack_require__.e函数是动态加载的核心实现,它通过动态创建一个script标签来实现代码的异步加载。加载开始前会创建一个promise存到installedChunks对象当中,加载成功则调用resolve,失败则调用reject。resolve后不会传入模块本身,而是通过__webpack_require__来加载模块内容,require的模块id由webpack来生成:

__webpack_require__.e /* import() */(0)
    .then(__webpack_require__.bind(null, 1))
    .then(logger => {
        logger.default();
    });

这里之所以要加上default是因为遇到按需加载时,如果使用的是ES Module,webpack会将export default编译成__webpack_exports__对象的default属性(感谢@MrCanJu的指正)。详细请看动态加载的chunk的代码,0.bundle_asy的内容如下:

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

        "use strict";
        Object.defineProperty(__webpack_exports__, "__esModule", {
            value: true
        });
        /* harmony export (immutable) */
        __webpack_exports__["default"] = log;

        function log() {
            console.log('Log from logger');
        }
    })
]);

代码非常好理解,加载成功后立即调用上文提到的webpackJsonp方法,将chunkId和模块内容传入。这里要分清2个概念,一个是chunkId,一个moduleId。这个chunk的chunkId是0,里面只包含一个module,moduleId是1。一个chunk里面可以包含多个module。

执行流程如下图所示:

[图片上传失败...(image-549ae6-1619760017928)]

四、总结

本文通过分析webpack生成的启动代码,讲解了webpack是如何实现模块管理和动态加载的,希望对你有所帮助。

如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容