跳到主要内容

Chrome 扩展采集数据全是空值?Cookie 同名不同值导致映射失败

· 阅读需 3 分钟

在为客户构建电商数据采集系统时遇到此问题,记录根因与解法。

TL;DR

1688 平台 Cookie 中 last_midunb 都包含用户 ID,但格式不同(b2b-xxx vs 纯数字)。数据库存的是 b2b- 前缀格式。原代码遍历 Cookie 数组时先匹配到了 unb,导致严格等于比较失败,所有采集数据写入时关联不到店铺,结果全是零值。

解法:把 Cookie key 优先级列表放在外层循环,Cookie 数组放在内层循环,确保高优先级的 key 先被查找。

问题现象

1688 生意参谋日报采集后,数据库里看板数据全是 0,但询盘数据有值:

shop_id | report_date | reveal_cnt | uv | pay_amt | effective_inq_users
2 | 2026-05-13 | 0 | 0 | 0.00 | 56
2 | 2026-05-14 | 0 | 0 | 0.00 | 41

后端日志报错:No shop found for memberId: 2214126315258

但数据库里存的 platform_account_idb2b-2214126315258ad300

根因

unb=2214126315258                          # 纯数字
last_mid=b2b-2214126315258ad300 # b2b- 前缀 + 后缀

数据库映射表 shops.platform_account_id 存的是 b2b- 前缀格式。代码做的是严格等于匹配:

const match = mapping.find(m => m.platform_account_id === memberId);
// "2214126315258" !== "b2b-2214126315258ad300" → 匹配失败

遍历顺序陷阱

原代码的外层循环是 Cookie 数组、内层是 key 列表:

// ❌ 错误:Cookie 数组在外层,key 优先级无效
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var i = 0; i < cookies.length; i++) { // 外层:Cookie
var pair = cookies[i].trim();
for (var k = 0; k < keys.length; k++) { // 内层:key
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

document.cookie 的返回顺序不是固定的。如果 unb 的 Cookie 在数组中排在 last_mid 前面,就会先匹配到 unb,返回纯数字格式的 ID——last_mid 的优先级形同虚设。

解决方案

交换循环层级:key 优先级列表放在外层,Cookie 数组放在内层:

// ✅ 正确:key 优先级列表在外层
var keys = ['last_mid', '__last_memberid__', 'unb'];
for (var k = 0; k < keys.length; k++) { // 外层:按优先级遍历 key
for (var i = 0; i < cookies.length; i++) { // 内层:在所有 Cookie 中查找
var pair = cookies[i].trim();
if (pair.indexOf(keys[k] + '=') === 0) {
return pair.substring(keys[k].length + 1);
}
}
}

key 列表按优先级在外层遍历,无论 document.cookie 返回顺序如何,始终先查找 last_mid,保证拿到和数据库格式一致的用户 ID,映射不会失败。

验证

// 在浏览器控制台确认两个 Cookie 都存在
document.cookie.split(';')
.filter(c => /last_mid|unb/.test(c.trim()))
.map(c => c.trim())
// ['unb=2214126315258', 'last_mid=b2b-2214126315258ad300']

注意事项

Chrome 扩展中使用 chrome.cookies.get() 读取 Cookie 时不存在此问题——它是按名称精确查询,天然支持优先级。但 document.cookie 字符串解析时务必注意循环层级。另外,如果你的扩展通过 postMessage 传递数据,也要注意 postMessage targetOrigin 的安全风险;如果热重载后消息被处理两次,需要手动管理监听器生命周期。


Node.js ESM 动态 import 报模块找不到?检查文件扩展名

· 阅读需 4 分钟

在为客户构建 SaaS 数据分析平台时遇到此问题,记录根因与解法。

TL;DR

Node.js ESM 模式下,import('./path/to/module') 不会自动解析 ./path/to/module.js。TypeScript 编译的 dist 产物如果遗漏 .js 后缀,模块加载时抛出 ERR_MODULE_NOT_FOUND。如果这个 import 在延迟逻辑中(如定时器、条件分支),应用启动正常但运行一段时间后崩溃,PM2 表现为重启计数飙升。

解法:确保所有 ESM 动态 import 路径包含 .js 扩展名,并在 build 流程中自动修复。

问题现象

PM2 状态显示应用不断重启:

│ name             │ ↺    │ status │ uptime │
│ analytics-api │ 9 │ online │ 28m │

错误日志每几分钟重复:

Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/dist/domains/video/cleanup'
imported from /app/dist/server.js

但文件实际存在:

$ ls dist/domains/video/
cleanup.js executor.js queue.js

根因

ESM 不自动解析文件扩展名

Node.js 的 CommonJS (require()) 会自动尝试 .js.json 等后缀。ESM (import) 不会

// ❌ ESM 模式下找不到模块
import('./domains/video/cleanup')
// Node.js 查找: ./domains/video/cleanup (精确路径,无扩展名)
// 实际文件: ./domains/video/cleanup.js

// ✅ 必须带 .js 后缀
import('./domains/video/cleanup.js')

延迟 import 隐藏了问题

这个 import 在定时器中延迟执行:

// server.ts — 启动时不立即执行
import('./domains/video/cleanup.js').then(({ startCleanupScheduler }) => {
startCleanupScheduler(); // 几秒后才触发
});

应用启动成功(DB 连接、端口监听都正常),定时器触发时 import 报错 → 进程崩溃 → PM2 重启 → 再次启动成功 → 定时器再次触发 → 再次崩溃。形成崩溃循环

为什么之前没问题?

旧版本的 dist 产物是通过手动 build 生成的,build 脚本包含 .js 后缀修复步骤。某次部署时跳过了 build 后的修复步骤,直接用 tsc 输出的产物部署,tsc 不修改 import 路径。

解决方案

1. TypeScript 源码中写 .js 后缀

TypeScript 官方推荐:即使在 .ts 文件中,也写 .js 后缀:

// ✅ TypeScript 源码中也写 .js
import('./domains/video/cleanup.js').then(({ startCleanupScheduler }) => {
startCleanupScheduler();
});

2. Build 后自动修复(推荐)

在 build 流程中添加后缀修复脚本,自动为没有扩展名的 import 补上 .js

{
"scripts": {
"build": "tsc && node fix-imports.js"
}
}

fix-imports.js 核心逻辑:

import { readFileSync, writeFileSync, readdirSync } from 'fs';
import { join } from 'path';

function fixImports(dir) {
for (const file of readdirSync(dir, { withFileTypes: true })) {
const fullPath = join(dir, file.name);
if (file.isDirectory()) {
fixImports(fullPath);
} else if (file.name.endsWith('.js')) {
let content = readFileSync(fullPath, 'utf8');
// 修复动态 import: from 'xxx' → from 'xxx.js'
const fixed = content.replace(
/import\(['"](\.[^'"]+)['"]\)/g,
(match, path) => path.endsWith('.js') ? match : match.replace(path, path + '.js')
);
// 修复静态 import: from 'xxx' → from 'xxx.js'
const fixed2 = fixed.replace(
/from\s+['"](\.[^'"]+)['"]/g,
(match, path) => path.endsWith('.js') ? match : match.replace(path, path + '.js')
);
if (fixed2 !== content) {
writeFileSync(fullPath, fixed2);
}
}
}
}

build 流程自动为所有相对路径 import 补全 .js 后缀,TypeScript 源码保持无后缀写法,ESM 部署不再出现模块找不到的错误。

注意事项

  • 此问题只在 Node.js ESM 模式("type": "module".mjs 文件)下出现,CommonJS 不受影响
  • 静态 import(import ... from './foo')同样受此限制,不仅限于动态 import();ESM 的 import 提升还会导致另一个常见问题——dotenv 在 import 链之后才执行,环境变量读不到
  • 使用 tsxts-node 开发时不报错(它们会自动解析后缀),但 node dist/server.js 生产运行时出错
  • PM2 崩溃循环的典型特征:重启计数(↺)持续增长,每次 uptime 不超过几分钟

Chrome 扩展热重载后消息被处理两次?WXT HMR 多实例叠加监听器

· 阅读需 3 分钟

在为客户构建电商数据采集 Chrome 扩展时遇到此问题,记录根因与解法。

TL;DR

WXT 框架 HMR 热重载时,content script 重新执行但旧的 window.addEventListener('message', ...) 不会被清除。每热重载一次就多一个监听器实例,导致每条 postMessage 被处理 N 次。

解法:注册新监听器前,从 window 变量取出旧监听器引用并 removeEventListener

问题现象

浏览器控制台显示同一条消息被两个不同实例捕获:

content.js:114 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: nxctn6
content.js:2 [CCL] CCL_SHOP_REPORT_DAILY daily caught - 实例: t6jce7

每条 postMessage 被处理两次,导致后台发送重复请求。

根因

WXT (基于 Vite 的 Chrome 扩展框架) 开发模式下,修改 content script 后触发 HMR:

  1. 新的 content script 模块被加载执行
  2. 新的 window.addEventListener('message', messageListener) 被注册
  3. 旧的监听器函数仍然存在于内存中——HMR 不负责清理 DOM 事件监听器

结果:window 上挂载了多个独立的 message 监听器,每条 postMessage 触发所有实例。

解决方案

在 content script 入口处,注册新监听器前移除旧的:

const instanceId = Math.random().toString(36).slice(2, 8);

// 取出旧监听器引用
const prevListener = (window as any).__cclMessageListener;
if (prevListener) {
window.removeEventListener('message', prevListener);
}

// 定义新监听器
const messageListener = (event: MessageEvent) => {
// ... 处理逻辑
};

// 存储当前引用(供下次 HMR 取用)
(window as any).__cclMessageListener = messageListener;

// 注册
window.addEventListener('message', messageListener);

关键点removeEventListener 必须传入和 addEventListener 相同的函数引用。把函数存到 window 变量上,下次 HMR 时就能取出旧引用并正确移除。这样无论热重载多少次,始终只有一个活跃的 message 监听器。

如果你同时遇到 postMessage 的 targetOrigin 安全问题Cookie 取值导致数据全零,建议一并排查。

注意事项

  • 此问题不仅限于 WXT,所有支持 content script 热重载的框架(Plasmo、CRXJS 等)都可能遇到
  • window 上的变量在页面刷新前一直存在,HMR 只替换脚本模块不清除 window 属性
  • 生产构建不存在此问题(content script 只加载一次),但开发时会产生难以排查的重复请求

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

WSL2 + Docker 两个网络坑:端口被静默占用 & host 模式 localhost 不通

· 阅读需 5 分钟

TL;DR

WSL2 + Docker Desktop 有两个常见的网络坑:

  1. 端口被静默占用:Docker 容器映射 5432 后,SSH 隧道 localhost:5432 连到的是容器内的 PostgreSQL 而非远程服务器——密码没错,连的实例错了
  2. host 模式 localhost 不通network_mode: host 共享的是 Docker 工具 VM 网络,不是 WSL2 网络——curl localhost:8080 失败

容器端口在 WSL2 里访问不到?Docker Desktop 的 network_mode:host 陷阱

· 阅读需 4 分钟

在为客户搭建 AI 数据分析平台(Airflow + PostgreSQL)的本地开发环境时遇到此问题,记录根因与解法。

TL;DR

Docker Desktop for Windows (WSL2 backend) 下,network_mode: host 的容器端口无法从 WSL2 宿主机访问。容器内 ss 显示端口在监听,但宿主机 curl localhost:PORT connection refused。原因是 host 模式共享的是 Docker 内部工具 VM 的网络,不是 WSL2 的网络。解法:override 文件中用 network_mode: !reset 移除 host 模式,改用 bridge + external network + 端口映射。

修改 WordPress Block Theme 不生效?FSE 开发 5 大难题排查指南

· 阅读需 8 分钟

在为客户开发 WordPress Block Theme 时反复遇到这五个问题,每次排查都花了不少时间。整理成指南,帮助同样在做 FSE 开发的同学快速定位。

TL;DR

五个问题按频率排序:文件修改不生效(数据库缓存覆盖文件)、块嵌套错乱(注释未关闭)、子主题内容不渲染(缺少 post-content 块)、SVG 图标消失(WP_Filesystem 被插件污染)、WP-CLI 邮件失败(SMTP 插件在命令行不生效)。每个场景都给出可直接复用的排查命令。

WooCommerce 升级后编辑器显示异常、页面 404?Block Theme 排查指南

· 阅读需 7 分钟

在为客户构建 WooCommerce Block Theme 时遇到了这四个与 WooCommerce 相关的问题,每个都与 FSE 架构和 WooCommerce 的块系统有关。记录排查过程,帮助同样在做 WooCommerce 主题开发的同学避坑。

TL;DR

四个 WooCommerce Block Theme 开发中的常见问题:块重命名导致 core/missing(升级后块名加了 -block 后缀)、修改 shop slug 后产品归档 404(rewrite 缓存未刷新)、模板 HTML 与 Gutenberg save 不匹配(动态块验证失败)、Cart/Checkout 模板未自动分配(需要手动指定)。每个场景给出可直接复用的修复方案。