使用WebAssembly在浏览器运行Python代码

1,352 阅读6分钟

长期以来,Python 社区一直在探讨使 Python 成为现代 Web 浏览器中一等公民的最佳方法。最大的挑战是: Web 浏览器实际上只支持一种编程语言:JavaScript。然而,随着网络技术的进步,我们已经将越来越多的应用程序推向了网络,如游戏、科学可视化以及音频和视频编辑软件。这意味着我们已经将繁重的计算带到了 Web 上——这是 JavaScript 所不为之而设计的。所有这些挑战都提出了对 Web 的低级语言的需求,该语言可以提供快速、可移植、紧凑和安全的执行。因此,主要的浏览器供应商致力于这个想法,并在 2017 年向世界推出了 WebAssembly。

在本教程中,我们将了解 WebAssembly 如何帮助您在浏览器中运行 Python 代码。

什么是WebAssembly

根据 Mozilla 开发者网络 (MDN) 文档中的定义,WebAssembly (WASM) 是:

一种新型代码,可以在现代 Web 浏览器中运行,并提供新功能和性能的重大提升。它不是手工编写的,而是被设计为C,C++,Rust等源语言的有效编译目标。

因此,WASM 让我们在浏览器中使用不同语言(不仅仅是 JavaScript)编写的代码,具有以下好处:

  • 它快速、高效且便携。
  • 它是安全的,因为代码在安全的沙盒执行环境中运行。
  • 它可以在客户端运行。

因此,在上面的示例中,我们不需要担心用户在我们的服务器上运行代码,因为代码执行发生在客户端的 Web 浏览器中。

WebAssembly 并不是为了扼杀 JavaScript 而设计的。它是对 JavaScript 的补充。在 JavaScript 不合适使用场景,可以使用它。例如游戏、图像识别和图像/视频编辑等。

Pyodide

本教程使用 Pyodide 库运行 Python 代码,该代码将 CPython 解释器编译为 WebAssembly,并在浏览器的 JavaScript 环境中运行二进制文件。它带有许多预装的 Python 包。

Hello World

使用以下代码创建新的 HTML 文件:

<head>
  <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script>
  <script>
    async function main() {
      let pyodide = await loadPyodide({
        indexURL : "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/"
      });
      console.log(pyodide.runPython("print('Hello, world from the browser!')"));
    };
    main();
  </script>
</head>

在浏览器中打开文件。然后,在浏览器开发人员工具的控制台中,您应该执行如下操作:

Loading distutils
Loading distutils from https://cdn.jsdelivr.net/pyodide/v0.20.0/full/distutils.js
Loaded distutils
Python initialization complete
Hello, world from the browser!

如您所见,最后一行是在浏览器中执行 Python 代码的结果。

让我们快速看一下上面的代码:

  1. 首先,您可以使用 [CDN](Downloading and deploying Pyodide — Version 0.24.1) 或直接从 GitHub 版本下载并安装 [Pyodide](Downloading and deploying Pyodide — Version 0.24.1)。
  2. loadPyodide 加载并初始化 Pyodide wasm 模块。
  3. pyodide.runPython 将 Python 代码作为字符串并返回代码的结果。

Pyodide的优势

在前面的示例中,您了解了安装 Pyodide 并开始使用它是多么容易。您只需要从 CDN 导入 pyodide.js 并通过 初始化它 loadPyodide 。之后,您可以使用 pyodide.runPython("Your Python Code Here") 在浏览器中运行 Python 代码。

当您第一次下载 Pyodid 时,下载量很大,因为您正在下载完整的 CPython 解释器,但您的浏览器会缓存它,不需要再次下载。

Pyodide 限制

要第一次加载 Pyodide,需要四到五秒钟(取决于您的连接),因为您必须下载 ~10MB。此外,Pyodide 代码的运行速度比原生 Python 慢 3 到 5 倍左右。

其他选项

通常,如果要在浏览器中运行 Python,可以使用两种方法:

  1. 使用转译器将 Python 转换为 JavaScript。BrythonTranscryptSkulpt 都使用这种方法。
  2. 转换 Python 运行时以在浏览器中使用。Pyodide和PyPy.js使用这种方法。

选项 1 和 2 之间的一个主要区别是选项 1 中提到的库不支持 Python 包。也就是说,它们的下载大小比选项二中的库小得多,因此速度更快。

在本教程中,我们选择了 Pyodide,因为它具有更简单的语法并且支持 Python 包。如果您对其他选项感兴趣,请随时查看他们的文档。

Python 代码编辑器

在本节中,我们将创建一个简单的 Python 编辑器,该编辑器可以使用以下命令在浏览器中运行代码:

创建一个新项目:

$ mkdir python_editor_wasm
$ cd python_editor_wasm

创建并激活虚拟环境:

$ python3.10 -m venv env
$ source env/bin/activate
(env)$

Install Flask: 安装 Flask:

(env)$ pip install Flask

在项目的根目录中,创建一个名为 app.py 的文件,并添加以下代码:

from flask import Flask, render_template

app = Flask(__name__)

@app.route('/')
def index():
    return render_template('index.html')

if __name__ == '__main__':
    app.run(debug=True)

在项目的根目录下创建一个templates文件夹,并在其下添加 index.html 文件。

templates/index.html:

<!doctype html>
<html class="h-full bg-slate-900">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <!-- install tailwindcss from cdn, don't do this for production application -->
  <script src="https://cdn.tailwindcss.com"></script>
  <!-- install pyodide version 0.20.0 -->
  <script src="https://cdn.jsdelivr.net/pyodide/v0.20.0/full/pyodide.js"></script>
  <!-- import codemirror stylings -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.48.4/codemirror.min.css" />
  <!-- install codemirror.js version /5.63.3 from cdn -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/codemirror.min.js"
  integrity="sha512-XMlgZzPyVXf1I/wbGnofk1Hfdx+zAWyZjh6c21yGo/k1zNC4Ve6xcQnTDTCHrjFGsOrVicJsBURLYktVEu/8vQ=="
  crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <!-- install codemirror python language support -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/mode/python/python.min.js"
  integrity="sha512-/mavDpedrvPG/0Grj2Ughxte/fsm42ZmZWWpHz1jCbzd5ECv8CB7PomGtw0NAnhHmE/lkDFkRMupjoohbKNA1Q=="
  crossorigin="anonymous" referrerpolicy="no-referrer"></script>
  <!-- import codemirror dracula theme styles from cdn -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.63.3/theme/dracula.css"/>
  <style>
    /* set codemirror ide height to 100% of the textarea */
    .CodeMirror {
      height: 100%;
    }
  </style>
</head>
<body class="h-full overflow-hidden max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 mt-8">
  <p class="text-slate-200 text-3xl my-4 font-extrabold mx-2 pt-8">Run Python in your browser</p>
  <div class="h-3/4 flex flex-row">
    <div class="grid w-2/3 border-dashed border-2 border-slate-500 mx-2">
      <!-- our code editor, where codemirror renders it's editor -->
      <textarea id="code" name="code" class="h-full"></textarea>
    </div>
    <div class="grid w-1/3 border-dashed border-2 border-slate-500 mx-2">
      <!-- output section where we show the stdout of the python code execution -->
      <textarea readonly class="p-8 text-slate-200 bg-slate-900" id="output" name="output"></textarea>
    </div>
  </div>
  <!-- run button to pass the code to pyodide.runPython() -->
  <button onclick="evaluatePython()" type="button" class="mx-2 my-4 h-12 px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm bg-green-700 hover:bg-green-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-700 text-slate-300">Run</button>
  <!-- clean the output section -->
  <button onclick="clearHistory()" type="button" class="mx-2 my-4 h-12 px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm bg-red-700 hover:bg-red-900 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-700 text-slate-300">Clear History</button>
  <script src="/static/js/main.js"></script>
</body>
</html>

在 index.html 文件的头部,我们导入了用于样式的 Tailwind CSS、Pyodide.js 版本 0.20.0 以及 CodeMirror 及其依赖项。

UI 有三个重要组件:

  1. Editor:用户可以在其中编写 Python 代码。它是一个 textarea 带有 id 的 code HTML 元素。当我们初始化时 codemirror ,我们让它知道我们想将这个元素用作代码编辑器。
  2. Output:显示代码输出的位置。它是带有 id 的 output textarea 元素。当 Pyodide 执行 Python 代码时,它会将结果输出到此元素。我们还在此元素中显示了错误消息。
  3. Run button:当用户单击此按钮时,我们获取编辑器元素的值并将其作为字符串传递给 pyodide.runPython .当返回结果时 pyodide.runPython ,我们将其显示在输出元素中。

现在,在项目的根目录中,创建static/js文件夹。然后,在js文件夹下,创建一个名为 main.js 的新文件。

static/js/main.js:

// find the output element
const output = document.getElementById("output");

// initialize codemirror and pass configuration to support Python and the dracula theme
const editor = CodeMirror.fromTextArea(
  document.getElementById("code"), {
    mode: {
      name: "python",
      version: 3,
      singleLineStringErrors: false,
    },
    theme: "dracula",
    lineNumbers: true,
    indentUnit: 4,
    matchBrackets: true,
  }
);
// set the initial value of the editor
editor.setValue("print('Hello world')");
output.value = "Initializing...\n";

// add pyodide returned value to the output
function addToOutput(stdout) {
  output.value += ">>> " + "\n" + stdout + "\n";
}

// clean the output section
function clearHistory() {
  output.value = "";
}

// init pyodide and show sys.version when it's loaded successfully
async function main() {
  let pyodide = await loadPyodide({
    indexURL: "https://cdn.jsdelivr.net/pyodide/v0.20.0/full/",
  });
  output.value = pyodide.runPython(`
    import sys
    sys.version
  `);
  output.value += "\n" + "Python Ready !" + "\n";
  return pyodide;
}

// run the main function
let pyodideReadyPromise = main();

// pass the editor value to the pyodide.runPython function and show the result in the output section
async function evaluatePython() {
  let pyodide = await pyodideReadyPromise;
  try {
    pyodide.runPython(`
      import io
      sys.stdout = io.StringIO()
    `);
    let result = pyodide.runPython(editor.getValue());
    let stdout = pyodide.runPython("sys.stdout.getvalue()");
    addToOutput(stdout);
  } catch (err) {
    addToOutput(err);
  }
}
  1. 初始化了 CodeMirror,支持 Python 和 Dracula 主题。
  2. 初始化 Pyodide。
  3. 添加了一个调用 evaluatePython 的函数,该函数在用户单击 Run 按钮时执行。它将元素的值传递给 pyodide.runPython output 元素,并通过 在 code 元素中显示结果 addToOutput 。
  4. 添加了一个名为的函数 clearHistory ,该函数在用户单击 Clear History 按钮时清除 output 元素。

若要在本地运行 Flask 开发服务器,请运行:

(env)$ flask run

服务现在应该在端口 5000 上运行。导航到浏览器中的 http://127.0.0.1:5000 以测试代码编辑器。

原文: Running Python in the Browser with WebAssembly | TestDriven.io