根本原因
该问题由多种因素叠加导致:
1. **Manifest V3 资源声明缺失**
- 通过 `chrome.runtime.getURL()` 动态加载的 `js/selector-recorder.js`、`js/hover-highlight.js` 未在 `web_accessible_resources` 中声明。
- 在 Manifest V3 中,这类资源不声明就无法被 content script 正常访问,导致资源请求返回 `chrome-extension://invalid/`。
2. **Content Script 在多 frame 中重复执行**
- `manifest.json` 中配置了 `all_frames: true`。
- 没有防重复初始化的机制,导致在多个 frame 中多次创建 `ContentExtractor`、多次注册 `chrome.runtime.onMessage` 监听器。
3. **特殊页面处理不当**
- 在 `chrome://`、`edge://`、`about:` 等特殊协议页面,或其他扩展页面中,`chrome.runtime` 行为与普通网页不同。
- 缺少"跳过这些页面"的逻辑,导致在不受支持的环境里仍尝试加载脚本。
4. **缺少 chrome.runtime 可用性检查**
- 直接调用 `chrome.runtime.getURL()`,未检查 `chrome` 和 `chrome.runtime` 是否存在。
- 在部分环境下返回 `invalid` URL 或抛出异常。
5. **消息监听器重复注册**
- `ContentExtractor.init()` 在错误路径(异常重试等)下可能被多次调用。
- 未加"已注册标记"的监听器会重复处理消息,引发难以追踪的副作用。
解决方案
1. **在 manifest.json 中声明 web_accessible_resources**
```json
{
"web_accessible_resources": [
{
"resources": [
"js/selector-recorder.js",
"js/hover-highlight.js"
],
"matches": ["<all_urls>"]
}
]
}
```
2. **为 Content Script 添加防重复初始化标记**
```javascript
// 防止重复初始化
if (typeof window !== 'undefined' && !window.__CONTENT_EXTRACTOR_INITIALIZED__) {
window.__CONTENT_EXTRACTOR_INITIALIZED__ = true;
const isTopFrame = window === window.top;
const isSpecial = isSpecialPage();
if (isTopFrame || !isSpecial) {
try {
const contentExtractor = new ContentExtractor();
window.__contentExtractor = contentExtractor;
} catch (error) {
console.error('ContentExtractor 初始化失败:', error);
}
} else {
console.log('在特殊页面或子 frame 中,跳过 ContentExtractor 初始化');
}
} else {
console.log('ContentExtractor 已初始化,跳过重复初始化');
}
```
3. **为 HoverHighlighter 添加防重复初始化标记**
```javascript
function initializeHoverHighlighter() {
try {
if (typeof window !== 'undefined' && !window.__HOVER_HIGHLIGHTER_INITIALIZED__) {
window.__HOVER_HIGHLIGHTER_INITIALIZED__ = true;
if (!window.hoverHighlighter) {
if (typeof HoverHighlighter !== 'undefined') {
window.hoverHighlighter = new HoverHighlighter();
console.log('悬停高亮系统初始化成功');
} else {
window.hoverHighlighter = {
activate: () => {},
deactivate: () => {},
isActive: false,
highlightElement: () => {},
removeHighlight: () => {}
};
}
}
}
} catch (error) {
if (typeof window !== 'undefined' && !window.hoverHighlighter) {
window.hoverHighlighter = {
activate: () => {},
deactivate: () => {},
isActive: false,
highlightElement: () => {},
removeHighlight: () => {}
};
}
}
}
// 只在顶层 frame 中初始化
if (typeof window !== 'undefined' && window === window.top) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => {
initializeHoverHighlighter();
});
} else {
initializeHoverHighlighter();
}
}
```
4. **添加 chrome.runtime 可用性检查**
```javascript
function isChromeRuntimeAvailable() {
try {
return typeof chrome !== 'undefined' &&
chrome.runtime &&
typeof chrome.runtime.getURL === 'function';
} catch (e) {
return false;
}
}
```
5. **添加特殊页面检测**
```javascript
function isSpecialPage() {
try {
const url = window.location.href;
const isSpecialProtocol = /^(chrome|edge|about|moz-extension|chrome-extension):\/\//i.test(url);
if (!isSpecialProtocol) return false;
if (url.startsWith('chrome-extension://')) {
if (isChromeRuntimeAvailable()) {
try {
const extensionId = chrome.runtime.id;
return !url.startsWith('chrome-extension://' + extensionId);
} catch (e) {
return true;
}
}
return true;
}
return true;
} catch (e) {
return true;
}
}
```
6. **增强外部脚本加载错误处理**
```javascript
function loadExternalScripts() {
return new Promise((resolve) => {
if (!isChromeRuntimeAvailable()) {
console.warn('chrome.runtime 不可用,跳过外部脚本加载');
resolve();
return;
}
if (isSpecialPage()) {
console.warn('特殊页面,跳过外部脚本加载');
resolve();
return;
}
let loadedCount = 0;
const totalScripts = 2;
let hasError = false;
const checkCompletion = () => {
loadedCount++;
if (loadedCount === totalScripts) {
if (hasError) {
console.warn('部分外部脚本加载失败,但将继续运行');
}
resolve();
}
};
// 此处分别加载 selector-recorder.js 和 hover-highlight.js,
// 每个都要检查 URL 是否包含 'invalid',并处理 onerror 和 document.head 不存在的情况
// (具体实现略,同当前项目中的修复代码)。
});
}
```
7. **防止消息监听器重复注册**
```javascript
init() {
// 防止重复注册消息监听器
if (this.messageListenerRegistered) {
console.log('消息监听器已注册,跳过重复注册');
return;
}
this.messageListenerRegistered = true;
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
// 正常消息处理逻辑
});
}
```
上述修改后,问题页面不再出现循环刷新和脚本加载失败的报错。