Monorepo 管理项目(以Vue为例)


Monorepo 是管理项目代码的一个方式,指在一个项目仓库(repo)中管理多个模块/包(package)。 Vue3源码采用 monorepo 方式进行管理,将模块拆分到package目录中。

  • 一个仓库可维护多个模块,不用到处找仓库
  • 方便版本管理和依赖管理,模块之间的引用,调用都非常方便

搭建Monorepo环境

Vue3中使用pnpm workspace来实现monorepo (pnpm是快速、节省磁盘空间的包管理器。主要采用符号链接的方式管理模块)

全局安装pnpm

npm install pnpm -g # 全局安装pnpm
pnpm init # 初始化配置文件

创建.npmrc文件

shamefully-hoist = true # 将模块所依赖的包提升到node_modules中

这里您可以尝试一下安装Vue3, pnpm install vue@next此时默认情况下vue3中依赖的模块不会被提升到node_modules下。 添加羞耻的提升可以将Vue3,所依赖的模块提升到node_modules

配置workspace

新建 pnpm-workspace.yaml

packages:
  - 'packages/*'

将packages下所有的目录都作为包进行管理。这样我们的Monorepo就搭建好了。确实比lerna + yarn workspace更快捷

环境搭建

打包项目Vue3采用rollup进行打包代码,安装打包所需要的依赖

依赖
typescript 在项目中支持Typescript
rollup 打包工具
rollup-plugin-typescript2 rollup 和 ts的 桥梁
@rollup/plugin-json 支持引入json
@rollup/plugin-node-resolve 解析node第三方模块
@rollup/plugin-commonjs 将CommonJS转化为ES6Module
minimist 命令行参数解析
execa@4 开启子进程
pnpm install typescript rollup rollup-plugin-typescript2 @rollup/plugin-json @rollup/plugin-node-resolve @rollup/plugin-commonjs minimist execa@4 esbuild@0.15.18   -D -w

初始化TS

pnpm tsc --init

先添加些常用的ts-config配置,后续需要其他的在继续增加

  • tsconfig.json
{
  "compilerOptions": {
    "outDir": "dist", // 输出的目录
    "sourceMap": true, // 采用sourcemap
    "target": "es2016", // 目标语法
    "module": "esnext", // 模块格式
    "moduleResolution": "node", // 模块解析方式
    "strict": false, // 严格模式
    "resolveJsonModule": true, // 解析json模块
    "esModuleInterop": true, // 允许通过es6语法引入commonjs模块
    "jsx": "preserve", // jsx 不转义
    "lib": ["esnext", "dom"], // 支持的类库 esnext及dom
  }
}

创建模块

我们现在packages目录下新建两个package

  • reactivity 响应式模块
  • shared 共享模块

所有包的入口均为src/index.ts 这样可以实现统一打包

  • 每个包下执行pnpm init

  • reactivity/package.json

{
  "name": "@vue/reactivity",
  "version": "1.0.0",
  "main": "index.js",
  "module":"dist/reactivity.esm-bundler.js",
  "unpkg": "dist/reactivity.global.js",
  "buildOptions": {
    "name": "VueReactivity",
    "formats": [
      "esm-bundler",
      "cjs",
      "global"
    ]
  }
}

shared/package.json

{
    "name": "@vue/shared",
    "version": "1.0.0",
    "main": "index.js",
    "module": "dist/shared.esm-bundler.js",
    "buildOptions": {
        "formats": [
            "esm-bundler",
            "cjs"
        ]
    }
}

formats为自定义的打包格式,有esm-bundler在构建工具中使用的格式、esm-browser在浏览器中使用的格式、cjs在node中使用的格式、global立即执行函数的格式

配置ts引用关系

  • tsconfig.json

    
    {
      "compilerOptions": {
        "baseUrl": "./", //以当前项目为根目录
        "paths": {
          "@vue/*":["packages/*/src"]  //目录匹配
        }
      }
    }

独立安装依赖

pnpm install @vue/shared@workspace --filter @vue/reactivity

这行代码是一个 pnpm 命令,用于在指定工作区中安装 @vue/reactivity 的依赖。以下是各参数的解释:

  • pnpm: 包管理器 pnpm 的命令行工具。
  • install: 安装依赖。
  • @vue/shared@workspace: 安装 @vue/shared 的工作区版本。
  • --filter @vue/reactivity: 过滤出 @vue/reactivity 的依赖并安装。

换句话说,这个命令将在 @vue/shared 的工作区中,只安装 @vue/reactivity 的依赖。

开发环境esbuild打包

创建开发时执行脚本, 参数为要打包的模块

解析用户参数

{
  "scripts": {
    "dev": "node scripts/dev.js reactivity -f global",
     "build": "node scripts/build.js"
	}
}
  • scripts/dev.js
const { build } = require("esbuild")
const { resolve } = require("path")
const args = require("minimist")(process.argv.slice(2))

const target = args._[0] || "reactivity"
const format = args.f || "global"

const pkg = require(resolve(__dirname, `../packages/${target}/package.json`))

const outputFormat = format.startsWith("global") // 输出的格式
  ? "iife"
  : format === "cjs"
  ? "cjs"
  : "esm"

const outfile = resolve(
  // 输出的文件
  __dirname,
  // `../packages/${target}/dist/${target}.${format}.js`
  `../packages/${target}/dist/${target}.js`
)

build({
  entryPoints: [resolve(__dirname, `../packages/${target}/src/index.ts`)],
  outfile,
  bundle: true,
  sourcemap: true,
  format: outputFormat,
  globalName: pkg.buildOptions?.name,
  platform: format === "cjs" ? "node" : "browser",
  watch: {
    // 监控文件变化
    onRebuild(error) {
      if (!error) console.log(`rebuilt~~~~`)
    },
  },
}).then(() => {
  console.log("watching~~~")
})

生产环境rollup打包

rollup.config.js

const path = require("path")
// 获取packages目录
const packagesDir = path.resolve(__dirname, "packages")
// 获取对应的模块
const packageDir = path.resolve(packagesDir, process.env.TARGET)
// 全部以打包目录来解析文件
const resolve = (p) => path.resolve(packageDir, p)
const pkg = require(resolve("package.json"))
const name = path.basename(packageDir) // 获取包的名字

// 配置打包信息
const outputConfigs = {
  "esm-bundler": {
    file: resolve(`dist/${name}.esm-bundler.js`),
    format: "es",
  },
  cjs: {
    file: resolve(`dist/${name}.cjs.js`),
    format: "cjs",
  },
  global: {
    file: resolve(`dist/${name}.global.js`),
    format: "iife",
  },
}
// 获取formats
const packageFormats = process.env.FORMATS && process.env.FORMATS.split(",")
const packageConfigs = packageFormats || pkg.buildOptions.formats

const json = require("@rollup/plugin-json")
const commonjs = require("@rollup/plugin-commonjs")
const { nodeResolve } = require("@rollup/plugin-node-resolve")
const tsPlugin = require("rollup-plugin-typescript2")

function createConfig(format, output) {
  output.sourcemap = process.env.SOURCE_MAP
  output.exports = "named"
  let external = []
  if (format === "global") {
    output.name = pkg.buildOptions.name
  } else {
    // cjs/esm 不需要打包依赖文件
    external = [...Object.keys(pkg.dependencies || {})]
  }
  return {
    input: resolve("src/index.ts"),
    output,
    external,
    plugins: [json(), tsPlugin(), commonjs(), nodeResolve()],
  }
}
// 开始打包把
module.exports = packageConfigs.map((format) =>
  createConfig(format, outputConfigs[format])
)

srcipts/build.js

const fs = require("fs")
const execa = require("execa")
debugger
const targets = fs.readdirSync("packages").filter((f) => {
  if (!fs.statSync(`packages/${f}`).isDirectory()) {
    return false
  }
  return true
})
async function runParallel(source, iteratorFn) {
  const ret = []
  for (const item of source) {
    const p = Promise.resolve().then(() => iteratorFn(item))
    ret.push(p)
  }
  return Promise.all(ret)
}
async function build(target) {
  await execa("rollup", ["-c", "--environment", `TARGET:${target}`], {
    stdio: "inherit",
  })
}
runParallel(targets, build)

文章作者: 高红翔
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 高红翔 !
  目录