monorepo 埋点 SDK 项目实践

388 阅读7分钟

前言

前面文章中介绍了深入使用 PerformanceObserver前端错误监控sdk初步实践等文章,假设现在有一个需求,我们想要实现一个埋点 SDK,会涉及到很多模块。例如:性能监控模块、用户信息模块、错误监控模块等。这些模块其实都可以作为一个单独的包独立使用。

本文我们来聊聊如何使用 pnpm 构建 monorepo 项目,,使用 vitest 进行项目测试,以及如何使用 rollup 打包。但不涉及具体的 SDK 实现细节,这个后续会单独出一篇。

monorepo

如果你想将监控相关的SDK设计成多个可独立使用的模块,同时保持一定的代码组织和共享逻辑,你可以采用一种称为“monorepo”(单仓库多包)的开发模式。这样,你可以将不同的功能模块(如monitor-coremonitor-sharedweb-performance)放在同一个仓库中,但每个模块都有自己的package.json文件,以便于独立发布和管理。

/monitor-sdk
  ├── packages
  │   ├── monitor-core
  │   │   ├── src
  │   │   │   └── index.js
  │   │   ├── test
  │   │   │   └── index.test.js
  │   │   ├── README.md
  │   │   └── package.json
  │   └── web-performance
  │       ├── src
  │       │   └── index.js
  │       ├── test
  │       │   └── index.test.js
  │       ├── README.md
  │       └── package.json
  ├── scripts
  │   └── build.sh
  ├── LICENSE
  ├── README.md
  └── package.json

在这个结构中:

  • /packages目录下包含所有子模块。
  • 每个子模块有自己的src目录存放源码,test目录存放测试代码,以及一个package.json文件。
  • /packages之外的package.json文件是整个 monorepo 的顶层配置文件,你可以在这里指定一些通用的开发依赖(如构建工具、测试框架等),或者定义一些跨模块的脚本。

pnpm

接下来来看下如何使用 pnpm 来初始化一个 monorepo 项目。

创建 coreweb-performance 两个子包如下图。 image.png

图1

pnpm 操作

为指定子包下安装依赖

pnpm --filter web-performance add lodash
  • --filter web-performance: 这个选项指定了要操作的子包名称。在这里,web-performance 是子包的名称,表示只对名为 web-performance 的子包进行操作。
  • add: 这是用于添加依赖项的命令。
  • lodash: 这是要安装的依赖项的名称。

添加内部模块之间的互相依赖。

pnpm --filter core add -S web-performance
  • --filter core: 这个选项指定了要操作的子包名称。在这里,core 是子包的名称,表示只对名为 core 的子包进行操作。
  • add: 这是用于添加依赖项的命令。
  • -S: 这个选项表示将依赖项安装到 dependencies, -D 表示按照到 devDependencies
  • web-performance: 这是要安装的依赖项的名称。

其他常用命令

#安装软件包及其依赖的任何软件包 如果workspace有配置会优先从workspace安装
pnpm add <pkg>
#安装项目所有依赖
pnpm install
#更新软件包的最新版本
pnpm update
#移除项目依赖
pnpm remove
#运行脚本
pnpm 脚本
#创建一个 package.json 文件
pnpm init
#以一个树形结构输出所有的已安装package的版本及其依赖
pnpm list

vitest 测试

在 core 包中添加内部 web-performance 子包依赖后,可以通过 vitest 测试相关功能。

{
  "name": "core",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "test": "vitest"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "web-performance": "workspace:^"
  }
}

测试用例写在 core/test 包内添加测试代码。 image.png

图2

在根目录下添加 vitest 依赖,直接开箱即用,运行 pnpm test 运行测试代码。当我们代码发生变更,测试用例也会自动热更新重新执行,非常方便。

image.png

图3

image.png

图4

为什么使用 pnpm

在了解 pnpm 基础的使用后,来看看使用 pnpm 的优势。

高效使用磁盘

不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink,它们指向的是同一个文件。

硬连接

假设您有 100 个项目都依赖 lodash,并且您使用 pnpm 进行管理。在这种情况下:

  1. lodash 只会被安装一次,并存储在全局的 .pnpm 目录中。
  2. 每个项目中的 node_modules 目录下的 lodash 都是一个指向 .pnpm 目录中 lodash 的硬链接。
  3. 如果您删除其中一个项目中的 lodash 硬链接,这不会影响其他项目中的 lodash 硬链接。
  4. 只有当所有项目中的 lodash 硬链接都被删除,并且没有其他项目依赖 lodash 时,pnpm 才会从 .pnpm 目录中移除 lodash

安全性高

使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。
在 pnpm 中每个依赖各自维护了自己的 node_modules 形成嵌套的结构。

image.png

pnpm-test\node_modules.pnpm\express@4.19.2\node_modules\bytes 是软连接,真正的源文件在 pnpm-test\node_modules.pnpm\bytes@3.1.2

刪除.pnpm 下的依赖文件夹,再次打开 express@4.19.2\node_modules 依赖就会报错

rollup

Rollup 是一个用于 JavaScript 的模块打包工具,它将小的代码片段编译成更大、更复杂的代码,例如库或应用程序。它使用 JavaScript 的 ES6 版本中包含的新标准化代码模块格式,而不是以前的 CommonJS 和 AMD 等特殊解决方案。

多个子包打包

由于我们这里使用的是 monorepo , 需要对所有子包进行打包。

方式一,单文入口打包

  1. 在各自的子包中都添加对应的 rollup.config.mjs 文件。
  2. 在 package.json 中都定义 build script
  3. 在根目录下添加 rollup 依赖,这样不需要在每个子目录下添加打包依赖
  4. 执行 pnpm run -r build 执行所有子包中所有打包脚本
{
  "name": "web-performance",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "build": "rollup -c"
  }
}

image.png

方式二,多入口打包

由于 rollup 本身支持多入口打包,你可以从配置文件中导出一个数组,以便一次从多个不相关的输入进行打包。

下面打包文件配置完成了多入口文件的打包

import { fileURLToPath} from 'node:url'
import fs from 'node:fs'
import path from 'node:path';
const fileUrl = new URL('./packages',import.meta.url);
const packagesDir = fileURLToPath(fileUrl)
const packageDirs = fs.readdirSync(packagesDir)

const common = {
    output: {
        entryFileNames: '[hash].js',
        dir: 'dist',
        format: 'es'
    }
}

export default packageDirs.map(dir => {
    return {
        ...common,
        input: path.resolve(packagesDir, dir, './src/index.js'),
        output: {
            ...common.output,
            dir:  path.resolve(packagesDir, dir, './dist'),
        }
    }
})

问题

我们查看打包后的文件,发现有以下几个问题

  • 引入的函数没有打包进来
  • async 语法没有使用 babel 编译,导致低版本的浏览器可能无法支持该语法。

image.png

常用插件

Rollup 插件用于扩展 Rollup 打包器的功能,使其实现对不同文件类型的处理(如 CSS、图像)、代码转换(如 ES6 到 ES5、TypeScript 编译)、优化(如压缩、tree shaking)以及自定义构建流程等操作。这些插件增强了 Rollup 的灵活性,使其能够适应多样化的项目需求。

像我们前面路径解析问题导致函数没有被打包进 bundle 文件及语法编译都可以使用对应的插件解决。介绍几个常用的插件如下。

@rollup/plugin-node-resolve

解决当外部依赖无法正常解析

pnpm add @rollup/plugin-node-resolve -D
import resolve from '@rollup/plugin-node-resolve'
const common = {
    output: {
        entryFileNames: '[hash].js',
        dir: 'dist',
        format: 'es'
    },
    plugins: [
        resolve()
    ]
}

@rollup/plugin-babel

js 语法编译器,例如将 async 转为 ES5 支持的语法。

pnpm  add @babel/preset-env @babel/core @rollup/plugin-babel -D
import babel from '@rollup/plugin-babel';
const common = {
    output: {
        entryFileNames: '[hash].js',
        dir: 'dist',
        format: 'es'
    },
    plugins: [
        resolve(),
        babel({
            presets: ['@babel/preset-env']
        })
    ]
}

rollup-plugin-clear

pnpm  add rollup-plugin-clear -D
import clear from 'rollup-plugin-clear'
export default packageDirs.map(dir => {
    const outputDir = path.resolve(packagesDir, dir, './dist');
    return {
        // ...
        plugins: [
            ...common.plugins,
            clear({
                targets: [outputDir]
            }),
        ]
    }
})

总结

到此我们成功搭建了一个基于 monorepo 的 SDK 开发环境,使用 pnpm workspaces 来管理多个相关的项目,通过 vitest 实现了单元测试以确保代码质量,最后利用 rollup 进行了项目打包,形成了一个从开发到测试再到构建的完整流程。