guide.rst 39 KB

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