前言
你有没有想过:
- Chrome扩展是怎么"注入"到网页里的?
- VSCode的几万个插件是怎么加载的?
- 为什么插件崩溃不会搞崩主程序?
- 怎么设计一个自己的插件系统?
今天从原理到实现,彻底搞懂插件加载机制。
插件系统的本质
┌─────────────────────────────────────────────────────────────────────────────┐
│ 插件系统的核心问题 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【什么是插件?】 │
│ ───────────────────────────────────────── │
│ │
│ 插件 = 在不修改主程序代码的情况下,扩展主程序功能的模块 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 主程序 (Host) │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ 核心功能 │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │
│ │ │ │ 插件接口 │ │ 插件接口 │ │ 插件接口 │ │ │ │
│ │ │ │ (Hook) │ │ (Hook) │ │ (Hook) │ │ │ │
│ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │
│ │ │ │ │ │ │ │ │
│ │ └──────────┼────────────────┼────────────────┼────────────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │ 插件A │ │ 插件B │ │ 插件C │ │ │
│ │ └────────┘ └────────┘ └────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【插件系统要解决的问题】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 发现: 怎么找到插件? │
│ 2. 加载: 怎么把插件代码加载进来? │
│ 3. 隔离: 怎么防止插件搞崩主程序? │
│ 4. 通信: 主程序和插件怎么交互? │
│ 5. 生命周期: 怎么管理插件的启动/停止/卸载? │
│ 6. 权限: 怎么限制插件能做什么? │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
插件加载的通用模式
┌─────────────────────────────────────────────────────────────────────────────┐
│ 插件加载的几种模式 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【模式1: 动态链接库 (DLL/SO)】 │
│ ───────────────────────────────────────── │
│ │
│ 典型应用: Photoshop滤镜、游戏MOD、音频插件(VST) │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 主程序 插件 (plugin.dll) │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ │ LoadLibrary │ │ │ │
│ │ │ 扫描目录 │──────────────►│ 导出函数 │ │ │
│ │ │ │ │ - init() │ │ │
│ │ │ 加载DLL │ GetProcAddr │ - process() │ │ │
│ │ │ │◄──────────────│ - cleanup() │ │ │
│ │ │ 调用函数 │ │ │ │ │
│ │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 优点: 性能最好,可以用任何语言写 │
│ 缺点: 不安全,崩溃会影响主程序,跨平台麻烦 │
│ │
│ 【模式2: 脚本解释执行】 │
│ ───────────────────────────────────────── │
│ │
│ 典型应用: 游戏Lua脚本、Emacs Lisp、Vim脚本 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 主程序 │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ 脚本引擎 │ │ 插件脚本 │ │ │ │
│ │ │ │ (Lua/Python) │◄─────│ plugin.lua │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ 解释执行 │ │ function xxx │ │ │ │
│ │ │ │ 提供API │ │ end │ │ │ │
│ │ │ └─────────────────┘ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 优点: 相对安全,热加载方便,跨平台 │
│ 缺点: 性能较低,功能受限于暴露的API │
│ │
│ 【模式3: 独立进程 + IPC】 │
│ ───────────────────────────────────────── │
│ │
│ 典型应用: Chrome扩展、VSCode插件、Figma插件 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 主进程 插件进程 │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ │ │ │ │ │
│ │ │ 核心功能 │ IPC/消息传递 │ 插件代码 │ │ │
│ │ │ │◄─────────────────►│ │ │ │
│ │ │ 插件管理 │ (JSON-RPC等) │ 沙箱环境 │ │ │
│ │ │ │ │ │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ │ │ │
│ │ 主进程和插件进程完全隔离 │ │
│ │ 插件崩溃不影响主程序 │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 优点: 最安全,隔离性好,崩溃不影响主程序 │
│ 缺点: 通信开销,内存占用高 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Chrome扩展加载机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ Chrome扩展架构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【扩展的组成部分】 │
│ ───────────────────────────────────────── │
│ │
│ my-extension/ │
│ ├── manifest.json # 扩展清单 (必需) │
│ ├── background.js # 后台脚本 (Service Worker) │
│ ├── content.js # 内容脚本 (注入到网页) │
│ ├── popup.html # 弹出页面 │
│ ├── popup.js │
│ ├── options.html # 设置页面 │
│ └── icons/ # 图标 │
│ │
│ 【manifest.json 示例】 │
│ ───────────────────────────────────────── │
│ │
│ { │
│ "manifest_version": 3, │
│ "name": "My Extension", │
│ "version": "1.0", │
│ "permissions": ["tabs", "storage", "activeTab"], │
│ "host_permissions": ["https://*.example.com/*"], │
│ "background": { │
│ "service_worker": "background.js" │
│ }, │
│ "content_scripts": [{ │
│ "matches": ["https://*.google.com/*"], │
│ "js": ["content.js"], │
│ "run_at": "document_idle" │
│ }], │
│ "action": { │
│ "default_popup": "popup.html", │
│ "default_icon": "icon.png" │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Chrome扩展的进程模型
┌─────────────────────────────────────────────────────────────────────────────┐
│ Chrome扩展进程模型 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Browser Process │ │
│ │ (浏览器主进程) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Extension System │ │ │
│ │ │ │ │ │
│ │ │ - 扩展注册表 │ │ │
│ │ │ - 权限管理 │ │ │
│ │ │ - 消息路由 │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ └──────────────────────────────┼──────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┼───────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Extension │ │ Renderer │ │ Renderer │ │
│ │ Process │ │ Process │ │ Process │ │
│ │ │ │ (网页A) │ │ (网页B) │ │
│ │ ┌─────────┐ │ │ │ │ │ │
│ │ │Service │ │ IPC │ ┌─────────┐ │ │ ┌─────────┐ │ │
│ │ │Worker │ │◄───────►│ │Content │ │ │ │Content │ │ │
│ │ │(后台) │ │ │ │Script │ │ │ │Script │ │ │
│ │ └─────────┘ │ │ └─────────┘ │ │ └─────────┘ │ │
│ │ │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ 【各部分的运行环境】 │
│ ───────────────────────────────────────── │
│ │
│ 1. Service Worker (后台脚本) │
│ - 运行在独立的扩展进程中 │
│ - 可以访问全部Chrome API │
│ - 没有DOM访问权限 │
│ - 事件驱动,空闲时会被终止 │
│ │
│ 2. Content Script (内容脚本) │
│ - 运行在网页的渲染进程中 │
│ - 可以访问网页DOM │
│ - 只能用部分Chrome API │
│ - 与网页JS隔离 (不同的JS上下文) │
│ │
│ 3. Popup/Options页面 │
│ - 运行在扩展进程中 │
│ - 完整的HTML页面 │
│ - 可以访问Chrome API │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Content Script注入机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ Content Script 注入原理 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【注入时机】 │
│ ───────────────────────────────────────── │
│ │
│ manifest.json中的 "run_at" 选项: │
│ │
│ document_start: DOM构建前注入 (最早) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ HTML下载 → [注入] → DOM解析 → DOMContentLoaded → load │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ document_end: DOM构建完成后注入 │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ HTML下载 → DOM解析 → [注入] → DOMContentLoaded → load │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ document_idle: 页面空闲时注入 (默认,最安全) │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ HTML下载 → DOM解析 → DOMContentLoaded → [注入] → load │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ 【隔离的世界 (Isolated World)】 │
│ ───────────────────────────────────────── │
│ │
│ Content Script 和网页JS运行在不同的"世界"中: │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Renderer Process │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ V8 引擎 │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │
│ │ │ │ Main World │ │ Isolated World │ │ │ │
│ │ │ │ (网页JS) │ │ (Content Script)│ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ │ │ window.xxx │ 共 │ 自己的window │ │ │ │
│ │ │ │ 自定义全局变量 │ 享 │ chrome.* API │ │ │ │
│ │ │ │ │ DOM │ │ │ │ │
│ │ │ │ │◄───────►│ │ │ │ │
│ │ │ │ 可能有恶意代码 │ │ 扩展的代码 │ │ │ │
│ │ │ └─────────────────┘ └─────────────────┘ │ │ │
│ │ │ │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 两个世界: │
│ - 共享DOM: 都可以读写DOM │
│ - 隔离JS: 不共享全局变量、原型链 │
│ - 好处: 网页不能篡改扩展代码,扩展不会污染网页 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Chrome扩展消息传递
javascript
/**
* Chrome扩展消息传递机制
*/
// ==================== Content Script ====================
// content.js - 运行在网页中
// 1. 向Background发送消息
chrome.runtime.sendMessage(
{ type: 'getData', url: window.location.href },
(response) => {
console.log('收到回复:', response);
}
);
// 2. 监听来自Background的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === 'executeTask') {
// 执行任务
const result = doSomething(message.data);
sendResponse({ success: true, result });
}
return true; // 保持消息通道开放(异步响应需要)
});
// 3. 与网页通信 (通过DOM事件)
// Content Script → 网页
window.postMessage({ source: 'my-extension', data: 'hello' }, '*');
// 网页 → Content Script
window.addEventListener('message', (event) => {
if (event.data.source === 'webpage') {
console.log('收到网页消息:', event.data);
}
});
// ==================== Background (Service Worker) ====================
// background.js
// 1. 监听来自Content Script的消息
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
console.log('来自:', sender.tab?.url);
if (message.type === 'getData') {
// 可以执行网络请求等操作
fetch('https://api.example.com/data')
.then(r => r.json())
.then(data => sendResponse({ data }))
.catch(err => sendResponse({ error: err.message }));
return true; // 异步响应
}
});
// 2. 向特定标签页发送消息
async function sendToTab(tabId, message) {
try {
const response = await chrome.tabs.sendMessage(tabId, message);
return response;
} catch (err) {
console.error('发送失败:', err);
}
}
// 3. 向所有标签页广播
async function broadcast(message) {
const tabs = await chrome.tabs.query({});
for (const tab of tabs) {
try {
await chrome.tabs.sendMessage(tab.id, message);
} catch (err) {
// 某些页面可能没有注入content script
}
}
}
// 4. 长连接 (Port)
chrome.runtime.onConnect.addListener((port) => {
console.log('建立连接:', port.name);
port.onMessage.addListener((msg) => {
console.log('收到:', msg);
port.postMessage({ reply: 'got it' });
});
port.onDisconnect.addListener(() => {
console.log('连接断开');
});
});
// ==================== Popup ====================
// popup.js
// 向Background发送消息
document.getElementById('btn').addEventListener('click', async () => {
const response = await chrome.runtime.sendMessage({ type: 'action' });
console.log(response);
});
// 获取当前标签页并发送消息
async function sendToCurrentTab(message) {
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
const response = await chrome.tabs.sendMessage(tab.id, message);
return response;
}
Chrome扩展加载流程
┌─────────────────────────────────────────────────────────────────────────────┐
│ Chrome扩展加载流程 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【安装阶段】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 用户安装扩展 (商店/开发者模式) │
│ │ │
│ ▼ │
│ 2. Chrome解析 manifest.json │
│ │ - 检查版本兼容性 │
│ │ - 验证权限声明 │
│ │ - 解析各组件配置 │
│ ▼ │
│ 3. 注册扩展到Extension System │
│ │ - 分配唯一ID │
│ │ - 建立权限映射 │
│ │ - 注册URL匹配规则 │
│ ▼ │
│ 4. 启动Service Worker (如果有) │
│ │ - 创建扩展进程 │
│ │ - 执行background.js │
│ │ - 注册事件监听 │
│ ▼ │
│ 5. 安装完成 │
│ │
│ 【页面加载阶段】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 用户访问网页 │
│ │ │
│ ▼ │
│ 2. Chrome检查URL匹配规则 │
│ │ matches: ["https://*.google.com/*"] │
│ ▼ │
│ 3. 匹配成功 → 准备注入Content Script │
│ │ │
│ ▼ │
│ 4. 等待注入时机 (run_at) │
│ │ document_start / document_end / document_idle │
│ ▼ │
│ 5. 创建Isolated World │
│ │ - 独立的JS执行环境 │
│ │ - 注入chrome.* API │
│ ▼ │
│ 6. 执行Content Script │
│ │ - 可以访问DOM │
│ │ - 与网页JS隔离 │
│ ▼ │
│ 7. 建立与Background的通信通道 │
│ │
│ 【事件驱动】 │
│ ───────────────────────────────────────── │
│ │
│ Service Worker空闲30秒后会被终止 │
│ 收到消息/事件时重新唤醒 │
│ │
│ 事件 ──► 唤醒SW ──► 处理 ──► 响应 ──► 空闲 ──► 终止 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
VSCode插件加载机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ VSCode架构概览 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ VSCode = Electron + Monaco Editor + Extension System │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Electron 应用 │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Main Process │ │ │
│ │ │ (Node.js) │ │ │
│ │ │ │ │ │
│ │ │ - 窗口管理 │ │ │
│ │ │ - 文件系统访问 │ │ │
│ │ │ - 插件进程管理 │ │ │
│ │ │ │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌───────────────────┼───────────────────┐ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Renderer │ │ Extension │ │ Extension │ │ │
│ │ │ Process │ │ Host │ │ Host │ │ │
│ │ │ │ │ (Node.js) │ │ (Web Worker)│ │ │
│ │ │ - UI渲染 │ │ │ │ │ │ │
│ │ │ - Monaco │ │ - 插件A │ │ - 插件D │ │ │
│ │ │ - Workbench│ │ - 插件B │ │ - 插件E │ │ │
│ │ │ │ │ - 插件C │ │ │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【两种Extension Host】 │
│ ───────────────────────────────────────── │
│ │
│ 1. Node.js Extension Host (本地) │
│ - 完整的Node.js环境 │
│ - 可以访问文件系统、运行子进程 │
│ - 大部分插件在这里运行 │
│ │
│ 2. Web Extension Host (浏览器/远程) │
│ - Web Worker环境 │
│ - 用于vscode.dev/远程开发 │
│ - 功能受限 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
VSCode插件结构
┌─────────────────────────────────────────────────────────────────────────────┐
│ VSCode插件结构 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ my-extension/ │
│ ├── package.json # 插件清单 (必需) │
│ ├── src/ │
│ │ └── extension.ts # 插件入口 │
│ ├── out/ # 编译输出 │
│ │ └── extension.js │
│ ├── node_modules/ │
│ └── README.md │
│ │
│ 【package.json 关键字段】 │
│ ───────────────────────────────────────── │
│ │
│ { │
│ "name": "my-extension", │
│ "displayName": "My Extension", │
│ "version": "1.0.0", │
│ "engines": { "vscode": "^1.74.0" }, │
│ │
│ // 入口文件 │
│ "main": "./out/extension.js", │
│ │
│ // 激活事件 (什么时候加载插件) │
│ "activationEvents": [ │
│ "onLanguage:javascript", // 打开JS文件时 │
│ "onCommand:myext.hello", // 执行命令时 │
│ "onView:myExtView", // 打开视图时 │
│ "workspaceContains:**/.git", // 工作区包含.git时 │
│ "*" // 启动时立即加载 (不推荐) │
│ ], │
│ │
│ // 贡献点 (向VSCode注册功能) │
│ "contributes": { │
│ "commands": [{ │
│ "command": "myext.hello", │
│ "title": "Hello World" │
│ }], │
│ "languages": [...], │
│ "grammars": [...], │
│ "themes": [...], │
│ "views": [...], │
│ "menus": [...], │
│ "configuration": [...] │
│ } │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
VSCode插件激活机制
┌─────────────────────────────────────────────────────────────────────────────┐
│ VSCode插件激活机制 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ 【懒加载设计】 │
│ ───────────────────────────────────────── │
│ │
│ VSCode有几万个插件,不可能全部一起加载 │
│ 所以采用"按需激活"策略 │
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ VSCode启动 │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 扫描所有插件的 package.json │ │
│ │ │ │ │
│ │ ├──► 读取 activationEvents │ │
│ │ │ │ │
│ │ ├──► 读取 contributes (注册命令/菜单/视图等) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 建立激活事件映射表 │ │
│ │ ┌──────────────────────────────────────────────────────────────┐ │ │
│ │ │ onLanguage:javascript → [插件A, 插件B] │ │ │
│ │ │ onLanguage:python → [插件C] │ │ │
│ │ │ onCommand:ext.hello → [插件A] │ │ │
│ │ │ onView:gitHistory → [插件D] │ │ │
│ │ └──────────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ 等待激活事件触发... │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【激活流程】 │
│ ───────────────────────────────────────── │
│ │
│ 用户打开 .js 文件 │
│ │ │
│ ▼ │
│ 触发 onLanguage:javascript 事件 │
│ │ │
│ ▼ │
│ 查找映射表,找到需要激活的插件 [A, B] │
│ │ │
│ ▼ │
│ 对每个插件: │
│ │ │
│ ├──► 1. 在Extension Host中创建沙箱环境 │
│ │ │
│ ├──► 2. 加载插件的main文件 (require) │
│ │ │
│ ├──► 3. 调用 activate() 函数 │
│ │ │ │
│ │ ▼ │
│ │ export function activate(context) { │
│ │ // 插件初始化代码 │
│ │ // 注册命令、事件监听等 │
│ │ } │
│ │ │
│ └──► 4. 插件开始工作 │
│ │
│ 【卸载流程】 │
│ ───────────────────────────────────────── │
│ │
│ VSCode关闭/插件禁用 │
│ │ │
│ ▼ │
│ 调用 deactivate() 函数 │
│ │ │
│ ▼ │
│ export function deactivate() { │
│ // 清理资源 │
│ } │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
VSCode插件代码示例
typescript
/**
* VSCode插件示例
*/
import * as vscode from 'vscode';
// ==================== 激活函数 ====================
export function activate(context: vscode.ExtensionContext) {
console.log('插件已激活');
// 1. 注册命令
const disposable = vscode.commands.registerCommand('myext.hello', () => {
vscode.window.showInformationMessage('Hello World!');
});
context.subscriptions.push(disposable);
// 2. 注册代码补全
const completionProvider = vscode.languages.registerCompletionItemProvider(
'javascript',
{
provideCompletionItems(document, position) {
const completionItem = new vscode.CompletionItem('mySnippet');
completionItem.insertText = new vscode.SnippetString('console.log($1);');
return [completionItem];
}
},
'.' // 触发字符
);
context.subscriptions.push(completionProvider);
// 3. 注册Hover提示
const hoverProvider = vscode.languages.registerHoverProvider('javascript', {
provideHover(document, position) {
const range = document.getWordRangeAtPosition(position);
const word = document.getText(range);
if (word === 'console') {
return new vscode.Hover('这是控制台对象');
}
}
});
context.subscriptions.push(hoverProvider);
// 4. 监听文档变化
vscode.workspace.onDidChangeTextDocument(event => {
console.log('文档变化:', event.document.fileName);
});
// 5. 监听配置变化
vscode.workspace.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('myext.setting')) {
const value = vscode.workspace.getConfiguration('myext').get('setting');
console.log('配置变化:', value);
}
});
// 6. 创建状态栏项
const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
statusBar.text = '$(check) My Extension';
statusBar.command = 'myext.hello';
statusBar.show();
context.subscriptions.push(statusBar);
// 7. 创建TreeView
const treeDataProvider = new MyTreeDataProvider();
vscode.window.createTreeView('myExtView', {
treeDataProvider
});
// 8. 创建Webview
const panel = vscode.window.createWebviewPanel(
'myWebview',
'My Webview',
vscode.ViewColumn.One,
{ enableScripts: true }
);
panel.webview.html = getWebviewContent();
// Webview通信
panel.webview.onDidReceiveMessage(message => {
console.log('收到Webview消息:', message);
});
panel.webview.postMessage({ type: 'update', data: 'hello' });
}
// ==================== 卸载函数 ====================
export function deactivate() {
console.log('插件已停用');
}
// ==================== TreeView数据提供者 ====================
class MyTreeDataProvider implements vscode.TreeDataProvider<string> {
getTreeItem(element: string): vscode.TreeItem {
return new vscode.TreeItem(element);
}
getChildren(element?: string): string[] {
if (!element) {
return ['Item 1', 'Item 2', 'Item 3'];
}
return [];
}
}
// ==================== Webview内容 ====================
function getWebviewContent(): string {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { padding: 20px; }
</style>
</head>
<body>
<h1>Hello Webview</h1>
<button onclick="sendMessage()">发送消息</button>
<script>
const vscode = acquireVsCodeApi();
function sendMessage() {
vscode.postMessage({ type: 'click', data: 'button clicked' });
}
window.addEventListener('message', event => {
console.log('收到:', event.data);
});
</script>
</body>
</html>
`;
}
VSCode Extension Host通信
┌─────────────────────────────────────────────────────────────────────────────┐
│ VSCode进程间通信 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Renderer Process │ │
│ │ (UI / Workbench) │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────────────────────────┐ │ │
│ │ │ Main Thread │ │ │
│ │ │ │ │ │
│ │ │ ExtHostProxy ExtHostProxy │ │ │
│ │ │ (commands) (languages) │ │ │
│ │ │ │ │ │ │ │
│ │ └───────┼───────────────────────────────┼─────────────────────┘ │ │
│ │ │ │ │ │
│ └───────────┼───────────────────────────────┼─────────────────────────┘ │
│ │ JSON-RPC │ │
│ │ (通过IPC通道) │ │
│ ┌───────────┼───────────────────────────────┼─────────────────────────┐ │
│ │ │ │ │ │
│ │ ┌───────┼───────────────────────────────┼─────────────────────┐ │ │
│ │ │ ▼ ▼ │ │ │
│ │ │ ExtHostCommands ExtHostLanguages │ │ │
│ │ │ │ │ │
│ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │
│ │ │ │ 插件A │ │ 插件B │ │ 插件C │ │ 插件D │ │ │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ │ │ Node.js │ │ Node.js │ │ Node.js │ │ Node.js │ │ │ │
│ │ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │ │
│ │ │ │ │ │
│ │ │ Extension Host Process │ │ │
│ │ └─────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ Extension Host Process (Node.js) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
│ 【通信协议: JSON-RPC】 │
│ ───────────────────────────────────────── │
│ │
│ 请求: │
│ { │
│ "jsonrpc": "2.0", │
│ "id": 1, │
│ "method": "ExtHostCommands.executeCommand", │
│ "params": ["myext.hello", []] │
│ } │
│ │
│ 响应: │
│ { │
│ "jsonrpc": "2.0", │
│ "id": 1, │
│ "result": { ... } │
│ } │
│ │
│ 【为什么用独立进程?】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 隔离性: 插件崩溃不影响UI │
│ 2. 性能: 插件运算不阻塞UI渲染 │
│ 3. 安全: 可以限制插件权限 │
│ 4. 调试: 可以单独调试插件进程 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
自己实现一个简单的插件系统
typescript
/**
* 简单插件系统实现
*
* 演示插件系统的核心概念
*/
// ==================== 类型定义 ====================
interface Plugin {
name: string;
version: string;
activate(context: PluginContext): void;
deactivate?(): void;
}
interface PluginContext {
subscriptions: Disposable[];
registerCommand(name: string, handler: Function): Disposable;
registerHook(event: string, handler: Function): Disposable;
}
interface Disposable {
dispose(): void;
}
interface PluginManifest {
name: string;
version: string;
main: string;
activationEvents: string[];
}
// ==================== 插件管理器 ====================
class PluginManager {
private plugins: Map<string, PluginInstance> = new Map();
private commands: Map<string, Function> = new Map();
private hooks: Map<string, Set<Function>> = new Map();
private activationMap: Map<string, string[]> = new Map();
/**
* 扫描并注册插件
*/
async scanPlugins(pluginDir: string): Promise<void> {
const fs = require('fs');
const path = require('path');
const entries = fs.readdirSync(pluginDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const manifestPath = path.join(pluginDir, entry.name, 'package.json');
if (fs.existsSync(manifestPath)) {
const manifest: PluginManifest = JSON.parse(
fs.readFileSync(manifestPath, 'utf-8')
);
console.log(`发现插件: ${manifest.name}@${manifest.version}`);
// 注册激活事件
for (const event of manifest.activationEvents) {
if (!this.activationMap.has(event)) {
this.activationMap.set(event, []);
}
this.activationMap.get(event)!.push(manifest.name);
}
// 存储插件信息(未激活)
this.plugins.set(manifest.name, {
manifest,
path: path.join(pluginDir, entry.name),
activated: false,
instance: null,
context: null,
});
}
}
}
}
/**
* 触发激活事件
*/
async triggerActivation(event: string): Promise<void> {
const pluginNames = this.activationMap.get(event);
if (!pluginNames) return;
for (const name of pluginNames) {
await this.activatePlugin(name);
}
}
/**
* 激活插件
*/
async activatePlugin(name: string): Promise<void> {
const pluginInstance = this.plugins.get(name);
if (!pluginInstance || pluginInstance.activated) {
return;
}
console.log(`激活插件: ${name}`);
try {
// 加载插件模块
const mainPath = require('path').join(
pluginInstance.path,
pluginInstance.manifest.main
);
const plugin: Plugin = require(mainPath);
// 创建上下文
const context = this.createContext(name);
// 调用activate
plugin.activate(context);
// 更新状态
pluginInstance.activated = true;
pluginInstance.instance = plugin;
pluginInstance.context = context;
console.log(`插件 ${name} 激活成功`);
} catch (error) {
console.error(`插件 ${name} 激活失败:`, error);
}
}
/**
* 停用插件
*/
async deactivatePlugin(name: string): Promise<void> {
const pluginInstance = this.plugins.get(name);
if (!pluginInstance || !pluginInstance.activated) {
return;
}
console.log(`停用插件: ${name}`);
// 调用deactivate
if (pluginInstance.instance?.deactivate) {
try {
pluginInstance.instance.deactivate();
} catch (error) {
console.error(`插件 ${name} 停用出错:`, error);
}
}
// 清理订阅
if (pluginInstance.context) {
for (const sub of pluginInstance.context.subscriptions) {
sub.dispose();
}
}
pluginInstance.activated = false;
pluginInstance.instance = null;
pluginInstance.context = null;
}
/**
* 创建插件上下文
*/
private createContext(pluginName: string): PluginContext {
const subscriptions: Disposable[] = [];
const self = this;
return {
subscriptions,
registerCommand(name: string, handler: Function): Disposable {
const fullName = `${pluginName}.${name}`;
self.commands.set(fullName, handler);
console.log(`注册命令: ${fullName}`);
return {
dispose() {
self.commands.delete(fullName);
}
};
},
registerHook(event: string, handler: Function): Disposable {
if (!self.hooks.has(event)) {
self.hooks.set(event, new Set());
}
self.hooks.get(event)!.add(handler);
console.log(`注册Hook: ${event}`);
return {
dispose() {
self.hooks.get(event)?.delete(handler);
}
};
}
};
}
/**
* 执行命令
*/
async executeCommand(name: string, ...args: any[]): Promise<any> {
const handler = this.commands.get(name);
if (!handler) {
throw new Error(`命令不存在: ${name}`);
}
return handler(...args);
}
/**
* 触发Hook
*/
async triggerHook(event: string, ...args: any[]): Promise<void> {
// 先触发可能的激活事件
await this.triggerActivation(`onHook:${event}`);
const handlers = this.hooks.get(event);
if (!handlers) return;
for (const handler of handlers) {
try {
await handler(...args);
} catch (error) {
console.error(`Hook处理出错:`, error);
}
}
}
/**
* 获取所有插件状态
*/
getPluginList(): { name: string; activated: boolean }[] {
return Array.from(this.plugins.entries()).map(([name, instance]) => ({
name,
activated: instance.activated,
}));
}
}
interface PluginInstance {
manifest: PluginManifest;
path: string;
activated: boolean;
instance: Plugin | null;
context: PluginContext | null;
}
// ==================== 示例插件 ====================
// plugins/hello-plugin/package.json
const helloManifest = {
name: "hello-plugin",
version: "1.0.0",
main: "./index.js",
activationEvents: ["onCommand:hello"]
};
// plugins/hello-plugin/index.js
const helloPlugin: Plugin = {
name: "hello-plugin",
version: "1.0.0",
activate(context) {
// 注册命令
const cmd = context.registerCommand('sayHello', (name: string) => {
console.log(`Hello, ${name}!`);
return `Greeted ${name}`;
});
context.subscriptions.push(cmd);
// 注册Hook
const hook = context.registerHook('fileOpen', (filename: string) => {
console.log(`[hello-plugin] 文件打开: ${filename}`);
});
context.subscriptions.push(hook);
},
deactivate() {
console.log('[hello-plugin] 正在清理...');
}
};
// ==================== 使用示例 ====================
async function main() {
const manager = new PluginManager();
// 扫描插件目录
await manager.scanPlugins('./plugins');
console.log('\n--- 插件列表 ---');
console.log(manager.getPluginList());
// 触发命令相关的激活事件
await manager.triggerActivation('onCommand:hello');
console.log('\n--- 执行命令 ---');
const result = await manager.executeCommand('hello-plugin.sayHello', 'World');
console.log('命令结果:', result);
console.log('\n--- 触发Hook ---');
await manager.triggerHook('fileOpen', 'test.txt');
console.log('\n--- 停用插件 ---');
await manager.deactivatePlugin('hello-plugin');
console.log('\n--- 最终状态 ---');
console.log(manager.getPluginList());
}
main();
C语言实现插件系统
c
/**
* C语言插件系统实现
*
* 使用动态链接库 (DLL/SO) 方式
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <dlfcn.h> // Linux动态加载
#include <dirent.h>
// ==================== 插件接口定义 ====================
#define PLUGIN_API_VERSION 1
// 插件必须导出的函数
typedef struct {
const char* name;
const char* version;
int api_version;
// 生命周期函数
int (*init)(void* host_api);
void (*destroy)(void);
// 功能函数 (可选)
int (*process)(void* data, int size);
const char* (*get_info)(void);
} PluginInfo;
// 宿主提供给插件的API
typedef struct {
void (*log)(const char* message);
void* (*alloc)(size_t size);
void (*free)(void* ptr);
int (*register_command)(const char* name, void* handler);
int (*trigger_event)(const char* event, void* data);
} HostAPI;
// 获取插件信息的函数签名
typedef PluginInfo* (*GetPluginInfoFunc)(void);
// ==================== 插件管理器 ====================
#define MAX_PLUGINS 64
typedef struct {
char path[256];
void* handle; // dlopen句柄
PluginInfo* info;
int active;
} LoadedPlugin;
typedef struct {
LoadedPlugin plugins[MAX_PLUGINS];
int plugin_count;
HostAPI host_api;
} PluginManager;
// 全局插件管理器
static PluginManager g_manager;
// ==================== 宿主API实现 ====================
void host_log(const char* message) {
printf("[HOST] %s\n", message);
}
void* host_alloc(size_t size) {
return malloc(size);
}
void host_free(void* ptr) {
free(ptr);
}
int host_register_command(const char* name, void* handler) {
printf("[HOST] 注册命令: %s\n", name);
// 实际实现中这里会存储命令处理器
return 0;
}
int host_trigger_event(const char* event, void* data) {
printf("[HOST] 触发事件: %s\n", event);
// 通知所有插件
return 0;
}
// ==================== 插件管理函数 ====================
/**
* 初始化插件管理器
*/
void plugin_manager_init(void) {
memset(&g_manager, 0, sizeof(g_manager));
// 设置宿主API
g_manager.host_api.log = host_log;
g_manager.host_api.alloc = host_alloc;
g_manager.host_api.free = host_free;
g_manager.host_api.register_command = host_register_command;
g_manager.host_api.trigger_event = host_trigger_event;
}
/**
* 加载单个插件
*/
int load_plugin(const char* path) {
if (g_manager.plugin_count >= MAX_PLUGINS) {
fprintf(stderr, "插件数量已达上限\n");
return -1;
}
printf("加载插件: %s\n", path);
// 打开动态库
void* handle = dlopen(path, RTLD_NOW);
if (!handle) {
fprintf(stderr, "dlopen失败: %s\n", dlerror());
return -1;
}
// 获取插件信息函数
GetPluginInfoFunc get_info = (GetPluginInfoFunc)dlsym(handle, "get_plugin_info");
if (!get_info) {
fprintf(stderr, "找不到get_plugin_info: %s\n", dlerror());
dlclose(handle);
return -1;
}
// 获取插件信息
PluginInfo* info = get_info();
if (!info) {
fprintf(stderr, "get_plugin_info返回NULL\n");
dlclose(handle);
return -1;
}
// 检查API版本
if (info->api_version != PLUGIN_API_VERSION) {
fprintf(stderr, "API版本不匹配: 期望%d, 实际%d\n",
PLUGIN_API_VERSION, info->api_version);
dlclose(handle);
return -1;
}
// 初始化插件
if (info->init) {
int ret = info->init(&g_manager.host_api);
if (ret != 0) {
fprintf(stderr, "插件初始化失败: %d\n", ret);
dlclose(handle);
return -1;
}
}
// 存储插件信息
LoadedPlugin* plugin = &g_manager.plugins[g_manager.plugin_count];
strncpy(plugin->path, path, sizeof(plugin->path) - 1);
plugin->handle = handle;
plugin->info = info;
plugin->active = 1;
g_manager.plugin_count++;
printf("插件加载成功: %s v%s\n", info->name, info->version);
return 0;
}
/**
* 扫描目录加载所有插件
*/
int scan_and_load_plugins(const char* dir) {
DIR* d = opendir(dir);
if (!d) {
fprintf(stderr, "无法打开目录: %s\n", dir);
return -1;
}
struct dirent* entry;
int loaded = 0;
while ((entry = readdir(d)) != NULL) {
// 检查是否是.so文件
const char* ext = strrchr(entry->d_name, '.');
if (ext && strcmp(ext, ".so") == 0) {
char path[512];
snprintf(path, sizeof(path), "%s/%s", dir, entry->d_name);
if (load_plugin(path) == 0) {
loaded++;
}
}
}
closedir(d);
printf("共加载 %d 个插件\n", loaded);
return loaded;
}
/**
* 卸载插件
*/
void unload_plugin(int index) {
if (index < 0 || index >= g_manager.plugin_count) {
return;
}
LoadedPlugin* plugin = &g_manager.plugins[index];
if (!plugin->active) {
return;
}
printf("卸载插件: %s\n", plugin->info->name);
// 调用销毁函数
if (plugin->info->destroy) {
plugin->info->destroy();
}
// 关闭动态库
dlclose(plugin->handle);
plugin->active = 0;
plugin->handle = NULL;
plugin->info = NULL;
}
/**
* 卸载所有插件
*/
void unload_all_plugins(void) {
for (int i = g_manager.plugin_count - 1; i >= 0; i--) {
unload_plugin(i);
}
}
/**
* 调用所有插件的process函数
*/
int process_all_plugins(void* data, int size) {
int processed = 0;
for (int i = 0; i < g_manager.plugin_count; i++) {
LoadedPlugin* plugin = &g_manager.plugins[i];
if (plugin->active && plugin->info->process) {
int ret = plugin->info->process(data, size);
if (ret == 0) {
processed++;
}
}
}
return processed;
}
/**
* 打印所有插件信息
*/
void list_plugins(void) {
printf("\n=== 已加载插件 ===\n");
for (int i = 0; i < g_manager.plugin_count; i++) {
LoadedPlugin* plugin = &g_manager.plugins[i];
if (plugin->active) {
printf("[%d] %s v%s\n", i, plugin->info->name, plugin->info->version);
if (plugin->info->get_info) {
printf(" %s\n", plugin->info->get_info());
}
}
}
printf("==================\n\n");
}
// ==================== 示例插件代码 ====================
/*
* 这部分代码需要编译成单独的.so文件
* gcc -shared -fPIC -o my_plugin.so my_plugin.c
*/
#if 0 // 示例插件代码
static HostAPI* g_host = NULL;
int my_plugin_init(void* host_api) {
g_host = (HostAPI*)host_api;
g_host->log("My Plugin 初始化");
g_host->register_command("my_command", NULL);
return 0;
}
void my_plugin_destroy(void) {
if (g_host) {
g_host->log("My Plugin 销毁");
}
}
int my_plugin_process(void* data, int size) {
// 处理数据
return 0;
}
const char* my_plugin_get_info(void) {
return "这是一个示例插件";
}
// 导出的插件信息
static PluginInfo g_plugin_info = {
.name = "My Plugin",
.version = "1.0.0",
.api_version = PLUGIN_API_VERSION,
.init = my_plugin_init,
.destroy = my_plugin_destroy,
.process = my_plugin_process,
.get_info = my_plugin_get_info,
};
// 必须导出此函数
PluginInfo* get_plugin_info(void) {
return &g_plugin_info;
}
#endif
// ==================== 主程序 ====================
int main(int argc, char* argv[]) {
printf("=== 插件系统演示 ===\n\n");
// 初始化
plugin_manager_init();
// 加载插件
const char* plugin_dir = argc > 1 ? argv[1] : "./plugins";
scan_and_load_plugins(plugin_dir);
// 列出插件
list_plugins();
// 处理数据
char data[] = "test data";
process_all_plugins(data, sizeof(data));
// 清理
unload_all_plugins();
printf("=== 程序结束 ===\n");
return 0;
}
总结对比
┌─────────────────────────────────────────────────────────────────────────────┐
│ 插件系统对比 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┬─────────────┬─────────────┬─────────────┬─────────────┐ │
│ │ │ Chrome扩展 │ VSCode插件 │ DLL/SO插件 │ 脚本插件 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 隔离方式 │ 进程隔离 │ 进程隔离 │ 无隔离 │ 沙箱 │ │
│ │ │ +JS沙箱 │ │ │ │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 语言 │ JS/TS │ JS/TS │ 任意 │ 特定语言 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 通信方式 │ 消息传递 │ JSON-RPC │ 函数调用 │ API调用 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 性能 │ 中等 │ 中等 │ 最高 │ 较低 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 安全性 │ 高 │ 高 │ 低 │ 中等 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 热加载 │ 支持 │ 支持 │ 困难 │ 支持 │ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 崩溃影响 │ 不影响主程序│ 不影响主程序│ 可能崩主程序│ 不影响主程序│ │
│ ├─────────────┼─────────────┼─────────────┼─────────────┼─────────────┤ │
│ │ 典型应用 │ 浏览器扩展 │ 编辑器插件 │ 游戏MOD │ 游戏脚本 │ │
│ │ │ │ │ 音频VST │ 编辑器宏 │ │
│ └─────────────┴─────────────┴─────────────┴─────────────┴─────────────┘ │
│ │
│ 【设计插件系统的关键点】 │
│ ───────────────────────────────────────── │
│ │
│ 1. 清晰的插件接口 │
│ - 定义插件必须实现的函数/接口 │
│ - 版本兼容性检查 │
│ │
│ 2. 生命周期管理 │
│ - 加载/激活/停用/卸载 │
│ - 资源清理机制 │
│ │
│ 3. 通信机制 │
│ - 宿主→插件: 调用插件函数 │
│ - 插件→宿主: 回调/事件/API │
│ - 插件↔插件: 事件总线/消息传递 │
│ │
│ 4. 安全与隔离 │
│ - 权限控制 │
│ - 沙箱隔离 │
│ - 错误处理 │
│ │
│ 5. 扩展点设计 │
│ - 命令/菜单 │
│ - UI组件 │
│ - 数据处理管道 │
│ - 事件钩子 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
一句话总结:
插件系统的本质是"约定接口 + 动态加载 + 隔离通信"。Chrome用进程隔离+消息传递保证安全,VSCode用独立Extension Host+JSON-RPC实现功能丰富又不卡UI,传统DLL方式性能最好但最不安全。选哪种方案取决于你对性能、安全、灵活性的权衡。
参考资料:
- Chrome Extension开发文档
- VSCode Extension API
- Chromium源码
- VSCode源码