前一阵子主要研究了webpack的源码实现,这篇文章用于记录webpack打包后代码的阅读,更多的是在代码中添加自己的注释理解,使用的webpack版本是4.5.0。
主流程
先看一下最简单的代码在打包后的实现:
打包前代码:
| 1 | // index.js | 
| 1 | // util.js | 
webpack config:
| 1 | const path = require('path'); | 
打包后代码:
| 1 | (function(modules) { | 
初一看眼花缭乱的,实际上就是一个IIFE,简化后的格式其实很简单:
| 1 | (function(modules) { | 
函数的入参modules是一个对象,对象的key就是每个js模块的相对路径,value就是一个函数。IIFE会先require入口模块,然后入口模块会在执行时require其他模块例如util.js,接下来看看详细的实现。
__webpack_require__
| 1 | // The module cache | 
函数的入参是模块的id,返回值module.exports是一个对象,modules[moduleId].call就是在执行模块对应的函数,如果模块有export的东西,会放到module.exports中。同时有一个缓存对象用于存放已经require过的模块。
所以这个__webpack_require__就是来模拟import一个模块,并在最后返回所有模块export的变量。 我们再看一个示范应该就能清楚了:
| 1 | // './webpack-sourcecode/util.js' | 
__webpack_require__.d其实就是Object.defineProperty:
| 1 | // define getter function for harmony exports | 
所以util模块最后往__webpack_exports__导出了一个log变量实际上就是log函数。
其他辅助函数
__webpack_require__.r
用于标记一个ES模块:
| 1 | // define __esModule on exports | 
修改了exports的toString方法,并添加了一个__esModule成员。
__webpack_require__.t
暂时不知道干嘛的,不过函数的作用在注释中已经很清楚了。
| 1 | // create a fake namespace object | 
__webpack_require__.n
暂时也不知道干嘛的,不过应该是__webpack_require__.t配合使用的,因为都用到了同一个default变量。
| 1 | // getDefaultExport function for compatibility with non-harmony modules | 
以上就是webpack打包产物的主流程代码。
code splitting 代码分割
涉及到代码分割时,打包产物有一些变化,直观感受是多出来一些js文件。代码分割的方式有多种,详见官网文档,这里我们以古老的require.ensure做示范:
| 1 | // index.js | 
| 1 | // runtime.js | 
打包的产物与正常流程相比有一些不同之处,注意到此时有两个输出js文件了,在我的示范中一个是main.xxx.js,这是由入口文件entry生成的;另一个是0.xxx.js,这个就是我们通过require.ensure动态加载的chunk。通常我们称main的为主chunk。
主 chunk
我们最关心的是require.ensure是如何生效的,因为这个其实是webpack独有的语法,node中require函数上并没有这个属性。另外很容易发现主chunk中并没有runtime.js的代码,这是很自然的,放在一起就一起加载了。。
打包产物的关键代码:
| 1 | // "./webpack-sourcecode/index.js" | 
很明显require.ensure转化为了一个Promise,这符合我们的直觉毕竟是异步加载。另外,如果require.ensure的参数数组有多个元素,打包后代码稍微有一点不一样:
| 1 | Promise.all(/*! require.ensure */ [__webpack_require__.e(0), __webpack_require__.e(1)]).then(/**/); | 
__webpack_require__.oe就是一个很简单的打印错误不贴出来了,我们重点是_webpack_require__.e的实现方式。
| 1 | // 已加载的chunk缓存 | 
上述代码加上注释应该不难懂了,核心就是将require.ensure转化为模拟jsonp去加载目标chunk文件。
下一步看看异步加载chunk的代码。
异步 chunk
| 1 | // 0.xxx.js | 
这个chunk中只有一个module,其中的代码我们已经熟悉了,整个chunk的代码看起来很简单,就是往一个数组window['webpackJsonp']中塞入一个元素,这个数组是哪里来的呢,在哪里用到了呢? 其实它是在主chunk中有用到,主chunk除了require了入口module外,还有这么一段:
| 1 | // main.xxx.js | 
这样我们在异步chunk中执行的window['webpackJsonp'].push其实是webpackJsonpCallback函数。
webpackJsonpCallback
看名字应该能猜到,它是我们上面说的模拟jsonp完成后执行的逻辑,注意script.onload/onerror会在webpackJsonpCallback执行完后再执行,所以onload/onerror其实是用来检查webpackJsonpCallback的完成度:有没有将installedChunks中对应的chunk值设为 0.
| 1 | function webpackJsonpCallback(data) { | 
common chunk
这个其实也是一种code splitting方案,用于有多个entry的配置中提取公共代码的。为此我们需要修改下webpack config以及每个entry的代码,我们使用最新的SplitChunksPlugin来生成公共chunk。
配置
| 1 | // webpack config | 
| 1 | // index.js | 
| 1 | // runtime.js | 
util.js中的代码不做修改,按照期望会有 3 个文件生成:main.xxx.js、runtime.xxx.js,公共代码抽取生成的chunk按照SplitChunksPlugin的配置应该是default~main~runtime.xxx.js。
主 chunk 代码
生成的打包产物main.xxx.js、runtime.xxx.js基本一致,因为它俩现在地位相同都是主chunk:都不会有util.js的代码,同时相较于require.ensure生成的代码有了一些变化,我们先从如何加载default~main~runtime.xxx.js看起:
| 1 | (function(modules) { | 
看起来像是__webpack_require__的实现有变化,但查看代码却发现不是这样,仔细对比发现是 IIFE 里的代码有不同,加载index.js的方式变了:
| 1 | var deferredModules = []; | 
这次又多出来个数组,看看checkDeferredModules:
| 1 | function checkDeferredModules() { | 
ok 现在我们明白了main.xxx.js这个chunk中的index.js什么时候才能执行了,就是等到default~main~runtime这个common chunk加载完才能执行。那么剩下的问题就是在何时会去加载default~main~runtime呢?因为checkDeferredModules并没有做这件事。
很遗憾整个主chunk中都没有找到这样的代码,连模拟jsonp的代码都没有,直到在插件文档中找到这样一段话:
You can combine this configuration with the HtmlWebpackPlugin. It will inject all the generated vendor chunks for you.
也就是说插件会自动将生成的chunk插入到html中,查看我们实际的某个项目发现确实如此:
| 1 | // webpack config, 注意只有一个entry | 
| 1 | <!-- 模板: index.template.html --> | 
| 1 | <!-- 打包产物 dist/index.html, 除了index.xxx.js是在webpack中配置的entry,其余script都是插件替我们添加的 --> | 
TODO: SplitChunksPlugin源码分析。
终于真想大白,浏览器会替我们加载common chunk,接下来看看default~main~runtime。
common chunk 代码
| 1 | (window['webpackJsonp'] = window['webpackJsonp'] || []).push([ | 
这段代码很熟悉和require.ensure的产物一样就不说了。push实际调用的是webpackJsonpCallback,仔细相关其实需要在webpackJsonpCallback中再执行一次checkDeferredModules,因为只有这样我们的主chunk所有依赖chunk才能被标记为加载完成,后续才能require主module。实际上确实是这样:
webpackJsonpCallback
| 1 | function webpackJsonpCallback(data) { | 
绝大部分与require.ensure版本一样,executeModules应该是这个common chunk依赖的chunk??
与我们的猜测一致,每个common chunk在加载完之后,都会检查所属主chunk的所有依赖chunk是否都加载完成,之后才能去require主module。
Tree Shaking
webpack 本身并不会删除任何多余的代码,删除无用代码的工作是 UglifyJS做的. webpack 做的事情是 unused harmony exports 的标记,也就是他会分析你的代码,把不用的 exports 删除,但是变量的声明并没有删除。
具体的 Tree shaking 这里就不叙述了,webpack 官网和这篇博客都讲的很清楚。
注意: 在自己做测试时,发现需要webpack config添加一个额外的配置才起作用:
| 1 | optimization: { | 
这个在webpack源码的WebpackOptionsApply.js中有标明:
| 1 | if (options.optimization.usedExports) { |