webpack打包后的bundle.js为何能运行在浏览器中

前言

前端开发常常涉及到webpack的使用,而打包之后的bundle.js为何能直接通过script标签通过src引入之后,就能够正常使用呢?
我们知道,浏览器和Node环境不同,不支持Commonjs的导入规范,也就是说Node中的require函数无法直接在浏览器中生效,需要进行转换。我们可以通过browserify等转换工具对其进行转换,达到在浏览器中正常运行的目的。
运行在浏览器当中的js文件,我们通常是打包在同一个文件当中的,然后通过script标签一并引入,然后便可以正常运行。webpack是如何解决模块化,并最终使得我们的代码生效的呢,本文将通过对打包之后的bundle.js的代码进行分析,理解webpack打包的代码逻辑,以及使用import函数进行按序加载后,bundle.js会发生的变化。
本文源代码来自于《深入浅出webpack》,有感兴趣的朋友可以自行查阅该书第五章原理部分。

一、目录结构和代码内容

1.1、目录结构

目录结构.png

test文件夹下面有index.html,main.js,show.js和bundle.js四个文件。其中html是我们要打开浏览的页面,这和我们通常会遇到的情况类似,通过htmlWebpackPlugin打包html模板过来一个html用于我们页面的展示。
main.js是js文件的主入口,也就是在webpack当中entry所描述的文件,webpack的打包从这里开始,递归找出所有的依赖,进行打包。
show.js内部有一个函数,被main.js所引用。
bundle.js是打包之后的文件。此处我们不讨论webpack的配置,因为我们重点关注的是为何bundle.js能直接在浏览器中运行。另外,webpack的配置如何,最终打包出来的bundle.js,其实结构都类似。随着webpack版本的更迭,最终打包出来的形式可能有所差异,但以本文的例子来理解,对今后理解wbepack也是有好处的。

1.2、index.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>
    <div id="app"></div>
    <script src="./bundle.js"></script>
</body>
</html>

1.3、main.js

const show = require('./show.js');
show('Webpack');

1.4、show.js

function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
}
module.exports = show;

1.5、bundle.js

(
    function(modules) {
        var installedModules = {};
        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;
        }
        __webpack__require__.p = "";
        return __webpack__require__(__webpack__require__.s = 0);
    }(
        [
            (function(module,exports,__webpack__require__) {
                const show = __webpack__require__(1);
                show('Webpack');
            }),
            (function(module,exports) {
                function show(content) {
                    window.document.getElementById('app').innerText = 'Hello,' + content;
                }
                module.exports = show;
            })
        ]
    )
)

二、分析bundle.js

2.1、初步理解bundle.js的结构

第一眼看到bundle.js,可能会觉得很乱,尤其是看到webpack_require这种带下划线的命名,但其实仔细看,这个bundle.js的结构,就是一个立即执行函数(IIFE),该立即执行函数的参数,是一个数组,数组内包含了bundle.js依赖的所有模块,模块用function包裹,所以我们可以看到,模块在参数中的样子,其实就是模块文件外,套了一个function的“壳子”一样。

function(modules) {
    var installedModules = {};
    function __webpack__require__(moduleId){};
    __webpack__require__.p = "";
    return __webpack__require__(__webpack__require__.s = 0);
}([module1,module2])

首先,在这个立即执行函数内,定义了一个webpack_require函数,webpack_require函数的参数,是moduleId,也就是模块的索引值。比如后面module1的索引是0,module2的索引是1。在立即执行函数内的最后一句,return webpack_require(webpack_require.s = 0),也就是说返回webpack_require(0)的结果。
其次,在这个立即执行函数内,定义了一个installedModules对象,这个对象,是用来缓存加载过的模块的,在执行webpack_require的时候,会优先查询一遍installedModules里面有没有本模块,如果有的话,直接取他的exports出来就完成了导入,而不必再去执行。
也就是说,这个立即执行函数,其实就是执行了一下webpack_require(0),然后返回webpack_require(0)的执行结果,作为这个立即执行函数的结果返回。(但我们可以看见,该立即执行函数,并没有任何变量来接收返回的结果,之所以这样,是因为我们在打包的过程中,没有配置library,如果配置了library的值为name,那么bundle.js将会产生一个var name来接收我们立即执行函数的返回值,但这不是本文的重点,此处略过)。
webpack_require.p中的p,是指的publicPath,这部分将在后面按序加载的部分详解,此处根本用不着,此处略过。

2.2、webpack_require到底做了什么

注意,不要弄混淆这里的几个变量,modules,是立即执行函数的参数,也就是模块数组。module,是指在webpack_require函数当中定义的的一个对象,他的作用是去接收模块的exports,并且缓存在installedModules当中。

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

该函数接收一个moduleId,也就是模块在数组中的索引为参数,以方便我们去modules数组(参数)中找到对应的模块(也就是包裹了一个function“壳子”的模块)。
前文也提到过,此处使用了一个installedModules来缓存读取到的模块。

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

上述代码中,modules[moduleId]拿到对应模块,然后使用call方法,传入当前module对象(也就是第五行定义的,用于接收模块导出内容的对象)等参数。
也就是说,上述代码就是执行了一遍模块代码,这也是为什么导入这个bundle.js之后,会执行我们期望引入模块代码的原因了。
接着往下走,在执行这个id为0的module的代码的时候,其实是没有exports的内容的,也就是没有导出值,我们只是在执行下面的代码。

(function(module,exports,__webpack__require__) {
    const show = __webpack__require__(1);
    show('Webpack');
})

这里我们发现,之前代码中的require,变成了__webpack_require,去引入moduleId为1的模块,拿到其exports出来的内容show函数之后,我们执行了它。__webpack_require(1),我们一样就好理解了,执行了moduleId为1的模块代码,并且取出了它的exports的值。

2.3、小结

webpack_require其实和我们commonJS的require差不多,我们可以理解为他做了以下三件事情(以webpack_require(0)为例)。
①、执行moduleId为0的模块代码。
②、拿到了moduleId为0的模块exports出来的代码并返回。
③、将加载后的模块,缓存在了installedModules中,以方便下次使用。
前两节基于打包之后的bundle.js的文件内容,分析了webpack能够让同步引入的模块代码在浏览器当中正常执行的原因,下一节将分析异步加载的内容。

三、异步加载

我们知道,当采用import函数,是异步加载其他模块的,这会导致webpack在打包的时候,将被异步加载的模块从原来的bundle当中拆分出来,形成一个单独的chunk。
那在异步加载的情况下,webpack是如何保证正常加载和使用的呢?

//bundle.js
(function (modules) {
    window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
      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];
        }
      }
      while (resolves.length) {
        resolves.shift()();
      }
    };
  
    var installedModules = {};

    var installedChunks = {
      1: 0
    };
  
    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;
    }
  
    __webpack_require__.e = function requireEnsure(chunkId) {
      var installedChunkData = installedChunks[chunkId];
      if (installedChunkData === 0) {
        return new Promise(function (resolve) {
          resolve();
        });
      }
      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;
  
      script.src = __webpack_require__.p + "" + chunkId + ".bundle.js";
  
      var timeout = setTimeout(onScriptComplete, 120000);
      script.onerror = script.onload = onScriptComplete;
  
      function onScriptComplete() {
        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;
    };
    __webpack_require__.p = '';
    return __webpack_require__(__webpack_require__.s = 0);
  })
  (
    [
      (function (module, exports, __webpack_require__) {
        __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
          show('Webpack');
        });
      })
    ]
  );
// 0.bundle.js
webpackJsonp([0],[
  /* 0 */,
  /* 1 */
  /***/ (function(module, exports) {
  
  function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
  }
  
  // 通过 CommonJS 规范导出 show 函数
  module.exports = show;
  
  
  /***/ })
  ]);

书中bundle.js和0.bundle.js在书籍勘误方面有疏忽,一些关键细节有问题,导致示例程序无法直接运行,本文中的代码经过了修正,可以直接运行。其中,出现问题的地方有两处。
第一、在整个大的自执行函数的函数体末尾,return _webpack_require(webpack_require.s = 0)这一句之前,缺少了一个webpack_require.p = ''。在异步加载的示例中,由于script标签需要一个src,而src是加上了这个p的,也就是publicPath(可以被Express中间件代理然后隐藏资源真实位置信息)。
第二、在0.bundle.js当中,webpackJsonp的第二个参数,也就是chunks数组,其实是[,(function(){})]的形式,也就是说function前面有一个逗号。如果这里没有逗号,那么该chunk会安装到modules的0号位置上,而1号位置则为空,后面就会报undefined的错误。
由于是异步加载,show函数的文件被单独拆分成了一个0.bundle.js,注意,此时0.bundle.js其实是chunk,这是理解chunk和module差别的一个好机会。chunk,翻译成中文就是“块”,也就是代码块,也就是“一堆模块”。module,是模块,webpack当中一个文件就是一个模块,chunk其实就是module的集合,chunk内部有多个module,这些module是等待被“安装”到modules数组(就是最外层大的自执行函数的参数)中的。
接下来跟着代码执行的顺序,来理解打包后的代码。

3.1、理解0.bundle.js

0.bundle.js,是拆分出去的一个chunk,以方便我们按需加载。0.bundle.js是webpackJsonp(),也就是执行webpackJsonp函数,其参数有两个,第一个是[0],第二个是一个数组[,function(){}],注意这里的逗号,非常关键。[0],也就是第一个参数,他的成员,代表的是,当本文件(也就是0.bundle.js)被加载了之后,哪些chunk可以被标记为“已安装”。这样说可能有点抽象,在整个自执行函数内,有一个installedChunks和一个installedModules,installedChunks意思就是已经安装了的chunk,如果安装成功,那么chunk的键对应的值就是0,比如。

var installedChunks = {
  0:0
  1:[function(){},function(){},promise实例]
}

上述这个installedChunks对象的键0,对应的值是0,代表的就是0这个下标的chunk已经安装好了。
先小结一下,0.bundle.js这种chunk文件,作用就是把内部的模块,安装到modules(也就是外层自执行函数的参数)数组中。

3.2、理解bundle.js

bundle.js就是我们打包之后的代码,它的形式是这样的:

function(){
  ...
}(
  [
      (function (module, exports, __webpack_require__) {
        __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
          show('Webpack');
        });
      })
    ]
)

这个自执行函数开始执行。首先会执行

return __webpack_require__(__webpack_require__.s = 0);

对第二节比较熟悉就可以知道,这里是在同步加载0号模块,接下来就执行webpack_require函数了。webpack_require函数就是同步加载模块的方法,只不过做了一个缓存。通过执行模块代码,然后用installedModules的去接收其exports的内容达到引入的目的。
所以,就会执行到这个0号的module,也就是上面的:

__webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
          show('Webpack');
});

这里开始,执行了一个webpack_require.e(0),这个e函数,也就是requireEnsure,从上面完整代码可以看出,他的作用,是通过script标签,去引入这个0号chunk,也就是0.bundle.js,并且返回一个promise。
这里是一个比较关键的地方,script有一个onload事件,onerror,和一个超时的时间,其回调函数都是onScriptComplete,作用就是让这个promise有一个确定的态。
注意,当script标签指定src后,就开始下载文件了,这里思考一下,script标签下载完之后,会先执行onScriptComplete,还是script标签内部自己的代码呢?
答案就是script标签自己的代码,script标签自己的代码,也就是webpackJsonp函数的执行,会在script标签加载完之后立马运行,而onload会在其后运行。
webpackJsonp一运行,就会给installedChunks打上标记,那些被加载过的chunk,就会标记为0,也就是加载过了,并且会改变modules参数,modules数组,就会像之前第二节一样,show函数的代码,将会出现在数组的index为1的位置上。

for (moduleId in moreModules) {
     if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
     }
}

在自执行函数内部,有一个这个步骤,他的作用,就是我们上述提到的,把chunk当中的模块,安装到modules数组当中,所以之前,在下面的代码中:

// 0.bundle.js
webpackJsonp([0],[
  /* 0 */,
  /* 1 */
  /***/ (function(module, exports) {
  
  function show(content) {
    window.document.getElementById('app').innerText = 'Hello,' + content;
  }
  
  // 通过 CommonJS 规范导出 show 函数
  module.exports = show;
  
  
  /***/ })
  ]);

第二个参数,代表的是需要安装的模块,而且在数组当中的顺序,决定了他会安装在modules的哪个位置,所以必须有那个逗号,才会保证它会被安装在modules的1号位置。
另外,再解释一下下面这段代码

window["webpackJsonp"] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) {
      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];
        }
      }
      while (resolves.length) {
        resolves.shift()();
      }
    };

其中

for (; i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if (installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
}
...
while (resolves.length) {
      resolves.shift()();
}

它的逻辑,就是把installedChunks打上标记,表示已经加载过了,那么再执行webpack_require.e(0),的时候,就可以直接返回一个fullfilled态的promise,直接执行then就可以。因为0号chunk的东西都已经加载过了,而且都放在了modules数组里。
那么当0号chunk加载完毕之后,自然加载这个chunk的promise就该变为fullfilled态了,所以才遍历了之后,再全部resolve掉。也就是说,这个webpackJsonp的作用,就是在告诉那些想要引入0号chunk的promise,已经加载ok,可以执行下一步了。当然,0.bundle.js也可能引入其他的chunk,当然chunkIds可能就不只是[0]了。

3.3、理解chunk加载后的行为

(function (module, exports, __webpack_require__) {
    __webpack_require__.e(0).then(__webpack_require__.bind(null, 1)).then((show) => {
      show('Webpack');
    });
})

3.2当中,我们花了很多的篇幅说完了webpack_require.e(0)做的事情,modules中已经有了chunk安装过来的代码,我们就可以放心的then了,由于1号module是我们需要的show函数,这里调用了webpack_require.bind(null, 1),再进行then的,为什么这里要用bind而不用call或者apply呢,因为这里是then的第一个参数,也就是onfullfilled,也就是成功的回调,需要的是一个函数,而不是函数的执行,因此bind传给他一个函数就可以了,这个函数会作为成功的回调直接执行。
这里不太熟悉promise的小伙伴可能有一点晕,可以看一下promise/A+规范,就会很清楚了。webpack_require是一个同步的加载函数,执行之后,会返回module.exports,这里解释一下,先给一个例子。

new Promise((resolve) => {
    resolve(1);
}).then(() => {
    return 2;
}).then(res => {
    console.log(res);
    // 2
})

上面的第二个then,其fullefilled函数,返回的是2,这个2会被resolve,然后传递给下一个then,类似的。

.then(__webpack_require__.bind(null, 1)).then((show) => {show('webpack')})

这个webpack_require.bind(null, 1)作为一个fullfiiled参数,返回的是module.exports,也就是show函数,会像上面的例子一样,传递给下一个then,因此show就能够正常拿到并且使用了。

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

推荐阅读更多精彩内容