Skip to content

Vite 构建打包和拆分

Vite 默认拆包策略

vite 默认拆包策略总体与 webpack 相似,就是会分为同步chunk(Initital Chunk)和异步chunk(Async Chunk),然后分开打包各自 chunk 代码。

这样会造成几个问题:

  1. Initital Chunk 和 Async Chunk 引入相同模块的话,会重复打包进各自的 chunk
  2. chunk 的体积过大,修改一行代码就会整个 chunk 重新生成。不利与缓存

基于以上的问题所有就有了拆包策略

TIP

Vite 2.9 版本之前,vite 会对于 Initital Chunk 的业务代码和第三方包代码(node_modeules)分别打包为单独的 chunk

自定义拆包策略

针对更细粒度的拆包,Vite 的底层打包引擎 Rollup 提供了manualChunks,让我们能自定义拆包策略,它属于 Vite 配置的一部分,示例如下。

js
export default {
    build: {
        rollupOptions: {
            output: {
                // manualChunks 配置
                manualChunks: {},
            },
        }
    },
}

manualChunks 主要有两种配置的形式,可以配置为一个对象或者一个函数。我们先来看看对象的配置,也是最简单的配置方式,你可以在上述的示例项目中添加如下的manualChunks配置代码。

js
{
    ...
    output: {
        // manualChunks 配置
        manualChunks: {
            // 将 React 相关库打包成单独的 chunk 中
            'react-vendor': ['react', 'react-dom'],
            // 将 Lodash 库的代码单独打包
            'lodash': ['lodash-es'],
            // 将组件库的代码打包
            'library': ['antd', '@arco-design/web-react'],
        },
    },
}

在对象格式的配置中,key代表 chunk 的名称,value为一个字符串数组,每一项为第三方包的包名。

js
manualChunks(id) {
    if (id.includes('antd') || id.includes('@arco-design/web-react')) {
        return 'library';
    }
    if (id.includes('lodash')) {
        return 'lodash';
    }
    if (id.includes('react')) {
        return 'react';
    }
}

在函数格式中,Rollup 会对每一个模块调用 manualChunks 函数,在 manualChunks 的函数入参中你可以拿到模块 id 及模块详情信息,经过一定的处理后返回 chunk 文件的名称,这样当前 id 代表的模块便会打包到你所指定的 chunk 文件中。

更详细的配置推荐去 Rollup 官网查看

循环引用问题

Rollup 上会存在循环引用问题

(但本人目前并没有遇到,所以只是记下解决方案)

js

// 确定 react 相关包的入口路径
const chunkGroups = {
  'react-vendor': [
    require.resolve('react'),
    require.resolve('react-dom')
  ],
}


// Vite 中的 manualChunks 配置
function manualChunks(id, { getModuleInfo }) { 
  for (const group of Object.keys(chunkGroups)) {
    const deps = chunkGroups[group];
    if (
      id.includes('node_modules') && 
      // 递归向上查找引用者,检查是否命中 chunkGroups 声明的包 
      isDepInclude(id, deps, [], getModuleInfo)
     ) { 
      return group;
    }
  }
}

// 缓存对象
const cache = new Map();

function isDepInclude (id: string, depPaths: string[], importChain: string[], getModuleInfo): boolean | undefined  {
    const key = `${id}-${depPaths.join('|')}`;
    // 出现循环依赖,不考虑
    if (importChain.includes(id)) {
        cache.set(key, false);
        return false;
    }
    // 验证缓存
    if (cache.has(key)) {
        return cache.get(key);
    }
    // 命中依赖列表
    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;
    }
    // 核心逻辑,递归查找上层引用者
    const isInclude = moduleInfo.importers.some(
        importer => isDepInclude(importer, depPaths, importChain.concat(id), getModuleInfo)
    );
    // 设置缓存
    cache.set(key, isInclude);
    return isInclude;
};

如果存在引用循环问题,推荐使用第三方库来解决 vite-plugin-chunk-split