如何用WebAssembly在浏览器中运行Python的教程

1,010 阅读7分钟

在本教程中,我们将看一下WebAssembly如何帮助你在浏览器中运行Python代码。

要明确的是,JavaScript本身就是一种强大的编程语言。它只是不适合某些事情。关于这一点,请看JavaScript的创造者Brendan Eich的《从ASM.JS到WebAssembly》。

我们正在建造的东西

比方说,你想教一门Python课程。为了使你的课程更加有趣和好玩,你想在每一节课后为你的学生提供一个练习,这样他们就可以练习他们所学到的知识。

这里的问题是,学生需要通过安装特定版本的Python来准备一个开发环境,创建并激活一个虚拟环境,并安装所有必要的包。这可能会耗费大量的时间和精力。由于每台机器都是不同的,所以也很难对此提供确切的指示。

虽然你可以创建一个后端,在Docker容器或AWS Lambda函数中运行提交的代码,但你选择保持堆栈简单,在课程内容中添加一个Python编辑器,可以在客户端、在网络浏览器中运行Python代码,并向用户展示结果。这正是你在本教程中要建立的东西。

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

一种可以在现代网络浏览器中运行的新型代码,它提供了新的功能和性能上的重大改进。它主要不是用来手写的,而是被设计成C、C++、Rust等源语言的有效编译目标。

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

  1. 它是快速、高效和可移植的。
  2. 它是安全的,因为代码是在一个安全的沙盒执行环境中运行的。
  3. 它可以在客户端运行。

因此,在我们上面的例子中,如果用户在我们的服务器上运行代码,我们不需要担心,如果成千上万的学生尝试练习代码,我们也不需要担心,因为代码的执行发生在客户端,在Web浏览器上。

WebAssembly的设计并不是为了杀死JavaScript。它是对JavaScript的补充。它可以在JavaScript不是合适的工具时使用,比如游戏、图像识别和图像/视频编辑,等等。

请看WebAssembly.org使用案例,了解更多你可能想利用WebAssembly的情况。

Pyodide

本教程使用Pyodide库来运行Python代码,它将CPython解释器编译成WebAssembly,并在浏览器的JavaScript环境中运行二进制。它带有一些预装的Python软件包。你也可以使用Micropip来使用更多默认没有的软件包。

你好,世界

用下面的代码创建一个新的HTML文件。

在你的浏览器中打开该文件。然后,在浏览器的开发者工具的控制台中,你应该做如下的事情。

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或直接从GitHub发布的版本下载并安装Pyodide。
  2. loadPyodide加载并初始化 Pyodide wasm 模块。
  3. pyodide.runPython将Python代码作为一个字符串,并返回代码的结果。

Pyodide的优势

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

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

在Pyodide上还有一个庞大而活跃的社区。

Pyodide的局限性

第一次加载Pyodide,需要四到五秒(取决于你的连接),因为你必须下载~10MB。另外,Pyodide代码的运行速度比本地Python慢3到5倍。

其他选择

一般来说,如果你想在浏览器中运行Python,你有两种方法可用。

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

方案一和方案二的一个主要区别是,方案一中提到的库不支持Python包。也就是说,它们的下载量比方案二中的库要小得多,因此,它们的速度也更快。

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

Python代码编辑器

在本节中,我们将创建一个简单的Python编辑器,可以在浏览器中运行代码。

创建一个新的项目。

$ mkdir python_editor_wasm
$ cd python_editor_wasm

创建并激活一个虚拟环境。

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

安装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. 编辑器。用户可以在这里写Python代码。id 它是一个textarea 的HTML元素,code 。当我们初始化codemirror ,我们让它知道我们想用这个元素作为代码编辑器。
  2. 输出。将显示代码输出的地方。这是一个textarea 元素,它的名字是id ofoutput 。当 Pyodide 执行 Python 代码时,它将把结果输出到这个元素。我们也在这个元素中显示错误信息。
  3. 运行按钮。当用户点击这个按钮时,我们抓取编辑器元素的值并将其作为字符串传递给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,支持PythonDracula主题
  2. 初始化了Pyodide。
  3. 添加了一个名为evaluatePython 的函数,当用户点击Run 按钮时执行。它将code 元素的值传递给pyodide.runPython ,并通过addToOutputoutput 元素中显示结果。
  4. 添加了一个名为clearHistory 的函数,当用户点击Clear History 按钮时,它将清除output 元素。

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

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

总结

在本教程中,我们几乎没有触及Pyodide和WebAssembly的冰山一角。我们看到了如何使用WebAssembly在浏览器中运行Python代码,但一般来说,WebAssembly涵盖了更广泛的使用情况。

我们的部署平台比以往任何时候都更加多样化,我们根本没有时间和金钱去不断地为多个平台重写软件。WebAssembly可以影响客户端Web开发、服务器端开发、游戏、教育、云计算、移动平台、物联网、无服务器等世界。

WebAssembly的目标是提供快速、安全、便携和紧凑的软件。

你可以在这里找到代码库。