概览
monorepo(monolithic repository)是一种项目架构,简单的来说:一个仓库内包含多个开发项目(模块,包)。对于前端项目:vue3、element都是采用的这种架构模式。接下来,着重以下几个点来展开:
- monorepo常见表现形式
- monorepo的优缺点
- monorepo的简单实现(rollup实现)。代码地址
monorepo常见表现形式
以上是element(vue2版本)和vue3的源码目录结构。这两个项目下都有一个packages目录,对于element来说目录下得每一个模块都是一个组件,vue3将其各个模块拆分到packages下面,每一个模块都是一个项目(你会发现每个模块下都有一个package.json文件)
monorepo的优缺点
对于monorepo的优点以及发展历史可以参考精读《Monorepo 的优势》。总结一下:
- 各模块独立方便管理(对于element来说,修改表单只需要修改packages下的form目录)
- 结构清晰(模块独立之后,结构自然清晰)
- 缺点就是仓库代码体积可能比较大(一个仓库包含多个项目,项目多了,体积自然会大)
monorepo的简单实现
讲完一些原理的东西,是时候常见的打包工具(webpack、rollup)来实现一个monorepo了,感受一下他的好处。实现一个monorepo需要注意一下几个点:
- 包管理工具必须使用yarn(原因后面会提)
- dev(开发)和build(打包所有)的实现
- 模块之间通信
项目介绍
接下来我们就模拟Vue3。使用rollup+ts。搭建两个模块:reactive(模块)和shared(公共模块)。目录结构如下
.
├── README.md
├── package.json
├── scripts
| ├── build.js
| └── dev.js
└── packges
├── reactivity
│ └── src
| └── package.json
└── shared
├── src
└── package.json
总项目的packages配置
{
"private": "true",
"name": "monorepo-stu",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"workspaces": [
"packages/*"
]
}
workspaces要配置packages/*,使用yarn install的时候,yarn会将package的所有包设置软连接到node_modules,这样就使用了各个模块的互相通信。通常会设置一个公共模块,一些公共方法放入此包,其他各个包独立
子模块packages配置
{
"name": "reactivity",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"buildOptions": {
// 暴露全局变量的名字
"name": "Reactivity",
// 打包的类型
"formats": ["cjs", "esm-bundler", "global"]
}
}
buildOptions是我们自定义打包配置,name为暴露全局变量的名称。formats为打包类型:
- cjs ==》 commonJS(module.exports)
- esm-bundler ==> (import)
- global ==> (iife立即执行函数,暴露全局变量)
build.js打包脚本实现
build.js读取packages下的模块,生成rollup执行命令,并行打包(依赖execa)所有模块。
- 读取packages所有模块
- 遍历模块,并行打包
// 把packages目录下得所有包都进行打包
// 找到packages下得所有模块
const fs = require('fs')
const path = require('path')
// 开启子进程,进行打包, 最终使用rollup进行打包
// 必须用5.1.1版本及以下, v6只支持import导入
const execa = require('execa')
// 读取packages下得目录,得忽略掉文件
const targets = fs.readdirSync(path.resolve(process.cwd(), './packages')).filter(f => fs.statSync(`packages/${f}`).isDirectory())
// 对目标进行依次打包,并且并行打包
async function build (target) {
// rollup -c -environment TARGET:shared
await execa('rollup', ['-c' , '--environment' ,`TARGET:${target}`],
{stdio: 'inherit'} // 子进程打包信息共享给父进程
)
}
function runParallel(targets, iteratorFn) {
const res = []
for(const item of targets) {
const p = iteratorFn(item)
res.push(p)
}
return Promise.all(res)
}
runParallel(targets, build)
build.js执行的命令如下:
- rollup -c --environment TARGET:reactivity
- rollup -c --environment TARGET:shared
dev.js打包脚本实现
// 打特定的包
// 必须用5.1.1版本及以下, v6只支持import导入
const execa = require('execa')
// 特定的package名称
const target = 'reactivity'
build(target)
// 对目标进行依次打包,并且并行打包
async function build (target) {
// rollup -cw -environment TARGET:shared 持续编译打包
await execa('rollup', ['-cw' , '--environment' ,`TARGET:${target}`],
{stdio: 'inherit'} // 子进程打包信息共享给父进程
)
}
dev.js比build.js更方便了,不要多进程打包。命令由rollup -c --environment TARGET:shared变为rollup -cw --environment TARGET:shared,达到持续打包的目的
rollup.config.js
上一步我们执行了不同的rollup打包命令,此文件的目的是:根据命令的环境变量生成相应的rollup打包配置。
import ts from 'rollup-plugin-typescript2' // 解析ts插件
import { nodeResolve } from '@rollup/plugin-node-resolve' // 解析第三方模块
import path from 'path'
// 解析json,这里主要用于解析package.json
import json from '@rollup/plugin-json'
// import serve from 'rollup-plugin-serve'
const packagesDir = path.resolve(__dirname, 'packages')
// 打包的基准目录
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// // 根据packages基准目录,拼接path
const resolve = (p) => path.resolve(packageDir, p)
const pkg = require(resolve('package.json'))
const name = path.basename(packageDir)
// 对打包类型 做一个映射表, 根据提供的formats 格式化打包内容
const outputConfig = {
'esm-bundler': {
file: resolve(`dist/${name}.esm-bundler.js`),
format: 'es' // esm
},
'cjs': {
file: resolve(`dist/${name}.cjs.js`),
format: 'cjs' // commonjs
},
'global': {
file: resolve(`dist/${name}.global.js`),
format: 'iife' // 立即执行函数
}
}
// 获取package中的buildOptions, 按需打包
const options = pkg.buildOptions
function createConfig(format, output) {
output.name = options.name
output.sourcemap = true
// 生成rollup配置
return {
input: resolve('src/index.ts'),
output,
plugins: [
json(),
ts({
tsconfig: path.resolve(__dirname, 'tsconfig.json')
}),
nodeResolve({ // 解析第三方文件
extensions: ['.js', '.ts']
}),
]
}
}
// 导出生成的rollup配置
export default options.formats.map(format => createConfig(format, outputConfig[format]))