跳到主要内容

2 篇博文 含有标签「Chrome插件」

查看所有标签

Chrome 扩展数据直写生产库?加个 DRY-RUN 开关安全调试

· 阅读需 4 分钟

TL;DR

Chrome 扩展在开发调试时,每次测试采集都会把数据写进生产数据库。用三层 DRY-RUN 开关解决:.env.development 设环境变量 → 客户端读取后给请求加 X-Dry-Run Header → 服务端拦截该 Header 返回数据预览,不执行写入。生产环境不设该变量,完全不受影响。


问题现象

Chrome 扩展采集电商数据后通过 API 提交到后端。开发调试阶段,每触发一次采集就往生产数据库写一条记录。测试几次后,数据库里全是脏数据,影响线上数据准确性。

没有"只看不写"的开关,要么注释掉提交代码(容易忘改回来),要么连着生产库调试(脏数据不可避免)。


根因

Chrome 扩展的开发环境和生产环境共用同一个 API 端点。fetch 请求里没有任何标记区分"这是一次调试提交"还是"这是一次真实提交"。后端收到请求就执行写入,无法区分意图。

开发时需要一个"预览模式"——看到数据会提交什么,但不实际写入。


解决方案

三层实现:环境变量 → HTTP Header → 服务端拦截。

Step 1:声明环境变量类型

// client/src/env.d.ts
interface ImportMetaEnv {
// ... 其他变量
readonly VITE_INQUIRY_DRY_RUN?: string;
}

Step 2:开发环境配置

# client/.env.development(仅开发环境)
VITE_INQUIRY_DRY_RUN=true
# client/.env.production(不设置此变量)
# 生产环境始终走真实写入

Step 3:客户端读取环境变量,条件加 Header

在 Service Worker(background script)中,读取环境变量并给请求加标记:

// background.ts
chrome.storage.local.get('sessionToken', (result) => {
const sessionToken = result.sessionToken;
if (!sessionToken) return;

// 读取 DRY-RUN 开关
const dryRun = import.meta.env.VITE_INQUIRY_DRY_RUN === 'true';

const headers: Record<string, string> = {
'Content-Type': 'application/json',
'Authorization': `Bearer ${sessionToken}`,
};

// DRY-RUN 模式加标记 Header
if (dryRun) {
headers['X-Dry-Run'] = 'true';
}

fetch(`${baseURL}/inquiry/collect`, {
method: 'POST',
headers,
body: JSON.stringify({ memberId, rows }),
});
});

WXT/Vite 在构建时将 import.meta.env.VITE_INQUIRY_DRY_RUN 内联替换为字符串。开发构建(.env.development)值为 "true",生产构建(.env.production)值为 undefined=== 'true' 自然为 false

Step 4:服务端拦截 DRY-RUN 请求

// server/src/functions/analytics/inquiry.ts
router.post('/collect', async (req, res) => {
// ... 认证、memberId 映射等前置逻辑

const dryRun = req.headers['x-dry-run'] === 'true';

if (dryRun) {
// 不写入数据库,返回数据预览
return res.json({
code: 200,
message: 'dry-run ok',
data: {
dryRun: true,
shop_id: match.shop_id,
platform_id: match.platform_id,
memberId,
rowCount: rows.length,
sampleRows: rows.slice(0, 3),
},
timestamp: Date.now(),
});
}

// 正常流程:写入数据库
const result = await analyticsClient.post('/internal/data/import/inquiry', payload);
res.json({ code: 200, data: result });
});

服务端在 DRY-RUN 模式下仍然执行认证和数据校验(确保数据格式正确),只是跳过最终的数据库写入。这样既能验证数据格式,又不产生脏数据。


注意事项

不要在生产 .env 中设置 DRY-RUN 变量

.env.production 不设置 VITE_INQUIRY_DRY_RUN,生产构建中该变量为 undefined,条件判断自然为 false。永远不要在生产配置中开启此开关。

客户端要检查 dryRun 标志

DRY-RUN 模式下服务端返回 200 状态码和 { dryRun: true, data: {...} }。客户端代码如果只检查 code === 200 会导致误判为"写入成功"。检查 response.data.dryRun 区分真实写入和预览。

DRY-RUN 仍然执行认证和数据校验

不要在认证之前就拦截 DRY-RUN 请求。保持完整的请求链路(认证 → 校验 → 拦截),这样能验证请求格式和权限是否正确,只是跳过最后一步写入。


Chrome 扩展 Service Worker 读不到登录态?跨上下文 Token 同步方案

· 阅读需 4 分钟

TL;DR

Chrome 扩展使用 sidepanel 作为 UI 入口,用户登录后 token 存入 localStorage。但 Service Worker(background script)没有 localStorage,调用直接抛 ReferenceError。解决方案:sidepanel 登录后通过 chrome.runtime.sendMessage 将 token 同步给 Service Worker,由 Service Worker 写入 chrome.storage.local。两边各取所需——sidepanel 读 localStorage,Service Worker 读 chrome.storage.local


问题现象

用户在 sidepanel 登录成功,localStorage 中写入了 sessionToken。但当 Service Worker(background script)尝试读取 token 发起 API 请求时:

ReferenceError: localStorage is not defined
at getAuthToken (background.js:42)
at submitCollectedData (background.js:87)

登录在 sidepanel 没问题,但所有从 Service Worker 发出的认证请求全部失败。


根因

Chrome 扩展有多个执行上下文,各自拥有的 API 不同:

上下文windowlocalStoragechrome.storagechrome.runtime
Sidepanel / Popup可能 undefined
Content Script有(隔离)
Service Worker

关键限制:

  • Service Worker 没有 windowdocumentlocalStorage,只能用 chrome.storage API
  • Sidepanel 在 DevTools 上下文中 chrome.storage 可能是 undefined

登录流程在 sidepanel 将 token 存入 localStorage。Service Worker 尝试从 localStorage 读取 → ReferenceError


解决方案

架构

Sidepanel (login)
├→ localStorage.setItem(token) ← sidepanel 读取
└→ chrome.runtime.sendMessage(syncToken)
└→ Service Worker
└→ chrome.storage.local.set(token) ← Service Worker 读取

Token 存在两个地方:localStorage(sidepanel 读取)和 chrome.storage.local(Service Worker 读取)。登录和登出时通过消息机制保持两边同步。

Step 1:登录函数 — 双写

登录成功后,先写 localStorage(sidepanel 可用),再通过消息同步给 Service Worker:

// auth.ts — 登录成功后
localStorage.setItem('sessionToken', token);
localStorage.setItem('userInfo', JSON.stringify(user));
localStorage.setItem('subscriptionInfo', JSON.stringify(subscription));

// 同步到 chrome.storage.local(通过 Service Worker)
try {
chrome.runtime.sendMessage({
action: 'syncToken',
token,
user,
subscription,
});
} catch (e) {
// 非扩展上下文(单元测试、普通网页),忽略
}

try/catch 是必要的——在非扩展上下文中 chrome.runtime.sendMessage 会抛异常。

Step 2:Service Worker — 消息处理

在 background script 中监听消息,将 token 写入 chrome.storage.local

// background.ts
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
// 同步 token:sidepanel → chrome.storage.local
if (request.action === 'syncToken') {
const { token, user, subscription } = request;
const data: Record<string, string> = {};
if (token) data.sessionToken = token;
if (user) data.userInfo = typeof user === 'string' ? user : JSON.stringify(user);
if (subscription)
data.subscriptionInfo =
typeof subscription === 'string' ? subscription : JSON.stringify(subscription);

chrome.storage.local.set(data, () => {
console.log('[syncToken] token synced to chrome.storage.local');
sendResponse({ success: true });
});
return true; // 异步 sendResponse 必须返回 true
}

// 登出时清除 token
if (request.action === 'clearToken') {
chrome.storage.local.remove(
['sessionToken', 'userInfo', 'subscriptionInfo'],
() => {
console.log('[clearToken] token cleared from chrome.storage.local');
sendResponse({ success: true });
}
);
return true;
}
});

Step 3:Service Worker 从 chrome.storage.local 读取 token

// Before(Service Worker 中报错):
const sessionToken = localStorage.getItem('sessionToken');

// After(正确方式):
chrome.storage.local.get('sessionToken', (result) => {
const sessionToken = result.sessionToken;
if (!sessionToken) {
console.warn('No session token found');
return;
}
// 使用 token 发起 API 请求
fetch('https://api.example.com/data', {
headers: { Authorization: `Bearer ${sessionToken}` },
});
});

Step 4:登出清除两边

// auth.ts
function clearAuthData(): void {
localStorage.removeItem('sessionToken');
localStorage.removeItem('userInfo');
localStorage.removeItem('subscriptionInfo');
try {
chrome.runtime.sendMessage({ action: 'clearToken' });
} catch (e) {
// 非扩展上下文,忽略
}
}

注意事项

双存储必须同步

Token 存在两个地方:localStorage(sidepanel 读)+ chrome.storage.local(Service Worker 读)。登录和登出都必须同时操作两边,否则状态不一致。

onMessage 中 return true 不能省

chrome.runtime.onMessage 监听器中使用异步 sendResponse(如 chrome.storage.local.set 的回调)时,必须 return true。否则消息通道提前关闭,sendResponse 调用无效。

不要在 sidepanel 直接用 chrome.storage

DevTools 上下文中 chrome.storage 可能是 undefined,无法直接写入。用 localStorage + 消息转发是最可靠的方案。