Brython:浏览器中的 Python(2)

举报
Yuchuan 发表于 2021/12/07 17:16:26 2021/12/07
【摘要】 在本教程中,您学习了如何: 在本地环境中安装和使用 Brython 在前端 Web 应用程序中用 Python 替换 JavaScript 操作DOM 与JavaScript交互 创建浏览器扩展 比较Brython 的替代品

目录

与 JavaScript 交互

Brython 允许 Python 代码与 JavaScript 代码交互。最常见的模式是从 Brython 访问 JavaScript。反过来,虽然可能,但并不常见。您将在JavaScript 单元测试部分看到 JavaScript 调用 Python 函数的示例。

JavaScript

到目前为止,您已经体验了一些 Python 代码与 JavaScript 代码交互的场景。特别是,您已经能够通过调用 来显示消息框browser.alert()

您可以alert在 Brython 控制台中运行的以下三个示例中看到实际操作,而不是在标准的 CPython 解释器 shell 中:

>>>
>>> import browser
>>> browser.alert("Real Python")

或者你可以使用window

>>>
>>> from browser import window
>>> window.alert("Real Python")

或者你可以使用this

>>>
>>> from javascript import this
>>> this().alert("Real Python")

由于Brython暴露出的新层,二者的全球性质alert()window,你可以调用alertbrowser.window,甚至上javascript.this

以下是允许访问 JavaScript 函数的主要 Brython 模块:

模块 语境 例子
browser 包含内置名称和模块 browser.alert()
browser.document 访问 DOM document.getElementById("element-id")
document["element-id"]
browser.html 创建 HTML 元素 html.H1("This is the title")
browser.window 访问Window函数和对象 window.navigator
window.frames
javascript 访问 JavaScript 中定义的对象 javascript.this()
javascript.JSON.parse()

除了浏览器中可用的 JavaScript 函数和 API,您还可以访问您编写的 JavaScript 函数。以下示例演示了如何从 Brython 访问自定义 JavaScript 函数:

 1<!doctype html>
 2<html>
 3  <head>
 4    <meta charset="utf-8">
 5    <script
 6        src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython.js">
 7    </script>
 8    <script type="text/javascript">
 9      function myMessageBox(name) {
10        window.alert(`Hello ${name}!`);
11      }
12    </script>
13  </head>
14  <body onload="brython()">
15    <script type="text/python">
16      from browser import window
17      window.myMessageBox("Jon")
18    </script>
19  </body>
20</html>

这是它的工作原理:

  • 第 9 行定义myMessageBox()了 JavaScript 块中的自定义函数。
  • 第 17 行调用myMessageBox().

您可以使用相同的功能访问 JavaScript 库。您将在Web UI 框架部分了解如何与 Vue.js(一种流行的 Web UI 框架)进行交互。

浏览器网页 API

浏览器公开了您可以从 JavaScript 访问的 Web API,而 Brython 可以访问相同的 API。在本节中,您将扩展 Base64 计算器以在浏览器页面重新加载之间存储数据。

允许此功能的Web APIWeb Storage API。它包括两种机制:

  1. sessionStorage
  2. localStorage

您将localStorage在接下来的示例中使用。

如前所述,Base64 计算器会创建一个字典,其中包含映射到此字符串的 Base64 编码值的输入字符串。加载页面后数据会保留在内存中,但会在您重新加载页面时清除。保存数据localStorage将在页面重新加载之间保留字典。这localStorage是一个键值存储

要访问localStorage,您需要导入storage. 为了贴近初步实现,你会加载和字典数据保存到localStorageJSON格式。保存和获取数据的关键是b64data. 修改后的代码包括新的导入和一个load_data()函数:

from browser.local_storage import storage
import json, base64

def load_data():
    data = storage.get("b64data")
    if data:
        return json.loads(data)
    else:
        storage["b64data"] = json.dumps({})
        return {}

load_data()在加载 Python 代码时执行。它从localStoragePython 字典中获取 JSON 数据并填充该字典,该字典将用于在页面生命周期内将数据保存在内存中。如果没有找到b64datalocalStorage,然后它创建一个空的字典键b64datalocalStorage,并返回一个空的字典。

您可以load_data()通过展开下面的框来查看完整的 Python 代码。它展示了如何使用localStorageWeb API 作为持久存储,而不是依赖于临时内存存储,就像本示例的前一个实现一样。

可访问 localStorage 的完整源代码显示隐藏

您可以从browser和其他子模块访问所有 Web API 函数。Brython 文档中提供了有关访问 Web API 的高级文档。有关更多详细信息,您可以查阅Web API 文档并使用Brython 控制台来试验 Web API。

在某些情况下,您可能需要在熟悉的 Python 函数和来自 Web API 的函数之间进行选择。例如,在上面的代码中,您使用 Python Base64 编码base64.b64encode(),但您可以使用 JavaScript 的btoa()

>>>
>>> from browser import window
>>> window.btoa("Real Python")
'UmVhbCBQeXRob24='

您可以在在线控制台中测试这两种变体。Usingwindow.btoa()仅适用于 Brython 上下文,而base64.b64encode()可以使用常规 Python 实现(如CPython )执行。请注意,在 CPython 版本中,base64.b64encode()将 abytearray作为参数类型,而 JavaScriptwindow.btoa()则采用字符串。

如果性能是一个问题,那么请考虑使用 JavaScript 版本。

网页界面框架

AngularReactVue.jsSvelte等流行的 JavaScript UI 框架已成为前端开发人员工具包的重要组成部分,Brython 与其中一些框架无缝集成。在本节中,您将使用 Vue.js 版本 3 和 Brython 构建一个应用程序。

您将构建的应用程序是一个计算字符串散列的表单。这是正在运行的 HTML 页面的屏幕截图:

布莱顿 Vue.js

bodyHTML页面中定义的绑定和模板声明:

<!DOCTYPE html >
<html>
  <head>
    <meta charset="utf-8"/>
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/pure/2.0.3/pure-min.min.css"/>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/3.0.2/vue.global.prod.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython_stdlib.min.js"></script>
    <script src="main.py" type="text/python"></script>
    <style>
      body { padding: 30px; }
      [v-cloak] { visibility: hidden; }
    </style>
  </head>

<body onload="brython(1)">
  <div id="app">
    <form class="pure-form" onsubmit="return false;">
      <fieldset>
        <legend>Hash Calculator</legend>
        <input type="text" v-model.trim="input_text"
               placeholder="Text to Encode" autocomplete="off"/>
        <select v-model="algo" v-cloak>
          <option v-for="name in algos" v-bind:value="name">

          </option>
        </select>
        <button @click="compute_hash" type="submit"
                class="pure-button pure-button-primary">Ok</button>
      </fieldset>
    </form>
    <p v-cloak></p>
  </div>
</body>

如果您不熟悉 Vue,那么您将在下面快速介绍一些内容,但请随时查阅官方文档以获取更多信息:

  • Vue.js 指令是特殊的属性值,以 为前缀v-,提供 DOM 和Vue.js 组件值之间的动态行为和数据映射:
    • v-model.trim="input_text"将输入值绑定到Vue 模型 input_text并修剪该值。
    • v-model="algo"将下拉列表的值绑定到algo.
    • v-for="name in algos"将选项值绑定到name.
  • Vue 模板用双花括号括起来的变量表示。Vue.js 将相应的占位符替换为 Vue 组件中的相应值:
    • hash_value
    • name
  • 事件处理程序由 at 符号 (@)标识,如 in@click="compute_hash"

对应的 Python 代码描述了 Vue 和附加的业务逻辑:

 1from browser import alert, window
 2from javascript import this
 3import hashlib
 4
 5hashes = {
 6    "sha-1": hashlib.sha1,
 7    "sha-256": hashlib.sha256,
 8    "sha-512": hashlib.sha512,
 9}
10
11Vue = window.Vue
12
13def compute_hash(evt):
14    value = this().input_text
15    if not value:
16        alert("You need to enter a value")
17        return
18    hash_object = hashes[this().algo]()
19    hash_object.update(value.encode())
20    hex_value = hash_object.hexdigest()
21    this().hash_value = hex_value
22
23def created():
24    for name in hashes:
25        this().algos.append(name)
26    this().algo = next(iter(hashes))
27
28app = Vue.createApp(
29    {
30        "el": "#app",
31        "created": created,
32        "data": lambda _: {"hash_value": "", "algos": [], "algo": "", "input_text": ""},
33        "methods": {"compute_hash": compute_hash},
34    }
35)
36
37app.mount("#app")

Vue.js 的声明性质显示在带有 Vue 指令和模板的 HTML 文件中。它还在 Python 代码中通过第 11 行和第 28 至 35 行的 Vue 组件声明进行了演示。这种声明性技术将 DOM 的节点值与 Vue 数据连接起来,允许框架的反应性行为。

这消除了您必须在前一个示例中编写的一些样板代码。例如,请注意,您不必从 DOM 中使用类似document["some_id"]. 创建 Vue 应用程序并调用app.mount()处理 Vue 组件到相应 DOM 元素的映射以及 JavaScript 函数的绑定。

在 Python 中,访问 Vue 对象字段需要您通过以下方式引用 Vue 对象javascript.this()

  • 第 14 行获取组件字段的值this().input_text
  • 第 21 行更新数据组件this().hash_value
  • 第 25 行向列表中添加了一个算法this().algos
  • 第 26 行this().algo使用 的第一个键进行实例化hashes{}

如果 Vue 与 Brython 的结合引起了您的兴趣,那么您可能想查看vuepy 项目,它为 Vue.js 提供完整的 Python 绑定并使用 Brython 在浏览器中运行 Python。

WebAssembly

在某些情况下,您可以使用WebAssembly来提高 Brython 甚至 JavaScript 的性能。WebAssemblyWasm是所有主要浏览器都支持的二进制代码。它可以在浏览器中提供优于 JavaScript 的性能改进,并且是CC++Rust等语言的编译目标。如果您不使用 Rust 或 Wasm,则可以跳过本节。

在以下演示使用 WebAssembly 的方法的示例中,您将在 Rust 中实现一个函数并从 Python 调用它。

这不是一个彻底的 Rust 教程。它只会划伤表面。有关 Rust 的更多详细信息,请查看Rust 文档

通过启动安装锈使用rustup。要编译 Wasm 文件,您还需要添加wasm32目标

$ rustup target add wasm32-unknown-unknown

使用cargo在 Rust 安装期间安装的项目创建一个项目:

$ cargo new --lib op

上面的命令在名为op. 在此文件夹中,您将找到Cargo.tomlRust 构建配置文件,您需要修改该文件以表明您要创建动态库。您可以通过添加突出显示的部分来做到这一点:

[package]
name = "op"
version = "0.1.0"
authors = ["John <john@example.com>"]
edition = "2018"

[lib]
crate-type=["cdylib"]

[dependencies]

src/lib.rs通过将其内容替换为以下内容进行修改:

#[no_mangle]
pub extern fn double_first_and_add(x: u32, y: u32) -> u32 {
    (2 * x) + y
}

在项目的根目录中Cargo.toml,编译你的项目:

$ cargo build --target wasm32-unknown-unknown

接下来,创建一个web包含以下内容的目录index.html

 1<!-- index.html -->
 2<!DOCTYPE html>
 3<html>
 4<head>
 5  <script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython.min.js"></script>
 6  <script src="main.py" type="text/python"></script>
 7</head>
 8<body onload="brython()">
 9
10<form class="pure-form" onsubmit="return false;">
11  <h2>Custom Operation using Wasm + Brython</h2>
12  <fieldset>
13    <legend>Multiply first number by 2 and add result to second number</legend>
14    <input type="number" value="0" id="number-1" placeholder="1st number"
15           autocomplete="off" required/>
16    <input type="number" value="0" id="number-2" placeholder="2nd number"
17           autocomplete="off" required/>
18    <button type="submit" id="submit" class="pure-button pure-button-primary">
19        Execute
20    </button>
21  </fieldset>
22</form>
23
24<br/>
25<div id="result"></div>
26</body>
27</html>

上面的第 6 行main.py从同一目录加载以下内容:

 1from browser import document, window
 2
 3double_first_and_add = None
 4
 5def add_rust_fn(module):
 6  global double_first_and_add
 7  double_first_and_add = module.instance.exports.double_first_and_add
 8
 9def add_numbers(evt):
10    nb1 = document["number-1"].value or 0
11    nb2 = document["number-2"].value or 0
12    res = double_first_and_add(nb1, nb2)
13    document["result"].innerHTML = f"Result: ({nb1} * 2) + {nb2} = {res}"
14
15document["submit"].bind("click", add_numbers)
16window.WebAssembly.instantiateStreaming(window.fetch("op.wasm")).then(add_rust_fn)

突出显示的行是允许 Brython 访问 Rust 函数的粘合剂double_first_and_add()

  • 第 16 行读取op.wasmusing WebAssembly,然后add_rust_fn()在下载 Wasm 文件时调用。
  • 第 5 行实现了add_rust_fn(),它将 Wasm 模块作为参数。
  • 第 7 行分配double_first_and_add()给本地double_first_and_add名称以使其可用于 Python。

在同一web目录中,op.wasmtarget/wasm32-unknown-unknown/debug/op.wasm以下位置复制:

$ cp target/wasm32-unknown-unknown/debug/op.wasm web

项目文件夹布局如下所示:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
├── target
│   ...
└── web
    ├── index.html
    ├── main.py
    └── op.wasm

这显示了使用cargo new. 为清楚起见,target部分省略。

现在在web以下位置启动服务器:

$ python3 -m http.server
Serving HTTP on :: port 8000 (http://[::]:8000/) ...

最后,将您的 Internet 浏览器指向http://localhost:8000. 您的浏览器应呈现如下所示的页面:

WASM 布莱顿

这个项目展示了如何创建一个可以从 JavaScript 或 Brython 使用的 WebAssembly。由于构建 Wasm 文件会产生大量开销,因此这不应是您解决特定问题的首选方法。

如果 JavaScript 不能满足您的性能要求,那么 Rust 可能是一个选择。如果您已经拥有要与之交互的 Wasm 代码(您构建的代码或现有的 Wasm 库),这将非常有用。

使用 Rust 生成 WebAssembly 的另一个可能的好处是它可以访问 Python 或 JavaScript 中不存在的库。如果您想使用用 C 语言编写并且不能与 Brython 一起使用的 Python 库,它也很有用。如果 Rust 中存在这样的库,那么您可能会考虑构建一个 Wasm 文件以将其与 Brython 一起使用。

在 Brython 中应用异步开发

同步编程是您可能最熟悉的计算行为。例如,当执行 A、B 和 C 三个语句时,程序首先执行 A,然后是 B,最后是 C。每个语句在将其传递到下一个之前都会阻塞程序的流程。

想象一种技术,A 将首先被执行,B 将被调用但不会立即执行,然后 C 将被执行。您可以将 B 视为将来被执行的承诺。因为 B 是非阻塞的,所以它被认为是异步的。有关异步编程的其他背景知识,您可以查看Python 中的异步功能入门

JavaScript 是单线程的,特别是在涉及网络通信时依赖于异步处理。例如,获取 API 的结果不需要阻止其他 JavaScript 函数的执行。

使用 Brython,您可以通过许多组件访问异步功能:

随着 JavaScript 的发展,回调已逐渐被承诺或异步函数取代。在本教程中,您将学习如何使用来自 Brython 的 promise 以及如何使用browser.ajaxbrowser.aio模块,它们利用了 JavaScript 的异步特性。

CPython 库中的asyncio模块不能在浏览器上下文中使用,在 Brython 中被替换为browser.aio.

Brython 中的 JavaScript Promise

在 JavaScript 中,promise是一个可能在未来某个时候产生结果的对象。完成后产生的值将是一个值或错误的原因。

下面的示例说明了如何使用Promise来自 Brython的 JavaScript对象。您可以在在线控制台中使用此示例:

>>>
 1>>> from browser import timer, window
 2>>> def message_in_future(success, error):
 3...   timer.set_timeout(lambda: success("Message in the future"), 3000)
 4...
 5>>> def show_message(msg):
 6...   window.alert(msg)
 7...
 8>>> window.Promise.new(message_in_future).then(show_message)
 9<Promise object>

在 Web 控制台中,您可以立即获得有关 Python 代码执行的反馈:

  • 第 1 行导入timer设置超时并window访问Promise对象。
  • 第 2 行定义了一个executormessage_in_future()当承诺成功时,在超时结束时返回一条消息。
  • 第 5 行定义了一个show_message()显示警报的函数。
  • 第 8 行创建了一个带有 executor 的 promise,用一个then块链接,允许访问 promise 的结果。

在上面的例子中,超时人为地模拟了一个长时间运行的函数。承诺的实际使用可能涉及网络调用。经过3秒,许用值成功完成"Message in the future"

如果执行程序函数message_in_future()检测到错误,则它可以error()将错误原因作为参数进行调用。您可以使用新的链式方法.catch(), 在Promise对象上实现这一点,如下所示:

>>>
>>> window.Promise.new(message_in_future).then(show_message).catch(show_message)

您可以在下图中看到成功完成承诺的行为:

这个项目展示了如何创建一个可以从 JavaScript 或 Brython 使用的 WebAssembly。由于构建 Wasm 文件会产生大量开销,因此这不应是您解决特定问题的首选方法。

如果 JavaScript 不能满足您的性能要求,那么 Rust 可能是一个选择。如果您已经拥有要与之交互的 Wasm 代码(您构建的代码或现有的 Wasm 库),这将非常有用。

使用 Rust 生成 WebAssembly 的另一个可能的好处是它可以访问 Python 或 JavaScript 中不存在的库。如果您想使用用 C 语言编写并且不能与 Brython 一起使用的 Python 库,它也很有用。如果 Rust 中存在这样的库,那么您可能会考虑构建一个 Wasm 文件以将其与 Brython 一起使用。

在 Brython 中应用异步开发

同步编程是您可能最熟悉的计算行为。例如,当执行 A、B 和 C 三个语句时,程序首先执行 A,然后是 B,最后是 C。每个语句在将其传递到下一个之前都会阻塞程序的流程。

想象一种技术,A 将首先被执行,B 将被调用但不会立即执行,然后 C 将被执行。您可以将 B 视为将来被执行的承诺。因为 B 是非阻塞的,所以它被认为是异步的。有关异步编程的其他背景知识,您可以查看Python 中的异步功能入门

JavaScript 是单线程的,特别是在涉及网络通信时依赖于异步处理。例如,获取 API 的结果不需要阻止其他 JavaScript 函数的执行。

使用 Brython,您可以通过许多组件访问异步功能:

随着 JavaScript 的发展,回调已逐渐被承诺或异步函数取代。在本教程中,您将学习如何使用来自 Brython 的 promise 以及如何使用browser.ajaxbrowser.aio模块,它们利用了 JavaScript 的异步特性。

CPython 库中的asyncio模块不能在浏览器上下文中使用,在 Brython 中被替换为browser.aio.

Brython 中的 JavaScript Promise

在 JavaScript 中,promise是一个可能在未来某个时候产生结果的对象。完成后产生的值将是一个值或错误的原因。

下面的示例说明了如何使用Promise来自 Brython的 JavaScript对象。您可以在在线控制台中使用此示例:

>>>
 1>>> from browser import timer, window
 2>>> def message_in_future(success, error):
 3...   timer.set_timeout(lambda: success("Message in the future"), 3000)
 4...
 5>>> def show_message(msg):
 6...   window.alert(msg)
 7...
 8>>> window.Promise.new(message_in_future).then(show_message)
 9<Promise object>

在 Web 控制台中,您可以立即获得有关 Python 代码执行的反馈:

  • 第 1 行导入timer设置超时并window访问Promise对象。
  • 第 2 行定义了一个executormessage_in_future()当承诺成功时,在超时结束时返回一条消息。
  • 第 5 行定义了一个show_message()显示警报的函数。
  • 第 8 行创建了一个带有 executor 的 promise,用一个then块链接,允许访问 promise 的结果。

在上面的例子中,超时人为地模拟了一个长时间运行的函数。承诺的实际使用可能涉及网络调用。经过3秒,许用值成功完成"Message in the future"

如果执行程序函数message_in_future()检测到错误,则它可以error()将错误原因作为参数进行调用。您可以使用新的链式方法.catch(), 在Promise对象上实现这一点,如下所示:

>>>
>>> window.Promise.new(message_in_future).then(show_message).catch(show_message)

您可以在下图中看到成功完成承诺的行为:

Brython 控制台中的承诺

在控制台中运行代码时,可以看到Promise先创建了对象,然后在超时后显示消息框。

Ajax in Brython

当函数被限定为I/O bound时,异步函数特别有用。这与受CPU 限制的函数形成对比。一个I / O绑定函数是大多花费时间等待输入或输出到结束,而函数CPU限制功能被计算。通过网络调用 API 或查询数据库是受 I/O 限制的执行,而计算素数序列则受 CPU 限制。

Brython的browser.ajax自曝HTTP一样的功能get()post()是,默认情况下,异步的。这些函数采用blocking可以设置为True同步呈现相同函数的参数。

要异步调用HTTPGET,请ajax.get()按如下方式调用:

ajax.get(url, oncomplete=on_complete)

要以阻塞模式获取 API,请将blocking参数设置为True

ajax.get(url, blocking=True, oncomplete=on_complete)

以下代码显示了进行阻塞 Ajax 调用和非阻塞 Ajax 调用之间的区别:

 1from browser import ajax, document
 2import javascript
 3
 4def show_text(req):
 5    if req.status == 200:
 6        log(f"Text received: '{req.text}'")
 7    else:
 8        log(f"Error: {req.status} - {req.text}")
 9
10def log(message):
11    document["log"].value += f"{message} \n"
12
13def ajax_get(evt):
14    log("Before async get")
15    ajax.get("/api.txt", oncomplete=show_text)
16    log("After async get")
17
18def ajax_get_blocking(evt):
19    log("Before blocking get")
20    try:
21        ajax.get("/api.txt", blocking=True, oncomplete=show_text)
22    except Exception as exc:
23        log(f"Error: {exc.__name__} - Did you start a local web server?")
24    else:
25        log("After blocking get")
26
27document["get-btn"].bind("click", ajax_get)
28document["get-blocking-btn"].bind("click", ajax_get_blocking)

上面的代码说明了同步和异步两种行为:

  • 第 13 行定义了ajax_get(),它使用ajax.get(). 的默认行为ajax.get()是异步的。ajax_get()返回,并show_text()分配给参数oncomplete在接收到远程文件后回调/api.txt

  • 第 18 行定义了ajax_get_blocking(),它演示了如何使用ajax.get()阻塞行为。在这种情况下,show_text()ajax_get_blocking()返回之前调用。

当您运行完整示例并单击Async GetBlocking Get 时,您将看到以下屏幕:

浏览器 Ajax

可以看到,在第一个场景中,ajax_get()是完全执行的,API 调用的结果是异步发生的。在第二种情况下,在从 返回之前显示 API 调用的结果ajax_get_blocking()

Brython 中的异步 IO

随着asyncio,Python 3.4 开始公开新的异步功能。在 Python 3.5 中,异步支持通过async/await语法得到了丰富。由于与浏览器事件循环不兼容,Brython 实现browser.aio为标准的替代品asyncio

Brython 模块browser.aio和 Python 模块asyncio都支持使用asyncandawait关键字并共享通用函数,如run()and sleep()。这两个模块都实现了其他不同的函数,这些函数属于它们各自的执行上下文、CPython 上下文环境asyncio和浏览器环境browser.aio

协程

您可以使用run()sleep()来创建协程。为了说明在 Brython 中实现的协程的行为,您将实现CPython 文档中提供的协程示例的变体:

 1from browser import aio as asyncio
 2import time
 3
 4async def say_after(delay, what):
 5    await asyncio.sleep(delay)
 6    print(what)
 7
 8async def main():
 9    print(f"started at {time.strftime('%X')}")
10
11    await say_after(1, 'hello')
12    await say_after(2, 'world')
13
14    print(f"finished at {time.strftime('%X')}")
15
16asyncio.run(main())

除了第一import行,代码与您在 CPython 文档中找到的相同。它演示了关键字asyncand的使用await和显示run()sleep()实际操作:

  • 1层线的用途asyncio为的别名browser.aio。尽管它隐藏了aio,但它使代码接近 Python 文档示例以方便比较。
  • 第 4 行声明了协程say_after()。注意使用async.
  • 第 5 行调用asyncio.sleep()withawait以便当前函数将控制权交给另一个函数直到sleep()完成。
  • 第 8 行声明了另一个协程,该协程本身将调用该协程say_after()两次。
  • 第 9 行调用run(),一个非阻塞函数,它接受一个协程——main()在这个例子中——作为参数。

请注意,在浏览器的上下文中,aio.run()利用了内部 JavaScript 事件循环。这与asyncio.run()CPython 中的相关函数不同,后者完全管理事件循环。

要执行此代码,请将其粘贴到在线 Brython 编辑器中,然后单击Run。您应该得到类似于以下屏幕截图的输出:

Brython 中的异步 IO

首先执行脚本,然后"hello"显示,最后"world"显示。

有关 Python 中协程的更多详细信息,您可以查看Python 中的异步 IO:完整演练

异步 I/O 的通用概念适用于所有采用这种模式的平台。在 JavaScript 中,事件循环本质上是环境的一部分,而在 CPython 中,这是使用asyncio.

上面的示例是有意练习,以保持代码与 Python 文档示例中显示的完全相同。使用 Brython 在浏览器中进行编码时,建议显式使用browser.aio,正如您将在下一节中看到的。

网页特定功能

要向 API 发出异步调用,如上一节所述,您可以编写如下函数:

async def process_get(url):
    req = await aio.get(url)

请注意关键字async和的使用await。该函数需要定义为async使用await. 在此函数的执行过程中,当到达对 的调用时await aio.get(url),该函数将控制权交还给主事件循环,同时等待网络调用aio.get()完成。其余的程序执行不会被阻塞。

以下是如何调用的示例process_get()

aio.run(process_get("/some_api"))

该函数aio.run()执行协程process_get()。它是非阻塞的。

一个更完整的代码示例展示了如何使用关键字asyncandawait以及如何aio.run()aio.get()是互补的:

 1from browser import aio, document
 2import javascript
 3
 4def log(message):
 5    document["log"].value += f"{message} \n"
 6
 7async def process_get(url):
 8    log("Before await aio.get")
 9    req = await aio.get(url)
10    log(f"Retrieved data: '{req.data}'")
11
12def aio_get(evt):
13    log("Before aio.run")
14    aio.run(process_get("/api.txt"))
15    log("After aio.run")
16
17document["get-btn"].bind("click", aio_get)

在 Python 3 的最新版本中,您可以使用asyncawait关键字:

  • 第 7 行定义process_get()了关键字async
  • 第 9 行aio.get()使用关键字 进行调用await。Usingawait要求用 定义封闭函数async
  • 第 14 行显示了如何使用aio.run(),它将async要调用的函数作为参数。

要运行完整示例,您需要启动 Web 服务器。您可以使用python3 -m http.server. 它在端口 8000 和默认页面上启动本地 Web 服务器index.html

浏览器一体机

屏幕截图显示了单击Async Get后执行的步骤序列。使用aio模块和关键字的组合,asyncawait展示了如何接受 JavaScript 推广的异步编程模型。

分发和打包 Brython 项目

您用于安装 Brython 的方法可能会影响您部署 Brython 项目的方式和位置。特别是,要部署到 PyPI,最好的选择是首先从 PyPI 安装 Brython,然后使用brython-cli. 但是到私有服务器或云提供商的典型 Web 部署可以利用您选择的任何安装方法。

您有几个部署选项:

  • 手动和自动部署
  • 部署到 PyPI
  • 部署到 CDN

您将在以下部分中探索其中的每一个。

手动和自动 Web 部署

您的应用程序包含网站所需的所有静态依赖项、CSS、JavaScript、Python 和图像文件。Brython 是 JavaScript 文件的一部分。所有文件都可以按原样部署在您选择的提供商上。您可以查阅Web 开发教程使用 Fabric 和 Ansible 自动化 Django 部署,了解有关部署 Brython 应用程序的详细信息。

如果你决定使用brython-cli --modules预编译Python代码,然后将文件部署不会有任何Python源代码,只brython.jsbrython_modules.js。您也不会包含,brython_stdlib.js因为所需的模块brython_modules.js已经包含在其中。

部署到 PyPI

当你从 PyPI 安装 Brython 时,你可以brython-cli用来创建一个可以部署到 PyPI 的包。创建这样一个包的目标是扩展默认的 Brython 模板作为自定义项目的基础,并使 Brython 网站可以从 PyPI 中使用。

按照从 PyPI 安装部分中的说明进行操作后,在新web项目中执行以下命令:

$ brython-cli --make_dist

系统会提示您回答几个旨在创建 的问题brython_setup.json,您可以稍后修改这些问题。完成命令后,您将拥有一个名为的目录,__dist__其中包含创建可安装包所需的文件。

您可以在本地测试这个新包的安装,如下所示:

$ pip install -e __dist__

随后,您还可以web通过执行以下命令来确认新命令是否与包一起部署在本地:

$ python -m web --help
usage: web.py [-h] [--install]

optional arguments:
  -h, --help  show this help message and exit
  --install   Install web in an empty directory

请注意,该web命令的行为与 Brython 在初始安装后的行为完全相同。您刚刚创建了一个可部署到 PyPI 的自定义可安装 Brython 包。有关如何将包部署到 PyPI 的详细说明,请查看如何将开源 Python 包发布到 PyPI

部署到 PyPI 后,您可以pipPython 虚拟环境中安装 Brython 包。您将能够使用您创建的新命令创建新的自定义应用程序:

$ python -m <application_name> --install

总而言之,以下是部署到 PyPI 的步骤:

  1. 从 PyPI 安装 Brython。
  2. 使用brython-cli --install.
  3. 使用brython-cli --make-dist.
  4. 将此包部署到 PyPI。

其他安装方法(CDN、GitHub 和 npm)不包括在内brython-cli,因此不太适合准备 PyPI 包。

部署到 CDN

就像CDN 服务器上可用的brython.jsbrython_stdlibs.js一样,您还可以将静态资产、图像、样式和 JavaScript 文件(包括 Python 文件或 )部署brython_modules.js到 CDN。CDN 的示例包括:

如果您的应用程序是开源的,那么您可以获得免费的 CDN 支持。示例包括CDNJSjsDelivr

创建 Google Chrome 扩展

Chrome 扩展程序是使用网络技术构建的组件,并集成到 Chrome 中以自定义您的浏览环境。通常,这些扩展程序的图标显示在 Chrome 窗口的顶部,地址栏的右侧。

公共扩展程序可从Chrome 网上应用店获得。要学习,您将从本地文件安装 Google Chrome 扩展程序:

浏览器扩展图标

在 Brython 中实现 Google Chrome 扩展之前,您将首先实现一个 JavaScript 版本,然后将其转换为 Brython。

JS 中的 Hello World 扩展

作为初学者,您将实现一个将执行以下操作的扩展:

  1. 单击扩展程序图标时打开一个弹出窗口
  2. 单击弹出窗口按钮时打开提示消息
  3. 在初始弹出窗口的底部附加您输入的消息

以下屏幕截图说明了此行为:

JS Chrome 扩展

在一个空文件夹中,创建文件manifest.json来配置扩展:

 1// manifest.json
 2{
 3    "name": "JS Hello World",
 4    "version": "1.0",
 5    "description": "Hello World Chrome Extension in JavaScript",
 6    "manifest_version": 2,
 7    "browser_action": {
 8        "default_popup": "popup.html"
 9    },
10    "permissions": ["declarativeContent", "storage", "activeTab"]
11}

此示例的重要字段是默认弹出文件 ,popup.html您还必须创建该文件。有关其他字段的信息以及更多信息,您可以查阅清单文件格式文档

在同一文件夹中,创建popup.html用于定义扩展用户界面的文件:

 1<!-- popup.html -->
 2<!DOCTYPE html>
 3<html>
 4  <head>
 5    <script src="popup.js" defer></script>
 6  </head>
 7  <body>
 8    <button id="hello-btn">Hello JS</button>
 9    <div id="hello"></div>
10  </body>
11</html>

HTML 文件包含指向扩展的 JavaScript 业务逻辑的链接,并描述了其用户界面:

  • 第 5 行引用popup.js,其中包含扩展的逻辑。
  • 第 8 行定义了button将绑定到 中的处理程序的popup.js
  • 第 9 行声明了一个字段,JavaScript 代码将使用该字段来显示一些文本。

您还需要创建popup.js

 1// popup.js
 2'use strict';
 3
 4let helloButton = document.getElementById("hello-btn");
 5
 6helloButton.onclick = function (element) {
 7  const defaultName = "Real JavaScript";
 8  let name = prompt("Enter your name:", defaultName);
 9  if (!name) {
10    name = defaultName;
11  }
12  document.getElementById("hello").innerHTML = `Hello, ${name}!`;
13};

JavaScript 代码的主要逻辑包括声明一个onclick绑定到hello-btnHTML 容器字段的处理程序:

  • 第 2 行调用脚本模式,在 JavaScript 中启用更严格的验证以揭示 JavaScript 错误。
  • 第 4 行选择由hello-btnin标识的字段popup.html并将其分配给一个变量。
  • 第 6 行定义了将在用户单击按钮时处理事件的处理程序。此事件处理程序提示用户输入他们的姓名,然后将<div>标识的内容更改hello为提供的名称。

在安装此扩展之前,请执行以下步骤:

  1. 打开屏幕右侧的 Google Chrome 菜单。
  2. 打开子菜单更多工具
  3. 单击扩展程序

屏幕将显示您当前安装的扩展程序(如果有)。它可能看起来像这样:

Chrome 扩展程序

要安装新的扩展程序,您需要执行以下步骤:

  1. 确保在屏幕的右上角启用了开发人员模式。
  2. 单击加载解包
  3. 选择包含您刚刚创建的所有文件的文件夹。

如果安装过程中没有发生错误,那么您现在应该会在浏览器地址栏的右侧看到一个带有J的新图标。要测试您的扩展程序,请单击下面显示的工具栏的J图标:

工具栏中的 J 图标

如果在安装或执行过程中出现任何错误,那么您应该会在扩展卡的Remove按钮右侧看到一个红色的错误按钮:

扩展错误

您可以单击错误以显示错误并确定根本原因。更正后,通过单击扩展卡右下角的圆形箭头重新加载扩展,然后重复该过程,直到它按预期工作。

要测试新安装的扩展程序,您可以单击浏览器工具栏右侧显示的J图标。如果图标未显示,则单击扩展以列出已安装的扩展并选择与您刚安装的 JS Hello World 扩展对齐的图钉按钮。

Python 中的 Hello World 扩展

如果您已经到了这一步,那么您已经完成了最困难的步骤,主要是为了熟悉创建和安装 Chrome 扩展程序的过程。这些步骤与 Brython 类似,但您将在本节中了解到一些不同之处。

清单文件将是不同的,具有不同的扩展名,并且为了更好的衡量,不同的描述:

 1// manifest.json
 2{
 3    "name": "Py Hello World",
 4    "version": "1.0",
 5    "description": "Hello World Chrome Extension in Python",
 6    "manifest_version": 2,
 7    "browser_action": {
 8        "default_popup": "popup.html"
 9    },
10    "content_security_policy": "script-src 'self' 'unsafe-eval';object-src 'self'",
11    "permissions": ["declarativeContent", "storage", "activeTab"]
12}

请注意,您还必须包含一个新属性content_security_policy. 这是必要的,以便可以在 chrome 扩展系统中放宽反对政策eval()。请记住,Brython 使用eval().

这不是您引入的,也不是您可以在 Brython 中控制的。eval()如果您想使用 Brython 作为浏览器扩展程序的语言,则需要启用 using 。如果不添加unsafe-evalcontent_security_policy,则会看到以下错误:

Uncaught EvalError: Refused to evaluate a string as JavaScript because
'unsafe-eval' is not an allowed source of script in the following Content
Security Policy directive: "script-src 'self' blob: filesystem:".

HTML 文件也将有一些更新,如下所示:

 1<!-- popup.html -->
 2<!DOCTYPE html>
 3<html>
 4  <head>
 5    <script src="brython.min.js" defer></script>
 6    <script src="init_brython.js" defer></script>
 7    <script src="popup.py" type="text/python" defer></script>
 8  </head>
 9  <body>
10    <button id="hello-btn">Hello Py</button>
11    <div id="hello"></div>
12  </body>
13</html>

HTML 代码与您用于在 JavaScript 中创建 Chrome 扩展程序的代码非常相似。有几个细节值得注意:

  • 第 5 行brython.min.js从本地包加载。出于安全原因,加载本地脚本,您无法从 CDN 等外部源加载。
  • 第 6 行加载init_brython.js,它调用brython().
  • 7号线负载popup.py
  • 第 9 行声明body没有通常的onload="brython()".

另一个安全约束会阻止您brython()onload发生body标记时调用。解决方法是在文档中添加一个监听brython(),并在文档内容加载后指示浏览器执行:

// init_brython.js
document.addEventListener('DOMContentLoaded', function () {
    brython();
});

最后,您可以在以下 Python 代码中看到此应用程序的主要逻辑:

# popup.py
from browser import document, prompt

def hello(evt):
    default = "Real Python"
    name = prompt("Enter your name:", default)
    if not name:
        name = default
    document["hello"].innerHTML = f"Hello, {name}!"

document["hello-btn"].bind("click", hello)

这样,您就可以像处理 JavaScript chrome 扩展一样继续安装和测试了。

测试和调试 Brython

目前没有方便的库来单元测试 Brython 代码。随着 Brython 的发展,您将看到更多用于在浏览器中测试和调试 Python 代码的选项。可以将 Python 单元测试框架用于可在浏览器外部使用的独立 Python 模块。在浏览器中,带有浏览器驱动程序的 Selenium 是一个不错的选择。调试也是有限的,但也是可能的。

Python 单元测试

Python 单元测试框架,如内置的unittestpytest,在浏览器中不起作用。您可以将这些框架用于 Python 模块,这些模块也可以在 CPython 的上下文中执行。任何 Brython 特定的模块browser都无法在命令行中使用此类工具进行测试。有关 Python 单元测试的更多信息,请查看 Python 测试入门

Selenium

Selenium是一个自动化浏览器的框架。它与浏览器中使用的语言无关,无论是 JavaScript、Elm、Wasm 还是 Brython,因为它使用WebDriver概念来表现得像用户与浏览器交互。您可以查看使用 Python 和 Selenium进行现代 Web 自动化以获取有关此框架的更多信息。

JavaScript 单元测试

有许多专注于 JavaScript 的测试框架,如MochaJasmineQUnit,在整个 JavaScript 生态系统中表现良好。但它们不一定非常适合对浏览器中运行的 Python 代码进行单元测试。一种选择需要将 Brython 函数全局公开给 JavaScript,这与最佳实践背道而驰。

为了说明将 Brython 函数公开给 JavaScript 的选项,您将使用QUnit,这是一个 JavaScript 单元测试套件,可以在 HTML 文件中独立运行:

 1<!-- index.html -->
 2<!DOCTYPE html >
 3<html>
 4
 5<head>
 6  <meta charset="utf-8">
 7  <meta name="viewport" content="width=device-width">
 8  <title>Test Suite</title>
 9  <link rel="stylesheet" href="https://code.jquery.com/qunit/qunit-2.13.0.css">
10  <script src="https://cdnjs.cloudflare.com/ajax/libs/brython/3.9.0/brython.min.js"></script>
11  <script src="https://code.jquery.com/qunit/qunit-2.13.0.js"></script>
12</head>
13
14<body onload="brython()">
15<div id="qunit"></div>
16<div id="qunit-fixture"></div>
17<script type="text/python">
18from browser import window
19
20def python_add(a, b):
21  return a + b
22
23window.py_add = python_add
24</script>
25
26<script>
27const js_add = (a, b) => a + b;
28QUnit.module('js_add_test', function() {
29  QUnit.test('should add two numbers', function(assert) {
30    assert.equal(js_add(1, 1), 2, '1 + 1 = 2 (javascript');
31  });
32});
33
34QUnit.module('py_add_test', function() {
35  QUnit.test('should add two numbers in Brython', function(assert) {
36    assert.equal(py_add(2, 3), 5, '2 + 3 = 5 (python)');
37  });
38});
39
40QUnit.module('py_add_failed_test', function() {
41  QUnit.test('should add two numbers in Brython (failure)', function(assert) {
42    assert.equal(py_add(2, 3), 6, '2 + 3 != 6 (python)');
43  });
44});
45</script>
46
47</body>
48</html>

在一个 HTML 文件中,您编写了 Python 代码、JavaScript 代码和 JavaScript 测试来验证在浏览器中执行的两种语言的函数:

  • 第 11 行导入 QUnit 框架。
  • 第 23 行暴露python_add()给 JavaScript。
  • 第 28 行定义js_add_test测试 JavaScript 函数js_add()
  • 第 34 行定义py_add_test了测试 Python 函数python_add()
  • 第 40 行定义py_add_failed_test了测试 Python 函数python_add()的错误。

您不需要启动 Web 服务器来执行单元测试。index.html在浏览器中打开,您应该会看到以下内容:

QUnit HTML 测试套件

该页面显示了两个成功的测试,js_add_test()py_add_test(),一个失败的测试,py_add_failed_test()

将 Python 函数暴露给 JavaScript 展示了如何使用 JavaScript 单元测试框架在浏览器中执行 Python。虽然可以进行测试,但一般不建议这样做,因为它可能与现有的 JavaScript 名称冲突。

在 Brython 中调试

在撰写本文时,还没有用户友好的工具来调试您的 Brython 应用程序。您无法生成允许您在浏览器开发工具中逐步调试的源映射文件。

这不应该阻止您使用 Brython。以下是一些有助于调试和排除 Brython 代码故障的提示:

  • 使用print()browser.console.log()在浏览器的开发人员工具控制台中打印变量值。
  • 使用Python 3.8中的酷新功能中所述的f-string 调试
  • 偶尔使用开发者工具清除浏览器的 IndexedDB
  • 通过选中浏览器开发人员工具的网络选项卡中的禁用缓存复选框,在开发过程中禁用浏览器缓存
  • 添加选项brython()启用要在 JavaScript 控制台中显示的其他调试信息。
  • 复制brython.jsbrython_stdlib.min.js本地复制以加快开发过程中的重新加载。
  • 在您import编写 Python 代码时启动本地服务器
  • Chrome 扩展程序进行故障排除时,从扩展程序打开检查器。

Python 的优点之一是REPL(读取-评估-打印循环)。在线 Brython 控制台提供了一个平台来试验、测试和调试一些代码片段的行为。

探索 Brython 的替代品

Brython 并不是在浏览器中编写 Python 代码的唯一选择。有一些替代方案可用:

每个实现都从不同的角度解决问题。Brython 试图通过提供对与 JavaScript 相同的 Web API 和 DOM 操作的访问来替代 JavaScript,但具有 Python 语法和习语的吸引力。与某些可能具有不同目标的替代方案相比,它被打包为一个小下载。

这些框架如何比较?

Skulpt

雕塑在浏览器Python 代码编译为 JavaScript。编译发生在页面加载后,而在 Brython 中,编译发生在页面加载期间。

虽然它没有内置函数来操作 DOM,但 Skulpt 在其应用程序上非常接近 Brython。这包括教育用途和成熟的 Python 应用程序,如Anvil 所示

Skulpt 是一个朝着 Python 3 发展的维护项目。 Brython 在与 CPython 3.9 的模块上与浏览器中的执行兼容。

转密

Transcrypt包含一个命令行工具,用于将 Python 代码编译为 JavaScript 代码。编译据说是提前(AOT)。然后可以将生成的代码加载到浏览器中。Transcrypt 占用空间小,大约 100KB。它速度快并且支持 DOM 操作。

Skulpt 和 Brython 的区别在于,Transcrypt 在下载并在浏览器中使用之前,先使用 Transcrypt 编译器编译为 JavaScript。这实现了速度和小尺寸。但是,它阻止了 Transcrypt 像其他平台一样被用作教育平台。

Pyodide

Pyodide是 CPython 解释器的 WebAssembly 编译。它在浏览器中解释 Python 代码。没有 JavaScript 编译阶段。尽管 Pyodide 与 PyPy.js 一样,需要您下载大量数据,但它加载了NumPyPandasMatplotlib等科学库。

您可以将 Pyodide 视为完全在浏览器中运行的Jupyter Notebook环境,而不是由后端服务器提供服务。您可以使用一个活生生的例子来试验 Pyodide 。

pypy.js

PyPy.js 使用通过emscripten编译为 JavaScript的PyPy Python 解释器,使其兼容在浏览器中运行。

除了项目目前处于休眠状态之外,PyPy.js 是一个大包,大约 10 MB,对于典型的 Web 应用程序来说是望而却步。通过打开PyPy.js 主页,您仍然可以使用 PyPy.js 作为在浏览器中学习 Python 的平台。

PyPy.js 使用 emscripten 编译为 JavaScript。Pyodide 更进一步,特别是利用 emscripten 和 Wasm 将 Python C 扩展(如NumPy)编译为 WebAssembly。

在撰写本文时,PyPy.js 似乎没有得到维护。对于与编译过程相同的内容,请考虑 Pyodide。

结论

在本教程中,您深入了解了在浏览器中编写 Python 代码的几个方面。这可能会让您对尝试使用 Python 进行前端开发产生一些兴趣。

在本教程中,您学习了如何:

  • 在本地环境中安装和使用 Brython
  • 在前端 Web 应用程序中用 Python 替换 JavaScript
  • 操作DOM
  • JavaScript交互
  • 创建浏览器扩展
  • 比较Brython 的替代品

除了访问通常为 JavaScript 保留的功能之外,Brython 的最佳用途之一是作为学习和教学工具。现在,您可以访问在浏览器中运行的 Python编辑器控制台,开始探索 Brython 的多种用途。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。