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,每隔
相关推荐
路边草随风42 分钟前
SparkSession read() 执行Impala任意sql返回Dataset
java·sql·spark
开心香辣派小星43 分钟前
23种设计模式-18观察者(Observer)模式
java·开发语言·设计模式
Slow菜鸟1 小时前
Java项目基础架构(一)| 工程架构选型指南
java·开发语言·架构
一字白首1 小时前
Vue 进阶,指令补充 + computed+watch
前端·javascript·vue.js
暮之沧蓝1 小时前
React(18-19)总结
前端·react.js·前端框架
专注于大数据技术栈1 小时前
java学习--注解之@Deprecated
java·学习
HIT_Weston1 小时前
50、【Ubuntu】【Gitlab】拉出内网 Web 服务:http.server 单/多线程分析(二)
前端·ubuntu·gitlab
我太想进步了C~~1 小时前
Prompt Design(提示词工程)入门级了解
前端·人工智能·算法
crary,记忆1 小时前
如何理解 React的UI渲染
前端·react.js·ui·前端框架