浏览器插件制作流程

目标与效果

  • 打开浏览器扩展图标 → 弹出页 执行 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”可以把扩展整个文件夹搬到家目录中尝试