guide.rst 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968
  1. User's guide
  2. ============
  3. 如果你接触过Web开发,你可能对接下来描述的PyWebIO的用法感到不太习惯,不同于传统Web开发的后端实现接口、前端进行展示交互的模式,在PyWebIO中,所有的逻辑都通过编写Python代码实现。
  4. 你可以按照编写控制台程序的逻辑编写PyWebIO应用,只不过这里的终端变成了浏览器。通过PyWebIO提供的命令式API,
  5. 你可以简单地调用 ``put_text`` 、 ``put_image`` 、 ``put_table`` 等函数输出文本、图片、表格等内容到浏览器,也可以调用 ``input`` 、 ``select`` 、
  6. ``file_upload`` 等函数在浏览器上显示不同表单来接收用户的输入。此外PyWebIO中还提供了点击事件、布局等支持,让你可以使用最少的代码完成与用户的交互,
  7. 并尽可能提供良好的用户体验。
  8. 本篇使用指南从几个方面对PyWebIO的使用进行介绍,覆盖了PyWebIO的绝大部分特性。本文档中大部分示例代码的右上方都有一个Demo链接,点击后可以在线预览代码的运行效果。
  9. 输入
  10. ------------
  11. 输入函数都定义在 :doc:`pywebio.input </input>` 模块中,可以使用 ``from pywebio.input import *`` 引入。
  12. 调用输入函数会在浏览器上弹出一个输入表单来获取输入。PyWebIO的输入函数是阻塞式的(和Python内置的 `input` 一样),在表单被成功提交之前,输入函数不会返回。
  13. 基本输入
  14. ^^^^^^^^^^^
  15. 首先是一些基本类型的输入
  16. 文本输入:
  17. .. exportable-codeblock::
  18. :name: text-input
  19. :summary: 文本输入
  20. age = input("How old are you?", type=NUMBER)
  21. put_text('age = %r' % age) # ..demo-only
  22. 这样一行代码的效果为:浏览器会弹出一个文本输入框来获取输入,在用户完成输入将表单提交后,函数返回用户输入的值。
  23. 下面是一些其他类型的输入函数:
  24. .. exportable-codeblock::
  25. :name: basic-input
  26. :summary: 基本输入
  27. # 密码输入
  28. password = input("Input password", type=PASSWORD)
  29. put_text('password = %r' % password) # ..demo-only
  30. ## ----
  31. # 下拉选择框
  32. gift = select('Which gift you want?', ['keyboard', 'ipad'])
  33. put_text('gift = %r' % gift) # ..demo-only
  34. ## ----
  35. # 勾选选项
  36. agree = checkbox("用户协议", options=['I agree to terms and conditions'])
  37. put_text('agree = %r' % agree) # ..demo-only
  38. ## ----
  39. # 单选选项
  40. answer = radio("Choose one", options=['A', 'B', 'C', 'D'])
  41. put_text('answer = %r' % answer) # ..demo-only
  42. ## ----
  43. # 多行文本输入
  44. text = textarea('Text Area', rows=3, placeholder='Some text')
  45. put_text('text = %r' % text) # ..demo-only
  46. ## ----
  47. # 文件上传
  48. img = file_upload("Select a image:", accept="image/*")
  49. if img: # ..demo-only
  50. put_image(img['content'], title=img['filename']) # ..demo-only
  51. 输入选项
  52. ^^^^^^^^^^^
  53. 输入函数可指定的参数非常丰富(全部参数及含义请见 :doc:`函数文档 </input>` ):
  54. .. exportable-codeblock::
  55. :name: input-args
  56. :summary: 输入参数
  57. input('This is label', type=TEXT, placeholder='This is placeholder',
  58. help_text='This is help text', required=True)
  59. 以上代码将在浏览器上显示如下:
  60. .. image:: /assets/input_1.png
  61. 我们可以为输入指定校验函数,校验函数应在校验通过时返回None,否则返回错误消息:
  62. .. exportable-codeblock::
  63. :name: input-valid-func
  64. :summary: 输入指定校验函数
  65. def check_age(p): # 检验函数校验通过时返回None,否则返回错误消息
  66. if p < 10:
  67. return 'Too young!!'
  68. if p > 60:
  69. return 'Too old!!'
  70. age = input("How old are you?", type=NUMBER, validate=check_age)
  71. put_text('age = %r' % age) # ..demo-only
  72. 当用户输入了不合法的值时,页面上的显示如下:
  73. .. image:: /assets/input_2.png
  74. :func:`pywebio.input.textarea` 还支持使用 `Codemirror <https://codemirror.net/>`_ 实现代码风格的编辑区,只需使用 ``code`` 参数传入Codemirror支持的选项即可(最简单的情况是直接传入 ``code={}`` 或 ``code=True``):
  75. .. exportable-codeblock::
  76. :name: codemirror
  77. :summary: textarea代码编辑
  78. code = textarea('Code Edit', code={
  79. 'mode': "python", # 编辑区代码语言
  80. 'theme': 'darcula', # 编辑区darcula主题, Visit https://codemirror.net/demo/theme.html#cobalt to get more themes
  81. }, value='import something\n# Write your python code')
  82. put_code(code, language='python') # ..demo-only
  83. 文本框的显示效果为:
  84. .. image:: /assets/codemirror_textarea.png
  85. :ref:`这里 <codemirror_options>` 列举了一些常用的Codemirror选项,完整的Codemirror选项请见:https://codemirror.net/doc/manual.html#config
  86. 输入组
  87. ^^^^^^^
  88. PyWebIO支持输入组, 返回结果为一个字典。`pywebio.input.input_group()` 接受单项输入组成的列表作为参数, 返回以单项输入函数中的 ``name`` 作为键、以输入数据为值的字典:
  89. .. exportable-codeblock::
  90. :name: input-group
  91. :summary: 输入组
  92. def check_age(p): # 检验函数校验通过时返回None,否则返回错误消息 # ..demo-only
  93. if p < 10: # ..demo-only
  94. return 'Too young!!' # ..demo-only
  95. if p > 60: # ..demo-only
  96. return 'Too old!!' # ..demo-only
  97. # ..demo-only
  98. data = input_group("Basic info",[
  99. input('Input your name', name='name'),
  100. input('Input your age', name='age', type=NUMBER, validate=check_age)
  101. ])
  102. put_text(data['name'], data['age'])
  103. 输入组中同样支持使用 ``validate`` 参数设置校验函数,其接受整个表单数据作为参数:
  104. .. exportable-codeblock::
  105. :name: input-group
  106. :summary: 输入组
  107. def check_age(p): # 检验函数校验通过时返回None,否则返回错误消息 # ..demo-only
  108. if p < 10: # ..demo-only
  109. return 'Too young!!' # ..demo-only
  110. if p > 60: # ..demo-only
  111. return 'Too old!!' # ..demo-only
  112. # ..demo-only
  113. def check_form(data): # 检验函数校验通过时返回None,否则返回 (input name,错误消息)
  114. if len(data['name']) > 6:
  115. return ('name', '名字太长!')
  116. if data['age'] <= 0:
  117. return ('age', '年龄不能为负数!')
  118. data = input_group("Basic info",[ # ..demo-only
  119. input('Input your name', name='name'), # ..demo-only
  120. input('Input your age', name='age', type=NUMBER, validate=check_age) # ..demo-only
  121. ], validate=check_form) # ..demo-only
  122. put_text(data['name'], data['age']) # ..demo-only
  123. .. attention::
  124. PyWebIO 根据是否在输入函数中传入 ``name`` 参数来判断输入函数是在 `input_group` 中还是被单独调用。
  125. 所以当单独调用一个输入函数时, **不要** 设置 ``name`` 参数;而在 `input_group` 中调用输入函数时,需 **务必提供** ``name`` 参数
  126. 输出
  127. ------------
  128. 输出函数都定义在 :doc:`pywebio.output </output>` 模块中,可以使用 ``from pywebio.output import *`` 引入。
  129. 调用输出函数后,内容会实时输出到浏览器,在应用的生命周期内,可以在任意时刻调用输出函数。
  130. 基本输出
  131. ^^^^^^^^^^^^^^
  132. PyWebIO提供了一系列函数来输出表格、链接等格式:
  133. .. exportable-codeblock::
  134. :name: basic-output
  135. :summary: 基本输出
  136. # 文本输出
  137. put_text("Hello world!")
  138. ## ----
  139. # 表格输出
  140. put_table([
  141. ['商品', '价格'],
  142. ['苹果', '5.5'],
  143. ['香蕉', '7'],
  144. ])
  145. ## ----
  146. # Markdown输出
  147. put_markdown('~~删除线~~')
  148. ## ----
  149. # 文件输出
  150. put_file('hello_word.txt', b'hello word!')
  151. ## ----
  152. # 显示一个弹窗
  153. popup('popup title', 'popup text content')
  154. PyWebIO提供的全部输出函数见 :doc:`pywebio.output </output>` 模块。另外,PyWebIO还支持一些第三方库来进行数据可视化,参见 :doc:`第三方库生态 </libraries_support>` 。
  155. .. _combine_output:
  156. 组合输出
  157. ^^^^^^^^^^^^^^
  158. 函数名以 ``put_`` 开始的输出函数,可以与一些输出函数组合使用,作为最终输出的一部分:
  159. `put_table() <pywebio.output.put_table>` 支持以 ``put_xxx()`` 调用作为单元格内容:
  160. .. exportable-codeblock::
  161. :name: putxxx
  162. :summary: 组合输出
  163. put_table([
  164. ['Type', 'Content'],
  165. ['html', put_html('X<sup>2</sup>')],
  166. ['text', '<hr/>'], # 等价于 ['text', put_text('<hr/>')]
  167. ['buttons', put_buttons(['A', 'B'], onclick=...)], # ..doc-only
  168. ['buttons', put_buttons(['A', 'B'], onclick=put_text)], # ..demo-only
  169. ['markdown', put_markdown('`Awesome PyWebIO!`')],
  170. ['file', put_file('hello.text', b'hello world')],
  171. ['table', put_table([['A', 'B'], ['C', 'D']])]
  172. ])
  173. 上例显示效果如下:
  174. .. image:: /assets/put_table.png
  175. 类似地, `popup() <pywebio.output.popup>` 也可以将 ``put_xxx()`` 调用作为弹窗内容:
  176. .. exportable-codeblock::
  177. :name: popup
  178. :summary: 弹窗
  179. popup('Popup title', [
  180. put_html('<h3>Popup Content</h3>'),
  181. 'plain html: <br/>', # 等价于 put_text('plain html: <br/>')
  182. put_table([['A', 'B'], ['C', 'D']]),
  183. put_buttons(['close_popup()'], onclick=lambda _: close_popup())
  184. ])
  185. 其他接受 ``put_xxx()`` 调用作为参数的输出函数还有 `put_collapse() <pywebio.output.put_collapse>` 、 `put_scrollable() <pywebio.output.put_scrollable>` 、`put_row() <pywebio.output.put_row>` 等,
  186. 此外,还可以通过 `put_widget() <pywebio.output.put_widget>` 自定义可接收 ``put_xxx()`` 调用的输出组件,具体用法请参考函数文档。
  187. 使用组合输出时,如果想在内容输出后,对其中的 ``put_xxx()`` 子项进行动态修改,可以使用 `output() <pywebio.output.output>` 函数,
  188. `output() <pywebio.output.output>` 就像一个占位符,它可以像 ``put_xxx()`` 一样传入 `put_table` 、 `popup` 、 `put_widget` 等函数中作为输出的一部分,
  189. 并且,在输出后,还可以对其中的内容进行修改(比如重置或增加内容):
  190. .. exportable-codeblock::
  191. :name: output
  192. :summary: 内容占位符——`output()`
  193. hobby = output(put_text('Coding'))
  194. put_table([
  195. ['Name', 'Hobbies'],
  196. ['Wang', hobby] # hobby 初始为 Coding
  197. ])
  198. ## ----
  199. hobby.reset(put_text('Movie')) # hobby 被重置为 Movie
  200. ## ----
  201. hobby.append(put_text('Music'), put_text('Drama')) # 向 hobby 追加 Music, Drama
  202. ## ----
  203. hobby.insert(0, put_markdown('**Coding**')) # 将 Coding 插入 hobby 顶端
  204. 事件回调
  205. ^^^^^^^^^^^^^^
  206. 从上面可以看出,PyWebIO把交互分成了输入和输出两部分:输入函数为阻塞式调用,会在用户浏览器上显示一个表单,在用户提交表单之前输入函数将不会返回;输出函数将内容实时输出至浏览器。这种交互方式和控制台程序是一致的,因此PyWebIO应用非常适合使用控制台程序的编写逻辑来进行开发。
  207. 此外,PyWebIO还支持事件回调:PyWebIO允许你输出一些控件,当控件被点击时执行提供的回调函数。
  208. 下面是一个例子:
  209. .. exportable-codeblock::
  210. :name: onclick-callback
  211. :summary: 事件回调
  212. from functools import partial
  213. def edit_row(choice, row):
  214. put_text("You click %s button ar row %s" % (choice, row))
  215. put_table([
  216. ['Idx', 'Actions'],
  217. [1, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=1))],
  218. [2, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=2))],
  219. [3, put_buttons(['edit', 'delete'], onclick=partial(edit_row, row=3))],
  220. ])
  221. `put_table() <pywebio.output.put_table>` 的调用不会阻塞。当用户点击了某行中的按钮时,PyWebIO会自动调用相应的回调函数:
  222. .. image:: /assets/table_onclick.*
  223. 当然,PyWebIO还支持单独的按钮控件:
  224. .. exportable-codeblock::
  225. :name: put-buttons
  226. :summary: 按钮控件
  227. def btn_click(btn_val):
  228. put_text("You click %s button" % btn_val)
  229. put_buttons(['A', 'B', 'C'], onclick=btn_click)
  230. .. note::
  231. 在PyWebIO会话(关于会话的概念见下文 :ref:`Server与script模式 <server_and_script_mode>` )结束后,事件回调也将不起作用,你可以在任务函数末尾处使用 :func:`pywebio.session.hold()` 函数来将会话保持,这样在用户关闭浏览器页面前,事件回调将一直可用。
  232. 输出域Scope
  233. ^^^^^^^^^^^^^^
  234. PyWebIO使用Scope模型来对内容输出的位置进行灵活地控制,PyWebIO的内容输出区可以划分出不同的输出域,PyWebIO将输出域称作 `Scope` 。
  235. 输出域为输出内容的容器,各个输出域之间上下排列,输出域也可以进行嵌套。
  236. 每个输出函数(函数名形如 `put_xxx()` )都会将内容输出到一个Scope,默认为"当前Scope","当前Scope"由运行时上下文确定,输出函数也可以手动指定输出到的Scope。Scope名在会话内唯一。
  237. .. _use_scope:
  238. **use_scope()**
  239. 可以使用 `use_scope() <pywebio.output.use_scope>` 开启并进入一个新的输出域,或进入一个已经存在的输出域:
  240. .. exportable-codeblock::
  241. :name: use-scope
  242. :summary: 使用`use_scope()`创建或进入输出域
  243. with use_scope('scope1'): # 创建并进入scope 'scope1'
  244. put_text('text1 in scope1')
  245. put_text('text in parent scope of scope1')
  246. with use_scope('scope1'): # 进入之前创建的scope 'scope1'
  247. put_text('text2 in scope1')
  248. 以上代码将会输出::
  249. text1 in scope1
  250. text2 in scope1
  251. text in parent scope of scope1
  252. `use_scope() <pywebio.output.use_scope>` 还可以使用 `clear` 参数将scope中原有的内容清空:
  253. .. exportable-codeblock::
  254. :name: use-scope
  255. :summary: 使用`use_scope()`清空输出域内容
  256. with use_scope('scope2'):
  257. put_text('create scope2')
  258. put_text('text in parent scope of scope2')
  259. ## ----
  260. with use_scope('scope2', clear=True): # 进入之前创建的scope2,并清空原有内容
  261. put_text('text in scope2')
  262. 以上代码将会输出::
  263. text in scope2
  264. text in parent scope of scope2
  265. `use_scope() <pywebio.output.use_scope>` 还可以作为装饰器来使用:
  266. .. exportable-codeblock::
  267. :name: use-scope-decorator
  268. :summary: `use_scope()`作为装饰器来使用
  269. import time # ..demo-only
  270. from datetime import datetime
  271. @use_scope('time', clear=True)
  272. def show_time():
  273. put_text(datetime.now())
  274. while 1: # ..demo-only
  275. show_time() # ..demo-only
  276. time.sleep(1) # ..demo-only
  277. 第一次调用 ``show_time`` 时,将会在当前位置创建 ``time`` 输出域并在其中输出当前时间,之后每次调用 ``show_time()`` ,时间都会输出到相同的区域。
  278. Scope是可嵌套的,初始条件下,PyWebIO应用只有一个最顶层的 ``ROOT`` Scope。每创建一个新Scope,Scope的嵌套层级便会多加一层,每退出当前Scope,Scope的嵌套层级便会减少一层。
  279. PyWebIO使用Scope栈来保存运行时的Scope的嵌套层级。
  280. 例如,如下代码将会创建3个Scope:
  281. .. exportable-codeblock::
  282. :name: use-scope-nested
  283. :summary: 嵌套Scope
  284. with use_scope('A'):
  285. put_text('Text in scope A')
  286. with use_scope('B'):
  287. put_text('Text in scope B')
  288. with use_scope('C'):
  289. put_text('Text in scope C')
  290. put_html("""<style> # ..demo-only
  291. #pywebio-scope-A {border: 1px solid red;} # ..demo-only
  292. #pywebio-scope-B {border: 1px solid blue;margin:2px} # ..demo-only
  293. #pywebio-scope-C {border: 1px solid green;margin-top:2px} # ..demo-only
  294. </style>""") # ..demo-only
  295. put_text() # ..demo-only
  296. put_buttons([('Put text to %s' % i, i) for i in ('A', 'B', 'C')], lambda s: put_text(s, scope=s)) # ..demo-only
  297. 以上代码将会产生如下Scope布局::
  298. ┌─ROOT────────────────────┐
  299. │ │
  300. │ ┌─A───────────────────┐ │
  301. │ │ Text in scope A │ │
  302. │ │ ┌─B───────────────┐ │ │
  303. │ │ │ Text in scope B │ │ │
  304. │ │ └─────────────────┘ │ │
  305. │ └─────────────────────┘ │
  306. │ │
  307. │ ┌─C───────────────────┐ │
  308. │ │ Text in scope C │ │
  309. │ └─────────────────────┘ │
  310. └─────────────────────────┘
  311. .. _scope_param:
  312. **输出函数的scope相关参数**
  313. 输出函数(函数名形如 ``put_xxx()`` )在默认情况下,会将内容输出到"当前Scope",可以通过 ``use_scope()`` 设置运行时上下文的"当前Scope"。
  314. 此外,也可以通过输出函数的 ``scope`` 参数指定输出的目的Scope:
  315. .. exportable-codeblock::
  316. :name: put-xxx-scope
  317. :summary: 输出函数的`scope`参数
  318. with use_scope('scope3'):
  319. put_text('text1 in scope3') # 输出到当前Scope:scope3
  320. put_text('text in ROOT scope', scope='ROOT') # 输出到ROOT Scope
  321. put_text('text2 in scope3', scope='scope3') # 输出到scope3
  322. 以上将会输出::
  323. text1 in scope3
  324. text2 in scope3
  325. text in ROOT scope
  326. ``scope`` 参数除了直接指定目标Scope名,还可以使用一个整形通过索引Scope栈来确定Scope:0表示最顶层也就是ROOT Scope,-1表示当前Scope,-2表示进入当前Scope前所使用的Scope,......
  327. 默认条件下,在同一Scope中的输出内容,会根据输出函数的调用顺序从上往下排列,最后调用的输出函数会输出内容到目标Scope的底部。通过输出函数的 ``position`` 参数可以将输出内容插入到目标Scope的其他位置。
  328. 一个Scope中各次输出的元素具有像数组一样的索引,最前面的编号为0,以此往后递增加一;同样可以使用负数对Scope中的元素进行索引,-1表示最后面的元素,-2表示次后面的元素......
  329. ``position`` 参数类型为整形, ``position>=0`` 时表示输出内容到目标Scope的第position号元素的前面; ``position<0`` 时表示输出内容到目标Scope第position号元素之后:
  330. .. exportable-codeblock::
  331. :name: put-xxx-position
  332. :summary: 输出函数的`position`参数
  333. with use_scope('scope1'):
  334. put_text('A') # 输出内容: A
  335. ## ----
  336. with use_scope('scope1'): # ..demo-only
  337. put_text('B', position=0) # 输出内容: B A
  338. ## ----
  339. with use_scope('scope1'): # ..demo-only
  340. put_text('C', position=-2) # 输出内容: B C A
  341. ## ----
  342. with use_scope('scope1'): # ..demo-only
  343. put_text('D', position=1) # 输出内容: B D C A
  344. **输出域控制函数**
  345. 除了 `use_scope()` , PyWebIO同样提供了以下scope控制函数:
  346. * `set_scope(name) <pywebio.output.set_scope>` : 在当前位置(或指定位置)创建scope
  347. * `clear(scope) <pywebio.output.clear>` : 清除scope的内容
  348. * `remove(scope) <pywebio.output.remove>` : 移除scope
  349. * `scroll_to(scope) <pywebio.output.scroll_to>` : 将页面滚动到scope处
  350. 页面环境设置
  351. ^^^^^^^^^^^^^^
  352. **页面标题**
  353. 调用 `set_env(title=...) <pywebio.session.set_env>` 可以设置页面标题。
  354. **自动滚动**
  355. 在进行一些持续性的输出时(比如日志输出),有时希望在有新输出后自动将页面滚动到最下方,这时可以调用 `set_env(auto_scroll_bottom=True) <pywebio.session.set_env>` 来开启自动滚动。
  356. 注意,开启后,只有输出到ROOT Scope才可以触发自动滚动。
  357. **输出动画**
  358. PyWebIO在输出内容时默认会使用淡入的动画效果来显示内容,可使用 `set_env(output_animation=False) <pywebio.session.set_env>` 来关闭动画。
  359. 有关不同环境配置的效果可查看 :demo_host:`set_env Demo </?pywebio_api=set_env_demo>`
  360. 布局
  361. ^^^^^^^^^^^^^^
  362. 一般情况下,使用上文介绍的各种输出函数足以完成各种内容的展示,但直接调用输出函数产生的输出之间都是竖直排列的,如果想实现更复杂的布局(比如在页面左侧显示一个代码块,在右侧显示一个图像),就需要借助布局函数。
  363. ``pywebio.output`` 模块提供了3个布局函数,通过对他们进行组合可以完成各种复杂的布局:
  364. * `put_row() <pywebio.output.put_row>` : 使用行布局输出内容. 内容在水平方向上排列
  365. * `put_column() <pywebio.output.put_column>` : 使用列布局输出内容. 内容在竖直方向上排列
  366. * `put_grid() <pywebio.output.put_grid>` : 使用网格布局输出内容
  367. 通过组合 ``put_row()`` 和 ``put_column()`` 可以实现灵活布局:
  368. .. exportable-codeblock::
  369. :name: put-row-column
  370. :summary: 布局函数
  371. put_row([
  372. put_column([
  373. put_code('A'),
  374. put_row([
  375. put_code('B1'), None, # None 表示输出之间的空白
  376. put_code('B2'), None,
  377. put_code('B3'),
  378. ]),
  379. put_code('C'),
  380. ]), None,
  381. put_code('D'), None,
  382. put_code('E')
  383. ])
  384. 显示效果如下:
  385. .. image:: /assets/layout.png
  386. :align: center
  387. 布局函数还支持自定义各部分的尺寸::
  388. put_row([put_image(...), put_image(...)], size='40% 60%') # 左右两图宽度比2:3
  389. 更多布局函数的用法及代码示例请查阅 :ref:`布局函数文档 <style_and_layout>` .
  390. 样式
  391. ^^^^^^^^^^^^^^
  392. 如果你熟悉 `CSS样式 <https://www.google.com/search?q=CSS%E6%A0%B7%E5%BC%8F>`_ ,你还可以使用 `style() <pywebio.output.style>` 函数给输出设定自定义样式。
  393. 可以给单个的 ``put_xxx()`` 输出设定CSS样式,也可以配合组合输出使用:
  394. .. exportable-codeblock::
  395. :name: style
  396. :summary: 输出样式
  397. style(put_text('Red'), 'color: red')
  398. ## ----
  399. put_table([
  400. ['A', 'B'],
  401. ['C', style(put_text('Red'), 'color: red')],
  402. ])
  403. ``style()`` 也接受列表作为输入,``style()`` 会为列表的每一项都设置CSS样式,返回值可以直接输出,可用于任何接受 ``put_xxx()`` 列表的地方:
  404. .. exportable-codeblock::
  405. :name: style-list
  406. :summary: 批量设置输出样式
  407. style([
  408. put_text('Red'),
  409. put_markdown('~~del~~')
  410. ], 'color: red')
  411. ## ----
  412. put_collapse('title', style([
  413. put_text('text'),
  414. put_markdown('~~del~~'),
  415. ], 'margin-left: 20px'))
  416. .. _server_and_script_mode:
  417. Server模式与Script模式
  418. ------------------------------------
  419. 在 :ref:`Hello, world <hello_word>` 一节中,已经知道,PyWebIO支持在普通的脚本中调用和使用
  420. `start_server() <pywebio.platform.tornado.start_server>` 启动一个Web服务两种模式。
  421. **Server模式**
  422. 在Server模式下,PyWebIO会启动一个Web服务来持续性地提供服务。需要提供一个任务函数(类似于Web开发中的视图函数),当用户访问服务地址时,PyWebIO会开启一个新会话并运行任务函数。
  423. 使用 `start_server() <pywebio.platform.tornado.start_server>` 来启动PyWebIO的Server模式, `start_server() <pywebio.platform.tornado.start_server>` 除了接收一个函数作为任务函数外,
  424. 还支持传入函数列表或字典,从而使一个PyWebIO Server下可以有多个不同功能的服务,服务之间可以通过 `go_app() <pywebio.session.go_app>` 或 `put_link() <pywebio.output.put_link>` 进行跳转::
  425. def task_1():
  426. put_text('task_1')
  427. put_buttons(['Go task 2'], [lambda: go_app('task_2')])
  428. hold()
  429. def task_2():
  430. put_text('task_2')
  431. put_buttons(['Go task 1'], [lambda: go_app('task_1')])
  432. hold()
  433. def index():
  434. put_link('Go task 1', app='task_1') # 使用app参数指定任务名
  435. put_link('Go task 2', app='task_2')
  436. start_server([index, task_1, task_2]) # 或 start_server({'index': index, 'task_1': task_1, 'task_2': task_2})
  437. 可以使用 `pywebio.platform.seo()` 函数来设置任务函数SEO信息(在被搜索引擎索引时提供的网页信息,包含应用标题和应用简介),如果不使用 ``seo()`` 函数,默认条件下,PyWebIO会将任务函数的函数注释作为SEO信息(应用标题和简介之间使用一个空行分隔)。
  438. .. attention::
  439. 注意,在Server模式下,仅能在任务函数上下文中对PyWebIO的交互函数进行调用。比如如下调用是 **不被允许的** ::
  440. import pywebio
  441. from pywebio.input import input
  442. port = input('Input port number:') # ❌ 在任务函数上下文之外调用了PyWebIO交互函数!!
  443. pywebio.start_server(my_task_func, port=int(port))
  444. **Script模式**
  445. Script模式下,在任何位置都可以调用PyWebIO的交互函数。
  446. 如果用户在会话结束之前关闭了浏览器,那么之后会话内对于PyWebIO交互函数的调用将会引发一个 `SessionException <pywebio.exceptions.SessionException>` 异常。
  447. .. _thread_in_server_mode:
  448. 并发
  449. ^^^^^^^^^^^^^^
  450. PyWebIO 支持在多线程环境中使用。
  451. **Script模式**
  452. 在 Script模式下,你可以自由地启动线程,并在其中调用PyWebIO的交互函数。当所有非 `Daemon线程 <https://docs.python.org/3/library/threading.html#thread-objects>`_ 运行结束后,脚本退出。
  453. **Server模式**
  454. Server模式下,如果需要在新创建的线程中使用PyWebIO的交互函数,需要手动调用 `register_thread(thread) <pywebio.session.register_thread>` 对新进程进行注册(这样PyWebIO才能知道新创建的线程属于哪个会话)。
  455. 如果新创建的线程中没有使用到PyWebIO的交互函数,则无需注册。没有使用 `register_thread(thread) <pywebio.session.register_thread>` 注册的线程不受会话管理,其调用PyWebIO的交互函数将会产生 `SessionNotFoundException <pywebio.exceptions.SessionNotFoundException>` 异常。
  456. 当会话的任务函数和会话内通过 `register_thread(thread) <pywebio.session.register_thread>` 注册的线程都结束运行时,会话关闭。
  457. Server模式下多线程的使用示例::
  458. def show_time():
  459. while True:
  460. with use_scope(name='time', clear=True):
  461. put_text(datetime.datetime.now())
  462. time.sleep(1)
  463. def app():
  464. t = threading.Thread(target=show_time)
  465. register_thread(t)
  466. put_markdown('## Clock')
  467. t.start() # 在后台运行show_time()
  468. # ❌ 没有使用register_thread注册的线程调用PyWebIO交互函数会产生异常
  469. threading.Thread(target=show_time).start()
  470. put_text('Background task started.')
  471. start_server(app, port=8080, debug=True)
  472. .. _session_close:
  473. 会话的结束
  474. ^^^^^^^^^^^^^^
  475. 会话还会因为用户的关闭浏览器而结束,这时当前会话内还未返回的PyWebIO输入函数调用将抛出 `SessionClosedException <pywebio.exceptions.SessionClosedException>` 异常,之后对于PyWebIO交互函数的调用将会产生 `SessionNotFoundException <pywebio.exceptions.SessionNotFoundException>` 或 `SessionClosedException <pywebio.exceptions.SessionClosedException>` 异常。
  476. 可以使用 `defer_call(func) <pywebio.session.defer_call>` 来设置会话结束时需要调用的函数。无论是因为用户主动关闭页面还是任务结束使得会话关闭,设置的函数都会被执行。
  477. `defer_call(func) <pywebio.session.defer_call>` 可以用于资源清理等工作。在会话中可以多次调用 `defer_call() <pywebio.session.defer_call>` ,会话结束后将会顺序执行设置的函数。
  478. 与Web框架集成
  479. ---------------
  480. .. _integration_web_framework:
  481. 可以将PyWebIO应用集成到现有的Python Web项目中,PyWebIO应用与Web项目共用一个Web框架。目前支持与Flask、Tornado、Django和aiohttp Web框架的集成。
  482. 集成方法
  483. ^^^^^^^^^^^
  484. 不同Web框架的集成方法如下:
  485. .. tabs::
  486. .. tab:: Tornado
  487. 需要在Tornado应用中引入一个 ``RequestHandler`` ::
  488. import tornado.ioloop
  489. import tornado.web
  490. from pywebio.platform.tornado import webio_handler
  491. from pywebio import STATIC_PATH
  492. class MainHandler(tornado.web.RequestHandler):
  493. def get(self):
  494. self.write("Hello, world")
  495. if __name__ == "__main__":
  496. application = tornado.web.Application([
  497. (r"/", MainHandler),
  498. (r"/tool", webio_handler(task_func)), # task_func 为使用PyWebIO编写的任务函数
  499. ])
  500. application.listen(port=80, address='localhost')
  501. tornado.ioloop.IOLoop.current().start()
  502. 以上代码调用 `webio_handler(task_func) <pywebio.platform.tornado.webio_handler>` 来获得PyWebIO和浏览器进行通讯的Tornado `WebSocketHandler <https://www.tornadoweb.org/en/stable/websocket.html#tornado.websocket.WebSocketHandler>`_ ,
  503. 并将其绑定在 ``/tool`` 路由下。启动Tornado服务器后,访问 ``http://localhost/tool`` 即可打开PyWebIO应用
  504. .. attention::
  505. 当使用Tornado后端时,PyWebIO使用WebSocket协议和浏览器进行通讯,如果你的Tornado应用处在反向代理(比如Nginx)之后,
  506. 可能需要特别配置反向代理来支持WebSocket协议,:ref:`这里 <nginx_ws_config>` 有一个Nginx配置WebSocket的例子。
  507. .. tab:: Flask
  508. 需要添加一个PyWebIO相关的路由,用来和浏览器进行Http通讯::
  509. from pywebio.platform.flask import webio_view
  510. from pywebio import STATIC_PATH
  511. from flask import Flask, send_from_directory
  512. app = Flask(__name__)
  513. # task_func 为使用PyWebIO编写的任务函数
  514. app.add_url_rule('/tool', 'webio_view', webio_view(task_func),
  515. methods=['GET', 'POST', 'OPTIONS']) # 接口需要能接收GET、POST和OPTIONS请求
  516. app.run(host='localhost', port=80)
  517. 以上代码使用 `webio_view(task_func) <pywebio.platform.flask.webio_view>` 来获得运行PyWebIO应用的Flask视图 ,
  518. 并调用 `Flask.add_url_rule <https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.add_url_rule>`_ 将其绑定在 ``/tool`` 路径下。启动Flask应用后,访问 ``http://localhost/tool`` 即可打开PyWebIO应用
  519. .. tab:: Django
  520. 在django的路由配置文件 ``urls.py`` 中加入PyWebIO相关的路由即可::
  521. # urls.py
  522. from functools import partial
  523. from django.urls import path
  524. from django.views.static import serve
  525. from pywebio import STATIC_PATH
  526. from pywebio.platform.django import webio_view
  527. # task_func 为使用PyWebIO编写的任务函数
  528. webio_view_func = webio_view(task_func)
  529. urlpatterns = [
  530. path(r"tool", webio_view_func), # http通信接口
  531. ]
  532. 以上代码使用添加了一条路由规则将PyWebIO应用的视图函数绑定到 ``/tool`` 路径下。
  533. 启动Django应用后,访问 ``http://localhost/tool`` 即可打开PyWebIO应用
  534. .. tab:: aiohttp
  535. 需要添加一个PyWebIO相关的路由,用来和浏览器进行WebSocket通讯::
  536. from aiohttp import web
  537. from pywebio.platform.aiohttp import static_routes, webio_handler
  538. app = web.Application()
  539. # task_func 为使用PyWebIO编写的任务函数
  540. app.add_routes([web.get('/tool', webio_handler(task_func))]) # websocket通信接口
  541. web.run_app(app, host='localhost', port=80)
  542. 启动aiohttp应用后,访问 ``http://localhost/tool`` 即可打开PyWebIO应用
  543. .. attention::
  544. 当使用aiohttp后端时,PyWebIO使用WebSocket协议和浏览器进行通讯,如果你的aiohttp应用处在反向代理(比如Nginx)之后,
  545. 可能需要特别配置反向代理来支持WebSocket协议,:ref:`这里 <nginx_ws_config>` 有一个Nginx配置WebSocket的例子。
  546. .. _integration_web_framework_note:
  547. 注意事项
  548. ^^^^^^^^^^^
  549. **PyWebIO静态资源的托管**
  550. PyWebIO默认使用CDN来获取前端的静态资源,如果要将PyWebIO应用部署到离线环境中,需要自行托管静态文件,
  551. 并将 ``webio_view()`` 或 ``webio_handler()`` 的 ``cdn`` 参数设置为 ``False`` ,此时需要将静态资源托管在和PyWebIO应用同级的目录下。
  552. 同时,也可以通过 ``cdn`` 参数直接设置PyWebIO静态资源的部署目录。
  553. PyWebIO的静态文件的路径可保存在 ``pywebio.STATIC_PATH`` 中,可使用命令 ``python3 -c "import pywebio; print(pywebio.STATIC_PATH)"`` 将其打印出来。
  554. .. note:: 使用 ``start_server()`` 启动的应用,如果将 ``cdn`` 参数设置为 ``False`` ,会自动启动一个本地的静态资源托管服务,无需手动托管。
  555. .. _coroutine_based_session:
  556. 基于协程的会话
  557. ---------------
  558. 此部分内容属于高级特性,您不必使用此部分也可以实现PyWebIO支持的全部功能。PyWebIO中所有仅用于协程会话的函数或方法都在文档中有特别说明。
  559. PyWebIO的会话实现默认是基于线程的,用户每打开一个和服务端的会话连接,PyWebIO会启动一个线程来运行任务函数。
  560. 除了基于线程的会话,PyWebIO还提供了基于协程的会话。基于协程的会话接受协程函数作为任务函数。
  561. 基于协程的会话为单线程模型,所有会话都运行在一个线程内。对于IO密集型的任务,协程比线程占用更少的资源同时又拥有媲美于线程的性能。
  562. 另外,协程的上下文切换具有可预测性,能够减少程序同步与加锁的需要,可以有效避免大多数临界区问题。
  563. 使用协程会话
  564. ^^^^^^^^^^^^^^^^
  565. 要使用基于协程的会话,需要使用 ``async`` 关键字将任务函数声明为协程函数,并使用 ``await`` 语法调用PyWebIO输入函数:
  566. .. code-block:: python
  567. :emphasize-lines: 5,6
  568. from pywebio.input import *
  569. from pywebio.output import *
  570. from pywebio import start_server
  571. async def say_hello():
  572. name = await input("what's your name?")
  573. put_text('Hello, %s' % name)
  574. start_server(say_hello, auto_open_webbrowser=True)
  575. 在协程任务函数中,也可以使用 ``await`` 调用其他协程或标准库 `asyncio <https://docs.python.org/3/library/asyncio.html>`_ 中的可等待对象( `awaitable objects <https://docs.python.org/3/library/asyncio-task.html#asyncio-awaitables>`_ ):
  576. .. code-block:: python
  577. :emphasize-lines: 6,10
  578. import asyncio
  579. from pywebio import start_server
  580. async def hello_word():
  581. put_text('Hello ...')
  582. await asyncio.sleep(1) # await asyncio 库中的 awaitable objects
  583. put_text('... World!')
  584. async def main():
  585. await hello_word() # await 协程
  586. put_text('Bye, bye')
  587. start_server(main, auto_open_webbrowser=True)
  588. .. attention::
  589. 在基于协程的会话中, :doc:`pywebio.input </input>` 模块中的定义输入函数都需要使用 ``await`` 语法来获取返回值,
  590. 忘记使用 ``await`` 将会是在使用基于协程的会话时常出现的错误。
  591. 其他在协程会话中也需要使用 ``await`` 语法来进行调用函数有:
  592. * `pywebio.session.run_asyncio_coroutine(coro_obj) <pywebio.session.run_asyncio_coroutine>`
  593. * `pywebio.session.eval_js(expression) <pywebio.session.eval_js>`
  594. * `pywebio.session.hold() <pywebio.session.hold>`
  595. .. warning::
  596. 虽然PyWebIO的协程会话兼容标准库 ``asyncio`` 中的 ``awaitable objects`` ,但 ``asyncio`` 库不兼容PyWebIO协程会话中的 ``awaitable objects`` .
  597. 也就是说,无法将PyWebIO中的 ``awaitable objects`` 传入 ``asyncio`` 中的接受 ``awaitable objects`` 作为参数的函数中,比如如下调用是 **不被支持的** ::
  598. await asyncio.shield(pywebio.input())
  599. await asyncio.gather(asyncio.sleep(1), pywebio.session.eval_js('1+1'))
  600. task = asyncio.create_task(pywebio.input())
  601. .. _coroutine_based_concurrency:
  602. 协程会话的并发
  603. ^^^^^^^^^^^^^^^^
  604. 在基于协程的会话中,你可以启动线程,但是无法在其中调用PyWebIO交互函数( `register_thread() <pywebio.session.register_thread>` 在协程会话中不可用)。
  605. 但你可以使用 `run_async(coro) <pywebio.session.run_async>` 来异步执行一个协程对象,新协程内可以使用PyWebIO交互函数:
  606. .. code-block:: python
  607. :emphasize-lines: 10
  608. from pywebio import start_server
  609. from pywebio.session import run_async
  610. async def counter(n):
  611. for i in range(n):
  612. put_text(i)
  613. await asyncio.sleep(1)
  614. async def main():
  615. run_async(counter(10))
  616. put_text('Main coroutine function exited.')
  617. start_server(main, auto_open_webbrowser=True)
  618. `run_async(coro) <pywebio.session.run_async>` 返回一个 `TaskHandle <pywebio.session.coroutinebased.TaskHandle>` ,通过 `TaskHandle <pywebio.session.coroutinebased.TaskHandle>` 可以查询协程运行状态和关闭协程。
  619. 协程会话的关闭
  620. ^^^^^^^^^^^^^^^^
  621. 与基于线程的会话类似,在基于协程的会话中,当任务函数和在会话内通过 `run_async() <pywebio.session.run_async>` 运行的协程全部结束后,会话关闭。
  622. 对于因为用户的关闭浏览器而造成的会话结束,处理逻辑和 :ref:`基于线程的会话 <session_close>` 一致:
  623. 此时当前会话内还未返回的PyWebIO输入函数调用将抛出 `SessionClosedException <pywebio.exceptions.SessionClosedException>` 异常,之后对于PyWebIO交互函数的调用将会产生 `SessionNotFoundException <pywebio.exceptions.SessionNotFoundException>` 或 `SessionClosedException <pywebio.exceptions.SessionClosedException>` 异常。
  624. 协程会话也同样支持使用 `defer_call(func) <pywebio.session.defer_call>` 来设置会话结束时需要调用的函数。
  625. 协程会话与Web框架集成
  626. ^^^^^^^^^^^^^^^^^^^^^^^^^
  627. 基于协程的会话同样可以与Web框架进行集成,只需要在原来传入任务函数的地方改为传入协程函数即可。
  628. 但当前在使用基于协程的会话集成进Flask或Django时,存在一些限制:
  629. 一是协程函数内还无法直接通过 ``await`` 直接等待asyncio库中的协程对象,目前需要使用 `run_asyncio_coroutine() <pywebio.session.run_asyncio_coroutine>` 进行包装。
  630. 二是,在启动Flask/Django这类基于线程的服务器之前需要启动一个单独的线程来运行事件循环。
  631. 使用基于协程的会话集成进Flask的示例:
  632. .. code-block:: python
  633. :emphasize-lines: 12,20
  634. import asyncio
  635. import threading
  636. from flask import Flask, send_from_directory
  637. from pywebio import STATIC_PATH
  638. from pywebio.output import *
  639. from pywebio.platform.flask import webio_view
  640. from pywebio.platform import run_event_loop
  641. from pywebio.session import run_asyncio_coroutine
  642. async def hello_word():
  643. put_text('Hello ...')
  644. await run_asyncio_coroutine(asyncio.sleep(1)) # 无法直接 await asyncio.sleep(1)
  645. put_text('... World!')
  646. app = Flask(__name__)
  647. app.add_url_rule('/hello', 'webio_view', webio_view(hello_word),
  648. methods=['GET', 'POST', 'OPTIONS'])
  649. # 事件循环线程
  650. threading.Thread(target=run_event_loop, daemon=True).start()
  651. app.run(host='localhost', port=80)
  652. 最后,使用PyWebIO编写的协程函数不支持Script模式,总是需要使用 ``start_server`` 来启动一个服务或者集成进Web框架来调用。
  653. Last but not least
  654. ---------------------
  655. 以上就是PyWebIO的全部功能了,你可以继续阅读接下来的文档,或者立即开始PyWebIO应用的编写了。
  656. 最后再提供一条建议,当你在使用PyWebIO遇到设计上的问题时,可以问一下自己:如果在是在终端程序中我会怎么做?
  657. 如果你已经有答案了,那么在PyWebIO中一样可以使用这样的方式完成。如果问题依然存在或者觉得解决方案不够好,
  658. 你可以考虑使用 `put_buttons() <pywebio.output.put_buttons>` 提供的回调机制。
  659. 好了,Have fun with PyWebIO!