浏览器插件制作流程

目标与效果

  • 打开浏览器扩展图标 → 弹出页 执行 hello.py → 在页面内看到输出

  • 完全离线:不依赖 CDN;Pyodide 运行时随扩展打包

  • 便于扩展:后续可把任何 Python 逻辑放进来

  • 适用于以下浏览器

    • Google Chrome 140
    • Chromium Version 141
    • Firefox
  1. 下载 Pyodide 离线包

    • 用于浏览器扩展或本地部署(例如前端中直接运行 Python),通常选择 “full distribution” 版本,即包含所有 vendored 包(第三方库预打包)的大包。通常文件比较大(300多M)
    • 若你想减小体积,仅用基础功能,也可以选 “core” 版本(只含运行时和标准库)——然后手动引入其他包。

    离线包解压后能看到 pyodide.js / pyodide.wasm / python_stdlib.zip(或 pyodide_py.tar 等)及若干 .data/.json 文件

  2. 创建项目目录结构。在任意空文件夹(例:my-explorer-extension/ )内创建如下结构:

    my-explorer-extension/
    ├─ manifest.json
    ├─ popup.html
    ├─ popup.js
    ├─ py/
    │ └─ hello.py
    └─ pyodide/ # ← 把 Pyodide 离线包整个丢进来
    ├─ pyodide.js
    ├─ pyodide.wasm
    ├─ python_stdlib.zip
    ├─ ...若干 .data/.json
  3. 核心文件内容

    1. manifest.json (Chromium MV3)
      {
      "manifest_version": 3,
      "name": "Python Hello (Pyodide)",
      "version": "1.0.0",
      "action": {
      "default_title": "Python Hello",
      "default_popup": "popup.html"
      },
      "icons": { "128": "icon128.png" },
      "web_accessible_resources": [
      { "resources": ["pyodide/**", "py/hello.py"], "matches": ["<all_urls>"] }
      ],
      "content_security_policy": {
      "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'none';"
      }
      }
    • 不得加载远程脚本(CDN)。Pyodide 必须随扩展打包。

    • WASM 权限:'wasm-unsafe-eval' 让扩展页能加载 WebAssembly。

    • web_accessible_resources 允许在扩展页面读取 pyodide/**py/hello.py

    1. popup.html 点击扩展后的弹出页面 HTML

      <!doctype html>
      <html>
      <head>
      <meta charset="utf-8" />
      <title>Python Hello</title>
      <style>
      body { font: 14px/1.4 system-ui, sans-serif; padding: 12px; width: 360px; }
      button { padding: 8px 12px; border-radius: 8px; border: 1px solid #ddd; }
      #status { margin: 8px 0; color: #666; }
      pre { background: #f6f6f6; padding: 10px; border-radius: 8px; min-height: 80px; }
      </style>
      </head>
      <body>
      <h3>Run Python in a Chrome Extension</h3>
      <div id="status">Loading Python runtime…</div>
      <button id="run" disabled>Run hello.py</button>
      <pre id="out">[output will appear here]</pre>
      <script src="popup.js"></script>
      </body>
      </html>

    2. popup.js

      let pyodide;
      const statusEl = document.getElementById("status");
      const outEl = document.getElementById("out");
      const runBtn = document.getElementById("run");

      (async () => {
      try {
      // 1) 从扩展内部动态导入 pyodide.js
      const url = chrome.runtime.getURL("pyodide/pyodide.js");
      await import(url);

      // 2) 初始化 Pyodide;indexURL 指向扩展内的 pyodide 目录
      pyodide = await loadPyodide({
      indexURL: chrome.runtime.getURL("pyodide/")
      });

      statusEl.textContent = "Python ready ✅";
      runBtn.disabled = false;
      } catch (e) {
      statusEl.textContent = "Failed to load Python runtime.";
      console.error(e);
      }
      })();

      runBtn.addEventListener("click", async () => {
      runBtn.disabled = true;
      outEl.textContent = "Running hello.py…";
      try {
      // 3) 读取扩展内的 hello.py
      const helloUrl = chrome.runtime.getURL("py/hello.py");
      const code = await (await fetch(helloUrl)).text();

      // 4) 捕获 stdout
      await pyodide.runPythonAsync(`
      import sys
      class _Cap:
      def __init__(self): self.buf=[]
      def write(self, s): self.buf.append(s)
      def flush(self): pass
      old_stdout = sys.stdout
      cap = _Cap()
      sys.stdout = cap
      `);

      // 5) 执行 python 源码
      await pyodide.runPythonAsync(code);

      // 6) 取回缓冲
      const output = await pyodide.runPythonAsync("''.join(cap.buf)");
      outEl.textContent = output;

      // 7) 复位 stdout(可选)
      await pyodide.runPythonAsync("sys.stdout = old_stdout");
      } catch (e) {
      outEl.textContent = "Error: " + e.message;
      console.error(e);
      } finally {
      runBtn.disabled = false;
      }
      });

    3. py/hello.py Python 文件,本示例以输出内容为例

      print("hello world from Python inside a browser extension!")


  4. 在本地加载与试跑(Chromium)

    1. 打开 chrome://extensions
    2. 右上角打开 开发者模式
    3. 选择 加载未打包的扩展程序 ,在弹出窗口中选择示例程序目录,本示例为 my-explorer-extension/
    4. 正常加载扩展程序后,会在 我的扩展程序 中看到 Python Hello (Pyodide) 1.0.0 (manifest.json 文件中定义的扩展程序名称和版本),点击浏览器的 扩展 ,下拉列表中也会看到新加的扩展程序。
    5. 在扩展中选择 Python Hello (Pyodide) 执行,会弹出 popup.html 页面,并可以点击 Run hello.py 运行 Python 程序。

常见错误

Manifest is not valid JSON. Can’t read file. Could not load manifest.

在 Windows 系统上的 Chrome 中运行正常,Linux 中 Chromium Version 141.0.7390.54 (Official Build) snap (64-bit) 报错: Error Manifest is not valid JSON. Can't read file. Could not load manifest.

原因 : 不是描述所说的 JSON 格式错误,而是因为 snap 沙箱读不到文件 。snap 版 Chromium 对文件访问有严格沙箱:默认只能读 $HOME 里的内容(比如 ~/Downloads , ~/Documents )。如果扩展目录在 可移动磁盘 或 Windows 分区挂载(例如 /media/xxx/…/mnt/d/… ,就会出现 “Can’t read file”可以把扩展整个文件夹搬到家目录中尝试