使用pnpm-workspace创建一个monorepo
最近学了一下monorepo相关的东西, 在这里写一下.
monorepo是用来解决多项目管理的一个方案. 一般的话我们有三个方案:
- monorepo 大仓 由多个独立的项目构成, 统一放在一个仓库里
- Monolithic repo巨石应用 论规模也是大仓, 包含一个巨大的应用, 模块间几乎没有隔离
- Multirepo 多仓 每个项目都在自己的仓库里
monorepo的文件夹结构大概是这样的:
- apps 用来存放对外展示或者内部使用的网站, 里面的app被称为子包
- packages 用来存放内部使用或者对外使用的包, 里面的package也被称为子包
- package.json 工作区的package.json, 一般会配合pnpm使用
- tsconfig.base.json 统一的ts配置, 子包的tsconfig会继承这个配置
- 其他配置如eslint, cz, commitlint等, 统一代码规范, 不用每个团队每个项目都配置一次
安装pnpm
npm install -g pnpm
pnpm的workspace跟monorepo很搭, npm和yarn虽然也有类似workspace, 不过貌似没有pnpm好的样子.
初始化项目
package.json
可以直接使用pnpm init
来创建这样一个package.json, 不过认识一下package.json也挺好的.
在根目录新建一个package.json
{
"name": "water-frontend",
"type": "module",
"version": "1.0.0",
"description": "学习monorepo",
"author": "water2027",
"license": "MIT"
}
大概这样.
创建workspace
在根目录新建一个pnpm-workspace.yaml来配置哪些东西在工作区里.
packages:
- 'apps/*'
- 'packages/*'
这表示apps和packages里的都在工作区中, 都是子包.
现在的话, monorepo其实已经差不多成型了. 可以试着创建子包了.
在packages新建一个文件夹, 这里叫utils
. 在utils里新建package.json, 可以使用pnpm init
新建.
对于package.json中的名字, 一般是@xxx/package, xxx是大仓(package.json里的)的名字, packge是包名. 这里的话是@water-frontend/utils
.
在utils里新建一个index.js, 然后写入
export function add(x, y) {
return x + y
}
这样utils包就导出了一个函数add了. 之后可以在app里导入, 其他package也可以导入.
在apps中新建一个app, 随便叫什么, 这里叫study
.
在study文件夹中新建package.json, 可以使用pnpm init
新建, 也可以自己写, 注意一下包名.
在study文件夹新建一个index.js, 然后写入
import { add } from '@water-frontend/utils'
function main() {
console.log(add(1, 2))
}
main()
这时候会发现找不到这个包, 这是因为还没有安装到app里.
在app中, 执行
pnpm add @water-frontend/utils --workspace
--workspace表示从工作区里的包安装, 如果不加的话会从网络里获取. 安装好后就可以运行了.
进一步的配置
typescript
先安装依赖
pnpm add -Dw typescript ts-node @types/node
这个-Dw是-D和-w的组合, -w表示在工作区安装. 如果不加这个-w的话就是在当前目录安装.
然后在根目录新建一个tsconfig.json和tsconfig.base.json. 这个tsconfig.base.json命名没什么所谓, 也可以是其他的, 只要子包的tsconfig.json正确扩展就可以了.
在tsconfig.base.json中写入
{
"compilerOptions": {
// 启用增量编译,只编译发生变化的文件,提高编译速度
"incremental": true,
// 启用项目引用功能,允许TypeScript项目引用其他TypeScript项目
"composite": true,
// 允许导入JSON文件作为模块
"resolveJsonModule": true,
// 启用所有严格类型检查选项
"strict": true,
// 生成对应的.d.ts声明文件
"declaration": true,
// 为声明文件生成source map文件
"declarationMap": true,
// 只生成声明文件,不生成JavaScript文件
"emitDeclarationOnly": true,
// 当有类型错误时不生成输出文件
"noEmitOnError": true,
// 允许从没有默认导出的模块中默认导入
"allowSyntheticDefaultImports": true,
// 启用ES模块与CommonJS模块的互操作性
"esModuleInterop": true,
// 跳过库文件的类型检查,提高编译性能
"skipLibCheck": true
}
}
具体可以查看文档.
在根目录的tsconfig.json中写入
{
"extends": "./tsconfig.base.json"
}
然后的话就是配置子包的tsconfig了.
在apps/study和packages/utils里都新建一个tsconfig.json, 写入
{
"extends": "../../tsconfig.base.json"
}
子包可以自行按需扩展.
可以继续在package.json里写scripts方便使用, ts-node, tsc之类的. 这里就不展示了.
ESLint和prettier
这两个的话我一般会用antfu的配置. 可以根据个人需求配置.
在项目根目录执行
pnpm dlx @antfu/eslint-config@latest
然后按需选用就配置好了. 这个配置的话跟在普通项目中配置没有区别.
可以在scripts配置一下, 方便使用.
{
"scripts": {
"lint": "eslint ./ --ext .ts,.tsx,.js,.jsx,.vue",
"lint:fix": "eslint ./ --ext .ts,.tsx,.js,.jsx,.vue --fix"
}
}
husky
git hooks, 可以配置各种钩子. 比较常用的可能是pre-commit和commit-msg了.
先安装husky
pnpm add -Dw husky
然后初始化
pnpm exec husky init
之后直接在编辑器里编辑.husky里的东西就可以了. 比如需要一个pre-commit, 那么在.husky文件夹下新建一个pre-commit, 然后进行编辑.
在pre-commit写入
pnpm lint:fix
这样commit前就会运行这个命令了. 还有其它钩子可以使用, 配置也是一样.
在commit-msg写入
echo "$1"
这个$1在这里是临时commit消息文件的路径. 没有其他参数了.
lint-staged
如果每次都全量运行eslint太慢了, 如果可以只对暂存的文件eslint检查就好了.
据说oxc会快很多, 不过跟eslint有一些兼容性问题, 我还没尝试过.
安装lint-staged
pnpm add -Dw lint-staged
然后新建一个lint-staged.config.js, 写入
export default {
'*.ts': 'eslint',
}
这个表示所有的.ts文件都传给eslint. 比如暂存区中有a.json, b.ts, c.ts, 那么就会执行
eslint b.ts c.ts
还支持其它配置, 比如!*.ts之类的, 具体可以查看文档.
有了这个之后就可以不全量eslint了. 在pre-commit写入
npx lint-staged
commitlint
这个是用来规范提交信息的.
安装
pnpm add -Dw @commitlint/cli @commitlint/config-conventional
后面那个是快捷配置, 也可以根据需要自行编写.
新建commitlint.config.mjs, 写入
export default {
extends: ["@commitlint/config-conventional"],
rules: {
// ...
}
}
然后在.husky里新建一个钩子: commit-msg
, 写入
npx --no -- commitlint --edit "$1"
这样之后commit就会检查格式是否正确了.
cz
这个是方便开发者提交代码的工具, 确保每次提交都是符合规范的(至少格式是规范的).
安装
pnpm add -D -w commitizen cz-conventional-changelog
然后新建.czrc, 写入
{
"path": "cz-conventional-changelog"
}
可以在根目录的package.json里加一个脚本
{
"cz": "cz"
}
之后使用pnpm cz
就可以提交代码了.
编辑器的配置
如果大家都是用vscode的话, 还可以配置.vscode, 其它编辑器也有类似的配置文件.
新建.vscode文件夹, 新建settings.json, 也有其它的配置文件.
{
// Disable the default formatter, use eslint instead
"prettier.enable": false,
"editor.formatOnSave": false,
// Auto fix
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.organizeImports": "never"
},
// Silent the stylistic rules in you IDE, but still auto fix them
"eslint.rules.customizations": [
{ "rule": "style/*", "severity": "off", "fixable": true },
{ "rule": "format/*", "severity": "off", "fixable": true },
{ "rule": "*-indent", "severity": "off", "fixable": true },
{ "rule": "*-spacing", "severity": "off", "fixable": true },
{ "rule": "*-spaces", "severity": "off", "fixable": true },
{ "rule": "*-order", "severity": "off", "fixable": true },
{ "rule": "*-dangle", "severity": "off", "fixable": true },
{ "rule": "*-newline", "severity": "off", "fixable": true },
{ "rule": "*quotes", "severity": "off", "fixable": true },
{ "rule": "*semi", "severity": "off", "fixable": true }
],
// Enable eslint for all supported languages
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact",
"vue",
"html",
"markdown",
"json",
"json5",
"jsonc",
"yaml",
"toml",
"xml",
"gql",
"graphql",
"astro",
"svelte",
"css",
"less",
"scss",
"pcss",
"postcss"
]
}
这是antfu的eslint自动生成的配置, 可以让eslint完成格式化代码.
还可以配置extensions.json之类的, 帮助团队成员快速安装扩展.
CI
关于这一部分, 我其实还不太了解. 对于一个个人开发者来说这个好像有点远, 甚至monorepo也有点远.
如果每一次推送或者更新都要全量编译代码的话, 那代价太大了. 如果某个子包没有修改就不编译就好了.
这个可以通过配置来实现, 比如计算出一个值之类的, 如果值不变那么不编译.
有一些monorepo脚手架可以帮助配置, 还提供了一些额外的功能比如依赖关系分析, 任务编排等. nx和turborepo可能是比较热门的选择, 功能全面且强大, 不过我没有实际使用过, 刚接触monorepo.
参考资料
可以看看我的仓库