pnpm + changesets 搭建 monorepo 架构的前端监控系统

7,793 阅读8分钟

现在越来越多的前端工程都选择 monorepo 的架构进行开发,比如 Vue、React、Babel 等项目都采用 monorepo 的方式进行管理

monorepo 的组织结构如下:

├── packages
|   ├── pkg1
|   |   ├── package.json
|   ├── pkg2
|   |   ├── package.json
├── package.json

vuejs 所示,所有子项目都在 packages 目录中

image.png

本篇作为 web-see 前端监控的架构篇,主要聊聊如何使用 monorepo,以及带来的好处有哪些?

关于前端监控系统的知识点,这里推荐笔者的 如何从0到1搭建前端监控平台,文中有详细的介绍

下面先聊聊 monorepo 架构的优势以及如何搭建、如何发布

monorepo 的简单介绍

简单来说,monorepo 就是把多个子工程放到一个 git 仓库中进行管理,各工程之间共用同一套构建流程、代码规范,各工程可以使用link软链接的方式实现相互引用,方便版本的统一管理

monorepo 架构的优势:

1、可以将一个大型项目,拆分成多个子项目,更容易维护和管理代码

2、提高代码共享和重用性,这些子项目可以共享代码和库,可以减少代码重复,降低维护成本

3、由于所有代码都在同一个代码库中,可以更容易地对代码进行构建和测试,有利于持续集成和持续交付

4、更方便的进行版本控制和管理,可以结合 changesets 类似的发布工具,跟踪代码的变更历史和版本变更

pnpm + workspace 搭建 monorepo 项目

pnpm 提出了 workspace 的概念,内置了对 monorepo 的支持,可以用来快速搭建项目

以下 pnpm-monorepo-changesets 示例的仓库地址,感兴趣的小伙伴可以动手试试

1、安装 pnpm

npm install -g pnpm

2、初始化项目

pnpm init

在根目录下存在 pnpm-workspace.yaml 文件,用来指定工作空间的目录

packages:
  - 'packages/*'

3、创建 packages 目录

在 packages 目录下创建 pkg1 和 pkg2 两个文件(代表两个子工程),分别执行 pnpm init 命令,初始化工程

在 pkg1 和 pkg2 的 src 目录下创建 index.ts 文件,作为项目的入口文件

image.png

// pkg1/src/index.ts

export function pk1(): any {
  console.log('I am pk1');
}
// pkg2/src/index.ts

import {pk1} from '@websee/pk1'

function pk2() {
  pk1()
  console.log('I am pk2');
}
export default pk2;

4、修改 pkg1 和 pkg2 中 package.jsonname 属性

分别将 name 修改为 @websee/pk1@websee/pk2,这里的 @websee 是在 npm 官网上创建的组件名

注意: 这个组织名一定要提前创建好,否则各工程相互引用时会报错

image.png

5、修改 pkg1 和 pkg2 package.json 中的 main 属性

main 属性为该工程的入口文件,默认为 "main": "index.js", 修改为 "main": "src/index.ts",并添加 publishConfig 属性

最终 package.json 如下

{
  "name": "@websee/pk1",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.ts",
  "publishConfig": {
    "main": "dist/index.js"
  },
  "files": [
    "dist"
  ],
  "type": "module",
  "author": "海阔天空",
  "license": "ISC",
  "dependencies": {}
}

5、各工程间相互引用

要在 pk2 中使用 pk1 的代码,传统的写法如下

// pkg2/src/index.ts
import {pk1} from '../../pk1/src'

这种相对路径的写法很繁琐且不易维护,如果当某一工程的目录结构发生变化时,其他所有引用该工程的文件都要修改

pnpm 通过 workspace 的实现,可以通过直接引用子工程的 name 名称,就可以实现各工程的相互引用,代码如下

// pkg2/src/index.ts
import {pk1} from '@websee/pk1'

pnpm 提供了 --filter 参数,可以用来对特定的 package 进行操作

pkg1 中将 pkg2 作为依赖进行安装,在根目录下执行

pnpm install @websee/pk1 --filter @websee/pk2

此时查看 pkg2 的 package.json,可以看到 dependencies 字段自动添加了 pk1 的引用,证明相互引用添加成功

// pkg2/package.json
"dependencies": {
  "@websee/pk1": "workspace:^1.0.0"
}

6、打包验证

这里使用 rollup 打包,安装依赖,pnpm 提供了 -w 参数,可以将依赖包安装到工程的根目录下,作为所有 package 的公共依赖

pnpm install rollup@2.78.0 rollup-plugin-typescript2@0.34.1 typescript@4.9.4 -wD 

创建rollup.config.js

import fs from 'fs';
import path from 'path';
import typescript from 'rollup-plugin-typescript2';
const packagesDir = path.resolve(__dirname, 'packages');
const packageFiles = fs.readdirSync(packagesDir);
function output(path) {
  return [
    {
      input: [`./packages/${path}/src/index.ts`],
      output: [
        {
          file: `./packages/${path}/dist/index.js`,
          format: 'umd',
          name: 'web-see',
          sourcemap: true
        }
      ],
      plugins: [
        typescript({
          tsconfigOverride: {
            compilerOptions: {
              module: 'ESNext'
            }
          },
          useTsconfigDeclarationDir: true
        })
      ]
    }
  ];
}

export default [...packageFiles.map((path) => output(path)).flat()];

rollup.config.js 会读取 packages 文件中各子目录的名称,并将每一个目录设置成打包的入口文件,并配置对应的出口路径

在根目录 package.json 中配置打包命令

"scripts": {
   "build": "rollup -c"
 }

执行 pnpm run build,会在 packages 各目录下生成对应的 dist 文件

changesets

changesets 用来进行版本控制和管理

1、安装依赖

pnpm install @changesets/cli -wD

2、初始化

pnpm changeset init

执行完初始化命令后,会在工程的根目录下生成 .changeset 目录

3、在根目录 package.json 中配置对应的命令

"scripts": {
    "build": "rollup -c",
    "changeset": "changeset",
    "packages-version": "changeset version",
    "publish": "changeset publish --registry=https://registry.npmjs.com/"
}

下面用两个具体的例子,来演示下 changeset 的发包流程

注意:npm 包一般的版本结构为:1.0.0,类似这样的三位数版本号,分别是对应的 changeset version 里面的:majorminorpatch

npm包版本 1.0.0 更新为 1.1.0

这里 @websee/pk1@websee/pk2 的初始版本都为 1.0.0

执行 pnpm run changeset

1、选择要发布的包

image.png

2、发布 minor,选择对应的包

现在是 1.0.0 更新为 1.1.0,这里选择 minor

image.png

3、填写 changelog

image.png

4、Is this your desired changeset 选择true

image.png

执行 pnpm run packages-version

image.png

提示 All files have been updated

打开 pk1 和 pk2 下的 package.json,发现版本号已修改完成

image.png

同时各目录下会自动生成 CHANGELOG.md 文件,记录版本号的变化

# @websee/pk1

## 1.1.0

### Minor Changes

- 1.1.0

执行 pnpm run publish

发布 1.1.0 版本

image.png

在 npm 官网上搜索 @websee/pk1,证明发布成功

image.png

npm包 1.1.0 更新为 2.1.0

继续发布 2.1.0 版本,

执行 pnpm run changeset

不同点在于选择发布 major,剩余的流程和上面的都一样

image.png

为何要使用 monorepo 架构搭建前端监控

目前 web-see 前端监控 SDK,主要功能有代码报错、性能检测、页面录屏、记录用户行为、白屏检测等功能

老版本存在的主要问题有: test 分支

1、这些功能的代码全部耦合在一起,随着SDK功能的增多,体积越来越大,打包后的体积为 147K

image.png

2、有些用户用不到某些功能,不希望加载该插件,以减少SDK体积

3、用户想要自定义扩展非常不方便

使用 monorepo 架构改造后的结果: main 分支

1、将 SDK 主要拆分为3个项目

  • @websee/core 核心模块:包含代码报错、记录用户行为、白屏检测等功能,体积为 41K
  • @websee/performance 性能检测模块,体积为 26K
  • @websee/recordscreen 页面录屏模块,体积为 116K

2、用户可以根据自己的需求,选择项的安装性能检测与页面录屏模块

3、用户想要自定义扩展其他功能,可以继续在 packages 添加新的模块,并且模块间相互引用更加方便快捷

image.png

当前的 packages 目录

├── packages
|   ├── common // 公共变量
|   |   ├── package.json
|   ├── core // 核心模块
|   |   ├── package.json
|   ├── performance // 性能检测
|   |   ├── package.json
|   ├── recordscreen // 页面录屏
|   |   ├── package.json
|   |── types // ts类型
|   |   ├── package.json
|   |── utils // 公共方法
|   |   ├── package.json
├── package.json

SDK 架构设计

SDK 为支持插件 可拓展可插拔的特点,整体架构是 内核 + 插件 + 发布订阅模式 的设计

1、@websee/core 核心模块主要是内核 + 发布订阅模式

// handlers 存储所有的事件和对应的回调函数
const handlers: { [key in EVENTTYPES]?: ReplaceCallback[] } = {};
// subscribeEvent 设置标识,并将处理的方法放置到handlers中,如{ xhr: [ funtion ] }
export function subscribeEvent(handler: ReplaceHandler): boolean {
  if (!handler || getFlag(handler.type)) return false;
  setFlag(handler.type, true);
  handlers[handler.type] = handlers[handler.type] || [];
  handlers[handler.type]?.push(handler.callback);
  return true;
}
export function notify(type: EVENTTYPES, data?: any): void {
  if (!type || !handlers[type]) return;
  // 获取对应事件的回调函数并执行,回调函数为addReplaceHandler事件中定义的事件
  handlers[type]?.forEach(callback => {
    nativeTryCatch(
      () => {
        callback(data);
      },
      (e: any) => {
        console.error(`web-see 重写事件notify的回调函数发生错误,Type:${type} ${e}`);
      }
    );
  });
}

2、@websee/performance@websee/recordscreen 插件都继承于BasePlugin

export abstract class BasePlugin {
  public type: string; // 插件类型
  constructor(type: string) {
    this.type = type;
  }
  abstract bindOptions(options: object): void; // 校验参数
  abstract core(sdkBase: SdkBase): void; // 核心方法
  abstract transform(data: any): void; // 数据转化
}

3、通过调用 @websee/coreuse方法来注册插件

function use(plugin: any, option: any) {
  const instance = new plugin(option);
  if (
    !subscribeEvent({
      callback: data => {
        instance.transform(data);
      },
      type: instance.type,
    })
  ) return;
  nativeTryCatch(() => {
    // 执行插件的core方法
    instance.core({ transportData, breadcrumb, options, notify });
  });
}

SDK 安装说明

以下为 vue2 的安装说明

import webSee from '@websee/core';
import performance from '@websee/performance';
import recordscreen from '@websee/recordscreen';

Vue.use(webSee, {
  dsn: 'http://test.com/reportData',
  apikey: 'abcd',
  silentWhiteScreen: true, // 开启白屏检测
  skeletonProject: true, // 页面包含骨架屏
  repeatCodeError: true, // 开启错误上报去重,重复的代码错误只上报一次
  userId: '123',
  handleHttpStatus(data) { // (自定义 hook) 根据接口返回的 response 判断请求是否正确
    let { url, response } = data;
    let { code } = typeof response === 'string' ? JSON.parse(response) : response;
    if (url.includes('/getErrorList')) {
      return code === 200 ? true : false;
    } else {
      return true;
    }
  }
});

// 注册性能检测插件
webSee.use(performance);
// 注册页面录屏插件
webSee.use(recordscreen);

最后通过 changesets 来管理各个模块的版本,统一发布

总结

本文通过 web-see 前端监控实际的案例,来讲解采用 monorepo 架构的好处以及它解决的实际问题

有兴趣的小伙伴可以结合git仓库的源码和本文一起阅读,帮助加深理解

后续

下一篇会继续讨论前端监控,聊一聊前端监控的报警机制

参考文章:
pnpm + workspace + changesets 构建你的 monorepo 工程
腾讯三面:说说前端监控平台/监控SDK的架构设计和难点亮点?

文章系列

文章系列地址:github.com/xy-sea/blog

文中如有错误或不严谨的地方,请给予指正,十分感谢。如果喜欢或有所启发,欢迎 star