Webpack:将动态导入的模块拆分为单独的块,同时将库保留在主包中

Webpack: Split dynamic imported modules into separate chunks while keeping the library in the main bundle

我正在开发一个网络应用程序,其中包含应使用 dynamic imports 延迟加载的小部件。 每个小部件都应该为其接收单独的包,并且它们的依赖项仅在用户请求时才由浏览器获取。有一个例外:流行的小部件应该包含在主包中以及它们使用或扩展的库 类 中。下面是我打算实现的文件夹结构和我的目标输出:

File Structure                       Desired chunk

src/
├── widgets/
│   ├── popular-widget/index.ts      main
│   ├── edge-case-widget/index.ts    edge-case-widget
│   └── interesting-widget/index.ts  interesting-widget
├── library/
│   ├── Widget.ts                    main
│   └── WidgetFactory.ts             main
└── index.ts                         main (incl. entry point)

dist/
├── edge-case-widget.app.js          edge-case-widget
├── interesting-widget.app.js        interesting-widget
└── main.app.js                      main (incl. entry point)

为了延迟加载小部件,我在 WidgetFactorycreate 方法中使用了以下表达式。这工作正常,小部件模块被 Webpack 神奇地拾取。

const Widget = await import(`../widgets/${name}/index`)

我试图通过在 Webpack 中配置 optimization.splitChunks.cacheGroups 来解决代码拆分挑战。提供 test 函数,我将模块分配到 librarywidgets 缓存组中。

module.exports = {
  entry: './src/index.ts',
  [...]
  optimization: {
    splitChunks: {
      cacheGroups: {
        library: {
          test: module => isInLibrary(module.identifier()),
          name: 'library',
          chunks: 'all',
          minSize: 0
        },
        widgets: {
          test: module => isWidgetBundle(module.identifier()),
          name: module => widgetNameFromModuleId(module.identifier()),
          chunks: 'async',
          minSize: 0
        }
      }
    }
  }
}

我卡住了!

希望您能阐明我使用 Webpack 的陡峭学习曲线。

根据我平时使用的Webpack(v4)配置,我会这样配置:

splitChunks: {
    chunks: 'async',
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    name: true,
    cacheGroups: {
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true,
        },
        library: {
            test: module => module => isInLibrary(module.identifier()),
            chunks: 'all',
            enforce: true
        },
    }
}

如果只是懒惰地需要小部件,请不要将 widgets 添加到 cacheGroups。

Webpack 应该为每个异步需求解析库中的小部件依赖关系。

我没有尝试强制使用包名称,但我认为它的行为是一样的。

使用您需要的配置创建了一个简单的 github repo

  1. SplitChunks 配置(如果需要,将 testname 函数更改为您的):
    splitChunks: {
            cacheGroups: {
                widgets: {
                    test: module => {
                        return module.identifier().includes('src/widgets');
                    },
                    name: module => {
                        const list = module.identifier().split('/');
                        list.pop();
                        return list.pop();
                    },
                    chunks: 'async',
                    enforce: true
                }
            }
    }
  1. 如果您希望 PopularWidget 在 main 中,则不应动态导入它。使用常规 import 语句。因为 webpack 总是会为任何动态导入创建一个新块。所以请看一下 this 文件。
    import { Widget } from '../widgets/popular-widget';

    export class WidgetFactory {
        async create(name) {
            if (name === 'popular-widget') return await Promise.resolve(Widget);
            return await import(`../widgets/${name}/index`)
        }
    }

UPD
更改配置以保持 node_modules 与小部件相关。
我为创建新的小部件块编写了两个简单的函数。
这里的技巧是 module.issuer 属性 这意味着谁导入了这个文件。因此函数 isRelativeToWidget 将 return 对于在小部件文件结构的任何深度导入的任何依赖项(node_modules 或不)为真。
可以找到代码here.
结果配置:

splitChunks: {
            chunks: 'all',
            cacheGroups: {
                vendors: false,
                default: false,
                edgeCaseWidget: getSplitChunksRuleForWidget('edge-case-widget'),
                interestingWidget: getSplitChunksRuleForWidget('interesting-widget')
            }
        }

function isRelativeToWidget(module, widgetName) {
    if (module.identifier().includes(widgetName)) return true;

    if (module.issuer) return isRelativeToWidget(module.issuer, widgetName)
}

function getSplitChunksRuleForWidget(widgetName) {
    return {
        test: module => isRelativeToWidget(module, widgetName),
        name: widgetName,
        chunks: 'async',
        enforce: true,
    }
}