关于 pnpm 管理参考:一文讲透 pnpm 管理 monorepo
什么是 Monorepo
Monorepo(单一仓库)是一种代码管理策略,它指的是在一个单一的版本控制仓库中维护多个项目或包。这些项目可能彼此独立,或者相互依赖,比如共享代码库、库组件、服务应用程序、前端应用等。
初始化仓库
-
创建项目目录 monorepo-demo 并初始化
package.json:# Linux / MacOS / Windows mkdir monorepo-demo cd monorepo-demo npm init在此步中填写正确的包名(可加入命名空间),例如 @demo/root,可忽略第四步中的操作。
-
初始化子项目 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,可忽略第四步中的操作。
-
由于根项目不需要发布,所以设置为私有化(等同于在根项目的
package.json中加入private="true"):npm pkg set private="true" -
(可选)设置根项目与子项目名称:
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
- 在子项目 a 中创建文件(或手动在 @demo/a 下创建两个文件:
index.js,answerer.js):
# Linux / MacOS
touch @demo/a/{index.js,answerer.js}
# Windows
type nul > @demo\a\index.js
type nul > @demo\a\answerer.js
-
index.js代码:console.log("Hello, @demo/a"); -
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
-
在子项目 b 中创建文件(或手动在 @demo/b 下创建一个文件:
index.js):# Linux / MacOS touch @demo/b/index.js # Windows type nul > @demo\b\index.js -
index.js代码:const a = require("@demo/a/answerer"); a.getAnswer();
配置运行脚本
-
为子项目添加运行脚本(等同于在子项目的
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 -
在根项目添加运行脚本,关联子项目运行脚本(等同于在根项目的
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 ... 时,做了三件事情:
-
在项目中自动创建 @demo/a 与 @demo/b 两个子项目。
-
在根
package.json文件中自动加入下面内容:"workspaces": [ "@demo/a", "@demo/b" ] -
由于
package.json中加入了workspaces,所以会自动在根项目的 node_modules 目录中创建子项目的依赖软链接。
其实可以试试手动将 workspaces 内容删除,运行 npm prune 或 npm 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 的效果:
- 进入 @demo/a 中,运行
npm link,将 @demo/a 项目以软链接的方式注册到全局的 npm 包目录中。 - 进入根项目目录,使用
npm link --save @demo/a命令,添加对 @demo/a 的依赖。
使用 npm unlink --save @demo/a 从项目中移除依赖。
使用 npm unlink --global @demo/a 从 npm 包目录中移除软链接。
本文为原创内容,版权所有 © 2024 姚生。欢迎转载,但请注明出处,并附上原文链接。未经授权不得用于商业用途。如有疑问,请联系作者。