引言
大家好啊,我是前端拿破轮。😁
在上一篇文章《从0到1搭一个monorepo项目(一)》中,我们谈到了,如果采用传统的multirepo
方式来开发一个项目,会遇到诸多问题。并且找到了解决这些问题的方法,就是monorepo
。那么这篇文章,我们就来具体从0到1搭建一个monorepo
的项目。
搭建过程
准备工作
安装
node
等前端的基本工程化环境不在本系列文章的讨论范围之内,大家可自行搜索相关文章。
首先需要我们安装pnpm,可以使用npm
或者brew
安装,windows
用户可以使用scoop
安装。
bash
# 安装pnpm
scoop install pnpm
然后我们创建我们的工作文件夹,这里我取名叫做study-island
,即学习岛,是一个无人自习室管理系统项目。
bash
# 创建文件夹
mkdir study-island
# 进入文件夹
cd study-island
# 在vscode中打开此文件夹
code .
初始化
然后我们先利用pnpm
进行项目初始化。
bash
pnpm init
我们可以发现在项目的根目录自动生成了一个package.json
文件,并写入了一些基本的配置内容。

接下来我们就要就要进行monorepo
管理了,市面上有非常多的工具可以进行选择,这里我们就使用pnpm
本身提供的轻量级的monorepo
管理能力。
首先创建一个pnpm-workspace.yaml
文件,用来配置pnpm
的工作区。
bash
touch pnpm-workspace.yaml
我们在其中写入以下内容:
yaml
packages:
# 匹配apps目录下的直接子目录
- 'apps/*'
# 匹配packages目录下的直接子目录
- 'packages/*'
这个配置文件就是告诉pnpm
,在apps
和packages
目录下的直接子文件夹都是属于子包 ,方便pnpm
进行monorepo
的管理。
有了这个配置文件后,pnpm
就能够识别我们的项目中,哪些目录是属于子包的。
然后我们就需要创建这些子包的目录。
bash
mkdir apps packages
利用脚手架搭建业务子包
其中apps
存放的是我们的业务代码,packages
存放的是我们的公共模块代码,比如utils
公用工具库,还有UI
组件库等。
接着我们进入apps
目录,并创建我们的业务子包。这里我们使用vite
来进行创建。
pc端项目
PC端我们就创建最普通的react
项目即可。
bash
# 进入apps目录
cd apps
# 利用vite脚手架创建项目
pnpm create vite@latest

这里我们利用vite
提供的交互式命令行进行项目创建,项目名我们就直接输入pc
。在选择变体的时候选择TypeScript + SWC
,因为我们后面要使用的UI
组件库是基于React 18
的,所以变体不能选择TypeScript + React Compiler
,因为这是React 19
才引入的优化机制。
实验性质的rolldown-vite
我们出于稳定性考虑也先不选用。
然后不要现在安装和运行,因为在monorepo
项目中,我们一般不会进入子包目录运行命令,而是直接在根目录运行。
mobile端项目
mobile端项目我们使用React Native
来开发。
方便起见,我们使用对RN
进行封装的Expo工具链来获得开箱即用的体验。
bash
pnpm create create-expo-app

关于下载Android Studio
和配置模拟器的相关步骤不在本文讨论范围,大家可以自行搜索,本文还是主要聚焦在如何搭建一个monorepo
的项目。
后端项目
后端我们采用Nest.js框架来搭建成熟的服务器。
首先我们需要全局安装@nestjs/cli
工具包。
bash
# 全局安装cli工具
pnpm add -g @nestjs/cli
# 创建一个新项目,命名为backend
nest new backend
到目前为止,我们的三个模块已经全部创建完毕,整个项目目录如下图所示:

调整git仓库
由于我们在创建pc
项目时,使用了脚手架vite
,创建mobile
项目时,使用了脚手架expo
,创建backend
项目时,使用了脚手架@nestjs/cli
,这些脚手架工具有一些会在创建项目的时候自动初始化git
仓库,这显然不是我们想要的。我们已经采用了monorepo
整个项目只有根目录有一个git
仓库即可,子包中不能有git
仓库。所以我们需要将子包中的git
仓库删除,并将.gitignore
文件统一整合到根目录下。
整合后如下图所示:

开发依赖统一安装到根目录
在package.json
中主要有两种依赖类型dependencies
和devDependencies
,前者是运行时依赖,后者是开发依赖。也就是说,后者仅仅在开发阶段用到,运行时是不需要的,所以在monorepo
项目中,我们可以把子包的所有开发依赖都统一配置到根目录的package.json
中,方便统一管理,也能避免版本冲突。
删除子包中的devDependencies
字段,将内容全部整合到根目录的package.json
中,重复的依赖我们只保留一个版本即可。
锁定node和pnpm版本
在package.json
中配置engines
字段如下:
json
"engines": {
"node": "22.20.0",
"pnpm": "10.17.0"
}
这样当我们使用不匹配的node
版本进行安装时,会有如下警告:

如果我们想要更严格的控制,当版本不符合时直接报错,我们可以在项目的根目录下创建.npmrc
文件,并将engine-strict
设置为true
。
.npmrc
engine-strict=true
这样再在根目录运行pnpm i
命令时就会报错如下图:

配置git提交规范
这里我们需要安装如下几个工具:
bash
pnpm add -Dw @commitlint/cli @commitlint/config-conventional commitizen cz-git
这里的
-Dw
表示-D
即安装开发依赖和--workspace-root
,即告诉pnpm
我确定就是要在项目的根目录进行安装。假设没有
-w
命令,我们直接安装,会有如下提示:意思是说,运行这个命令会在工作空间的根目录添加依赖,
pnpm
感觉这个不一定时我们的本意,如果我们确实想要在根目录添加依赖就加上-w
或者--workspace-root
表示确认。
- @commitlint/cli是
commitlint
工具的核心。 - @commitlint/config-conventional是基于
conventional commits
规范的配置文件,用来约束git
提交的格式。 - commitizen提供了交互式 攥写
commit
信息的插件。 - cz-git是国人开发的工程性更强,更加高度自定义的
commitizen
适配器和cli
,相当于commitizen
的UI和逻辑增强版。
commitizen
库提供了一个git-cz
的命令,可以进行交互式生成git
的提交信息。
他们的关系大致如下图:
sql
开发者输入命令:
↓
npx cz / git cz
↓
【commitizen】提供交互界面
↓
使用适配器(adapter)
├─ cz-conventional-changelog (官方)
└─ cz-git (社区增强版)
↓
生成 commit message
↓
【commitlint】校验是否符合规范
↓
commit 成功 or 拒绝提交
所以我们在根目录的package.json
中配置脚本和适配器:
json
// package.json
...,
"scripts": {
"commit": "git-cz"
}
...,
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
创建commitlint
的配置文件:
bash
touch commitlint.config.js
写入如下配置:
js
/** @type {import('cz-git').UserConfig} */
export default {
// 继承 commitlint 的常见规范(Angular 风格)
extends: ["@commitlint/config-conventional"],
// commitlint 规则(影响 commit message 校验)
rules: {
// 提交主体前必须有空行(body 与 header 之间),严重级别 2(错误)
"body-leading-blank": [2, "always"],
// footer 前是否需要空行
"footer-leading-blank": [1, "always"],
// header 最大长度
"header-max-length": [2, "always", 108],
// 不允许空的 subject
"subject-empty": [2, "never"],
// 不允许空的 type
"type-empty": [2, "never"],
// 对 subject 的大小写不做强制
"subject-case": [0],
// type 的可选值
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release",
],
],
},
// 自定义错误提示信息(中文)
plugins: [
{
rules: {
"subject-empty": ({ subject }) => {
const isValid = subject && subject.trim().length > 0;
return [
isValid,
"❌ 提交描述不能为空!\n💡 请在冒号后添加描述,例如: feat: 添加用户登录功能",
];
},
"type-empty": ({ type }) => {
const isValid = type && type.trim().length > 0;
return [
isValid,
"❌ 提交类型不能为空!\n💡 格式: <类型>: <描述>\n📚 可用类型: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, wip, workflow, types, release",
];
},
"type-enum": ({ type }) => {
const validTypes = [
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release",
];
const isValid = validTypes.includes(type);
return [
isValid,
`❌ 提交类型 "${type}" 不在允许的范围内!
💡 请使用以下类型之一:
✨ feat - 新功能
🐛 fix - 修复bug
📚 docs - 文档变更
🎨 style - 代码格式
📦 refactor - 代码重构
🚀 perf - 性能优化
🧪 test - 测试相关
🏗️ build - 构建/依赖变更
👷 ci - 持续集成
🔧 chore - 其他修改
⏪ revert - 回滚提交
🚧 wip - 进行中的工作
🔁 workflow - 工作流相关
🔤 types - 类型定义
🏷️ release - 版本发布`,
];
},
"header-max-length": ({ header }) => {
const maxLength = 108;
const isValid = header.length <= maxLength;
return [
isValid,
`❌ 提交信息头部太长了!
📏 当前长度: ${header.length} 字符
📏 最大长度: ${maxLength} 字符
💡 请精简描述,保持简洁明了`,
];
},
"body-leading-blank": ({ body }) => {
if (!body) return [true];
const isValid = body.startsWith("\n");
return [
isValid,
"❌ 提交正文前需要有空行!\n💡 在标题和正文之间添加一个空行",
];
},
"footer-leading-blank": ({ footer }) => {
if (!footer) return [true];
const isValid = footer.startsWith("\n");
return [
isValid,
"⚠️ 页脚前建议有空行!\n💡 在正文和页脚之间添加一个空行",
];
},
"subject-case": ({ subject }) => {
// 这个规则已关闭,但保留提示以供参考
return [true];
},
},
},
],
// 自定义帮助信息 URL 或提示文本
helpUrl: `请使用 pnpm commit 命令进行交互式提交,不要直接使用 git commit 命令`,
// cz-git 的 prompt 配置(交互式提交时的提示)
prompt: {
// types 与上面的 type-enum 必须一致
types: [
{
value: "feat",
name: "✨ feat: 新功能(feature)",
emoji: "✨",
},
{
value: "fix",
name: "🐛 fix: 修复 bug",
emoji: "🐛",
},
{
value: "docs",
name: "📚 docs: 文档变更",
emoji: "📚",
},
{
value: "style",
name: "🎨 style: 代码格式(不影响逻辑)",
emoji: "🎨",
},
{
value: "refactor",
name: "📦 refactor: 代码重构(无新增/修复)",
emoji: "📦",
},
{
value: "perf",
name: "🚀 perf: 性能优化",
emoji: "🚀",
},
{
value: "test",
name: "🧪 test: 测试相关",
emoji: "🧪",
},
{
value: "build",
name: "🏗️ build: 构建系统/外部依赖变更",
emoji: "🏗️",
},
{
value: "ci",
name: "👷 ci: 持续集成/流程相关",
emoji: "👷",
},
{
value: "chore",
name: "🔧 chore: 构建过程或辅助工具变更",
emoji: "🔧",
},
{
value: "revert",
name: "⏪ revert: 回滚到以前的提交",
emoji: "⏪",
},
{
value: "wip",
name: "🚧 wip: 正在进行的工作(未完成)",
emoji: "🚧",
},
{
value: "workflow",
name: "🔁 workflow: 工作流(如 GitHub Actions)",
emoji: "🔁",
},
{
value: "types",
name: "🔤 types: 类型定义文件/TS 类型相关",
emoji: "🔤",
},
{
value: "release",
name: "🏷️ release: 发布/版本相关",
emoji: "🏷️",
},
],
// 影响范围,常见子项目/包名
scopes: [
{ value: "root", name: "root: 项目根目录" },
{ value: "backend", name: "backend: 后端相关" },
{ value: "mobile", name: "mobile: 移动端相关" },
{ value: "pc", name: "pc: PC端相关" },
],
allowCustomScopes: true,
allowEmptyScopes: true,
// 跳过不必要的问题
skipQuestions: ["body", "breaking", "footerPrefix", "footer"],
// 提示文本(中文提示更友好)
messages: {
type: "📌 选择你要提交的类型:",
scope: "🎯 选择一个影响范围 (可选,直接回车跳过):",
customScope: "🎯 请输入自定义的影响范围:",
subject: "📝 填写简短精炼的变更描述:\n (例如: 添加用户登录功能)\n",
body: '🔍 填写更详细的变更描述 (可选)。使用 "|" 换行:\n',
breaking: "💥 列举非兼容性重大的变更 (可选):\n",
footerPrefixesSelect: "🔗 选择关联issue前缀 (可选):",
customFooterPrefix: "🔗 输入自定义issue前缀:",
footer: "🔗 列举关联的 ISSUE (可选)。例如: #31, #34:\n",
generatingByAI: "🤖 正在通过 AI 生成你的提交简短描述...",
generatedSelectByAI: "💡 选择一个 AI 生成的简短描述:",
confirmCommit: "✅ 是否确认以上commit信息提交?",
},
// 允许在 feat 和 fix 类型中添加 breaking changes
allowBreakingChanges: ["feat", "fix"],
// subject 长度限制
subjectLimit: 100,
// 默认 scope 枚举分隔符
scopeEnumSeparator: ",",
// 自定义选择框宽度
maxHeaderLength: 108,
maxSubjectLength: 100,
minSubjectLength: 3,
// 默认 body 和 footer 的最大长度
defaultBody: "",
defaultFooter: "",
// 是否使用 emoji(建议开启,让提交历史更直观)
useEmoji: true,
emojiAlign: "center",
// 主题前缀(可选,如果不想要可以设为空)
themeColorCode: "",
// 如果有 AI 生成功能可以配置(需要额外插件支持)
// aiNumber: 3,
// aiQuestionCB: ({ answers, aiQuestions }) => { }
},
};
然后运行pnpm commit
即可执行git-cz
脚本。

然后我们就可以使用交互式命令行来提交啦。

但是这里还有一个问题,我们这是使用了pnpm commit
来运行git-cz
脚本进行交互式提交,如果有新人接手了我们的项目,他并不知道这个脚本,直接使用git commit
,就会绕过我们的交互式工具,就可以任意上传commit
信息了,还是无法实现规范效果。

这里我们可能会想,要是有一个工具可以在我每次git
提交前都检查一下我的提交信息是否符合规范,不符合就无法成功提交就好了。
这就是我们接下来要说的工具husky
。
首先我们需要安装husky
bash
# 安装husky
pnpm -Dw add husky
# 初始化
pnpm husky init
然后我们会发现项目目录下多了一个文件夹.huksy

bash
# 配置husky的commit-msg钩子
# Add commit message linting to commit-msg hook mac和linux用户用这个命令
echo "pnpm dlx commitlint --edit \$1" > .husky/commit-msg
# Windows users should use ` to escape dollar signs windows用户用这个命令
echo "pnpm dlx commitlint --edit `$1" > .husky/commit-msg
我们这个时候如果再想绕过监管就难以做到了。

总结
本文我们从0-1搭建了一个monorepo
的项目,主要进行了项目初始化,并利用vite
搭建了pc
端项目,利用expo
搭建了移动端的RN
项目,利用nest.js
搭建了服务端项目。同时配置了git
提交的强制规范。后续内容会在本专栏持续更新,欢迎大家订阅关注:Monorepo项目搭建。
好了,这篇文章就到这里啦,如果对您有所帮助,欢迎点赞,收藏,分享👍👍👍。您的认可是我更新的最大动力。由于笔者水平有限,难免有疏漏不足之处,欢迎各位大佬评论区指正。
往期推荐✨✨✨
我是前端拿破轮,关注我,一起学习前端知识,下期见!