高效前端工程化:Monorepo、pnpm与Vue3集成实战指南


theme: smartblue

引言

在当今快速发展的前端开发领域,高效地管理和组织代码库成为提升开发效率的关键。随着项目规模的扩大,传统的单体仓库逐渐显露出局限性,而新兴的包管理工具如 PNPM、项目结构模式如 MonorepoTurborepo 开始受到广泛关注。将教会大家如何快速搭建 monorepo + pnpm + trborepo +vue3 + element-plus 项目架构。

pnpm:下一代包管理器

pnpm(Package Manager) 是一个快速、节省磁盘空间的 JavaScript 包管理器,它通过引入“链接”和“硬链接”的概念来优化 Node.js 项目的依赖管理。与 npmYarn 相比,pnpm 在安装依赖时,会创建依赖的唯一实例,并通过硬链接或符号链接的方式供各个项目共享,大大减少了磁盘占用和安装时间。此外,pnpm 的精确依赖解析机制能有效避免“dependency hell”,保障项目的稳定性和可复现性。

Monorepo:一统天下的仓库策略

Monorepo(单一仓库)是一种将多个相关项目的源代码存储在一个单一版本控制系统仓库中的策略。这种模式下,无论是微服务架构的后端服务,还是包含多个前端应用的大型项目,都可以共处一室,共享配置、依赖和工具链。Monorepo 的优势在于简化跨项目协作、代码复用、统一版本管理和 CI/CD 流程。然而,随之而来的是对版本控制系统的高效管理需求,以及如何处理大型仓库带来的构建速度问题。

Turborepo:为Monorepo加速

Turborepo 正是针对 Monorepo 模式下构建速度慢的问题提出的一种解决方案。它通过智能缓存、并行执行和增量构建等技术,显著加快了 Monorepo 中项目的构建和测试速度。Turborepo 能够识别出哪些文件或包没有变化,从而跳过不必要的工作,仅重新构建那些受影响的部分。这种优化对于大型组织而言尤为重要,它使得即使仓库包含成百上千个子项目,开发者也能获得接近即时的反馈循环,极大提升了开发效率。

单体仓库与上述方案的对比

相比之下,传统的单体仓库是指一个项目对应一个仓库的模式,适用于小型项目或初创阶段的项目。在单体仓库中,所有源代码、配置文件和依赖都紧密耦合在一起,便于管理但难以扩展。随着项目复杂度增加,代码库的维护成本和团队间的协调成本会迅速上升。

  • 可维护性与扩展性MonorepoTurborepo 由于支持跨项目共享和高效管理,明显优于单体仓库,尤其适合中大型项目和企业级应用。
  • 开发效率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) 两个项目

  • 新建 uiutils 文件夹,并利用 pnpm init 进行初始化。同时在各自的 package.json 文件中 新增属性 "private": true, 其中 name 属性值,可以自定义合适的名称。

ui 项目的名称这里自定义为 @repo/uiutils 项目的名称这里自定义为 @repo/utils

  • ui 项目下自定义新建 components 文件夹,用来存放公共的UI组件,暂时新建两个组件 FormatMoney.vueSlider.vue;然后同层级下新建 index.js 文件来导出组件提供外部访问。
  1. 利用 element-plus UI组件来开发公共UI组件,因此先安装依赖
pnpm i vue element-plus
  1. 编写 FormatMoney.vue, Slider.vue, index.js 文件

FormatMoney.vue


  
    
      
    
    
      
    
    
      格式化
      重置
    
  

<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) return
  formEl.validate((valid) => {
    if (valid) {
      numberValidateForm.amount = formatMoney(numberValidateForm.money, ‘$’);
      console.log(‘submit!’)
    } else {
      console.log(‘error submit!’)
    }
  })
}

const resetForm = (formEl) => {
  if (!formEl) return
  formEl.resetFields()
}

</script setup>

Slider.vue


import "element-plus/dist/index.css";
import { ElSlider } from "element-plus";
import { ref } from 'vue'

const value1 = ref(0)

       默认值        

<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 scoped></script setup>

index.js

import FormatMoney from './components/FormatMoney.vue'
import Slider from './components/Slider.vue'

export {
  FormatMoney,
  Slider
}

  1. 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&nbsp;+&nbsp;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 文件下创建各种子项目,暂且创建 docsweb 两个项目,操作如下:

pnpm create vite 

执行上面的命令,按照提示一步步根据自己需求创建两个项目,最终目录结构如下:

6.回到根目录,在根目录下全局安装 @repo/ui@repo/utils ,这样在任何子应用或者子包都可以相互使用。 如果要安装到根项目中(即全局项目中)那么可以在指令后面加上 -w

pnpm i -w @reop/ui @repo/utils

安装完毕后,可以在 package.json 文件中看到如下信息:

7.在子项目 docsweb 中使用

  • 进入 web 项目,将 App.vue 文件内容修改如下:

import { tips } from "repo-utils";
import { FormatMoney } from "repo-ui";

  

web项目

  

<style scoped>
h1 {
  margin-bottom: 50px;
}

</style scoped></script setup>

  • 进入 docs 项目,将 App.vue 文件内容修改如下:

import { tips } from "repo-utils";
import { FormatMoney, Slider } from "repo-ui";
tips("我是docs项目");

  

docs项目

   h1 {   margin-bottom: 50px; }

</style scoped></script setup>

分别启动项目,运行效果如下:

使用 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 devpnpm 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__/&nbsp;as&nbsp;a&nbsp;separate&nbsp;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__/&nbsp;as&nbsp;a&nbsp;separate&nbsp;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.67s

 Tasks:    2 successful, 2 total
Cached:    0 cached, 2 total
  Time:    25.729s

关于 Turborepo 具体教程,请参考官网文档进行查阅,目前正在翻译官网文档,后续会开放浏览地址供阅览。

写在最后

至此一步步完成了利用 pnpm, monorepo, turborepo 等技术搭建多项目多包统一仓库管理的架构,解决了项目规模扩大后的管理难题,提高了开发效率和团队协作水平。选择合适的工具和策略,对提升项目成功率至关重要。

源码放到github仓库上,点击进行查看


这是一个从 https://juejin.cn/post/7368820207593406473 下的原始话题分离的讨论话题