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)