引言
在当今快速发展的前端开发领域,高效地管理和组织代码库成为提升开发效率的关键。随着项目规模的扩大,传统的单体仓库逐渐显露出局限性,而新兴的包管理工具如 PNPM
、项目结构模式如 Monorepo
和 Turborepo
开始受到广泛关注。将教会大家如何快速搭建 monorepo + pnpm + trborepo +vue3 + element-plus
项目架构。
pnpm:下一代包管理器
pnpm(Package Manager)
是一个快速、节省磁盘空间的 JavaScript
包管理器,它通过引入“链接”和“硬链接”的概念来优化 Node.js
项目的依赖管理。与 npm
和 Yarn
相比,pnpm
在安装依赖时,会创建依赖的唯一实例,并通过硬链接或符号链接的方式供各个项目共享,大大减少了磁盘占用和安装时间。此外,pnpm
的精确依赖解析机制能有效避免“dependency hell”,保障项目的稳定性和可复现性。
Monorepo:一统天下的仓库策略
Monorepo
(单一仓库)是一种将多个相关项目的源代码存储在一个单一版本控制系统仓库中的策略。这种模式下,无论是微服务架构的后端服务,还是包含多个前端应用的大型项目,都可以共处一室,共享配置、依赖和工具链。Monorepo
的优势在于简化跨项目协作、代码复用、统一版本管理和 CI/CD
流程。然而,随之而来的是对版本控制系统的高效管理需求,以及如何处理大型仓库带来的构建速度问题。
Turborepo:为Monorepo加速
Turborepo
正是针对 Monorepo
模式下构建速度慢的问题提出的一种解决方案。它通过智能缓存、并行执行和增量构建等技术,显著加快了 Monorepo
中项目的构建和测试速度。Turborepo
能够识别出哪些文件或包没有变化,从而跳过不必要的工作,仅重新构建那些受影响的部分。这种优化对于大型组织而言尤为重要,它使得即使仓库包含成百上千个子项目,开发者也能获得接近即时的反馈循环,极大提升了开发效率。
单体仓库与上述方案的对比
相比之下,传统的单体仓库是指一个项目对应一个仓库的模式,适用于小型项目或初创阶段的项目。在单体仓库中,所有源代码、配置文件和依赖都紧密耦合在一起,便于管理但难以扩展。随着项目复杂度增加,代码库的维护成本和团队间的协调成本会迅速上升。
-
可维护性与扩展性:
Monorepo
和Turborepo
由于支持跨项目共享和高效管理,明显优于单体仓库,尤其适合中大型项目和企业级应用。 -
开发效率:
pnpm
通过优化依赖管理提升安装速度;Turborepo
则通过智能构建机制,解决了Monorepo
的构建效率问题,两者共同推动了开发效率的飞跃。 -
协作与代码复用:
Monorepo
鼓励跨项目代码共享,而Turborepo
在此基础上进一步优化了协作体验,单体仓库在这方面则显得较为局限。
架构搭建
1.创建项目
新建文件夹自定义命名,暂且为 monorepo-demo
,然后用VSCode编辑器打开,新建终端,操作如下:
2.利用 pnpm init
在根目录初始化
PS G:\wokespace\FullStackProjects\pnpm-monorepo-demo> pnpm init
Wrote to G:\wokespace\FullStackProjects\pnpm-monorepo-demo\package.json{"name": "pnpm-monorepo-demo","version": "1.0.0","description": "","main": "index.js","scripts": {"test": "echo \"Error: no test specified\" && exit 1"},"keywords": [],"author": "","license": "ISC"
}
PS G:\wokespace\FullStackProjects\pnpm-monorepo-demo>
打开 package.json
文件,"private": true
加这个为防止我们意外地将私有项目发布。
3. 根据pnpm
中的文档在根目录创建 pnpm-workspace.yaml
文件
根据自己项目需求创建合适的目录结构,示例如下:
4.在 packages
文件下创建存放 公共的UI组件 (ui) 和公共的工具函数 (utils) 两个项目
- 新建
ui
和utils
文件夹,并利用pnpm init
进行初始化。同时在各自的package.json
文件中 新增属性"private": true
, 其中name
属性值,可以自定义合适的名称。
ui
项目的名称这里自定义为 @repo/ui
, utils
项目的名称这里自定义为 @repo/utils
。
- 在
ui
项目下自定义新建components
文件夹,用来存放公共的UI组件,暂时新建两个组件FormatMoney.vue
和Slider.vue
;然后同层级下新建index.js
文件来导出组件提供外部访问。
- 利用
element-plus
UI组件来开发公共UI组件,因此先安装依赖
pnpm i vue element-plus
- 编写
FormatMoney.vue
,Slider.vue
,index.js
文件
FormatMoney.vue
<template><el-formref="formRef"style="max-width: 600px":model="numberValidateForm"label-width="auto"class="demo-ruleForm"><el-form-itemlabel="金额"prop="money":rules="[{ required: true, message: '金额不能为空' },{ type: 'number', message: '金额是数字类型' },]"><el-inputv-model.number="numberValidateForm.money"type="text"autocomplete="off"/></el-form-item><el-form-itemlabel="格式化后的金额"prop="amount" ><el-inputv-model="numberValidateForm.amount"type="text"autocomplete="off"readonly/></el-form-item><el-form-item><el-button type="primary" @click="submitForm(formRef)">格式化</el-button><el-button @click="resetForm(formRef)">重置</el-button></el-form-item></el-form>
</template><script setup>
import "element-plus/dist/index.css";
import { ElForm, ElFormItem, ElInput, ElButton } from "element-plus";
import { reactive, ref } from 'vue'
import { formatMoney } from "repo-utils";console.log('formatMoney', formatMoney(2342113241, '$'));const formRef = ref()const numberValidateForm = reactive({money: '',amount: ''
})const submitForm = (formEl) => {if (!formEl) returnformEl.validate((valid) => {if (valid) {numberValidateForm.amount = formatMoney(numberValidateForm.money, '$');console.log('submit!')} else {console.log('error submit!')}})
}const resetForm = (formEl) => {if (!formEl) returnformEl.resetFields()
}
</script>
Slider.vue
<script setup>
import "element-plus/dist/index.css";
import { ElSlider } from "element-plus";
import { ref } from 'vue'const value1 = ref(0)
</script><template><div class="slider-demo-block"><span class="demonstration">默认值</span><el-slider v-model="value1" /></div>
</template><style scoped>
.slider-demo-block {max-width: 600px;display: flex;align-items: center;
}
.slider-demo-block .el-slider {margin-top: 0;margin-left: 12px;
}
.slider-demo-block .demonstration {font-size: 14px;color: black;width: 120px;padding: 10px;
}
</style>
index.js
import FormatMoney from './components/FormatMoney.vue'
import Slider from './components/Slider.vue'export {FormatMoney,Slider
}
- UI 项目最后的目录结构如下:
- 在
utils
项目下新建两个文件fun.js (存放各种工具方法)
和index.js (提供对外访问的方法)
fun.js
// 弹窗提示
export const tips = (message, title = "提示") => {window.alert(`${title}: ${message}`)
}// 加运算
export const addOperation = (a, b) => {window.alert(`1加2的结果是${a + b}`);
}// 格式化金额
export const formatMoney = (money, symbol = "", decimals = 2) => {return (Math.round((parseFloat(money) + Number.EPSILON) * Math.pow(10, decimals)) / Math.pow(10, decimals)).toFixed(decimals).replace(/\B(?=(\d{3})+\b)/g, ",").replace(/^/, `${symbol}`)
};
index.js
export * from "./fun.js";
5. 利用 vite
工具在 apps
文件下创建各种子项目,暂且创建 docs
和 web
两个项目,操作如下:
pnpm create vite
执行上面的命令,按照提示一步步根据自己需求创建两个项目,最终目录结构如下:
6.回到根目录,在根目录下全局安装 @repo/ui
和 @repo/utils
,这样在任何子应用或者子包都可以相互使用。 如果要安装到根项目中(即全局项目中)那么可以在指令后面加上 -w
。
pnpm i -w @reop/ui @repo/utils
安装完毕后,可以在 package.json
文件中看到如下信息:
7.在子项目 docs
和 web
中使用
- 进入
web
项目,将App.vue
文件内容修改如下:
<script setup>
import { tips } from "repo-utils";
import { FormatMoney } from "repo-ui";
</script><template><h1>web项目</h1><FormatMoney></FormatMoney>
</template><style scoped>
h1 {margin-bottom: 50px;
}
</style>
- 进入
docs
项目,将App.vue
文件内容修改如下:
<script setup>
import { tips } from "repo-utils";
import { FormatMoney, Slider } from "repo-ui";
tips("我是docs项目");
</script><template><h1>docs项目</h1><Slider></Slider>
</template>
<style scoped>
h1 {margin-bottom: 50px;
}
</style>
分别启动项目,运行效果如下:
使用 Turborepo
构建打包
1.全局安装 Turborepo
pnpm i -g turbo# 检测是否安装成功
λ turbo --version
1.13.3
2.根目录新建文件 turbo.json
, 默认内容如下:
{"$schema": "https://turbo.build/schema.json","globalDependencies": ["**/.env.*local"],"pipeline": {"build": {"dependsOn": ["^build"],"outputs": [".next/**", "!.next/cache/**", "dist/**"]},"lint": {"dependsOn": ["^lint"]},"dev": {"cache": false,"persistent": true}}
}
3.在根目录下修改 package.json
文件,添加执行命令脚本, 具体如下:
4. 进入各个子项目或者子包中,修改 .gitignore
文件,增加如下内容:
.turbo
build/
dist/
.next/
5.根目录执行 pnpm dev
或 pnpm build
,会对子项目全量启动或打包,具体如下:
- 全量启动项目
λ pnpm dev> monorepo-demo@1.0.0 dev G:\wokespace\FullStackProjects\pnpm-monorepo-demo
> turbo dev• Packages in scope: docs, repo-ui, repo-utils, web
• Running dev in 4 packages
• Remote caching disabled
docs:dev: cache bypass, force executing 0906b5c91c3b269b
web:dev: cache bypass, force executing b446edc8270ef0f2
docs:dev:
docs:dev: > docs@0.0.0 dev G:\wokespace\FullStackProjects\monorepo-demo\apps\docs
docs:dev: > vite
docs:dev:
web:dev:
web:dev: > web@0.0.0 dev G:\wokespace\FullStackProjects\monorepo-demo\apps\web
web:dev: > vite
web:dev:
docs:dev: Port 5173 is in use, trying another one...
web:dev:
web:dev: VITE v5.2.8 ready in 20313 ms
web:dev:
web:dev: ➜ Local: http://localhost:5173/
web:dev: ➜ Network: use --host to expose
web:dev: ➜ Vue DevTools: Open http://localhost:5173/__devtools__/ as a separate window
web:dev: ➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
web:dev:
docs:dev:
docs:dev: VITE v5.2.8 ready in 20368 ms
docs:dev:
docs:dev: ➜ Local: http://localhost:5174/
docs:dev: ➜ Network: use --host to expose
docs:dev: ➜ Vue DevTools: Open http://localhost:5174/__devtools__/ as a separate window
docs:dev: ➜ Vue DevTools: Press Alt(⌥)+Shift(⇧)+D in App to toggle the Vue DevTools
docs:dev:
- 全量打包子项目
λ pnpm build> monorepo-demo@1.0.0 build G:\wokespace\FullStackProjects\monorepo-demo
> turbo build• Packages in scope: docs, repo-ui, repo-utils, web
• Running build in 4 packages
• Remote caching disabled
web:build: cache miss, executing 5987b2c98bceeb10
docs:build: cache miss, executing aff8ae65ab7f527e
web:build:
docs:build:
web:build: > web@0.0.0 build G:\wokespace\FullStackProjects\monorepo-demo\apps\web
web:build: > vite build
web:build:
docs:build: > docs@0.0.0 build G:\wokespace\FullStackProjects\monorepo-demo\apps\docs
docs:build: > vite build
docs:build:
web:build: vite v5.2.8 building for production...
docs:build: vite v5.2.8 building for production...
web:build: transforming...
docs:build: transforming...
web:build: ✓ 1435 modules transformed.
docs:build: ✓ 1435 modules transformed.
docs:build: rendering chunks...
web:build: rendering chunks...
docs:build: computing gzip size...
web:build: computing gzip size...
docs:build: dist/index.html 0.43 kB │ gzip: 0.29 kB
web:build: dist/index.html 0.43 kB │ gzip: 0.29 kB
docs:build: dist/assets/AboutView-C6Dx7pxG.css 0.09 kB │ gzip: 0.10 kB
web:build: dist/assets/AboutView-C6Dx7pxG.css 0.09 kB │ gzip: 0.10 kB
web:build: dist/assets/index-B_6bWB-a.css 328.68 kB │ gzip: 45.17 kB
web:build: dist/assets/AboutView-9L8e1QJt.js 0.23 kB │ gzip: 0.20 kB
docs:build: dist/assets/index-DM4ReSuC.css 328.68 kB │ gzip: 45.17 kB
web:build: dist/assets/index-BUYK55Bj.js 175.04 kB │ gzip: 65.02 kB
docs:build: dist/assets/AboutView-CgU-TZmY.js 0.23 kB │ gzip: 0.20 kB
docs:build: dist/assets/index-wHPYhhNI.js 195.12 kB │ gzip: 72.35 kB
web:build: ✓ built in 14.67s
docs:build: ✓ built in 14.67sTasks: 2 successful, 2 total
Cached: 0 cached, 2 totalTime: 25.729s
关于 Turborepo
具体教程,请参考官网文档进行查阅,目前正在翻译官网文档,后续会开放浏览地址供阅览。
写在最后
至此一步步完成了利用 pnpm
, monorepo
, turborepo
等技术搭建多项目多包统一仓库管理的架构,解决了项目规模扩大后的管理难题,提高了开发效率和团队协作水平。选择合适的工具和策略,对提升项目成功率至关重要。