前言
公司现在的工程结构是前端工程和 Node 工程放在同一个仓库中,可以在 CICD 和机器上省钱,但是团队并没有对工程结构提出规范,这就导致开发们自由发挥,出现了工程目录混乱、可读性差的问题。
目前这段时间正好是我的业务空窗期,所以想尝试解决这个问题,经过了一番探索最终落地方案选择了 PNPM + Monorope 模式。
由于痛点是在开发背景限制下优化单仓库前后端结构问题,所以说此方案不是一个传统的 Monorope 工程,一是没发挥共享组件功能,二是没充分发挥依赖包共享功能,三是允许幻影依赖。
希望能帮助到与我有相同开发背景的达瓦里氏
背景
公司的 CICD 流程是由运维部门负责的,也就是说开发是不能直接修改 CICD 脚本的。这是出于安全的考虑,但是带来的影响则是 team 间的沟通成本增加了开发的创造成本。
所以我也将在不改动现有CICD脚本的情况下,来实现工程结构的升级。
一方面不增加与运维部门的沟通成本,另一方面在向其他team推行此方案时更顺利。
如果你的开发背景与我类似,那强烈邀请看到最后。
先看目录结构
当前结构
ng + express 为例
.
├── README.md
├── angular.json
├── decorate-angular-cli.js
├── dist
│ ├── app
│ └── server
├── http-client.env.json
├── ngsw-config.json
├── nx.json
├── package-lock.json
├── package.json
├── src
│ ├── app
│ ├── assets
│ ├── design.scss
│ ├── environments
│ ├── favicon.ico
│ ├── index.app.html
│ ├── index.html
│ ├── index.latest.html
│ ├── index.pro.html
│ ├── index.stage.html
│ ├── main.ts
│ ├── server // node 工程
│ │ ├── ...
│ │ └── server.ts
│ ├── styles.scss
│ └── types.d.ts
├── tsconfig.app.json
├── tsconfig.json
├── tsconfig.spec.json
└── tslint.json
改为 Monorepo 模式后
├── app // ng 工程
| ├── src
| ├── README.md
| ├── angular.json
| ├── karma.conf.js
| ├── .browserslistrc
| ├── .editorconfig
| ├── package.json
| ├── tsconfig.app.json
| └── tsconfig.json
├── server // node 工程
| ├── src
| ├── .eslintrc.js
| ├── .prettierrc
| ├── nest-cli.json
| ├── README.md
| ├── tsconfig.build.json
| ├── tsconfig.json
| └── package.json
├── dist
| ├── app
| ├── server
| └── package.json
├── node_modules // 包含了app和server的依赖
├── .npmrc // pnpm config
├── pnpm-workspace.yaml // workspaces 声明
├── package.json // 根目录下的package可以编写一些 script 来聚合app和server中的 script,比如 build 和 start
├── Readme.md
可以看到主要的改动是
- 提升了 server 的目录层级
- 使 app 与 server 工程高度内聚
- 隔离了 app 与 server 工程
与嵌套相比,Monorope 的好处
- 项目结构更清晰,可读性增强
- 省去了手动合并 package.json 的任务
- app与server的隔离等级提升,避免了ts,tslint,eslint等等不必要的互相影响
如何构建
在开始前可以先阅读下一下几篇文章帮助理解
初始化项目
以 new-bff 举例
新建项目文件夹 new-bff
在项目根目录添加 .npmrc
auto-install-peers=true
strict-peer-dependencies=false
shamefully-hoist=true
在项目根目录添加 pnpm-workspace.yaml
packages:
- 'app'
- 'server'
初始化 app 和 server
app
app 可以选用你想用的前端框架:Ng,React,Vue,以下使用 Ng 举例
npm install -g @angular/cli
cd new-bff
ng new app
server
与 app 相同,你也可以使用你熟悉的 web 框架,以下使用 nestjs 举例
npm install -g @nestjs/cli
cd new-bff
nest new server
安装项目依赖
安装 pnpm
npm install -g pnpm
安装依赖
pnpm install
pnpm 会遍历 pnpm-workspace.yaml 中声明的工作空间,并执行 install
执行 build
pnpm run -r build
pnpm 会遍历 pnpm-workspace.yaml 中声明的工作空间,并执行 run build
提醒 :bulb:
可以将构建命令与启动命令集成到根目录的 package.json 的
scripts
中
注意 :warning:
- 在
build
前请先检查app/angular.json
中的projects.app.architect.build.options.outputPath
属性是否指向了根目录下的dist
文件夹- 在
build
前请先检查server/tsconfig.json
中的compilerOptions.outDir
属性是否指向了根目录下的dist
文件夹
小结
至此整个 new-bff 项目的构建就完成了。
如果只是想用 PNPM 来构建 Monorope 模式的单仓库的前后端项目的话,可以说已经结束了。
别忘了点赞哟(づ ̄3 ̄)づ╭❤~
下面的内容,是我在结合公司现有的 CICD 过程中遇到的一些问题和解决过程。
采坑经历
记录了方案的迭代过程和是如何做到兼容 CICD 的
如何兼容现有Jenkins 的 CICD 脚本
现有 CICD 脚本
可以简单概括为以下流程
- clone 项目到服务器
- 根目录下运行
npm install
安装依赖 - 运行 package.json 中的
build
命令 - 运行
npm pack
打包并压缩项目源代码(不含node_modules)并缓存 - 运行
npm prune
对node_modules
瘦身 - 运行
tar cvxf
命令对根录下的node_modules
压缩,并缓存 - 执行
node dist/server/main.js
启动服务
之所以要对源码和node_modules
分别做压缩缓存,主要目的是可以将代码与依赖解耦。
经过解耦之后,如果我们一个新的版本中package.json
没有更新只有代码更新,过程中就可以省略5、6两步了
问题来了
使用 yarn workspaces
yarn workspaces 的使用可以看这篇文章
使用yarn workerspace
安装依赖后,在两个子空间中都会产生一个node_modules
├── app
| ├── package.json
| └── node_modules
├── server
| ├── package.json
| └── node_modules
├── dist
| ├── app
| | ├── package.json
| ├── server
| | ├── package.json
| └── package.json
├── package.json
├── node_modules
现在我们的项目中一共有三个node_modules
了,三个node_modules
中的内容是不同的,如果只是这样这个方案是没有问题的。
但是如果我们用这个方案到服务器上部署,那么在 CICD 过程中,就会遇到两个问题
- 只有根目录下的
node_modules
会被压缩缓存 node dist/server/main.js
命令会失败,因为 node 查找依赖的逻辑是从package.json
文件的所在目录开始逐级向上查找。dist 文件夹下是不会有node_modules
的,根目录下的 app 和 server 的node_modules
也是不被访问到的。
在 yarn 的文档中我没有找到可以将子空间中的node_modules
的内容提升到根目录的node_modules
中,所以 yarn 无法满足我们的需求
也可能是我没有找到,大家可以尝试再看看 yarn 文档
使用 pnpm workspaces
将 yarn 替换为 pnpm 后,上述问题也是存在的,项目中还是存在三个node_modules
├── app
| ├── package.json
| └── node_modules
| | └── ng@1.0.0 // 依赖并不存在于当前位置,当前位置的依赖包只是一个软链接(一个快捷方式)
├── server
| ├── package.json
| └── node_modules
| | └── nest@1.0.0 // 依赖并不存在于当前位置,当前位置的依赖包只是一个软链接(一个快捷方式)
├── dist
| ├── app
| | ├── package.json
| ├── server
| | ├── package.json
| └── package.json
├── package.json
├── node_modules
| └── .pnpm // 依赖的真实安装位置
| | ├── ng@1.0.0
| | └── nest@1.0.0
不同之处在于子空间中的node_modules
指向了根目录下的node_modules
。
当我走到这一步时,我感觉我已经成功了。
既然依赖包是安装在根目录下的node_modules
,那即使 dist 中子空间不包含node_modules
,那必然可以逐级向上获取到根目录下的依赖,所以我尝试运行node dist/server/main.js
,结果
❯ node dist/server/main.js system 12:48:47
internal/modules/cjs/loader.js:905
throw err;
^
Error: Cannot find module '@nestjs/core'
Require stack:
- /new-bff/dist/server/main.js
打脸是真的快 :facepalm:
原因是根目录的node_modules
下多了一层.pnpm
路径
如何取消.pnpm路径
.pnpm
的作用可以看这篇文章
简单来说.pnpm
的作用就是隔离各个子空间的依赖,避免幻影依赖问题。关于幻影依赖问题我就不在这里解释了上面的文章中有说到。
那我要取消.pnpm
不就意味着我会出现幻影依赖问题吗?
经过调查发现,其实不会,因为幻影依赖问题也可以通过eslint
来解决。
退一步讲,即使我不解决幻影依赖也可以,因为它并不会对我的代码运行产生影响,因为我们的子空间并不会被发布到npm上,所以即使发生了也无所谓。
好了,那如何取消.pnpm
呢?办法就在根目录下的.npmrc文件中
shamefully-hoist=true
使用这个设置来取消.pnpm
隔离,关于这个设置可以在pnpm文档中查看。
最后
现在回顾下 CICD 流程,服务器上只缓存根目录的node_modules
的情况下,当我们运行node dist/server/main.js
,会逐级向上查找依赖,找到根目录的node_modules
,CICD 流程不需要任何改动,我们就能收获一个干净整洁的项目工程。
谢谢观看,别忘了点赞哟