JS 注入机制深度解析

第五章: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)

🧩 实现逻辑

  1. 连接调试器 :使用 Electron 的 debugger API。
  2. 发送指令 :调用 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);

🧠 异步执行与沙箱隔离

注入的脚本通常需要执行异步操作(如 sleepwaitForElement)。因此,我们将整个脚本包裹在一个 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 格式的"指令集",并在目标网页的上下文中逐条"解释"并执行。

🧩 设计思路

  1. 指令集 (Instruction Set) :即我们定义的 WorkflowStep 数组。
  2. 解释器 (Interpreter) :一个 async 函数,包含循环、条件判断和 DOM 操作能力。
  3. 调度器 (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.');
})();

📝 关键设计点解析

  1. 异步轮询 (Polling)

    • 网页是动态的,元素可能通过 AJAX 延迟加载。简单的 document.querySelector 往往会返回 null
    • 我们实现了 waitForElement,每隔
相关推荐
ywf12151 小时前
前端的dist包放到后端springboot项目下一起打包
前端·spring boot·后端
恋猫de小郭1 小时前
2026,Android Compose 终于支持 Hot Reload 了,但是收费
android·前端·flutter
hpoenixf7 小时前
2026 年前端面试问什么
前端·面试
还是大剑师兰特7 小时前
Vue3 中的 defineExpose 完全指南
前端·javascript·vue.js
泯泷7 小时前
阶段一:从 0 看懂 JSVMP 架构,先在脑子里搭出一台最小 JSVM
前端·javascript·架构
大阿明7 小时前
Spring Boot(快速上手)
java·spring boot·后端
bearpping8 小时前
Java进阶,时间与日期,包装类,正则表达式
java
邵奈一8 小时前
清明纪念·时光信笺——项目运行指南
java·实战·项目
mengchanmian8 小时前
前端node常用配置
前端
sunwenjian8868 小时前
Java进阶——IO 流
java·开发语言·python