高级特性

本部分介绍PyWebIO的高级特性。

使用start_server()启动多应用

start_server() 接收一个函数作为PyWebIO应用,另外, start_server() 还支持传入函数列表或字典,从而启动多个PyWebIO应用,应用之间可以通过 go_app()put_link() 进行跳转:

def task_1():
    put_text('task_1')
    put_buttons(['Go task 2'], [lambda: go_app('task_2')])

def task_2():
    put_text('task_2')
    put_buttons(['Go task 1'], [lambda: go_app('task_1')])

def index():
    put_link('Go task 1', app='task_1')  # Use `app` parameter to specify the task name
    put_link('Go task 2', app='task_2')

# equal to `start_server({'index': index, 'task_1': task_1, 'task_2': task_2})`
start_server([index, task_1, task_2])

start_server() 的第一个参数的类型为字典时,字典键为应用名,类型为列表时,函数名为应用名。

可以通过 app URL参数选择要访问的应用(例如使用 http://host:port/?app=foo 来访问 foo 应用), 为提供了 app URL参数时默认使用运行 index 应用,当 index 应用不存在时,PyWebIO会提供一个默认的索引页作为 index 应用。

与Web框架整合

可以将PyWebIO应用集成到现有的Python Web项目中,PyWebIO应用与Web项目共用一个Web框架。目前支持与Flask、Tornado、Django、aiohttp和FastAPI(Starlette) Web框架的集成。

不同Web框架的集成方法如下:

使用 pywebio.platform.tornado.webio_handler() 来获取在Tornado中运行PyWebIO应用的 WebSocketHandler 类:

import tornado.ioloop
import tornado.web
from pywebio.platform.tornado import webio_handler

class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")

if __name__ == "__main__":
    application = tornado.web.Application([
        (r"/", MainHandler),
        (r"/tool", webio_handler(task_func)),  # `task_func` is PyWebIO task function
    ])
    application.listen(port=80, address='localhost')
    tornado.ioloop.IOLoop.current().start()

以上代码将 PyWebIO 应用的 WebSocketHandler 绑定到了 /tool 路径下。 启动Tornado后,访问 http://localhost/tool 即可打开PyWebIO应用。

注意

当使用Tornado后端时,PyWebIO使用WebSocket协议和浏览器进行通讯,如果你的Tornado应用处在反向代理(比如Nginx)之后,可能需要特别配置反向代理来支持WebSocket协议,这里 有一个Nginx配置WebSocket的例子。

Notes

生产环境部署

在生产环境中,你可能会使用一些 WSGI/ASGI 服务器(如 uWSGI、Gunicorn、Uvicorn)部署 Web 应用程序。由于 PyWebIO 应用程序会在进程中存储会话状态,当使用基于 HTTP 的会话(使用Flask 和 Django后端时)并生成多个进程来处理请求时,请求可能会被分发到错误的进程中。因此,在使用基于 HTTP 的会话时,只能启动一个进程来处理请求。

如果仍然希望使用多进程来提高并发,一种方式是使用 Uvicorn+FastAPI,或者你也可以启动多个Tornado/aiohttp进程,并在它们之前添加外部的负载均衡软件(如 HAProxy 或 nginx)。这些后端使用 WebSocket 协议与浏览器进行通信,所以不存在上述问题。

PyWebIO静态资源的托管

PyWebIO默认使用CDN来获取前端的静态资源,如果要将PyWebIO应用部署到离线环境中,需要自行托管静态文件, 并将 webio_view()webio_handler()cdn 参数设置为 False

cdn=False 时需要将静态资源托管在和PyWebIO应用同级的目录下。 同时,也可以通过 cdn 参数直接设置PyWebIO静态资源的URL目录。

PyWebIO的静态文件的路径保存在 pywebio.STATIC_PATH 中,可使用命令 python3 -c "import pywebio; print(pywebio.STATIC_PATH)" 将其打印出来。

备注

使用 start_server() 启动的应用,如果将 cdn 参数设置为 False ,会自动启动一个本地的静态资源托管服务,无需手动托管。

基于协程的会话

在大部分情况下,你并不需要使用基于协程的会话。PyWebIO中所有仅用于协程会话的函数或方法都在文档中均有特别说明。

PyWebIO的会话实现默认是基于线程的,用户每打开一个和服务端的会话连接,PyWebIO会启动一个线程来运行任务函数。 除了基于线程的会话,PyWebIO还提供了基于协程的会话。基于协程的会话接受协程函数作为任务函数。

基于协程的会话为单线程模型,所有会话都运行在一个线程内。对于IO密集型的任务,协程比线程占用更少的资源同时又拥有媲美于线程的性能。 另外,协程的上下文切换具有可预测性,能够减少程序同步与加锁的需要,可以有效避免大多数临界区问题。

使用协程会话

要使用基于协程的会话,需要使用 async 关键字将任务函数声明为协程函数,并使用 await 语法调用PyWebIO输入函数:

 from pywebio.input import *
 from pywebio.output import *
 from pywebio import start_server

 async def say_hello():
     name = await input("what's your name?")
     put_text('Hello, %s' % name)

 start_server(say_hello, auto_open_webbrowser=True)

在协程任务函数中,也可以使用 await 调用其他协程或标准库 asyncio 中的可等待对象( awaitable objects ):

 import asyncio
 from pywebio import start_server

 async def hello_word():
     put_text('Hello ...')
     await asyncio.sleep(1)  # await awaitable objects in asyncio
     put_text('... World!')

 async def main():
     await hello_word()  # await coroutine
     put_text('Bye, bye')

 start_server(main, auto_open_webbrowser=True)

注意

在基于协程的会话中, pywebio.input 模块中的定义输入函数都需要使用 await 语法来获取返回值,忘记使用 await 将会是在使用基于协程的会话时常出现的错误。

其他在协程会话中也需要使用 await 语法来进行调用函数有:

警告

虽然PyWebIO的协程会话兼容标准库 asyncio 中的 awaitable objects ,但 asyncio 库不兼容PyWebIO协程会话中的 awaitable objects .

也就是说,无法将PyWebIO中的 awaitable objects 传入 asyncio 中的接受 awaitable objects 作为参数的函数中,比如如下调用是 不被支持的

await asyncio.shield(pywebio.input())
await asyncio.gather(asyncio.sleep(1), pywebio.session.eval_js('1+1'))
task = asyncio.create_task(pywebio.input())

协程会话的并发

在基于协程的会话中,你可以启动线程,但是无法在其中调用PyWebIO交互函数( register_thread() 在协程会话中不可用)。 但你可以使用 run_async(coro) 来异步执行一个协程对象,新协程内可以使用PyWebIO交互函数:

 from pywebio import start_server
 from pywebio.session import run_async

 async def counter(n):
     for i in range(n):
         put_text(i)
         await asyncio.sleep(1)

 async def main():
     run_async(counter(10))
     put_text('Main coroutine function exited.')


 start_server(main, auto_open_webbrowser=True)

run_async(coro) 返回一个 TaskHandler ,通过该 TaskHandler 可以查询协程运行状态和关闭协程。

会话的结束

和基于线程的会话一样,当用户关闭浏览器页面后,会话也随之关闭。

浏览器页面关闭后,当前会话内还未返回的PyWebIO输入函数调用将抛出 SessionClosedException 异常,之后对于PyWebIO交互函数的调用将会产生 SessionNotFoundExceptionSessionClosedException 异常。

协程会话也同样支持使用 defer_call(func) 来设置会话结束时需要调用的函数。

协程会话与Web框架集成

基于协程的会话同样可以与Web框架进行集成,只需要在原来传入任务函数的地方改为传入协程函数即可。

但当前在使用基于协程的会话集成进Flask或Django时,存在一些限制:

一是协程函数内还无法直接通过 await 直接等待asyncio库中的协程对象,目前需要使用 run_asyncio_coroutine() 进行包装。

二是,在启动Flask/Django这类基于线程的服务器之前需要启动一个单独的线程来运行事件循环。

使用基于协程的会话集成进Flask的示例:

 import asyncio
 import threading
 from flask import Flask, send_from_directory
 from pywebio import STATIC_PATH
 from pywebio.output import *
 from pywebio.platform.flask import webio_view
 from pywebio.platform import run_event_loop
 from pywebio.session import run_asyncio_coroutine

 async def hello_word():
     put_text('Hello ...')
     await run_asyncio_coroutine(asyncio.sleep(1))  # can't just "await asyncio.sleep(1)"
     put_text('... World!')

 app = Flask(__name__)
 app.add_url_rule('/hello', 'webio_view', webio_view(hello_word),
                             methods=['GET', 'POST', 'OPTIONS'])

 # thread to run event loop
 threading.Thread(target=run_event_loop, daemon=True).start()
 app.run(host='localhost', port=80)

最后,使用PyWebIO编写的协程函数不支持Script模式,总是需要使用 start_server 来启动一个服务或者集成进Web框架来调用。