前言

只是搭建一个用于打包模块的环境,更多细节还需要自己查询资料。

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支持一般,而且requireimport很难共存,所以这里建议还是按照老一套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仓库了。

分类: 前端基础建设与架构 标签: pnpmmonoreporollup

评论

全部评论 1

  1. lzzz
    lzzz
    Google Chrome MacOS
    monorepo workspace 互相依赖,要怎么打包

目录