└── README.md /README.md: -------------------------------------------------------------------------------- 1 | ###分析webpack中的Compiler/Compilation/Stats对象及构建顺序 2 | 3 | 1. Compiler instance of watch method 4 | ```js 5 | Compiler.prototype.watch = function(watchOptions, handler) { 6 | this.fileTimestamps = {}; 7 | this.contextTimestamps = {}; 8 | var watching = new Watching(this, watchOptions, handler); 9 | return watching; 10 | }; 11 | ``` 12 | 注意:watch的时候,我们会给我们的Compiler实例对象添加一个fileTimestamps和contextTimestamps对象。而且可以清楚的知道这里返回的是我们通过watchOptions实例化的一个Watching实例对象 13 | 我们可以使用下面这种方式调用,例如在atool-build中调用: 14 | ```js 15 | if (args.watch) { 16 | compiler.watch(args.watch || 200, doneHandler); 17 | } else { 18 | compiler.run(doneHandler); 19 | } 20 | ``` 21 | 2.compiler实例化的时候同时实例化一个Parser对象 22 | ```js 23 | function Compiler() { 24 | Tapable.call(this); 25 | this.parser = { 26 | plugin: function(hook, fn) { 27 | this.plugin("compilation", function(compilation, data) { 28 | data.normalModuleFactory.plugin("parser", function(parser) { 29 | parser.plugin(hook, fn); 30 | }); 31 | }); 32 | }.bind(this), 33 | apply: function() { 34 | this.plugin("compilation", function(compilation, data) { 35 | data.normalModuleFactory.plugin("parser", function(parser) { 36 | parser.apply.apply(parser, args); 37 | }); 38 | }); 39 | }.bind(this) 40 | }; 41 | 42 | this.options = {}; 43 | } 44 | ``` 45 | 我们的Compiler会先继承了Tapable;在parser.plugin中注入的回调函数。具体parser的使用,请见文末参考文献。 46 | ```js 47 | compiler.parser.plugin("var rewire", function (expr) { 48 | //if you original module has 'var rewire' 49 | //you now have a handle on the expresssion object 50 | return true; 51 | }); 52 | ``` 53 | 54 | 3.调用compiler.run方法会分别执行'before-run','run',继而调用compiler的compile方法 55 | ```js 56 | Compiler.prototype.run = function(callback) { 57 | var self = this; 58 | var startTime = new Date().getTime(); 59 | //before run 60 | self.applyPluginsAsync("before-run", self, function(err) { 61 | if(err) return callback(err); 62 | //run 63 | self.applyPluginsAsync("run", self, function(err) { 64 | if(err) return callback(err); 65 | self.readRecords(function(err) { 66 | if(err) return callback(err); 67 | //compile函数被调用,我们传入run函数的回调函数会在compile回调函数中调用 68 | //也就是在compiler的'done'之后回调 69 | self.compile(function onCompiled(err, compilation) { 70 | if(err) return callback(err); 71 | if(self.applyPluginsBailResult("should-emit", compilation) === false) { 72 | var stats = compilation.getStats(); 73 | stats.startTime = startTime; 74 | stats.endTime = new Date().getTime(); 75 | self.applyPlugins("done", stats); 76 | return callback(null, stats); 77 | } 78 | self.emitAssets(compilation, function(err) { 79 | if(err) return callback(err); 80 | if(compilation.applyPluginsBailResult("need-additional-pass")) { 81 | compilation.needAdditionalPass = true; 82 | var stats = compilation.getStats(); 83 | stats.startTime = startTime; 84 | stats.endTime = new Date().getTime(); 85 | self.applyPlugins("done", stats); 86 | self.applyPluginsAsync("additional-pass", function(err) { 87 | if(err) return callback(err); 88 | self.compile(onCompiled); 89 | }); 90 | return; 91 | } 92 | self.emitRecords(function(err) { 93 | if(err) return callback(err); 94 | var stats = compilation.getStats(); 95 | stats.startTime = startTime; 96 | stats.endTime = new Date().getTime(); 97 | self.applyPlugins("done", stats); 98 | return callback(null, stats);//调用'done' 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); 104 | }); 105 | }; 106 | ``` 107 | 注意: 108 | (1)我们compiler.compile方法运行结束后会进行相应的回调,其中回调函数就是我们通过compile.run调用时候传入的函数 109 | 110 | (2)其中我们要注意我们传入的callback会被传入一个参数,这个参数是通过如下方式来获取到的: 111 | 112 | ```js 113 | var stats = compilation.getStats(); 114 | stats.startTime = startTime; 115 | stats.endTime = new Date().getTime(); 116 | ``` 117 | 那么getStats到底得到的是什么呢? 118 | ```js 119 | getStats() { 120 | return new Stats(this); 121 | } 122 | ``` 123 | 也就是说我们得到的是一个Stats对象,具体用法看参考文献。那么我们给出一个例子: 124 | ```js 125 | function doneHandler(err, stats) { 126 | if (args.json) { 127 | const filename = typeof args.json === 'boolean' ? 'build-bundle.json' : args.json; 128 | const jsonPath = join(fileOutputPath, filename); 129 | writeFileSync(jsonPath, JSON.stringify(stats.toJson()), 'utf-8'); 130 | console.log(`Generate Json File: ${jsonPath}`); 131 | } 132 | //如果出错,那么退出码是1 133 | const { errors } = stats.toJson(); 134 | if (errors && errors.length) { 135 | process.on('exit', () => { 136 | process.exit(1); 137 | }); 138 | } 139 | // if watch enabled only stats.hasErrors would log info 140 | // otherwise would always log info 141 | if (!args.watch || stats.hasErrors()) { 142 | const buildInfo = stats.toString({ 143 | colors: true, 144 | children: true, 145 | chunks: !!args.verbose, 146 | modules: !!args.verbose, 147 | chunkModules: !!args.verbose, 148 | hash: !!args.verbose, 149 | version: !!args.verbose, 150 | }); 151 | if (stats.hasErrors()) { 152 | console.error(buildInfo); 153 | } else { 154 | console.log(buildInfo); 155 | } 156 | } 157 | if (err) { 158 | process.on('exit', () => { 159 | process.exit(1); 160 | }); 161 | console.error(err); 162 | } 163 | if (callback) { 164 | callback(err); 165 | } 166 | } 167 | ``` 168 | 主要的代码就是调用stats.toJson方法,内容就是获取本次编译的主要信息。同时参考文献中也给出了一个输出的例子,可以自己查看。 169 | 170 | (3)我们自己的回调函数是在compiler的'done'回调以后触发的,而且和compiler的'done'回调一样,我们也是也是给我们的函数传入err和Stats对象! 171 | 172 | 然后我们看看compile中的内容: 173 | ```js 174 | Compiler.prototype.compile = function(callback) { 175 | self.applyPluginsAsync("before-compile", params, function(err) { 176 | self.applyPlugins("compile", params); 177 | var compilation = self.newCompilation(params); 178 | //调用compiler的compile方法,我们才会构建出一个Compilation实例对象,在 179 | //'make'钩子里面我们就可以获取到compilation对象了 180 | self.applyPluginsParallel("make", compilation, function(err) { 181 | compilation.finish(); 182 | compilation.seal(function(err) { 183 | self.applyPluginsAsync("after-compile", compilation, function(err) { 184 | //在compilation.seal方法调用以后我们才会执行'after-compile' 185 | }); 186 | }); 187 | }); 188 | }); 189 | }; 190 | ``` 191 | 我们再来看看compilation的finish方法: 192 | ```js 193 | finish() { 194 | this.applyPlugins1("finish-modules", this.modules); 195 | this.modules.forEach(m => this.reportDependencyErrorsAndWarnings(m, [m])); 196 | } 197 | ``` 198 | 我们再来看看compilation.seal方法: 199 | ```js 200 | seal(callback) { 201 | self.applyPlugins0("seal"); 202 | self.applyPlugins0("optimize"); 203 | while(self.applyPluginsBailResult1("optimize-modules-basic", self.modules) || 204 | self.applyPluginsBailResult1("optimize-modules", self.modules) || 205 | self.applyPluginsBailResult1("optimize-modules-advanced", self.modules)); 206 | self.applyPlugins1("after-optimize-modules", self.modules); 207 | //这里是optimize module 208 | while(self.applyPluginsBailResult1("optimize-chunks-basic", self.chunks) || 209 | self.applyPluginsBailResult1("optimize-chunks", self.chunks) || 210 | self.applyPluginsBailResult1("optimize-chunks-advanced", self.chunks)); 211 | //这里是optimize chunk 212 | self.applyPlugins1("after-optimize-chunks", self.chunks); 213 | //这里是optimize tree 214 | self.applyPluginsAsyncSeries("optimize-tree", self.chunks, self.modules, function sealPart2(err) { 215 | self.applyPlugins2("after-optimize-tree", self.chunks, self.modules); 216 | const shouldRecord = self.applyPluginsBailResult("should-record") !== false; 217 | self.applyPlugins2("revive-modules", self.modules, self.records); 218 | self.applyPlugins1("optimize-module-order", self.modules); 219 | self.applyPlugins1("advanced-optimize-module-order", self.modules); 220 | self.applyPlugins1("before-module-ids", self.modules); 221 | self.applyPlugins1("module-ids", self.modules); 222 | self.applyModuleIds(); 223 | self.applyPlugins1("optimize-module-ids", self.modules); 224 | self.applyPlugins1("after-optimize-module-ids", self.modules); 225 | self.sortItemsWithModuleIds(); 226 | self.applyPlugins2("revive-chunks", self.chunks, self.records); 227 | self.applyPlugins1("optimize-chunk-order", self.chunks); 228 | self.applyPlugins1("before-chunk-ids", self.chunks); 229 | self.applyChunkIds(); 230 | self.applyPlugins1("optimize-chunk-ids", self.chunks); 231 | self.applyPlugins1("after-optimize-chunk-ids", self.chunks); 232 | self.sortItemsWithChunkIds(); 233 | if(shouldRecord) 234 | self.applyPlugins2("record-modules", self.modules, self.records); 235 | if(shouldRecord) 236 | self.applyPlugins2("record-chunks", self.chunks, self.records); 237 | self.applyPlugins0("before-hash"); 238 | self.createHash(); 239 | self.applyPlugins0("after-hash"); 240 | if(shouldRecord) 241 | self.applyPlugins1("record-hash", self.records); 242 | self.applyPlugins0("before-module-assets"); 243 | self.createModuleAssets(); 244 | if(self.applyPluginsBailResult("should-generate-chunk-assets") !== false) { 245 | self.applyPlugins0("before-chunk-assets"); 246 | self.createChunkAssets(); 247 | } 248 | self.applyPlugins1("additional-chunk-assets", self.chunks); 249 | self.summarizeDependencies(); 250 | if(shouldRecord) 251 | self.applyPlugins2("record", self, self.records); 252 | 253 | self.applyPluginsAsync("additional-assets", err => { 254 | if(err) { 255 | return callback(err); 256 | } 257 | self.applyPluginsAsync("optimize-chunk-assets", self.chunks, err => { 258 | if(err) { 259 | return callback(err); 260 | } 261 | self.applyPlugins1("after-optimize-chunk-assets", self.chunks); 262 | self.applyPluginsAsync("optimize-assets", self.assets, err => { 263 | if(err) { 264 | return callback(err); 265 | } 266 | self.applyPlugins1("after-optimize-assets", self.assets); 267 | if(self.applyPluginsBailResult("need-additional-seal")) { 268 | self.unseal(); 269 | return self.seal(callback); 270 | } 271 | return self.applyPluginsAsync("after-seal", callback); 272 | }); 273 | }); 274 | }); 275 | }); 276 | } 277 | ``` 278 | 从上面提到的第3点,我们可以知道webpack的编译过程大致如下: 279 |
280 | 'before run'
281 |   'run'
282 |     compile:func//调用compile函数
283 |         'before compile'
284 |            'compile'//(1)compiler对象的第一阶段
285 |                newCompilation:object//创建compilation对象
286 |                'make' //(2)compiler对象的第二阶段 
287 |                     compilation.finish:func
288 |                        "finish-modules"
289 |                     compilation.seal
290 |                          "seal"
291 |                          "optimize"
292 |                          "optimize-modules-basic"
293 |                          "optimize-modules-advanced"
294 |                          "optimize-modules"
295 |                          "after-optimize-modules"//首先是优化模块
296 |                          "optimize-chunks-basic"
297 |                          "optimize-chunks"//然后是优化chunk
298 |                          "optimize-chunks-advanced"
299 |                          "after-optimize-chunks"
300 |                          "optimize-tree"
301 |                             "after-optimize-tree"
302 |                             "should-record"
303 |                             "revive-modules"
304 |                             "optimize-module-order"
305 |                             "advanced-optimize-module-order"
306 |                             "before-module-ids"
307 |                             "module-ids"//首先优化module-order,然后优化module-id
308 |                             "optimize-module-ids"
309 |                             "after-optimize-module-ids"
310 |                             "revive-chunks"
311 |                             "optimize-chunk-order"
312 |                             "before-chunk-ids"//首先优化chunk-order,然后chunk-id
313 |                             "optimize-chunk-ids"
314 |                             "after-optimize-chunk-ids"
315 |                             "record-modules"//record module然后record chunk
316 |                             "record-chunks"
317 |                             "before-hash"
318 |                                compilation.createHash//func
319 |                                  "chunk-hash"//webpack-md5-hash
320 |                             "after-hash"
321 |                             "record-hash"//before-hash/after-hash/record-hash
322 |                             "before-module-assets"
323 |                             "should-generate-chunk-assets"
324 |                             "before-chunk-assets"
325 |                             "additional-chunk-assets"
326 |                             "record"
327 |                             "additional-assets"
328 |                                 "optimize-chunk-assets"
329 |                                    "after-optimize-chunk-assets"
330 |                                    "optimize-assets"
331 |                                       "after-optimize-assets"
332 |                                       "need-additional-seal"
333 |                                          unseal:func
334 |                                            "unseal"
335 |                                       "after-seal"
336 |                     "after-compile"//(4)完成模块构建和编译过程(seal函数回调)    
337 |     "emit"//(5)compile函数的回调,compiler开始输出assets,是改变assets最后机会
338 |     "after-emit"//(6)文件产生完成
339 | 
340 | 注意:上面没有标出第三个阶段,也就是compiler的'build-module'阶段,在这个阶段,我们调用了addEntry等方法通过入口文件_addModuleChain,processModuleDependencies等方法分析模块的依赖关系! 341 | 详细内容可以参考下面这种图: 342 | ![](https://img.alicdn.com/tps/TB1GVGFNXXXXXaTapXXXXXXXXXX-4436-4244.jpg) 343 | 344 | 4.上面的第二段代码有compiler.watch方法的调用 345 | 其本质是使用了相关的配置生成了Watching对象: 346 | ```js 347 | var watching = new Watching(this, watchOptions, handler); 348 | ``` 349 | 我们看看Watching对象是如何处理的: 350 | ```js 351 | Watching.prototype.watch = function(files, dirs, missing) { 352 | this.watcher = this.compiler.watchFileSystem.watch(files, dirs, missing, this.startTime, this.watchOptions, function(err, filesModified, contextModified, missingModified, fileTimestamps, contextTimestamps) { 353 | this.watcher = null; 354 | if(err) return this.handler(err); 355 | this.compiler.fileTimestamps = fileTimestamps; 356 | this.compiler.contextTimestamps = contextTimestamps; 357 | this.invalidate(); 358 | }.bind(this), function(fileName, changeTime) { 359 | this.compiler.applyPlugins("invalid", fileName, changeTime); 360 | }.bind(this)); 361 | }; 362 | ``` 363 | 这个很容易看出来,如果我们的文件发生了变化,那么我们直接调用Watching实例的invalidate方法,并通知compiler重新开始编译过程!这也是我们最重要的watch逻辑!这也是我们为什么有上面这样的代码: 364 | ```js 365 | if (args.watch) { 366 | compiler.watch(args.watch || 200, doneHandler); 367 | } else { 368 | compiler.run(doneHandler); 369 | } 370 | ``` 371 | 而且我们知道compiler的watch方法返回的是一个watching,那么我们看看Watching对象的内部结构: 372 | ```js 373 | 374 | function Watching(compiler, watchOptions, handler) { 375 | this.startTime = null; 376 | this.invalid = false;//是否已经文件变化 377 | this.error = null; 378 | this.stats = null; 379 | this.handler = handler; 380 | this.compiler = compiler;//compiler句柄 381 | this.running = true; 382 | } 383 | Watching.prototype._go = function() { 384 | }; 385 | Watching.prototype._done = function(err, compilation) { 386 | }; 387 | Watching.prototype.watch = function(files, dirs, missing) { 388 | }; 389 | 390 | Watching.prototype.invalidate = function() { 391 | if(this.watcher) { 392 | this.watcher.pause(); 393 | this.watcher = null; 394 | } 395 | if(this.running) { 396 | this.invalid = true; 397 | return false; 398 | } else { 399 | this._go(); 400 | } 401 | }; 402 | Watching.prototype.close = function(callback) { 403 | if(callback === undefined) callback = function() {}; 404 | if(this.watcher) { 405 | this.watcher.close(); 406 | this.watcher = null; 407 | } 408 | if(this.running) { 409 | this.invalid = true; 410 | this._done = function() { 411 | callback(); 412 | }; 413 | } else { 414 | callback(); 415 | } 416 | }; 417 | ``` 418 | 通过上面的结果你应该可以知道invalidate和close方法的具体作用了,这里就不在赘述 419 | 420 | 5.我们看看如何获取到我们最重要的compiler对象 421 | ```js 422 | // Run compiler. 423 | const compiler = webpack(webpackConfig); 424 | // Hack: remove extract-text-webpack-plugin log 425 | if (!args.verbose) { 426 | compiler.plugin('done', (stats) => { 427 | stats.stats.forEach((stat) => { 428 | stat.compilation.children = stat.compilation.children.filter((child) => { 429 | return child.name !== 'extract-text-webpack-plugin'; 430 | }); 431 | }); 432 | }); 433 | } 434 | ``` 435 | (1)我们的'done'回调是当'emit,after-emit'都调用结束了以后才会触发的,所以这时候我们所有的文件assets都已经生成结束了。 436 | 437 | (2)当我们调用webpack方法的时候,返回的就是compiler对象! 438 | 439 | (3)我们的stats对象有一个compilation属性,从构造函数就可以看到: 440 | ```js 441 | class Stats { 442 | constructor(compilation) { 443 | this.compilation = compilation; 444 | this.hash = compilation.hash; 445 | } 446 | } 447 | ``` 448 | 同时我们的compilation.children也是一个数组 449 | ```js 450 | this.children.forEach(child => { 451 | this.fileDependencies = this.fileDependencies.concat(child.fileDependencies); 452 | this.contextDependencies = this.contextDependencies.concat(child.contextDependencies); 453 | this.missingDependencies = this.missingDependencies.concat(child.missingDependencies); 454 | }); 455 | ``` 456 | 不过我们的compilation对象的fileDependencies,contextDependencies等会包含所有的子模块的内容!至于上面的'done'回调处理,我们就是为了防止我们的extract-text-webpack-plugin输出太多的log而设置的!请看下面的参考文献 457 | 458 | 参考资料: 459 | http://webpack.github.io/docs/node.js-api.html#stats 460 | 461 | http://taobaofed.org/blog/2016/09/09/webpack-flow/ 462 | 463 | https://github.com/webpack/extract-text-webpack-plugin/issues/35 464 | 465 | http://webpack.github.io/docs/plugins.html#the-parser-instance --------------------------------------------------------------------------------