头顶头,手把手实现 Vue3 使用 Monorepo 方式管理项目代码

2,832 阅读2分钟

Monorepo 是什么?

Monorepo 是管理项目代码的一种方式,指在一个仓库中管理多个模块/包

优点:

  • 一个仓库可以同时维护多个模块,不用拆分多个仓库
  • 方便版本管理和依赖管理,模块之间互相引用,调用方便

缺点:

  • 因为多个模块/包共存,仓库体积会比较大

仿照Vue3的代码结构,手写一套

搭建项目结构

  1. 创建项目目录
mkdir vue3-monorepo && cd vue3-monorepo
yarn init -y
​
mkdir packages && cd packages
​
mkdir reactivity
mkdir shared
  1. 修改 package.json
{
  "name": "vue3-monorepo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
+  "private": true,
+  "workspaces":[
+    "packages/*"
+  ]
}
  1. init reactivity shared
cd reactivity && yarn init -y
cd shared && yarn init -y
  1. 创建 reactivity shared的目录
vue3-monorepo
|---package.json
---packages
     ---reactivity
     |   |   package.json
     |   |   
+    |   ---src
+    |           index.ts           
     ---shared
         |   package.json
+        ---src
+                index.ts
  1. reactivity shared 的入口文件 index.ts 增加点代码
// reactivity/src/index.ts
+ const Reactivity = {}
+ export { Reactivity }
// shared/src/index.ts
+ const Shared = {}
+ export { Shared }

搭建构建环境

  1. 安装依赖
包名描述
typescript支持ts
rollup打包工具
rollup-plugin-typescript2rollup和ts的桥梁
@rollup/plugin-node-resolve解析node的第三方模块
@rollup/plugin-json支持引入json文件
execa开启子进程
# --ignore-workspace-root-check or -W 允许在根目录下安装依赖
yarn add typescript rollup rollup-plugin-typescript2 @rollup/plugin-node-resolve @rollup/plugin-json execa --ignore-workspace-root-check
  1. 初始化 typescript 配置
# 在根目录(vue3-monorepo)下执行
# 前面已经安装过 typescript,所以在项目的 node_modules/.bin 目录下会有 tsc,此时就是调用这个命令生成 tsconfig.json 文件
npx tsc --init
vue3-monorepo
  |---node_modules
  |---packages
  |---package.json
+ ---tsconfig.json
// tsconfig.json

{
"target": "ESNext",
"module": "ESNext",
"baseUrl": "./",
    "moduleResolution": "node",
    "paths": {
      "@vue/*":["packages/*/src"]
    }
}
  1. 修改reactivity shared目录下的 package.json 配置模块/包名以及打包选项
# reactivity/package.json
​
{
+   "name": "@vue/reactivity",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
+   "module": "dist/reactivity.esm-bundler.js", // ESModule的入口
+   "buildOptions": { // 自定义字段,用来构建配置
+       "name": "VueReactivity",
+       "formats": [
+           "esm-bundler",
+           "cjs",
+           "global"
+       ]
+   }
}
# shared/package.json
{
+	"name": "@vue/shared",
	"version": "1.0.0",
	"main": "index.js",
	"license": "MIT",
+	"module": "dist/reactivity.esm-bundler.js", // ESModule的入口
+	"buildOptions": { // 自定义字段,用来构建配置
+		"name": "VueShared",
+		"formats": [
+			"esm-bundler",
+			"cjs",
+		]
+	}
}

修改完成后,在根目录执行 yarn install ,然后查看 node_modules 下会有一个 @vue 目录,下面有 reactivity shared 两个软链,如果没有,刷新文件目录

  1. 增加命令
# vue3-monorepo/package.json

{
  "name": "vue3-monorepo",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "private": true,
  "workspaces": [
    "packages/*"
  ],
+  "scripts": {
+    "dev": "node scripts/dev.js",
+    "build": "node scripts/build.js"
+  },
  "dependencies": {}
}
  1. 创建脚本文件
// vue3-monorepo/scripts/build.js

// 把packages 目录下的所有包进行打包
const fs = require('fs') // node 文件模块
const execa = require('execa') // 开启子线程

// 读取 packages 目录,并排除不是目录的文件
const targets = fs.readdirSync('packages').filter(f => fs.statSync(`packages/${f}`).isDirectory())

async function build(targets) {
  await execa('rollup', ['-c', '--environment', `TARGET:${targets}`], {
    stdio: 'inherit'
  })
}

// 遍历packages下的目录,并执行build
function runParallel(targets, iteratorFn) {
  const res = []
  for (const iterator of targets) {
    const p = iteratorFn(iterator)
    res.push(p)
  }
  return Promise.all(res)
}

runParallel(targets, build)
// vue3-monorepo/scripts/dev.js

// 只针对具体的某个包
const fs = require('fs')
const execa = require('execa')

const target = 'reactivity' // 自定义要开发的包名

async function build(target) {
  await execa('rollup', ['-cw', '--environment', `TARGET:${target}`], {
    stdio: 'inherit'
  })
}

build(target)
  1. 创建 rollup 配置文件
// vue3-monorepo/rollup.config.js

import path from 'path'
import json from '@rollup/plugin-json'
import ts from 'rollup-plugin-typescript2'
import resolvePlugin from '@rollup/plugin-node-resolve'

const packageName = process.env.TARGET // 脚本文件中build函数传递过来的自定义环境变量
const packagesPath = path.resolve(__dirname, 'packages') // packages path
const packageDirPath = path.resolve(packagesPath, packageName) // packages 每个包的 path
const resolve = p => path.resolve(packageDirPath, p)
const packageJSON = require(resolve('package.json'))

const outputConfig = {
  'esm-bundler': {
    file: resolve(`dist/${packageName}.esm-bundler.js`),
    format: 'es'
  },
  cjs: {
    file: resolve(`dist/${packageName}.cjs.js`),
    format: 'cjs'
  },
  global: {
    file: resolve(`dist/${packageName}.global.js`),
    format: 'iife'
  }
}

const buildOptions = packageJSON.buildOptions

function createConfig(format, output) {
  output.name = buildOptions.name
  output.sourcemap = buildOptions.sourcemap

  return {
    input: resolve(`src/index.ts`),
    output,
    plugins: [
      json(),
      ts({
        tsconfig: path.resolve(__dirname, 'tsconfig.json')
      }),
      resolvePlugin()
    ]
  }
}

export default buildOptions.formats.map(format => createConfig(format, outputConfig[format]))
  1. 执行打包构建
yarn run build # 同时构建packages下所有模块

yarn run dev # 单独构建指定的模块