User’s guide¶
如果你接触过Web开发,你可能对接下来描述的PyWebIO的用法感到不太习惯,不同于传统Web开发的后端实现接口、前端进行展示交互的模式,在PyWebIO中,所有的逻辑都通过编写Python代码实现。
你可以按照编写控制台程序的逻辑编写PyWebIO应用,只不过这里的终端变成了浏览器。通过PyWebIO提供的命令式API,
你可以简单地调用 put_text
、 put_image
、 put_table
等函数输出文本、图片、表格等内容到浏览器,也可以调用 input
、 select
、
file_upload
等函数在浏览器上显示不同表单来接收用户的输入。此外PyWebIO中还提供了点击事件、布局等支持,让你可以使用最少的代码完成与用户的交互,
并尽可能提供良好的用户体验。
本篇使用指南从几个方面对PyWebIO的使用进行介绍,覆盖了PyWebIO的绝大部分特性。本文档中大部分示例代码的右上方都有一个Demo链接,点击后可以在线预览代码的运行效果。
输入¶
输入函数都定义在 pywebio.input 模块中,可以使用 from pywebio.input import *
引入。
调用输入函数会在浏览器上弹出一个输入表单来获取输入。PyWebIO的输入函数是阻塞式的(和Python内置的 input
一样),在表单被成功提交之前,输入函数不会返回。
基本输入¶
首先是一些基本类型的输入。
文本输入:
age = input("How old are you?", type=NUMBER)
这样一行代码的效果为:浏览器会弹出一个文本输入框来获取输入,在用户完成输入将表单提交后,函数返回用户输入的值。
下面是一些其他类型的输入函数:
# Password input
password = input("Input password", type=PASSWORD)
# Drop-down selection
gift = select('Which gift you want?', ['keyboard', 'ipad'])
# Checkbox
agree = checkbox("User Term", options=['I agree to terms and conditions'])
# Single choice
answer = radio("Choose one", options=['A', 'B', 'C', 'D'])
# Multi-line text input
text = textarea('Text Area', rows=3, placeholder='Some text')
# File Upload
img = file_upload("Select a image:", accept="image/*")
输入选项¶
输入函数可指定的参数非常丰富(全部参数及含义请见 函数文档 ):
input('This is label', type=TEXT, placeholder='This is placeholder',
help_text='This is help text', required=True)
以上代码将在浏览器上显示如下:
我们可以为输入指定校验函数,校验函数应在校验通过时返回None,否则返回错误消息:
def check_age(p): # return None when the check passes, otherwise return the error message
if p < 10:
return 'Too young!!'
if p > 60:
return 'Too old!!'
age = input("How old are you?", type=NUMBER, validate=check_age)
当用户输入了不合法的值时,页面上的显示如下:
pywebio.input.textarea()
还支持使用 Codemirror 实现代码风格的编辑区,只需使用 code
参数传入Codemirror支持的选项即可(最简单的情况是直接传入 code={}
或 code=True
):
code = textarea('Code Edit', code={
'mode': "python", # code language
'theme': 'darcula', # Codemirror theme. Visit https://codemirror.net/demo/theme.html#cobalt to get more themes
}, value='import something\n# Write your python code')
以上代码将在浏览器上显示如下:
这里 列举了一些常用的Codemirror选项,完整的Codemirror选项请见:https://codemirror.net/doc/manual.html#config
输入组¶
PyWebIO支持输入组, 返回结果为一个字典。pywebio.input.input_group()
接受单项输入组成的列表作为参数, 返回以单项输入中的 name
作为键、以输入数据为值的字典:
data = input_group("Basic info",[
input('Input your name', name='name'),
input('Input your age', name='age', type=NUMBER, validate=check_age)
])
put_text(data['name'], data['age'])
输入组中同样支持使用 validate
参数设置校验函数,其接受整个表单数据作为参数:
def check_form(data): # input group validation: return (input name, error msg) when validation fail
if len(data['name']) > 6:
return ('name', 'Name too long!')
if data['age'] <= 0:
return ('age', 'Age can not be negative!')
注意
PyWebIO 根据是否在输入函数中传入 name
参数来判断输入函数是在 input_group
中还是被单独调用。所以当单独调用一个输入函数时, 不要 设置 name
参数;而在 input_group
中调用输入函数时,需 务必提供 name
参数。
输出¶
输出函数都定义在 pywebio.output 模块中,可以使用 from pywebio.output import *
引入。
调用输出函数后,内容会实时输出到浏览器,在应用的生命周期内,可以在任意时刻调用输出函数。
基本输出¶
PyWebIO提供了一系列函数来输出表格、链接等格式:
# Text Output
put_text("Hello world!")
# Table Output
put_table([
['Commodity', 'Price'],
['Apple', '5.5'],
['Banana', '7'],
])
# Markdown Output
put_markdown('~~Strikethrough~~')
# File Output
put_file('hello_word.txt', b'hello word!')
# PopUp Output
popup('popup title', 'popup text content')
PyWebIO提供的全部输出函数见 pywebio.output 模块。另外,PyWebIO还支持一些第三方库来进行数据可视化,参见 第三方库生态 。
注解
If you use PyWebIO in interactive execution environment of Python shell, IPython or jupyter notebook,
you need call show()
method explicitly to show output:
>>> put_text("Hello world!").show()
>>> put_table([
... ['A', 'B'],
... [put_markdown(...), put_text('C')]
... ]).show()
组合输出¶
函数名以 put_
开始的输出函数,可以与一些输出函数组合使用,作为最终输出的一部分:
put_table()
支持以 put_xxx()
调用作为单元格内容:
put_table([
['Type', 'Content'],
['html', put_html('X<sup>2</sup>')],
['text', '<hr/>'], # equal to ['text', put_text('<hr/>')]
['buttons', put_buttons(['A', 'B'], onclick=...)],
['markdown', put_markdown('`Awesome PyWebIO!`')],
['file', put_file('hello.text', b'hello world')],
['table', put_table([['A', 'B'], ['C', 'D']])]
])
以上代码将在浏览器上显示如下:
类似地, popup()
也可以将 put_xxx()
调用作为弹窗内容:
popup('Popup title', [
put_html('<h3>Popup Content</h3>'),
'plain html: <br/>', # Equivalent to: put_text('plain html: <br/>')
put_table([['A', 'B'], ['C', 'D']]),
put_buttons(['close_popup()'], onclick=lambda _: close_popup())
])
另外,你可以使用 put_widget()
来创建可以接受 put_xxx()
的自定义输出控件。
接受 put_xxx()
调用作为参数的完整输出函数清单请见 输出函数列表
占位符
使用组合输出时,如果想在内容输出后,对其中的 put_xxx()
子项进行动态修改,可以使用 output()
函数,
output()
就像一个占位符,它可以像 put_xxx()
一样传入 put_table
、 popup
、 put_widget
等函数中作为输出的一部分,
并且,在输出后,还可以对其中的内容进行修改(比如重置或增加内容):
hobby = output('Coding') # equal to output(put_text('Coding'))
put_table([
['Name', 'Hobbies'],
['Wang', hobby] # hobby is initialized to Coding
])
hobby.reset('Movie') # hobby is reset to Movie
hobby.append('Music', put_text('Drama')) # append Music, Drama to hobby
hobby.insert(0, put_markdown('**Coding**')) # insert the Coding into the top of the hobby
上下文管理器
一些接受 put_xxx()
调用作为参数的输出函数支持作为上下文管理器来使用:
with put_collapse('This is title'):
for i in range(4):
put_text(i)
put_table([
['Commodity', 'Price'],
['Apple', '5.5'],
['Banana', '7'],
])
支持上下文管理器的完整函数清单请见 输出函数列表
事件回调¶
从上面可以看出,PyWebIO把交互分成了输入和输出两部分:输入函数为阻塞式调用,会在用户浏览器上显示一个表单,在用户提交表单之前输入函数将不会返回;输出函数将内容实时输出至浏览器。这种交互方式和控制台程序是一致的,因此PyWebIO应用非常适合使用控制台程序的编写逻辑来进行开发。
此外,PyWebIO还支持事件回调:PyWebIO允许你输出一些控件并绑定回调函数,当控件被点击时相应的回调函数便会被执行。
下面是一个例子:
from functools import partial
def edit_row(choice, row):
put_text("You click %s button ar row %s" % (choice, row))
put_table([
['Idx', 'Actions'],
[1, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=1))],
[2, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=2))],
[3, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=3))],
])
put_table()
的调用不会阻塞。当用户点击了某行中的按钮时,PyWebIO会自动调用相应的回调函数:
当然,PyWebIO还支持单独的按钮控件:
def btn_click(btn_val):
put_text("You click %s button" % btn_val)
put_buttons(['A', 'B', 'C'], onclick=btn_click)
注解
在PyWebIO会话(关于会话的概念见下文 Server与script模式 )结束后,事件回调也将不起作用,你可以在任务函数末尾处使用 pywebio.session.hold()
函数来将会话保持,这样在用户关闭浏览器页面前,事件回调将一直可用。
输出域Scope¶
PyWebIO使用Scope模型来对内容输出的位置进行灵活地控制,PyWebIO的内容输出区可以划分出不同的输出域,PyWebIO将输出域称作 Scope
。
输出域为输出内容的容器,各个输出域之间上下排列,输出域也可以进行嵌套。
每个输出函数(函数名形如 put_xxx()
)都会将内容输出到一个Scope,默认为”当前Scope”,”当前Scope”由运行时上下文确定,输出函数也可以手动指定输出到的Scope。Scope名在会话内唯一。
use_scope()
可以使用 use_scope()
开启并进入一个新的输出域,或进入一个已经存在的输出域:
with use_scope('scope1'): # 创建并进入scope 'scope1'
put_text('text1 in scope1') # 输出内容到 scope1
put_text('text in parent scope of scope1') # 输出内容到 ROOT scope
with use_scope('scope1'): # 进入之前创建的scope 'scope1'
put_text('text2 in scope1') # 输出内容到 scope1
以上代码将会输出:
text1 in scope1
text2 in scope1
text in parent scope of scope1
use_scope()
还可以使用 clear
参数将scope中原有的内容清空:
with use_scope('scope2'):
put_text('create scope2')
put_text('text in parent scope of scope2')
with use_scope('scope2', clear=True): # enter the existing scope and clear the previous content
put_text('text in scope2')
以上代码将会输出:
text in scope2
text in parent scope of scope2
use_scope()
还可以作为装饰器来使用:
from datetime import datetime
@use_scope('time', clear=True)
def show_time():
put_text(datetime.now())
第一次调用 show_time
时,将会创建 time
输出域并在其中输出当前时间,之后每次调用 show_time()
,输出域都会被新的内容覆盖。
Scope是可嵌套的,初始条件下,PyWebIO应用只有一个最顶层的 ROOT
Scope。每创建一个新Scope,Scope的嵌套层级便会多加一层,每退出当前Scope,Scope的嵌套层级便会减少一层。PyWebIO使用Scope栈来保存运行时的Scope的嵌套层级。
例如,如下代码将会创建3个Scope:
with use_scope('A'):
put_text('Text in scope A')
with use_scope('B'):
put_text('Text in scope B')
with use_scope('C'):
put_text('Text in scope C')
以上代码将会产生如下Scope布局:
┌─ROOT────────────────────┐
│ │
│ ┌─A───────────────────┐ │
│ │ Text in scope A │ │
│ │ ┌─B───────────────┐ │ │
│ │ │ Text in scope B │ │ │
│ │ └─────────────────┘ │ │
│ └─────────────────────┘ │
│ │
│ ┌─C───────────────────┐ │
│ │ Text in scope C │ │
│ └─────────────────────┘ │
└─────────────────────────┘
输出函数的scope相关参数
输出函数(函数名形如 put_xxx()
)在默认情况下,会将内容输出到”当前Scope”,可以通过 use_scope()
设置运行时上下文的”当前Scope”。
此外,也可以通过输出函数的 scope
参数指定输出的目的Scope:
with use_scope('scope3'):
put_text('text1 in scope3') # output to current scope: scope3
put_text('text in ROOT scope', scope='ROOT') # output to ROOT Scope
put_text('text2 in scope3', scope='scope3') # output to scope3
以上代码将会输出:
text1 in scope3
text2 in scope3
text in ROOT scope
scope
参数除了直接指定目标Scope名,还可以使用一个整形通过索引Scope栈来确定Scope:0表示最顶层也就是ROOT Scope,-1表示当前Scope,-2表示进入当前Scope前所使用的Scope,……
默认条件下,在同一Scope中的输出内容,会根据输出函数的调用顺序从上往下排列,最后调用的输出函数会输出内容到目标Scope的底部。通过输出函数的 position
参数可以将输出内容插入到目标Scope的其他位置。
一个Scope中各次输出的元素具有像数组一样的索引,最前面的编号为0,以此往后递增加一;同样可以使用负数对Scope中的元素进行索引,-1表示最后面的元素,-2表示次后面的元素……
position
参数类型为整形, position>=0
时表示输出内容到目标Scope的第position号元素的前面; position<0
时表示输出内容到目标Scope第position号元素之后:
with use_scope('scope1'):
put_text('A')
put_text('B', position=0) # insert B before A -> B A
put_text('C', position=-2) # insert C after B -> B C A
put_text('D', position=1) # insert D before C B -> B D C A
输出域控制函数
除了 use_scope()
, PyWebIO同样提供了以下scope控制函数:
set_scope(name)
: 在当前位置(或指定位置)创建scopeclear(scope)
: 清除scope的内容remove(scope)
: 移除scopescroll_to(scope)
: 将页面滚动到scope处
页面环境设置¶
页面标题
调用 set_env(title=…)
可以设置页面标题。
自动滚动
在进行一些持续性的输出时(比如日志输出),有时希望在有新输出后自动将页面滚动到最下方,这时可以调用 set_env(auto_scroll_bottom=True)
来开启自动滚动。
注意,开启后,只有输出到ROOT Scope才可以触发自动滚动。
输出动画
PyWebIO在输出内容时默认会使用淡入的动画效果来显示内容,可使用 set_env(output_animation=False)
来关闭动画。
有关不同环境配置的效果可查看 set_env Demo
布局¶
通常,使用上述输出函数足以完成大部分输出,但是这些输出之间全都是竖直排列的。如果想创建更复杂的布局,需要使用布局函数。
pywebio.output
模块提供了3个布局函数,通过对他们进行组合可以完成各种复杂的布局:
put_row()
: 使用行布局输出内容. 内容在水平方向上排列put_column()
: 使用列布局输出内容. 内容在竖直方向上排列put_grid()
: 使用网格布局输出内容
通过组合 put_row()
和 put_column()
可以实现灵活布局:
put_row([
put_column([
put_code('A'),
put_row([
put_code('B1'), None, # None represents the space between the output
put_code('B2'), None,
put_code('B3'),
]),
put_code('C'),
]), None,
put_code('D'), None,
put_code('E')
])
以上代码将在浏览器上显示如下:
布局函数还支持自定义各部分的尺寸:
put_row([put_image(…), put_image(…)], size='40% 60%') # 左右两图宽度比2:3
更多布局函数的用法及代码示例请查阅 布局函数文档 .
Server模式与Script模式¶
在PyWebIO中,有两种方式用来运行PyWebIO应用:作为脚本运行和使用 start_server()
或 path_deploy()
来作为Web服务运行。
Overview¶
Server模式
在Server模式下,PyWebIO会启动一个Web服务来持续性地提供服务。当用户访问服务地址时,PyWebIO会开启一个新会话并运行PyWebIO应用。
使用 start_server()
启动一个Web Server来将PyWebIO应用作为Web服务运行, start_server()
可以接收一个函数作为PyWebIO应用;
也支持使用函数列表或字典,从而使一个PyWebIO Server下可以有多个不同功能的服务,服务之间可以通过 go_app()
或 put_link()
进行跳转:
def task_1():
put_text('task_1')
put_buttons(['Go task 2'], [lambda: go_app('task_2')])
hold()
def task_2():
put_text('task_2')
put_buttons(['Go task 1'], [lambda: go_app('task_1')])
hold()
def index():
put_link('Go task 1', app='task_1') # 使用app参数指定任务名
put_link('Go task 2', app='task_2')
# 等价于 start_server({'index': index, 'task_1': task_1, 'task_2': task_2})
start_server([index, task_1, task_2])
start_server()
提供了远程访问的支持,当开启远程访问后(通过在 start_server()
中传入 remote_access=True
开启 ),你将会得到一个用于访问当前应用的临时的公网访问地址,其他人任何都可以使用此地址访问你的应用。远程接入可以很方便地将应用临时分享给其他人。当前远程接入功能由 localhost.run 提供。
使用 path_deploy()
可以从一个路径中部署PyWebIO应用。位于该路径下的python文件需要包含名字为 main
的PyWebIO任务函数才能被视为PyWebIO应用程序。服务端会根据用户访问的URL来确定需要加载的文件并从中读取PyWebIO应用来运行。
例如,给定如下文件结构:
.
├── A
│ └── a.py
├── B
│ └── b.py
└── c.py
如果使用以上路径调用 path_deploy()
,你可以通过 URL http://<host>:<port>/A/b
来访问 b.py
文件中的PyWebIO应用。当文件在运行 path_deploy()
之后被修改,可以使用 reload
URL参数来重载文件: http://<host>:<port>/A/b?reload
你还可以使用 pywebio-path-deploy
命令来启动一个和 path_deploy()
效果一样的server。关于命令的更多信息请查阅命令帮助: pywebio-path-deploy --help
在Server模式下,可以使用 pywebio.platform.seo()
函数来设置任务函数SEO信息(在被搜索引擎索引时提供的网页信息,包含应用标题和应用简介),如果不使用 seo()
函数,默认条件下,PyWebIO会将任务函数的函数注释作为SEO信息(应用标题和简介之间使用一个空行分隔)。
注意
注意,在Server模式下, input
、 output
和 session
模块内的函数仅能在任务函数上下文中进行调用。比如如下调用是 不被允许的
import pywebio
from pywebio.input import input
port = input('Input port number:') # ❌ error
pywebio.start_server(my_task_func, port=int(port))
Script模式
Script模式下,在任何位置都可以调用PyWebIO的交互函数。
如果用户在会话结束之前关闭了浏览器,那么之后会话内对于PyWebIO交互函数的调用将会引发一个 SessionException
异常。
并发¶
PyWebIO 支持在多线程环境中使用。
Script模式
在 Script模式下,你可以自由地启动线程,并在其中调用PyWebIO的交互函数。当所有非 Daemon线程 运行结束后,脚本退出。
Server模式
Server模式下,如果需要在新创建的线程中使用PyWebIO的交互函数,需要手动调用 register_thread(thread)
对新进程进行注册(这样PyWebIO才能知道新创建的线程属于哪个会话)。
如果新创建的线程中没有使用到PyWebIO的交互函数,则无需注册。没有使用 register_thread(thread)
注册的线程不受会话管理,其调用PyWebIO的交互函数将会产生 SessionNotFoundException
异常。
当会话的任务函数和会话内通过 register_thread(thread)
注册的线程都结束运行时,会话关闭。
Server模式下多线程的使用示例:
def show_time():
while True:
with use_scope(name='time', clear=True):
put_text(datetime.datetime.now())
time.sleep(1)
def app():
t = threading.Thread(target=show_time)
register_thread(t)
put_markdown('## Clock')
t.start() # run `show_time()` in background
# ❌ this thread will cause `SessionNotFoundException`
threading.Thread(target=show_time).start()
put_text('Background task started.')
start_server(app, port=8080, debug=True)
会话的结束¶
会话还会因为用户的关闭浏览器而结束,这时当前会话内还未返回的PyWebIO输入函数调用将抛出 SessionClosedException
异常,之后对于PyWebIO交互函数的调用将会产生 SessionNotFoundException
或 SessionClosedException
异常。
可以使用 defer_call(func)
来设置会话结束时需要调用的函数。无论是因为用户主动关闭页面还是任务结束使得会话关闭,设置的函数都会被执行。
defer_call(func)
可以用于资源清理等工作。在会话中可以多次调用 defer_call()
,会话结束后将会顺序执行设置的函数。
与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的例子。
使用 pywebio.platform.flask.webio_view()
来获取在Flask中运行PyWebIO应用的视图函数:
from pywebio.platform.flask import webio_view
from flask import Flask
app = Flask(__name__)
# `task_func` is PyWebIO task function
app.add_url_rule('/tool', 'webio_view', webio_view(task_func),
methods=['GET', 'POST', 'OPTIONS']) # need GET,POST and OPTIONS methods
app.run(host='localhost', port=80)
以上代码使用添加了一条路由规则将PyWebIO应用的视图函数绑定到 /tool
路径下。
启动Flask应用后,访问 http://localhost/tool
即可打开PyWebIO应用
使用 pywebio.platform.django.webio_view()
来获取在Django中运行PyWebIO应用的视图函数:
# urls.py
from django.urls import path
from pywebio.platform.django import webio_view
# `task_func` is PyWebIO task function
webio_view_func = webio_view(task_func)
urlpatterns = [
path(r"tool", webio_view_func),
]
以上代码使用添加了一条路由规则将PyWebIO应用的视图函数绑定到 /tool
路径下。
启动Django应用后,访问 http://localhost/tool
即可打开PyWebIO应用
使用 pywebio.platform.aiohttp.webio_handler()
来获取在aiohttp中运行PyWebIO应用的 Request Handler 协程:
from aiohttp import web
from pywebio.platform.aiohttp import webio_handler
app = web.Application()
# `task_func` is PyWebIO task function
app.add_routes([web.get('/tool', webio_handler(task_func))])
web.run_app(app, host='localhost', port=80)
启动aiohttp应用后,访问 http://localhost/tool
即可打开PyWebIO应用
注意
当使用aiohttp后端时,PyWebIO使用WebSocket协议和浏览器进行通讯,如果你的aiohttp应用处在反向代理(比如Nginx)之后, 可能需要特别配置反向代理来支持WebSocket协议,这里 有一个Nginx配置WebSocket的例子。
使用 pywebio.platform.fastapi.webio_routes()
来获取在FastAPI/Starlette中运行PyWebIO应用的路由组件,你可以将其挂载在到FastAPI/Starlette应用中。
FastAPI:
from fastapi import FastAPI
from pywebio.platform.fastapi import webio_routes
app = FastAPI()
@app.get("/app")
def read_main():
return {"message": "Hello World from main app"}
# `task_func` is PyWebIO task function
app.mount("/tool", FastAPI(routes=webio_routes(task_func)))
Starlette:
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route, Mount
from pywebio.platform.fastapi import webio_routes
async def homepage(request):
return JSONResponse({'hello': 'world'})
app = Starlette(routes=[
Route('/', homepage),
Mount('/tool', routes=webio_routes(task_func)) # `task_func` is PyWebIO task function
])
使用 uvicorn <module>:app` 启动server后,访问 ``http://localhost:8000/tool/
即可打开PyWebIO应用
See also: FastAPI doc , Starlette doc
注意
当使用FastAPI或Starlette后端时,PyWebIO使用WebSocket协议和浏览器进行通讯,如果你的aiohttp应用处在反向代理(比如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会启动一个线程来运行任务函数。 除了基于线程的会话,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
可以查询协程运行状态和关闭协程。
会话的结束¶
与基于线程的会话类似,在基于协程的会话中,当任务函数和在会话内通过 run_async()
运行的协程全部结束后,会话关闭。
对于因为用户的关闭浏览器而造成的会话结束,处理逻辑和 基于线程的会话 一致:
此时当前会话内还未返回的PyWebIO输入函数调用将抛出 SessionClosedException
异常,之后对于PyWebIO交互函数的调用将会产生 SessionNotFoundException
或 SessionClosedException
异常。
协程会话也同样支持使用 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框架来调用。