来自掘金小册 深入浅出 Vite 的学习实践与总结
代码分割解决的问题
首先看看传统单 chunk 打包模式的问题:
- 无法做到按需加载
- 所有代码都打包在一起(一个 chunk),那么页面初始化需要的代码和路由组件的代码全都打包在一起了
- 线上缓存复用率低
- 一般 chunk 的名称是根据文件内容生成的 hash 值,那么每次修改代码,chunk 名称都会发生变化,这样浏览器就没办法利用本地缓存了
所以进行代码分割就是为了解决这些问题,提高页面加载性能。
Vite的默认拆包策略
2.9 版本之前
- 一个 chunk 对应一个 CSS 文件
- 业务代码会打包成一个 chunk
- 第三方依赖包打包成一个 chunk
2.9 版本之后
- 变化就在于第三方的包和业务代码都在一个 chunk 中了
如果想要更细粒度地控制代码分割策略,就得使用 manualChunks 配置,使用方式大概如下:
export default defineConfig({
build: {
rollupOptions: {
output: {
/**
* 1.以对象的方式使用
* 将 lodash 模块打包成一个 chunk,名称是 lodash
*/
manualChunks: {
lodash: ['lodash'],
},
/**
* 2.以函数的形式使用
* 将第三方包全部打包在一个chunk中,名称叫 vendor
*/
manualChunks(id, { getModuleInfo, getModuleIds }) {
if (id.includes('node_modules')) {
return 'vendor';
}
},
},
},
},
});
在 Vite 的源码中,在没有配置manualChunks
时,默认拆包策略就是这样的:
function splitVendorChunk(
options: { cache?: SplitVendorChunkCache } = {}
): GetManualChunk {
const cache = options.cache ?? new SplitVendorChunkCache()
return (id, { getModuleInfo }) => {
// 是第三方包 && 非 CSS 模块 && 是初始化需要的 chunk
if (
id.includes('node_modules') &&
!isCSSRequest(id) &&
staticImportedByEntry(id, getModuleInfo, cache.cache)
) {
return 'vendor'
}
}
}
这个默认拆包策略是以插件的方式存在的,由于 2.9之后就不使用该策略了,所以如果还要用的话就得自己导入插件进行使用:
import { splitVendorChunkPlugin } from 'vite'
export default defineConfig({
plugins: [splitVendorChunkPlugin()]
})
vite-plugin-chunk-split源码学习
vite-plugin-chunk-split 是一个 Vite 插件,支持多种拆包策略,可避免手动操作manualChunks
潜在的循环依赖问题。
我将源码改动了一点点、去除了一些边界处理啥的,然后添加了大量的注释,下面来看看源码:
/**
* chunk 分割插件
*/
export default function chunkSplit(params?: ChunkSplit): Plugin {
const { strategy = 'default', customSplitting = {} } = params;
let manualChunks: ManualChunksOption;
// 所有文件打包到一起,也就是一个 chunk
if (strategy === 'all-in-one') {
manualChunks = wrapCustomSplitConfig(() => {
return null;
}, customSplitting);
}
// 默认拆包方式
if (strategy === 'default') {
manualChunks = wrapCustomSplitConfig((id, { getModuleInfo }) => {
if (id.includes('node_modules') && !isCSSIdentifier(id)) {
if (staticImportedScan(id, getModuleInfo)) {
return 'vendor';
}
}
}, customSplitting);
}
// 实现不打包的效果,一个文件一个 chunk
if (strategy === 'unbundle') {
manualChunks = wrapCustomSplitConfig(
(id, { getModuleInfo }): string | undefined => {
// 针对第三方的包,将静态模块和动态模块进行拆分
if (id.includes('node_modules') && !isCSSIdentifier(id)) {
if (staticImportedScan(id, getModuleInfo)) {
return 'vendor';
} else {
return 'async-vendor';
}
}
const cwd = process.cwd();
// 不是第三方包 && 不是 CSS 相关模块
if (!id.includes('node_modules') && !isCSSIdentifier(id)) {
const extname = path.extname(id);
return path.relative(cwd, id).replace(extname, '');
}
return;
},
customSplitting
);
}
return {
name: 'vite-plugin-chunk-split',
config() {
return {
build: {
rollupOptions: {
output: {
manualChunks,
},
},
},
};
},
};
}
核心逻辑就在函数wrapCustomSplitConfig
中:
/**
*
* @param manualChunks rollup output.manualChunks 的选项值
* @param customOptions 自定义分割逻辑
* @returns output.manualChunks
*/
const wrapCustomSplitConfig = (
manualChunks: GetManualChunk,
customOptions: CustomSplitting
): ManualChunksOption => {
/** 模块入口文件路径 */
const depsInGroup: Record<string, string[]> = {};
const groups = Object.keys(customOptions);
for (const group of groups) {
// 获取自定义规则的 value
const packageInfo = customOptions[group];
depsInGroup[group] = packageInfo
// 筛选出值为字符串的,也就是模块名称
.filter((item): boolean => typeof item === 'string')
.map((item) => resolveEntry(item as string));
}
// 返回 manualChunks 处理函数
return (moduleId, { getModuleIds, getModuleInfo }) => {
/**
* 用来判断是否为某个模块的间接依赖
* @param id 模块 id
* @param depPaths 模块文件路径
* @param importChain 引用链
* @returns 是否命中 depPaths 中的路径,存在依赖关系
*/
const isDepInclude = (
id: string,
depPaths: string[],
importChain: string[]
): boolean | undefined => {
const key = `${id}-${depPaths.join('|')}`;
/**
* 存在循环引用,不能打包到一起
* 例如 importChain = [A, B, C],而当前 id 为 A,则产生了循环依赖
*/
if (importChain.includes(id)) {
cache.set(key, false);
return false;
}
// 取缓存数据
if (cache.has(key)) {
return cache.get(key);
}
// 命中 depPaths 中的包,存在依赖关系,可以打包到一起
if (depPaths.includes(id)) {
importChain.forEach((item) =>
cache.set(`${item}-${depPaths.join('|')}`, true)
);
return true;
}
const moduleInfo = getModuleInfo(id);
// 没有获取到模块信息 || 模块没有被引用
if (!moduleInfo || !moduleInfo.importers) {
cache.set(key, false);
return false;
}
/**
* 向上递归查找引用者,看看该模块是不是命中 depPaths 中的包
* importers 为当前模块的引用者列表
*/
const isInclude = moduleInfo.importers.some((importer) =>
isDepInclude(importer, depPaths, importChain.concat(id))
);
cache.set(key, isInclude);
return isInclude;
};
for (const group of groups) {
const deps = depsInGroup[group];
/** 自定义的分割策略信息 */
const packageInfo = customOptions[group];
// 非 CSS 相关模块才进行处理
if (!isCSSIdentifier(moduleId)) {
// 是第三方依赖 && 该模块被 deps 中的包进行了引用
if (
moduleId.includes('node_modules') &&
isDepInclude(moduleId, deps, [])
) {
return group;
}
// 正则表达式的处理
for (const rule of packageInfo) {
// 符合正则匹配,则打包到 group chunk 中
if (rule instanceof RegExp && rule.test(moduleId)) {
return group;
}
}
}
}
// 默认 chunk 处理
return manualChunks(moduleId, { getModuleIds, getModuleInfo });
};
};