Chrome 中 executeScript 无法访问 chrome-extension:// 页面怎么办?
在 Chrome 中,扩展自己的 chrome-extension://…/main.html 页面应直接加载扩展包内的 JavaScript,无需调用 chrome.scripting.executeScript()。增加 host_permissions 也无法解除扩展页面的注入限制。
要根据用户选项创建对象,可以在按钮的点击事件中读取表单,再调用构造函数。脚本加载和对象实例化可以分开进行。如果希望点击后才加载文件,可以使用动态 import()。
为什么现有代码会报错?
chrome.scripting.executeScript() 用于向目标页面注入脚本,注入的脚本属于 content scripts。Chrome 不允许通过这种方式向扩展页面注入代码,即使目标页面属于当前扩展。MDN 的 API 说明也列出了这一限制。
错误提示中的“必须请求此主机的访问权限”容易让人误以为补充权限就能解决。但目标是扩展页面时,增加权限没有用:
*://*/*中的协议通配符只匹配http和https,不包含chrome-extension。activeTab、tabs和scripting都不能解除扩展页面的注入限制。content_scripts中的配置不会替代main.html自己的脚本引用。
协议匹配规则可见 Chrome 的 match patterns 文档。
通过 fetch() 获取远程页面内容,也不会让当前标签页导航到该地址。当前文档仍然是 chrome-extension://…/main.html,脚本应按扩展页面的方式加载。
按需加载模块,并传入用户选项
可以让 ascript.js 导出类,在点击按钮后加载模块,再用当前表单中的值创建实例。下面三个文件需要放在同一目录,并随扩展一起打包。
1. 在 main.html 中提供控件
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>bingo</title>
</head>
<body>
<label>
匹配文本:
<input id="keyword" type="text" value="example">
</label>
<label>
<input id="case-sensitive" type="checkbox">
区分大小写
</label>
<button id="test" type="button">test</button>
<pre id="result"></pre>
<script type="module" src="./main.js"></script>
</body>
</html>
模块脚本默认延迟执行,因此这里可以直接访问已解析完成的控件,无需再包一层 DOMContentLoaded。
2. 让 ascript.js 导出类
export class Bingo {
constructor({ keyword, caseSensitive }) {
this.keyword = keyword;
this.caseSensitive = caseSensitive;
}
run(text) {
const source = this.caseSensitive ? text : text.toLowerCase();
const keyword = this.caseSensitive
? this.keyword
: this.keyword.toLowerCase();
return source.includes(keyword);
}
}
这个文件只定义并导出类,不在顶层创建实例。实际项目中的请求、解析等业务逻辑可以放在相应方法里。
3. 在 main.js 中读取选项、加载模块并实例化
const button = document.getElementById("test");
const result = document.getElementById("result");
button.addEventListener("click", async () => {
button.disabled = true;
try {
const options = {
keyword: document.getElementById("keyword").value.trim(),
caseSensitive: document.getElementById("case-sensitive").checked,
};
if (!options.keyword) {
result.textContent = "请输入匹配文本";
return;
}
const { Bingo } = await import("./ascript.js");
const bingo = new Bingo(options);
const matched = bingo.run("Example page content");
result.textContent = matched ? "匹配成功" : "未匹配";
} catch (error) {
console.error(error);
result.textContent =
error instanceof Error ? error.message : String(error);
} finally {
button.disabled = false;
}
});
import("./ascript.js") 加载本地模块,返回包含模块导出内容的对象。随后,new Bingo(options) 将用户选项传给构造函数。动态 import() 的用法说明中也介绍了根据用户操作加载代码的场景。
同一模块在同一页面上下文中成功加载后会被缓存,后续点击通常不会重新执行模块顶层代码。点击处理函数中的 new Bingo(options) 则会在每次点击时运行,用当次读取的选项创建新对象。
如果只需延迟实例化,可以提前加载脚本
通过 <script> 引入文件后,可以等用户点击时再创建对象。对于体积不大的模块,可以在 main.js 顶部静态导入:
import { Bingo } from "./ascript.js";
然后删除点击处理函数中的这一行:
const { Bingo } = await import("./ascript.js");
其余代码保持不变。模块随页面加载,实例仍在用户点击按钮后才创建。
普通脚本标签也可以这样使用:先加载定义类或初始化函数的文件,再由 main.js 的事件处理函数传入选项。依赖用户输入的初始化操作应放在事件处理函数中,避免写在文件顶层。
manifest 和使用时需要注意什么?
如果扩展只运行自己的控制面板,无需向外部网页注入脚本,可以移除相应的 content_scripts 配置,以及仅用于注入的 scripting、activeTab 权限。上面的代码也不再需要 chrome.tabs.query()。是否保留 tabs 权限,应根据扩展的其他功能判断。
本地模块加载和远程请求需要分别处理:
- 扩展页面加载自身包内的 JS 文件,不需要声明
web_accessible_resources。该配置用于向网页或其他扩展开放资源,详见 Chrome 文档。 - 如果控制面板需要通过
fetch()跨源读取远程内容,仍应声明并获得目标站点的host_permissions,例如"https://example.com/*"。这些权限用于网络请求,详见 Chrome 的跨源请求说明。 - 示例适用于扩展 HTML 页面中的
main.js。不要直接将动态import()的写法搬到 Manifest V3 的后台 service worker 中。
Manifest V3 扩展页面还受 CSP 约束。代码应放在随扩展打包的文件中,避免使用内联事件处理器、eval() 或 new Function()。默认策略允许加载扩展自身的本地脚本,具体限制见 Chrome 的 CSP 文档。
备注:内容仅供参考。
