「Webpack进阶」Webpack打包后的代码是怎样的?
webpack
是我们现阶段要掌握的重要的打包工具之一,我们知道 webpack
会递归的构建依赖关系图,其中包含应用程序的每个模块,然后将这些模块打包成一个或者多个 bundle
。
那么webpack
打包后的代码是怎样的呢?是怎么将各个 bundle
连接在一起的?模块与模块之间的关系是怎么处理的?动态 import()
的时候又是怎样的呢?
本文让我们一步步来揭开 webpack
打包后代码的神秘面纱
准备工作
创建一个文件,并初始化
mkdir learn-webpack-output |
根目录中新建一个文件 webpack.config.js
,这个是 webpack
默认的配置文件
const path = require('path'); |
然后我们回到 package.json
文件中,在 npm script
中添加启动 webpack
配置的命令
"scripts": { |
新建一个 src
文件夹,新增 index.js
文件和 sayHello
文件
// src/index.js |
一切准备完毕,执行 yarn build
分析主流程
看输出文件,这里不放具体的代码,有点占篇幅,可以点击这里查看
其实就是一个 IIFE
莫慌,我们一点点拆分开看,其实总体的文件就是一个 IIFE
——立即执行函数。
(function(modules) { // webpackBootstrap |
函数的入参 modules
是一个对象,对象的 key
就是每个 js
模块的相对路径,value
就是一个函数(我们下面称之为模块函数)。IIFE
会先 require
入口模块。即上面就是 ./src/index.js
:
// 入口文件 |
然后入口模块会在执行时 require
其他模块例如 ./src/sayHello.js"
以下为简化后的代码,从而不断的加载所依赖的模块,形成依赖树,比如如下的模块函数中就引用了其他的文件 sayHello.js
{ |
重要的实现机制——__webpack_require__
这里去 require
其他模块的函数主要是 __webpack_require__
。接下来主要介绍一下 __webpack_require__
这个函数
// 缓存模块使用 |
第一步,webpack
这里做了一层优化,通过对象 installedModules
进行缓存,检查模块是否在缓存中,有则直接从缓存中获取,没有则创建并放入缓存中,其中 key
值就是模块 Id
,也就是上面所说的文件路径
第二步,然后执行模块函数,将 module
, module.exports
, __webpack_require__
作为参数传递,并把模块的函数调用对象指向 module.exports
,保证模块中的 this
指向永远指向当前的模块。
第三步,最后返回加载的模块,调用方直接调用即可。
所以这个__webpack_require__
就是来加载一个模块,并在最后返回模块 module.exports
变量
webpack 是如何支持 ESM 的
可能大家已经发现,我上面的写法是 ESM
的写法,对于模块化的一些方案的了解,可以看看我的另外一篇文章【面试说】Javascript 中的 CJS, AMD, UMD 和 ESM是什么?
我们重新看回模块函数
{ |
我们看看 __webpack_require__.r
函数
__webpack_require__.r = function(exports) { |
就是为 __webpack_exports__
添加一个属性 __esModule
,值为 true
再看一个 __webpack_require__.n
的实现
// getDefaultExport function for compatibility with non-harmony modules |
__webpack_require__.n
会判断module是否为es模块,当__esModule
为 true 的时候,标识 module 为es 模块,默认返回module.default
,否则返回module
。
最后看 __webpack_require__.d
,主要的工作就是将上面的 getter
函数绑定到 exports 中的属性 a 的 getter
上
// define getter function for harmony exports |
我们最后再看会 sayHello.js
打包后的模块函数,可以看到这里的导出是 __webpack_exports__["default"]
,实际上就是 __webpack_require__.n
做了一层包装来实现的,其实也可以看出,实际上 webpack
是可以支持 CommonJS
和 ES Module
一起混用的
"./src/sayHello.js": |
目前为止,我们大致知道了 webpack
打包出来的文件是怎么作用的了,接下来我们分析下代码分离的一种特殊场景——动态导入
动态导入
代码分离是 webpack
中最引人注目的特性之一。此特性能够把代码分离到不同的 bundle
中,然后可以按需加载或并行加载这些文件。代码分离可以用于获取更小的 bundle
,以及控制资源加载优先级,如果使用合理,会极大影响加载时间。
常见的代码分割有以下几种方法:
- 入口起点:使用
entry
配置手动地分离代码。 - 防止重复:使用 Entry dependencies 或者
SplitChunksPlugin
去重和分离 chunk。 - 动态导入:通过模块的内联函数调用来分离代码。
本文我们主要看看动态导入,我们在 src
下面新建一个文件 another.js
function Another() { |
修改 index.js
import sayHello from './sayHello'; |
我们来看下打包出来的内容,忽略 .map 文件,可以看到多出一个 0.bundle.js
文件,这个我们称它为动态加载的 chunk
,bundle.js
我们称为主 chunk
输出的代码的话,主 chunk
看这里,动态加载的 chunk
看这里 ,下面是针对这两份代码的分析
主 chunk 分析
我们先来看看主 chunk
内容多了很多,我们来细看一下:
首先我们注意到,我们动态导入的地方编译后变成了以下,这是看起来就像是一个异步加载的函数
if (true) { |
所以我们来看 __webpack_require__.e
这个函数的实现
__webpack_require__.e
——使用 JSONP 动态加载
// 已加载的chunk缓存 |
可以看出将
import()
转换成模拟JSONP
去加载动态加载的chunk
文件设置
chunk
加载的三种状态并缓存在installedChunks
中,防止chunk重复加载。这些状态的改变会在webpackJsonpCallback
中提到// 设置 installedChunks[chunkId]
installedChunkData = installedChunks[chunkId] = [resolve, reject];
复制代码installedChunks[chunkId]
为0
,代表该chunk
已经加载完毕installedChunks[chunkId]
为undefined
,代表该chunk
加载失败、加载超时、从未加载过installedChunks[chunkId]
为Promise
对象,代表该chunk
正在加载
看完__webpack_require__.e
,我们知道的是,我们通过 JSONP 去动态引入 chunk
文件,并根据引入的结果状态进行处理,那么我们怎么知道引入之后的状态呢?我们来看异步加载的 chunk
是怎样的
异步 Chunk
// window["webpackJsonp"] 实际上是一个数组,向中添加一个元素。这个元素也是一个数组,其中数组的第一个元素是chunkId,第二个对象,跟传入到 IIFE 中的参数一样 |
主要做的事情就是往一个数组 window['webpackJsonp']
中塞入一个元素,这个元素也是一个数组,其中数组的第一个元素是 chunkId
,第二个对象,跟主 chunk
中 IIFE 传入的参数类似。关键是这个 window['webpackJsonp']
在哪里会用到呢?我们回到主 chunk
中。在 return __webpack_require__(__webpack_require__.s = "./src/index.js");
进入入口之前还有一段
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; |
jsonpArray
就是 window["webpackJsonp"]
,重点看下面这一句代码,当执行 push
方法的时候,就会执行 webpackJsonpCallback
,相当于做了一层劫持,也就是执行完 push 操作的时候就会调用这个函数
jsonpArray.push = webpackJsonpCallback; |
webpackJsonpCallback ——加载完动态 chunk 之后的回调
我们再来看看 webpackJsonpCallback
函数,这里的入参就是动态加载的 chunk
的 window['webpackJsonp']
push 进去的参数。
var installedChunks = { |
当我们 JSONP
去加载异步 chunk
完成之后,就会去执行 window["webpackJsonp"] || []).push
,也就是 webpackJsonpCallback
。主要有以下几步
- 遍历要加载的 chunkIds,找到未执行完的 chunk,并加入到 resolves 中
for(;i < chunkIds.length; i++) { |
这里未执行的是非 0 状态,执行完就设置为0
installedChunks[chunkId][0]
实际上就是 Promise 构造函数中的 resolve// __webpack_require__.e
var promise = new Promise(function(resolve, reject) {
installedChunkData = installedChunks[chunkId] = [resolve, reject];
});
复制代码挨个将异步
chunk
中的module
加入主chunk
的modules
数组中原始的数组
push
方法,将data
加入window["webpackJsonp"]
数组执行各个
resolves
方法,告诉__webpack_require__.e
中回调函数的状态
只有当这个方法执行完成的时候,我们才知道 JSONP
成功与否,也就是script.onload/onerror
会在 webpackJsonpCallback
之后执行。所以 onload/onerror
其实是用来检查 webpackJsonpCallback
的完成度:有没有将 installedChunks
中对应的 chunk
值设为 0
动态导入小结
大致的流程如下图所示
总结
本篇文章分析了 webpack
打包主流程以及和动态加载情况下输出代码,总结如下
- 总体的文件就是一个
IIFE
——立即执行函数 webpack
会对加载过的文件进行缓存,从而优化性能- 主要是通过
__webpack_require__
来模拟import
一个模块,并在最后返回模块export
的变量 webpack
是如何支持ES Module
的- 动态加载
import()
的实现主要是使用JSONP
动态加载模块,并通过webpackJsonpCallback
判断加载的结果