浏览器扩展脚本为什么有时候不生效:注入时机、iframe 和单页路由,多多开票助手

发布时间:2026/10/3 6:56:24
浏览器扩展脚本为什么有时候不生效:注入时机、iframe 和单页路由,多多开票助手 脚本没生效先确认它到底有没有被注入我自己写的这个扩展起初就是给自己用的名字叫多多开票助手官网 duoduoke.net。它盯的是电商订单和发票这条线要在三种页面上去批量申请发票拼多多的发票页、1688 的买家订单页还有 1688 的站内聊天页。三段脚本的写法差不多跑起来的命运却完全不一样有的页面一切正常有的页面控制台干干净净一行日志都没有。一开始我在代码里到处找问题后来才想明白脚本在那些页面上压根就没被注入自然什么都不会打印。content script 的注入是浏览器按 manifest 里的规则做的规则匹配不上或者 frame 不对代码连执行的机会都没有。想清楚这一点排查顺序也就顺了先看有没有注入再看注入了几个 frame再看路由变化之后有没有重新注入。这篇按这个顺序讲。确认有没有注入有个土办法很管用打开控制台看左上角的执行上下文下拉框它会列出当前页面里的所有 frame。逐个切过去在控制台里敲一句typeof chrome.runtime能返回对象就说明这个 frame 上有扩展脚本在跑。这比在代码里到处加日志快。给一句能直接记住的content_scripts 默认只注入顶层文档iframe 里的页面要显式声明 all_frames 才会被注入。六条注册分开放别写一条通吃的我最早的写法是一条matches: [all_urls]所有页面都注入脚本里再自己判断location.host。图省事代价来得很快。一是权限描述变难看。用户装扩展的时候all_urls会显示成「读取您在所有网站上的数据」装的人一看就犹豫。二是每个页面都要跑一遍判断包括那些永远用不上的页面。现在的写法是分开注册一段脚本只匹配它真正要待的页面content_scripts: [ { matches: [https://mobile.yangkeduo.com/transac_modify_invoice.html*], js: [invoiceAutomate.bundle.js], run_at: document_idle }, { matches: [https://air.1688.com/app/ctf-page/trade-order-list/buyer-order-list.html*], js: [1688OrderInvoice.bundle.js], run_at: document_idle } ]注册条数变多了但每条的范围一目了然。哪个页面用哪段脚本看 manifest 就知道不用去翻代码。有一个副作用要提前知道匹配不上的时候浏览器是静默的。控制台不会有任何提示你可能只是发现功能没反应。所以我在每段脚本的入口都留了一条能看见的信号用来区分「没注入」和「注入了但出错了」。run_at 三档我用的是哪两档run_at决定注入的时机一共三档。document_start是文档刚开始解析就注入这时候 DOM 还没建起来适合要抢在页面自己的脚本之前改行为的场景。document_end是 DOM 建完但图片、样式这些还没加载完。document_idle是浏览器觉得空闲的时候也是默认值最晚可能落在load之后。我这边分两种用法拼多多的商品页用document_end因为要在页面自己的脚本铺开之前先接管一段发票页、订单页、聊天页都用document_idle。这里要提醒一句document_idle不等于「页面已经加载完」它只是个相对空闲的时机。所以别指望run_at替你等到某个按钮出现元素什么时候出现得自己等。等的方式也别用死循环去问「出现了吗」盯着 DOM 的变化比每秒问一次省事得多。iframe 里的页面不加 all_frames 就进不去1688 的聊天窗口不是一个独立页面它嵌在 iframe 里。我一开始用同样的写法给聊天页注册脚本订单页跑得好好的聊天页没反应。原因很直接content_scripts 默认只注入顶层文档iframe 不进去。补上all_frames之后才进去{ matches: [ https://air.1688.com/app/ocms-fusion-components-1688/def_cbu_web_im/index.html*, https://air.1688.com/app/ocms-fusion-components-1688/def_cbu_web_im_core/index.html* ], js: [1688InvoiceChat.bundle.js], run_at: document_idle, all_frames: true }这个坑的麻烦在于它太安静。宿主页面本身是好的只有嵌进去的那一块没反应很容易被当成聊天组件自己的毛病其实是我的脚本压根没进去。加上 all_frames脚本会在每个 frame 里各跑一遍all_frames管的不是某一个 iframe是匹配到的 frame 全都注入。聊天页上可能同时挂着好几个 iframe于是同一份脚本会跑好几遍。多个副本同时跑会互相打架都在等元素、都想发消息、都去动同一份状态。所以每个副本必须自己判断我是不是那个该干活的。订单页那份的判断写在入口第一行// 只允许最外层文档跑且地址必须落在订单列表页 if (window window.top location.href.includes(buyer-order-list.html)) { init1688OrderPage(); }window window.top用来排除掉所有嵌在别人里面的副本只留最外层那一个。聊天页那边不好这么简单处理因为它的业务本来就在 iframe 里所以改成按当前 frame 的地址参数判断地址里带的订单号和会话对象要跟手上这条任务对得上对不上就不动。顺着 frame 再说一件相关的事。我想在扫订单的时候顺便钻进 iframe 里取节点写了个递归遍历遇到同源的能进去遇到跨域的会抛异常if (String(node.tagName || ).toLowerCase() iframe) { try { if (node.contentDocument) walkOpenShadowRoots(node.contentDocument, visitor, visited); } catch (_) { // 跨域 frame 直接跳过这是有意的 } }跨域 iframe 的contentDocument是null这不是暂时拿不到而是浏览器从根上就不让访问。有人为了穿进去会把all_frames和host_permissions一路放宽那是拿权限换一点便利我没这么干。单页路由切换脚本不会重新注入这一条最容易吃亏因为它只影响页面内部跳转的场景。拼多多那套页面是单页应用。从订单列表点进一个详情页地址栏变了可文档没有重新加载。而 content script 是跟着文档走的文档不重新加载脚本就不会重新跑。结果就是我在列表页加的那个悬浮入口点进详情页之后还挂在那儿反过来某个只有详情页才该出现的东西永远不出现因为脚本还是列表页那次跑的那一份。处理办法是自己监听路由变化变了就重新同步一次界面window.addEventListener(popstate, syncEntry); window.addEventListener(hashchange, syncEntry); syncEntry(); function syncEntry() { if (shouldShowPddFloatingEntry(location.href)) createEntry(); else removeEntry(); }syncEntry里不重建界面只判断这个地址该不该有它该有就建不该有就拆掉。这样无论路由怎么跳界面都跟得上。pushState 不触发 popstate我加了一条轮询兜底上面那段代码看着完整其实漏了一类跳转。popstate只在前进、后退这类历史导航时派发。而很多前端路由用的是pushState和replaceState直接改地址这两个 API 不会派发popstate也不会派发hashchange。也就是说只靠这两个事件监听路由会漏掉一部分页面内部的跳转。我是测试时发现的从某些入口点进去悬浮按钮的位置不对手动刷新一下又正常了。刷新会重新注入问题自己就消失了这类 bug 最容易漏掉。补的办法很土但稳let lastHref location.href; setInterval(() { if (location.href lastHref) return; lastHref location.href; syncEntry(); }, 1000);一秒查一次地址栏变了就同步。它不是最优解最优解是去 hook 页面的history.pushState可那要往别人的运行时里插代码风险更大。用一秒的延迟换一段不侵入的代码这笔账我算得过来。小结三件事按顺序过一遍脚本不生效的问题基本能定位。先看匹配范围matches写窄了脚本压根不来。再看 frame嵌在 iframe 里的页面要all_frames但要记得它会进不止一个 frame每个副本都得自己认领身份。再看时机单页应用切路由不会重新注入得自己监听而且不能只认popstate和hashchange。这三件事有个共同的麻烦出错都不吭声。匹配不上没提示frame 不对没提示路由变了没重新注入也没提示。所以我现在的习惯是给每段脚本的入口留一条能看见的信号先确认它在不在再去找它为什么不对。一个不说话的脚本比一个会报错的脚本难查十倍。关键词content_scripts, 浏览器扩展, all_frames, run_at, SPA路由, pushState, iframe注入, Chrome扩展开发