monorepo的理解以及简单实现

17,867 阅读3分钟

概览

monorepo(monolithic repository)是一种项目架构,简单的来说:一个仓库内包含多个开发项目(模块,包)。对于前端项目:vue3、element都是采用的这种架构模式。接下来,着重以下几个点来展开:

  1. monorepo常见表现形式
  2. monorepo的优缺点
  3. monorepo的简单实现(rollup实现)。代码地址

monorepo常见表现形式

image.png

image.png 以上是element(vue2版本)和vue3的源码目录结构。这两个项目下都有一个packages目录,对于element来说目录下得每一个模块都是一个组件,vue3将其各个模块拆分到packages下面,每一个模块都是一个项目(你会发现每个模块下都有一个package.json文件)

monorepo的优缺点

对于monorepo的优点以及发展历史可以参考精读《Monorepo 的优势》。总结一下:

  1. 各模块独立方便管理(对于element来说,修改表单只需要修改packages下的form目录)
  2. 结构清晰(模块独立之后,结构自然清晰)
  3. 缺点就是仓库代码体积可能比较大(一个仓库包含多个项目,项目多了,体积自然会大)

monorepo的简单实现

讲完一些原理的东西,是时候常见的打包工具(webpack、rollup)来实现一个monorepo了,感受一下他的好处。实现一个monorepo需要注意一下几个点:

  1. 包管理工具必须使用yarn(原因后面会提)
  2. dev(开发)和build(打包所有)的实现
  3. 模块之间通信

项目介绍

接下来我们就模拟Vue3。使用rollup+ts。搭建两个模块:reactive(模块)和shared(公共模块)。目录结构如下

.
├── README.md
├── package.json
├── scripts
|   ├── build.js
|   └── dev.js
└── packges
    ├── reactivity
    │   └── src
    |   └── package.json
    └── shared
        ├── src
        └── package.json

image.png

总项目的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为打包类型:

  1. cjs ==》 commonJS(module.exports)
  2. esm-bundler ==> (import)
  3. global ==> (iife立即执行函数,暴露全局变量)

build.js打包脚本实现

build.js读取packages下的模块,生成rollup执行命令,并行打包(依赖execa)所有模块。

  1. 读取packages所有模块
  2. 遍历模块,并行打包
// 把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执行的命令如下:

  1. rollup -c --environment TARGET:reactivity
  2. 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]))