每次打包发小小文件很多,小到几百B、几kB等,网上也没找到好用的插件,自己就写了一个,大家参考学习,欢迎提取建议
import path from 'path';
import { PluginOption } from 'vite';
import fs from 'fs';
import esbuild from 'esbuild';
/**
* 合并小文件插件
*/
export function mergeSmallFilePlugin() {
const plugin: PluginOption = {
name: 'vite:yu-bundle-plugin',
enforce: 'post',
apply: 'build',
async writeBundle(options, bundle) {
// 小于 10kB 的文件进行合并
const minChunkSize = 10 * 1000; // 10kB
const chunks = Object.values(bundle).filter(chunk => chunk.type === 'chunk');
// 只合并到动态导入入口
const dynChunks = chunks.filter(chunk => chunk.isDynamicEntry);
// 记录文件引用次数,后面删除替换 __vite__mapDeps 使用
const chunkCountMap: Record<string, number> = {};
for (const dynChunk of dynChunks) {
// 仅非动态导入文件才能合并
const impChunks = dynChunk.imports
.map(s => bundle[s])
.filter(chunk => chunk && chunk.type === 'chunk')
.filter(chunk => !chunk.isDynamicEntry)
.filter(chunk => {
chunkCountMap[chunk.fileName] ||= 0;
chunkCountMap[chunk.fileName]++;
if (chunk.code.length < minChunkSize) {
// 合并后计数减1
chunkCountMap[chunk.fileName]--;
return true;
}
return false;
});
if (!impChunks.length) {
continue;
}
// esbuild 需要合并的路径列表
const internal = impChunks.map(chunk => chunk.fileName.replace(/^assets/g, '.'));
console.log(`esbuild:\x1B[32m dist/${dynChunk.fileName}\x1B[0m`);
for (const impChunk of impChunks) {
console.log(
`\x1B[33m${(impChunk.code.length / 1024).toFixed(2)}kB\x1B[0m`.padEnd(8, ' '),
`dist/${impChunk.fileName}`
);
}
// 合并到 dynChunk
await esbuild.build({
stdin: {
contents: dynChunk.code,
loader: 'js',
resolveDir: 'dist/assets',
},
outfile: `dist/${dynChunk.fileName}`,
charset: 'utf8',
format: 'esm',
platform: 'browser',
bundle: true,
minify: false,
minifyWhitespace: true,
packages: 'external',
allowOverwrite: true,
plugins: [
{
name: 'myexternal',
setup(build) {
build.onResolve({ filter: /.*/ }, async args => {
if (internal.includes(args.path)) {
// 需要合并的路径
return {
path: path.resolve('dist/assets', args.path),
external: false,
};
}
return {
path: args.path,
external: true,
};
});
},
},
],
});
}
// 找到 __vite__mapDeps 进行替换
const indexChunks = chunks.filter(chunk => chunk.code.includes('const __vite__mapDeps'));
// 没有被引用的才需要替换
const internal = Object.entries(chunkCountMap)
.filter(([_, count]) => count === 0)
.map(([file]) => file.replace(/^assets/g, '.'));
if (indexChunks.length && internal.length) {
const internalToken = new RegExp(`(${internal.join('|')})`, 'g');
for (const indexChunk of indexChunks) {
// 替换 __vite__mapDeps
const code = indexChunk.code.replace(internalToken, '').toString();
console.log(`replace __vite__mapDeps: dist/${indexChunk.fileName}`);
await fs.promises.writeFile(path.resolve(`dist/${indexChunk.fileName}`), code);
}
for (const file of internal) {
// 删除未引用的文件
console.log(`rm dist/assets${file.substring(1)}`);
await fs.promises.rm(path.resolve(`dist/assets`, file));
}
}
},
};
return [plugin];
}