Open ChuChencheng opened 4 years ago
打包之前的步骤还包括如何解析模块、生成依赖图等步骤
解析模块大概就是个把 ES 模块的语法转换成 __webpack_require__
等自有模块方法的过程,涉及如何将代码转换为 AST
在生成依赖图的时候,会从入口出发,解析入口模块,根据 AST 获取它导入的依赖,再去解析各个依赖,是一个循环遍历的过程
而打包时,就会根据上面步骤生成的依赖图,去赋值 modules
参数,注册相应的模块。
关于 jsonp 加载异步模块,这边稍微总结几句话
如何分析 Webpack 打包
准确地说,本文是分析 Webpack 打包的结果,目的是看看 Webpack 如何将每个模块(文件)组合起来,在浏览器中是如何执行的打包代码,包括如何加装异步的块。
因此,只要写个简单的项目,分析其打包出来的代码即可。
准备一个精简的项目
按照 Webpack 官方 getting started 教程初始化一个项目,然后写入以下文件:
src/index.js
:src/sync-hello.js
:src/sync-util.js
:src/async-hello.js
:就这么简单的四个文件。
分析模块依赖关系
显然,每个文件作为一个模块的话,四个文件有以下关系:
执行打包
在
package.json
中添加脚本:mode
记得设置为development
,否则打包出来的代码会是压缩混淆过的,难以分析。在
webpack.config.js
中,把 sourceMap 改一下:这是防止 Webpack 使用
eval
来打包模块,也是为了方便分析模块的代码。最后打包出来有两个文件,一个
main.js
,一个async.js
分析打包结果
打开
dist/main.js
文件,可以看到内容充满了各种注释,四个加起来不到 20 行的代码,一个入口就有 200 多行,不过这些都是实现模块化的必要代码,让我们慢慢来分析。整体结构
首先,从整体来看,可以发现整个
main.js
外层是一个自执行函数的结构:main.js
文件被加载到浏览器后,就会执行这个函数,这个函数也就相当于是 Webpack 的引导程序。函数的参数
modules
显然是我们书写的各个模块,在后面以一个对象的形式传入,接着来看看我们写的模块被转换成什么样子模块参数 modules
转到传入的参数
modules
,发现是个对象:可以看到一共是三个模块,都是同步加载的模块,异步的模块不在
main.js
里面。我们从
"./src/sync-hello.js"
这个模块入手,因为它既有import
也有export
可以看到,我们在文件中写的 ES6
import
跟export
都没了,变成了使用__webpack_require__
,__webpack_exports__
这两个参数,个人觉得,因为所有的同步模块都被打包到了同一个文件中,所以就不能再用 ES6 的模块导入导出方法,需要 Webpack 内部自己实现从同一个文件中引入不同的模块。其导入导出的风格类似 CommonJS
导入类似
require
函数,不过参数并不是路径,而是一个 id ,就是modules
参数对象的 key 值导出则是在
__webpack_exports__
上挂上导出的内容,类似exports
对象。这里导出了一个default
属性。其他的模块也是类似的,修改了
import
与export
引导函数
看完参数,就该看看函数本体了
从上述模块的改写可以知道,重点在于如何导入导出模块,因此我们重点看看
__webpack_require__
这个函数在
__webpack_require__
中,可以看到,每个模块其实是一个对象module
,其属性exports
就是模块的导出,__webpack_require__
的功能其实就是,根据moduleId
在缓存中查找对应模块执行的结果,如果没有找到,则执行modules
参数中对应moduleId
的函数,把module.exports
作为__webpack_exports__
参数传入,函数内部对__webpack_exports__
的修改,就是对module.exports
的修改。__webpack_require__
最终返回的是moduleId
对应模块的导出,也就是module.exports
定义好
__webpack_require__
后,引导函数最后执行了导入入口模块./src/index.js
,至此,一个 Webpack 应用就开始执行了。异步模块怎么办
可能都知道, Webpack 导入异步模块是用了 jsonp ,那具体是个什么样的过程呢?
我们先看看有用到异步导入的模块,也就是
./src/index.js
,在代码中是这么写的经过 Webpack 打包,变成了:
也就是说,
import()
被转换成了:那么我们来看看
__webpack_require__.e
是何方神圣下面我把异步加载 jsonp 相关的代码都揪出来了,这段代码都在顶层的自执行函数,也就是引导函数中:
直接讲一下遇到异步模块的时候是怎么一个流程吧。
webpackJsonpCallback
, 异步导入方法__webpack_require__.e
还有window["webpackJsonp"]
这个全局的变量,并重写window["webpackJsonp"].push
方法为webpackJsonpCallback
import()
,由于 Webpack 打包前的转换,会变成调用__webpack_require__.e
__webpack_require__.e
中,对于没有下载的异步模块,会用 JS 新建 script 标签的方式去下载模块的代码,并创建一个 Promise ,把 Promise 存在一个缓存中其中会执行
window["webpackJsonp"].push
方法,也就是webpackJsonpCallback
webpackJsonpCallback
中,会把异步代码中的模块都保存到modules
参数中,并且 resolve 存在缓存中对应块的 Promise__webpack_require__.e
中定义的 script 标签回调onScriptComplete
就开始处理后续,如果异步 chunk 没有成功加载,则把缓存里,即installedChunks[chunkId]
置为 undefined ,表示未加载,下次会重新再去下载这个 chunk 。根据上述步骤,可以得到一些结论:
__webpack_require__.e
负责将异步的代码块(chunk ,里面包含异步模块)通过 script 标签下载下来webpackJsonpCallback
jsonp 回调,在异步代码下载完成后,负责把异步模块注册到modules
里,并 resolve 对应的 Promise所以,
__webpack_require__.e(chunkId)
返回的是一个 Promise ,当它 resolve 的时候,表示异步模块已经被注册到modules
中,可以 require 了因此
__webpack_require__.e(chunkId)
后, Webpack 内部还要再执行一个then
:相当于:
最终返回一个新的 Promise , resolve 的值就是
__webpack_require__(moduleId)
,这样,下一个then
就能接收到异步模块导出的值了。(关于在then
里面 return 一个值会如何处理,参照 #26 )跑起来看看?
在
dist
文件夹下新建一个index.html
,引入打包后的main.js
:直接用浏览器打开这个文件,可以看到正确的结果
在开发者工具的 Network 面板中,可以看到请求了三个文件:
index.html
,main.js
,async.js
在 Elements 面板中,展开
head
标签,可以看到多了个 src 为async.js
的script
标签一切正常。