【前端工程化】一文看懂现代Monorepo(npm)工程

微前端与Monorepo的架构设计 本文主要介绍如何使 Monorepo 与微前端结合使用,篇幅较长,请结合目录酌情阅读。微前端/Monorepo方案都存在很多争议,争论无意义,选择适合自己团队的方案即可。技术实战以 Micro App + pnpm 技术栈,讲解了从零搭建到项目部署的一个完整微前端项目。 阅读详情

引言:本文介绍了目前流行的 pnpm workspace + changesets + turborepo 构建npm包项目的方案,这套方案也适用于其他大型Monorepo项目。此外,还补充了前端工程下package.json、tsconfig和husky等配置知识以及CI/CD相关常识。

原文:【前端工程化】一文看懂现代Monorepo(npm)工程

一、认识package.json

一个package.json代表了一个项目,可以通过npm/yarn/pnpm命令初始化一个package.json:

pnpm init

然后自行完善package.json。下面我选举了几个著名npm包的package.json作为学习参考,并且重点介绍一些字段含义来帮我们理解项目。

1.比如 vueusenpm包的 packages/shared/package.json

{
  "name": "@vueuse/shared",
  "type": "module",
  "version": "14.0.0",
  "author": "Anthony Fu <https://github.com/antfu>",
  "license": "MIT",
  "funding": "https://github.com/sponsors/antfu",
  "homepage": "https://github.com/vueuse/vueuse/tree/main/packages/shared#readme",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/vueuse/vueuse.git",
    "directory": "packages/shared"
  },
  "bugs": {
    "url": "https://github.com/vueuse/vueuse/issues"
  },
  "keywords": [
    "vue",
    "vue-use",
  ],
  "sideEffects": false,
  "exports": {
    ".": "./dist/index.js",
    "./*": "./dist/*"
  },
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "unpkg": "./dist/index.iife.min.js",
  "jsdelivr": "./dist/index.iife.min.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist"
  ],
  "scripts": {
    "build": "tsdown",
    "prepack": "pnpm run build",
    "test:attw": "attw --pack --config-path ../../.attw.json ."
  },
  "peerDependencies": {
    "vue": "^3.5.0"
  }
}

2.比如ahooks的packages/hooks/package.json

{
  "name": "ahooks",
  "version": "3.9.6",
  "description": "react hooks library",
  "keywords": [
    "ahooks",
    "umi hooks",
    "react hooks"
  ],
  "main": "./lib/index.js",
  "module": "./es/index.js",
  "types": "./lib/index.d.ts",
  "unpkg": "dist/ahooks.js",
  "sideEffects": false,
  "authors": {
    "name": "brickspert",
    "email": "brickspert.fjl@alipay.com"
  },
  "publishConfig": {
    "registry": "https://registry.npmjs.org/"
  },
  "repository": "https://github.com/alibaba/hooks",
  "homepage": "https://github.com/alibaba/hooks",
  "scripts": {
    "build": "gulp && webpack-cli",
    "test": "vitest run --color",
    "test:cov": "vitest run --color --coverage",
    "tsc": "tsc --noEmit"
  },
  "files": [
    "dist",
    "lib",
    "es",
    "metadata.json",
    "package.json",
    "README.md"
  ],
  "dependencies": {
  },
  "peerDependencies": {
    "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
    "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
  },
  "license": "MIT",
  "gitHead": "11f6ad571bd365c95ecb9409ca3050cbbfc9b34a"
}

3.element-plus的packages/element-plus/package.json

{
  "name": "element-plus",
  "version": "0.0.0-dev.1",
  "description": "A Component Library for Vue 3",
  "keywords": [
    "element-plus",
  ],
  "homepage": "https://element-plus.org/",
  "bugs": {
    "url": "https://github.com/element-plus/element-plus/issues"
  },
  "license": "MIT",
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "types": "es/index.d.ts",
  "exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    },
    "./global": {
      "types": "./global.d.ts"
    },
    "./es": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs"
    },
    "./lib": {
      "types": "./lib/index.d.ts",
      "require": "./lib/index.js"
    },
    "./es/*.mjs": {
      "types": "./es/*.d.ts",
      "import": "./es/*.mjs"
    },
    "./es/*": {
      "types": [
        "./es/*.d.ts",
        "./es/*/index.d.ts"
      ],
      "import": "./es/*.mjs"
    },
    "./lib/*.js": {
      "types": "./lib/*.d.ts",
      "require": "./lib/*.js"
    },
    "./lib/*": {
      "types": [
        "./lib/*.d.ts",
        "./lib/*/index.d.ts"
      ],
      "require": "./lib/*.js"
    },
    "./*": "./*"
  },
  "unpkg": "dist/index.full.js",
  "jsdelivr": "dist/index.full.js",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/element-plus/element-plus.git"
  },
  "publishConfig": {
    "access": "public"
  },
  "style": "dist/index.css",
  "peerDependencies": {
    "vue": "^3.2.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
  },
  "web-types": "web-types.json",
  "browserslist": [
    "> 1%",
    "not ie 11",
    "not op_mini all"
  ]
}

4.比如 naive-ui的 package.json (naive-ui项目不是monorepo)

{
  "name": "naive-ui",
  "version": "2.43.1",
  "packageManager": "pnpm@9.5.0",
  "description": "A Vue 3 Component Library. Fairly Complete, Theme Customizable, Uses TypeScript, Fast",
  "author": "07akioni",
  "license": "MIT",
  "homepage": "https://www.naiveui.com",
  "repository": {
    "type": "git",
    "url": "https://github.com/tusen-ai/naive-ui"
  },
  "keywords": [
    "naive-ui",
  ],
  "sideEffects": false,
  "main": "lib/index.js",
  "module": "es/index.mjs",
  "unpkg": "dist/index.js",
  "jsdelivr": "dist/index.js",
  "types": "es/index.d.ts",
  "files": [
    "README.md",
    "dist",
    "es",
    "generic",
    "lib",
    "volar.d.ts",
    "web-types.json"
  ],
  "scripts": {
    "start": "pnpm run dev",
    "dev": "pnpm run clean && pnpm run gen-version && pnpm run gen-volar-dts && NODE_ENV=development vite",
  },
  "web-types": "./web-types.json",
  "peerDependencies": {
    "vue": "^3.0.0"
  },
  "dependencies": {
    
  },
  "devDependencies": {
    
  },

}

文件目录

1."type"字段

可以设置"type": "module" 或者"type": "commonjs"

含义:这个字段指定了Node.js应该如何处理.js文件的模块系统。

作用:

  • 当设置为"module"时,所有.js文件都会被当作ES模块处理
  • 这意味着您可以使用import/export语法而不是require/module.exports
  • 如果没有设置或者设置为"commonjs",则使用CommonJS模块系统

但是你要注意:js/ts文件是"谁"运行的,如果是Node,自然遵循上述原则。但如果是webpack/vite/rollup/tsdown这类构建工具其实"type"这个字段对这类js/ts文件约束不到,因为此时不管是import还是require写的,构建工具都应该能识别并转换到target(target指配置打包cjs还是esm格式)

a. vueuse声明了"type": "module" ,然后采用.js + "import"语法
b. 你还会发现很多项目也不爱写type:“module”,然后有两种处理方式:

  • .mjs + "import"的语法,比如element-plus
  • .js + "require"的语法,比如ahooks

vueuse项目部分截图
image.png
ahooks项目部分截图
image.png

P.S. 项目下vitest.config.ts种都是采用import语法,不管你声没声明"type": "module",这是因为Node v12 模块系统就开始支持require和import两种语法了。

2."module"字段

含义:当使用import ... from xx导入包时,构建工具(如webpack、rollup或vite等)用来识别ES模块版本的入口文件。

作用:

  • 当打包工具支持ES模块时,会优先使用这个字段指定的文件作为入口
  • 有助于实现tree-shaking(摇树优化),因为ES模块是静态分析的

在您的项目中:"module": "./dist/index.mjs" 表示打包工具使用ESM导入时,从入口./dist/index.mjs这个文件导入。

3."main"字段

含义:当使用require()导入包时,Node.js会查找这个字段指定的文件。

作用:

  • "main"字段指定了包的CommonJS入口点。
  • 这是传统的包入口点定义方式。

与"module"字段的区别:

  • "main": CommonJS入口(用于Node.js的require)
  • "module": ES模块入口(用于打包工具的ESM的import/export)
4."exports"字段(现代替代方案)

含义:这是Node.js 12+引入的现代包入口点定义方式,提供了更精细的控制。

在您的项目中,可以这样配置

"exports": {
    ".": {
	    "types": "./dist/index.d.ts",    // TypeScript类型定义
	    "import": "./dist/index.js"     // ES模块导入入口
	}
    "./*": "./dist/*"
  }

如果你同时提供了ESM和CommonJS产物导出,你可以这样配置

"exports": {
    ".": {
      "types": "./es/index.d.ts",
      "import": "./es/index.mjs",
      "require": "./lib/index.js"
    }
}

解释

  • ".": "./dist/index.js" - 当其他模块通过 import ... from '@caikengren/uni-hooks'导入您的包时,Node.js将使用 index.js 文件作为入口点。
  • "./*": "./dist/*" - 这允许导入包的子路径,例如 import useXXX from '@caikengren/uni-hooks/useXXX' 。这种模式允许访问包内的特定文件或子模块。
  • "types": "./dist/index.d.ts" - 告诉 TypeScript 编译器在哪里找到该包的类型声明文件。

补充说明

  • exports应该比这比传统的 main/module 字段提供了更强大的控制能力。 exports比较新,为了兼容性,一般也需要对应配置好main/module。
  • 控制包的访问边界。使用 exports 字段的一个重要好处是它可以保护包的内部文件。在没有 exports 字段的情况下,包的所有文件都可以被导入。而使用 exports 字段后,只有明确定义的路径才能被外部访问,这为包作者提供了更好的封装性。
5."files"字段

定义了npm上传到仓库时,需要上传哪些文件(目录),通常包含你打包的代码文件、package.json、README还有其他运行时要用到的文件 。

例如:类似ahooks,它针对Node CJS、浏览器ESM和浏览器unpkg的形式打包了三份代码,分别放在三个不同文件夹,这些文件夹都是要上传的。所以files字段定义了dist、lib和es。
image.png

发布配置

"private": true,
"publishConfig": {
  "tag": "1.1.0",
  "registry": "https://registry.npmjs.org/",
  "access": "public"
}
1."private"字段

1.private字段可以防止我们意外地将私有库发布到npm服务器。即当我们使用npm publish发布时不会被当做一个“npm包”被发布。 比如monorepo项目的根package.json文件(仅管理项目结构用,不是一个单独的npm包)。

2.但即使"private": true,当我配置了publishConfig,那么也是可以继续发布的。明确配置了publishConfig可以保证这个包是可以安全发布的。

3.默认 "private"字段为 true。

2."publishConfig"字段

1.当发布包时这个配置会起作用,在这里配置tag或仓库地址。

2."registry":你团队/公司的的npm仓库地址。

3."tag":默认就是 latest(表示发布的就是正式版)。有时候需要发布beta版本(公开测试),那么这个tag就起作用了。

4.假设我们的version: 1.1.1-beta.0,然后通过npm publish --tag beta发布(注意:此时发布命令多了一个–tag参数),如果不显示指定–tag,那么就会用到我们的publishConfig.tag作为这个tag值。

5.tag值的作用就是告诉仓库这是个beta版本。当有人如下安装时:

npm i your-package@latest

6.不会安装最近发布的beta版,而是安装最近最新发布的latest版(发行版)。
如果需要安装beta版,需要手动指定版本:

npm i your-package@1.1.1-beta.0

依赖配置

1."dependencies"

dependencies:声明的是项目的生产环境中所必须的依赖包。打包时会把这些依赖的代码打包进去(这些包往往是运行时起作用)。

"dependencies": {
   "react": "^18.0.2",
   "react-dom": "~18.0.2",
   "lodash": "4.17.21" 
}

这里每一项配置都是一个键值对(key-value), key表示模块名称,value表示模块的版本号。版本号遵循主版本号.次版本号.修订号的格式规定:

  • 固定版本: "lodash": "4.17.21"表示安装时只安装这个指定的版本;
  • 波浪号: "react-dom": "~18.0.2"表示安装18.0.x的最新版本(不低于18.0.2),也就是说安装时不会改变主版本号和次版本号,修订号会安装最新的;
  • 插入号: "react": "^18.0.2"表示安装18.x.x的最新版本(不低于18.0.2),也就是说安装时不会改变主版本号,次版本号和修订号会安装最新的;

通常推荐"^18.0.2"这种,保证大版本的最新。

2."devDependencies"

devDependencies:声明的是项目的开发环境、项目运行起来所必须的依赖包。打包时不会把这些依赖的代码打包进去(这些包往往是编译时起作用)。像typescript,vite还有一些vite plugin和type包。

"devDependencies": {
    "@types/node": "^20.10.0",
    "@vitejs/plugin-vue": "^6.0.1",
    "tsdown": "^0.15.12",
    "typescript": "^5.4.0",
    "vite": "^5.0.0",
  }
3."peerDependencies"

peerDependencies:这个专门用于npm包项目。我的npm包项目和安装这个npm的项目可能依赖同一个模块(另外一个npm包),那么我的npm包打包时就不需要把这个模块打包进去。比如:
我的npm包是依赖vue框架,所有安装我这个npm包的项目都应该安装了vue依赖,那么就有
我的npm包package.json:

"name": "my-package",
"peerDependencies": {
    "vue": "^3.4.0"
}

a.当别人项目的vue的大版本 和 我的npm包相同

"dependencies": {
    "vue": "^3.4.1"
}

此时别人项目npm i my-package时可以安装成功的。my-package和别人项目一起依赖vue@3.4.1。

b.但是当别人项目的vue的大版本 和 我的npm包不同

"dependencies": {
    "vue": "^2.4.1"
}

此时别人项目npm i my-package时无法成功,会提示别人(报错):

npm ERR! Could not resolve dependency: npm ERR! peer vue@"^3.4.0 from my-package@1.1.1

此时有一些比较安全可以绕过的方式:

npm i my-package --legacy-peer-deps

👉 适用于你明确知道自己在干什么,比如“这个库虽然没声明支持 vue 2,但其实可以工作”。(这是有一定风险的)

参考

关于前端大管家 package.json,你知道多少?
PACKAGE.JSON
在NPM上发布beta或alpha版

二、认识tsconfig配置和eslint config

tsconfig

tsconfig.json 是 TypeScript 项目的核心配置文件,用于控制 TypeScript 编译器(tsc)的行为。它决定了哪些文件会被编译、如何编译、以及输出结果的格式和位置。下面我将结合你的项目实际情况,详细解释常见的配置项及其作用:

1. compilerOptions

这是最重要的配置块,决定了编译器的具体行为。常见字段如下:

  • target:指定编译后的 JavaScript 版本,比如 "ESNext"、"ES2020"。影响新语法的支持。
  • module:指定模块系统,如 "ESNext"、"CommonJS"。你的项目采用 ESM,通常设置为 "ESNext"。
  • lib:指定要包含在编译中的库,比如 ["DOM", "ESNext"],决定可用的全局类型。
  • declaration:是否生成类型声明文件(.d.ts),通常库项目会开启。
  • outDir:编译输出目录,比如 "dist"。
  • baseUrl: 通常设置为根目录".",它是ts中识别路径的"起点"。比如import ... from 'src/...',表示从.路径下找src目录。
  • rootDir:源码根目录,决定哪些文件被编译。
  • strict:开启所有严格类型检查选项,建议库项目开启。
  • esModuleInterop:允许默认导入 CommonJS 模块,提升兼容性。
  • skipLibCheck:跳过库文件的类型检查,加快编译速度。
2.paths 别名

TypeScript 仅在“编译期”识别路径别名,它不会影响运行时。要让别名在运行时也可用,需要让打包器(Vite/Rollup/tsdown)或 Node 的解析与之保持一致。必须先确保设置了baseUrl

常见配置:

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@caikengren/uni-hooks-shared": ["packages/uni-hooks-shared/index.ts"],
      "@caikengren/uni-hooks": ["packages/uni-hooks/index.ts"]
    }
  }
}

运行时配合(示例以 Vite 为例):

// vite.config.ts
import { defineConfig } from 'vite'
import path from 'node:path'

export default defineConfig({
  resolve: {
    alias: {
      '@caikengren/uni-hooks-shared': path.resolve(__dirname, 'packages/uni-hooks-shared/index.ts'),
      '@caikengren/uni-hooks': path.resolve(__dirname, 'packages/uni-hooks/index.ts'),
    },
  },
})

在库项目中,更推荐通过“包名”引用,而不是源码别名:

// packages/hooks/package.json
{
  "name": "@caikengren/uni-hooks",
  "dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^"
  }
}

这样在代码里直接:

import { foo } from '@caikengren/uni-hooks-shared'

避免了别名与打包器的双重维护。

3. include 和 exclude
  • include:指定要编译的文件或目录(如 ["src"])。
  • exclude:指定不编译的文件或目录(如 ["node_modules", "dist"])。
4.extends

用于“继承”一份基础配置,统一 Monorepo 多包的 TypeScript 行为。

推荐在根目录放置一份基准:

// tsconfig.json(根)
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "lib": ["ESNext", "DOM"],
    "strict": true,
    "declaration": true,
    "skipLibCheck": true
  }
}

子包继承并按需覆盖:

// packages/uni-hooks-shared/tsconfig.json
{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src"]
}

可以多层继承,但保持链路清晰,避免在各包内重复配置相同选项。

P.S. 如果只有根目录有tsconfig.json,子包没有tsconfig.json,子包会受到根目录tsconfig.json的约束。

eslint config

关于js格式和规范上,传统都是eslint + prettier,但配置时比较繁琐,还要处理两者的冲突。参考antfu大佬的译文,可以使用@antfu/eslint-config(vueuse项目就使用了这个)来做eslint检查。
@antfu/eslint-config是由 Vue 核心团队成员、开源界大神 Anthony Fu 创建并维护的一套 ESLint 配置集合。它是目前社区中最流行的 ESLint 配置之一(GitHub Star 数非常高),其核心特点是“固执己见” (Opinionated) 且“开箱即用”。它采用了 ESLint 最新的 Flat Config (扁平化配置) 系统。

核心亮点:
  1. 一站式解决方案:它不仅仅是一个 ESLint 配置,它集成了 Prettier 的功能(通过 ESLint Stylistic),你不需要再安装 Prettier。
  2. 自动检测:它会自动检测你的项目中是否使用了 Vue、React、TypeScript、UnoCSS 等,并自动启用相关规则。
  3. 代码风格:
    • 无分号 (No Semicolons)
    • 单引号 (Single Quotes)
    • 尾随逗号 (Trailing Commas)
  4. 多文件支持:除了 JS/TS,还支持 JSON、YAML、Markdown、HTML 等文件的 lint 和格式化。
  5. 导入排序:内置了 eslint-plugin-simple-import-sort,自动把 import 整理得干干净净。
安装&配置
pnpm i -wD eslint @antfu/eslint-config

虽然这个包主打“零配置”,但实际开发中,我们通常需要根据团队习惯微调一些规则。

@antfu/eslint-config 的默认导出是一个工厂函数,它接受任意数量的参数。

// eslint.config.js
import antfu from '@antfu/eslint-config'

export default antfu(
  {
    // ============================================================
    // 1. 全局功能配置 (Options)
    // ============================================================

    // 显式启用/禁用特定框架支持(默认会自动检测,但显式写出更清晰)
    vue: true,
    typescript: true,

    // 格式化风格配置 (替代 Prettier)
    stylistic: {
      indent: 2, // 缩进 2 空格
      quotes: 'single', // 单引号
      jsx: true, // 支持 JSX
    },

    // 忽略文件 (相当于 .eslintignore)
    ignores: [
      'patches',
      'playground',
      'playgrounds',
      'docs',
      '**/types',
      '**/cache',
      '**/*.svg',
      '.cursor',
      '.trae',
      'scripts',
    ],
  },

  // ============================================================
  // 2. 具体规则覆盖 (Overrides)
  // ============================================================

  // 一般性规则覆盖
  {
    rules: {
      // 允许使用 console.log (默认是 warn 或 error)
      'no-console': 'off',
      // 允许使用未使用的变量 (通常用于解构时忽略某些属性)
      'unused-imports/no-unused-vars': 'warn',
      // 允许使用 @ts-ignore(覆盖 antfu 默认的禁用策略)
      'ts/ban-ts-comment': ['error', { 'ts-ignore': false }],
      // 如果你真的想要分号 (虽然 antfu 默认是无分号的)
      // 'style/semi': ['error', 'always'],
      'style/max-statements-per-line': 'off',

      'ts/ban-types': 'off',
      'node/no-callback-literal': 'off',
      'import/namespace': 'off',
      'import/default': 'off',
      'import/no-named-as-default': 'off',
      'import/no-named-as-default-member': 'off',
      'node/prefer-global/process': 'off',
      'ts/unified-signatures': 'off',
      'ts/no-unsafe-function-type': 'off',
      'ts/no-dynamic-delete': 'off',
    },
  },

  // 针对特定文件的规则覆盖
  {
    files: ['**/*.vue'],
    rules: {
      // Vue 组件名必须由多个单词组成?关掉它
      'vue/multi-word-component-names': 'off',
    },
  },

  {
    files: ['**/*.json'],
    rules: {
      // 允许 JSON 文件末尾有逗号 (某些工具不支持,可能需要关掉)
      'jsonc/comma-dangle': 'off',
    },
  },
)
配置lint script
{
	"sciprts": {
		"lint": "eslint --cache .",
		"lint:fix": "eslint --cache --fix .",
	}
}
  • --cache启用缓存,缓存会把上次已通过的文件记住,下一次只对变更或受影响的文件重新检查,提升速度。检查的目标路径是当前目录.,具体包含哪些文件由 ESLint 配置决定。
  • 加上--fix 自动修复可自动修复的 ESLint 违规(如缩进、引号、分号等)。不能自动修复的规则会保留为错误或警告,需要手动处理。

pnpm-lock文件

1.pnpm-lock.yaml 用于锁定整个 Workspace 的依赖树,保证“可重复安装”(同样的依赖版本与拓扑)。
2.如果没有这个lock文件,会发生什么呢?
比如我的依赖:

packageX: ^1.0.0

当第三方包 packageX发布了新版:1.1.0,那么再次安装(pnpm/yarn install)时都会安装1.1.0新版本的库,有时候可能会带来问题(尤其生产环境不推荐这样)。

3.关键点:

  • 单仓 Monorepo 只有“根锁文件”,子包不生成独立锁;所有安装动作最终写回根锁。
  • 锁文件应提交到仓库,CI/同事机器据此实现一致的安装结果。
  • 发布到 npm 不会携带锁文件;库消费者不受你的锁文件影响。

4.常用命令:

# 严格模式(锁与 package.json 不一致时失败)
pnpm install --frozen-lockfile

# 仅更新锁文件(不下载依赖)
pnpm install --lockfile-only

5.与 npm/yarn 的差异:

  • pnpm 采用“内容寻址存储(Content-Addressable Store)”,远程包先存入全局 .pnpm-store,再以符号链接挂到 node_modules,磁盘占用更小。
  • Workspace 下通过 workspace: 协议把本地包也以符号链接方式关联,升级内部版本时,由根锁统一反映变更。

6.多人协作建议:

  • 不手改锁文件;依赖升级统一用 pnpm up <pkg>@<range> 或在子包用 --filter 精准升级。
  • 合并冲突时,优先保留最新一次“成功安装后”的锁版本。

参考

【译】antfu博客:为什么我不用Prettier

三、认识monorepo和相关工具

monorepo 介绍

1.monorepo 是多个包在同一个项目中管理的方式,比较流行的一种管理方式。
软件项目管理经历了三个阶段:

阶段名称管理方式产生背景 / 特点优势劣势
1​.**Monolith (单体巨石应用)**​单仓库管理所有项目代码项目初期,业务复杂度低,所有代码集中在一个仓库中。结构简单,初期管理方便。1. 随着业务增长,代码量庞大,复杂度高。 2. 构建效率低下。
2.**MultiRepo (多仓库多模块)**​每个业务模块独立一个仓库为解耦巨石应用,将项目拆分为多个模块,分别管理。1. 模块解耦,复杂度降低。 2. 各模块可独立开发、测试、发布。 3. 构建效率提升。1. 跨仓库代码共享困难。 2. 依赖管理复杂(底层模块升级,依赖方需手动更新)。 3. 仓库数量增多后,工程管理难度增加。 4. 构建耗时增加(需按顺序构建多个仓库)。
3.​**MonoRepo (单仓库多模块)**​单一仓库中管理多个项目或模块为解决MultiRepo在模块数量增多后产生的管理问题。1. 代码共享便捷。 2. 共享依赖,减少包安装和包一致性。 3. 共享工程配置,保证代码风格和质量一致。 4. 便于构建工具优化(如增量构建、并行构建)。1. 仓库体积较大。 2. 需要工具支持来管理权限和优化构建性能。

2.monorepo项目目录结构:

|-- node_modules
|-- packages
|   |-- packageA
|   |   |-- package.json
|   |-- packageB
|   |   |-- package.json
|   |-- packageC
|   |   |-- package.json
|-- eslint.config.js
|-- package.json

1个package.json代表一个项目,最外面的package.json代表根项目,里面的package.json代表子包项目A、B和C等。
根项目存在的意义是:管理这些子包,比如共享根项目的依赖(npm包)、子包间依赖自动link和递归执行子包script命令等。

3.monorepo是一种管理规范,那么pnpm workspace协议就是用来支撑起这种规范的技术协议。具体做法:

根目录添加pnpm-workspace.yaml 配置文件:

packages:
  - 'packages/*'

这样一来,/packages下的所有项目都会被 pnpm 识别为 workspace 的成员包

4.下面我们来看JS/TS项目领域,monorepo项目如何通过pnpm/changeset/turbo等工具解决下面三个问题:

  1. 共享代码/依赖,依赖管理
  2. 版本更新、自动tag和发布
  3. 并行/串行构建所有项目,处理构建顺序

5.后续我们用这个monorepo项目结构进行讲解。(记住这个结构,下面会多次用到)

|-- node_modules
|-- packages
|   |-- hooks
|   |   |-- package.json ("name":"@caikengren/uni-hooks")
|   |-- shared
|   |   |-- package.json ("name":"@caikengren/uni-hooks-shared")
|   |-- use-request
|   |   |-- package.json  ("name":"@caikengren/uni-use-request")
|-- eslint.config.js
|-- package.json

image.png

pnpm workspace 管理依赖

问题1:共享代码/依赖,依赖管理

这个问题可以拆成两个子问题来看:

  1. 多个子包依赖外部第三方npm包,比如依赖vue/react,那么这些这些包是不是可以共享(而不是每个子包项目都安装一遍)?
  2. 子包之间存在依赖,比如子包A依赖本地的子包B、C,但B、C都还没发布怎么依赖?能不能本地B、C链接到A的node_modules下呢?
1.pnpm i 安装依赖

针对上面的第一个问题,通过把依赖安装在根目录,那么子项目寻找依赖就会采用根目录的依赖,从而实现共享依赖
–workspace参数
根目录下安装vue:

pnpm i -w vue

-w相当于--workspace,表示在根目录下安装依赖。

比如我要安装typescript:

pnpm i -wD typescript

-D表示安装devDependencies,-w和-D可以合并为-wD。

安装完成后,只有根目录package.json会有依赖声明,如下:

"dependencies": {
    "vue": "^3.4.0"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
  }

–filter参数
比如我有一个@caikengren/uni-hooks子包,依赖了另外两个子包

  • @caikengren/uni-use-request
  • @caikengren/uni-hooks-shared
    那么可以通过下面的方式,表示在@caikengren/uni-hooks这个子包中安装另外两个依赖。
pnpm i @caikengren/uni-use-request@workspace:^ --filter @caikengren/uni-hooks

pnpm i @caikengren/uni-hooks-shared@workspace:^ --filter @caikengren/uni-hooks

--filter xxx表示在xxx子包下安装依赖,其中依赖的版本使用@workspace:^占位。

安装完成后 packages/uni-hooks目录的package.json会出现:

"dependencies": {
    "@caikengren/uni-hooks-shared": "workspace:^",
    "@caikengren/uni-use-request": "workspace:^"
}

@workspace:^
@workspace:^ 是一种特殊的协议前缀,用于在依赖声明中引用当前 workspace 中的本地包,并自动使用该包的版本号加上 ^ 语义化版本范围。

也就是说,pnpm 会:

  1. 在当前 workspace 中查找名为 my-local-pkg 的本地包,不去远程下载了;
  2. 获取它在 package.json 中声明的版本号(比如 1.2.3);
  3. 将依赖解析为 ^1.2.3,即允许安装兼容的次要版本更新(遵循 semver 规范);
  4. 但前提是这个包必须存在于 workspace 中。如果不存在,构建会失败。
2.pnpm自动link

前面,我们利用@workspace:^在uni-hooks子包中安装了另外两个依赖:@caikengren/uni-hooks-shared 和@caikengren/uni-use-request,那么这两依赖实际是指向哪里呢?

自动link原理
image.png

1.pnpm i -w B@workspace:^ C@workspace:^ --filter A
B,C被软链接到A的node_modules

2.pnpm i -w Other@5.0.0
Other实际上是安装到.pnpm store,.pnpm目录相当于.pnpm store的映射(内容同步的),.pnpm目录下的Other会被软链接到根目录的node_modules

拓展补充:ln是shell命令(ln -s相当于windows的快捷方式)。
ln -s(软链接)的文件可读写(需要原文件允许软链接读写),本质不是同一文件,而是创建了一个链接符号。
ln (硬链接)的文件可以读写,和原文件本质上是同一个文件,仅在不同地方展示了。

区分:

  • pnpm 的特性:安装的远程依赖会被放到.pnpm目录,然后软链接到node_modules,从而依赖共享,节省磁盘空间。
  • pnpm workspace的特性: 自动link。通过@workspace:^安装的本地依赖,直接软链接到node_modules,即自动link。

我们的项目中:
uni-hooks子包依赖本地@caikengren/uni-hooks-shared 和@caikengren/uni-use-request ,安装后你会发现node_modules下的依赖有箭头符号,说明是“符号链接”(软链接)
image.png

疑问 workspace:*和workspace:^的区别?

1.@caikengren/packageA@workspace:* 打包后 (产物/发布到 npm):

pnpm 会将其替换为当前 workspace 中该包的精确版本。

{
  "dependencies": {
    "@caikengren/packageA": "1.0.0" 
  }
}

2.@caikengren/packageA@workspace:^ 打包后 (产物/发布到 npm):

pnpm 会将其替换为以当前版本为基准的 (^) 范围。

{
  "dependencies": {
    "@caikengren/packageA": "^1.0.0"
  }
}

changeset 发布

问题2:版本更新、changelog、自动tag和发布

changesets 主要关心 monorepo 项目下子项目版本的更新、changelog 文件生成、包的发布。一个 changeset 是个包含了在某个分支或者 commit 上改动信息的 md 文件,它会包含这样一些信息:

  • 需要发布的包
  • 包版本的更新层级(遵循 semver 规范)
  • CHANGELOG 信息
1.项目准备

安装工具

pnpm i -wD @changesets/cli

安装完成后,会在node_modules/.bin目录(依赖包提供的命令会出现在这)看到changeset,意味着可以用npx来运行这个命令。
在这里插入图片描述

初始化

npx changeset init

会多出一个changeset的文件,如下
image.png

2.本地认证准备

假设你已经注册了一个npm账号(或私有仓库账号),然后通过terminal登录,在终端输入

npm adduser
# 或者(效果一样)
npm login
3.发布流程

假设你已经改过代码并且提交了,准备发布新版本。

1.创建一个新的 changeset 文件。

npx changeset add

说明:

  • 这个命令会交互式地让你选择要升级的包(在 monorepo 中)、版本类型(patch / minor / major)以及填写变更描述。
  • 生成的文件会保存在 .changeset/ 目录下,格式如 clever-horses-fix.md。
  • 这些文件后续会被用来决定如何 bump 版本和生成 changelog。

第一个问题:选择要改版的子包项目。「上下」键选择子包,「空格」键表示选择,「enter」确定最终选择。
changeset add是要依据commit提交信息
image.png

进入第二个问题:选择要递增的版本级别(依次是major/minor/patch )。按「enter」进入待办项目的下一个版本级别。(比如现在是major, 「enter」后进入minor)
image.png

进入第三个问题:总结
image.png

进入第四个问题:是否确认
image.png

最后,会多出一个changeset文件(md文件),描述了版本变更、changelog内容
image.png

2.根据 .changeset/ 中的所有 changeset 文件,自动 bump 相关包的版本,并更新依赖关系。

npx changeset version

说明:

  • 它会读取所有未处理的 changeset,计算每个包的新版本号。
  • 自动修改 package.json 中的版本字段。
  • 如果是 monorepo,还会更新内部依赖的版本(比如 pkg-a 依赖 pkg-b,且 pkg-b 升级了,这里会自动更新引用)。
  • 同时会删除已处理的 changeset 文件(或将其归档)。

image.png

可以看出子包下面都多了/修改了CHANGELOG.md文件,并且package.json的版本号发生了变化(1.0.0->1.1.0)
image.png

image.png

3.将新版本的包发布到npm仓库

npx changeset publish
  • 说明:
    • 它会执行 npm publish 对每个需要发布的包。
    • 默认只发布那些版本号发生变化的包。
    • 需要你已经登录到 npm(npm whoami)。
    • 此外,会给每个版本变动的包打tag.

先提交和push代码

git add .
git commit -m 'chore(release): v1.1.0'
git push

再发布(企业级发布,这一步通常属于CI,在云端流水线完成)

npx changeset publish

image.png

然后在npm仓库 可以看到发布的三个包
image.png

turborepo 构建

问题3:并行/串行构建所有项目,处理构建顺序

1.介绍

为什么需要 Turborepo

  • Monorepo 常见痛点:构建串行、慢;跨包依赖编排复杂;重复构建浪费时间。
  • yarn workspace 通常串行构建,整体耗时长且不可控。
  • pnpm workspace 能并行,但对复杂依赖拓扑的顺序编排有限,且缺少内建的强缓存机制。
  • Turborepo面向这些问题,提供有序并行、增量与缓存的组合解法。

它解决了什么问题

  • 构建编排:用任务依赖图(DAG)明确“谁先谁后”,避免手工脚本胶水。
  • 并行执行:独立包并行跑,依赖包按顺序跑,缩短全量构建时间。
  • 增量构建:对输入做哈希,只重建被改动影响的任务,减少无效工作。
  • 本地与远程缓存:同样输入直接复用产物;接入远程缓存后,CI 与开发者之间共享结果。
2.并行和构建编排

使用turbo初始化一个 Monorepo 的项目,有以下几个 package:

  • apps/web,依赖 shared
  • apps/docs,依赖 shared
  • packages/shared,被 web 和 docs 依赖

目录如下:
image.png

yarn 命令 只能串行运行任务: yarn workspaces run lint && yarn workspaces run build && yarn workspaces run test
image.png
但是,要使用 Turborepo 可以更快地完成相同的工作,您可以使用 turbo run lint build test:
在这里插入图片描述

你会发现有些任务是可以并行执行的——lint和test任务可以并行。还有些是执行顺序依赖的——web和docs 的构建依赖share构建完成。

配置如下:

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}
  • tasks:声明任务编排与产物输出,key是turbo run Xxx的Xxx。
  • dependsOn:用 ^task 表示“先跑上游依赖的同名任务”,自动根据依赖图排序。
  • outputs:声明任务产物(如 dist/**),让 Turborepo准确缓存和复用构建结果。

关于^task的理解是,不同包的任务编排:
当前 package 执行 build 任务之前,需要 先运行它依赖的包(dependencies / devDependencies / peerDependencies) 的 build 任务。
当你配置了"dependsOn": ["^build"],那么「web和docs 的构建依赖share构建完成后,再执行」,否则不会。

关于dependsOn另外一种情况,没有^,表示同一个包中的任务编排。比如下面就表达了所有的test任务执行前,要先完成build命令。

{ 
	"tasks": { 
		"test": { "dependsOn": ["build"] } 
	}
}

进一步,你还可以限定到某个包的 任务编排。比如下面表达了 所有lint任务执行前,要先完成utils包的build任务。

{
  "tasks": {
    "lint": {
      "dependsOn": ["utils#build"] 
    }
  }
}
3.缓存和增量构建

第一次trubo run build后,会生成缓存存放在 node_modules/.cache/turbo/目录下

第一次构建:还没产物(没有dist目录),记录子包项目文件的hash值。
image.png

第二次构建:子包文件的hash标识和之前的比较,如果没变化,则跳过构建。
image.png

配置如下:

//turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "test": {
      "dependsOn": ["^build"]
    },
    "lint": {},
  }
}

P.S. tasks是新字段名,对应旧pipeline,含义一样。

此外,还有cache配置和--filter命令参数:

cache:默认开启;可对 dev 等任务关闭缓存,保证开发时的即时性。

{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "dev": {
      "cache": false
    },
  }
}

--filter:按包名或路径过滤执行范围。

turbo run <command> --filter=<package name>

构建(打包)工具

tsdown的主要功能

相关功能请查阅tsdown中文文档,目前可以看到vueuse项目已经采用了tsdown来打包库项目。

下面简单介绍下tsdown功能:

1.零配置TypeScript支持
tsdown内置了对TypeScript的完整支持,无需额外的配置:

{
  "scripts": {
    "build": "tsdown"
  }
}

2.多格式输出
支持同时生成多种模块格式:

{
  "tsdown": {
    "format": ["esm", "cjs", "umd"],
    "entry": "src/index.ts",
    "outDir": "dist"
  }
}

并且自动为每个输出格式生成对应的.d.ts声明文件:

dist/
├── index.esm.js
├── index.esm.d.ts
├── index.cjs.js
├── index.cjs.d.ts
├── index.umd.js
└── index.umd.d.ts

3.Tree-shaking
极速Tree-shaking,自动移除未使用的代码:

// src/utils.ts
export function used() { return 'used'; }
export function unused() { return 'unused'; }

// src/index.ts
export { used } from './utils';
// unused函数会被自动移除

4.外部依赖处理
配置外部依赖,避免将依赖打包进输出文件:

{
  "tsdown": {
    "external": ["react", "vue"],
    "globals": {
      "react": "React",
      "vue": "Vue"
    }
  }
}
Rollup vs Vite vs tsdown
Rollup - 经典选择

Rollup是最早专注于ESM打包的工具之一,以其优秀的Tree-shaking能力而闻名。

优势:

  • 出色的Tree-shaking优化
  • 生成简洁的ESM输出
  • 丰富的插件生态系统
  • 适合库开发

劣势:

  • 配置相对复杂
  • 开发模式下的HMR支持有限
  • 构建速度相对较慢
Vite - 现代开发体验

Vite基于原生ESM,提供了极快的开发服务器和优化的构建流程。

优势:

  • 闪电般的冷启动速度
  • 即时的热模块替换(HMR)
  • 开箱即用的TypeScript支持
  • 现代化的开发体验

劣势:

  • 主要用于应用开发
  • 对于纯库打包可能过于复杂
  • 依赖Node.js环境
tsdown - 专为ts库项目

tsdown是一个新兴的基于rolldown(基于rust)的打包工具,专门为TypeScript项目设计,结合了现代打包工具的优点。

性能对比

在实际项目中,三种工具的构建性能对比如下:

工具冷启动时间HMR速度输出大小配置复杂度
Rollup中等慢小高
Vite快极快中等低
tsdown极快快小极低
选择建议
  • 库开发:推荐使用tsdown,零配置、多格式输出、类型声明自动生成
  • 应用开发:推荐使用Vite,优秀的开发体验和构建性能
  • 传统项目:如果已有Rollup配置且运行良好,可以继续使用

参考

带你了解更全面的 Monorepo - 优劣、踩坑、选型
从零到一使用 turborepo + pnpm 搭建企业级 Monorepo 项目
Changesets: 流行的 monorepo 场景发包工具

五、CI/CD

解释:

  • CI (Continuous Integration 持续整合)
  • CD (Continuous Delivery/Deployment 持续交付/部署)
CI

CI的目标:

  • 自动运行测试:自动执行单元测试、集成测试等。例如:npm test
  • 代码质量检查:包括 ESLint、Prettier、TypeScript 类型检查等。例如:npm run lint、npm run type-check
  • 构建验证:确保包可以正确构建(如编译 TypeScript 、打包等)。例如`npm run build

其中自动化测试、代码检查和构建验证一般在什么时候触发?

  • push到主分支 (main, master)之前。必须的
  • push到特征分支之前。可选
  • 提PR之前。可选

常用的CI平台 :

平台特点
GitHub Actions免费、与 GitHub 深度集成,YAML 配置,社区生态丰富
GitLab CI内置于 GitLab,适合私有部署
Travis CI曾经流行,现逐渐被 GitHub Actions 取代
CD

1.产物管理 (Artifact Management)
下载构建产物: 从 CI 流程(如 Jenkins workspace、GitHub Actions artifacts)中拉取打包好的 dist 或 build 目录。给当前的发布包打上 Tag 或版本号。(如果是docker,则会构建镜像和推送到镜像仓库)

2.环境配置与注入 (Configuration Injection)

  • 构建时注入:如果是静态站点,通常在 CI 阶段就通过 DefinePlugin 或 import.meta.env 写入了。
  • 运行时注入(CD 阶段) 如果是 Docker 容器化部署,通常在 CD 阶段通过 K8s ConfigMap 或环境变量将配置注入到容器中。

3.部署执行 (Deployment Execution)

a. 静态资源部署 (SPA - Vue/React)

  • 上传对象存储: 将 HTML/CSS/JS 上传到云厂商的对象存储(AWS S3, Aliyun OSS, Tencent COS)。
  • 更新CDN:为了防止用户在发布过程中访问到 404 文件,通常会先上传带 Hash 的静态资源(JS/CSS),最后上传入口文件(index.html)。

b.服务端应用部署 (SSR - Next.js/Nuxt/Node BFF)

  • 容器更新: 更新 Kubernetes (K8s) 的 Deployment 镜像版本,执行滚动更新(Rolling Update)。
  • 服务重启: 传统服务器上使用 PM2 reload。

c.npm包这类工程产物

  • 上传到npm仓库。使用npx changeset publish或者npm publish完成上传。

Git规范配置 - husky

git规范方案

该方案需要安装以下依赖

  • husky
  • lint-staged
  • commitizen
  • cz-git
  • @commitlint/cli
  • @commitlint/config-conventional
pnpm i -wD husky lint-staged commitizen cz-git @commitlint/cli @commitlint/config-conventional

1.husky拦截Git hooks

Git Hooks:Git 原生就自带一套“钩子”机制。在 .git/hooks/ 目录下,有一堆脚本(如 pre-commit.sample)。

  • 机制:当你执行特定的 Git 命令(如 commit、push、merge)时,Git 会自动去检查这个目录下有没有对应的脚本文件。如果有,就执行它。
  • 痛点:.git 目录是不会被提交到代码仓库的(它被 .gitignore 忽略)。这意味着,你在本地配置的钩子脚本,你的同事拉取代码后是看不到的,无法同步团队规范。

Husky 的核心作用就是“篡改”Git 查找钩子的路径,并将其指向项目代码中的位置(通常是根目录下的 .husky/ 文件夹),这个文件夹是可以提交到 Git 仓库共享给所有人的。

2.lint-staged 检查代码

每次提交代码前,我们希望"提交的代码"能通过eslint检查。这里有两个关键词:eslint检查和提交的代码。lint-staged就是用来做提交的代码eslint检查(而不是全部代码检查)。

3.commitlint校验commit信息

所有的git项目,都应该去遵循一个git规范,这个规范之一就是git commit的message。你肯定见过很多这样的message:chore(ci): xxx, feat(components): xxx。下面就介绍下常用的一种规范结构。

Commit Message 结构:

  • Header(头部): 必须包含,是Commit Message的核心部分。
    • 类型 (type): 标记Commit的类别,例如 feat (新功能), fix (修复bug), docs (文档), style (代码风格) 等。
    • 范围 (scope): 可选,用于说明Commit影响的范围,如文件、组件或模块。
    • 主题 (subject): 简短地描述本次提交的内容,通常不超过50个字符,首字母小写。
    • 格式: type(scope): subject。
  • Body(描述): 可选,对Commit进行详细描述,可以分成多行。
    • 解释提交的动机、解决的问题等。
    • 每行建议不超过72个字符,以避免自动换行影响可读性。
  • Footer(尾部): 可选,用于关联Issue编号或标记重大性变更 (Breaking Changes)。
    • 关联Issue: 使用如 Closes #123 或 Fixes #123。

@commitlint/cli 用来校验 Git 提交信息是否符合约定的格式。配合 @commitlint/config-conventional 使用,可强制统一提交类型(如 feat、fix、docs、refactor、chore 等)与消息结构,提升版本管理与自动化发布的可靠性。

4.commitizen和cz-git 交互式提交

commitizen提供了cli(命令交互式)的方式来完成Commit Message填写。
cz-git 是 Commitizen 的一个适配器(adapter),通常配合 commitizen 使用。基于cz-git可以更灵活的控制你的提交Message规范。

配置工具

1.初始化husky目录

npx husky init

image.png

pre-commit是 Git 众多钩子中的一个,顾名思义,它在 Commit 之前 执行。也就是说这个文件里的脚本,会在git commit真正执行前执行,避免不规范的提交。

2.commitizen 配置

  • Scripts: 添加 commit 命令来启动交互式提交。
  • Config: 指定 commitizen 使用 cz-git 适配器。
    修改package.json:
{
  "scripts": {
    "prepare": "husky", //?
    "commit": "git-cz" //使用git-cz 来做提交(多个git命令的组合)
  },
  "config": { //commitizen工具的配置
    "commitizen": {
      "path": "node_modules/cz-git"
    }
  }
}

请根据你项目中实际安装的 eslint/prettier 情况调整 lint-staged 里的命令)

2.lint-staged配置

  • Lint-staged: 配置暂存区文件的校验规则。
    修改package.json:
{
  "scripts": {
    "prepare": "husky", 
    "commit": "git-cz" 
  },
  "lint-staged": { // lint-staged工具的配置
    "*.{js,ts,vue,jsx,tsx}": [
      "eslint --fix",
      "prettier --write"
    ],
    "*.{json,md,css,scss}": [
      "prettier --write"
    ]
  }
}

添加husky钩子,pre-commit: 提交前执行 lint-staged(代码格式化/检查)。

echo "npx lint-staged" > .husky/pre-commit

3. Commitlint & Cz-git配置

cz-git 的强大之处在于它可以直接读取 commitlint 的配置,从而实现“一份配置,两处生效”。

在根目录新建文件 commitlint.config.js (如果是 type: module 项目则为 .mjs 或 .cjs):

// commitlint.config.js
/** @type {import('cz-git').UserConfig} */
export default {
  // 继承的规则
  extends: ['@commitlint/config-conventional'],
  // 自定义规则
  rules: {
    // type 类型定义,表示 git 提交的 type 必须在以下类型范围内
    'type-enum': [
      2,
      'always',
      [
        'feat', // 新功能 feature
        'fix', // 修复 bug
        'docs', // 文档注释
        'style', // 代码格式(不影响代码运行的变动)
        'refactor', // 重构(既不增加新功能,也不是修复bug)
        'perf', // 性能优化
        'test', // 增加测试
        'chore', // 构建过程或辅助工具的变动
        'revert', // 回退
        'build' // 打包
      ]
    ],
    // subject 大小写不做校验
    'subject-case': [0]
  },
  // cz-git 的交互配置(可选,用于定制交互界面)
  prompt: {
    useEmoji: true, // 是否使用 emoji
    // 可以在这里自定义提示语,例如中文化
    messages: {
      type: '选择你要提交的类型 :',
      scope: '选择一个提交范围(可选):',
      customScope: '请输入自定义的提交范围 :',
      subject: '填写简短精炼的变更描述 :\n',
      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
      footerPrefixsSelect: '选择关联issue前缀(可选):',
      customFooterPrefix: '输入自定义issue前缀 :',
      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
      confirmCommit: '是否提交或修改commit ?'
    },
    // 设置 scope 范围(根据项目模块调整)
    scopes: ['components', 'hooks', 'utils', 'styles', 'deps']
  }
}

添加 Husky钩子,提交时校验 commit message 格式。

echo "npx --no-install commitlint --edit \$1" > .husky/commit-msg
结果演示

现在有6个修改
image.png

#添加到暂存区
git add .![image.png](http://openwrite.cn/uploads/212/56587/b87d66aa-4bc8-4230-81d1-bef11eaaeeee.png)
# 执行commit script(替代git commit)
pnpm commit

image.png

六、实战monorepo形式的npm项目

关于这一块,可以参考我的一个项目 @caikengren/uni-hooks,后续我会根据这个项目展开写几篇博客和大家一起交流学习。

Monorepo 是什么?前端项目的多模块管理终极方案 包管理工具是用来管理项目依赖,发布包,安装依赖的工具,它们都提供了对工作区(workspace)的支持,允许在单个代码库中管理多个项目或包,这种工作区支持在单个代码库中同时开发,测试和管理读个相关的项目,而无需使用多个独立的代码仓库Monorepo将多个子项目统一到一个仓库,实现依赖一致,模块共享,构建加速工具选型: pnpm+Work 轻量高效;Turborepo构建优化;Lerna/Changesets自动发布实践要点:明确模块边界,配置增量构建,自动化CI/CD,合理版本策略。 阅读详情

相关推荐

基于PNPM Workspace与Turborepo的现代前端Monorepo工程化实践

在现代前端开发中,Monorepo(单体仓库)架构因其卓越的代码复用性和统一的工具链管理,已成为管理复杂项目集合的主流方案。其核心原理是将多个相关项目置于同一代码库,通过工作区协议实现包间的便捷引用与原子化提交。这一架构的技术价值在于显著提升团队协作效率、保障代码规范一致性,并优化构建与依赖管理。然而,传统方案常伴随“幽灵依赖”和“依赖分身”等痛点。为此,结合PNPM Workspace的硬链接机制与Turborepo的智能任务编排及缓存能力,构成了解决这些问题的工程化最佳实践。该组合特别适用于中大型前端应

weixin_27059669的博客 625

前端工程化实践:Monorepo与Lerna管理

Monorepo(单仓库)是指在一个Git仓库中管理多个相关项目的开发方式。这种方式的优点在于:集中式管理:所有项目都在一个仓库中,方便代码共享、版本同步和协同开发。模块化:可以创建独立的模块,方便复用和维护。统一的CI/CD:一次配置,全仓库生效,简化持续集成和部署流程。更好的依赖管理:可以更容易地管理项目间的依赖关系。Lerna是一个命令行工具,用于在Monorepo中管理多包项目。它提供了版本管理和发布功能,使得在单个仓库中管理多个npm包变得简单。

天涯学馆 1240

持而盈之,不如其已,写 ABAP 时懂得止,系统才有余地

ABAP开发中的"适度哲学":代码不是越满越好。看似周全的代码往往因过度承载职责而成为隐患,如混杂多业务逻辑的报表或工具类。《道德经》"持而盈之,不如其已"启示我们:开发应保持边界清晰,为变更留余地。在ABAP实践中,这体现在:1)方法单一职责,避免校验、持久化、提交混杂;2)RAP开发中合理使用determination/validation等特性;3)SQL查询避免"一次读全"的过度优化。真正的稳健来自适度克制,而非功能堆砌。

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 1507

2024年了你还不懂什么是monorepo?五分钟教会你!

Monorepo是一种项目开发与管理的策略模式,它代表"单一代码仓库"(在Monorepo模式中,所有相关的项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中,这些项目之间还可能会有依赖关系。npmyarnpnpm等是用来管理项目依赖、发布包、安装依赖的工具,它们都提供了对工作区(workspace)的支持,允许在单个代码库中管理多个项目或包。这种工作区支持在单个代码库中同时开发、测试和管理多个相关的项目,而无需使用多个独立的代码仓库。这些包管理工具与monorepo。

小庄zzz的博客 1万+

告别混乱!AutoSar BswM模式管理实战:手把手教你配置Rule与ActionList(附避坑指南)

本文详细解析了AutoSar BswM模式管理的实战配置,重点介绍了Rule与ActionList的高效组合方法,帮助开发者避免常见陷阱。通过具体案例演示了从基础到复杂的配置步骤,包括模式仲裁、逻辑表达式构建及ActionList优化策略,提升汽车电子系统的协调性与可靠性。

weixin_27066963的博客 259

YALMIP介绍及怎么在Matlab中加YALMIP、SDPT3

目录YALMIP简介怎么在Matlab中加YALMIP?1. 创建决策变量:2. 添加约束:3. 参数配置4. 求解怎么在MatlabR2019a中加YALMIP?1.下载工具包2.放置工具包3.添加路径4.测试安装结果怎么在MatlabR2019a中加SDPT3? YALMIP简介 怎么在Matlab中加YALMIP? yalmip是一位“集大成者”,它不仅自己包含基本的线性规划求解算法,比如linprog(线性规划)、bintprog(二值线性规划)、bnb(分支界定算法)等,他还提供了对cpl

m0_51431440的博客 2万+

前端项目的代码管理方式---monorepo

Lerna 是一个用来优化托管在git\npm上的多package代码库工作流的一个管理工具,可以让你在主项目下管理多个子项目,从而解决了多个包互相依赖,且发布时需要手动维护多个包的问题。幽灵依赖:在使用npm i 安装依赖时,有一些包的依赖包也会同时安装,但是这些包并未在package.json 中声明,这显然是不安全的,这种情况也被称为幽灵依赖。pnpm 有个根目录,一般都是保存在 user/.pnpm-store 下,pnpm 通过硬链接的方式保证了相同的包不会被重复下载。并且有4个相同的硬链接。

不咋爱学习の任大猫 4053

基于ROS2系统开发机器人

以上是基于ROS 2开发机器人的基本流程和一个简单的发布者-订阅者示例。通过这个流程,你可以逐步扩展和优化你的机器人系统,实现更复杂的功能。

xuweilin的博客 1495

从 npm 到 pnpm,再到 Monorepo:一篇读懂现代前端工程化

npm、pnpm 和 Monorepo 经常一起出现,但它们并不是同一层面的概念。本文用一篇科普文章,讲清三者的核心机制、区别、优缺点和适用场景,帮助你在实际项目中做出更合适的选择。

林间藏好物,码间藏星光 专注分享优质网站、开源项目、小众工具、模型算法 安静攒资源,慢慢变厉害•͈ᴗ⁃͈ ✧ 176

【深度剖析】三一重工的数字化转型(中篇)

三一重工在数字化转型方面取得了提高效率、降低成本、加强研发创新和改善内控管理等多方面的成效。本研究旨在通过对三一重工数字化转型的深入分析,引导企业思考在第四次工业革命与第三次能源革命交汇的超级技术窗口期,如何进一步把握发展机遇,并为同行业其他企业提供探索性的建议。作为装备制造行业的佼佼者,三一重工的数字化转型受到了业界的广泛关注。作为行业内首批接触并实践数字化转型的企业之一,三一重工的转型经验不仅成功,而且积累了丰富的理论与实践,为其他制造业企业的数字化转型提供了可借鉴的范例。

领信卓越的博客 2306

LeRobot 入门教程(九)使用 Android、iOS 手机控制机械臂

本指南详细介绍了如何使用iOS/Android手机远程控制机器人。内容包括:1)安装依赖(pip install leroobot[phone]);2)平台支持说明(iOS需HEBI Mobile I/O应用/Android用WebXR);3)手机方向校准方法;4)控制模式设定(按钮/手势);5)示例脚本使用(遥操作/数据记录/回放/策略评估);6)关键参数配置(运动学/安全限制/夹爪控制);7)故障排查方法。支持ROS2集成,提供URDF文件处理、逆向运动学计算和实时可视化功能,适用于物理机器人和仿真环境

2506_90492529的博客 1657

前端工程化之前端工程化详解 & 包管理工具

前端工程化 = 前端 + 软件工程软件工程:系统和软件工程层面上的方法、规范,技术手段提升开发的效率,软件工程来自于实体行业类似建筑行业的方法论,将建筑行业的方法论运用到软件领域上,软件领域的相关方法论又延伸到前端上前端工程化 = 将工程化的方法系统化应用到前端开发中Clean Code 代码整洁指导基于业务诉求 => 产出架构设计 又快又好又稳 <= 系统 演进 可量化的方法。

qq_34306228的博客 1376

手把手教您如何群晖/飞牛NAS等服务器上一键部署洛雪音乐同步服务,手机电脑无缝同步歌单,支持外网访问

🎵 手机电脑歌单不同步?用Docker在群晖/飞牛NAS等服务器上搭个同步服务器, 3分钟搞定 !支持docker-compose一键部署,手机、电脑、平板收藏夹实时同步,外出也能用域名远程访问 ~ ✨

XiaoqiangClub的博客 4603

现代前端工程化-基于 Monorepo 的 lerna 详解(从原理到实战)

本文你能学到什么? 看完本文后希望可以检查一下图中的内容是否都掌握了,文中的例子最好实际操作一下,下面开始正文。本文是前端工程化系列中的一篇,回不断更新,下篇更新内容可看文末的下期预告!宗...

程序员成长指北 7479

Fiori Elements 的 Adapting the UI: Analytical List Page 全解析与实战

摘要:本文详细介绍了如何通过SAP Fiori elements的UI Adaptation功能对Analytical List Page(ALP)进行细粒度适配。重点讲解了页头过滤区、图表区域、表格区域等关键组件的可配置属性,包括过滤区折叠/固定、图表类型限制、表格滚动阈值等实用功能。文章强调这些适配均可通过可视化工具完成,无需修改业务代码,并提供了典型场景的实践清单。适用于SAP Fiori elements for OData V2项目,建议配合SAP Fiori tools的Adaptation E

2007 年 ~ 2025 年,深耕 SAP 技术 18 年 434

ROCm rocr-libhsakmt分析系列1-2:多GPU跨卡VRAM访问机制分析

本文基于ROCr-Runtime的libhsakmt库源码,深入分析了多GPU系统中跨设备内存访问的实现机制。主要内容包括: 实现流程概述:通过P2P拓扑发现、统一虚拟地址空间、页表映射和硬件转发四个步骤,实现零拷贝的跨GPU内存访问。

图形/计算GPU的驱动与虚拟化技术分享园地 2504

为什么前端团队都在用 monorepo?sample-monorepo 架构优势深度解析

前端工程化发展至今,**monorepo(单仓多包)** 已成为大型团队的主流选择。作为 GitHub 加速计划中的经典示例项目,**sample-monorepo** 以极简的方式展示了基于 npm workspaces 与 TypeScript project references 的 monorepo 架构:一个仓库同时管理 React 组件库、React 应用与 Express 服务端,堪

gitblog_00828的博客 618

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

初学者的第一堂 monorepo 课:sample-monorepo 带你理解 npm workspaces 核心概念

对于刚接触前端工程化的新手来说,**npm workspaces** 往往是最值得先弄明白的概念之一。sample-monorepo 是一个专门为教学设计的 monorepo 示例项目,它用极简的代码把 npm workspaces 和 TypeScript project references 的核心机制完整展示出来。读完这篇文章,你不仅能看懂 monorepo 是什么,还能亲手跑起来,彻底搞懂

gitblog_01069的博客 715

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

pnpm才是前端工程化项目的未来

pnpm才是前端工程化项目的未来

weixin_65832899的博客 1406

SIMATIC S7-1200, S7-1500 PID 控制[手册]

SIMATIC S7-1200, S7-1500 PID 控制[手册]

3 分钟读懂 sample-monorepo:npm workspaces 多包管理入门完全指南

sample-monorepo 是一个基于 **npm workspaces** 与 **TypeScript project references** 的 monorepo 示例项目,它用极简的代码向你演示了多包仓库的目录组织、依赖管理、构建与测试全流程。无论你是前端新手,还是正在为大型项目拆分代码库的工程师,本文都能帮你快速理解 monorepo 的核心思想,并在 3 分钟内搭建出属于自己的第

gitblog_01044的博客 605

monorepo前端解析,零基础入门到精通,收藏这篇就够了

Monorepo是一种项目开发与管理的策略模式,它代表"单一代码仓库"(在Monorepo模式中,所有相关的项目和组件都被存储在一个统一的代码仓库中,而不是分散在多个独立的代码仓库中,这些项目之间还可能会有依赖关系。npmyarnpnpm等是用来管理项目依赖、发布包、安装依赖的工具,它们都提供了对工作区(workspace)的支持,允许在单个代码库中管理多个项目或包。这种工作区支持在单个代码库中同时开发、测试和管理多个相关的项目,而无需使用多个独立的代码仓库。这些包管理工具与monorepo。

Python_0011的博客 919

前端工程化-基于Monorepo模式下的重构

前端工程化-基于 monorepo 模式下的 lerna 实践

... 603

现代前端工程化-基于 Monorepo 的 lerna 模块(从原理到实战)

本文你能学到什么? 看完本文后希望可以检查一下图中的内容是否都掌握了,文中的例子最好实际操作一下,下面开始正文。本文是前端工程化系列中的一篇,回不断更新,下篇更新内容可看文末的下期预告!宗...

frontend_frank的博客 1734

react-keyframes工程化实战:microbundle、lerna monorepo与canary发布四件套

react-keyframes 是一个让你在 React 中轻松创建**帧动画(frame-based animations)**的轻量组件库:只需几个 `<Frame>` 组件,就能实现逐帧切换的打字机、跑马灯等效果。本文不聊组件用法,而是带你拆解它背后一整套可直接复用的**前端库工程化方案**:microbundle 构建、lerna monorepo 管理、canary 预发布与 stabl

gitblog_00259的博客 828

现代前端工程化-基于 Monorepo 的 lerna

本文你能学到什么? 看完本文后希望可以检查一下图中的内容是否都掌握了,文中的例子最好实际操作一下,下面开始正文。本文是前端工程化系列中的一篇,回不断更新,下篇更新内容可看文末的下期预告!宗...

奇舞周刊 878
上一篇: 【AI Agent】n8n入门 - day1
下一篇: Node写MCP入门教程
疯狂踩坑人
博客等级 码龄9年 12粉丝 17原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值