MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Webpack 避免重复打包的其他方案

2024-08-067.1k 阅读

1. 认识重复打包问题

在前端项目开发中,随着项目规模的扩大,代码依赖关系变得错综复杂,重复打包问题就很容易出现。重复打包不仅会导致打包后的文件体积增大,增加用户加载页面的时间,降低用户体验,还会消耗更多的构建资源,延长构建时间。

例如,假设有两个模块 moduleAmoduleB,它们都依赖于 lodash 库。如果在 Webpack 配置不当的情况下,lodash 可能会被打包到 moduleAmoduleB 各自的输出文件中,导致 lodash 的代码在最终的打包文件里出现多次。

2. Webpack 常用避免重复打包方案及局限

2.1 CommonsChunkPlugin(Webpack 4 及之前)

在 Webpack 4 及之前版本,CommonsChunkPlugin 是常用的提取公共代码的插件。它能够将多个入口 chunk 中的公共模块提取出来,放到一个单独的文件中。

const webpack = require('webpack');

module.exports = {
  entry: {
    app1: './src/app1.js',
    app2: './src/app2.js'
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  },
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'common'
    })
  ]
};

上述代码中,CommonsChunkPlugin 会分析 app1.jsapp2.js 的依赖,将公共部分提取到 common.js 文件中。然而,CommonsChunkPlugin 存在一些局限性。它对于复杂的依赖关系处理能力有限,尤其是在多层嵌套依赖的情况下,可能无法精准地提取公共代码,而且在 Webpack 5 中已经被移除。

2.2 SplitChunksPlugin(Webpack 4+)

SplitChunksPlugin 是 Webpack 4 引入并在 Webpack 5 中进一步优化的插件,用于更灵活地分割代码。它的默认配置就能很好地处理公共代码提取问题。

module.exports = {
  entry: {
    app1: './src/app1.js',
    app2: './src/app2.js'
  },
  output: {
    filename: '[name].js',
    path: __dirname + '/dist'
  },
  optimization: {
    splitChunks: {
      chunks: 'all'
    }
  }
};

chunks: 'all' 表示对所有类型的 chunk(包括初始 chunk、异步 chunk 等)都进行代码分割。SplitChunksPlugin 虽然功能强大,但在一些特殊场景下,比如对于动态导入的模块,配置不够灵活,可能无法满足所有避免重复打包的需求。

3. 基于 externals 配置避免重复打包

3.1 externals 原理

externals 配置项允许你将某些模块排除在打包范围之外,而是通过其他方式(如 CDN)引入。Webpack 在打包时遇到这些外部依赖模块,不会将其打包进输出文件,从而避免重复打包。

3.2 简单示例

假设项目依赖 vue,我们可以通过 externals 配置不将 vue 打包进项目。

module.exports = {
  //...其他配置
  externals: {
    'vue': 'Vue'
  }
};

这里的 'vue': 'Vue' 表示,在代码中 import Vue from 'vue' 这样的导入,Webpack 不会将 vue 模块打包,而是认为在运行环境中已经有一个全局变量 Vue 可以使用。在 HTML 文件中,可以通过 CDN 引入 vue

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Webpack externals example</title>
</head>
<body>
  <div id="app"></div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.min.js"></script>
  <script src="dist/app.js"></script>
</body>
</html>

3.3 适用场景

这种方式适用于依赖一些常用的第三方库,且这些库可以通过 CDN 引入的场景。它不仅能避免重复打包,还能利用 CDN 的缓存机制,提高页面加载速度。但如果项目部署在没有网络的环境,或者对 CDN 稳定性要求极高的场景下,就需要谨慎使用。

4. 利用 Webpack 的别名(alias)避免重复打包

4.1 别名原理

Webpack 的 alias 配置可以为模块路径创建别名。通过合理设置别名,可以确保相同模块在整个项目中始终从同一个路径引入,从而避免因不同路径引入导致的重复打包。

4.2 示例配置

假设项目中有一个 utils 文件夹,里面有一些工具函数,在不同模块中可能会以不同相对路径引入。可以通过 alias 统一引入路径。

module.exports = {
  //...其他配置
  resolve: {
    alias: {
      '@utils': path.resolve(__dirname, 'src/utils')
    }
  }
};

这样在代码中,无论在哪个模块,都可以通过 import { someFunction } from '@utils/someUtil.js' 来引入,而不是使用相对路径,确保了 utils 模块在整个项目中的唯一性,避免重复打包。

4.3 复杂场景应用

在大型项目中,可能存在多层嵌套的模块结构,并且不同层级的模块都依赖一些公共模块。通过 alias 可以将这些公共模块统一映射到一个路径,方便管理和维护,同时避免重复打包。例如,项目中有一个通用的 styles 文件夹,不同组件库和业务模块都可能引用其中的样式文件。可以设置 alias@styles: path.resolve(__dirname,'src/styles'),所有对样式文件的引用都通过这个别名,确保样式文件在打包时不会重复。

5. 手动分析依赖树避免重复打包

5.1 依赖树分析工具

在 Webpack 中,可以使用 webpack-bundle-analyzer 插件来分析打包后的依赖树。它会生成一个可视化界面,展示每个模块在打包文件中的大小、依赖关系等信息。 首先安装插件:npm install --save-dev webpack-bundle-analyzer。 然后在 Webpack 配置中添加插件:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;

module.exports = {
  //...其他配置
  plugins: [
    new BundleAnalyzerPlugin()
  ]
};

运行 Webpack 构建后,会自动打开一个浏览器窗口,展示依赖树分析结果。

5.2 基于分析结果优化

通过分析依赖树,我们可以发现哪些模块被重复打包。例如,如果发现某个模块在多个地方被打包,可能是因为不同路径引入导致的。我们可以根据 alias 等方法统一引入路径。另外,如果某个模块的重复打包是由于不合理的依赖嵌套造成的,就需要调整项目的依赖结构。

比如,在依赖树分析中发现 a 模块依赖 b 模块,c 模块也依赖 b 模块,但是 ac 之间存在不必要的间接依赖导致 b 模块被重复打包。可以通过重构代码,让 ac 直接依赖 b 的同一个实例,避免重复打包。

6. 使用 DllPlugin 和 DllReferencePlugin 避免重复打包

6.1 DllPlugin 原理

DllPlugin 用于将一些不会频繁变动的第三方库提前打包成一个动态链接库(DLL)。Webpack 在后续构建项目时,不会重新打包这些库,而是直接引用这个 DLL 文件,从而大大加快构建速度,同时避免第三方库的重复打包。

6.2 配置步骤

首先,创建一个单独的 Webpack 配置文件,例如 webpack.dll.js

const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['lodash', 'vue']
  },
  output: {
    path: path.join(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_library'
  },
  plugins: [
    new webpack.DllPlugin({
      name: '[name]_library',
      path: path.join(__dirname, 'dll', '[name].manifest.json')
    })
  ]
};

上述配置中,将 lodashvue 打包到 vendor.dll.js 文件中,并生成 vendor.manifest.json 清单文件。

然后,在主 Webpack 配置文件中使用 DllReferencePlugin

const path = require('path');
const webpack = require('webpack');

module.exports = {
  //...其他配置
  plugins: [
    new webpack.DllReferencePlugin({
      context: __dirname,
      manifest: require('./dll/vendor.manifest.json')
    })
  ]
};

这样在项目构建时,Webpack 会直接引用 vendor.dll.js 中的代码,而不会重复打包 lodashvue

6.3 优缺点

优点是可以显著提高构建速度,特别适合大型项目中依赖大量第三方库的情况。缺点是配置相对复杂,需要额外管理 DLL 文件和清单文件。如果第三方库版本更新,需要重新构建 DLL 文件。

7. 基于 Monorepo 结构避免重复打包

7.1 Monorepo 概念

Monorepo 是一种将多个项目或模块放在同一个代码仓库中的管理方式。与多仓库(Polyrepo)不同,Monorepo 可以更好地共享代码和依赖,从而避免重复打包。

7.2 在 Monorepo 中避免重复打包的方法

在 Monorepo 中,可以使用工具如 Lerna 或 Yarn Workspaces 来管理项目。以 Yarn Workspaces 为例,假设项目结构如下:

root/
├── packages/
│   ├── app1/
│   │   ├── src/
│   │   └── package.json
│   ├── app2/
│   │   ├── src/
│   │   └── package.json
│   └── common/
│       ├── src/
│       └── package.json
└── package.json

在根目录的 package.json 中配置 Yarn Workspaces:

{
  "private": true,
  "workspaces": [
    "packages/*"
  ]
}

这样,common 模块可以被 app1app2 共享。在 Webpack 配置中,通过正确设置模块解析路径,可以确保 common 模块只被打包一次。例如,在 app1app2 的 Webpack 配置中,可以设置 alias 来指向 common 模块的路径:

module.exports = {
  //...其他配置
  resolve: {
    alias: {
      '@common': path.resolve(__dirname, '../common/src')
    }
  }
};

7.3 优势与挑战

优势在于更好的代码共享和依赖管理,减少重复打包,提高开发效率。但挑战在于项目规模较大时,仓库管理难度增加,可能会出现不同模块之间的版本兼容性问题。

8. 结合构建工具链避免重复打包

8.1 与 Babel 结合

Babel 是前端开发中常用的 JavaScript 转译工具。在 Webpack 中使用 Babel 时,可以通过配置 babel-plugin-transform-imports 插件来优化导入,避免重复打包。例如,对于 antd 库,默认情况下可能会引入整个库,导致打包体积增大。可以通过如下配置只引入需要的组件:

//.babelrc
{
  "plugins": [
    [
      "transform-imports",
      {
        "antd": {
          "transform": "antd/lib/[member]",
          "imports": ["antd"]
        }
      }
    ]
  ]
}

这样在代码中 import { Button } from 'antd' 时,Babel 会将其转换为 import Button from 'antd/lib/button',只引入 Button 组件,避免了整个 antd 库的重复打包。

8.2 与 PostCSS 结合

在处理 CSS 时,PostCSS 可以通过插件来优化 CSS 导入,避免重复。例如,postcss-import 插件可以将多个 CSS 文件合并成一个,并且会自动处理重复的导入。在 Webpack 配置中,可以这样使用:

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
           'css-loader',
          {
            loader: 'postcss-loader',
            options: {
              plugins: [
                require('postcss-import')
              ]
            }
          }
        ]
      }
    ]
  }
};

通过这种方式,在 CSS 构建过程中可以避免重复导入相同的 CSS 代码,从而减少打包后的 CSS 文件体积。

9. 持续监控和优化重复打包问题

9.1 构建脚本集成分析工具

在项目的构建脚本中集成依赖分析工具,例如 webpack-bundle-analyzer。可以在每次构建时自动生成依赖分析报告,方便开发人员及时发现重复打包问题。例如,在 package.json 中添加脚本:

{
  "scripts": {
    "build:analyze": "webpack --config webpack.prod.js && webpack-bundle-analyzer dist/stats.json"
  }
}

这样在执行 npm run build:analyze 时,会在构建完成后自动打开依赖分析报告。

9.2 代码审查关注依赖引入

在代码审查过程中,关注模块的引入方式。检查是否存在通过不同路径引入相同模块的情况,及时发现潜在的重复打包风险。例如,如果发现某个模块在不同文件中通过相对路径和别名两种方式引入,就需要统一引入方式,避免重复打包。

9.3 定期优化依赖结构

随着项目的发展,依赖关系可能会变得复杂。定期对项目的依赖结构进行梳理和优化,删除不必要的依赖,合并重复的依赖。例如,检查项目中是否存在多个功能类似的库,是否可以用一个库替代,从而减少重复打包的可能性。

通过以上多种方案的综合应用,可以有效地避免 Webpack 中的重复打包问题,提升项目的性能和开发效率。在实际项目中,需要根据项目的特点和需求,选择合适的方案或组合使用这些方案。