搭建 monorepo 项目

为什么使用monorepo?

聊到为什么使用 monorepo 那就要先了解不使用 monorepo 的痛点和使用 monorepo 的优点

痛点1:公共代码重复 or 发布频繁

方案A: 复制粘贴,把 A 项目代码粘贴到 B 项目等等,只要有一处改动其它的地方也要进行改动

方案B: 拆成独立 NPM 包

  • 每次修改都要 改代码->打包->发版->各项目更新依赖
  • 调试困难:本地改了 utils,要发 beta 版才能在 web-app 里试

痛点2:跨项目原子提交难以实现

你想同时:

  • 在 utils 中 新增一个 formatDate 函数
  • 在 web-app 和 mobile-app 中调用它

在 Multi-repo 下:

  • 必须分 3 次提交 + 3 次 PR
  • 如果中间某步失败,状态不一致

痛点3:工具链碎片化

  • 每个项目有自己的 ESLint / Prettier / TypeScript 配置
  • 团队规范难以统一,新人上手成本高

优点1:天然支持跨项目引用(无需发布)

通过 pnpm workspace

less 复制代码
// apps/web-app/package.json
{
  "dependencies": {
    "@my-org/utils": "workspace:*"
  }
}

**workspace: *** 代表直接链接到本地 packages/utils 目录

修改 utils 中的内容,web-app 项目中立即生效(无需发版)

调试时与在同一个项目中一样

优点2:原子提交:一次PR,多项目协同变更

一次提交,完整闭环。

Code Review 时能看到全貌,避免"改了 utils 但没人用"的问题。

优点3:统一工程化配置

根目录下可以配置统一的 tsconfig.ts、eslintrc、prettierrc

所有的子项目可以继承,强制统一规范

升级 TypeScript ,改一个地方,全部 repo 生效

优点4:高效构建与缓存(需配置 Turborepo / Nx)

第一次构建:按顺序 utils -> web-app -> mobile-app

第二次修改:只修改 web-app 中的内容,只构建web-app ,无需构建 utils ,直接命中缓存的 utils

CI时间大幅度缩减

所以:当协作成本 > 仓库管理成本时,Monorepo 就赢了。

典型的 monorepo 项目结构

perl 复制代码
my-monorepo/
├── apps/
│   ├── web-app/          # 前端应用
│   └── mobile-app/       # 移动端
├── packages/
│   ├── shared-utils/     # 工具函数
│   ├── ui-components/    # 公共组件库(React/Vue)
│   └── api-types/        # API 类型定义(TS interface)
├── package.json          # 定义 workspaces
└── pnpm-workspace.yaml   # pnpm 工作区声明

根目录:pnpm-workspace.yaml

markdown 复制代码
packages:
    - apps/*
    - packages/*

根目录:package.json

css 复制代码
{
  workspace: ["apps/*", "packages/*"]
}

pnpm 会自动将 packages链接到 node_modules中,实现本地 "npm包"

注意: 在开发过程中,如果软链失效,可以通过 pnpm i --force 强制从新安装,链接软链

现在就可以搭建一个 monorepo 的项目了

第一步:创建一个目录

使用下方命令进行项目初始化

csharp 复制代码
pnpm init

第二步:创建项目目录和公共方法目录

在根目录创建一个 apps 的目录:这个目录下,放的是你的应用

在根目录创建一个 packages 的目录,这个目录下,放的是你的公共方法

增加 pnpm-workspace.yaml 管理项目

markdown 复制代码
packages:
    - apps/*
    - packages/*

所以现在的目录结构会变为

go 复制代码
my-monorepo/
├── apps/
│   ├── web-app/          
│   └── mobile-app/       
├── packages/
│   ├── shared-utils/    
│   ├── ui-components/    
│   └── api-types/        
├── package.json         
└── pnpm-workspace.yaml   

第三步:给子项目的package.json中引入公共依赖

子项目 web-app 和 moblie-app 的package.json增加下面内容

scss 复制代码
{
  "devDependencies": {
    "@shared-utils": "workspace:*",
    "@ui-components": "workspace:*",
    "@api-types": "workspace:*"
  }
}

这样一个最基本的 monorepo 结构的项目就搭建好了。

如果子项目使用的是 vite ,需要修改一下配置

增加 alias , 在开发环境下,绕过构建产物,直接引用源码( src ),实现"热更新 + 实时调试"公共包。

javascript 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { resolve } from 'path';

// https://vite.dev/config/
export default defineConfig(({ mode }) => {
  const isDev = mode === 'development';

  return {
    plugins: [react()],
    resolve: {
      alias: isDev ? {
        '@ui-components': resolve(import.meta.dirname, '../../packages/ui-components/src'), // 开发环境使用软连接
      } : undefined,
    },
  }
})

为刚搭建好的 monorepo 项目增加工程化配置

现在我们的项目已经搭建好了,大致的目录结构为

第一步:先增加 TS 配置

根目录创建 tsconfig.json

内容根据自己项目的公共 ts 配置进行配置

json 复制代码
{
    "compilerOptions": {
      "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
      "target": "ES2022",
      "useDefineForClassFields": true,
      "lib": ["ES2022", "DOM", "DOM.Iterable"],
      "module": "ESNext",
      "types": ["vite/client"],
      "skipLibCheck": true,
  
      /* Bundler mode */
      "moduleResolution": "bundler",
      "allowImportingTsExtensions": true,
      "verbatimModuleSyntax": true,
      "moduleDetection": "force",
      "noEmit": true,
      "jsx": "react-jsx",
  
      /* Linting */
      "strict": true,
      "noUnusedLocals": true,
      "noUnusedParameters": true,
      "erasableSyntaxOnly": true,
      "noFallthroughCasesInSwitch": true,
      "noUncheckedSideEffectImports": true
    }
  }
  

子项目 web-app、mobile-app 的 tsconifg.json

json 复制代码
{
  "extends": "../../tsconfig.json",
  "files": [],
  "include": ["src", "vite.config.ts"]
}

安装 typescript

csharp 复制代码
pnpm add -D -w typescript

根目录 package.json

json 复制代码
{
  "scripts": {
    "check-types": "tsc -b"
  },
}

通过命令 pnpm check-types 就可以检查整个 monorepo 项目的代码

第二步:增加 eslint 配置

安装 eslint 的依赖

sql 复制代码
pnpm add -D -w @eslint/js 
  @typescript-eslint/eslint-plugin
  @typescript-eslint/parser
  eslint
  eslint-config-prettier
  eslint-plugin-react-hooks
  eslint-plugin-react-refresh

根目录创建 eslint.config.cjs

php 复制代码
// eslint.config.cjs
const eslintJs = require('@eslint/js');
const parser = require('@typescript-eslint/parser');
const plugin = require('@typescript-eslint/eslint-plugin');
const reactHooks = require('eslint-plugin-react-hooks');
const reactRefresh = require('eslint-plugin-react-refresh');
const eslintConfigPrettier = require('eslint-config-prettier');
const globals = require('globals');

// 手动构造 @typescript-eslint/recommended 的等效配置
const tsRecommended = {
  files: ['**/*.ts', '**/*.tsx'],
  languageOptions: {
    parser,
    parserOptions: {
      ecmaVersion: 2022,
      sourceType: 'module'
    }
  },
  plugins: {
    '@typescript-eslint': plugin
  },
  rules: {
    // 来自 @typescript-eslint/eslint-plugin 的 recommended 规则
    '@typescript-eslint/no-unused-vars': 'error',
    '@typescript-eslint/no-explicit-any': 'warn',
    '@typescript-eslint/no-var-requires': 'error',
    '@typescript-eslint/no-empty-function': 'warn',
    // 可根据需要扩展更多...
  }
};

/** @type {import('eslint').Linter.FlatConfig[]} */
module.exports = [
  {
    ignores: ['**/node_modules/**', '**/dist/**', '**/build/**', '**/.next/**', '**/*.d.ts']
  },

  // 基础 JS 规则
  eslintJs.configs.recommended,

  // TypeScript 规则(手动定义)
  tsRecommended,

  // React 规则
  {
    files: ['**/*.{js,jsx,ts,tsx}'],
    languageOptions: {
      globals: {
        ...globals.browser
      }
    },
    plugins: {
      'react-hooks': reactHooks,
      'react-refresh': reactRefresh
    },
    rules: {
      'react-hooks/rules-of-hooks': 'error',
      'react-hooks/exhaustive-deps': 'warn',
      'react-refresh/only-export-components': 'warn'
    }
  },

  // Prettier 兼容(最后)
  eslintConfigPrettier
];

我的项目用了 globals 依赖包 所以也需要安装一下

csharp 复制代码
pnpm add -D -w globals
  • globals 包提供了常见环境的全局变量列表,例如:
    • globals.browserwindow, document, fetch...
    • globals.nodeprocess, require, __dirname...
    • globals.jestdescribe, test, expect...

ESLint 需要这些信息来判断某个变量是"未声明"还是"环境全局"。

根目录的 package.json 中增加 scripts

json 复制代码
{
  "scripts": {
    "check-types": "tsc -b",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx",
    "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx --fix"
  },
}

第三步:增加 git commit 钩子函数

如果要使用 git 钩子,那就要安装一些依赖

csharp 复制代码
pnpm add -D -w husky lint-staged

根目录的 package.json 中增加配置

json 复制代码
"husky": {
    "hooks": {
      "pre-commit": "lint-staged"
    }
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix"]
  }

如果想要钩子运行起来还需要在根目录增加.husky文件夹,文件夹下创建 pre-commit 文件,使用下面命令进行创建

这个是 husky 官方提供的命令

bash 复制代码
pnpm exec husky install

这个命令不会在 .husky 下生成 pre-commit 需要自己手动创建,内容为

这个是 husky v9版本以下写法

bash 复制代码
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm lint-staged

v9 以上写法

复制代码
pnpm lint-staged

然后就可以通过以下命令进行测试了

csharp 复制代码
git init 

git add .
git commit -m "改动的msg"

当出现这种就算成功了

相关推荐
Jingyou39 分钟前
JavaScript 实现深拷贝
前端·javascript
编程猪猪侠39 分钟前
Vue 通用复选框组互斥 Hooks:兼容 Element Plus + Ant Design Vue
前端·javascript·vue.js
linda261839 分钟前
说说 Map 和 Set 的区别及实际应用
前端·javascript
_一两风40 分钟前
“点一下就能改”——这个功能为首富赚到了多少money?
前端·javascript
小飞侠在吗42 分钟前
vue setup与OptionsAPI
前端·javascript·vue.js
疯不皮42 分钟前
tiptiap3如何实现编辑器内部嵌套多个富文本编辑器
前端·vue.js·开源
溪饱鱼43 分钟前
主动与被动AI交互范式
前端·后端·aigc
我叫黑大帅44 分钟前
如何实现UniApp登录拦截?
前端·javascript·vue.js
写代码的皮筏艇44 分钟前
Sequelize 详细指南
前端·后端