close
  • 简体中文
  • 重复依赖问题

    Rsdoctor 会在产物预警中报告对同一份产物中含有多个重复依赖包的情况

    重复包的危害

    • 安全性
      • 很多包采用单例模式,默认用户使用时只会加载一次,如 core-js、react 和一些组件库会污染全局环境,多版本共存会引发运行时错误
    • 运行时性能
      • 增大产物体积,网络传输变慢
      • 相同功能的代码运行多次

    如何解决重复包问题?

    解决依赖多版本的问题,可以从依赖和构建两个层面解决。

    依赖层面

    1. 使用 npm dedupe命令

    一般包管理器会根据 semver 范围尽量安装相同版本的包,但由于 lock 文件的存在,长期项目可能会存在一些重复依赖。

    包管理器会提供 dedupe 命令如 npm/yarn/pnpm dedupe 等,在 semver 正确的范围内进行重复依赖的优化。

    2. 使用 resolutions

    在 semver 的限制下,dedupe 命令的效果可能不是特别好。比如产物中包含的依赖为 [email protected][email protected] 它们分别被 "debug": "^4" 和另一个包的 "debug": "^3" 所依赖。

    这时可以尝试使用包管理器的 resolutions 的功能,如 pnpm 的 pnpm.overrides.pnpmfile.cjsyarn 的 resolutions 等功能 。它们的特点是可以突破 semver 的束缚,安装时改变 package.json 中声明的版本号,来精确控制安装的版本。

    但在使用前需要注意包版本之间的兼容性,评估是否有必要进行优化。例如:同一个包不同版本之间的逻辑变化是否会影响项目功能。

    构建层面

    使用 resolve.alias

    几乎所有的打包工具都提供了自定义 npm 包解析路径的功能。因此我们可以通过在编译时手动指定 package 的 resolve 路径,来达到消除重复依赖的目的,例如:以 RspackWebpack 为例,如果 lodash 重复打包,我们可以进行如下配置,将所有 lodash 的 resolve 路径指定到当前目录的 node_modules 中。

    rspack.config.js
    const path = require('path');
    
    module.exports = {
      //...
      resolve: {
        alias: {
          lodash: path.resolve(__dirname, 'node_modules/lodash'),
        },
      },
    };

    这种方法同样需要注意包版本之间的兼容性。

    常见的重复包处理案例

    处理 pnpm-workspace 中的重复包

    该项目中,web 依赖了 [email protected] 并通过 "component": "workspace:*" 引入了 componentcomponent 依赖 [email protected]。项目结构如下:

    ├── node_modules
    ├── apps
    │   └── web
    │       ├── node_modules
    │       │   ├── component  -> ../../packages/component
    │       │   └── react      -> ../../../node_modules/.pnpm/[email protected]
    │       ├── src
    │       │   └── index.js
    │       ├── rspack.config.js
    │       └── package.json
    └── packages
        └── component
            ├── node_modules
            │   └── react      -> ../../../node_modules/.pnpm/[email protected]
            ├── src
            │   └── index.js
            └── package.json

    apps/web 下执行 rspack build,打包 web 下的代码时,会解析到 [email protected],接着打包 component 下的代码时,会解析到 [email protected],这导致 web 项目的产物中同时含有两个版本的 React

    解决方案

    此问题可以通过打包工具的 resolve.alias 来解决。让 Rspack 或 webpack 解析 React 时只解析到 apps/web/node_modules/react 这一个版本,示例代码如下:

    // apps/web/rspack.config.js
    const path = require('path');
    
    module.exports = {
      //...
      resolve: {
        alias: {
          react: path.dirname(require.resolve('react/package.json')),
        },
      },
    };

    Peer dependency 引起的重复包问题

    这种处理方法同样适用于由于 pnpm workspacepeerDependencies 多分身引起的重复包的项目中,项目目录结构如下:

    ├── node_modules
    ├── apps
    │   └── web
    │       ├── node_modules
    │       │   ├── component  ->  ../../packages/component
    │       │   ├── axios      ->  ../../../node_modules/.pnpm/[email protected][email protected]
    │       │   └── debug      ->  ../../../node_modules/.pnpm/[email protected]
    │       ├── src
    │       │   └── index.js
    │       ├── rspack.config.js
    │       └── package.json
    └── packages
        └── component
            ├── node_modules
            │   └── axios      ->  ../../../node_modules/.pnpm/[email protected]
            ├── src
            │   └── index.js
            └── package.json

    在该项目中,在 apps/web 下执行 rspack build,打包 web 下的代码时,会解析到 [email protected][email protected],接着打包 packages/component 下的代码时,会解析到 [email protected],它们虽然是同一版本,但路径不同,产物中也会存在两份 axios

    解决方案如下,让 web 项目构建时都只解析到 webnode_modulesaxios 包即可。

    // apps/web/rspack.config.js
    alias: {
      'axios': path.resolve(__dirname, 'node_modules/axios')
    }