第五章:JS 注入机制深度解析
在 Electron 自动化应用中,"注入 (Injection)" 是最核心的黑魔法。本章将深入剖析两种截然不同的注入技术及其应用场景:CDP 预加载注入 (用于反检测)和 Runtime 运行时注入(用于执行任务)。
5.1 注入的双重境界
很多初学者容易混淆这两种注入方式。让我们先明确它们的区别:
| 特性 | CDP 注入 (Pre-load) | Runtime 注入 (On-demand) |
|---|---|---|
| 核心 API | Page.addScriptToEvaluateOnNewDocument |
webContents.executeJavaScript |
| 执行时机 | 页面加载前(DOM Ready 之前) | 页面加载后(任意时刻) |
| 生命周期 | 永久生效(刷新页面依然存在) | 一次性(刷新后失效) |
| 主要用途 | 修改环境指纹(反检测)、Mock API | 执行点击、输入、滚动等操作 |
| 权限 | 高(可覆盖原生对象) | 中(受限于页面上下文) |
5.2 CDP 注入:反检测的基石
为了让目标网站认为我们是真实浏览器,我们需要在网页 JS 执行之前,修改 navigator 等核心对象。普通的 preload.js 有时执行得不够早,且容易被检测。因此,我们使用 Chrome DevTools Protocol (CDP)。
🧩 实现逻辑
- 连接调试器 :使用 Electron 的
debuggerAPI。 - 发送指令 :调用
Page.addScriptToEvaluateOnNewDocument。
ts
async function injectAntiDetection(webContents) {
try {
// 1. Attach 调试器
if (!webContents.debugger.isAttached()) {
webContents.debugger.attach('1.3');
}
// 2. 准备反检测脚本 (核心 Payload)
const script = `
// 抹除 WebDriver 特征
Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
// 伪造插件列表 (Headless Chrome 通常为空)
Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3] });
// 伪造 WebGL 厂商 (避免显示为 Google SwiftShader)
const getParameter = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function(parameter) {
if (parameter === 37445) return 'Intel Inc.';
return getParameter.apply(this, arguments);
};
`;
// 3. 发送 CDP 指令
await webContents.debugger.sendCommand('Page.enable');
await webContents.debugger.sendCommand('Page.addScriptToEvaluateOnNewDocument', {
source: script
});
} catch (err) {
console.error('Anti-detection injection failed:', err);
}
}
📝 教学重点:
- 时机 :必须在
did-start-loading事件中调用,确保在页面任何脚本运行前生效。 - 持久性:CDP 注入的脚本在页面刷新或跳转后依然有效,这是它最大的优势。
5.3 Runtime 注入:自动化执行的引擎
当我们需要控制页面(如点击按钮)时,我们使用 executeJavaScript。
🧩 数据传递的艺术
最大的挑战是:如何将主进程的数据(如用户输入的密码)传递给网页内的脚本?
由于 executeJavaScript 接收的是字符串,我们不能直接传变量。我们必须使用 序列化 (Serialization)。
错误示范
ts
const password = '123';
// ❌ 错误:网页上下文中不存在 'password' 变量
view.webContents.executeJavaScript('document.querySelector("#pass").value = password');
正确示范:模板字符串 + JSON 序列化
ts
const password = '123';
// ✅ 正确:将值硬编码进字符串中
const script = `document.querySelector("#pass").value = "${password}"`;
但如果数据是复杂的对象(如整个工作流步骤),简单的字符串拼接容易出错(比如内容包含引号)。最佳实践是使用 JSON.stringify:
ts
const steps = [{ id: 1, type: 'click' }, { id: 2, type: 'input' }];
// 1. 在主进程序列化
const stepsJson = JSON.stringify(steps);
// 2. 在脚本中反序列化 (或者直接作为 JS 对象字面量)
const script = `
(async () => {
const steps = ${stepsJson};
console.log('I have steps:', steps);
})();
`;
view.webContents.executeJavaScript(script);
🧠 异步执行与沙箱隔离
注入的脚本通常需要执行异步操作(如 sleep 或 waitForElement)。因此,我们将整个脚本包裹在一个 IIFE(立即执行异步函数)中。
ts
(async () => {
// 定义辅助函数
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// 执行逻辑
await sleep(1000);
document.querySelector('button').click();
})();
关于隔离
尽管我们在 webPreferences 中开启了 contextIsolation: true,但 executeJavaScript 执行的代码运行在主世界(Main World)。这意味着它可以直接访问页面上的 window 对象、全局变量(如 jQuery 的 $),这对于自动化操作非常方便。
5.4 动作执行引擎设计 (Action Execution Engine)
注入的脚本不仅仅是简单的命令堆砌,它实际上是一个 小型的解释器 (Interpreter)。它接收 JSON 格式的"指令集",并在目标网页的上下文中逐条"解释"并执行。
🧩 设计思路
- 指令集 (Instruction Set) :即我们定义的
WorkflowStep数组。 - 解释器 (Interpreter) :一个
async函数,包含循环、条件判断和 DOM 操作能力。 - 调度器 (Dispatcher) :根据
step.type分发到具体的执行逻辑。
🧩 核心代码结构
ts
(async () => {
// 1. 上下文准备
const steps = ${stepsJson};
const values = ${valuesJson};
// 辅助函数:轮询等待元素
const waitForElement = async (selector, timeout = 5000) => {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = document.querySelector(selector);
if (el) return el;
await new Promise(r => setTimeout(r, 100));
}
return null;
};
// 2. 执行循环 (Execution Loop)
console.log('Starting workflow execution...');
for (const step of steps) {
console.log('Executing step:', step.id);
// 获取当前步骤的动态值 (优先使用用户输入,否则使用默认值)
const stepValue = values[step.id] || step.value;
// 3. 容错处理 (Error Handling)
try {
// 处理前置延时
if (step.delay) await new Promise(r => setTimeout(r, step.delay));
// 查找目标元素
const el = await waitForElement(step.selector);
if (!el) {
console.warn(`Element not found: ${step.selector}`);
continue; // 即使找不到元素,也尝试继续执行后续步骤
}
// 4. 动作分发 (Action Dispatcher)
switch (step.type) {
case 'input':
// 处理输入逻辑 (Set/Type/InnerText)
handleInput(el, step.mode, stepValue);
break;
case 'click':
el.click();
break;
case 'select':
// 处理下拉框逻辑
handleSelect(el, step.mode, stepValue);
break;
case 'scroll':
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
break;
}
} catch (err) {
console.error(`Step ${step.id} failed:`, err);
// 可以在这里决定是中断执行还是记录错误
}
}
console.log('Workflow execution completed.');
})();
📝 关键设计点解析
-
异步轮询 (Polling):
- 网页是动态的,元素可能通过 AJAX 延迟加载。简单的
document.querySelector往往会返回null。 - 我们实现了
waitForElement,每隔
- 网页是动态的,元素可能通过 AJAX 延迟加载。简单的