Monorepo
约 1471 字大约 5 分钟
2025-12-23
基本概念
Monorepo(单一代码库)指把多个相关项目或包放在同一个仓库中统一管理。它强调共享依赖与配置、统一的构建与发布流程,便于跨包协作、代码复用与一致性治理,同时也需要更完善的依赖管理与构建工具来控制规模与效率
在 Monorepo 管理中常用的工具包括 pnpm、yarn、Lerna 等,通常会采用如下的目录组织方式
docs文档类型的包
apps通常放可独立运行/部署的应用项目
backend
frontend
mobile
web
packages通常放可被复用的库/组件/工具包
cli
release.sh
api
user.ts
hooks
useUserFetcher.ts
i18n
locales
zh.json
en.json
utils
request.ts
components
Input.vue
Form.vue
Table.vue
package.json
pnpm-workspace.yaml
基本使用
如果要用 pnpm 进行工程管理,那么首选需要创建 pnpm-workspace.yaml
touch pnpm-workspace.yaml该文件主要用来定义工作区范围
packages:
- "packages/*"
- "apps/*"
- "docs/*"常见的 monorepo 命令
pnpm -w install在根目录安装并链接所有工作区依赖pnpm -w init在工作区根目录初始化package.jsonpnpm -r run build在所有包中执行 build 脚本pnpm -F <pkg> dev在指定包中执行脚本(如pnpm -F web dev)pnpm add <dep> -w把依赖添加到根工作区pnpm add <dep> -F <pkg>把依赖添加到指定包pnpm list -r查看所有包的依赖树
环境版本锁定
为避免不同开发机/CI 的 Node 与 pnpm 版本不一致,通常会在仓库中固定版本:
- 在
package.json声明engines与packageManager - 在
.nvmrc或.node-version固定 Node 版本 - 在
.npmrc声明engine-strict=true来严格校验engines的版本信息
{
"engines": {
"node": ">=18 <21"
},
"packageManager": "pnpm@8.15.0"
}engine-strict=true18.19.0通用配置
使用 TypeScript
安装 TypeScript
pnpm i -D -w typescript @types/node初始化
tsconfig.jsonnpx tsc --init特殊的子包配置
在不同子包下可以有自己单独的配置
{ "extends": "../../tsconfig.json", "compilerOptions": { "lib": ["ESNext", "DOM"], "include": ["src"] } }
Prettier
安装 prettier
pnpm i -D -w prettier创建
prettier.config.js/** * @type {import('prettier').Config} * @see https://www.prettier.cn/docs/options */ export default { // 指定最大换行长度 printWidth: 130, // 缩进制表符宽度 | 空格数 tabWidth: 2, // 使用制表符而不是空格缩进行 (true:制表符,false:空格) useTabs: false, // 结尾不用分号 (true:有,false:没有) semi: true, // 使用单引号 (true:单引号,false:双引号) singleQuote: true, // 在对象字面量中决定是否将属性名用引号括起来 可选值 "<as-needed|consistent|preserve>" quoteProps: "as-needed", // 在JSX中使用单引号而不是双引号 (true:单引号,false:双引号) jsxSingleQuote: false, // 多行时尽可能打印尾随逗号 可选值"<none|es5|all>" trailingComma: "none", // 在对象,数组括号与文字之间加空格 "{ foo: bar }" (true:有,false:没有) bracketSpacing: true, // 将 > 多行元素放在最后一行的末尾,而不是单独放在下一行 (true:放末尾,false:单独一行) bracketSameLine: false, // (x) => {} 箭头函数参数只有一个时是否要有小括号 (avoid:省略括号,always:不省略括号) arrowParens: "avoid", // 指定要使用的解析器,不需要写文件开头的 @prettier requirePragma: false, // 可以在文件顶部插入一个特殊标记,指定该文件已使用 Prettier 格式化 insertPragma: false, // 用于控制文本是否应该被换行以及如何进行换行 proseWrap: "preserve", // 在html中空格是否是敏感的 "css" - 遵守 CSS 显示属性的默认值, "strict" - 空格被认为是敏感的 ,"ignore" - 空格被认为是不敏感的 htmlWhitespaceSensitivity: "css", // 控制在 Vue 单文件组件中 <script> 和 <style> 标签内的代码缩进方式 vueIndentScriptAndStyle: false, // 换行符使用 lf 结尾是 可选值 "<auto|lf|crlf|cr>" endOfLine: "auto", // 这两个选项可用于格式化以给定字符偏移量(分别包括和不包括)开始和结束的代码 (rangeStart:开始,rangeEnd:结束) rangeStart: 0, rangeEnd: Infinity };创建
.prettierignoredist public .local node_modules pnpm-lock.yamlprettier脚本"scripts": { "lint:prettier": "prettier --write \"**/*.{js,ts,json,tsx,css,vue,html,md}\"" }
ESlint
安装 ESLint 及其相关依赖
pnpm i -D -w eslint @eslint/js globals pnpm i -D -w typescript-eslint eslint-plugin-vue eslint-config-prettier创建
eslint.config.jsimport js from "@eslint/js"; import globals from "globals"; import tseslint from "typescript-eslint"; import vue from "eslint-plugin-vue"; import eslintConfigPrettier from "eslint-config-prettier"; const baseFiles = [ "apps/frontend/**/*.{js,ts,tsx,vue}", "packages/**/*.{js,ts,tsx,vue}" ]; const tsFiles = ["apps/frontend/**/*.{ts,tsx}", "packages/**/*.{ts,tsx}"]; const vueFiles = ["apps/frontend/**/*.vue", "packages/**/*.vue"]; export default [ { ignores: ["dist", "public", ".local", "node_modules", "pnpm-lock.yaml"] }, { files: baseFiles, languageOptions: { globals: { ...globals.browser, ...globals.node } } }, { ...js.configs.recommended, files: baseFiles }, ...tseslint.configs.recommended.map(config => ({ ...config, files: baseFiles })), { files: tsFiles, rules: { "@typescript-eslint/naming-convention": [ "error", { selector: "typeLike", format: ["PascalCase"] }, { selector: "enumMember", format: ["PascalCase"] }, { selector: "variable", format: ["camelCase", "UPPER_CASE"], leadingUnderscore: "allow" }, { selector: "function", format: ["camelCase"] }, { selector: "parameter", format: ["camelCase"], leadingUnderscore: "allow" }, { selector: "default", format: ["camelCase"], leadingUnderscore: "allow" } ] } }, { ...vue.configs["flat/recommended"], files: vueFiles }, { files: vueFiles, languageOptions: { parserOptions: { parser: tseslint.parser } }, rules: { "vue/attributes-order": [ "error", { order: [ "DEFINITION", "LIST_RENDERING", "CONDITIONALS", "RENDER_MODIFIERS", "GLOBAL", "UNIQUE", "SLOT", "TWO_WAY_BINDING", "OTHER_DIRECTIVES", "OTHER_ATTR", "EVENTS", "CONTENT" ], alphabetical: false } ] } }, { ...eslintConfigPrettier, files: baseFiles } ];eslint脚本"scripts": { "lint:eslint": "eslint .", "lint:eslint:fix": "eslint . --fix" }
git 提交规范
commitizen
commitizen 用于交互式生成提交信息
安装 commitizen 与适配器
pnpm i -D -w commitizen cz-conventional-changelog在
package.json配置 commitizen"config": { "commitizen": { "path": "cz-conventional-changelog" } }, "scripts": { "commit": "cz" }使用交互式提交
pnpm commit
commitlint
commitlint 用于校验校验提交信息是否符合规范
安装 commitlint
pnpm i -D -w @commitlint/cli @commitlint/config-conventional创建
commitlint.config.jsexport default { extends: ["@commitlint/config-conventional"] };添加
.husky/commit-msgpnpm exec commitlint --edit "$1"
husky
安装 husky
pnpm i -D -w husky初始化 Git Hooks
pnpm exec husky init修改
.husky/pre-commitpnpm lint-staged
lint-staged
安装 lint-staged
pnpm i -D -w lint-staged创建
lint-staged.config.jsexport default { "{apps/frontend,packages}/**/*.{js,ts,tsx,vue}": [ "eslint --fix", "prettier --write" ], "{apps/frontend,packages}/**/*.{json,css,less,scss,md,html}": [ "prettier --write" ] };lint-staged脚本"scripts": { "lint-staged": "lint-staged" }