模拟面试、简历指导、入职指导、项目指导、答疑解惑可私信找我~已帮助100+名同学完成改造!
大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~
上一个文章我通过两个小示例,带大家了解了 Vite 插件的开发流程,这一篇文章,我带大家实现一个功能更加高大上的插件~
我们要实现一个功能,在 Vite 打包后,我们想把打包产物的大小、类型、占比,都打印给出来,便于我们分析包的体积,就像下面这种效果:
使用方式是这样的:
import VitePluginVisualizer from "./vite-plugin-visualizer";
export default defineConfig({
base: "./",
plugins: [
vue(),
VitePluginVisualizer()
],
});
完整源码: https://github.com/sanxin-lin/vite-plugin-visualizer/tree/main/src/vite-plugins/vite-plugin-visualizer
我觉得实现这个插件需要分几个步骤:
以下代码都是伪代码,想看完整源码可以去这里: https://github.com/sanxin-lin/vite-plugin-visualizer/tree/main/src/vite-plugins/vite-plugin-visualizer
上篇文章说了,Vite 有自己的钩子,但他底层是 Rollup,所以也可以使用 Rollup 的钩子,我们可以用到 buildStart
这个钩子,这个钩子会开始打包时执行
// vite-plugin-visualizer.ts
import type { Plugin } from "vite";
const VitePluginVisualizer = (options: IOptions = {}): Plugin => {
let startTime = 0;
return {
name: "vite-plugin-visualizer",
buildStart() {
// 记录开始时间
startTime = Date.now();
},
}
}
我们需要拿到产物,才能进行计算,那我们要怎么在打包后拿到产物呢?这其实是一个时机,也就是打包后这个时机,我们可以用到 generateBundle
这个钩子,它会在产物产生的时候执行,它接收到的第二个参数 outputBundle
就是产物的map,是一个映射表的类型
// vite-plugin-visualizer.ts
const VitePluginVisualizer = (options: IOptions = {}): Plugin => {
let startTime = 0;
return {
name: "vite-plugin-visualizer",
buildStart() {
// 记录开始时间
startTime = Date.now();
},
generateBundle(_, outputBundle: Record<string, any>) {
// doing
}
}
}
只需要获取当前时间戳,减去开始时间即可
const VitePluginVisualizer = (options: IOptions = {}): Plugin => {
let startTime = 0;
return {
name: "vite-plugin-visualizer",
buildStart() {
// 记录开始时间
startTime = Date.now();
},
generateBundle(_, outputBundle: Record<string, any>) {
const time = (Date.now() - startTime) / 1000 + "s"
}
}
}
需要遍历所有包,通过名称去判断后缀,判断出这个文件时什么类型,接着需要获取文件信息身上的code
属性,去计算出code
的长度,也就是文件的大小
其实遍历去计算大小,统计文件类型,并不难,难的是,要建立一个 tree 树结构,去将文件的依赖关系体现出来
这样在后面展示的时候,才能展示出这种效果
let assetCount = 0;
let chunkCount = 0;
let packageCount = 0;
let totalSize = 0;
let jsSize = 0;
let cssSize = 0;
let imgSize = 0;
let htmlSize = 0;
let fontSize = 0;
const tableData = [];
for (const [bundleId, bundle] of Object.entries(outputBundle)) {
const fileType = path.extname(bundle.fileName).slice(1);
const size = bundle?.code?.length ?? bundle?.source?.length ?? 0;
if (fileType === "js") {
jsSize += size;
}
if (fileType === "css") {
cssSize += size;
}
if (["jpg", "jpeg", "png", "gif", "svg"].includes(fileType)) {
imgSize += size;
}
if (fileType === "html") {
htmlSize += size;
}
if (["woff", "woff2", "ttf", "otf"].includes(fileType)) {
fontSize += size;
}
const dependencyCount = Object.keys(bundle.modules ?? []).length;
totalSize += size;
assetCount++;
tableData.push({
fileName: bundle.fileName,
fileType,
size: Number(size / 1000).toFixed(2),
dependencyCount,
});
if (bundle.type == "chunk") {
packageCount += dependencyCount;
const modules = Object.entries(bundle.modules).map(
([id, { renderedLength, code }]: any) =>
getLength({ id, length: renderedLength, code })
);
const tree = buildTree(bundleId, modules, mapper);
if (tree.children.length === 0) {
const bundleSizes = getLength({
id: bundleId,
length: bundle.code.length,
code: bundle.code,
});
const facadeModuleId = bundle.facadeModuleId ?? `${bundleId}-unknown`;
const bundleUID = mapper.setNodePart(
bundleId,
facadeModuleId,
bundleSizes
);
const leaf = { name: bundleId, uid: bundleUID };
roots.push(leaf);
} else {
roots.push(tree);
}
}
chunkCount = Object.keys(outputBundle).length;
}
我们需要将我们统计到的数据,渲染到 html 页面上,我们可以分为几步:
我们刚刚看到了,页面上有图标、有表格,这些我们自己写起来很麻烦,所以我们可以利用 vue + echart + elementui 去写,怎么用呢?只需要在拼接 html 字符串的时候,拼接引用这些包的 script 标签即可
// 最终数据手收集
const outputBundlestats = {
title,
bundleObj: {
root,
time: (Date.now() - startTime) / 1000 + "s",
startTime: new Date().toLocaleString(),
totalSize: Number(totalSize / 1000).toFixed(2),
assetCount,
chunkCount,
packageCount,
jsSize: Number(jsSize / 1000).toFixed(2),
cssSize: Number(cssSize / 1000).toFixed(2),
imageSize: Number(imgSize / 1000).toFixed(2),
htmlSize: Number(htmlSize / 1000).toFixed(2),
fontSize: Number(fontSize / 1000).toFixed(2),
},
tableData,
treeData: roots,
};
// 创建 html 页面
const html = createHtml(outputBundlestats);
// 写到 html 文件里
fs.writeFileSync(path.join("./", outputFile), html);
// 创建 html 内容
export const createHtml = (allData: any) => {
const chartScript = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!-- 引入样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
body {
background-color: rgb(246, 247, 251);
}
.app {
min-height: 100vh;
display: flex;
flex-direction: column;
}
.container {
width: 1280px;
margin: 0px auto;
background-color: rgb(255, 255, 255);
border: 1px solid rgb(225, 223, 221);
border-radius: 2px;
padding: 0 20px 20px 20px;
}
.header-wrap {
min-height: 56px;
display: flex;
-webkit-box-align: center;
align-items: center;
padding: 12px 20px 0px;
font-size: 16px;
line-height: 24px;
font-weight: 600;
color: rgb(27, 26, 25);
}
.bundle-info {
display: flex;
border-radius: 4px;
border: 1px solid rgb(225, 223, 221);
padding: 12px 16px;
justify-content: space-between;
margin-bottom: 20px;
}
.bundle-info-title {
font-size: 14px;
font-weight: 600;
}
.bundle-overview {
flex-wrap: wrap;
width: auto;
overflow: visible;
height: auto;
display: flex;
justify-content: space-between;
}
.bundle-left {
width: 60%;
height: 300px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-content: space-between;
flex-wrap: wrap;
}
.bundle-left-item {
width: calc(30% - 20px);
height: calc(45% - 20px);
border-radius: 4px;
border: 1px solid rgb(225, 223, 221);
padding: 12px 16px;
text-align: left;
}
.kb {
font-size: 14px;
font-weight: 400;
}
.bundle-left-item-title {
text-align: left;
font-size: 14px;
font-weight: 600;
line-height: 22px;
}
.bundle-left-item-value {
font-size: 28px;
font-weight: 600;
line-height: 100px;
}
.bundle-right {
width: calc(40% - 20px);
border-radius: 4px;
height: 300px;
border: 1px solid rgb(225, 223, 221);
}
.pie {
position: relative;
width: 100%;
height: 100%;
padding: 20px;
}
.tablist {
margin-top: 20px;
}
.visualization {
width: 1248px;
height: 500px;
background: #eebe77;
}
</style>
<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 class="app" id="app">
<div class="container">
<div class="header-wrap">${allData.title}</div>
<div class="bundle-info">
<div>
<span class="bundle-info-title">project URL:</span
>{{ bundleObj.root }}
</div>
<div>
<span class="bundle-info-title">built in:</span> {{ bundleObj.time }}
</div>
<div>
<span class="bundle-info-title">built Date:</span>
{{ bundleObj.startTime }}
</div>
</div>
<div class="bundle-overview">
<div class="bundle-left">
<div
class="bundle-left-item"
v-for="item in bundleList"
:key="item.key"
>
<div class="bundle-left-item-title" v-if="item.content">
<el-popover
:ref="item.key"
:title="item.content.title"
width="200"
trigger="hover"
:content="item.content.content"
>
</el-popover>
<span v-popover="item.key">{{ item.title }}</span>
</div>
<div v-else class="bundle-left-item-title">{{ item.title }}</div>
<div class="bundle-left-item-value">
{{ bundleObj[item.key] }}
<span v-if="!item.content" class="kb">kb</span>
</div>
</div>
</div>
<div class="bundle-right">
<div class="pie" id="pie"></div>
</div>
</div>
<el-tabs type="border-card" class="tablist">
<el-tab-pane label="Visualization">
<div id="visualization" class="visualization"></div>
</el-tab-pane>
<el-tab-pane label="Assets Statistics">
<el-table :data="tableData" height="400" stripe style="width: 100%">
<el-table-column prop="fileName" sortable label="file" width="500px">
</el-table-column>
<el-table-column prop="fileType" sortable label="type"> </el-table-column>
<el-table-column prop="size" sortable label="size(kb)"> </el-table-column>
<el-table-column prop="dependencyCount" sortable label="dependencyCount">
</el-table-column>
</el-table>
</el-tab-pane>
</el-tabs>
</div>
</div>
</body>
<script>
var app = new Vue({
el: '#app',
data: {
bundleObj:${JSON.stringify(allData.bundleObj)},
tableData:${JSON.stringify(allData.tableData)},
bundleList:[
{
key: 'totalSize',
title: 'Bundle Size',
content: '',
},
{
key: 'jsSize',
title: 'Initial JS Size',
content: '',
},
{
key: 'cssSize',
title: 'Initial CSS Size',
content: '',
},
{
key: 'assetCount',
title: 'Assets Count',
content: {
title: 'Webpack ouput assets',
content:
'Files emitted by webpack.<br/> Including any JavaScript CSS font Image files which processed by webpack.',
},
},
{
key: 'chunkCount',
title: 'Chunks Count',
content: {
title: 'Chunks',
content:
'(1)initial is the main chunk for the entry point. This chunk contains all the modules and its dependencies that you specify for an entry point.initial is the main chunk for the entry point. This chunk contains all the modules and its dependencies that you specify for an entry point.non-initial is a chunk that may be lazy-loaded. It may appear when dynamic import or SplitChunksPlugin is being used.',
},
},
{
key: 'packageCount',
title: 'Packages Count',
content: {
title: 'node_modules',
content: 'Third part packages count in node_modules.',
},
},
]
},
methods: {
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)',
},
},
},
],
})
},
getLevelOption() {
return [
{
itemStyle: {
borderColor: '#777',
borderWidth: 0,
gapWidth: 1
},
upperLabel: {
show: false
}
},
{
itemStyle: {
borderColor: '#555',
borderWidth: 5,
gapWidth: 1
},
emphasis: {
itemStyle: {
borderColor: '#ddd'
}
}
},
{
colorSaturation: [0.35, 0.5],
itemStyle: {
borderWidth: 5,
gapWidth: 1,
borderColorSaturation: 0.6
}
}
];
},
setTreeChart(){
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('visualization'))
// 绘制图表
myChart.setOption({
title: {
text: 'visualization',
left: 'center'
},
tooltip: {
formatter: function(info) {
var value = info.value;
var treePathInfo = info.treePathInfo;
var treePath = [];
for (var i = 1; i < treePathInfo.length; i++) {
treePath.push(treePathInfo[i].name);
}
return [
'<div class="tooltip-title">' +
echarts.format.encodeHTML(treePath.join('/')) +
'</div>',
'size: ' + echarts.format.addCommas(value) + ' KB'
].join('');
}
},
series: [{
name: 'root',
type: 'treemap',
visibleMin: 300,
label: {
show: true,
formatter: '{b}'
},
upperLabel: {
show: true,
height: 30
},
itemStyle: {
borderColor: '#fff'
},
levels: this.getLevelOption(),
data: ${JSON.stringify(allData.treeData)},
}]
})
}
},
mounted(){
this.setPieChart()
this.setTreeChart()
}
})
</script>
</html>
`;
return chartScript;
};
最后运行打包命令,然后打开生成的 html 文件即可查看
完整源码: https://github.com/sanxin-lin/vite-plugin-visualizer/tree/main/src/vite-plugins/vite-plugin-visualizer
我是林三心
如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 -->
广州的兄弟可以约饭哦,或者约球~我负责打铁,你负责进球,谢谢~