为了账号安全,请及时绑定邮箱和手机立即绑定

从 0 开始编写一个vite打包产物分析插件-vite-plugin-stats-html

标签:
Html5

theme: smartblue

前言

在本文中,我们将讲解如何从0开始编写一个Vite打包产物分析插件工具,在你的vite构建的项目里,执行打包命令后,能生成一个打包产物分析报告html页面,这个网页会从多个角度展示打包产物的细节信息,从资源文件信息到第三方依赖等等,该报告包含以下内容(如下图):

  • 项目路径,打包一共花费时间,打包日期
  • 打包总体积,JS 文件体积,CSS 文件体积,以及饼状图显示的打包产物占比
  • 打包出的文件数量,入口点指定的所有模块及其依赖项,产物中引用的第三方依赖的数量
  • 同时我们也提供了类似 WebpackBundleAnalyzer/ rollup-plugin-visualizer 的工具,可以帮助我们更好的了解产物内容,可视化的依赖的引用关系
  • 表格展示打包的出的文件,包含文件类型,文件大小,里面引用第三方依赖数量

11.gif

安装体验

// 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 AnalyzerSource Map Explorer等工具来分析打包产物,对于vite构建的项目中,常用的也有rollup-plugin-visualizer等工具,生成一个交互式的依赖关系图表,用于展示应用程序的依赖关系和模块大小,帮助我们可视化分析打包产物,从而了解应用程序的资源占用情况和依赖关系。

通过打包产物分析,我们可以了解以下内容:

  1. 打包产物的大小:了解应用程序的资源占用情况,从而优化应用程序的性能和用户体验。
  2. 打包产物的依赖关系:了解应用程序的模块化程度,从而使得应用程序更加易于维护和扩展。
  3. 打包产物的性能:了解应用程序的性能瓶颈,从而优化应用程序的性能和用户体验。
  4. 打包产物的质量:了解应用程序的代码规范、可读性、可维护性等方面,从而提高应用程序的质量和可靠性。

二、Vite插件开发知识准备

在编写插件之前,我们必须先具备Vite插件开发前的知识准备。

2.1 Vite构建机制

Vite 使用 Rollup 作为生产环境的构建工具。Rollup会对项目中的代码进行静态分析,找出所有的模块依赖关系。这个过程中,Vite 会处理各种资源文件,如CSS、图片等,并将它们转换为合适的模块。所以正如Vite官网上所描述,我们可以得到结论,vite插件,它具备兼容rollup插件的钩子和拥有自己的独有的钩子。

1684828915078.png

2.2 Vite插件初探

为了编写这个Vite插件,我们需要了解Vite插件的编写方式。Vite插件是一个JavaScript模块,它导出一个函数,这个函数接受一个参数,这个参数是一个Vite插件API对象。通过查阅rollup的插件开发文档,我们发现generateBundle 钩子是用于在生成最终包的阶段进行额外的处理。该钩子可以获取到以下信息:

  1. outputOptions:输出选项对象,包含了输出文件的路径、格式等信息。
  2. bundle:打包生成的代码对象,包含了多个模块的信息,可以用来进一步分析和处理代码。
  3. 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输出到项目根目录下。

2.3 bundle 参数到底包含哪些东西

我们可以通过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
	}
}
                                
                

初步看看代码,我们可以看到它包含了所有模块信息的对象,它包含了多个属性和方法,用于描述和处理打包生成的代码。这些都是后面我们对插件开发及其重要的 属性和方法。属性和方法比如比较重要的:

  1. bundle[file]:一个模块的描述对象,其中 file 表示模块的文件名。该对象包含了模块的代码、依赖关系和其他信息。
  2. Object.keys(bundle):获取所有打包的文件名数组。
  3. bundle[file].code:获取某个模块的代码字符串。
  4. bundle[file].isEntry:一个布尔值,表示该模块是否是入口模块。
  5. bundle[file].facadeModuleId:一个字符串,表示该模块的 ID。
  6. bundle[file].modules:一个字符串数组,表示该模块依赖的所有模块的 ID。

我们开发打包产物分析插件,本质上就是对bundle信息的解剖和组合成我们需要的信息

接下来我们正儿八经写插件的功能了

三、Vite插件功能开发分解

我们通过Vite创建一个create-vite-extra 快速创建一个library 模板项目
image.png
然后进行改造一下目录创建 plugin目录里编写我们的核心代码功能

├── plugin                     // 服务端源代码
│   ├── buildTree.js           // 将依赖转换树
│   ├── createHtml.js          // 导出的产物分析报告html模板
│   ├── index.js               // 插件核心代码
│   ├── mapper.js              // 模块映射关系
├── package.json               // package.json
├── .gitignore                 // git 忽略项
└── vite.config.js              // vite配置文件

3.1 打包开始时间和打包结束时间统计

3.1.1 打包开始时间

通过查阅文档,我们可以知道rollup有个buildStart钩子,index.js这里我们记录打包开始时间

function visualizer(options = {}) {
let startTime;
return {
name: "visualizer",
buildStart() {
startTime = Date.now();
},

3.1.2 计算打包时长

结合打包结束时间,计算打包时长

time: (Date.now() - startTime) / 1000 + "s",

3.2 统计各文件类型和大小

我们通过遍历 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;
        }

3.4 统计第三方依赖

统计文件中的第三方依赖数量

 const dependencyCount = Object.keys(bundle.modules ?? []).length;

3.5 把打包文件生成依赖树需要的数据结构

因为我们最后我们要通过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);

3.6 把数据嵌入我们模板页面

我们通过建立一个字符串Html页面模板,这里我们方便处理数据和减少样式使用,通过 CDN 的方式我们可以很容易地使用`ElementUI和Vue的方式写我们的Html页面(如下),我们开始编写我们的页面UI

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <!-- import CSS -->
 <script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://unpkg.com/vue@2"></script>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="https://unpkg.com/element-ui/lib/index.js"></script>
<script class="lazyload" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsQAAA7EAZUrDhsAAAANSURBVBhXYzh8+PB/AAffA0nNPuCLAAAAAElFTkSuQmCC" data-original="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中展示饼状图

   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)',
                },
              },
            },
          ],
        })
    },

image.png

最后面我们通过NOde的文件能力把页面写出来

      const html = createHtml(outputBundlestats);
      await fs.writeFileSync(path.join("./", outputFile), html);

这样我们的插件就开发完了~

四、打包发布

4.1 引入插件的方式

我们回忆一下,我们前端在项目中引用一个三方模块的时候通常是ESM,CJS,UMD等,主流的:

4.1.1 ESM

ESM是ESModlule,是ECMASCript自己的模块体系,是 Javascript 提出的实现一个标准模块系统的方案。是编译的时候运行。如我们在vite.config.js使用ESM引入vite如下:

import { defineConfig } from "vite";

4.1.2 CJS

cjs 是 commonds 的缩写,被加载的时候运行,具有缓存。在第一次被加载时,会完整运行整个文件并输出一个对象,拷贝(浅拷贝)在内存中。下次加载文件时,直接从内存中取值。主要用于服务端。
导出

const obj = {a: 1);
module.exports = obj;

引入

const obj = require('"/test.js");

4.2 初探vite构建

因为整个插件项目我们是`Vite来搭建的,所以我们从vite官网上可以看到,只要配置build.lib,就可以打包成我们需要的库

1685157236910.png
我们在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文件

1685157868929.png
编写README ,最后发布到npm,这个步骤,在这里就不做更多的讲述了,我们可以尝试通过 安装到我们项目中测试一下

四、Vite插件总结

从0开始编写一个Vite打包产物分析插件需要了解Vite的打包机制、Vite插件的编写方式、中间文件的读取方式以及打包产物的分析方法。当然你也可以通过自己的想法把更多打包产物维度进行分析,开发成插件,更好地了解我们的代码的性能和质量,从而优化我们的应用程序。当然写这个插件比较仓促,后续也可以进行拓展,比如treeMap依赖关系图,目前UI还是不太美观,还有一些兼容性的问题可能会出现。

github项目地址:vite-plugin-stats-html

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
Web前端工程师
手记
粉丝
20
获赞与收藏
86

关注作者,订阅最新文章

阅读免费教程

  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消