Files
certd/.trae/documents/plugin-on-demand-dependency-loading.md
T

13 KiB
Raw Blame History

插件依赖按需加载方案

背景与目标

当前问题

  • 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 目录并动态导入
  • 插件注册到不同的 registryaccessRegistry, 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-agentSSH 相关插件)
  • 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 加载依赖的方法
  • 并发控制:避免多个插件同时触发安装

关键方法:

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

职责:

  • 维护插件名称到依赖列表的映射
  • 提供依赖查询接口

数据结构:

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):

const { ACMClient, ImportCertificateCommand } = await import("@aws-sdk/client-acm");

改造后:

import { DependencyManager } from "../../../modules/dependency/dependency-manager.js";

const depManager = new DependencyManager();
const { ACMClient, ImportCertificateCommand } = await depManager.ensureAndImport("@aws-sdk/client-acm");

简化方案(推荐):

创建辅助函数,减少改动量:

// 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. 编写单元测试

阶段二:依赖迁移

  1. 从主 package.json 移除可选依赖
  2. 将依赖添加到 optional-deps/package.json
  3. 创建依赖注册表,映射插件到依赖

阶段三:插件改造

  1. 创建 import-helper.ts 辅助函数
  2. 逐步改造插件代码,使用 importOptionalDep 加载依赖
  3. 优先改造重型依赖(AWS、阿里云、腾讯云等)

阶段四:测试与优化

  1. 端到端测试:验证依赖按需安装和加载
  2. 性能优化:缓存已加载的模块
  3. 错误处理:安装失败时的降级策略
  4. 文档:编写使用说明和迁移指南

关键技术决策

1. 依赖分组策略

选择:按插件分组

  • 每个插件声明自己需要的依赖
  • 优点:职责清晰,易于维护
  • 缺点:可能有重复依赖(但 pnpm 会去重)

备选:按功能分组

  • 将依赖按功能分组(如 "aws-deps", "aliyun-deps"
  • 优点:更细粒度控制
  • 缺点:增加复杂度

2. 安装触发时机

选择:首次使用时触发

  • 在插件的 execute()getClient() 方法中触发安装
  • 优点:真正的按需加载
  • 缺点:首次使用有延迟

备选:启动时预检查

  • 启动时扫描启用的插件,预安装依赖
  • 优点:避免运行时延迟
  • 缺点:可能安装不需要的依赖

3. 依赖路径解析

选择:使用绝对路径 + file:// 协议

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):

{
  "@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):

{
  "@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):

{
  "tencentcloud-sdk-nodejs": "^4.1.112",
  "cos-nodejs-sdk-v5": "^2.14.6"
}

华为云相关(plugin-huawei):

{
  "@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):

{
  "@azure/arm-dns": "^5.1.0",
  "@azure/identity": "^4.13.1"
}

Google Cloud 相关(plugin-google, plugin-cert/google):

{
  "@google-cloud/dns": "^5.3.1",
  "@google-cloud/publicca": "^1.3.0"
}

火山引擎相关(plugin-volcengine):

{
  "@volcengine/openapi": "^1.28.1",
  "@volcengine/tos-sdk": "^2.9.1"
}

SSH/网络相关(plugin-host, plugin-lib/ssh):

{
  "ssh2": "^1.17.0",
  "socks": "^2.8.3",
  "socks-proxy-agent": "^8.0.4",
  "basic-ftp": "^5.0.5"
}

其他存储/传输(plugin-qiniu, plugin-lib/qiniu):

{
  "qiniu": "^7.12.0"
}

邮件通知(plugin-notification/email):

{
  "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.jsonnode_modules
  2. 动态安装: 通过 DependencyManager 在首次使用时触发 pnpm install
  3. 路径加载: 使用绝对路径从独立目录加载依赖模块
  4. 最小改动: 通过辅助函数 importOptionalDep 简化插件代码改造

该方案可以显著减少主 node_modules 体积,提升初始安装速度,同时保持现有架构的兼容性和可维护性。