完成工作的方法,是爱惜每一分钟。
——达尔文
上篇文章我们手写了一个 150 行左右的 dev-server 代码,在代码的最后我们使用了Stats.toString
将本次打包的结果输出,效果如下图所示:
这时候的终端输出的日志虽然内容可以看,但是对于重点内容还是不够突出。如果我们的项目变大之后,大量的静态资源文件就会导致日志过长,根本找不到我们想要的信息,例如下图:
本篇文章将讲解如何通过 Stats 对象的数据结构找到想要的数据,并且做一个美化版的 Webpack 构建报告。
Stats 输出报告原理和实现步骤
在之前的原理篇介绍 Compiler 和 Compilation 对象时介绍过 Stats 对象的数据结构和 API。我们在 Webpack 打包的回调中,以及在compiler.hooks.done
Hook 中只能拿到 Stats,所以我们只能通过 Stats 来拿到 Entry 编译后的 Chunks 关系,然后从 Entry 作为入口,查找 Chunks 的关系,找出一个页面用了多少资源(Assets),最终计算页面资源整体大小。对于页面资源超过推荐资源大小时,则特殊标红展现,最后将页面用到的所有资源都通过tty-table展现表格。
使用 Stats 对象输出 Webpack 构建报告
webpack/lib/Stats.js
,我们在手写 Plugin 用到的根据 Entry 查找 chunks 及其 prefetch 标识就是从Stats.js
中找到的启发。下面开始我们的代码实现。
1. 获取 Stats 对象
首先我们需要在上篇 dev-server 文章的代码基础上,在webpack
的回调函数内或者在compiler.hooks.done
的回调中,拿到 Stats 对象并且进行 Stats 数据对象转换,使用stats.toJson
的方法,返回Stats
的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
|
const webpack = require('webpack'); const webpackConfig = require('./webpack.config.js');
webpack(webpackConfig, (err, stats) => { if (err) { console.error(err); return; } stats = stats.toJson({ all: false, entrypoints: true, assets: true, chunks: true, version: true, timings: true, performance: true });
report(stats, webpackConfig); });
const compiler = webpack(webpackConfig); compiler.hooks.done.tap('plugin name', stats => { if (stats.hasErrors()) { const info = stats.toJson(); console.error(info.errors); return; } stats = stats.toJson({ all: false, entrypoints: true, assets: true, chunks: true, version: true, timings: true, performance: true });
report(stats, webpackConfig); });
|
获取 Stats 对象之后,我们使用了stats.toJson
方法将打包结果 Stats 数据输出为 JSON 对象,这时候传入了report
函数,这个函数就是我们今天的重点实现。
2. 从 entry 中找出对应 chunk
首先我们来复习下 Stats 对象的数据结构,Stats 的对象数据结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| { "version": "1.4.13", "hash": "11593e3b3ac85436984a", "time": 2469, "outputPath": "/", "entrypoints": [ ], "assets": [ ], "chunks": [ ], "modules": [ ], "errors": [ ], "warnings": [ ] }
|
我们这次用到的是entrypoints
、assets
和chunks
,三者的关系是:
- entrypoints:是入口文件对应的 bundle 信息,内部包含每个 bundle 包含的:
name
:webpack.config.js 中 entry 配置 key 值,例如config.entry = {main:'src/index.js'}
,那么这个 entry
的 name
是main
;
chunks
:是 bundle 包含的 chunkId 数组,可以通过 id 在 stats.chunks
找到对应的 chunk;
assets
:包含了 bundle 实际输出的 asset 资源信息,包括 JS 和 CSS 文件路径;
children
/childrenAssets
:包含的是可以异步拉取的 chunk 信息,例如「魔法注释」标示的prefetch
、preload
。
assets
:是输出的每个静态资源的信息,包含了资源的体积、路径、chunks 等信息,静态资源不仅仅是 JS 和 CSS,还包括了图片、字体等 Webpack 处理的资源;
chunks
:chunk 对象数组,每个 chunk 对象包含了 id、name、files(assets 路径数组)、体积、parents(所属的父 chunk) 等信息。
我们在report
函数中首先做的事情是将每个entrypoints
中的 chunks 进行分类,提取出来单个 entry 用到的 chunk 和公共 chunk,这些公共的 chunk 来自于我们的splitChunks
配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function report(stats, webpackConfig) { let {assets, entrypoints, chunks} = stats; const uniChunksMap = new Set(); const commonChunksIds = new Set(); Object.keys(entrypoints).map(name => { entrypoints[name].chunks.forEach(chunkId => { if (uniChunksMap.has(chunkId)) { commonChunksIds.add(chunkId); } else { uniChunksMap.add(chunkId); } }); }); for (let chunkId of commonChunksIds) { console.log(chunks[chunkId]); } }
|
拿到 chunkId 那么我们就可以通过chunks[chunkId]
来获取对应的 chunk 对象了。
3. 将每个 entry 的资源进行分类
下面我们将再次遍历 entrypoints
数组,得到一个新的数组entries
,里面包含了:
1 2 3 4 5 6 7 8 9
| entries = [{ name, assets, prefetchAssets, preloadAssets, asyncAssets; },
]
|
这样我们一个 entry(或者称为一个页面更好理解)要加载,必须引入 assets
的 JS 和 CSS 文件,然后异步加载的资源根据类型不同,分别放在prefetchAssets
、preloadAssets
和asyncAssets
,这样分类的好处是:
- 很清晰地标示了不同类型资源的加载顺序和重要程度;
- 可以针对不同类型的资源进行不同的加载策略,这个在《手写 Plugin》文章已经介绍过 prefetch 的实现方式,跟这里前后呼应;
- 根据不同类型的资源与不同的加载策略,可以做一些分析工作。比如本文主要分析的是每个 entry(页面)初始化时需要的代码体积有什么不同,通过列出体积表格来帮我们判定页面首次加载用到的资源是否过大。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| function report(stats, webpackConfig) { let {assets, entrypoints, chunks} = stats; const uniChunksMap = new Set(); const commonChunksIds = new Set();
const entries = Object.keys(entrypoints).map(name => { const entry = entrypoints[name]; const {prefetch = [], preload = []} = entry.children;
let prefetchChunks = []; let preloadChunks = []; const prefetchAssets = flatten( prefetch.map(({chunks, assets}) => { prefetchChunks.push(...chunks); return getAssetsFiles(assets); }) ); const preloadAssets = flatten( preload.map(({chunks, assets}) => { preloadChunks.push(...chunks); return getAssetsFiles(assets); }) );
const asyncChunks = [];
entry.chunks.forEach(chunkId => { if (!commonChunksIds.has(chunkId)) { const children = chunks[chunkId].children; if (children.length) { asyncChunks.push( ...flatten( children .filter( chunkId => !~prefetchChunks.indexOf(chunkId) && !~preloadChunks.indexOf(chunkId) ) .map(chunkId => getAssetsFiles(chunks[chunkId].files)) ) ); } } }); return { name, assets: entry.assets, prefetchAssets, preloadAssets, asyncAssets: [...new Set(asyncChunks)] }; }); console.log(entries); }
|
4. 对 assets 进行重新处理
在这一步,我们主要做的事情是遍历 assets 对象,提取 assets 中的 JS 和 CSS 文件,毕竟 JS 和 CSS 影响页面加载速度更严重一些,而且不好优化。另外,通过遍历 assets 对象,我们创建一个assetsMap
,可以通过 asset
的 name
(这里的 name 实际就是对应着上面 entries 数组中每个 entry 的 asset 路径) 获取 asset
的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const isJS = val => /\.js$/.test(val); const isCSS = val => /\.css$/.test(val);
function report(stats, webpackConfig) { let {assets, entrypoints, chunks} = stats; const assetsMap = new Map(); assets = assets.filter(a => { if (isJS(a.name) || isCSS(a.name)) { const name = a.name; if (assetsMap.has(name)) { return false; } if (a.chunks.length === 1 && commonChunksIds.has(a.chunks[0])) { a.type = ['common']; } else { a.type = []; } assetsMap.set(name, { ...a, gzippedSize: getGzippedSize(a) }); return true; } return false; }); }
|
第三步,我们需要从chunks
中找到 Webpack Entry 的入口文件,这里使用了chunk
对象的chunk.entry
。如果是 Entry 类型的 chunk,则可以通过下面代码筛选出来:
1 2 3
| const chunks = json.chunks;
const entries = chunks.filter(chunk => chunk.entry);
|
第四步,我们可以从 Entry 的chunk
对象里面获取包含的全部 chunk 的 asset 对象。chunk
对象中有files
、siblings
和children
:
files
:
siblings
:
children
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function getAssetsFiles(files = []) { return files.filter(file => isJS(file) || isCSS(file)); } entries.map(({files, siblings, children, names}) => { files = getAssetsFiles(files); siblings.forEach(id => { if (chunks[id].files.length) { files.push(...getAssetsFiles(chunks[id].files)); } }); children.forEach(id => { if (chunks[id].files.length) { files.push(...getAssetsFiles(chunks[id].files)); } }); return { name: names.join('-'), files: files.map(file => assetsMap.get(file)) }; });
|
总结
通过本篇文章不仅可以美化 log,还可以让我们熟悉 Stats 的结构。