We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
准确地说,本文是分析 Webpack 打包的结果,目的是看看 Webpack 如何将每个模块(文件)组合起来,在浏览器中是如何执行的打包代码,包括如何加装异步的块。
因此,只要写个简单的项目,分析其打包出来的代码即可。
按照 Webpack 官方 getting started 教程初始化一个项目,然后写入以下文件:
src/index.js:
src/index.js
// 同步引入 import syncHello from './sync-hello' document.querySelector('#text').innerText += `${syncHello}\n` // 异步引入 import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => { document.querySelector('#text').innerText += `${asyncHello}\n` })
src/sync-hello.js:
src/sync-hello.js
import generateHello from './sync-util' export default generateHello('sync code')
src/sync-util.js:
src/sync-util.js
const generateHello = (source) => { return `Hello from ${source}` } export default generateHello
src/async-hello.js:
src/async-hello.js
import generateHello from './sync-util' export default generateHello('async code')
就这么简单的四个文件。
显然,每个文件作为一个模块的话,四个文件有以下关系:
index.js(入口) / \ sync-hello async-hello / / | / sync-util
在 package.json 中添加脚本:
package.json
webpack --mode development --config webpack.config.js
mode 记得设置为 development ,否则打包出来的代码会是压缩混淆过的,难以分析。
mode
development
在 webpack.config.js 中,把 sourceMap 改一下:
webpack.config.js
module.exports = { devtool: 'inline-source-map' }
这是防止 Webpack 使用 eval 来打包模块,也是为了方便分析模块的代码。
eval
最后打包出来有两个文件,一个 main.js ,一个 async.js
main.js
async.js
打开 dist/main.js 文件,可以看到内容充满了各种注释,四个加起来不到 20 行的代码,一个入口就有 200 多行,不过这些都是实现模块化的必要代码,让我们慢慢来分析。
dist/main.js
首先,从整体来看,可以发现整个 main.js 外层是一个自执行函数的结构:
(function (modules) { // webpack bootstrap })({ // modules })
main.js 文件被加载到浏览器后,就会执行这个函数,这个函数也就相当于是 Webpack 的引导程序。
函数的参数 modules 显然是我们书写的各个模块,在后面以一个对象的形式传入,接着来看看我们写的模块被转换成什么样子
modules
转到传入的参数 modules ,发现是个对象:
{ "./src/index.js": (function (module, __webpack_exports__, __webpack_require__) { // 模块内容 }) "./src/sync-hello.js": (function (module, __webpack_exports__, __webpack_require__) { // 模块内容 }) "./src/sync-util.js": (function (module, __webpack_exports__, __webpack_require__) { // 模块内容 }) }
可以看到一共是三个模块,都是同步加载的模块,异步的模块不在 main.js 里面。
我们从 "./src/sync-hello.js" 这个模块入手,因为它既有 import 也有 export
"./src/sync-hello.js"
import
export
{ "./src/sync-hello.js": (function(module, __webpack_exports__, __webpack_require__) { // 模块默认是严格模式 "use strict"; // 在 exports 中加上 __esModule 属性,表示是 ES 模块 __webpack_require__.r(__webpack_exports__); // 导入 sync-util 模块 var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js"); // 导出此模块 __webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('sync code')); }) }
可以看到,我们在文件中写的 ES6 import 跟 export 都没了,变成了使用 __webpack_require__, __webpack_exports__ 这两个参数,个人觉得,因为所有的同步模块都被打包到了同一个文件中,所以就不能再用 ES6 的模块导入导出方法,需要 Webpack 内部自己实现从同一个文件中引入不同的模块。
__webpack_require__
__webpack_exports__
其导入导出的风格类似 CommonJS
导入类似 require 函数,不过参数并不是路径,而是一个 id ,就是 modules 参数对象的 key 值
require
导出则是在 __webpack_exports__ 上挂上导出的内容,类似 exports 对象。这里导出了一个 default 属性。
exports
default
其他的模块也是类似的,修改了 import 与 export
看完参数,就该看看函数本体了
从上述模块的改写可以知道,重点在于如何导入导出模块,因此我们重点看看 __webpack_require__ 这个函数
(function (modules) { // 模块缓存 var installedModules = {} // The require function 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; // 返回模块的导出(exports) return module.exports; } // ...定义了一堆东西 // Load entry module and return exports return __webpack_require__(__webpack_require__.s = "./src/index.js"); })({ // modules })
在 __webpack_require__ 中,可以看到,每个模块其实是一个对象 module ,其属性 exports 就是模块的导出, __webpack_require__ 的功能其实就是,根据 moduleId 在缓存中查找对应模块执行的结果,如果没有找到,则执行 modules 参数中对应 moduleId 的函数,把 module.exports 作为 __webpack_exports__ 参数传入,函数内部对 __webpack_exports__ 的修改,就是对 module.exports 的修改。 __webpack_require__ 最终返回的是 moduleId 对应模块的导出,也就是 module.exports
module
moduleId
module.exports
定义好 __webpack_require__ 后,引导函数最后执行了导入入口模块 ./src/index.js ,至此,一个 Webpack 应用就开始执行了。
./src/index.js
可能都知道, Webpack 导入异步模块是用了 jsonp ,那具体是个什么样的过程呢?
我们先看看有用到异步导入的模块,也就是 ./src/index.js ,在代码中是这么写的
import(/* webpackChunkName: "async" */ './async-hello').then(({ default: asyncHello }) => { document.querySelector('#text').innerText += `${asyncHello}\n` })
经过 Webpack 打包,变成了:
__webpack_require__.e(/*! import() | async */ "async").then(__webpack_require__.bind(null, /*! ./async-hello */ "./src/async-hello.js")).then(({ default: asyncHello }) => { document.querySelector('#text').innerText += `${asyncHello}\n` })
也就是说, import() 被转换成了:
import()
__webpack_require__.e(chunkId).then(__webpack_require__.bind(null, moduleId))
那么我们来看看 __webpack_require__.e 是何方神圣
__webpack_require__.e
下面我把异步加载 jsonp 相关的代码都揪出来了,这段代码都在顶层的自执行函数,也就是引导函数中:
// 加载代码块的 jsonp 回调 function webpackJsonpCallback (data) { var chunkIds = data[0]; var moreModules = data[1]; // 把 data 参数数组的第二个元素添加到 modules 对象中(也就是引导函数的 modules 参数) // 把 chunkIds 里的元素都标记为已加载(即在 installedChunks 中置为 0 ) var moduleId, chunkId, i = 0, resolves = []; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && 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]; } } if (parentJsonpFunction) parentJsonpFunction(data); while (resolves.length) { resolves.shift()(); } }; // 存储已加载或正在加载的块(chunk) // undefined = chunk 未加载, null = chunk preloaded/prefetched // Promise = chunk 正在加载, 0 = chunk 已加载 var installedChunks = { "main": 0 }; // script path function function jsonpScriptSrc(chunkId) { return __webpack_require__.p + "" + ({"async":"async"}[chunkId]||chunkId) + ".js" } // 由于 main.js 只包含入口的块(chunk),因此提供以下函数 // 加载额外(异步)块的函数 __webpack_require__.e = function requireEnsure (chunkId) { var promises = []; // JSONP chunk loading for javascript var installedChunkData = installedChunks[chunkId]; if (installedChunkData !== 0) { // 0 表示 "已安装". // 一个 Promise 表示 "正在加载". if (installedChunkData) { promises.push(installedChunkData[2]); } else { // installedChunks[chunkId] === undefined , 表示此 chunk 未加载 // 因此将 installedChunks[chunkId] 赋值为 Promise 表示正在加载这个 chunk var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); promises.push(installedChunkData[2] = promise); // start chunk loading var script = document.createElement('script'); var onScriptComplete; script.charset = 'utf-8'; script.timeout = 120; if (__webpack_require__.nc) { script.setAttribute("nonce", __webpack_require__.nc); } script.src = jsonpScriptSrc(chunkId); // create error before stack unwound to get useful stacktrace later var error = new Error(); // script 标签下载、执行完成后的回调 onScriptComplete = function (event) { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if (chunk !== 0) { if (chunk) { var errorType = event && (event.type === 'load' ? 'missing' : event.type); var realSrc = event && event.target && event.target.src; error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')'; error.name = 'ChunkLoadError'; error.type = errorType; error.request = realSrc; chunk[1](error); } installedChunks[chunkId] = undefined; } }; // 2 分钟超时 var timeout = setTimeout(function () { onScriptComplete({ type: 'timeout', target: script }); }, 120000); script.onerror = script.onload = onScriptComplete; // 把 script 标签插入 html ,开始下载异步模块 document.head.appendChild(script); } } return Promise.all(promises); }; var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || []; var oldJsonpFunction = jsonpArray.push.bind(jsonpArray); // 重写 push 方法,因此 script 标签下载完成时执行的 jsonp 回调就是 webpackJsonpCallback jsonpArray.push = webpackJsonpCallback; jsonpArray = jsonpArray.slice(); for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]); // parentJsonpFunction 就是数组原来的 push 方法 var parentJsonpFunction = oldJsonpFunction;
直接讲一下遇到异步模块的时候是怎么一个流程吧。
webpackJsonpCallback
window["webpackJsonp"]
window["webpackJsonp"].push
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([["async"], { "./src/async-hello.js": (function (module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); var _sync_util__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/sync-util.js"); __webpack_exports__["default"] = (Object(_sync_util__WEBPACK_IMPORTED_MODULE_0__["default"])('async code')); }) }]);
其中会执行 window["webpackJsonp"].push 方法,也就是 webpackJsonpCallback
onScriptComplete
installedChunks[chunkId]
根据上述步骤,可以得到一些结论:
所以, __webpack_require__.e(chunkId) 返回的是一个 Promise ,当它 resolve 的时候,表示异步模块已经被注册到 modules 中,可以 require 了
__webpack_require__.e(chunkId)
因此 __webpack_require__.e(chunkId) 后, Webpack 内部还要再执行一个 then :
then
相当于:
__webpack_require__.e(chunkId).then(() => { return __webpack_require__(moduleId) })
最终返回一个新的 Promise , resolve 的值就是 __webpack_require__(moduleId) ,这样,下一个 then 就能接收到异步模块导出的值了。(关于在 then 里面 return 一个值会如何处理,参照 #26 )
__webpack_require__(moduleId)
在 dist 文件夹下新建一个 index.html ,引入打包后的 main.js :
dist
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="text"></div> <script src="./main.js"></script> </body> </html>
直接用浏览器打开这个文件,可以看到正确的结果
在开发者工具的 Network 面板中,可以看到请求了三个文件: index.html, main.js, async.js
在 Elements 面板中,展开 head 标签,可以看到多了个 src 为 async.js 的 script 标签
head
script
一切正常。
The text was updated successfully, but these errors were encountered:
打包之前的步骤还包括如何解析模块、生成依赖图等步骤
解析模块大概就是个把 ES 模块的语法转换成 __webpack_require__ 等自有模块方法的过程,涉及如何将代码转换为 AST
在生成依赖图的时候,会从入口出发,解析入口模块,根据 AST 获取它导入的依赖,再去解析各个依赖,是一个循环遍历的过程
而打包时,就会根据上面步骤生成的依赖图,去赋值 modules 参数,注册相应的模块。
Sorry, something went wrong.
关于 jsonp 加载异步模块,这边稍微总结几句话
No branches or pull requests
如何分析 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
标签一切正常。
The text was updated successfully, but these errors were encountered: