pnpm 搭建monorepo环境
前言
只是搭建一个用于打包模块的环境,更多细节还需要自己查询资料。
monorepo快速了解
如果有所了解,应该会知道安卓的repo仓库,他是MultiRepo模式,一个git仓库下面会有多个git仓库,通过主仓库的配置命令来批量管理,但是呢,在window上这种模式有着不小的缺陷,甚至没法正常使用。
于是后面又有了一种思路,这个就是Monorepo,它不再分多个git仓库,而是在一个git仓库里面管理多个项目,每个项目有自己的package.json文件,有自己的node_modules目录。
这也会产生一个问题,重复的依赖以及依赖层级过深的问题,于是这个问题就可以通过pnpm包管理器解决,首先它是扁平化管理,并且所有的插件并不是真正的存在在目录下,而是软连接的形式,具体可以去它网站看看。
现代的前端工程已经越来越离不开 Monorepo 了,无论是业务代码还是工具库,越来越多的项目已经采用 Monorepo 的方式来进行开发。Google 宁愿把所有的代码都放在一个 Monorepo 工程下面,Vue 3、Yarn、Npm7 等等知名开源项目的源码也是采用 Monorepo 的方式来进行管理的。
Monorepo目录结构示意:
├── packages
| ├── pkg1
| | ├── package.json
| ├── pkg2
| | ├── package.json
├── package.json
packages为模块的根,旗下会有多个子项目
那么monorepo除了再windows上用还解决了什么问题?
monorepo解决的痛点
1.代码复用
MultiRepo导致各个仓库相互独立,但是有一些逻辑可能会被重复用到,但是仓库独立,为了能省事,你可能会copy一份代码过来,这就导致同一份代码出现在多个仓库中。
如果这个代码出现问题,你就必须修改N份,这就带来了高维护成本,比较好的办法就是做成npm包,但是npm包的更新版本也需要走很多流程,需要测试,你很难保证现在的改动会不会还存在bug。
这种工作区的割裂,导致复用代码的成本很高,开发调试的流程繁琐,甚至在基础库频繁改动的情况下让人感到很抓狂,体验很差。
monorepo因为是一体式的,代码的复用可以非常好的体验,而且不同项目之间可以通过package中的Workspace
协议相互当做插件安装使用
2.版本管理
MultiRepo下,多个仓库可能一开始都依赖于某个1.0版本的插件,但是随着业务的开发,可能有些仓库并不会去频繁更新,如果这个插件发布了一个bug修复版本,或者重大更新版本,有些仓库有可能没有即时更新到,导致一些莫名的报错。
3.项目基建
由于在 MultiRepo 当中,各个项目的工作流是割裂的,因此每个项目需要单独配置开发环境、配置 CI 流程、配置部署发布流程等等,甚至每个项目都有自己单独的一套脚手架工具。
其实,很容易发现这些项目里的很多基建的逻辑都是重复的,如果是 10 个项目,就需要维护 10 份基建的流程,逻辑重复不说,各个项目间存在构建、部署和发布的规范不能统一的情况,这样维护起来就更加麻烦了。
4.工作流的一致性
由于所有的项目放在一个仓库当中,复用起来非常方便,如果有依赖的代码变动,那么用到这个依赖的项目当中会立马感知到。并且所有的项目都是使用最新的代码,不会产生其它项目版本更新不及时的情况。
5.项目基建成本的降低
所有项目复用一套标准的工具和规范,无需切换开发环境,如果有新的项目接入,也可以直接复用已有的基建流程,比如 CI 流程、构建和发布流程。这样只需要很少的人来维护所有项目的基建,维护成本也大大减低。
6.团队协作也更加容易
一方面大家都在一个仓库开发,能够方便地共享和复用代码,方便检索项目源码,另一方面,git commit 的历史记录也支持以功能为单位进行提交,之前对于某个功能的提交,需要改好几个仓库,提交多个 commit,现在只需要提交一次,简化了 commit 记录,方便协作。
pnpm 初始化环境
创建一个目录vue3-receiver
,名字随意,通过pnpm init -f
初始化,成功后会出现package.json
文件。
对这个文件加入一个配置说明:
"private": true, //防止npm发布,因为这个是拿来管理的
创建packages
目录用于存放子项目
创建pnpm-workspace.yaml
文件声明工作空间Workspace
填充以下内容:
packages:
- "packages/**"
此时目录结构如下:
├── packages
├── pnpm-workspace.yaml
├── package.json
创建一些子项目
遵循一下目录结构:
├── 子项目名
├── ├── src
| | ├── index.ts
├── package.json
index.ts里面随便导出一个函数或者变量,如果是函数不能为空,因为到时候打包需要用到rollup,它的Tree-shaking要求内容不能为空。
例子:
//index.ts
export default function() {
console.log("reactivity");
}
package.json也是pnpm init -f
初始化来的,进入到对应的子项目目录运行该命令。
然后给package.json添加一个自定义打包配置,用于告诉打包器rollup怎么打包这个文件。
"buildOptions": {
"name": "VueReactivity",
"formats": [
"esm-bundler",
"cjs",
"global"
]
}
name用于如果是global打包,这个模块将是挂载在window上的,那么它需要一个名字,这个name就是这个属性名。
formats表示打包的格式:
- esm-bundler es6模块
- cjs CommonJS
- global 全局模块
安装typescript
pnpm add typescript -W -D
因为我们此时存在多个package.json文件,所以必须声明安装的插件在哪个层级,-W
为根目录。
安装成功后初始化
npx tsc --init
此时就会有tsconfig.json文件了。
改动两个部分:
{
"module": "ESNEXT", //改为ESNEXT
"sourceMap": true, //开启sourceMap,这个后面rollup打包map文件不开这个会报错
}
创建打包脚本
虽然很想用ts格式,但是原生node目前ts支持一般,而且require
和import
很难共存,所以这里建议还是按照老一套node写js脚本。
pnpm add @rollup/plugin-node-resolve @types/node rollup rollup-plugin-typescript2 execa@5.1.1 -W -D
- rollup 打包器
- rollup-plugin-typescript2 rollup解析ts的插件
- @rollup/plugin-node-resolve rollup解析第三方模块
- @types/node node的ts声明插件
- execa 开启命令行的模块,一定要5.1.1版本,6.0之后是es6模块了,不方便使用了
在根目录创建scripts
文件夹,里面创建build.js
文件。
填充以下内容:
/*
* @Author: mulingyuer
* @Date: 2022-03-28 22:43:20
* @LastEditTime: 2022-03-29 00:06:17
* @LastEditors: mulingyuer
* @Description: rollup打包脚本
* @FilePath: \vue3-receiver\scripts\build.js
* 怎么可能会有bug!!!
*/
//node来解析目录
const fs = require("fs");
const path = require("path");
const execa = require("execa");
//模块根目录
const packagesDir = path.resolve(__dirname, "../packages");
//模块目录
const dirs = fs.readdirSync(packagesDir).filter(dirPath => {
const isDir = fs.statSync(path.resolve(packagesDir, dirPath)).isDirectory(); //判断是否为目录,过滤掉packages下非目录文件
return isDir;
});
//打包
async function build(dir) {
//execa运行一次会创建一次进程
await execa("rollup", ["-c", "--environment", `TARGET:${dir}`], {
stdio: "inherit"
});
}
//并行打包
async function runParallel(dirs) {
const result = [];
//for循环遍历所有模块目录,触发多个build方法
for (const dir of dirs) {
result.push(build(dir));
}
return Promise.all(result);
};
runParallel(dirs, build).then(() => {
console.log("打包成功");
})
逻辑很简单,拿到packages
目录下的所有模块目录,然后创建多个打包进程,execa
运行rollup打包器,传入一些命令:
-c
表示使用配置文件--environment
表示传入环境变量,后面接环境变量TARGET:${dir}
具体的环境变量值,值为模块的路径
此时打包器会读取根目录下的rollup.config.js
文件,为此我们还需要创建这个文件。
rollup.config.js 打包器配置脚本
/*
* @Author: mulingyuer
* @Date: 2022-03-28 23:33:13
* @LastEditTime: 2022-03-29 00:23:30
* @LastEditors: mulingyuer
* @Description: rollup打包配置
* @FilePath: \vue3-receiver\rollup.config.js
* 怎么可能会有bug!!!
*/
import ts from "rollup-plugin-typescript2"; //解析ts插件
import resolvePlugin from "@rollup/plugin-node-resolve"; //解析第三方模块
import path from "path";
// 目录
const packagesDir = path.resolve(__dirname, "./packages");
const packageDir = path.resolve(packagesDir, process.env.TARGET); //模块对应的目录:packages+环境变量
//通用获取模块目录下package.json的配置
const packagePath = path.resolve(packageDir, "package.json");
const pkg = require(packagePath);
//模块的打包配置属性
const buildOptions = pkg.buildOptions;
//模块名:路径地址最后一个/xxx
const name = path.basename(packageDir);
//不同打包格式配置
const ouputConfig = {
"esm-bundler": {
file: path.resolve(packageDir, `dist/${name}.esm-bundler.js`), //打包到模块目录下的dist目录下
format: "es",
},
"cjs": {
file: path.resolve(packageDir, `dist/${name}.cjs.js`),
format: "cjs",
},
"global": {
file: path.resolve(packageDir, `dist/${name}.global.js`),
format: "iife",
}
};
//获取对应的打包配置
function getBuildConfig(format) {
const output = ouputConfig[format];
//如果是iife全局打包,output必须要有name属性,这个name就是window上挂载的变量名
output.name = buildOptions.name;
//是否生成map文件
output.sourcemap = true;
return {
input: path.resolve(packageDir, `src/index.ts`), //模块打包的入口
output,
plugins: [
ts({
tsconfig: path.resolve(__dirname, "./tsconfig.json"), //打包ts时所使用的tsconfig配置文件
}),
resolvePlugin(), //解析第三方模块
]
}
}
export default buildOptions.formats.map(format => getBuildConfig(format))
这里的逻辑也很简单,拿到模块对应的package.json中的buildOptions
配置,根据这个配置生成不同的配置对象,如果要打包多个格式,export default 抛出的是一个数组,数组里有对应的配置对象。
创建一个开发用的脚本
在scripts目录下创建dev.js
脚本
/*
* @Author: mulingyuer
* @Date: 2022-03-29 00:27:22
* @LastEditTime: 2022-03-29 00:30:06
* @LastEditors: mulingyuer
* @Description: 开发打包
* @FilePath: \vue3-receiver\scripts\dev.js
* 怎么可能会有bug!!!
*/
const execa = require("execa");
//打包
async function build(dir) {
//execa运行一次会创建一次进程
await execa("rollup", ["-cw", "--environment", `TARGET:${dir}`], {
stdio: "inherit"
});
}
build("reactivity");
这个就用于指定某些模块打包,方便开发使用
根package.json增加打包命令
"scripts": {
"dev": "node scripts/dev.js",
"build": "node scripts/build.js"
},
到这,打包流程基本完成。
可以提交到git仓库了。
本文系作者 @木灵鱼儿 原创发布在木灵鱼儿站点。未经许可,禁止转载。
全部评论 1
lzzz
Google Chrome MacOS