mirror of
https://github.com/certd/certd.git
synced 2026-06-24 11:17:30 +08:00
413 lines
13 KiB
Markdown
413 lines
13 KiB
Markdown
# 插件依赖按需加载方案
|
||
|
||
## 背景与目标
|
||
|
||
### 当前问题
|
||
- `packages/ui/certd-server/node_modules` 包含 50+ 个插件的所有依赖,体积庞大
|
||
- 大量云厂商 SDK(AWS、阿里云、腾讯云、华为云等)只在特定插件中使用
|
||
- 用户通常只使用少数几个插件,但必须安装所有依赖
|
||
|
||
### 目标
|
||
实现依赖的按需下载和加载:
|
||
1. 插件依赖独立管理,不占用主 `node_modules` 空间
|
||
2. 只有当用户首次使用某插件时,才动态下载该插件需要的依赖
|
||
3. 依赖安装完成后,通过 `await import()` 从独立路径加载
|
||
4. 保持现有插件代码的最小改动
|
||
|
||
## 当前架构分析
|
||
|
||
### 插件加载机制
|
||
- 插件位于 `packages/ui/certd-server/src/plugins/` 下(50+ 个插件目录)
|
||
- `AutoLoadPlugins` 类在启动时扫描 `dist/plugins` 目录并动态导入
|
||
- 插件注册到不同的 registry:`accessRegistry`, `pluginRegistry`, `dnsProviderRegistry` 等
|
||
- 插件代码已经使用 `await import()` 进行懒加载(如 `await import("@aws-sdk/client-acm")`)
|
||
|
||
### 重型依赖分布
|
||
从 `packages/ui/certd-server/package.json` 分析,以下依赖体积大且仅特定插件使用:
|
||
|
||
**云厂商 SDK(按插件分组):**
|
||
- **AWS 插件**:`@aws-sdk/client-acm`, `@aws-sdk/client-cloudfront`, `@aws-sdk/client-iam`, `@aws-sdk/client-route-53`, `@aws-sdk/client-s3`, `@aws-sdk/client-sts`
|
||
- **阿里云插件**:`@alicloud/openapi-client`, `@alicloud/pop-core`, `@alicloud/tea-typescript`, `@alicloud/fc20230330` 等
|
||
- **腾讯云插件**:`tencentcloud-sdk-nodejs`, `cos-nodejs-sdk-v5`
|
||
- **华为云插件**:`@huaweicloud/huaweicloud-sdk-cdn`, `@huaweicloud/huaweicloud-sdk-core` 等
|
||
- **Azure 插件**:`@azure/arm-dns`, `@azure/identity`
|
||
- **Google Cloud 插件**:`@google-cloud/dns`, `@google-cloud/publicca`
|
||
- **火山引擎插件**:`@volcengine/openapi`, `@volcengine/tos-sdk`
|
||
|
||
**网络/工具库:**
|
||
- `ssh2`, `socks`, `socks-proxy-agent`(SSH 相关插件)
|
||
- `ali-oss`, `qiniu`, `basic-ftp`(存储/传输插件)
|
||
- `nodemailer`(邮件通知插件)
|
||
|
||
**通用依赖(保留在主 package.json):**
|
||
- `@midwayjs/*` 系列(框架核心)
|
||
- `@certd/*` 系列(项目内部包)
|
||
- `axios`, `lodash-es`, `dayjs`, `js-yaml` 等基础工具
|
||
|
||
## 设计方案
|
||
|
||
### 架构概览
|
||
|
||
```
|
||
packages/ui/certd-server/
|
||
├── package.json # 主依赖(框架、通用工具)
|
||
├── node_modules/ # 主依赖安装目录
|
||
├── optional-deps/ # 新增:可选依赖管理目录
|
||
│ ├── package.json # 可选依赖总配置(用于 pnpm install)
|
||
│ ├── pnpm-lock.yaml # 可选依赖锁文件
|
||
│ └── node_modules/ # 可选依赖安装目录
|
||
├── src/
|
||
│ └── modules/
|
||
│ └── dependency/ # 新增:依赖管理模块
|
||
│ ├── dependency-manager.ts # 核心:依赖管理器
|
||
│ ├── dependency-registry.ts # 依赖注册表(插件 -> 依赖映射)
|
||
│ └── types.ts # 类型定义
|
||
```
|
||
|
||
### 核心组件
|
||
|
||
#### 1. 依赖管理器(DependencyManager)
|
||
|
||
**职责:**
|
||
- 检查依赖是否已安装
|
||
- 动态执行 `pnpm install` 安装缺失依赖
|
||
- 提供从 `optional-deps/node_modules` 加载依赖的方法
|
||
- 并发控制:避免多个插件同时触发安装
|
||
|
||
**关键方法:**
|
||
```typescript
|
||
class DependencyManager {
|
||
// 确保依赖已安装,返回依赖模块
|
||
async ensureAndImport<T>(packageName: string): Promise<T>
|
||
|
||
// 检查依赖是否已安装
|
||
async isInstalled(packageName: string): Promise<boolean>
|
||
|
||
// 安装依赖(带锁,避免并发)
|
||
async installDependencies(packages: string[]): Promise<void>
|
||
|
||
// 从 optional-deps/node_modules 加载依赖
|
||
async loadModule<T>(packageName: string): Promise<T>
|
||
}
|
||
```
|
||
|
||
**实现要点:**
|
||
- 使用文件锁(如 `proper-lockfile`)防止并发安装
|
||
- 安装前检查 `optional-deps/node_modules/{packageName}` 是否存在
|
||
- 安装命令:`pnpm install --dir optional-deps --ignore-workspace`
|
||
- 加载时使用绝对路径:`import('file:///absolute/path/to/optional-deps/node_modules/package')`
|
||
|
||
#### 2. 依赖注册表(DependencyRegistry)
|
||
|
||
**职责:**
|
||
- 维护插件名称到依赖列表的映射
|
||
- 提供依赖查询接口
|
||
|
||
**数据结构:**
|
||
```typescript
|
||
interface PluginDependencyConfig {
|
||
pluginName: string;
|
||
dependencies: {
|
||
packageName: string;
|
||
version: string;
|
||
optional?: boolean; // 是否可选(安装失败不阻塞)
|
||
}[];
|
||
}
|
||
|
||
// 示例注册
|
||
dependencyRegistry.register('plugin-aws', [
|
||
{ packageName: '@aws-sdk/client-acm', version: '^3.964.0' },
|
||
{ packageName: '@aws-sdk/client-cloudfront', version: '^3.964.0' },
|
||
{ packageName: '@aws-sdk/client-route-53', version: '^3.964.0' },
|
||
]);
|
||
```
|
||
|
||
#### 3. 插件集成
|
||
|
||
**改造现有插件代码:**
|
||
|
||
改造前(`plugin-aws/libs/aws-client.ts`):
|
||
```typescript
|
||
const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");
|
||
```
|
||
|
||
改造后:
|
||
```typescript
|
||
import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";
|
||
|
||
const depManager = new DependencyManager();
|
||
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");
|
||
```
|
||
|
||
**简化方案(推荐):**
|
||
|
||
创建辅助函数,减少改动量:
|
||
```typescript
|
||
// src/modules/dependency/import-helper.ts
|
||
export async function importOptionalDep<T>(packageName: string): Promise<T> {
|
||
const depManager = new DependencyManager();
|
||
return await depManager.ensureAndImport<T>(packageName);
|
||
}
|
||
|
||
// 插件中使用
|
||
import { importOptionalDep } from "../../../modules/dependency/import-helper.js";
|
||
const { ACMClient } = await importOptionalDep("@aws-sdk/client-acm");
|
||
```
|
||
|
||
### 实施步骤
|
||
|
||
#### 阶段一:基础设施搭建
|
||
1. 创建 `optional-deps/` 目录结构
|
||
2. 生成 `optional-deps/package.json`(包含所有可选依赖)
|
||
3. 实现 `DependencyManager` 核心逻辑
|
||
4. 实现依赖安装锁机制
|
||
5. 编写单元测试
|
||
|
||
#### 阶段二:依赖迁移
|
||
6. 从主 `package.json` 移除可选依赖
|
||
7. 将依赖添加到 `optional-deps/package.json`
|
||
8. 创建依赖注册表,映射插件到依赖
|
||
|
||
#### 阶段三:插件改造
|
||
9. 创建 `import-helper.ts` 辅助函数
|
||
10. 逐步改造插件代码,使用 `importOptionalDep` 加载依赖
|
||
11. 优先改造重型依赖(AWS、阿里云、腾讯云等)
|
||
|
||
#### 阶段四:测试与优化
|
||
12. 端到端测试:验证依赖按需安装和加载
|
||
13. 性能优化:缓存已加载的模块
|
||
14. 错误处理:安装失败时的降级策略
|
||
15. 文档:编写使用说明和迁移指南
|
||
|
||
## 关键技术决策
|
||
|
||
### 1. 依赖分组策略
|
||
**选择:按插件分组**
|
||
- 每个插件声明自己需要的依赖
|
||
- 优点:职责清晰,易于维护
|
||
- 缺点:可能有重复依赖(但 pnpm 会去重)
|
||
|
||
**备选:按功能分组**
|
||
- 将依赖按功能分组(如 "aws-deps", "aliyun-deps")
|
||
- 优点:更细粒度控制
|
||
- 缺点:增加复杂度
|
||
|
||
### 2. 安装触发时机
|
||
**选择:首次使用时触发**
|
||
- 在插件的 `execute()` 或 `getClient()` 方法中触发安装
|
||
- 优点:真正的按需加载
|
||
- 缺点:首次使用有延迟
|
||
|
||
**备选:启动时预检查**
|
||
- 启动时扫描启用的插件,预安装依赖
|
||
- 优点:避免运行时延迟
|
||
- 缺点:可能安装不需要的依赖
|
||
|
||
### 3. 依赖路径解析
|
||
**选择:使用绝对路径 + `file://` 协议**
|
||
```typescript
|
||
const modulePath = path.resolve(__dirname, '../../optional-deps/node_modules', packageName);
|
||
return await import(`file://${modulePath}/index.js`);
|
||
```
|
||
|
||
**原因:**
|
||
- Node.js ESM 要求明确的 URL 格式
|
||
- 避免模块解析冲突
|
||
|
||
### 4. 并发控制
|
||
**选择:文件锁 + 内存锁双重保护**
|
||
- 使用 `proper-lockfile` 锁定 `optional-deps/` 目录
|
||
- 内存中使用 `Map` 记录正在安装的依赖
|
||
- 避免多个插件同时触发安装
|
||
|
||
### 5. 错误处理
|
||
**策略:**
|
||
- 安装失败时记录日志,抛出明确的错误信息
|
||
- 提供手动安装命令提示:`请运行: cd optional-deps && pnpm install`
|
||
- 支持降级:某些非核心依赖安装失败时,插件可以部分功能可用
|
||
|
||
## 验证方案
|
||
|
||
### 单元测试
|
||
1. 测试 `DependencyManager.isInstalled()` 正确检测依赖状态
|
||
2. 测试 `DependencyManager.installDependencies()` 成功安装依赖
|
||
3. 测试并发安装时的锁机制
|
||
4. 测试从 `optional-deps/node_modules` 加载模块
|
||
|
||
### 集成测试
|
||
1. 清空 `optional-deps/node_modules`
|
||
2. 启动服务,验证不触发安装
|
||
3. 调用 AWS 插件,验证触发安装并成功加载
|
||
4. 再次调用,验证不重复安装
|
||
5. 验证主 `node_modules` 体积减少
|
||
|
||
### 性能测试
|
||
1. 测量首次安装依赖的耗时
|
||
2. 测量后续加载的耗时(应该与正常 import 相近)
|
||
3. 对比改造前后的 `node_modules` 大小
|
||
|
||
## 风险与挑战
|
||
|
||
### 1. 首次使用延迟
|
||
**风险:** 用户首次使用插件时需要等待依赖安装(可能几十秒)
|
||
**缓解:**
|
||
- 在 UI 上显示安装进度
|
||
- 提供预安装命令:`pnpm run install-optional-deps`
|
||
- 文档说明首次使用会有延迟
|
||
|
||
### 2. 离线环境
|
||
**风险:** 离线环境无法下载依赖
|
||
**缓解:**
|
||
- 提供完整安装包(包含所有可选依赖)
|
||
- 支持手动复制 `node_modules`
|
||
|
||
### 3. 版本冲突
|
||
**风险:** 可选依赖与主依赖版本冲突
|
||
**缓解:**
|
||
- 使用 `--ignore-workspace` 隔离安装
|
||
- 定期同步主依赖版本
|
||
|
||
### 4. TypeScript 类型
|
||
**风险:** 动态导入的类型推断
|
||
**缓解:**
|
||
- 保留 `@types/*` 在主 `devDependencies`
|
||
- 使用泛型和类型断言
|
||
|
||
## 预期收益
|
||
|
||
1. **空间节省:** 主 `node_modules` 体积减少 60-70%(估算)
|
||
2. **安装速度:** 初始 `pnpm install` 速度提升 3-5 倍
|
||
3. **用户体验:** 不使用的插件不占用空间,按需加载
|
||
4. **维护性:** 依赖分组清晰,易于管理
|
||
|
||
## 后续优化
|
||
|
||
1. **依赖预热:** 在后台预安装常用插件依赖
|
||
2. **依赖缓存:** 支持从 CDN 或本地缓存安装
|
||
3. **依赖更新:** 提供命令批量更新可选依赖
|
||
4. **插件市场:** 支持从远程下载插件及其依赖配置
|
||
|
||
## 附录:依赖分类清单
|
||
|
||
### 可选依赖(迁移到 optional-deps/package.json)
|
||
|
||
**AWS 相关(plugin-aws, plugin-aws-cn):**
|
||
```json
|
||
{
|
||
"@aws-sdk/client-acm": "^3.964.0",
|
||
"@aws-sdk/client-cloudfront": "^3.964.0",
|
||
"@aws-sdk/client-iam": "^3.964.0",
|
||
"@aws-sdk/client-route-53": "^3.964.0",
|
||
"@aws-sdk/client-s3": "^3.964.0",
|
||
"@aws-sdk/client-sts": "^3.990.0"
|
||
}
|
||
```
|
||
|
||
**阿里云相关(plugin-aliyun, plugin-lib/aliyun):**
|
||
```json
|
||
{
|
||
"@alicloud/fc20230330": "^4.1.7",
|
||
"@alicloud/openapi-client": "^0.4.12",
|
||
"@alicloud/openapi-util": "^0.3.2",
|
||
"@alicloud/pop-core": "^1.7.10",
|
||
"@alicloud/sts-sdk": "^1.0.2",
|
||
"@alicloud/tea-typescript": "^1.8.0",
|
||
"@alicloud/tea-util": "^1.4.10",
|
||
"ali-oss": "^6.21.0"
|
||
}
|
||
```
|
||
|
||
**腾讯云相关(plugin-tencent, plugin-lib/tencent):**
|
||
```json
|
||
{
|
||
"tencentcloud-sdk-nodejs": "^4.1.112",
|
||
"cos-nodejs-sdk-v5": "^2.14.6"
|
||
}
|
||
```
|
||
|
||
**华为云相关(plugin-huawei):**
|
||
```json
|
||
{
|
||
"@huaweicloud/huaweicloud-sdk-cdn": "3.1.185",
|
||
"@huaweicloud/huaweicloud-sdk-core": "3.1.185",
|
||
"@huaweicloud/huaweicloud-sdk-elb": "3.1.185",
|
||
"@huaweicloud/huaweicloud-sdk-iam": "3.1.185",
|
||
"esdk-obs-nodejs": "^3.25.6"
|
||
}
|
||
```
|
||
|
||
**Azure 相关(plugin-azure):**
|
||
```json
|
||
{
|
||
"@azure/arm-dns": "^5.1.0",
|
||
"@azure/identity": "^4.13.1"
|
||
}
|
||
```
|
||
|
||
**Google Cloud 相关(plugin-google, plugin-cert/google):**
|
||
```json
|
||
{
|
||
"@google-cloud/dns": "^5.3.1",
|
||
"@google-cloud/publicca": "^1.3.0"
|
||
}
|
||
```
|
||
|
||
**火山引擎相关(plugin-volcengine):**
|
||
```json
|
||
{
|
||
"@volcengine/openapi": "^1.28.1",
|
||
"@volcengine/tos-sdk": "^2.9.1"
|
||
}
|
||
```
|
||
|
||
**SSH/网络相关(plugin-host, plugin-lib/ssh):**
|
||
```json
|
||
{
|
||
"ssh2": "^1.17.0",
|
||
"socks": "^2.8.3",
|
||
"socks-proxy-agent": "^8.0.4",
|
||
"basic-ftp": "^5.0.5"
|
||
}
|
||
```
|
||
|
||
**其他存储/传输(plugin-qiniu, plugin-lib/qiniu):**
|
||
```json
|
||
{
|
||
"qiniu": "^7.12.0"
|
||
}
|
||
```
|
||
|
||
**邮件通知(plugin-notification/email):**
|
||
```json
|
||
{
|
||
"nodemailer": "^6.9.16"
|
||
}
|
||
```
|
||
|
||
### 主依赖(保留在主 package.json)
|
||
|
||
**框架核心:**
|
||
- `@midwayjs/*` 系列
|
||
- `@koa/cors`
|
||
- `typeorm`, `better-sqlite3`, `mysql2`, `pg`
|
||
|
||
**项目内部包:**
|
||
- `@certd/*` 系列
|
||
|
||
**通用工具:**
|
||
- `axios`, `lodash-es`, `dayjs`, `js-yaml`
|
||
- `crypto-js`, `jsonwebtoken`, `bcryptjs`
|
||
- `reflect-metadata`, `uuid`, `nanoid`
|
||
- 等等
|
||
|
||
## 总结
|
||
|
||
本方案通过引入独立的可选依赖管理机制,实现了插件依赖的按需下载和加载。核心思路是:
|
||
|
||
1. **隔离管理:** 在 `optional-deps/` 目录下维护独立的 `package.json` 和 `node_modules`
|
||
2. **动态安装:** 通过 `DependencyManager` 在首次使用时触发 `pnpm install`
|
||
3. **路径加载:** 使用绝对路径从独立目录加载依赖模块
|
||
4. **最小改动:** 通过辅助函数 `importOptionalDep` 简化插件代码改造
|
||
|
||
该方案可以显著减少主 `node_modules` 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。
|