赞
踩
NPM地址:vite-plugin-stats-html
源码地址:vite-plugin-stats-html
在本文中,我们将讲解如何从0开始编写一个Vite打包产物分析插件工具,在你的vite构建的项目里,执行打包命令后,能生成一个打包产物分析报告html页面,这个页面会从多个角度展示打包产物的细节信息,从资源文件信息到第三方依赖等等,该报告包含以下内容(如上图):
JS
文件体积,CSS
文件体积,以及饼状图显示的打包产物占比WebpackBundleAnalyzer/ rollup-plugin-visualizer
的工具,可以帮助我们更好的了解产物内容,可视化的依赖的引用关系// npm
npm install --save-dev vite-plugin-stats-html
// or via yarn
yarn add --dev vite-plugin-stats-html
在 vite里配置 (vite.config.js)
// es
import { visualizer } from "vite-plugin-stats-html";
// or
// cjs
const { visualizer } = require("vite-plugin-stats-html");
module.exports = {
plugins: [visualizer()],
};
通过在vite构建的项目中执行项目打包命令
npm run build
// or
yarn build
...
在打包结束后会在项目根目录下自动生成一个 stats.html
文件,你可以在浏览器打开查看生成的报告。
(一)打包产物分析的意义
(二)Vite插件开发知识准备
(三)Vite插件功能开发分解
(四)打包发布
(五)Vite插件总结
本插件开发主要参考以下插件:
(1) perfsee 性能分析平台
(2) rollup-plugin-visualizer
作为前端人,优化和性能一直是绕不开的话题,对前端打包产物进行分析是非常必要的环节,前端应用程序的打包产物通常包含了代码、样式、图片、字体等资源,这些资源的大小和依赖关系都会对应用程序的性能和用户体验产生影响。
在Webpack
构建的项目实际开发中,我们可以使用例如Webpack Bundle Analyzer
、Source Map Explorer
等工具来分析打包产物,对于vite构建的项目中,常用的也有rollup-plugin-visualizer
等工具,生成一个交互式的依赖关系图表,用于展示应用程序的依赖关系和模块大小,帮助我们可视化分析打包产物,从而了解应用程序的资源占用情况和依赖关系。
通过打包产物分析,我们可以了解以下内容:
在编写插件之前,我们必须先具备Vite
插件开发前的知识准备。
Vite
使用 Rollup
作为生产环境的构建工具。Rollup
会对项目中的代码进行静态分析,找出所有的模块依赖关系。这个过程中,Vite 会处理各种资源文件,如 CSS
、图片等,并将它们转换为合适的模块。所以正如 Vite官网 所描述,我们可以得到结论,vite插件,它具备兼容rollup插件的钩子和拥有自己的独有的钩子。
为了编写这个Vite
插件,我们需要了解Vite
插件的编写方式。Vite
插件是一个JavaScript
模块,它导出一个函数,这个函数接受一个参数,这个参数是一个Vite插件API对象。通过查阅rollup
的插件开发文档,我们发现generateBundle
钩子是用于在生成最终包的阶段进行额外的处理。该钩子可以获取到以下信息:
outputOptions
:输出选项对象,包含了输出文件的路径、格式等信息。bundle
:打包生成的代码对象,包含了多个模块的信息,可以用来进一步分析和处理代码。isWrite
: 一个布尔值,用于判断当前是否是写入文件的操作,若为 false
则表示只是在生成代码而不是写入文件。下面是一个简单的Vite插件示例:
export default function visualizer() {
return {
name: 'visualizer',
async generateBundle(outputOptions, bundle) {
fs.writeFileSync(path.join("./", 'bundle.txt'), bundle);
},
};
}
在这个示例中,我们编写了一个名为visualizer
的插件,让它在生成打包产物时输出bundle
的内容,并把内容通过Node.js
中的一个文件系统模块,用于同步地将数据写入文件 bundle.txt
输出到项目根目录下。
我们可以通过Vite
创建一个项目,在页面中随便写点东西,比如引用ElementUI
,将这个插件添加到Vite
的配置文件中,来启用它,看看能生成具体 bundle.txt
什么内容。
{ "assets/index-e8828b4c.css": { "fileName": "assets/index-e8828b4c.css", "name": "index.css", "needsCodeReference": false, "source": "@charset \"UTF-8\";:root..." "type": "asset" }, "assets/index-60dd1a96.js": { "exports": [], "facadeModuleId": "E:/cao/my-test-2023/index.html", "isDynamicEntry": false, "isEntry": true, "isImplicitEntry": false, "moduleIds": ["\u0000vite/modulepreload-polyfill", ...], "name": "index", "type": "chunk", "dynamicImports": [], "fileName": "assets/index-60dd1a96.js", "implicitlyLoadedBefore": [], "importedBindings": {}, "imports": [], "modules": {}, "referencedFiles": [], "viteMetadata": { "importedAssets": {}, "importedCss": {} }, "code": "(function(){const ...", "map": null } }
初步看看代码,我们可以看到它包含了所有模块信息的对象,它包含了多个属性和方法,用于描述和处理打包生成的代码。这些都是后面我们对插件开发及其重要的 属性和方法。属性和方法比如比较重要的:
bundle[file]
:一个模块的描述对象,其中 file
表示模块的文件名。该对象包含了模块的代码、依赖关系和其他信息。Object.keys(bundle)
:获取所有打包的文件名数组。bundle[file].code
:获取某个模块的代码字符串。bundle[file].isEntry
:一个布尔值,表示该模块是否是入口模块。bundle[file].facadeModuleId
:一个字符串,表示该模块的 ID。bundle[file].modules
:一个字符串数组,表示该模块依赖的所有模块的 ID。我们开发打包产物分析插件,本质上就是对bundle信息的解剖和组合成我们需要的信息。
接下来我们正儿八经写插件的功能了
我们通过Vite创建一个create-vite-extra 快速创建一个library 模板项目
然后进行改造一下目录创建 plugin目录里编写我们的核心代码功能
├── plugin // 服务端源代码
│ ├── buildTree.js // 将依赖转换树
│ ├── createHtml.js // 导出的产物分析报告html模板
│ ├── index.js // 插件核心代码
│ ├── mapper.js // 模块映射关系
├── package.json // package.json
├── .gitignore // git 忽略项
└── vite.config.js // vite配置文件
通过查阅文档,我们可以知道rollup有个buildStart钩子,index.js
这里我们记录打包开始时间
function visualizer(options = {}) {
let startTime;
return {
name: "visualizer",
buildStart() {
startTime = Date.now();
},
结合打包结束时间,计算打包时长
time: (Date.now() - startTime) / 1000 + "s",
我们通过遍历 bundle 去记录各种文件大小和总体积
for (const [bundleId, bundle] of Object.entries(outputBundle)) { let type = path.extname(bundle.fileName).slice(1); let size = bundle?.code?.length || bundle?.source?.length; switch (type) { case "js": jsSize += size; break; case "css": cssSize += size; break; case "jpg": case "jpeg": case "png": case "gif": case "svg": imageSize += size; break; case "html": htmlSize += size; break; case "woff": case "woff2": case "ttf": case "otf": fontSize += size; break; default: break; }
统计文件中的第三方依赖数量
const dependencyCount = Object.keys(bundle.modules ?? []).length;
因为我们最后我们要通过echarts treemap
去展示可视化的打包产物树,所以我们这里去遍历bundle.modules
,并重新拼装modules
数据,并转换为构建正确的依赖关系。
这里主要就是参考rollup-plugin-visualizer
里的源码生成依赖树
const modules = await Promise.all(
Object.entries(bundle.modules).map(([id, { renderedLength, code }]) =>
ModuleLengths({ id, renderedLength, code })
)
);
tree = buildTree(bundleId, modules, mapper);
我们通过建立一个字符串Html页面模板,这里我们方便处理数据和减少样式使用,通过 CDN 的方式我们可以很容易地使用`ElementUI和Vue的方式写我们的Html页面(如下),我们开始编写我们的页面UI
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <!-- import CSS --> <script src="https://unpkg.com/vue@2"></script> <script src="https://unpkg.com/element-ui/lib/index.js"></script> <script src="https://cdn.jsdelivr.net/npm/echarts@5.2.2/dist/echarts.min.js"></script> </head> <body> <div id="app"> </div> </body> <!-- import Vue before Element --> <script> new Vue({ el: '#app', data: function() { return { } } }) </script> </html>
我们这里主要用到了elementUI
的表格,以及echarts
的饼状图和treeMap图,例如饼状图的拼装数据,在我们的DOM
展示饼状图,关于echarts
技术的细节可以移步官网
setPieChart(){ // 基于准备好的dom,初始化echarts实例 var myChart = echarts.init(document.getElementById('pie')) // 绘制图表 myChart.setOption({ title: { text: 'Bundle Overview', }, tooltip: { trigger: 'item', }, legend: { orient: 'vertical', left: 'left', top: '30%', }, series: [ { name: 'Bundle Overview', type: 'pie', radius: '50%', data: [ { value: ${allData.bundleObj.jsSize}, name: 'JS' }, { value: ${allData.bundleObj.cssSize}, name: 'CSS' }, { value: ${allData.bundleObj.imageSize}, name: 'Image' }, { value: ${allData.bundleObj.htmlSize}, name: 'Font' }, { value: ${allData.bundleObj.fontSize}, name: 'Html' }, ], emphasis: { itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0, 0, 0, 0.5)', }, }, }, ], }) },
最后面我们通过NOde的文件能力把页面写出来
const html = createHtml(outputBundlestats);
await fs.writeFileSync(path.join("./", outputFile), html);
这样我们的插件就开发完了~
我们回忆一下,我们前端在项目中引用一个三方模块的时候通常是ESM,CJS,UMD等,主流的:
ESM是ESModlule,是ECMASCript自己的模块体系,是 Javascript 提出的实现一个标准模块系统的方案。是编译的时候运行。如我们在vite.config.js
使用ESM引入vite如下:
import { defineConfig } from "vite";
cjs 是 commonds 的缩写,被加载的时候运行,具有缓存。在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值。主要用于服务端。
导出
const obj = {a: 1);
module.exports = obj;
引入
const obj = require('"/test.js");
因为整个插件项目我们是`Vite来搭建的,所以我们从vite官网上可以看到,只要配置build.lib,就可以打包成我们需要的库
我们在vite.config.js配置
import { defineConfig } from "vite";
export default defineConfig({
build: {
target: "modules",
lib: {
entry: "./plugin/index.js",
name: "vite-plugin-stats-html",
fileName: "vite-plugin-stats-html",
formats: ["es", "cjs", "umd"],
},
},
});
我们在package.json配置
"type": "module",
"files": [
"dist"
],
"main": "./dist/vite-plugin-stats-html.cjs",
"module": "./dist/vite-plugin-stats-html.js",
"exports": {
".": {
"import": "./dist/vite-plugin-stats-html.js",
"require": "./dist/vite-plugin-stats-html.cjs"
}
},
执行打包命令,即可生成打包后的dist文件
编写README ,最后发布到npm,这个步骤,在这里就不做更多的讲述了,我们可以尝试通过 安装到我们项目中测试一下
从0开始编写一个Vite打包产物分析插件需要了解Vite的打包机制、Vite插件的编写方式、中间文件的读取方式以及打包产物的分析方法。当然你也可以通过自己的想法把更多打包产物维度进行分析,开发成插件,更好地了解我们的代码的性能和质量,从而优化我们的应用程序。当然写这个插件比较仓促,后续也可以进行拓展,比如treeMap依赖关系图,目前UI还是不太美观,还有一些兼容性的问题可能会出现。
github项目地址:vite-plugin-stats-html
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。