guide.rst 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. User's guide
  2. ============
  3. 输入
  4. ------------
  5. 输入函数都定义在 :doc:`pywebio.input </input>` 模块中,可以使用 ``from pywebio.input import *`` 引入。
  6. 基本输入
  7. ^^^^^^^^^^^
  8. 首先是一些基本类型的输入
  9. 文本输入::
  10. age = input("How old are you?", type=NUMBER) # type can be in {TEXT, NUMBER, PASSWORD}
  11. 这样一行代码的效果如下,浏览器会弹出一个文本输入框来获取输入,在表单被提交之前,``input`` 函数不会返回。
  12. 一些其他类型的输入::
  13. # 下拉选择框::
  14. gift = select('Which gift you want?', ['keyboard', 'ipad'])
  15. # CheckBox::
  16. agree = checkbox("用户协议", options=['I agree to terms and conditions'])
  17. # Text Area::
  18. text = textarea('Text Area', rows='3', placeholder='Some text')
  19. # 文件上传
  20. img = file_upload("Select a image:", accept="image/*")
  21. 输入选项
  22. ^^^^^^^^^^^
  23. 输入函数可指定的参数非常丰富(全部参数及含义请见 :doc:`函数文档 </input>` )::
  24. input('This is label', type=TEXT, placeholder='This is placeholder', help_text='This is help text', required=True)
  25. 则将在浏览器上显示如下:
  26. .. image:: /assets/input_1.png
  27. 我们可以为输入指定校验函数,校验函数校验通过时返回None,否则返回错误消息::
  28. def check_age(p): # 检验函数校验通过时返回None,否则返回错误消息
  29. if p < 10:
  30. return 'Too young!!'
  31. if p > 60:
  32. return 'Too old!!'
  33. age = input("How old are you?", type=NUMBER, valid_func=check_age)
  34. 当用户输入了不合法的值时,页面上的显示如下:
  35. .. image:: /assets/input_2.png
  36. :func:`pywebio.input.textarea` 还支持使用 `Codemirror <https://codemirror.net/>`_ 实现代码风格的编辑区,只需使用 ``code`` 参数传入Codemirror支持的选项即可(最简单的情况是直接传入 ``code={}`` 或 ``code=True``)::
  37. code = textarea('Code Edit', code={
  38. 'mode': "python", # 编辑区代码语言
  39. 'theme': 'darcula', # 编辑区darcula主题, Visit https://codemirror.net/demo/theme.html#cobalt to get more themes
  40. }, value='import something\n# Write your python code')
  41. 文本框的显示效果为:
  42. .. image:: /assets/codemirror_textarea.png
  43. :ref:`这里 <codemirror_options>` 列举了一些常用的Codemirror选项
  44. 更多Codemirror选项请见:https://codemirror.net/doc/manual.html#config
  45. 输入组
  46. ^^^^^^^
  47. PyWebIO还支持一组输入, 返回结果为一个字典。`pywebio.input.input_group()` 接受单项输入组成的列表作为参数,同时为了在返回的结果中区别出每一项输入,还需要在单项输入函数中传入name参数,input_group返回的字典就是以单项输入函数中的name作为键::
  48. data = input_group("Basic info",[
  49. input('Input your name', name='name'),
  50. input('Input your age', name='age', type=NUMBER, valid_func=check_age)
  51. ], valid_func=check_form)
  52. print(data['name'], data['age'])
  53. 输入组中同样支持设置校验函数,其接受整个表单数据作为参数::
  54. def check_form(data): # 检验函数校验通过时返回None,否则返回 (input name,错误消息)
  55. if len(data['name']) > 6:
  56. return ('name', '名字太长!')
  57. if data['age'] <= 0:
  58. return ('age', '年龄不能为负数!')
  59. .. note::
  60. PyWebIO 根据是否在输入函数中传入 ``name`` 参数来判断输入函数是在 `input_group` 中还是被单独调用。
  61. 所以当你想要单独调用一个输入函数时,请不要设置 ``name`` 参数;而在 `input_group` 中调用输入函数时,**务必提供** ``name`` 参数
  62. 输出
  63. ------------
  64. 下文介绍的输出函数都定义在 :doc:`pywebio.output </output>` 模块中,可以使用 ``from pywebio.output import *`` 引入。
  65. 基本输出
  66. ^^^^^^^^^^^^^^
  67. PyWebIO也提供了一些便捷函数来输出表格,链接等格式::
  68. # 文本输出
  69. put_text("Hello world!")
  70. # 表格输出
  71. put_table([
  72. ['商品', '价格'],
  73. ['苹果', '5.5'],
  74. ['香蕉', '7'],
  75. ])
  76. # Markdown输出
  77. put_markdown('~~删除线~~')
  78. # 文件输出
  79. put_file('hello_word.txt', b'hello word!')
  80. 所有输出内容的函数名都以 ``put_`` 开始
  81. PyWebIO提供的全部输出函数请见 :doc:`pywebio.output </output>` 模块
  82. 输出事件回调
  83. ^^^^^^^^^^^^^^
  84. PyWebIO把程序与用户的交互分成了输入和输出两部分:输入函数为阻塞式调用,在用户提交表单之前将不会返回;对输出函数的调用将会立刻将内容输出至浏览器。
  85. 这非常符合控制台程序的编写逻辑。但PyWebIO能做的还远远不止这些,PyWebIO还允许你输出一些控件,当控件被点击时执行提供的回调函数,就像编写GUI程序一样。
  86. 下面是一个例子::
  87. from functools import partial
  88. def edit_row(choice, row):
  89. put_text("You click %s button ar row %s" % (choice, row))
  90. put_table([
  91. ['Idx', 'Actions'],
  92. [1, table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=1))],
  93. [2, table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=2))],
  94. [3, table_cell_buttons(['edit', 'delete'], onclick=partial(edit_row, row=3))],
  95. ])
  96. `put_table() <pywebio.output.put_table>` 的调用不会阻塞。当用户点击了某行中的按钮时,PyWebIO会自动调用相应的处理函数:
  97. .. image:: /assets/table_onclick.*
  98. 当然,PyWebIO还支持单独的按钮控件::
  99. def btn_click(btn_val):
  100. put_text("You click btn_val button" % btn_val)
  101. put_buttons(['A', 'B', 'C'], onclick=btn_click)
  102. 锚点
  103. ^^^^^^^^^^^^^^
  104. 就像在控制台输出文本一样,PyWebIO默认在页面的末尾输出各种内容,你可以使用锚点来改变这一行为。
  105. 你可以调用 `set_anchor(name) <pywebio.output.set_anchor>` 对当前输出位置进行标记。
  106. 你可以在任何输出函数中使用 ``before`` 参数将内容插入到指定的锚点之前,也可以使用 ``after`` 参数将内容插入到指定的锚点之后。
  107. 你也可以在输出函数中传入 ``anchor`` 参数为输出的内容打上锚点。
  108. 以下代码展示了在输出函数中使用锚点::
  109. set_anchor('top')
  110. put_text('A')
  111. put_text('B', anchor='b')
  112. put_text('C', after='top')
  113. put_text('D', before='b')
  114. 以上代码将输出::
  115. C
  116. A
  117. D
  118. B
  119. PyWebIO还提供了以下锚点控制函数:
  120. * `set_anchor(anchor) <pywebio.output.set_anchor>` 可以清除 ``anchor`` 锚点之前输出的内容
  121. * `clear_after(anchor) <pywebio.output.clear_after>` 可以清除 ``anchor`` 锚点之后输出的内容
  122. * `clear_range(start_anchor, end_anchor) <pywebio.output.clear_range>` 可以清除 ``start_anchor`` 到 ``end_anchor`` 锚点之间的内容
  123. * `scroll_to(anchor) <pywebio.output.scroll_to>` 可以将页面滚动到 ``anchor`` 锚点处
  124. 页面环境设置
  125. ^^^^^^^^^^^^^^
  126. **输出区外观**
  127. PyWebIO支持两种外观:输出区固定高度/可变高度。
  128. 可以通过调用 `set_output_fixed_height(True) <pywebio.output.set_output_fixed_height>` 来开启输出区固定高度。
  129. **设置页面标题**
  130. 调用 `set_title(title) <pywebio.output.set_title>` 可以设置页面标题。
  131. **自动滚动**
  132. 在不指定锚点进行输出时,PyWebIO默认在输出完毕后自动将页面滚动到页面最下方;在调用输入函数时,也会将页面滚动到表单处。
  133. 通过调用 `set_auto_scroll_bottom(False) <pywebio.output.set_auto_scroll_bottom>` 来关闭自动滚动。
  134. 两种运行模式:Server mode & Script mode
  135. ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  136. 在 :ref:`Hello, world <hello_word>` 一节中,已经知道,PyWebIO支持在普通的脚本中调用和使用
  137. `start_server() <pywebio.platform.start_server>` 启动一个Web服务两种模式。
  138. Server mode 下,需要提供一个任务函数来为每个用户提供服务,当用户访问服务地址时,PyWebIO会开启一个新会话并运行任务函数。
  139. 在任务函数外不能调用PyWebIO的交互函数,但是在由任务函数调用的其他函数内依然可以调用PyWebIO的交互函数。
  140. 在调用 ``start_server()`` 启动Web服务之前,不允许调用任何PyWebIO的交互函数。
  141. 比如如下调用是 **不被允许的** ::
  142. import pywebio
  143. from pywebio.input import input
  144. port = input('Input port number:')
  145. pywebio.start_server(some_func(), port=int(port))
  146. Script mode 下,在任何位置都可以调用PyWebIO的交互函数。
  147. 如果用户在会话结束之前关闭了浏览器,那么之后会话内对于PyWebIO交互函数的调用将会引发一个 ``SessionException`` 异常。
  148. 并发
  149. ^^^^^^^^^^^^^^
  150. PyWebIO 支持在多线程环境中使用。
  151. **Script mode**
  152. 在 Script mode 下,你可以自由地启动线程,并在其中调用PyWebIO的交互函数。当所有非 `Daemon线程 <https://docs.python.org/3/library/threading.html#thread-objects>`_ 运行结束后,脚本退出。
  153. **Server mode**
  154. Server mode 下,由于对多会话的支持,如果需要在新创建的线程中使用PyWebIO的交互函数,需要手动调用 `register_thread(thread) <pywebio.session.register_thread>` 对新进程进行注册。
  155. 如果新创建的线程中没有使用到PyWebIO的交互函数,则无需注册。
  156. 当当前会话的任务函数和会话内通过 `register_thread(thread) <pywebio.session.register_thread>` 注册的线程都结束运行时,会话关闭。
  157. 与Web框架集成
  158. ^^^^^^^^^^^^^^
  159. .. _integration_web_framework:
  160. PyWebIO 目前支持与Flask和Tornado Web框架的集成。
  161. 与Web框架集成需要完成两件事情:托管PyWebIO静态文件;暴露PyWebIO后端接口。
  162. 这其中需要注意静态文件和后端接口的路径约定,以及静态文件与后端接口分开部署时因为跨域而需要的特别设置。
  163. **与Tornado集成**
  164. 要将使用`PyWebIO`编写的任务函数集成进Tornado应用,需要在Tornado应用中引入两个 ``RequestHandler`` ,
  165. 一个 ``RequestHandler`` 用来提供静态的前端文件,另一个 ``RequestHandler`` 用来和浏览器进行WebSocket通讯::
  166. import tornado.ioloop
  167. import tornado.web
  168. from pywebio.platform.tornado import webio_handler
  169. from pywebio import STATIC_PATH
  170. class MainHandler(tornado.web.RequestHandler):
  171. def get(self):
  172. self.write("Hello, world")
  173. if __name__ == "__main__":
  174. application = tornado.web.Application([
  175. (r"/", MainHandler),
  176. (r"/tool/io", webio_handler(task_func)), # task_func 为使用PyWebIO编写的任务函数
  177. (r"/tool/(.*)", tornado.web.StaticFileHandler, {"path": STATIC_PATH, 'default_filename': 'index.html'})
  178. ])
  179. application.listen(port=80, address='localhost')
  180. tornado.ioloop.IOLoop.current().start()
  181. 以上代码调用 `webio_handler(task_func) <pywebio.platform.webio_handler>` 来获得和浏览器进行通讯的Tornado ``RequestHandler`` ,
  182. 并将其绑定在 ``/tool/io`` 路径下;同时将PyWebIO的静态文件使用``tornado.web.StaticFileHandler`` 托管到 ``/tool/(.*)`` 路径下。
  183. 启动Tornado服务后,访问 ``http://localhost/tool/`` 即可使用PyWebIO服务
  184. .. note::
  185. 在Tornado中,PyWebIO使用WebSocket协议和浏览器进行通讯,所以,如果你的Tornado应用处在反向代理(比如Nginx)之后,
  186. 可能需要特别配置反向代理来支持WebSocket协议,:ref:`这里 <nginx_ws_config>` 有一个Nginx配置WebSocket的例子。
  187. **与Flask集成**
  188. 和集成到Tornado相似,在与Flask集成的集成中,你也需要添加两个PyWebIO相关的路由:一个用来提供静态的前端文件,另一个用来和浏览器进行Http通讯::
  189. from pywebio.platform.flask import webio_view
  190. from pywebio import STATIC_PATH
  191. from flask import Flask, send_from_directory
  192. app = Flask(__name__)
  193. app.route('/io', methods=['GET', 'POST', 'OPTIONS'])(webio_view(task_func))
  194. @app.route('/')
  195. @app.route('/<path:static_file>')
  196. def serve_static_file(static_file='index.html'):
  197. return send_from_directory(STATIC_PATH, static_file)
  198. app.run(host='localhost', port=80)
  199. .. _integration_web_framework_note:
  200. **注意事项**
  201. PyWebIO默认通过当前页面的同级的 ``./io`` API与后端进行通讯,比如如果你将PyWebIO静态文件托管到 ``/A/B/C/(.*)`` 路径下,那么你需要将
  202. ``webio_handler()`` 返回的 ``RequestHandler`` 绑定到 ``/A/B/C/io`` 处。如果你没有这样做的话,你需要在打开PyWebIO前端页面时,
  203. 传入 ``_pywebio_addr`` Url参数来指定PyWebIO后端API地址,比如 ``/A/B/C/?_pywebio_addr=/D/pywebio`` 将PyWebIO后端API地址设置到了
  204. ``/D/pywebio`` 处。 ``_pywebio_addr`` 参数可以使用相对地址、绝对地址甚至指定其他服务器。
  205. 如果你不想自己托管静态文件,你可以使用PyWebIO的Github Page页面,只需要在页面上通过 ``_pywebio_addr`` 参数传入后端API地址就可以了。
  206. .. caution::
  207. 需要注意 ``_pywebio_addr`` 参数的格式:
  208. 相对地址可以为 ``./xxx/xxx`` 或 ``xxx/xxx`` 的格式
  209. 绝对地址以 ``/`` 开头,比如 ``/aaa/bbb``
  210. 指定其他服务器需要使用完整格式: ``ws://example.com:8080/aaa/io`` ,或者省略协议字段: ``//example.com:8080/aaa/io`` 。
  211. 省略协议字段时,PyWebIO根据当前页面的协议确定要使用的协议: 若当前页面为http协议,则后端接口为ws协议;若当前页面为https协议,则后端接口为wss协议;
  212. 当后端API与当前页面不再同一host下时,需要在 `webio_handler() <pywebio.platform.webio_handler>` 或
  213. `webio_view() <pywebio.platform.flask.webio_view>` 中使用 ``allowed_origins`` 或 ``check_origin``
  214. 参数来允许后端接收页面所在的host
  215. .. _coroutine_based_session:
  216. 基于协程的会话
  217. ^^^^^^^^^^^^^^
  218. PyWebIO的会话实现默认是基于线程的,用户每打开一个和服务端的会话连接,PyWebIO会启动一个线程来运行任务函数,你可以在会话中启动新的线程,通过 `register_thread(thread) <pywebio.session.register_thread>` 注册新创建的线程后新线程中也可以调用PyWebIO交互函数,当任务函数返回并且会话内所有的通过 `register_thread(thread) <pywebio.session.register_thread>` 注册的线程都退出后,会话结束。
  219. 除了基于线程的会话,PyWebIO还提供了基于协程的会话。基于协程的会话接受一个协程作为任务函数。
  220. 基于线程的会话为单线程模型,所有会话都运行在一个线程内。对于IO密集型的任务,协程比线程有更少的资源占用同时又拥有媲美于线程的性能。
  221. 要使用基于协程的会话,只需要在 `start_server() <pywebio.platform.start_server>` 中传入使用 ``async`` 声明的协程函数即可::
  222. from pywebio.input import *
  223. from pywebio.output import *
  224. from pywebio import start_server
  225. async def say_hello():
  226. name = await input("what's your name?")
  227. put_text('Hello, %s'%name)
  228. start_server(say_hello, auto_open_webbrowser=True)
  229. 在协程任务函数中,你可以使用 ``await`` 调用其他协程,也可以调用 `asyncio <https://docs.python.org/3/library/asyncio.html>`_ 库中的协程函数::
  230. import asyncio
  231. async def hello_word():
  232. put_text('Hello ...')
  233. await asyncio.sleep(1)
  234. put_text('... World!')
  235. async def main():
  236. await hello_word()
  237. put_text('Bye, bye')
  238. start_server(main, auto_open_webbrowser=True)
  239. 在基于协程的会话中,你可以启动线程,但是无法像基于线程的会话那样使用 `register_thread() <pywebio.session.register_thread>` 函数来使得在新线程内使用PyWebIO交互函数。
  240. 但你可以使用 `run_async(coro) <pywebio.session.run_async>` 来异步执行一个协程,新协程内可以使用PyWebIO交互函数::
  241. from pywebio.session import run_async
  242. async def counter(n):
  243. for i in range(n):
  244. put_text(i)
  245. await asyncio.sleep(1)
  246. async def main():
  247. run_async(counter(10))
  248. put_text('Bye, bye')
  249. start_server(main, auto_open_webbrowser=True)
  250. `run_async(coro) <pywebio.session.run_async>` 返回一个 `TaskHandle <pywebio.session.coroutinebased.TaskHandle>` ,通过 ``TaskHandle`` 你可以查询协程运行状态和关闭协程。
  251. 与基于线程的会话类似,在基于协程的会话中,当任务函数和在会话内通过 ``run_async()`` 运行的协程全部结束后,会话关闭。
  252. .. note::
  253. 在基于协程的会话中, :doc:`pywebio.input </input>` 模块中的输入函数都需要使用 ``await`` 语法来获取返回值,
  254. 忘记使用 ``await`` 将会是在使用基于协程的会话常常犯的错误。
  255. 与Web框架进行集成
  256. """""""""""""""""""""
  257. 基于协程的会话同样可以与Web框架进行集成,只需要在原来传入任务函数的地方改为传入协程函数即可。
  258. 但当前在使用基于协程的会话集成进Flask时,存在一些限制:
  259. 一是协程函数内还无法直接通过 ``await`` 直接调用asyncio库中的协程函数,目前需要使用
  260. `run_asyncio_coroutine() <pywebio.session.run_asyncio_coroutine>` 进行包装。二是,在启动Flask服务器之前需要启动一个单独的线程来运行事件循环。
  261. 使用基于协程的会话集成进Flask的示例::
  262. import asyncio
  263. import threading
  264. from flask import Flask, send_from_directory
  265. from pywebio import STATIC_PATH
  266. from pywebio.output import *
  267. from pywebio.platform.flask import webio_view, _setup_event_loop
  268. from pywebio.session import run_asyncio_coroutine
  269. async def hello_word():
  270. put_text('Hello ...')
  271. await run_asyncio_coroutine(asyncio.sleep(1))
  272. put_text('... World!')
  273. app = Flask(__name__)
  274. app.route('/io', methods=['GET', 'POST', 'OPTIONS'])(
  275. webio_view(hello_word)
  276. )
  277. @app.route('/')
  278. @app.route('/<path:static_file>')
  279. def serve_static_file(static_file='index.html'):
  280. return send_from_directory(STATIC_PATH, static_file)
  281. threading.Thread(target=_setup_event_loop, daemon=True).start()
  282. app.run(host='localhost', port='80')
  283. 最后,使用PyWebIO编写的协程函数不支持Script mode,总是需要使用 ``start_server`` 来启动一个服务或者集成进Web框架来调用。