const fs = require('fs'); const path = require('path'); const mkdirp = require('mkdirp'); const Cabinet = require('./cabinet'); const { STEPS } = require('./constant'); const tapHook = require('./tap-hook'); const { trace, error } = require('./log'); class BuildStatisticsPlugin { constructor({ path }) { // 统计完后待写入的文件地址 this._path = path; // 用来存放compiler hooks触发时,hooks名字和触发时刻的映射关系 this._cache = new Map; // Cabinet是一个柜子,柜子里有好多抽屉(drawer), // 我们可以将物品(gadget)根据抽屉类别,放置到不同的抽屉中。 // 以下我们取drawer为compilation对象,里面放置多个gadget,{event, timestamp} this._cabinet = new Cabinet; // watch到文件变更标志位,此时不写文件 this._isFileChanged = false; } apply(compiler) { // webpack 事件: 开始 tapHook(compiler, 'compile', () => { trace('开始compile'); const event = 'compiler.compile'; this._cache.set(event, +new Date); }); // webpack 事件: 载入文件 + 代码生成 + 优化 tapHook(compiler, 'compilation', compilation => { trace('创建一个新的compilation'); const event = 'compiler.compilation'; // 如果项目中使用了extract-text-webpack-plugin, // 则compiler.compilation在一次编译中可能会触发多次 // 因此,将多次触发相关的信息,根据compilation对象分类,放在一个Cabinet中 if (!this._cache.has(event)) { this._cache.set(event, this._cabinet); } // webpack 事件: 载入文件开始 trace('开始载入文件'); this._cabinet.put({ drawer: compilation, gadget: { event, timestamp: +new Date, }, }); // webpack 事件: 载入文件结束 tapHook(compilation, 'finishModules', modules => { trace('载入文件结束'); this._cabinet.put({ drawer: compilation, gadget: { event: 'compilation.finishModules', timestamp: +new Date, } }); }); // webpack 事件: 代码生成,得到所有的目标文件名和文件内容 tapHook(compilation, 'additionalAssets', callback => { trace('生成目标文件名和文件内容'); this._cabinet.put({ drawer: compilation, gadget: { event: 'compilation.additionalAssets', timestamp: +new Date, } }); callback(); }); // webpack 事件: 优化,压缩代码 tapHook(compilation, 'afterOptimizeChunkAssets', chunks => { trace('对文件内容进行压缩'); this._cabinet.put({ drawer: compilation, gadget: { event: 'compilation.afterOptimizeChunkAssets', timestamp: +new Date, } }); }); }); // webpack 事件: 结束,写文件 tapHook(compiler, 'done', stats => { trace('完成'); const event = 'compiler.done'; this._cache.set(event, +new Date); // 处理_cache中的数据,回调_done函数 // [{event,timestamp}] const statistics = this._getBuildStatistics(); trace('各事件发生的时刻:%j', statistics); // [{stage,timestamp,startTime,endTime,elapse}] const stages = this._convertToStages(statistics); trace('各阶段耗时:%j', stages); trace('hash:%s', stats.hash); // watch到文件变更时不写文件 // note: compiler.watchMode 在webpack 1.x中不支持,因此这里只判断 _isFileChanged // 否则应该判断 compiler.watchMode && this._isFileChanged if (this._isFileChanged) { trace('文件变更时不写文件'); return; } trace('准备写文件'); const filePath = this._path; const content = JSON.stringify({ hash: stats.hash, stages, }); this._writeToFile(filePath, content); }); // webpack 事件:watch到文件变更 tapHook(compiler, 'invalid', (fileName, changeTime) => { trace('监测到文件变更'); // 重新统计 this._cache = new Map; this._cabinet = new Cabinet; // 设置文件变更标志位,watch到文件变更时不写文件 this._isFileChanged = true; }); } // 各事件节点的时间 // [{event,timestamp}] _getBuildStatistics() { // note: Map的keys是按加入顺序保存的 const eventList = Array.from(this._cache.keys()); return eventList.reduce((memo, event) => { const value = this._cache.get(event); // value可能是一个时间戳,用于存储compiler hooks中的信息 // 也可能是一个Cabinet,用于存储compilation hooks中的信息 const isCabinet = value instanceof Cabinet; // compiler event: compile, done if (!isCabinet) { memo.push({ event, timestamp: value, }); return memo; } // compilation event: finishModule, additionalAssets, optimizeChunkAssets const statistics = this._getCabinetStatistics(value); return memo.concat(statistics); }, []); } // cabinet内,各事件节点的时间 // [{event,timestamp}] _getCabinetStatistics(cabinet) { try { // Cabinet中存储的是drawer到gadgetList的映射 // 其中drawer是compilation对象,gadgetList为[{event, timestamp}] const compilations = cabinet.getDrawerList(); // note: 第一个compilation就是入口文件对应的那个compilation // 由于extract-text-webpack-plugin会创建childCompiler,因此会出现多个compilation。 const firstCompilation = compilations[0]; const gadgetList = cabinet.getGadgetListFromDrawer(firstCompilation); return gadgetList; } catch (err) { error('获取compilation信息出错,%s', err.stack); return []; } } // 计算每个阶段的起始时间与耗时,从第二个节点开始往后分别记为一个阶段 // [{stage,timestamp,startTime,endTime,elapse}] _convertToStages(statistics) { const result = []; let startTime = statistics[0].timestamp; // 从第二个节点开始往后分别记为一个阶段,因此从索引 1 开始处理 for (let i = 1; i < statistics.length; i++) { const { event, timestamp } = statistics[i]; result.push({ // 将事件名映射成阶段名 stage: STEPS[event], startTime, endTime: timestamp, elapse: timestamp - startTime, }); startTime = timestamp; } return result; } _writeToFile(filePath, content) { try { const dirPath = path.resolve(filePath, '../'); if (!fs.existsSync(dirPath)) { trace('创建文件夹:%s', dirPath); mkdirp.sync(dirPath); } trace('写入文件:%s', filePath); fs.writeFileSync(filePath, content); } catch (err) { error('写入文件时发生错误:%s', err.stack); } } } module.exports = BuildStatisticsPlugin;