前言
前面文章中介绍了深入使用 PerformanceObserver和 前端错误监控sdk初步实践等文章,假设现在有一个需求,我们想要实现一个埋点 SDK,会涉及到很多模块。例如:性能监控模块、用户信息模块、错误监控模块等。这些模块其实都可以作为一个单独的包独立使用。
本文我们来聊聊如何使用 pnpm 构建 monorepo 项目,,使用 vitest 进行项目测试,以及如何使用 rollup 打包。但不涉及具体的 SDK 实现细节,这个后续会单独出一篇。
monorepo
如果你想将监控相关的SDK设计成多个可独立使用的模块,同时保持一定的代码组织和共享逻辑,你可以采用一种称为“monorepo”(单仓库多包)的开发模式。这样,你可以将不同的功能模块(如monitor-core、monitor-shared和web-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 项目。
创建 core 和 web-performance 两个子包如下图。
图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表示按照到 devDependenciesweb-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 包内添加测试代码。
图2
在根目录下添加 vitest 依赖,直接开箱即用,运行 pnpm test 运行测试代码。当我们代码发生变更,测试用例也会自动热更新重新执行,非常方便。
图3
图4
为什么使用 pnpm
在了解 pnpm 基础的使用后,来看看使用 pnpm 的优势。
高效使用磁盘
不会重复安装同一个包。用 npm/yarn 的时候,如果 100 个项目都依赖 lodash,那么 lodash 很可能就被安装了 100 次,磁盘中就有 100 个地方写入了这部分代码。但在使用 pnpm 只会安装一次,磁盘中只有一个地方写入,后面再次使用都会直接使用 hardlink,它们指向的是同一个文件。
硬连接
假设您有 100 个项目都依赖 lodash,并且您使用 pnpm 进行管理。在这种情况下:
lodash只会被安装一次,并存储在全局的.pnpm目录中。- 每个项目中的
node_modules目录下的lodash都是一个指向.pnpm目录中lodash的硬链接。 - 如果您删除其中一个项目中的
lodash硬链接,这不会影响其他项目中的lodash硬链接。 - 只有当所有项目中的
lodash硬链接都被删除,并且没有其他项目依赖lodash时,pnpm 才会从.pnpm目录中移除lodash。
安全性高
使用 npm/yarn 的时候,由于 node_module 的扁平结构,如果 A 依赖 B, B 依赖 C,那么 A 当中是可以直接使用 C 的,但问题是 A 当中并没有声明 C 这个依赖。因此会出现这种非法访问的情况。
在 pnpm 中每个依赖各自维护了自己的 node_modules 形成嵌套的结构。
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 , 需要对所有子包进行打包。
方式一,单文入口打包
- 在各自的子包中都添加对应的 rollup.config.mjs 文件。
- 在 package.json 中都定义
build script - 在根目录下添加 rollup 依赖,这样不需要在每个子目录下添加打包依赖
- 执行 pnpm run -r build 执行所有子包中所有打包脚本
{
"name": "web-performance",
"version": "1.0.0",
"description": "",
"main": "src/index.js",
"scripts": {
"build": "rollup -c"
}
}
方式二,多入口打包
由于 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 编译,导致低版本的浏览器可能无法支持该语法。
常用插件
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 进行了项目打包,形成了一个从开发到测试再到构建的完整流程。