
桔妹导读:webpack 对于每个前端儿来说都不陌生,它将每个静态文件当成一个模块,经过一系列的处理为我们整合出最后的需要的 js、css、图片、字体等文件。你有没有好奇过 webpack 是如何工作的?
这个系列将带大家深入 webpack 源码内部,剖析它的是如何解析资源,如何使用各种不同的 loader 来处理各种资源又是如何将各个资源链接起来,打包成最终的文件的。希望通过这个系列文章,能带给大家对 webpack 机制有更深刻的认识,进而更好的辅助大家对 webpack 的使用。

1.
写在前面的话

在阅读一个东西的源码之前,首先需要了解这个东西是什么,怎么用。这样在阅读源码过程中才能在大脑中形成一副整体的认知。所以,先了解一下 webpack 打包前后代码发生了什么?找一个简单的例子。
入口文件为 main.js, 在其中引入了 a.js, b.js
// main.jsimport { A } from './a'import B from './b'console.log(A)B()
// a.jsexport const A = 'a'
// b.jsexport default function () { console.log('b')}
经过 webpack 的一番蹂躏,最后变成了一个文件:bundle.js。先忽略细节,看最外面的代码结构。
(function(modules){ ...(webpack的函数) return __webpack_require__(__webpack_require__.s = "./demo01/main.js");})( { "./demo01/a.js": (function(){...}), "./demo01/b.js": (function(){...}), "./demo01/main.js": (function(){...}), })
最外层是一个立即执行函数,参数是 modules。a.js、b.js 和 main.js 最后被编译成三个函数(下文将这三个函数称为 module 函数),key 是文件的相对路径。bundle.js 会执行到 __webpack_require__(__webpack_require__.s = "./demo01/main.js"); 即通过 __webpack_require__('./demo01/main.js') 开始主入口函数的执行。
通过 bundle.js 的主接口可以清晰的看出,对于 webpack 每个文件就是一个 module。我们写的 import 'xxx',则最终为 __webpack_require__ 函数执行。更多的时候我们使用的是 import A from 'xxx' 或者 import { B } from 'xxx' ,可以猜想一下,这个 __webpack_require__ 函数中除了找到对应的 'xxx' 来执行,还需要一个返回 'xxx' 中 export 出来的内容。
function __webpack_require__(moduleId) { // Check if module is in cache if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // Execute the module function modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports;}
调用每一个 module 函数时,参数为 module、module.exports、__webpack_require__。module.exports 用来收集 module 中所有的 export xxx 。看 ”./demo/a.js“ 的 module 。
(function(module, __webpack_exports__, __webpack_require__) {
// ...__webpack_require__.d(__webpack_exports__, "A", function() { return A; });const A = 'a'
/***/ })
// ...__webpack_require__.d = function(exports, name, getter) { if(!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); }};// Object.prototype.hasOwnProperty.call__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property);};// ...
__webpack_require__.d(__webpack_exports__, "A", function() { return A; }); 简单理解就是
__webpack_exports__.A = A;
而 __webpack_exports__ 实际为上面的 __webpack_require__ 中传入的 moule.exports, 如此,就将 A 变量收集到了 module.exports 中。如此我们的
import { A } from './a.js' console.log(A)
就编译为
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./demo/a.js"); console.log(_a__WEBPACK_IMPORTED_MODULE_0__["A"])
对于 b.js 我们使用的是 export default,webpack 处理后,会在 module.exports 中增加一个 default 属性。
__webpack_exports__["default"] = (function () { console.log('b')});
最后 import B from './b.js 编译为
var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__("./demo/b.js")Object(_b__WEBPACK_IMPORTED_MODULE_1__["default"])()
2.
异步加载

在 webpack 中我们可以很方便的实现异步加载,以简单的 demo 入手。
// c.jsexport default { key: 'something'}
// main.js import('./c').then(test => { console.log(test)})
打包结果,异步加载的 c.js,最后打包在一个单独的文件 0.js 中。
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0],{"./demo/c.js": (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); __webpack_exports__["default"] = ({ key2: 'key2' }); })}]);
简化一下,执行的就是:
var t = window["webpackJsonp"] = window["webpackJonsp"] || [])t.push([[0], {function(){...}}])
执行 import('./c.js') 时,实际上通过在 HTML 中插入一个 script 标签加载 0.js。0.js 加载后会执行 window["webpackJsonp"].push 方法。在 main.js 在还有一段:
var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];jsonpArray.push = webpackJsonpCallback;
这里篡改了一下, window["webpackJsonp"] 的 push 方法,将 push 方法外包装了一层 webpackJonspCallback 的逻辑。当 0.js 加载后,会执行 window["webpackJsonp"].push,这时便会进入 webpackJsonpCallback 的执行逻辑。
function webpackJsonpCallback(data) { var chunkIds = data[0]; var moreModules = data[1]; // add "moreModules" to the modules object, // then flag all "chunkIds" as loaded and fire callback var moduleId, chunkId, i = 0, resolves = []; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(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()(); }};
在 webpackJsonpCallback 中会将 0.js 中的 chunks 和 modules 保存到全局的 modules 变量中,并设置 installedChunks 的标志位。
有两点需要详细说明的:
1.
我们知道 import('xxx.js') 会返一个 Promise 实例 promise,在 webpack 打包出来的最终文件中是如何处理这个 promise 的?
在加载 0.js 之前会在全局 installedChunks 中先存入了一个 promise 对象。
installedChunks[chunkId] = [resolve, reject, promise]
resolve 这个值在 webpackJsonpCallback 中会被用到,这时就会进入到我们写的 import('./c.js').then() 的 then 语句中了。
2.
在 main.js 中处理 webpackJsonp 过程中还有一段特殊的逻辑:
jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);...jsonpArray = jsonpArray.slice();for(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);var parentJsonpFunction = oldJsonpFunction;
也就是说如果之前已经存在全局的 window["webpackJsonp"] 那么在替换其 push 函数之前会将原有的 push 方法保存为 oldJsonpFunction,同时将已存在于 window["webpackJsonp"] 中的内容,一一执行 webpackJsonpCallback。并且在 webpackJsonpCallback 中也将异步加载的内容也会在 parentJsonpFunction 中同样执行一次。
if(parentJsonpFunction) parentJsonpFunction(data);
这样的同步意义何在?试想下面的场景,webpack 中多入口情况下,例如如下配置:
{entry: { bundle1: 'bundle1.js', bundle2: 'bundle2.js' }}
并且 bundle1 和 bundle2 中都用到了异步加载了 0.js。而且在同一个页面中同时加载了 bundle1 和 bundle2。那么由于上面的逻辑,执行的流程如下图:
通过上图可以看到,这样设计对于多入口的地方,可以将 bundle1.js 和 bundle2.js 中异步模块进行同步,这样不仅保证了 0.js 可以同时在两个文件中被引用,而且不会重复加载。
异步加载中,有两个需要注意的地方:
Promise
在 webpack 异步加载使用了 Promise。要兼容低版本的安卓,比如4.x 的代码来说,需要有全局的 Promise polyfill。
window["webpackJsonp"]
如果一个 HTML 页面中,会加载多个 webpack 独立打包出来的文件。那么这些文件异步加载的回调函数,默认都叫 "webpackJonsp",会相互冲突。需要通过 output.jsonpFunction 配置修改这个默认的函数名称。
3.
webpack 编译总流程

知道上面的产出,根据产出看 webpack 的总流程。这里我们暂时不考虑 webpack 的缓存、错误处理、watch 等逻辑,只看主流程。首先会有一个入口文件写在配置文件中,确定 webpack 从哪个文件开始处理。
step1 : webpack 配置文件处理
我们在写配置文件中 entry 的时候,肯定写过 ./main.js这时一个相对目录,所以会有一个将相对目录变成绝对目录的处理
step2 : 文件位置解析
webpack 需要从入口文件开始,顺藤摸瓜找到所有的文件。那么会有一个
step3 : 加载文件 step4 文件解析 step5 从解析结果中找到文件引入的其他文件
在加载文件的时候,我们会在 webpack 中配置很多的 loaders 来处理 js 文件的 babel 转化等等,还应该有文件对应的 loader 解析,loader 执行。
step3.1 : 找到所有对应的 loader,然后逐个执行
处理完整入口文件之后,得到依赖的其他文件,递归进行处理。最后得到了所有文件的 module 。最终输出的是打包完成的 bundle 文件。所以会有
step4 : module 合并成 chunk 中 输出最终文件
根据 webpack 的使用和结果,我们猜测了一下 webpack 中大概的流程。然后看一下 webpack 的源码,并和我们脑中的流程对比一下。实际的 webpack 流程图如下:
对整体框架和流程有了大致的概念之后,我们可以将源码拆分为一部分一部分来详细阅读。后续会通过一系列文章一一介绍:
底层 Tapable 介绍
webpack 的底层使用的 Tapable 用来处理各种类型的 hook,这部分主要介绍 Tapable 原理,
reslove 过程
webpack 中我们所写的各种相对路径/绝对路径,alias 等是如何被处理,最终找到正确的执行文件的。
loaders 处理
写在 webpack 配置中,各种 loaders 如何被加载、解析。
module 生成
js文件如何被解析,分析出依赖,同时递归处理所有的依赖。
chunk 生成
项目中各个文件之间的依赖图的生成,以及根据定义的规则,module 最终如何聚合为 chunk。
最终文件的生成
经历了上面的所有过程后,内存中保存了生成文件的各种信息,这些信息如何整合吐出最终真正执行的所有文件。

本文作者
▬

崔 静
滴滴 | 高级软件开发工程师
2015年,因为颜控,掉入前端深坑无法自拔,也是同年加入滴滴,正式踏上前端之路。喜欢技术和挑战,爱潜水,要做一条徜徉大海的前端鱼。
同时:我们的团队正在招聘高级/资深前端开发工程师,欢迎投递简历至:cuijing@didiglobal.com。
推荐阅读
▬
更多推荐
▬
滴滴开源 / Open Source
Mpx | Booster | Chameleon | DDMQ | DroidAssist | Rdebug | Doraemonkit | Kemon | Mand Moblie | virtualApk | 获取更多项目
技术干货 / Recommended article
深度解密 Go 语言:关于 interface 的 10 个问题 | 滴滴宋世君:DS(数据分析师),究竟是做什么的? | 一个 Golang 项目的测试实践全记录 | 阅读更多内容

本文深入探讨Webpack的工作原理,从源码角度解析Webpack如何解析资源、使用loader处理不同类型的资源,以及如何将资源链接并打包成最终文件。通过具体代码示例,详细解释Webpack的模块化机制和异步加载策略。



168

被折叠的 条评论
为什么被折叠?



