前一阵子主要研究了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) { |