一文讲透 npm 管理 monorepo

1,208 阅读7分钟

关于 pnpm 管理参考:一文讲透 pnpm 管理 monorepo

什么是 Monorepo

Monorepo(单一仓库)是一种代码管理策略,它指的是在一个单一的版本控制仓库中维护多个项目或包。这些项目可能彼此独立,或者相互依赖,比如共享代码库、库组件、服务应用程序、前端应用等。

初始化仓库

  1. 创建项目目录 monorepo-demo 并初始化 package.json

    # Linux / MacOS / Windows
    mkdir monorepo-demo
    cd monorepo-demo
    
    npm init
    

    在此步中填写正确的包名(可加入命名空间),例如 @demo/root,可忽略第四步中的操作。

  2. 初始化子项目 a 与 b:

    # --workspace 可以简写为 -w
    npm init --workspace @demo/a
    npm init --workspace @demo/b
    

    完成上一步后会在根目录的 package.json 中加入 workspaces 属性,属性值代表工作区。该值也可以使用通配符,例如 @demo/* 表示所有 @demo 目录下的内容。

    当前目录结构:

    .
    ├── @demo
    │   ├── a
    │   │   └── package.json
    │   └── b
    │       └── package.json
    ├── node_modules
    │   └── @demo
    │       ├── a ⇒ ../../@demo/a	# 软链接
    │       └── b ⇒ ../../@demo/b	# 软链接
    ├── package-lock.json
    └── package.json
    

    在此步中填写正确的包名称(可加入命名空间),例如 @demo/a,可忽略第四步中的操作。

  3. 由于根项目不需要发布,所以设置为私有化(等同于在根项目的 package.json 中加入 private="true"):

    npm pkg set private="true"
    
  4. (可选)设置根项目与子项目名称:

    npm pkg set name="@demo/root"
    npm pkg set name="@demo/a" --workspace @demo/a
    npm pkg set name="@demo/b" --workspace @demo/b
    

    注意,如果更改了子项目名称,记得更新根目录 package.json 中的 workspaces

依赖安装

安装全局(公共)依赖

lodash 依赖,在根目录中运行:

npm install --save lodash

基于子项目安装依赖

axios 依赖,三种方式:

  • 基于工作区的安装方式:

    npm install axios --workspace @demo/a
    
  • 基于 npm 运行目录的安装方式:

    npm install --save axios@3.1.0 --prefix @demo/b
    
  • 进入子项目目录安装:

    # Linux / MacOS
    cd @demo/a
    
    # Windows
    cd @demo\a
    
    npm install --save axios
    

编写测试代码

@demo/a

  1. 在子项目 a 中创建文件(或手动在 @demo/a 下创建两个文件:index.jsanswerer.js):
# Linux / MacOS
touch @demo/a/{index.js,answerer.js}

# Windows
type nul > @demo\a\index.js
type nul > @demo\a\answerer.js
  1. index.js 代码:

    console.log("Hello, @demo/a");
    
  2. answerer.js 代码:

    const axios = require("axios");
    
    module.exports.getAnswer = () => {
      axios({
        method: "get",
        url: "https://yesno.wtf/api",
      }).then(({ data }) => console.log(JSON.stringify(data)));
    };
    

@demo/b

  1. 在子项目 b 中创建文件(或手动在 @demo/b 下创建一个文件:index.js):

    # Linux / MacOS
    touch @demo/b/index.js
    
    # Windows
    type nul > @demo\b\index.js
    
  2. index.js 代码:

    const a = require("@demo/a/answerer");
    
    a.getAnswer();
    

配置运行脚本

  1. 为子项目添加运行脚本(等同于在子项目的 package.json 中加入 start="node index.js" 的运行脚本):

    npm pkg set scripts.start="node index.js" --workspace @demo/a
    npm pkg set scripts.start="node index.js" --workspace @demo/b
    
  2. 在根项目添加运行脚本,关联子项目运行脚本(等同于在根项目的 package.json 中加入两个 scripts 的运行脚本):

    npm pkg set scripts.start:a="npm run start --workspace @demo/a"
    npm pkg set scripts.start:b="npm run start --workspace @demo/b"
    npm pkg set scripts.start:all="npm run start --workspace @demo/a --workspace @demo/b"
    

运行

在根目录运行子项目:

npm run start:a		# 运行子项目 a,显示 Hello, @demo/a
npm run start:b		# 运行子项目 b,显示通过 a 请求的接口数据
npm run start:all	# 同时运行两个子项目

同样可以分别进入子项目运行其中的 start 脚本。

更多

关于 workspace

package.json 中的 workspaces 属性是 npm v7.0.0 引入的配置项,用于管理多包存储库中的依赖项和脚本的配置项。

在使用 npm init --workspace ... 时,做了三件事情:

  1. 在项目中自动创建 @demo/a 与 @demo/b 两个子项目。

  2. 在根 package.json 文件中自动加入下面内容:

    "workspaces": [
      "@demo/a",
      "@demo/b"
    ]
    
  3. 由于 package.json 中加入了 workspaces,所以会自动在根项目的 node_modules 目录中创建子项目的依赖软链接。

其实可以试试手动将 workspaces 内容删除,运行 npm prunenpm install,此时,node_modules 下面的子项目依赖将会被移除。

恢复 workspaces 内容后,再次运行 npm install,将会重新引用子项目。

workspaces 的目的其实很简单,同时管理和操作 monorepo 的子应用。

--prefix--workspace 区别

  • --prefix:用于指定 npm 命令要运行的目录。当你使用这个选项时,npm 会将该目录当作是它正在操作的项目根目录。

  • --workspace:用于支持工作区,指定使用哪个工作区的上下文,该选项可以指定一个或多个工作区来运行 npm 命令。

大多情况,可以使用 --prefix 替换 --workspace,命令执行后的结果是相同的。--prefix 指定为真实的目录名称,但是 --workspace 可以指定定义的工作区名称或者目录名称。建议使用 --workspace

在为子项目单独安装依赖时,二者还是有区别的:

  • 当使用 --prefix 时,依赖是明确安装在子应用的 node_modules 目录中,根项目的 node_modules 不变:

    npm isntall lodash --prefix @demo/a
    npm isntall lodash --prefix @demo/b
    
    # 目录结构
    .
    ├── @demo
    │   ├── a
    │   │   ├── node_modules
    │   │   │   └── lodash
    │   │   ├── package-lock.json
    │   │   └── package.json
    │   └── b
    │       ├── node_modules
    │       │   └── lodash
    │       ├── package-lock.json
    │       └── package.json
    └── package.json
    
  • 当使用 --workspace 时,如果没有版本冲突,依赖会安装在根目录(提升缘故)的 node_modules 中:

    npm install lodash --workspace @demo/a
    npm install lodash --workspace @demo/b
    
    # 目录结构
    .
    ├── @demo
    │   ├── a
    │   │   └── package.json
    │   └── b
    │       └── package.json
    ├── node_modules
    │   ├── @demo
    │   └── lodash
    ├── package-lock.json
    └── package.json
    

注意,当使用 --prefix 形式安装完依赖,一旦在根目录执行 npm install,结果将会与使用 --workspace 的结构一样。这是由于 npm 的依赖提升原因。

情景假设:当别人拉取了代码,使用 npm install 在根目录安装依赖,其实最终的效果就是 --workspace 的效果。

如果你是个强迫症患者,需要依赖都安装在子应用中,可将根目录执行的 npm --install 替换为 npm install --install-strategy nested

关于子项目依赖的版本冲突

假设子项目 a 需要使用 lodash@3.0.0 版本,子项目 b 需要使用 lodash@2.0.0,其他子项目使用公共的 lodash@4.0.0,那么 npm 的依赖提升是否会造成版本冲突,是如何管理的?

可以在根项目下安装通用 lodash@4.0.0,基于子项目 a 安装 lodash@3.0.0,基于子项目 b 安装 lodash@2.0.0

npm install lodash@4.0.0 --save
npm install lodash@3.0.0 --workspace @demo/a
npm install lodash@2.0.0 --workspace @demo/b

.
├── @demo
│   ├── a
│   │   ├── node_modules
│   │   │   └── lodash			# 3.0.0
│   │   └── package.json
│   └── b
│       ├── node_modules
│       │   └── lodash			# 2.0.0
│       └── package.json
├── node_modules
│   ├── @demo
│   │   ├── a ⇒ ../../@demo/a
│   │   └── b ⇒ ../../@demo/b
│   └── lodash					# 4.0.0
│       ├── add.js
│       ├── .
│       ├── .
│       ├── .
│       └── zipWith.js
├── package-lock.json
└── package.json

可以看到结果,是没有任何问题的。

如果没有通用版本,只有两个子项目的版本冲突,那么其中一个子项目的依赖会被提升至根目录的 node_modules 中,另一个依赖被安装至自己的 node_modules 中。

npm 依赖提升

Hoist(提升)是 npm(Node Package Manager,Node.js包管理器)在安装依赖时使用的一种策略。这个策略的目标是尽可能地减少存储在 node_modules 中的重复模块,从而降低项目的磁盘空间占用。

在没有 hoist 提升策略的情况下,每一个项目或者模块都会有自己的 node_modules 文件夹,这个文件夹里包含了该项目或模块所需的所有依赖。如果多个项目或模块依赖同一个模块,那么这个模块将会在每个 node_modules 文件夹中都有一个副本,这就造成了大量的重复和浪费。

npm 的 hoist 提升策略试图解决这个问题。在安装依赖时,npm 会检查所有的依赖关系,并尝试将共享的模块“提升”到一个更高级别的 node_modules 文件夹中。这样,所有依赖同一个模块的项目或模块都可以共享这个模块,而不需要每个都有一个副本。

如果不使用 workspaces,还有其他方式达到类似的效果吗?

可以的,模拟 workspaces 的效果:

  1. 进入 @demo/a 中,运行 npm link,将 @demo/a 项目以软链接的方式注册到全局的 npm 包目录中。
  2. 进入根项目目录,使用 npm link --save @demo/a 命令,添加对 @demo/a 的依赖。

使用 npm unlink --save @demo/a 从项目中移除依赖。

使用 npm unlink --global @demo/a 从 npm 包目录中移除软链接。

本文为原创内容,版权所有 © 2024 姚生。欢迎转载,但请注明出处,并附上原文链接。未经授权不得用于商业用途。如有疑问,请联系作者。