插件化架构
现代桌面应用开发中,插件化架构已成为提升应用可扩展性和可维护性的核心技术。通过插件化,可以将功能模块解耦,实现独立开发、部署和更新,类似于 VS Code 的扩展生态。构建主程序提供核心 API,插件在此基础上独立实现 UI 和业务逻辑的架构模式。
架构概览
┌─────────────────────────────────────────────────────────────────────┐
│ 主应用 (Main App) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 主进程 (Main Process) │ │
│ │ ┌────────────┐ ┌────────────┐ ┌────────────────────────┐ │ │
│ │ │ 窗口管理 │ │ 插件管理器 │ │ IPC 处理器 │ │ │
│ │ │ WindowMgr │ │ PluginMgr │ │ openAPI Handlers │ │ │
│ │ └────────────┘ └─────┬──────┘ └────────────────────────┘ │ │
│ └─────────────────────────┼─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ BrowserWindow (主窗口) │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ 主 UI 区域 │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
│ │ │ BrowserView │ │ BrowserView │ │ BrowserView │ │ │
│ │ │ 插件 A │ │ 插件 B │ │ 插件 C │ │ │
│ │ │ (独立进程) │ │ (独立进程) │ │ (独立进程) │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼────────────────┼────────────────────┼───────────────┘ │
│ │ │ │ │
│ └────────────────┼────────────────────┘ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ openAPI 桥接层 │ │
│ │ (preload + contextBridge) │ │
│ └────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘核心设计原则
| 原则 | 说明 |
|---|---|
| 进程隔离 | 每个插件运行在独立的 BrowserView 进程中,互不影响 |
| 权限最小化 | 插件只能通过 openAPI 访问受限的系统能力 |
| 统一通信 | 所有插件通过统一的 IPC 机制与主进程通信 |
| 生命周期管理 | 完整的安装、加载、卸载流程管理 |
插件加载机制对比
要在 Electron 中加载插件(本质上是独立的 Web 内容),有三种主流技术方案:<webview> 标签、<iframe> 标签和 BrowserView
<webview> 标签
<webview> 是 Electron 早期提供的用于嵌入 Web 内容的标签。它运行在一个独立的进程中,提供了较好的隔离性。
启用方式:
const win = new BrowserWindow({
webPreferences: {
webviewTag: true // 必须显式开启
}
})使用方式:
<webview id="plugin-view" src="file:///path/to/plugin/index.html"></webview>缺点:
- 稳定性问题:
Electron官方已不推荐使用<webview>,因为它所依赖的Chromium webview正在经历重大架构变革,导致其稳定性、导航和事件路由等方面存在诸多bug和性能问题 - 功能限制: 默认情况下,
<webview>中的页面没有Node.js集成,与主进程的通信也相对繁琐 - 性能滞后: 作为
DOM元素,它的加载和渲染受制于渲染进程,导致插件启动时机滞后
<iframe> 标签
<iframe> 是 HTML 标准标签,兼容性好,使用简单
使用方式:
<iframe id="plugin-frame" src="https://example.com"></iframe>缺点:
- 安全与集成限制:
<iframe>存在严格的同源策略限制。要加载本地插件,需要移除Content-Security-Policy,这会带来安全风险 - 功能孱弱:
<iframe>内的页面无法直接访问Node.js能力,也无法直接加载preload脚本。所有与主应用的交互都必须通过postMessage进行,这极大地限制了插件的能力上限 - 进程内渲染:
<iframe>与宿主页面共享同一个渲染进程,隔离性差,插件的崩溃可能会影响整个应用的稳定性
BrowserView
BrowserView 是 Electron 官方推荐的 <webview> 替代方案。它被设计为一个“子窗口”,直接附加到 BrowserWindow 上,但在操作系统的窗口层级中独立存在
优势:
- 独立进程与高性能: 每个
BrowserView实例都在独立的渲染进程中运行,提供最佳的性能和隔离性,其运行速度与Chrome标签页相当 - 完全的
webContents访问:BrowserView拥有自己的webContents实例,可以像BrowserWindow一样加载preload脚本、开启Node.js集成,并进行精细的权限控制 - 灵活的窗口管理: 开发者可以精确控制
BrowserView的位置和大小,实现复杂的UI布局
结论: 综合考虑性能、隔离性、功能和官方支持,BrowserView 是构建 Electron 插件化架构的最理想选择
基于 BrowserView 的插件化实现
标准插件目录结构
一个标准的插件包应包含以下文件:
my-plugin/
├── index.html # 插件的 UI 入口文件
├── index.js # 插件的业务逻辑脚本
├── preload.js # 预加载脚本,用于访问 Node.js 和 openAPI
└── package.json # 插件元信息,如名称、版本、入口等插件加载流程
在主进程中可以创建一个函数来加载和管理插件 BrowserView
// main/pluginLoader.js
import { BrowserView } from "electron"
import path from "path"
function loadPlugin(plugin, parentWindow) {
const { indexPath, preloadPath } = plugin // 从插件信息中获取路径
// 1. 创建 BrowserView 实例
const view = new BrowserView({
webPreferences: {
preload: preloadPath, // 指定插件自己的 preload 脚本
nodeIntegration: true, // 为插件开启 Node.js 能力
contextIsolation: true, // 推荐开启上下文隔离以增强安全性
devTools: true // 开发时开启 DevTools
}
})
// 2. 将 BrowserView 附加到父窗口
parentWindow.addBrowserView(view)
// 3. 设置视图的位置和大小
// 初始可以先隐藏,加载完成后再显示
const bounds = parentWindow.getBounds()
view.setBounds({ x: 0, y: 60, width: bounds.width, height: bounds.height - 60 })
view.setAutoResize({ width: true, height: true })
// 4. 加载插件的入口 HTML
view.webContents.loadFile(indexPath)
// 5. 监听加载完成事件
view.webContents.on("dom-ready", () => {
console.log(`Plugin "${plugin.name}" loaded successfully!`)
// 此处可以执行显示视图、传递初始化数据等操作
})
return view
}通信机制: openAPI 桥接
为了让插件能够安全地调用主应用提供的原生能力,需要建立一个通信桥梁。这主要通过 preload 脚本和 Electron 的 IPC (进程间通信) 机制实现。其核心思想是:主应用不直接暴露危险的 Node.js 模块,而是提供封装好的、权限受控的 API。
openAPI 调用流程解析
当插件调用 window.rubick.someApi() 时,内部的数据流转和进程通信遵循以下步骤:
-
插件渲染进程 (BrowserView):
- 调用
window.rubick.someApi()。 - 这个
rubick对象是由contextBridge安全暴露的,它并非一个真正的API实现,而是一个通往主应用的“信使”。
- 调用
-
contextBridge(预加载脚本):contextBridge接收到调用请求。- 它将调用转发给
ipcRenderer,例如ipcRenderer.invoke('api-name', ...args)。contextBridge确保了插件代码无法直接访问ipcRenderer或其他Node.js模块,只能调用我们预先定义好的接口。
-
IPC (进程间通信):
ipcRenderer将请求通过Electron的IPC通道发送给主进程。
-
主进程 (
ipcMain):ipcMain监听到对应的频道(如'api-name')。- 执行注册的处理器函数,该函数拥有完整的
Node.js环境访问权限,可以执行文件操作、调用原生对话框、访问数据库等。
-
返回结果 (可选):
- 主进程的处理器函数执行完毕后,将结果
return。 IPC通道将结果传回给发起请求的ipcRenderer。
- 主进程的处理器函数执行完毕后,将结果
-
Promise解析 (插件渲染进程):ipcRenderer.invoke返回的Promise被解析,插件的await表达式拿到最终结果。
这个流程确保了插件在一个受限的环境中运行,所有特权操作都必须通过主进程代理,由主应用集中管控,从而实现了安全与功能的平衡。
步骤 1: 在主应用中定义 openAPI
在主应用的 preload 脚本中,通过 contextBridge 暴露一个全局的 API 对象给渲染进程(包括 BrowserView)
// main/preload.js
import { contextBridge, ipcRenderer } from "electron"
const openAPI = {
// 异步调用示例:显示系统通知
showNotification: (body) => ipcRenderer.invoke("show-notification", body),
// 异步调用并返回数据:选择一个目录
selectDirectory: () => ipcRenderer.invoke("select-directory"),
// 同步调用示例:获取应用版本号
getAppVersion: () => ipcRenderer.sendSync("get-app-version"),
// 监听主进程消息示例
onShowMessage: (callback) =>
ipcRenderer.on("show-message", (_event, value) => callback(value))
}
// 将 API 安全地暴露到 window 对象
contextBridge.exposeInMainWorld("rubick", openAPI)步骤 2: 在主进程中处理 API 调用
在主进程中,使用 ipcMain 监听并处理来自插件的请求
// main/index.js
import { ipcMain, Notification, dialog } from "electron"
import { version } from "../package.json"
// 处理 'show-notification'
ipcMain.handle("show-notification", (event, body) => {
// event.sender 来自于发起请求的 webContents
// 可以通过它找到对应的 BrowserView 或 BrowserWindow
if (Notification.isSupported()) {
new Notification({ title: "来自主应用的消息", body }).show()
}
})
// 处理 'select-directory'
ipcMain.handle("select-directory", async (event) => {
const { canceled, filePaths } = await dialog.showOpenDialog({
properties: ["openDirectory"]
})
if (canceled) {
return null
} else {
return filePaths[0]
}
})
// 处理同步请求
ipcMain.on("get-app-version", (event) => {
event.returnValue = version
})
// 主动向插件发送消息
function showMessageToPlugin(pluginView, message) {
pluginView.webContents.send("show-message", message)
}步骤 3: 在插件中使用 openAPI
现在,插件的渲染进程代码可以像调用本地 JavaScript 方法一样使用这些 API。
// my-plugin/index.js
// 调用 API 选择目录
document.getElementById("select-dir-btn").addEventListener("click", async () => {
const dirPath = await window.rubick.selectDirectory()
if (dirPath) {
console.log("Selected directory:", dirPath)
await window.rubick.showNotification(`你选择的目录是: ${dirPath}`)
}
})
// 调用同步 API
const version = window.rubick.getAppVersion()
console.log("App Version:", version)
// 监听主进程消息
window.rubick.onShowMessage((message) => {
alert(`Received message from main app: ${message}`)
})插件生命周期管理
一个完整的插件化系统不仅要能加载插件,还必须有效地管理它们的整个生命周期,从安装、更新到最终的卸载。这通常由一个专门的插件管理器来协调,其核心围绕以下几个方面展开:
-
插件存储:
- 位置: 已安装的插件通常存储在用户数据目录中,例如
app.getPath('userData')/plugins - 结构: 每个插件都是一个独立的子目录,例如
.../plugins/my-plugin/
- 位置: 已安装的插件通常存储在用户数据目录中,例如
-
插件清单:
- 主应用需要维护一个清单文件(如
plugins.json),记录所有已安装插件的元信息,包括:name: 插件唯一标识符version: 版本号path: 插件的本地绝对路径enabled: 插件是否启用
- 主应用需要维护一个清单文件(如
-
安装流程:
- 用户通过插件市场或本地文件选择一个插件包(如
.zip文件) - 主应用下载并校验插件包的完整性(例如通过
SHA256校验和) - 将插件包解压到
plugins目录下的一个新文件夹 - 读取插件的
package.json,获取元信息 - 将新插件的信息更新到插件清单中
- 动态加载新安装的插件
- 用户通过插件市场或本地文件选择一个插件包(如
-
卸载流程:
- 用户请求卸载插件
- 主应用首先需要卸载/销毁正在运行的插件实例(对应的
BrowserView) - 从插件清单中移除该插件的条目
- 从文件系统中安全地删除插件目录
-
更新流程:
- 检测到新版本(通过轮询插件市场或启动时检查)
- 遵循与安装类似的流程,下载并解压新版插件包到一个临时目录
- 卸载旧版本插件
- 将新版本插件从临时目录移动到正式的插件目录
- 更新插件清单中的版本号等信息
- 重新加载插件
注意: 在进行文件操作时,需要确保操作的原子性和安全性,避免因更新失败等原因导致插件损坏。详细的实现可以参考
rubick项目中的runner.ts和插件管理相关模块。
最佳实践与安全注意事项
-
上下文隔离 (
contextIsolation): 始终保持contextIsolation: true。这是Electron安全的核心建议,可以有效防止插件的JavaScript代码直接访问Electron内部API和Node.js全局变量,避免原型链污染 -
沙箱模式 (
sandbox): 对于不需要Node.js能力的纯UI插件,可以开启沙箱模式 (sandbox: true),以最小权限原则运行插件,最大程度地降低安全风险 -
权限控制: 不应将整个
fs或child_process等危险模块直接暴露给插件。应根据插件需求,封装粒度更细、权限更小的API。例如,提供selectDirectory()API 而不是直接暴露fs -
校验插件来源: 在加载插件前,应对其来源和完整性进行校验(如通过
SHA256校验和),确保插件未被篡改 -
网络请求限制 (
webRequest): 可以使用session.webRequestAPI 拦截和审查插件发出的网络请求,防止恶意数据外泄。例如,你可以为插件创建一个独立的session并只允许它请求特定的域名:javascript// main/pluginLoader.js const view = new BrowserView({ webPreferences: { // ... partition: `persist:plugin_${plugin.name}` // 为插件创建独立的 session } }) // ... // 拦截网络请求 view.webContents.session.webRequest.onBeforeRequest((details, callback) => { const requestUrl = new URL(details.url) const allowedDomains = ["api.trusted.com", "resources.trusted.com"] if ( requestUrl.protocol === "file:" || allowedDomains.includes(requestUrl.hostname) ) { callback({ cancel: false }) // 允许本地文件和受信任的域名 } else { console.warn( `[Security] Blocked unauthorized request from plugin '${plugin.name}' to: ${requestUrl.hostname}` ) callback({ cancel: true }) // 阻止所有其他请求 } })
通信机制流程图
┌──────────────────────────────────────────────────────────────────────────┐
│ openAPI 调用流程 │
├──────────────────────────────────────────────────────────────────────────┤
│ │
│ ① 插件调用 API │
│ ┌─────────────────────────────────────────┐ │
│ │ 插件渲染进程 (BrowserView) │ │
│ │ │ │
│ │ const result = await window.rubick │ │
│ │ .selectDirectory() │ │
│ └───────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ② contextBridge 转发 │
│ ┌─────────────────────────────────────────┐ │
│ │ preload.js │ │
│ │ │ │
│ │ contextBridge.exposeInMainWorld({ │ │
│ │ selectDirectory: () => │ │
│ │ ipcRenderer.invoke('select-dir') │ │
│ │ }) │ │
│ └───────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ③ IPC 通道传输 │
│ ════════════════════════════════════════ │
│ │ │
│ ▼ │
│ ④ 主进程处理 │
│ ┌─────────────────────────────────────────┐ │
│ │ 主进程 (Main Process) │ │
│ │ │ │
│ │ ipcMain.handle('select-dir', async │ │
│ │ (event) => { │ │
│ │ const result = await dialog │ │
│ │ .showOpenDialog(...) │ │
│ │ return result.filePaths[0] │ │
│ │ }) │ │
│ └───────────────────┬─────────────────────┘ │
│ │ │
│ ▼ │
│ ⑤ 返回结果 │
│ ════════════════════════════════════════ │
│ │ │
│ ▼ │
│ ⑥ Promise 解析 │
│ ┌─────────────────────────────────────────┐ │
│ │ 插件渲染进程 │ │
│ │ │ │
│ │ // result = "/selected/path" │ │
│ │ console.log(result) │ │
│ └─────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────┘openAPI 接口说明
文件系统 API
// 预加载脚本中定义
const fileAPI = {
// 选择文件
selectFile: (options?: OpenDialogOptions) =>
ipcRenderer.invoke('file:select', options),
// 选择目录
selectDirectory: () =>
ipcRenderer.invoke('file:selectDir'),
// 读取文件
readFile: (path: string) =>
ipcRenderer.invoke('file:read', path),
// 写入文件
writeFile: (path: string, content: string) =>
ipcRenderer.invoke('file:write', path, content),
// 删除文件
deleteFile: (path: string) =>
ipcRenderer.invoke('file:delete', path),
// 获取文件信息
getFileInfo: (path: string) =>
ipcRenderer.invoke('file:info', path)
}系统交互 API
const systemAPI = {
// 显示通知
showNotification: (options: NotificationOptions) =>
ipcRenderer.invoke('notification:show', options),
// 显示消息框
showMessage: (options: MessageBoxOptions) =>
ipcRenderer.invoke('dialog:message', options),
// 打开外部链接
openExternal: (url: string) =>
ipcRenderer.invoke('shell:openExternal', url),
// 获取剪贴板内容
getClipboardText: () =>
ipcRenderer.invoke('clipboard:readText'),
// 设置剪贴板内容
setClipboardText: (text: string) =>
ipcRenderer.invoke('clipboard:writeText', text),
// 获取系统信息
getSystemInfo: () =>
ipcRenderer.invoke('system:info')
}窗口控制 API
const windowAPI = {
// 最小化窗口
minimize: () =>
ipcRenderer.invoke('window:minimize'),
// 最大化窗口
maximize: () =>
ipcRenderer.invoke('window:maximize'),
// 关闭窗口
close: () =>
ipcRenderer.invoke('window:close'),
// 设置窗口大小
setSize: (width: number, height: number) =>
ipcRenderer.invoke('window:setSize', width, height),
// 设置窗口位置
setPosition: (x: number, y: number) =>
ipcRenderer.invoke('window:setPosition', x, y)
}数据存储 API
const storageAPI = {
// 存储数据
set: (key: string, value: any) =>
ipcRenderer.invoke('storage:set', key, value),
// 获取数据
get: (key: string) =>
ipcRenderer.invoke('storage:get', key),
// 删除数据
delete: (key: string) =>
ipcRenderer.invoke('storage:delete', key),
// 清空数据
clear: () =>
ipcRenderer.invoke('storage:clear')
}完整插件开发示例
插件目录结构
my-plugin/
├── package.json # 插件元信息
├── index.html # UI 入口
├── src/
│ ├── index.js # 主逻辑
│ ├── style.css # 样式
│ └── components/ # 组件
└── preload.js # 预加载脚本(可选)package.json
{
"name": "my-plugin",
"version": "1.0.0",
"description": "一个示例插件",
"author": "Developer",
"main": "./index.html",
"pluginName": "我的插件",
"logo": "./logo.png",
"pluginType": "ui",
"features": [
{
"explain": "启动插件",
"cmds": ["my-plugin", "我的插件", "demo"]
}
]
}index.html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>我的插件</title>
<link rel="stylesheet" href="./src/style.css">
</head>
<body>
<div id="app">
<h1>文件管理器</h1>
<div class="toolbar">
<button id="selectBtn">选择文件</button>
<button id="saveBtn">保存内容</button>
</div>
<div class="content">
<textarea id="editor" placeholder="文件内容将显示在这里..."></textarea>
</div>
<div id="status"></div>
</div>
<script src="./src/index.js"></script>
</body>
</html>src/index.js
// 插件主逻辑
class FileManager {
constructor() {
this.currentFile = null
this.initUI()
this.bindEvents()
}
initUI() {
this.editor = document.getElementById('editor')
this.statusEl = document.getElementById('status')
this.selectBtn = document.getElementById('selectBtn')
this.saveBtn = document.getElementById('saveBtn')
}
bindEvents() {
// 选择文件
this.selectBtn.addEventListener('click', async () => {
try {
const filePath = await window.rubick.selectFile()
if (filePath) {
await this.loadFile(filePath)
}
} catch (error) {
this.showStatus(`错误: ${error.message}`, 'error')
}
})
// 保存文件
this.saveBtn.addEventListener('click', async () => {
if (!this.currentFile) {
const filePath = await window.rubick.selectFile({
action: 'save'
})
if (filePath) {
this.currentFile = filePath
} else {
return
}
}
await this.saveFile()
})
}
async loadFile(filePath) {
const content = await window.rubick.readFile(filePath)
this.editor.value = content
this.currentFile = filePath
this.showStatus(`已加载: ${filePath}`, 'success')
}
async saveFile() {
const content = this.editor.value
await window.rubick.writeFile(this.currentFile, content)
this.showStatus(`已保存: ${this.currentFile}`, 'success')
// 发送通知
await window.rubick.showNotification({
title: '保存成功',
body: `文件已保存到 ${this.currentFile}`
})
}
showStatus(message, type = 'info') {
this.statusEl.textContent = message
this.statusEl.className = `status ${type}`
setTimeout(() => {
this.statusEl.textContent = ''
}, 3000)
}
}
// 初始化插件
new FileManager()src/style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 16px;
background: #f5f5f5;
}
#app {
display: flex;
flex-direction: column;
height: 100vh;
}
h1 {
font-size: 18px;
margin-bottom: 12px;
}
.toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #1890ff;
color: white;
cursor: pointer;
transition: background 0.3s;
}
button:hover {
background: #40a9ff;
}
.content {
flex: 1;
display: flex;
}
#editor {
width: 100%;
height: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
resize: none;
}
#status {
margin-top: 12px;
padding: 8px;
border-radius: 4px;
font-size: 12px;
}
#status.success {
background: #f6ffed;
color: #52c41a;
border: 1px solid #b7eb8f;
}
#status.error {
background: #fff2f0;
color: #ff4d4f;
border: 1px solid #ffccc7;
}常见问题解答
Q1: 插件加载失败怎么办?
检查步骤:
- 确认插件目录结构正确
- 检查
package.json格式是否正确 - 查看
index.html路径是否存在 - 检查 BrowserView 的
webPreferences配置
// 添加错误监听
view.webContents.on('did-fail-load', (event, code, desc) => {
console.error(`插件加载失败: ${code} - ${desc}`)
})Q2: 插件如何调试?
方法一:通过 DevTools
// 在主进程中打开插件 DevTools
view.webContents.openDevTools()方法二:通过快捷键
在开发环境中,可以注册快捷键打开当前插件的 DevTools:
const { globalShortcut } = require('electron')
globalShortcut.register('CommandOrControl+Shift+I', () => {
const focusedView = getFocusedBrowserView()
focusedView?.webContents.openDevTools()
})Q3: 如何实现插件间通信?
可以通过主进程作为中转站:
// 主进程
ipcMain.handle('plugin:send', (event, targetPlugin, message) => {
const targetView = pluginManager.getView(targetPlugin)
if (targetView) {
targetView.webContents.send('plugin:message', {
from: getPluginByWebContents(event.sender),
message
})
}
})
// 插件中发送
await window.rubick.sendToPlugin('other-plugin', { type: 'greeting' })
// 插件中接收
window.rubick.onPluginMessage((data) => {
console.log('收到消息:', data)
})Q4: 如何处理插件崩溃?
view.webContents.on('render-process-gone', (event, details) => {
console.error(`插件崩溃: ${details.reason}`)
// 自动重启插件
setTimeout(() => {
reloadPlugin(plugin)
}, 1000)
})
view.webContents.on('unresponsive', () => {
console.warn('插件无响应')
// 可以选择重新加载
view.webContents.reload()
})
view.webContents.on('responsive', () => {
console.log('插件已恢复响应')
})Q5: 如何限制插件的内存和 CPU 使用?
// 可以定期检查插件的资源使用情况
setInterval(() => {
const views = pluginManager.getAllViews()
views.forEach(view => {
view.webContents.getProcessMemoryInfo().then(info => {
if (info.privateBytes > 100 * 1024 * 1024) { // 100MB
console.warn('插件内存使用过高')
// 可以选择重启或卸载插件
}
})
})
}, 30000) // 每30秒检查一次