多项目版本管理:monorepo 策略

news2025/1/22 9:25:48

monorepo 是什么

一个产品会有多个项目,每个项目之间会存在版本同步的问题,如何在其中一个项目发布上线后,保证每个项目版本升级后的版本同步问题,提出的解决方案就是 monorepo 策略。

monorepo 是一种将多个项目代码存储在一个仓库里的软件开发策略(mono 意为单一,repo 意为 仓库)。与之相对的是另一种流行的代码管理方式 MultiRepo,即每个项目对应一个单独的仓库来分散管理。

无论是比如 Google、Facebook,还是社区内部知名的开源项目 Babel、Vue3都使用了 monorepo 方案来管理他们的代码。

通过 monorepo 策略管理代码的代码仓库的目录结构会是这样:

不过,monorepo 不只是将不同子项目的目录汇集到一个目录之下,实际上操作起来所要考虑的事情复杂得多。

monorepo 的优劣

monorepo 不是银弹,有优亦有劣:

  • 易于代码复用:所有项目代码集中于单一仓库,易于抽离出共用的业务组件或工具,并通过 TypeScript,Lerna 或其他工具进行代码内引用;
  • 易于依赖管理:项目之间的引用路径内化于单一仓库,当某个项目的代码修改后,易于追踪其影响的是其他哪些项目。通过工具,易于版本依赖管理和版本号自动升级;并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况;
  • 易于代码重构:代码重构难在不确定对某个项目的修改是否对于其他项目是破坏性的。而 monorepo 使得能够明确知道代码的影响范围,并且能够对被影响的项目可以进行统一的测试,利于不断优化代码;
  • 倡导开放,透明,共享的组织文化,有利于开发者成长,代码质量的提升:monorepo 使得开发者都被鼓励去查看和修改他人的代码,同时,也会激起开发者维护代码,和编写单元测试的责任心(毕竟朋友来访之前,我们从不介意自己的房子究竟有多乱),这将会形成一种良性的技术氛围,从而保障整个组织的代码质量;
  • 不易于项目粒度的权限管理;
  • 额外的学习成本:monorepo 使得增加理清各个代码仓库之间的相互逻辑的成本;
  • 需要工具链和自动构建工具的支持:项目若很庞大且没有工具链的支持,那么 git 管理、安装依赖、构建、部署会很麻烦和耗时。比如可以基于 Lerna、Yarn Workspaces 等工具更加自动化的处理依赖包之间的构建和发布。

在实际场景来落地项目级别的 Monorepo,需要一套完整的工程体系来进行支撑,因为基于 Monorepo 的项目管理,不只是代码放到一起,还需要考虑项目间依赖分析、依赖安装、构建流程、测试流程、CI 及发布流程等诸多工程环节,同时还要考虑项目规模到达一定程度后的性能问题,比如项目构建/测试时间过长需要进行增量构建/测试、按需执行 CI 等等,在实现全面工程化能力的同时,也需要兼顾到性能问题。

Monorepo 如何落地

1 锁定环境:volta or nvm

除了使用 Docker 和显式的在文档中声明 node 和 npm(yarn)的版本之外,我们需要一个锁定环境的强力工具。相比使用 nvm,volta 支持当项目 CLI 工具与全局不兼容时,自动切换为项目指定的版本。

Volta 是用 Rust 构建的 JavaScript 工具管理器,它可以让我们轻松地在项目中锁定 node,npm(yarn) 的版本。只需在安装完 Volta 后,在项目的根目录中执行 volta pin 命令,那么无论您当前使用的 node 或 npm(yarn)版本是什么,volta 都会自动切换为您指定的版本。

volta pin node@12.20
volta pin yarn@1.19

/**

"volta": {
  "node": "12.20.2",
  "yarn": "1.19.2"
}

*/

2 利用 workspace 特性复用 package

workspace 特性使得:

  1. 避免重复安装包,减少了磁盘空间的占用,并降低了构建时间;
  2. 内部代码可以彼此相互引用;

考虑到支持 workspace 的 npm 版本目前不是 LTS,所以选择 yarn 作为包管理工具,而且需要完成如下工作:

        1. 调整目录结构,将相互关联的项目放置在同一个目录,推荐命名为 packages

请注意对子项目的命名统一以 @<repo_name>/ 开头,这是一种社区最佳实践,更容易让其他开发者了解整个应用的架构和在项目中找到所需的子项目。

        2. 在项目根目录里的 package.json 文件中,设置 workspaces 属性,属性值为之前创建的目录(packages);

比如在 babel 中:

 "workspaces": [
    "codemods/*",
    "eslint/*",
    "packages/*",
    "test/esm",
    "test/runtime-integration/*",
    "benchmark"
  ],

        3. 为了避免我们误操作将仓库发布,在 package.json 文件中,设置 private 属性为 true。

然后,在项目根目录中执行 npm installyarn install 后,项目根目录中生成由所有子项目共用的 npm 包和我们自己的子项目共同构成的 node_modules 目录,正因如此,才使得可以像引入一般的 npm 模块一样彼此相互引用。

3 统一 ESlint / Typescript / babel 配置

可以在 项目根目录设置通用的 tsconfig.base.json / .eslintrc / .babelrc 配置,然后在子项目的对应配置文件中声明继承 extend 属性即可:

// babel
{
  "extends": "../../.babelrc"
}

// eslint
{
  "extends": "../../.eslintrc",
  "parserOptions": {
    "project": "tsconfig.json"
  }
}

// typescript
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "composite": true, // 用于帮助 TypeScript 快速确定引用工程的输出文件位置
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

项目目录大致如下所示:

4 统一脚本 script 配置

每个子项目的 package.json 文件中的 scripts 属性都大同小异,我们可以使用 scripty 管理脚本命令提高复用性,简单来说,scripty 允许将脚本命令定义在文件中,并在 package.json 文件中直接通过文件名来引用。这使我们可以实现如下目的:

  • 子项目间复用脚本命令;
  • 像写代码一样编写脚本命令,无论它有多复杂,而在调用时,像调用函数一样调用;

同时,将脚本分为两类 package 类与 workspace 类,分别放在两个文件夹内:

如此既可以在项目根目录执行全局脚本,也可以针对单个项目执行特定的脚本。

注意:需要使用 chmod -R u+x scripts 命令使所有的 shell 脚本具备可执行权限,可以将其备注在 README.md 中。

子项目的 package.json 文件中的 scripts 属性简化为:

5 利用 lerna 统一包管理 

lerna 可以帮助解决 monorepo 因为多个子项目放在一个代码仓库,并且子项目之间又相互依赖时带来的两个棘手问题:

  • 在多个子目录执行相同的命令时需要手动进入各个目录,并执行命令;

  • 子项目更新后只能手动追踪依赖该项目的其他子项目,并升级其版本。

首先,在项目根目录使用 npx lerna init 初始化,根目录会生成一个 lerna.json 文件,稍作改动:

{
  "packages": ["packages/*"],
  "npmClient": "yarn", // 显式声明包客户端为 yarn
  "version": "independent",  // 将每个子项目的版本号看作是相互独立的
  "useWorkspaces": true, // 开启 workspaces 特性:子项目引用和通用包提升
}

当某个子项目代码更新后,运行 lerna publish 时,Lerna 将监听到代码变化的子项目并以交互式 CLI 方式让开发者决定需要升级的版本号,关联的子项目版本号不会自动升级,反之,当我们填入固定的版本号时,则任一子项目的代码变动,都会导致所有子项目的版本号基于当前指定的版本号升级。

Lerna 有很多有用的 CLI 命令:

  • lerna bootstrap:等同于 lerna link + yarn install,用于创建符合链接并安装依赖包;
  • lerna run:会像执行一个 for 循环一样,在所有子项目中执行 npm script 脚本,并且,它会非常智能的识别依赖关系,并从根依赖开始执行命令;
  • lerna exec:像 lerna run 一样,会按照依赖顺序执行命令,不同的是,它可以执行任何命令,例如 shell 脚本;
  • lerna publish:发布代码有变动的 package,因此首先需要在使用 Lerna 前使用 git commit 命令提交代码,好让 Lerna 有一个 baseline;
  • lerna add:将本地或远程的包作为依赖添加至当前的 monorepo 仓库中,该命令让 Lerna 可以识别并追踪包之间的依赖关系,因此非常重要;比如# 向 @mono/project2 和 @mono/project3 中添加 @mono/project1:
lerna add @mono/project1 '@mono/project{2,3}'

lerna 中有用的参数:

  • --concurrency <number>:参数可以使 Lerna 利用计算机上的多个核心,并发运行,从而提升构建速度;
  • --scope '@mono/{pkg1,pkg2}'--scope 参数可以指定 Lerna 命令的运行环境,通过使用该参数,Lerna 将不再是一把梭的在所有仓库中执行命令,而是可以精准地在我们所指定的仓库中执行命令,并且还支持示例中的模版语法;
  • --stream:该参数可使我们查看 Lerna 运行时的命令执行信息

结合 verdanccio 在本地创建 npm 代理仓库,先发布体验和验证,全局安装 npm install --global verdaccio,在项目根目录创建 .npmrc 文件,并在文件中将 npm 仓库地址改写为本地代理地址 registry="http://localhost:4873/",执行 shell 命令 verdaccio 后,访问  localhost:4837即可,这样,每当执行 lerna publish 时,子项目所构建成的 package 将会先发布在本地 npm 仓库中,只有执行 lerna bootstrap 时,verdaccio 才发布到远程 npm。

6  commitlint:约束 commit  信息

commitlint 可以帮助我们检查提交的 commit 信息,它强制约束我们的 commit 信息必须在开头附加指定类型,用于标示本次提交的大致意图,支持的类型关键字有:

  • feat:表示添加一个新特性;
  • chore:表示做了一些与特性和修复无关的改动;
  • fix:表示修复了一个 bug;
  • refactor:表示本次提交是因为重构了代码;
  • style:表示代码美化或格式化;
  • ...

因为 monorepo 仓库可能被不同的开发者提交不同子项目的代码,规范化的 commit 信息在故障排查或版本回滚时是很有必要的。

可以通过下面的命令安装 commitlint 以及周边依赖,其中 husky 使得能在提交 commit 信息时自动运行 commitlint 进行检查:

项目根目录下的 package.json 文件中 husky 配置为: 

在项目根目录中增加 commitlint.config.js 文件使得 commitlint 能感知到子项目名称,并设置文件内容为:

module.exports = {
  extends: [
    "@commitlint/config-conventional",
    "@commitlint/config-lerna-scopes",
  ],
};

除此之外,commitlint 还支持显示指定本次提交所对应的子项目名称。比如,针对名为@mono/project1 的子项目提交的 commit 信息可以写为:

而且,可以通过在命令行执行 echo "build(project1): change something" | npx commitlint 命令即可验证 commit 信息是否通过 commitlint 的检查。

总结:利用 tomono 基于已有的项目转化为 monorepo 项目

lerna import 命令用来将已有的包导入到 monorepo 仓库,并且还会保留该仓库的所有 commit 信息,但是该命令仅支持导入本地项目,并且不支持远程仓库、以及导入项目的分支和标签。

所以可以使用 tomono 导入远程仓库:

  • 首先下载 tomono 在用户根目录(~),然后创建一个包含所有需要导入 repo 地址的文本文件 repos.txt:
//格式为: Git仓库地址 子项目名称 迁移后的路径
git@github.com/backend.git @mono/backend packages/backend
git@github.com/frontend.git @mono/frontend packages/frontend
git@github.com/mobile.git @mono/mobile packages/mobile
  • 再执行 shell 命令即可导入:
cat repos.txt | ~/tomono/tomono.sh

除了上述的基于 Lerna 负责发布和版本控制,而使用 Yarn Workspaces 来管理多个应用程序之间的依赖的偏底层 monorepo 方案。也有一些集成的 Monorepo 方案,比如 nx 、rushstack,提供从初始化、开发、构建、测试到部署的全流程能力,有一套比较完整的 Monorepo 基础设施,适合直接拿来进行业务项目的开发。

而且基于 lerna 进行构建的 monorepo 项目,如果构建多个应用程序依赖,耗时很长,可以探索诸如 TurboRepo 的方案解决。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/408467.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【小程序项目开发-- 京东商城】uni-app之商品列表页面 (上)

&#x1f935;‍♂️ 个人主页: 计算机魔术师 &#x1f468;‍&#x1f4bb; 作者简介&#xff1a;CSDN内容合伙人&#xff0c;全栈领域优质创作者。 该文章收录专栏 ✨ 2022微信小程序京东商城实战 ✨ 文章目录一、前言介绍二、创建goodlist 分支&#xff08;选读*)三、商品列…

H5页面跳转小程序的三种方式

文章目录前言一、web-view标签返回小程序1.小程序启动页面只写web-view标签跳转到授权页面。2.编写auth.html3、把auth.html放到服务器就可以测试访问&#xff0c;打开小程序默认进入启动页面中的webview跳转到H5&#xff0c;授权成功后&#xff0c;通过wx.miniProgram.reLaunc…

Vue自定义网页顶部导航栏

Vue自定义web网页顶部导航栏 说明&#xff1a;此组件是为论坛类项目定制的一个实用的顶部导航栏&#xff0c;当然也可以用于其他的Web项目&#xff0c;只需要稍作修改便可以达到想要的效果。其中导航栏包含了搜索栏&#xff0c;用户头像&#xff0c;以及基本的导航标题。导航栏…

uniapp小程序自定义顶部导航栏,输入框软键盘把顶部顶上去的解决方法

首先在小程序input标签增加:adjust-position"false"的属性&#xff0c;然后已经可以把软键盘不使上方顶出&#xff0c;但是输入框也会因此被遮挡 解决方法&#xff1a;在input输入框聚焦的方法中增加操作 focus"inputBindFocus" 定义方法 inputBindFoc…

【vue3】基础概念的介绍

⭐【前言】 首先&#xff0c;恭喜你打开了一个系统化的学习专栏&#xff0c;在这个vue专栏中&#xff0c;大家可以根据博主发布文章的时间顺序进行一个学习。博主vue专栏指南在这&#xff1a;vue专栏的学习指南 &#x1f973;博主&#xff1a;初映CY的前说(前端领域) &#x1f…

Vue提升:理解vue中的 slot-scope=“scope“

slot是插槽&#xff0c;slot-scope“scope“语义更加明确&#xff0c;相当于一行的数据&#xff0c;在实际开发中会碰到如下的场景 这个工作状态是变化的&#xff0c;而我们就可以通过后端返回的具体值来判断这里应该显示什么样的内容&#xff0c;具体代码如下 <el-table-co…

利用vue实现登陆界面及其跳转

1.做登录框 步骤&#xff1a; &#xff08;1&#xff09; 创建vue项目&#xff0c;使用vite方式创建&#xff1b;npm init vuelatest &#xff08;2&#xff09;项目结构&#xff1a; src&#xff1a;代码书写位置&#xff1b; app.vue&#xff1a;根组件&#xff1b; main…

vue配置开发环境和生产环境

介绍 本文主要介绍开发、测试以及生产环境的配置。&#xff08;以下内容可根据需求进行配置&#xff09; 步骤 1、在src同级目录也就是根目录下新建文件&#xff1a;.env.development&#xff08;开发环境&#xff09;、.env.test&#xff08;测试环境&#xff09;、.env.pr…

微信小程序授权获取用户信息之wx.getUserInfo 切换到 wx.getUserProfile的使用(已弃用)

目录更新&#xff1a;wx.getUserProfile() 已弃用背景一、小程序获取用户信息相关接口调整说明二、wx.getUserProfile的使用1. 之前的wx.getUserInfo接口的使用2. 现在的wx.getUserProfile接口的使用三、wx.getUserInfo 切换到 wx.getUserProfile前后对比更多问题可参考&#x…

详解v-for中:key属性的作用

举个栗子 不设置key <div id"app"><div><input type"text" v-model"name"><button click"add">添加</button></div><ul><li v-for"(item, i) in list"><input type&qu…

众多mock工具,这一次我选对了

文章目录写在前面Mock介绍Mock能解决什么问题?传统Mock解决方案Postman接口测试工具Mock js第三方库Eolink解决方案全局Mock高级Mock返回结果Mock智能内置Mock智能自定义Mock约束条件MockEolink的Mock解决方案的优势:写在最后写在前面 交战之前&#xff0c;战士必先利其兵器&…

【node拓展】web开发模式 | express应用程序生成器

✅ 作者简介&#xff1a;一名普通本科大三的学生&#xff0c;致力于提高前端开发能力 ✨ 个人主页&#xff1a;前端小白在前进的主页 &#x1f525; 系列专栏 &#xff1a; node.js学习专栏 ⭐️ 个人社区 : 个人交流社区 &#x1f340; 学习格言: ☀️ 打不倒你的会使你更强&a…

JavaScript 30 JavaScript 日期格式

JavaScript 文章目录JavaScript30 JavaScript 日期格式30.1 JavaScript 日期输出30.2 JavaScript ISO 日期30.3 ISO 日期&#xff08;年和月&#xff09;30.4 ISO 日期&#xff08;只有年&#xff09;30.5 ISO 日期&#xff08;完整的日期加时、分和秒&#xff09;30.6 时区30.…

关于hash和history的区别和使用

一、区别 1、 history和hash都是利用浏览器的两种特性实现前端路由&#xff0c;history是利用浏览历史记录栈的API实现&#xff0c;hash是监听location对象hash值变化事件来实现 2、 history的url没有’#号&#xff0c;hash反之 3、 相同的url&#xff0c;history会触发添加…

最好用的 6 款 Vue 拖拽组件库推荐 - 卡拉云

本文首发&#xff1a;《最好用的 6 款 Vue 拖拽组件库推荐 - 卡拉云》 Vue 拖拽组件库&#xff08;drag-and-drop&#xff09;组件在使用 Vue 框架开发中非常常见的需求&#xff0c;做个内容行排序&#xff0c;拖拽小组件到网页上这类都需要用到拖拽组件。本文记录了我自己用过…

vue中使用echarts实现中国地图

第一种效果&#xff1a;不同省份颜色不同 代码&#xff1a; 注意&#xff1a; ①要实现这种效果&#xff0c;地图数据的name一定要是省份的名字&#xff0c;要不然颜色出不来&#xff1b; ②一定要有visualMap配置&#xff0c;并且它的seriesIndex属性 对应的是series的数组下标…

Vue项目打包部署

前几天看[小猪课堂发布的nginx部署](https://zhuanlan.zhihu.com/p/431796992)&#xff0c;跟着做了一遍&#xff0c;由于本人是第一次尝试&#xff0c;遇见了很多问题。经过查阅和搜索&#xff0c;终于解决掉了。下面给大家介绍一下我的流程和遇见的问题&#xff0c;我们可以多…

Vue3:探讨一下mixin

一、Vue2中的mixin 1、定义要混入的数据对象 // 定义一个 mixin 对象 export const myMixin {created() {this.hello()},methods: {hello() {console.log(hello from mixin!)}} } 2、在需要这些东西的地方去通过mixins获得mixin对象 <template><div><h1>…

浅识WebGL和Three.js

WebGL 想必各位看官大大都了解过&#xff0c;进行3D图形渲染&#xff0c;主要依赖显卡&#xff08;GPU&#xff09;为我们提供强大的运算支持。GPU也像不同CPU架构具备不同的指令集一样&#xff0c;不同的显卡厂商也为不同的GPU型号提供了不同的底层指令逻辑&#xff0c;所支持…

Axios发送请求

--- axios是什么: Axios&#xff0c;是一个基于promise的网络请求库&#xff0c;作用于node.js和浏览器中。 一、axios的特点&#xff1a; 1.在浏览器中发送XMLHttpRequest请求。 2.在node.js中可以发送请求 3.支持PromiseAPI 4.拦截请求和响应数据 二、axios的请求方式…