|
| 1 | +--- |
| 2 | +layout: post |
| 3 | +title: "webpack-源码分析" |
| 4 | +description: "webpack-源码分析" |
| 5 | +category: tech |
| 6 | +tags: ['webpack'] |
| 7 | +--- |
| 8 | +{% include JB/setup %} |
| 9 | + |
| 10 | +# 感受 |
| 11 | + |
| 12 | +先说下阅读感受,一开始打开webpack仓库,看到100多个文件,我的内心是懵逼的。然后看到一大半都是plugin,心中窃喜这些应该是不需要了解的。再后来读着读着发现很多想了解的功能实现都是通过plugin的。。。 |
| 13 | + |
| 14 | +webpack的代码真的还是很复杂的,感受最深的是其利用tapable来实现的一套插件体系,扩展性很好,设计者还是很机智的,在这套体系上,内部近百个插件有条不紊,还能支持外部开发自定义插件来扩展功能,如果不是这个机制,整个庞大的构建流会更迷吧。支持这么多功能的构建工具开发出来复杂度真的挺高的。 |
| 15 | + |
| 16 | +主流程还是比较清晰的在compiler里,但是去看compiler和compilation的生命周期就各有几十个,每个调用一批插件协调处理,如果不去debug,这个事件触发机制根本就找不到会触发哪些插件的事件回调,根本无法也没有意义一一看完,只能说后面有需要时再看具体的了。 |
| 17 | + |
| 18 | +抓住主线搞清整体流程,`make`, `seal`, `render`,`emitAssets`这些核心步骤实现,可以通过通过几个常用的loader和plugin了解了下这两部分的实现。 |
| 19 | + |
| 20 | +我向往的境界是读了能了解设计思路,放开徒手也能写出核心逻辑代码实现,再以后对新东西能自行通过api特征就推断出实现方式,然后读源码验证一下。显然差的还远,目前也就勉强够用,能了解到实现方式,能够知道去哪一块debug来解决遇到的问题,能够写loader和plugin扩展个性化功能。我没有在一堆源码里迷失主线,活着出来了已是万幸了。。。 |
| 21 | + |
| 22 | +# 概况 |
| 23 | + |
| 24 | +初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件 |
| 25 | + |
| 26 | +网上有个复杂的图,画清楚了这个步骤: |
| 27 | + |
| 28 | + |
| 29 | + |
| 30 | + |
| 31 | +webpack主要的2个部分是Compiler和Compilation,Compiler基本上只是执行最低限度的功能,以维持生命周期运行的功能。它将所有的加载、打包和写入工作,都委托到注册过的插件上。它只是构建任务调度器,而compilation则是具体的构建内容步骤 |
| 32 | + |
| 33 | +# tapable机制 |
| 34 | + |
| 35 | +要搞清webpack,还是先老老实实花时间搞清楚tapable机制,否则没法理解插件是怎么注册触发的。 |
| 36 | + |
| 37 | +tapable提供类似的插件接口。webpack 中许多对象扩展自 Tapable 类。这个类暴露 tap, tapAsync 和 tapPromise 方法,可以使用这些方法,注入自定义的构建步骤,这些步骤将在整个编译过程中不同时机触发。 |
| 38 | + |
| 39 | +tapable提供了很多类型的钩子,如同步的`SyncHook`和异步的`AsyncHook`。我们一开始创建钩子类型和可获取参数,相当于规范了这个钩子的模样,然后在这个钩子上注册插件,后续再在触发这个钩子上的插件,传入规范定的参数,进而触发插件注册的处理函数。 |
| 40 | + |
| 41 | +example: |
| 42 | + |
| 43 | +```javascript |
| 44 | +// 创建钩子 |
| 45 | +class Car { |
| 46 | + constructor() { |
| 47 | + this.hooks = { |
| 48 | + accelerate: new SyncHook(["newSpeed"]), |
| 49 | + brake: new SyncHook(), |
| 50 | + calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) |
| 51 | + }; |
| 52 | + } |
| 53 | + // 调用对应的钩子,会触发注册的插件回调函数 |
| 54 | + setSpeed(newSpeed) { |
| 55 | + this.hooks.accelerate.call(newSpeed); |
| 56 | + } |
| 57 | + useNavigationSystemPromise(source, target) { |
| 58 | + const routesList = new List(); |
| 59 | + return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => { |
| 60 | + return routesList.getRoutes(); |
| 61 | + }); |
| 62 | + } |
| 63 | + |
| 64 | + useNavigationSystemAsync(source, target, callback) { |
| 65 | + const routesList = new List(); |
| 66 | + this.hooks.calculateRoutes.callAsync(source, target, routesList, err => { |
| 67 | + if(err) return callback(err); |
| 68 | + callback(null, routesList.getRoutes()); |
| 69 | + }); |
| 70 | + } |
| 71 | + /* ... */ |
| 72 | +} |
| 73 | + |
| 74 | +// 注册插件 |
| 75 | +const myCar = new Car(); |
| 76 | +// Use the tap method to add a consument |
| 77 | +myCar.hooks.brake.tap("WarningLampPlugin", () => warningLamp.on()); |
| 78 | +myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`)); |
| 79 | +myCar.hooks.calculateRoutes.tapPromise("GoogleMapsPlugin", (source, target, routesList) => { |
| 80 | + // return a promise |
| 81 | + return google.maps.findRoute(source, target).then(route => { |
| 82 | + routesList.add(route); |
| 83 | + }); |
| 84 | +}); |
| 85 | +myCar.hooks.calculateRoutes.tapAsync("BingMapsPlugin", (source, target, routesList, callback) => { |
| 86 | + bing.findRoute(source, target, (err, route) => { |
| 87 | + if(err) return callback(err); |
| 88 | + routesList.add(route); |
| 89 | + // call the callback |
| 90 | + callback(); |
| 91 | + }); |
| 92 | +}); |
| 93 | + |
| 94 | +``` |
| 95 | + |
| 96 | +详情可见[https://webpack.docschina.org/api/plugins/#tapable](https://webpack.docschina.org/api/plugins/#tapable) |
| 97 | + |
| 98 | +代码[https://github.com/webpack/tapable](https://github.com/webpack/tapable) |
| 99 | + |
| 100 | +## webpack中的hooks |
| 101 | + |
| 102 | +利用tapable这样的设计就做到了将webpack功能碎片化,每个阶段拆成一个hooks,然后各种内外部插件都可以往这个hooks上注册插件,webpack在编译过程中的合适阶段会调用对应的钩子,从而触发一系列挂载在这个钩子上的插件。这样让webpack的每个构建过程的功能都可以无限扩展和定制。实现了一个强大的系统。 |
| 103 | + |
| 104 | +如complilation上注册了几十个hooks,在编译的不同阶段会被触发: |
| 105 | + |
| 106 | +```javascript |
| 107 | +class Compilation extends Tapable { |
| 108 | + /** |
| 109 | + * Creates an instance of Compilation. |
| 110 | + * @param {Compiler} compiler the compiler which created the compilation |
| 111 | + */ |
| 112 | + constructor(compiler) { |
| 113 | + super(); |
| 114 | + this.hooks = { |
| 115 | + /** @type {SyncHook<Module>} */ |
| 116 | + buildModule: new SyncHook(["module"]), |
| 117 | + /** @type {SyncHook<Module>} */ |
| 118 | + rebuildModule: new SyncHook(["module"]), |
| 119 | + /** @type {SyncHook<Module, Error>} */ |
| 120 | + failedModule: new SyncHook(["module", "error"]), |
| 121 | + /** @type {SyncHook<Module>} */ |
| 122 | + succeedModule: new SyncHook(["module"]), |
| 123 | + |
| 124 | + /** @type {SyncHook<Dependency, string>} */ |
| 125 | + addEntry: new SyncHook(["entry", "name"]), |
| 126 | + /** @type {SyncHook<Dependency, string, Error>} */ |
| 127 | + failedEntry: new SyncHook(["entry", "name", "error"]), |
| 128 | + /** @type {SyncHook<Dependency, string, Module>} */ |
| 129 | + succeedEntry: new SyncHook(["entry", "name", "module"]), |
| 130 | + |
| 131 | + /** @type {SyncWaterfallHook<DependencyReference, Dependency, Module>} */ |
| 132 | + dependencyReference: new SyncWaterfallHook([ |
| 133 | + "dependencyReference", |
| 134 | + "dependency", |
| 135 | + "module" |
| 136 | + ]), |
| 137 | + |
| 138 | + /** more **/ |
| 139 | + } |
| 140 | + } |
| 141 | +} |
| 142 | +``` |
| 143 | + |
| 144 | +webpack的主要功能都是通过plugin以`this.hooks.xxx.tap('pluginName', fn)`挂载到hooks上,在特定时机执行。 |
| 145 | +执行时即为`this.hooks.xxx.call('args', callback)`方式,清楚这个模式后,看源码就轻松很多。 |
| 146 | + |
| 147 | +# 流程 |
| 148 | + |
| 149 | +创建compiler,`WebpackOptionsApply.process` 会根据配置项注册对应的内部插件,compiler.run进入核心流程。 |
| 150 | +webpack的强大在于在生命周期调用对应的插件处理 |
| 151 | + |
| 152 | +关键生命周期: |
| 153 | +* entry-options |
| 154 | +* compile |
| 155 | +* make: 分析入口,创建模块对象 |
| 156 | +* build-module: 构建模块 |
| 157 | +* after-compile: |
| 158 | +* emit: seal封装。生成assets |
| 159 | +* after-emit: render组合输出 |
| 160 | + |
| 161 | +翻阅源码时重点查看如下关键函数: |
| 162 | + |
| 163 | +* compile |
| 164 | +* make |
| 165 | +* compilation.addEntry |
| 166 | +* compilation: _addModuleChain |
| 167 | +* buildModule(NormalModule.js) |
| 168 | +* build - doBuild(NormalModule.js) |
| 169 | +* doBuild - runLoaders: 调用loaders, 任何模块都被转成了标准的JS模块 |
| 170 | +* parse: 获得ast,调用 acorn 对JS代码进行语法分析,然后收集其中的依赖关系 |
| 171 | +* seal - createChunkAssets - mainTemplate.render, render的renderBootStrap是生成webpack样板代码,template.js的renderChunkModules是真正拼接模块代码字符串的地方。根据之前收集的依赖,决定生成多少文件,每个文件的内容是什么。 |
| 172 | +* render:这里是生成每个模块的代码片段 |
| 173 | +* emitAssets: 将各模块的代码片段整合输出到chunk文件中 |
| 174 | + |
| 175 | +# 其他一些记录点 |
| 176 | + |
| 177 | +## 插件调用时是在创建的临时函数里 |
| 178 | + |
| 179 | +调用的插件处理方法都是通过new Function通过字符串拼接生成的。这个函数不止是开发定义的函数,前后还有很多webpack加入的内容,每个函数都不同。 |
| 180 | + |
| 181 | +注册插件时,会将要调用的函数先生成好放在柯里化的返回函数里,然后在调用时进入执行,并接收所需参数 |
| 182 | + |
| 183 | + |
| 184 | + |
| 185 | +譬如`this.hooks.make.callAsync(compilation,...`调用时的临时函数有: |
| 186 | + |
| 187 | + |
| 188 | + |
| 189 | +意义何在?我理解是因为多样性,为了给每个插件调用加入不同的上下文执行代码。 |
| 190 | + |
| 191 | +## loader |
| 192 | + |
| 193 | +loader是把非js模块处理成js模块 |
| 194 | + |
| 195 | +runLoaders返回的result里是含有buffer原数据的数组,经过createSource处理成经过转化后的代码string, |
| 196 | +这时候如果loader返回了ast后面将用这个ast,如果没有在经过doBuild回调里的this.parser.parse,调用parse.js中parse函数里的acorn.parse获得ast |
| 197 | + |
| 198 | +## 代码拼接生成 |
| 199 | + |
| 200 | +代码分为webpack自己的脚手架代码部分,和经过loader处理的模块代码部分。 |
| 201 | + |
| 202 | +render - renderBootstrap处理脚手架部分 - this.hooks.render.call - this.hooks.render.tap("mainTemplate") - this.hooks.module.call - this.hooks.module.tap("JavascriptModulesPlugin")处理模块拼接部分 - emitAssets将拼接的代码输出成对应bundle文件 |
| 203 | + |
| 204 | + |
| 205 | + |
| 206 | + |
| 207 | + |
| 208 | + |
| 209 | + |
| 210 | + |
| 211 | + |
| 212 | + |
| 213 | + |
| 214 | + |
| 215 | + |
| 216 | +会看到一些CacheSource利用缓存加速: |
| 217 | + |
| 218 | + |
| 219 | + |
| 220 | +还有ReplaceSource既是处理模块化转化的部分: |
| 221 | + |
| 222 | + |
| 223 | + |
| 224 | +遍历拼接代码片段: |
| 225 | + |
| 226 | + |
| 227 | + |
| 228 | +最后输出到chunk文件: |
| 229 | + |
| 230 | + |
| 231 | + |
| 232 | + |
| 233 | + |
| 234 | +通过以上断点跟踪,就走完了代码拼接到输出的流程。 |
0 commit comments