monorepo 和 npm workspaces
在现代的Web开发中,项目变得越来越复杂。你可能有前端应用、后端API、共享的工具库等。如果所有这些都在不同的仓库中管理,会很难同步更新和依赖管理。Monorepo 和 npm workspaces
可以帮助解决这些问题。
什么是 Monorepo?
Monorepo 是“Mono Repository”的缩写,意味着所有的项目都存储在一个仓库中。这意味着,不同的项目或团队可以共享代码、依赖关系和其他资源,同时还能维护各自的开发和发布流程。
什么是 npm workspaces?
npm workspaces
是 npm 提供的一个功能,让你在 Monorepo 结构中更轻松地管理多个包或项目。在一个使用 npm 工作区(workspaces)的 monorepo 结构中,通常只有根目录下会生成一个package-lock.json
文件。这个文件会包含 monorepo 中所有工作区的依赖信息。标准的 npm 工作区结构不会为每个子工作区生成自己的package-lock.json
一个具体的例子,以说明在一个 monorepo 结构中使用单一的 package-lock.json
的好处。
假设您有一个名为“MegaApp”的大型项目,它是一个 monorepo,包含以下三个工作区或子项目:
frontend
:一个React前端应用。
backend
:一个Node.js的后端API。
utils
:共享的工具库,前端和后端都依赖于它。
这三个子项目都依赖于一个名为“AwesomeLib”的第三方库。在项目开始时,所有子项目都使用的是“AwesomeLib”的1.0.0
版本。
现在,假设frontend
团队发现了“AwesomeLib”的一个新功能,并决定升级到1.1.0
版本。如果每个子项目都有它自己的 package-lock.json
,则frontend
工作区可以轻松进行这种升级,而不影响其他部分。但很快,backend
团队可能会遇到与1.0.0
版本的“AwesomeLib”相关的一个已知错误,并决定也要升级到1.1.0
。这时,你就会得到一个状态,其中两个工作区使用的是1.1.0
版本,而utils
工作区仍然使用的是1.0.0
版本,这可能会导致难以追踪的错误和不一致。
有了单一的package-lock.json
,当frontend
团队升级到“AwesomeLib”的1.1.0
版本时,整个 monorepo 都会跟随升级。这确保所有子项目都使用相同版本的依赖项,从而减少了潜在的版本冲突。
此外,当其他团队成员或新的团队成员拉取项目并运行npm install
时,由于有一个统一的锁定文件,他们会得到确切相同的依赖版本,确保了项目的一致性和稳定性。
最后说一下 npm workspaces
的一些主要特点:
优点:
- 统一的依赖管理:所有子项目在同一个
node_modules
中共享依赖,这有助于确保整个 monorepo 中的所有项目使用相同版本的依赖项。
- 代码重用:共享公共逻辑或组件变得更加简单,因为所有的代码都在同一个代码库中。
- 原子提交:对多个项目的更改可以在一个单独的提交中完成,这使得跨项目的变更更容易跟踪和管理。
- 简化的版本管理:在 monorepo 中,所有子项目的版本可以一起更新,这简化了版本控制的过程。
- 跨项目的工具集成:例如,你可以在整个 monorepo 中使用一个统一的 linting 或测试设置。
缺点:
- 增大的代码库大小:随着时间的推移和项目的增加,代码库可能会变得非常大,这可能会导致一些工具(如某些 CI/CD 系统)运行变慢。
- 更复杂的CI/CD配置:需要更细致的策略来只构建和测试受到更改影响的项目。
- 潜在的版本冲突:如果不同的子项目需要不同版本的依赖,管理它们可能会变得更加困难。
- 学习曲线:对于那些不熟悉 monorepo 的开发者来说,初次设置和管理可能需要一些时间来适应。
- 隔离性问题:在单一的代码库中,一个子项目中的错误或问题可能会影响其他项目。
尽管如此,许多大型组织和项目(如 Facebook、Google 和 Babel)已经成功地采用了 monorepo,并证明了其可行性和效益。
Docker与NPM Workspaces: 如何平衡部署和依赖管理
我们常常会有多个子项目在一个大项目中。在这种情境下,如何管理和部署每个子项目变得至关重要。特别是,当我们想要结合Docker和npm workspaces时,这种管理变得更为复杂。
问题背景
- 当使用Docker来生成docker镜像以进行环境部署时,通常需要在每个子项目里都有
package-lock.json
。
- 然而,
npm workspaces
建议在子项目中不要有各自的package-lock.json
。
这两点看似矛盾,那么如何找到一个平衡点?
解决方案
为了平衡这两个需求,我们可以考虑在构建docker镜像时,从根目录复制package-lock.json
供每个子项目使用。
踩的坑以及如何爬出来 (bushi
- 使用根目录的
package-lock.json
,会导致子项目安装了不必要的依赖吗? - 答案: 不会。利用根目录下的
package-lock.json
来执行npm install
,系统只会安装子项目自己需要的包。因此,构建或部署的时间不会因为多余的依赖而增加。
- 更新某个子项目的包版本时,会不会导致所有子项目的依赖都需要同时更新和测试?
- 答案: 应该不会。在docker内部,我们可以使用特定指令(例如
npm run dev -w myapp
)来为特定子项目单独安装或更新依赖。这确保了更新某个子项目的依赖版本不会对其他子项目造成影响。
参考
Docker setup for yarn workspaces
Using npm Workspaces with Docker
来创建一个demo吧
首先,创建一个新的空的目录并初始化一个新的NPM项目:
mkdir monorepo-demo
cd monorepo-demo
npm init -y
在项目的根目录中创建工作区的目录结构:
mkdir packages
cd packages
mkdir frontend
mkdir bff
这样就创建了 frontend 和 bff 两个工作区。
现在我们需要在每个工作区中创建对应的Next.js和Nest.js应用:
在frontend工作区中创建一个Next.js应用:
cd frontend
npx create-next-app .
在bff工作区中创建一个Nest.js应用:
首先全局安装@nestjs/cli
:
npm install -g @nestjs/cli
然后在bff工作区中创建一个新的Nest.js应用:
cd ../bff
nest new .
现在你的目录结构应该是这样的:
monorepo-demo
- packages
- frontend (Next.js 应用)
- bff (Nest.js 应用)
接下来我们需要配置npm工作区。在 package.json
中,我们将配置npm工作区。将以下内容添加到你的 package.json
:
{
"name": "monorepo-demo",
"private": true,
"workspaces": ["packages/*"]
}
首先,我们需要创建用户服务。在 Nest.js 应用中,添加一个新的 users
模块:
cd packages/bff nest generate module users nest generate service users nest generate controller users
这将会创建 users
模块,服务和控制器。
在 users.service.ts
中,我们会创建一个模拟的用户数据:
import { Injectable } from '@nestjs/common'; @Injectable() export class UsersService { private readonly users = [ { id: 1, name: 'John Doe' }, { id: 2, name: 'Alice Cullen' }, { id: 3, name: 'Bob Hope' }, ]; findAll() { return this.users; } }
然后我们在 users.controller.ts
中添加一个新的API端点,通过调用 UsersService
来获取所有的用户:
import { Controller, Get } from '@nestjs/common'; import { UsersService } from './users.service'; @Controller('users') export class UsersController { constructor(private usersService: UsersService) {} @Get() findAll() { return this.usersService.findAll(); } }
现在,你可以启动 Nest.js 服务,然后在浏览器中访问 http://localhost:3001/users
,你应该可以看到我们模拟的用户数据。
然后我们需要在 Next.js 应用中创建一个新的页面来展示这些用户。在 pages/users.js
中添加以下内容:
import axios from 'axios'; export default function Users({ users }) { return ( <div> <h1>Users:</h1> {users.map((user) => ( <div key={user.id}>{user.name}</div> ))} </div> ); } export async function getServerSideProps() { const res = await axios.get('http://localhost:3001/users'); return { props: { users: res.data } }; }
最后,你可以在根目录下运行以下命令来分别启动你的应用:
npm run dev -w frontend
npm run start -w bff
然后你可以在浏览器中访问 http://localhost:3000/users
,你将看到从 BFF 获取的用户数据被正确地显示在页面上。
之后可以自己添加一些数据交互来丰富bff的功能。比如删除user, 重置user数据, 添加user等。
源码已经公开,请参照下面的github repo: