独立产品开发(Web3 工具)· 面试腹稿

前端开发 | 产品负责人 · 2022.02 – 2024.09
基于 WAX 公链的 GameFi 自动化 Chrome 插件,个人付费产品,技术栈 Chrome Extension + ES6 + Webpack

1 Chrome 插件三层通信架构

简历原文:设计并实现 Chrome 插件中 popup、background、content 组件的通信机制,用于断线重连功能。

口语版

口语版:Chrome 插件有三个独立运行环境:popup(用户界面弹窗)、background(后台常驻脚本)、content script(注入到目标页面的脚本)。它们之间不能直接调用函数,必须通过消息通信。

我的设计是 background 作为中枢:popup 负责用户配置和状态展示,content script 负责页面内 DOM 操作和游戏交互,background 负责调度逻辑、状态管理和定时任务。popup 和 content 都只跟 background 通信,不直接互联,这样架构清晰、好维护。

通信用的是 chrome.runtime.sendMessage(一次性消息)和 chrome.runtime.connect(长连接 port),长连接用于需要持续通信的场景,比如任务执行期间的状态上报。
面试官想听什么
  • Chrome 插件的三层架构是什么?各自的职责?
  • popup、background、content script 之间怎么通信?
  • 为什么 background 要作为中枢?
  • Manifest V2 和 V3 的区别?你用的哪个?

我会怎么讲

1. 三层架构

┌─────────────────────────────────────────────────┐
│                Chrome Extension                  │
│                                                  │
│  ┌──────────┐   chrome.runtime   ┌────────────┐ │
│  │  Popup   │ ←───────────────→ │ Background  │ │
│  │ (用户UI)  │    sendMessage    │  (调度中枢)  │ │
│  └──────────┘                   └──────┬─────┘ │
│                                        │        │
│                              chrome.tabs│        │
│                              sendMessage│        │
│                                        ▼        │
│                               ┌──────────────┐  │
│                               │Content Script│  │
│                               │ (页面内操作)  │  │
│                               └──────────────┘  │
│                                   ↕ DOM 操作     │
│                            ┌──────────────┐     │
│                            │  游戏页面 DOM  │     │
│                            └──────────────┘     │
└─────────────────────────────────────────────────┘
  • Popup:用户打开弹窗时加载,关闭即销毁。负责配置参数、启停控制、状态展示
  • Background(Service Worker):常驻后台,负责核心调度逻辑、定时器管理、状态持久化(chrome.storage
  • Content Script:注入到目标游戏页面,可访问 DOM,负责读取游戏状态、模拟用户操作

2. 通信方式

  • 一次性消息chrome.runtime.sendMessage + chrome.runtime.onMessage,适合请求-响应模式
  • 长连接chrome.runtime.connect 建立 port,适合持续通信(任务执行期间的状态流)
  • 存储同步chrome.storage.local + onChanged 监听,适合配置共享和状态持久化
// Background: 监听消息
chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  switch (msg.type) {
    case 'START_TASK':
      startAutomation(msg.config);
      sendResponse({ status: 'started' });
      break;
    case 'GET_STATUS':
      sendResponse({ status: currentStatus, stats: taskStats });
      break;
  }
  return true;  // 异步 sendResponse 需要返回 true
});

// Content Script → Background: 上报游戏状态
chrome.runtime.sendMessage({
  type: 'GAME_STATE',
  data: { energy: 120, cooldown: false }
});

2 断线重连机制

简历原文:通过重连机制,将服务器在拥堵时段的每小时掉线停工率从 40% 降至 0%

口语版

口语版:WAX 公链在高峰时段非常拥堵,游戏页面经常断连。如果工具不处理断线,用户睡觉时工具就停了,第二天醒来发现白跑了几个小时。

我做了多层重连:首先 content script 通过 DOM 检测判断是否断线(页面出现特定错误弹窗或白屏),检测到后通知 background。background 执行重连策略:先尝试刷新页面,刷新后等 content script 重新注入并上报页面状态;如果多次刷新失败,就重新打开 tab 页。整个过程有指数退避(避免高峰时频繁刷新加重负载),同时 popup 展示重连次数和状态日志。

上线后高峰时段掉线停工率从 40% 降到 0%,工具真正实现 24 小时无人值守。
面试官想听什么
  • 断线检测是怎么做的?怎么判断页面断连了?
  • 重连策略是什么?有退避机制吗?
  • 刷新后 content script 怎么恢复状态?
  • 40% → 0% 这个数据怎么统计的?

我会怎么讲

1. 断线检测

  • Content Script 定时轮询(setInterval)检测页面状态:
    • 检测特定错误弹窗 DOM(如 "连接超时"、"网络错误" 弹窗)
    • 检测游戏核心元素是否存在(白屏判断)
    • 心跳检测:定期向 background 发心跳,background 设超时,心跳中断说明 content script 可能已崩溃

2. 重连策略(指数退避)

// Background: 断线重连调度
let retryCount = 0;
const MAX_RETRIES = 10;
const BASE_DELAY = 3000;  // 3s

async function handleDisconnect(tabId) {
  if (retryCount >= MAX_RETRIES) {
    notifyUser('重连失败,请手动检查');
    return;
  }

  const delay = Math.min(BASE_DELAY * Math.pow(2, retryCount), 60000);
  retryCount++;

  await sleep(delay);

  // 策略 1: 刷新页面
  chrome.tabs.reload(tabId);

  // 等待 content script 重新注入并上报
  const recovered = await waitForHeartbeat(tabId, 15000);

  if (!recovered) {
    // 策略 2: 重新打开 tab
    chrome.tabs.remove(tabId);
    const newTab = await chrome.tabs.create({ url: GAME_URL });
    await waitForHeartbeat(newTab.id, 20000);
  }

  // 恢复后重置计数
  if (recovered) retryCount = 0;
}

3. 状态恢复

  • 任务进度和配置存在 chrome.storage.local 中,content script 重新注入后先读取存储恢复上下文
  • Background 维护一个状态机(idle → running → disconnected → reconnecting → running),确保重连过程中不会重复触发任务

3 超时容错与稳定性

简历原文:利用弹框检测和超时计数,实现超时容错机制。容错处理后操作失效率由 10% 降为 0%

口语版

口语版:链上操作(发交易)在高峰时段经常超时或失败,表现为游戏弹出错误弹窗或者操作无响应。如果不处理,工具就卡死在那个步骤了。

我做了两层容错:第一层是弹框检测,content script 用 MutationObserver 监听 DOM 变化,一旦检测到错误弹窗就自动点关闭并重试当前操作。第二层是超时计数,每个操作步骤设定超时时间,超时后自动跳过并重试,连续失败 N 次则跳过该轮等待下一轮。这两层配合,让工具在链上拥堵时也能持续运行。
面试官想听什么
  • 弹框检测具体怎么实现的?用什么 API?
  • 超时计数的阈值怎么定?
  • MutationObserver 的性能问题怎么处理?

我会怎么讲

1. 弹框检测(MutationObserver)

// Content Script: 监听错误弹窗
const observer = new MutationObserver((mutations) => {
  for (const mutation of mutations) {
    for (const node of mutation.addedNodes) {
      if (node.nodeType !== Node.ELEMENT_NODE) continue;
      // 检测游戏的错误弹窗
      const errorModal = node.querySelector?.('.error-modal, .timeout-dialog');
      if (errorModal) {
        const closeBtn = errorModal.querySelector('.close-btn, .confirm-btn');
        if (closeBtn) {
          closeBtn.click();
          chrome.runtime.sendMessage({ type: 'ERROR_DISMISSED' });
        }
      }
    }
  }
});

observer.observe(document.body, {
  childList: true,
  subtree: true
});
  • 只监听 childList(新增节点),不监听 attributes,减少回调频率
  • 检测到弹窗后自动关闭并通知 background 记录日志

2. 操作超时与重试

  • 每个自动化步骤(如"点击挖矿"→"确认交易"→"等待结果")都包裹在 Promise + setTimeout 超时中
  • 超时后自动重试当前步骤,最多重试 3 次
  • 连续失败则跳过本轮操作,等待冷却时间后进入下一轮
  • 所有失败和重试记录都存到 chrome.storage 中,popup 可查看运行日志
async function executeWithTimeout(action, timeoutMs = 15000, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await Promise.race([
        action(),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error('timeout')), timeoutMs)
        )
      ]);
    } catch (e) {
      logToStorage(`Step failed (attempt ${i + 1}): ${e.message}`);
      if (i < maxRetries - 1) await sleep(2000 * (i + 1));
    }
  }
  throw new Error('Max retries exceeded');
}

4 代码混淆与防破解

简历原文:用 Webpack + JavaScript Obfuscator 进行代码混淆,防破解。

口语版

口语版:这是付费工具,所以防破解很重要。Chrome 插件的代码天然暴露(用户可以直接看到源码),所以我用了多层混淆。

Webpack 打包后用 javascript-obfuscator 做深度混淆:变量名重命名、字符串加密、控制流平坦化、死代码注入。另外还做了授权校验:工具启动时向我的服务端验证激活码,激活码绑定设备指纹(浏览器 UA + 时区 + 屏幕分辨率等组合 hash),防止一码多用。
面试官想听什么
  • JavaScript Obfuscator 的核心混淆手段有哪些?
  • 混淆后代码体积和性能有什么影响?
  • 前端代码混淆能真正防破解吗?
  • 授权校验是怎么做的?

我会怎么讲

1. Webpack + javascript-obfuscator 配置

// webpack.config.js
const JavaScriptObfuscator = require('webpack-obfuscator');

module.exports = {
  plugins: [
    new JavaScriptObfuscator({
      rotateStringArray: true,         // 字符串数组旋转
      stringArray: true,               // 字符串提取到数组
      stringArrayEncoding: ['base64'], // 字符串 Base64 编码
      controlFlowFlattening: true,     // 控制流平坦化
      deadCodeInjection: true,         // 死代码注入
      selfDefending: true,             // 格式化后代码自毁
    })
  ]
};
  • 字符串加密:所有字符串提取到数组,运行时解密还原,让静态分析看不到明文
  • 控制流平坦化:把 if/else/for 转成 switch-case + 状态变量循环,大幅增加逆向难度
  • 死代码注入:插入不会执行的假逻辑,干扰分析
  • 自保护selfDefending 使得代码一旦被格式化就会崩溃

2. 授权校验

  • 工具启动时向服务端发请求验证激活码,服务端校验激活码有效期和设备绑定
  • 设备指纹:组合 User-Agent、时区、屏幕分辨率、语言等信息做 hash,绑定到激活码上
  • 校验逻辑分散在多个模块中,不是单一入口,增加破解成本

3. 前端混淆的局限性

  • 前端混淆不能防止破解,只能提高破解成本。Chrome 插件代码终究可以被调试和逆向
  • 核心保护还是靠服务端校验:关键业务逻辑(如交易签名)不放在前端
  • 混淆的实际价值:防止普通用户直接抄代码,增加竞品分析成本,保护商业逻辑一段时间