独立产品开发(Web3 工具)· 面试腹稿
1 Chrome 插件三层通信架构
口语版
口语版:Chrome 插件有三个独立运行环境:popup(用户界面弹窗)、background(后台常驻脚本)、content script(注入到目标页面的脚本)。它们之间不能直接调用函数,必须通过消息通信。
我的设计是 background 作为中枢:popup 负责用户配置和状态展示,content script 负责页面内 DOM 操作和游戏交互,background 负责调度逻辑、状态管理和定时任务。popup 和 content 都只跟 background 通信,不直接互联,这样架构清晰、好维护。
通信用的是
我的设计是 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 断线重连机制
口语版
口语版:WAX 公链在高峰时段非常拥堵,游戏页面经常断连。如果工具不处理断线,用户睡觉时工具就停了,第二天醒来发现白跑了几个小时。
我做了多层重连:首先 content script 通过 DOM 检测判断是否断线(页面出现特定错误弹窗或白屏),检测到后通知 background。background 执行重连策略:先尝试刷新页面,刷新后等 content script 重新注入并上报页面状态;如果多次刷新失败,就重新打开 tab 页。整个过程有指数退避(避免高峰时频繁刷新加重负载),同时 popup 展示重连次数和状态日志。
上线后高峰时段掉线停工率从 40% 降到 0%,工具真正实现 24 小时无人值守。
我做了多层重连:首先 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 超时容错与稳定性
口语版
口语版:链上操作(发交易)在高峰时段经常超时或失败,表现为游戏弹出错误弹窗或者操作无响应。如果不处理,工具就卡死在那个步骤了。
我做了两层容错:第一层是弹框检测,content script 用 MutationObserver 监听 DOM 变化,一旦检测到错误弹窗就自动点关闭并重试当前操作。第二层是超时计数,每个操作步骤设定超时时间,超时后自动跳过并重试,连续失败 N 次则跳过该轮等待下一轮。这两层配合,让工具在链上拥堵时也能持续运行。
我做了两层容错:第一层是弹框检测,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 代码混淆与防破解
口语版
口语版:这是付费工具,所以防破解很重要。Chrome 插件的代码天然暴露(用户可以直接看到源码),所以我用了多层混淆。
Webpack 打包后用 javascript-obfuscator 做深度混淆:变量名重命名、字符串加密、控制流平坦化、死代码注入。另外还做了授权校验:工具启动时向我的服务端验证激活码,激活码绑定设备指纹(浏览器 UA + 时区 + 屏幕分辨率等组合 hash),防止一码多用。
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 插件代码终究可以被调试和逆向
- 核心保护还是靠服务端校验:关键业务逻辑(如交易签名)不放在前端
- 混淆的实际价值:防止普通用户直接抄代码,增加竞品分析成本,保护商业逻辑一段时间