MonoRepo工程化
传统架构
- 独立项目结构:每个项目作为独立的单元开发、维护和部署
- 技术栈独立:不同的项目可能使用不同的技术栈和工具链
- 依赖管理:每个项目都有独立的 node_modules 和依赖配置
- 部署策略:各自独立部署和上线,通常依赖 CI/CD 工具
MenoRepo 架构
- 单一代码仓库:将多个相关项目(子包)集中在同一个代码仓库中进行管理
- 统一管理依赖:通过工具(如 PNPM、Lerna、Turborepo 等)实现依赖的统一安装和版本管理
- 共享代码:通过工作空间或内部包机制共享公共模块
- 统一构建和发布:可以对多个子包进行一次性构建和发布
MonoRepo 项目搭建
项目配置
在根目录下创建pnpm-workspace.yaml
文件
packages:
- "apps/*"
- "packages/*"
通过 pnpm
初始化项目,指定参数 --workspace-root
表示其去找文件所在位置作为根目录
pnpm --workspace-root init
锁定版本
在根目录下的package.json
中锁定依赖版本
{
"engines": {
"node": ">=22.0.0",
"npm": ">=10.0.0",
"pnpm": ">=10.0.0"
}
}
并且添加.npmrc
文件,用于启用引擎版本严格检查,若版本不匹配:npm 会直接抛出错误并终止操作,而非仅显示警告
engine-strict=true
此时当我降低node版本进行安装依赖进行测试,会直接报错
PS D:\mygit\monorepo> pnpm i -Dw typescript
ERR_PNPM_BAD_PM_VERSION This project is configured to use v10.18.1 of pnpm. Your current pnpm is v9.1.4
If you want to bypass this version check, you can set the "package-manager-strict" configuration to "false" or set the "COREPACK_ENABLE_STRICT" environment variable to "0"
TypeScript 配置
安装TypeScript 及其类型定义,其中w是--workspace-root
的缩写,用于指定在根目录下安装依赖
pnpm -Dw add typescript @types/node
统一管理
在根目录下新建一个tsconfig.json
文件,并添加以下内容:
{
"compilerOptions": {
"baseUrl": ".",
"module": "esnext",
"target": "esnext",
"types": [],
"lib": [
"esnext"
],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"verbatimModuleSyntax": true,
"moduleResolution": "bundler",
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
},
"exclude": [
"node_modules",
"dist"
]
}
具体管理
对于具体的子包,在其子包的根目录下创建tsconfig.json
文件,进行自定义配置
代码风格与质量检查
prettier
先安装prettier prettier 官方文档
pnpm -Dw add prettier
在根目录下添加.prettier.config.js
文件【prettier配置文件】和.prettierignore
文件【prettier忽略文件】,最后在package.json
当中添加执行的命令,执行pnpm run lint:prettier
即可格式化所有文件
export default {
// 换行长度
printWidth: 120,
// 缩进长度
tabWidth: 2,
// 使用制表符,而不是空格缩进
useTabs: false,
// 结尾用分号
semi: true,
// 单引号
singleQuote: true,
// 在对象字面量中决定是否将属性名用引号括起来
quoteProps: 'as-needed',
// 在jsx中使用双引号
jsxSingleQuote: false,
// 多行时尽可能打印尾括号
trailingComma: 'none',
// 使用分号
bracketSpacing: true,
// 在对象字面量中,是否将括号放在同一行
bracketSameLine: false,
// 箭头函数的参数是否用括号括起来
arrowParens: 'avoid',
// 指定要使用的解析器
requirePragma: false,
// 是否在文件顶部插入 @format 注释
insertPragma: false,
// 用于控制换行的行为
proseWrap: 'preserve',
htmlWhitespaceSensitivity: 'css',
// 控制vue单文件组件中的script和style标签是否缩进
vueIndentScriptAndStyle: false,
// 换行符使用if结尾
endOfLine: 'auto',
// 用于格式化以给定字符偏移量
rangeStart: 0,
rangeEnd: Infinity
};
dist
node_modules
.local
public
pnpm-lock.yaml
{
"scripts": {
"lint:prettier": "prettier --write \"**/*.{js,jsx,ts,tsx,vue,html,css,scss,md}\""
}
}
ESLint
安装依赖 eslint 官方文档
pnpm -Dw add eslint@latest @eslint/js globals typescript-eslint eslint-plugin-prettier eslint-config-prettier eslint-plugin-vue
类别 | 库名 |
---|---|
核心引擎 | eslint |
官方规则集 | @eslint/js |
全局变量支持 | globals |
TypeScript支持 | typescript-eslint |
Prettier集成 | eslint-plugin-prettier,eslint-config-prettier |
Vue.js 支持 | eslint-plugin-vue |
随后添加eslint.config.js
文件
import {defineConfig} from 'eslint/config';
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
import eslintConfigPrettier from 'eslint-config-prettier';
import eslintPluginPrettier from 'eslint-plugin-prettier';
import eslintPluginVue from 'eslint-plugin-vue';
import globals from 'globals';
// 忽略的文件
const ignores = [
'**/dist/**',
'**/node_modules/**',
'.*',
'scripts/**',
'**/*.d.ts'
];
export default defineConfig(
// 通用配置
{
ignores,
extends: [eslint.configs.recommended, ...tseslint.configs.recommended, eslintConfigPrettier],
plugins: {prettier: eslintPluginPrettier},
languageOptions: {
ecmaVersion: 'latest', // ECMAScript 版本
sourceType: 'module', // 模块化类型
parser: tseslint.parser // 解析器
},
rules: {
// 自定义规则
// 禁止使用 var
'no-var': 'error'
}
},
// 前端配置
{
ignores,
files: [
'app/frontend/**/*.{js,jsx,ts,tsx,vue}',
'packages/components/**/*.{js,jsx,ts,tsx,vue}'
],
extends: [
...eslintPluginVue.configs['flat/recommended'],
eslintConfigPrettier
],
languageOptions: {
globals: {
...globals.browser
}
}
},
// 后端配置
{
ignores,
files: ['apps/backend/**/*.{js,jsx,ts,tsx}'],
languageOptions: {
globals: {
...globals.node
}
}
}
);
随后执行命令pnpm eslint
,在前面的配置中,我们已经定义了no-var
规则,所以我用var定义一个变量之后看效果
PS D:\xxxx\monorepo> pnpm eslint
D:\xxxx\monorepo\apps\frontend\src\index.ts
2:1 error Unexpected var, use let or const instead no-var
拼写检查
还是先安装依赖:cspell 官方文档
pnpm -Dw add cspell @cspell/dict-lorem-ipsum
添加cspell.config.json
文件,在配置文件当中配置了./.cspell/custom-dictionary.txt
文件,这个文件用来存放自定义的字典。同时在package.json
文件中添加了cspell
命令,执行pnpm cspell
命令,即可检查所有文件的拼写错误。
{
"import": [
"@cspell/dict-lorem-ipsum/cspell-ext.json"
],
"caseSensitive": false,
"dictionaries": [
"custom-dictionary"
],
"dictionaryDefinitions": [
{
"name": "custom-dictionary",
"path": "./.cspell/custom-dictionary.txt",
"addWords": true
}
],
"ignorePaths": [
"node_modules/**",
"dist/**"
]
}
{
"scripts": {
"lint:spell": "cspell \"(packages|apps)/**/*.{js,jsx,ts,tsx,vue,html,css,scss,md}\""
}
}
此时我在ts代码当中故意写一个phoness
这个变量,运行pnpm cspell
命令,可以看到有错误提示。
> monorepo@1.0.0 lint:spell
> cspell "(packages|apps)/**/*.{js,jsx,ts,tsx,vue,html,css,scss,md}"
1/4 apps/backend/src/index.ts 523.64ms X
apps/backend/src/index.ts:2:7 - Unknown word (phoness)
CSpell: Files checked: 4, Issues found: 1 in 1 file.
Git提交规范
commitizen
先安装对应依赖 commitlint 官方文档
pnpm -Dw add @commitlint/cli @commitlint/config-conventional commitizen cz-git
@commitlint/cli
:是commitlint
工具的核心@commitlint/config-conventional
:用于定义conventional commit的规范commitizen
:提供了一个交互式撰写commit信息的插件cz-git
:是国人开发的一款工具,工程性更强,自定义更高,交互性更好
添加commitlint.config.js
配置文件,同时添加package.json
文件中的commit
命令,之后当执行该命令的时候,会进入交互式命令行,根据提示填写信息,生成符合conventional commit
规范的提交信息。在webstrom
或者vscode
安装通义灵码的插件在提交的时候可以点一下让其自动生成提交内容,也比较符合conventional commit
规范。
export default {
extends: ['@commitlint/config-conventional'],
// https://commitlint.js.org/reference/rules.html 所有的规则
rules: {
// 正文以空行开头
'body-leading-blank': [2, 'always'],
// 页脚以空行开头
'footer-leading-blank': [1, 'always'],
// 标题最大长度为 100 个字符
'header-max-length': [2, 'always', 100],
// 标题不能为空
'subject-empty': [2, 'never'],
// 类型不能为空
'type-empty': [2, 'never'],
// 标题必须为小写
'subject-case': [0],
// 类型必须为以下值之一
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复 bug
'docs', // 文档变更
'style', // 代码格式(不影响代码运行的变动)
'refactor', // 重构(即不是新增功能,也不是修复 bug 的代码变动)
'perf', // 性能优化
'test', // 增加测试
'chore', // 构建过程或辅助工具的变动
'revert' // 回退到上一个版本
]
]
},
prompt: {
// 禁用提交时自动打开浏览器
enableBrowser: false,
types: [
{value: 'feat', name: '新功能✨'},
{value: 'fix', name: '修复bug🐛'},
{value: 'docs', name: '文档变更📚'},
{value: 'style', name: '代码格式💄'},
{value: 'refactor', name: '重构♻️'},
{value: 'perf', name: '性能优化⚡️'},
{value: 'test', name: '增加测试✅'},
{value: 'build', name: '构建系统📦'},
{value: 'ci', name: '持续集成🔧'},
{value: 'chore', name: '构建过程或辅助工具的变动'},
{value: 'revert', name: '回退⏪'}
],
scopes: ['root', 'backend', 'frontend', 'components', 'utils'],
allowCustomScopes: true,
skipQuestions: ['body', 'footerPrefix', 'footer', 'breaking'],
message: {
type: '请选择提交类型:',
scope: '请选择本次提交所影响的范围:',
subject: '请输入本次提交的描述:',
body: '请输入本次提交的详细描述(可选):',
footerPrefix: '请输入本次提交所关联的issue前缀(可选):',
footer: '请输入本次提交所关联的issue(可选):',
confirmCommit: '确认提交?'
}
}
};
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}
husky
安装依赖同时初始化:husky 官方文档
pnpm -Dw add husky
pnpm husky init
在执行了初始化命令后会出现.husky/pre-commit
文件,在前面对prettier、eslint、cspell进行了命名行配置,所以修改这个文件的内容,表示在提交之前执行这三个检查,只有当三个检查都通过了之后才能进行commit
pnpm lint:prettier && pnpm lint:eslint && pnpm lint:spell
lint-staged
安装lint-staged lint-staged 官方文档
pnpm -Dw add lint-staged
添加配置文件.lintstagedrc.js
,同时添加package.json
文件配置lint-staged
命令
- 配置文件的键表示为这些文件类型,值表示为执行哪些命令
- 在
package.json
文件中配置precommit
命令,表示在提交之前执行lint-staged
命令,其中用git-cz
提交时,会自动执行pre-commit
命令
export default {
'**/*.{js,jsx,ts,tsx,vue,html,css,scss,md}': ['cspell lint'],
'*.{js,ts,vue,md}': ['prettier --write', 'eslint']
};
{
"scripts": {
"commit": "git-cz",
"precommit": "lint-staged"
}
}
统一打包
用rollup来进行统一打包,rollup 官方文档
pnpm -Dw add rollup
建立包依赖
以vue前端项目要使用utils工具包项目的依赖为例,只需要对utils进行打包,打包之后在vue项目的package.json
文件中添加如下依赖,其中@monorepo/utils
表示当前项目下的utils模块,workspace:*
表示当前项目下的utils模块的最新版本
{
"dependencies": {
"vue": "^3.5.22",
"@monorepo/utils": "workspace:*"
}
}
统一测试
- jest
- vitest
- mocha
vitest
这里就是用vitest来进行测试,先安装依赖;vitest 官方文档,需要先安装一下vite,其中vite注意一下版本和node版本的对应关系
pnpm -Dw add vite@6.0.0 vitest @vitest/browser vitest-browser-vue vue
添加一个vitest的配置文件vitest.config.js
。这个暂时先放着,还没完整的研究完vitest
import {defineConfig} from 'vitest/config';
// vitest配置 https://cn.vitest.dev/config/
export default defineConfig({
test: {
// 匹配排除测试文件的 glob 规则
exclude: [],
// 匹配包含测试文件的 glob 规则
include: ['**/*.{test,spec}.?(c|m)[jt]s?(x)']
}
});
最后在子包当中创建__tests__
目录,用来存放测试文件,贴两个测试文件的代码
import { expect, it, test } from 'vitest';
import { add, minus } from '../src/math.ts';
test('adds 1 + 2 to equal 3', () => {
expect(add(1, 2)).toBe(3);
});
test('subtracts 2 - 1 to equal 1', () => {
expect(minus(2, 1)).toBe(1);
});
// 多线程并发
it.concurrent('subtracts 100 - 100 to equal 0', () => {
expect(minus(100, 100)).toBe(0);
expect(add(100, 100)).toBe(200);
expect(minus(100, 100)).toBe(0);
expect(add(100, 100)).toBe(200);
expect(minus(200, 100)).toBe(100);
});
import { expect, test } from 'vitest';
import { hello } from '../src/string';
test('hello world', () => {
expect(hello()).toBe('hello world');
});