{T}

插件的生命周期管理

概述

插件生命周期管理是插件化架构的核心环节,涵盖插件从发布、安装、运行到卸载的完整流程。

code
┌─────────────────────────────────────────────────────────────────────┐
│                       插件生命周期流程                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│  │   发布    │───►│   安装    │───►│   运行    │───►│   卸载    │     │
│  │ Publish  │    │ Install  │    │   Run    │    │ Uninstall│     │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘     │
│       │              │              │              │               │
│       ▼              ▼              ▼              ▼               │
│  ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐     │
│  │ npm/oss  │    │ 下载/解压 │    │ 加载/初始化│   │ 清理资源  │     │
│  │ 上传发布 │    │ 安全校验  │    │ 运行监控  │    │ 删除文件  │     │
│  └──────────┘    └──────────┘    └──────────┘    └──────────┘     │
│                                                                     │
│                    ┌──────────┐                                     │
│                    │   更新    │                                     │
│                    │  Update  │                                     │
│                    └────┬─────┘                                     │
│                         │                                           │
│                         ▼                                           │
│                   ┌──────────┐                                      │
│                   │ 版本检查  │                                      │
│                   │ 热更新    │                                      │
│                   └──────────┘                                      │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

插件的发布

插件开发的最终目的是让用户能够方便地获取和使用。为此需要将插件发布到云端,形成一个中心化的插件市场。目前主流的插件发布方式有两种:

OSS 资源服务器模式

将插件打包成特定格式(如 uTools.upx 文件),然后上传到对象存储服务(OSS)上。用户通过下载并安装这些打包文件来使用插件

  • 优点:简单直接,易于理解
  • 缺点
    • 依赖管理困难:所有依赖项必须完整打包,导致插件体积较大
    • 更新机制复杂:需要自行实现版本检查和更新逻辑
    • 部署灵活性差:强依赖于特定的 OSS 服务

npm 包管理器模式

将每个插件作为独立的 npm 包发布到 npm registry。这种方式在现代插件化架构中越来越受欢迎

  • 优点
    • 自由化部署:通过切换 npm registry(如官方源、私有源),可以轻松实现公网和内网的自由部署
    • 高效的依赖管理:利用 npm 的包管理机制,可以自动处理依赖关系,避免重复安装,有效减小插件总体积
    • 成熟的生态:可以复用 npm 生态中海量的工具和库

发布流程

发布插件非常简单,只需在插件项目根目录下执行:

shell
$ npm publish

注意:在发布前,请务必确认当前的 npm registry 是否符合预期。例如:若要发布到内部网络,需先将 registry 切换至内网源。可以通过以下命令检查当前的 registry:

shell
$ npm config get registry

插件的目录结构

标准的插件项目遵循特定的目录结构,以便主程序能够正确加载和执行

bash
plugin-example/
|-- index.html       # 插件的 UI 入口文件
|-- preload.js       # 预加载脚本,用于桥接主进程和插件进程
└── package.json     # 插件的元数据描述文件

package.json

package.json 文件定义插件的所有元数据,除了标准的 npm 字段外,还扩展了一些自定义字段以满足插件系统的需求

标准字段示例

json
{
  "name": "plugin-demo",
  "version": "1.0.0",
  "description": "这是一个插件的描述信息。",
  "author": "Your Name",
  "dependencies": {
    // 插件的运行时依赖
  }
}

扩展字段详解

json
{
  // ...
  "pluginName": "演示插件", // 插件的中文名称,用于在 UI 中展示
  "main": "./index.html", // 插件的入口文件路径
  "logo": "path/to/logo.png", // 插件的 Logo 图标
  "pluginType": "ui", // 插件类型('ui' 或 'system')
  "features": [
    {
      "explain": "启动演示插件", // 功能描述
      "cmds": [
        "demo", // 激活插件的关键词
        "演示插件"
      ]
    }
  ]
}
  • pluginType:
    • ui: 带有用户界面的插件,通过 BrowserView 加载
    • system: 在后台运行的系统级插件,无界面
  • features: 定义了用户如何与插件交互。cmds 数组中的关键词可以被主程序捕获,用于激活或调用此插件

index.html

这是插件的 UI 入口,主程序将通过 BrowserView 加载此文件

javascript
// 主程序中创建 BrowserView 的逻辑
import { BrowserView } from "electron"

const createView = (plugin, window) => {
  const { indexPath, preload } = plugin
  const view = new BrowserView({
    webPreferences: {
      preload // 指定预加载脚本
      // ...
    }
  })

  window.setBrowserView(view)
  view.webContents.loadURL(indexPath) // 加载插件的 HTML 文件
}

简单的 index.html 示例:

html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>插件标题</title>
  </head>
  <body>
    <h1>Hello, Plugin!</h1>
  </body>
  <script>
    // 通过 preload 暴露的 API 与主程序或其他插件交互
    window.rubick.showNotification("插件加载成功!")
    window.pluginAPI.doSomething()
  </script>
</html>

preload.js

preload.js 是连接插件渲染进程和主进程的桥梁。由于渲染进程运行在沙箱环境中,出于安全考虑,它不能直接访问 Node.js 或 Electron 的 API。preload.js 在拥有 Node.js 环境访问权限的上下文中执行,可以通过 contextBridge 向插件的 window 对象安全地暴露特定 API。

javascript
// preload.js
import { contextBridge } from "electron"

const pluginAPI = {
  doSomething() {
    // 在这里可以安全地调用 Node.js 或 Electron 的 API
    console.log("Action from plugin!")
  }
}

// 将 API 暴露给插件的渲染进程
contextBridge.exposeInMainWorld("pluginAPI", pluginAPI)

插件的生命周期管理

基于 npm 的插件管理,可以方便地实现插件的安装、更新和卸载

插件的安装

在 Electron 应用中执行 npm install 命令,最直接的方式是借助 Node.js 的 child_process 模块(或 cross-spawn 这样的跨平台库)来调用系统 Shell

方法一:使用 spawn 执行 npm 命令

javascript
import spawn from "cross-spawn"

async function execCommand(cmd, modules, baseDir, registry) {
  return new Promise((resolve, reject) => {
    const args = [
      cmd,
      ...modules.map((m) => `${m}@latest`),
      "--save",
      `--registry=${registry}`
    ]

    const npmProcess = spawn("npm", args, { cwd: baseDir })

    let output = ""
    npmProcess.stdout.on("data", (data) => (output += data.toString()))
    npmProcess.stderr.on("data", (data) => (output += data.toString()))

    npmProcess.on("close", (code) => {
      if (code === 0) {
        resolve({ success: true, data: output })
      } else {
        reject({ success: false, error: output })
      }
    })
  })
}

// 安装 plugin-demo
// execCommand('install', ['plugin-demo'], '/path/to/plugins', 'https://registry.npm.taobao.org');

问题:此方法依赖于用户系统中已安装 Node.js 和 npm,否则会因找不到 npm 命令而失败

方法二:编程式使用 npm

为了解决环境依赖问题,可以将 npm 作为一个库直接集成到 Electron 应用中,以编程方式调用其命令。

注意:自 npm v7 以来,官方不再推荐或支持编程式 API,其稳定性无法得到保证。因此选择一个相对稳定且支持该用法的旧版本,如 v6.14.7

javascript
import npm from "npm"
import path from "path"

async function execCommand(cmd, modules, baseDir, registry) {
  return new Promise((resolve, reject) => {
    const config = {
      prefix: baseDir,
      save: true,
      cache: path.join(baseDir, "cache"),
      registry: registry
    }

    npm.load(config, (loadErr) => {
      if (loadErr) return reject({ success: false, error: loadErr })

      npm.commands[cmd](modules, (cmdErr, data) => {
        if (cmdErr) {
          reject({ success: false, error: cmdErr })
        } else {
          resolve({ success: true, data })
        }
      })
    })
  })
}

// 安装 plugin-demo
// execCommand('install', ['plugin-demo@latest'], '/path/to/plugins', 'https://registry.npm.taobao.org');

通过这种方式,插件安装不再依赖于用户的本地环境,大大提升了应用的健壮性

插件的更新

插件更新的本质是安装最新版本的 npm 包。实现逻辑如下:

  1. 检查新版本:对比本地安装的插件版本和 npm registry 上的最新版本。
  2. 执行更新:如果存在新版本,则执行 npm install <plugin-name>@latest 命令
javascript
import axios from "axios"
import fs from "fs"

async function upgradePlugin(name, baseDir, registry) {
  try {
    // 1. 获取本地版本
    const pkgPath = path.join(baseDir, "node_modules", name, "package.json")
    const localPkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"))
    const localVersion = localPkg.version

    // 2. 获取远程最新版本
    const registryUrl = `${registry}/${name}`
    const { data } = await axios.get(registryUrl)
    const latestVersion = data["dist-tags"].latest

    // 3. 对比版本并更新
    if (latestVersion > localVersion) {
      console.log(`发现新版本 ${latestVersion},正在更新...`)
      await execCommand("install", [`${name}@latest`], baseDir, registry)
      console.log("插件更新成功!")
    } else {
      console.log("已是最新版本。")
    }
  } catch (error) {
    console.error("插件更新失败:", error)
  }
}

插件的卸载

卸载插件同样简单,只需调用 npm uninstall 命令即可

javascript
async function uninstallPlugin(name, baseDir) {
  try {
    await execCommand("uninstall", [name], baseDir)
    console.log(`插件 ${name} 卸载成功!`)
  } catch (error) {
    console.error("插件卸载失败:", error)
  }
}

关于本地调试

在开发阶段,可以使用 npm link 来调试本地插件。在卸载或安装时,需要区分当前是开发环境还是生产环境,以执行 unlinkuninstall

javascript
async function uninstall(pluginName, options) {
  const command = options.isDev ? "unlink" : "uninstall"
  await execCommand(command, [pluginName])
}

完整的插件管理实现可以参考:rubick/src/core/plugin-handler/index.ts

插件安全校验机制

校验流程

code
┌─────────────────────────────────────────────────────────────────────┐
│                       插件安全校验流程                               │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  ┌──────────────┐                                                  │
│  │  下载插件包   │                                                  │
│  └───────┬──────┘                                                  │
│          │                                                         │
│          ▼                                                         │
│  ┌──────────────┐    失败    ┌──────────────┐                     │
│  │ 完整性校验    │───────────►│   拒绝安装    │                     │
│  │ (SHA256)     │            │              │                     │
│  └───────┬──────┘            └──────────────┘                     │
│          │ 通过                                                    │
│          ▼                                                         │
│  ┌──────────────┐    失败    ┌──────────────┐                     │
│  │ 签名验证     │───────────►│   拒绝安装    │                     │
│  │ (RSA/DSA)   │            │              │                     │
│  └───────┬──────┘            └──────────────┘                     │
│          │ 通过                                                    │
│          ▼                                                         │
│  ┌──────────────┐    失败    ┌──────────────┐                     │
│  │ 元数据校验   │───────────►│   拒绝安装    │                     │
│  │ (package.json)│           │              │                     │
│  └───────┬──────┘            └──────────────┘                     │
│          │ 通过                                                    │
│          ▼                                                         │
│  ┌──────────────┐                                                 │
│  │  安装插件    │                                                 │
│  └──────────────┘                                                 │
│                                                                    │
└─────────────────────────────────────────────────────────────────────┘

SHA256 完整性校验

typescript
// src/main/plugin/validator.ts
import crypto from 'crypto'
import fs from 'fs'

export async function verifyFileIntegrity(
  filePath: string,
  expectedHash: string
): Promise<boolean> {
  return new Promise((resolve, reject) => {
    const hash = crypto.createHash('sha256')
    const stream = fs.createReadStream(filePath)

    stream.on('data', (data) => hash.update(data))
    stream.on('end', () => {
      const actualHash = hash.digest('hex')
      resolve(actualHash === expectedHash)
    })
    stream.on('error', reject)
  })
}

// 使用示例
const isValid = await verifyFileIntegrity(
  '/path/to/plugin.zip',
  'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
)
if (!isValid) {
  throw new Error('插件文件完整性校验失败')
}

签名验证

typescript
// src/main/plugin/signer.ts
import crypto from 'crypto'

export interface PluginSignature {
  signature: string
  publicKey: string
  timestamp: number
}

export class PluginSigner {
  private privateKey: string
  private publicKey: string

  constructor(privateKey: string, publicKey: string) {
    this.privateKey = privateKey
    this.publicKey = publicKey
  }

  // 签名插件
  async signPlugin(pluginPath: string): Promise<PluginSignature> {
    const content = await fs.promises.readFile(pluginPath)
    const sign = crypto.createSign('RSA-SHA256')
    sign.update(content)
    sign.end()

    return {
      signature: sign.sign(this.privateKey, 'base64'),
      publicKey: this.publicKey,
      timestamp: Date.now()
    }
  }

  // 验证签名
  async verifySignature(
    pluginPath: string,
    signature: PluginSignature
  ): Promise<boolean> {
    try {
      const content = await fs.promises.readFile(pluginPath)
      const verify = crypto.createVerify('RSA-SHA256')
      verify.update(content)
      verify.end()

      return verify.verify(
        signature.publicKey,
        signature.signature,
        'base64'
      )
    } catch (error) {
      console.error('签名验证失败:', error)
      return false
    }
  }
}

元数据校验

typescript
// src/main/plugin/validator.ts
export interface PluginMetadata {
  name: string
  version: string
  main: string
  pluginName: string
  pluginType: 'ui' | 'system'
}

export function validatePluginMetadata(
  pkg: any
): { valid: boolean; errors: string[] } {
  const errors: string[] = []

  // 必需字段检查
  const requiredFields = ['name', 'version', 'main', 'pluginName', 'pluginType']
  for (const field of requiredFields) {
    if (!pkg[field]) {
      errors.push(`缺少必需字段: ${field}`)
    }
  }

  // 名称格式检查
  if (pkg.name && !/^[a-z0-9-]+$/.test(pkg.name)) {
    errors.push('插件名称只能包含小写字母、数字和连字符')
  }

  // 版本格式检查
  if (pkg.version && !/^\d+\.\d+\.\d+/.test(pkg.version)) {
    errors.push('版本号格式不正确,应为 semver 格式')
  }

  // 插件类型检查
  if (pkg.pluginType && !['ui', 'system'].includes(pkg.pluginType)) {
    errors.push('插件类型必须为 ui 或 system')
  }

  return {
    valid: errors.length === 0,
    errors
  }
}

权限声明与检查

json
// package.json 中声明权限
{
  "name": "my-plugin",
  "permissions": [
    "file:read",
    "file:write",
    "clipboard:read",
    "notification:show"
  ]
}
typescript
// src/main/plugin/permission.ts
export class PermissionManager {
  private allowedPermissions = new Map<string, string[]>()

  // 定义权限等级
  private permissionLevels = {
    safe: ['notification:show', 'clipboard:read'],
    moderate: ['file:read', 'file:write', 'dialog:open'],
    dangerous: ['shell:openExternal', 'process:spawn']
  }

  // 注册插件权限
  registerPlugin(pluginName: string, permissions: string[]) {
    this.allowedPermissions.set(pluginName, permissions)
  }

  // 检查权限
  hasPermission(pluginName: string, permission: string): boolean {
    const allowed = this.allowedPermissions.get(pluginName) || []
    return allowed.includes(permission)
  }

  // 获取权限等级
  getPermissionLevel(permission: string): string {
    for (const [level, perms] of Object.entries(this.permissionLevels)) {
      if (perms.includes(permission)) {
        return level
      }
    }
    return 'unknown'
  }
}

插件调试指南

开发环境调试

typescript
// src/main/plugin/debug.ts
import { BrowserView } from 'electron'

export function setupPluginDebug(view: BrowserView, pluginName: string) {
  // 自动打开 DevTools
  if (process.env.NODE_ENV === 'development') {
    view.webContents.openDevTools({ mode: 'detach' })
  }

  // 监听控制台消息
  view.webContents.on('console-message', (event, level, message, line, sourceId) => {
    const logLevel = ['verbose', 'info', 'warning', 'error'][level]
    console.log(`[${pluginName}] [${logLevel}] ${message}`)
  })

  // 监听加载事件
  view.webContents.on('did-finish-load', () => {
    console.log(`[${pluginName}] 加载完成`)
  })

  view.webContents.on('did-fail-load', (event, code, desc) => {
    console.error(`[${pluginName}] 加载失败: ${code} - ${desc}`)
  })
}

远程调试

typescript
// 启用远程调试端口
app.commandLine.appendSwitch('remote-debugging-port', '9222')

// 然后可以通过 chrome://inspect 访问

插件日志记录

typescript
// src/main/plugin/logger.ts
import { app } from 'electron'
import fs from 'fs'
import path from 'path'

export class PluginLogger {
  private logDir: string

  constructor() {
    this.logDir = path.join(app.getPath('userData'), 'plugin-logs')
    if (!fs.existsSync(this.logDir)) {
      fs.mkdirSync(this.logDir, { recursive: true })
    }
  }

  log(pluginName: string, level: string, message: string, data?: any) {
    const timestamp = new Date().toISOString()
    const logEntry = {
      timestamp,
      plugin: pluginName,
      level,
      message,
      data
    }

    const logFile = path.join(this.logDir, `${pluginName}.log`)
    fs.appendFileSync(logFile, JSON.stringify(logEntry) + '\n')

    // 同时输出到控制台
    console.log(`[${timestamp}] [${pluginName}] [${level}] ${message}`)
  }

  info(pluginName: string, message: string, data?: any) {
    this.log(pluginName, 'INFO', message, data)
  }

  error(pluginName: string, message: string, data?: any) {
    this.log(pluginName, 'ERROR', message, data)
  }

  warn(pluginName: string, message: string, data?: any) {
    this.log(pluginName, 'WARN', message, data)
  }
}

性能监控

typescript
// src/main/plugin/monitor.ts
export class PluginMonitor {
  private metrics = new Map<string, any>()

  startMonitoring(pluginName: string, view: BrowserView) {
    // 定期收集性能指标
    const interval = setInterval(async () => {
      try {
        const memoryInfo = await view.webContents.getProcessMemoryInfo()
        const cpuUsage = process.cpuUsage()

        this.metrics.set(pluginName, {
          memory: memoryInfo,
          cpu: cpuUsage,
          timestamp: Date.now()
        })

        // 内存预警
        if (memoryInfo.privateBytes > 200 * 1024 * 1024) {
          console.warn(`[${pluginName}] 内存使用超过 200MB`)
        }
      } catch (error) {
        console.error(`[${pluginName}] 监控失败:`, error)
      }
    }, 10000) // 每 10 秒采集一次

    return () => clearInterval(interval)
  }

  getMetrics(pluginName: string) {
    return this.metrics.get(pluginName)
  }
}

多窗口插件管理

typescript
// src/main/plugin/multiWindow.ts
import { BrowserWindow, BrowserView } from 'electron'

export class MultiWindowPluginManager {
  private windowPlugins = new Map<number, Map<string, BrowserView>>()

  // 在指定窗口加载插件
  loadPluginInWindow(
    windowId: number,
    pluginName: string,
    pluginPath: string
  ): BrowserView {
    const win = BrowserWindow.fromId(windowId)
    if (!win) {
      throw new Error(`Window ${windowId} not found`)
    }

    // 检查该窗口是否已有此插件
    let windowPlugins = this.windowPlugins.get(windowId)
    if (!windowPlugins) {
      windowPlugins = new Map()
      this.windowPlugins.set(windowId, windowPlugins)
    }

    if (windowPlugins.has(pluginName)) {
      return windowPlugins.get(pluginName)!
    }

    // 创建新的 BrowserView
    const view = new BrowserView({
      webPreferences: {
        preload: path.join(pluginPath, 'preload.js'),
        contextIsolation: true,
        nodeIntegration: false
      }
    })

    win.addBrowserView(view)
    view.webContents.loadFile(path.join(pluginPath, 'index.html'))
    windowPlugins.set(pluginName, view)

    return view
  }

  // 卸载窗口中的插件
  unloadPluginFromWindow(windowId: number, pluginName: string) {
    const windowPlugins = this.windowPlugins.get(windowId)
    if (!windowPlugins) return

    const view = windowPlugins.get(pluginName)
    if (view) {
      const win = BrowserWindow.fromId(windowId)
      win?.removeBrowserView(view)
      view.webContents.destroy()
      windowPlugins.delete(pluginName)
    }
  }

  // 获取窗口中的所有插件
  getWindowPlugins(windowId: number): string[] {
    return Array.from(this.windowPlugins.get(windowId)?.keys() || [])
  }
}

常见问题解答

Q1: 插件安装后无法加载?

排查步骤

  1. 检查插件目录结构是否完整
  2. 验证 package.json 格式是否正确
  3. 检查 main 字段指向的文件是否存在
  4. 查看主进程日志中的错误信息
javascript
// 添加详细日志
ipcMain.on('plugin:load-error', (event, error) => {
  console.error('插件加载错误:', error)
  logger.error('Plugin load error:', error)
})

Q2: 如何处理插件依赖冲突?

javascript
// 使用 npm 的 peerDependencies 声明兼容版本
{
  "peerDependencies": {
    "vue": "^3.0.0",
    "react": "^18.0.0"
  },
  "peerDependenciesMeta": {
    "vue": { "optional": true },
    "react": { "optional": true }
  }
}

// 主应用检查依赖兼容性
async function checkDependencies(plugin) {
  const dependencies = plugin.dependencies || {}
  const incompatible = []

  for (const [dep, version] of Object.entries(dependencies)) {
    const installed = await getInstalledVersion(dep)
    if (installed && !semver.satisfies(installed, version)) {
      incompatible.push({ dep, required: version, installed })
    }
  }

  return incompatible
}

Q3: 如何实现插件热更新?

typescript
// 监听插件文件变化
import chokidar from 'chokidar'

export class PluginHotReload {
  private watchers = new Map<string, FSWatcher>()

  watch(pluginPath: string, onChange: () => void) {
    const watcher = chokidar.watch(pluginPath, {
      ignored: /node_modules/,
      ignoreInitial: true
    })

    watcher.on('change', (filePath) => {
      console.log(`文件变化: ${filePath}`)
      onChange()
    })

    this.watchers.set(pluginPath, watcher)
  }

  unwatch(pluginPath: string) {
    const watcher = this.watchers.get(pluginPath)
    if (watcher) {
      watcher.close()
      this.watchers.delete(pluginPath)
    }
  }
}

Q4: 如何限制插件的网络访问?

typescript
// 使用 session 隔离网络请求
import { session } from 'electron'

function createPluginSession(pluginName: string) {
  const ses = session.fromPartition(`persist:plugin-${pluginName}`)

  // 只允许特定域名
  ses.webRequest.onBeforeRequest((details, callback) => {
    const allowedDomains = ['api.example.com', 'cdn.example.com']
    const url = new URL(details.url)

    if (allowedDomains.includes(url.hostname)) {
      callback({ cancel: false })
    } else {
      console.warn(`[${pluginName}] 阻止访问: ${url.hostname}`)
      callback({ cancel: true })
    }
  })

  return ses
}

Q5: 如何处理插件异常退出?

typescript
// 插件健康检查与自动恢复
export class PluginHealthChecker {
  private healthStatus = new Map<string, boolean>()

  startChecking(pluginName: string, view: BrowserView) {
    const interval = setInterval(async () => {
      try {
        // 发送心跳检测
        const response = await view.webContents.executeJavaScript(
          'typeof window.__pluginHealthCheck === "function" ? window.__pluginHealthCheck() : true'
        )

        this.healthStatus.set(pluginName, response)
      } catch (error) {
        console.warn(`[${pluginName}] 健康检查失败`)
        this.healthStatus.set(pluginName, false)

        // 自动重启插件
        this.restartPlugin(pluginName, view)
      }
    }, 30000) // 每 30 秒检查一次

    return () => clearInterval(interval)
  }

  async restartPlugin(pluginName: string, view: BrowserView) {
    console.log(`[${pluginName}] 正在重启...`)
    view.webContents.reload()
  }
}

版本兼容性

Electron 与 Node.js 版本对应

Electron 版本Node.js 版本Chromium 版本
28.x18.17.1120.0.6099.56
27.x18.17.1118.0.5993.54
26.x18.16.0116.0.5845.97
25.x18.15.0114.0.5735.134

npm 版本建议

  • 编程式 npm API:推荐使用 npm v6.x,因 v7+ 不再官方支持编程式调用
  • spawn 方式:可使用任意版本,但依赖用户环境
json
// package.json 中锁定 npm 版本
{
  "dependencies": {
    "npm": "6.14.18"
  }
}

兼容性检查工具

typescript
// src/main/plugin/compatibility.ts
import { app } from 'electron'
import semver from 'semver'

export interface CompatibilityResult {
  compatible: boolean
  issues: string[]
  warnings: string[]
}

export function checkPluginCompatibility(plugin: any): CompatibilityResult {
  const issues: string[] = []
  const warnings: string[] = []

  // 检查 Node.js 版本要求
  if (plugin.engines?.node) {
    const nodeVersion = process.versions.node
    if (!semver.satisfies(nodeVersion, plugin.engines.node)) {
      issues.push(
        `Node.js 版本不兼容: 要求 ${plugin.engines.node}, 当前 ${nodeVersion}`
      )
    }
  }

  // 检查 Electron 版本要求
  if (plugin.engines?.electron) {
    const electronVersion = process.versions.electron
    if (!semver.satisfies(electronVersion, plugin.engines.electron)) {
      issues.push(
        `Electron 版本不兼容: 要求 ${plugin.engines.electron}, 当前 ${electronVersion}`
      )
    }
  }

  // 检查 API 版本
  if (plugin.apiVersion && plugin.apiVersion !== CURRENT_API_VERSION) {
    warnings.push(
      `API 版本不匹配: 插件 ${plugin.apiVersion}, 主程序 ${CURRENT_API_VERSION}`
    )
  }

  return {
    compatible: issues.length === 0,
    issues,
    warnings
  }
}

参考链接