PAYATHON 2026

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 文档。

备注:内容仅供参考。