Browse Source

doc: fix mistakes and add content

wangweimin 4 years ago
parent
commit
7a90b90612

+ 161 - 1
docs/arch.rst

@@ -4,5 +4,165 @@ Architecture
 概念
 ------------
 
-`Session` 表示浏览器与程序交互产生的一次会话。PyWebIO在会话中运行 ``Task`` ,任务是
+``Session`` 表示浏览器与程序交互产生的一次会话。PyWebIO在会话中运行 ``Task`` ,任务是
+
+会话中除了起始的执行单元,也可以并发启动新的执行单元,在新的执行单元中也可以进行输入输出。
+
+在用户端,相同会话中的不同的执行单元的输入是独立的,共享输出空间,但输出域的栈结构各自独立。
+
+若用户正在填写一个执行单元的表单,会话中的其他执行单元也开始向用户请求输入,此时用户正在填写的表单将会隐藏,
+新的输入表单将会显示给用户,当用户填写完新表单并提交后,旧表单重新显示,之前在旧表单上的输入也会保留。
+
+在基于线程的会话中,会话中的每个执行单元都是一个线程
+
+在基于协程的会话中,会话中的每个执行单元都是一个协程
+
+除了并发执行的执行单元,会话中还有事件回调函数,目前就只有按钮控件可以绑定点击事件的回调函数。
+
+架构
+------------
+
+会话内的每个执行单元使用唯一的task_id进行标识,由于会话内的输入需要区分执行单元,所以每个表单提交时,
+除了表单的内容以外,还会携带表单所在的执行单元的task_id,这样,后台会话才可以知道该将表单数据传递给哪个执行单元。
+
+
+.. image:: /assets/architecture.png
+
+PyWebIO会话是由事件驱动的,这些事件来自用户在页面上的操作,比如提交表单,点击按钮,这些事件会通过http请求或websocket连接发送到后端框架。
+
+后端框架维护有当前在线的Session实例,后端框架在收到用户提交的事件后,回调用相关Session实例的 ``send_client_event()`` 方法将事件发送至会话;
+
+一个会话内会拥有至少一个执行单元,执行单元在调用PyWebIO的输入函数后会临时挂起,当会话收到用户的输入提交后,会话便将执行单元恢复执行,并提供用户输入的值。
+执行单元内,任何输入输出的调用都会转换成一些命令序列发送给会话.
+
+当后端框架通过HTTP与用户浏览器通信时,用户浏览器是以轮训的方式获取指令,会话会保存由执行单元生成的、还未发送到浏览器的命令序列,等待下次轮训时由后端框架取走。
+
+当后端框架通过WebSocket与用户建立连接时,任何由执行单元发送到会话的命令都会立即发送到后端,并由后端通过WebSocket连接通知用户浏览器。
+
+实现
+------------
+
+后端与Session的交互
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+后端框架负责从Session会话中获取来自PyWebIO的指令,并发送给用户浏览器;同时后端框架接收用户提交的数据,并发送给相应的会话实例。
+
+Session暴露给后端框架的方法仅有 `Session.send_client_event <pywebio.session.base.Session.send_client_event>` 、 `Session.get_task_commands <pywebio.session.base.Session.get_task_commands>` 和 `Session.close <pywebio.session.base.Session.close>` 。
+
+
+基于HTTP通信的后端的实现逻辑
+"""""""""""""""""""""""""""""""""
+
+**基于HTTP的前后端通信约定**
+
+前端按照固定间隔使用GET请求轮训后端接口,在请求中使用 ``webio-session-id`` HTTP头来传递会话ID。
+
+会话一开始时,会话ID由后端生成并通过响应中的 ``webio-session-id`` HTTP头返回给前端,后续前端的请求都会在请求头中使用 ``webio-session-id`` 字段传递会话ID。
+
+前端产生的事件使用POST请求发送给后端。对于前端的每次轮训和事件提交请求,后端都会返回当前未执行的指令序列作为响应,前端收到响应后会依次执行指令。
+
+**代码实现**
+
+以Flask后端为例,Flask后端与Session的交互都在Flask视图函数中实现,视图函数通过调用 `pywebio.platform.flask.webio_view <./_modules/pywebio/platform/flask.html#webio_view>`_ 获取,
+在 `webio_view` 中,先是实例化了一个 `pywebio.platform.httpbased.HttpHandler` ,然后声明了一个内部函数,这个内部函数就是Flask视图函数,
+在视图函数内,先是实例化了一个 `pywebio.platform.flask.FlaskHttpContext` 对象,然后通过调用 ``HttpHandler.handle_request(FlaskHttpContext)`` 就获得了视图的响应。
+
+这其中,FlaskHttpContext 的基类为 HttpContext ,HttpContext 接口各异的后端框架定义了一个统一的操作接口,用于从当前请求中获取请求相关的数据并设置请求的相应。 FlaskHttpContext 为HttpContext接口的Flask实现。
+
+而 HttpHandle 负责维护Session实例并实现HTTP请求与Session之间的交互,HttpHandle 与后端框架相关的交互全都通过 HttpContext 操作。
+
+HttpContext的生命周期为一次HTTP请求,HttpHandle的生命周期和整个后端框架的生命周期一致。
+
+HttpHandler.handle_request 负责处理前端发送给后端的每一次请求,HttpHandler.handle_request 的处理流程如下:
+
+1. 检测当前HTTP请求是否满足跨域设置
+2. 根绝当前请求的 webio-session-id 头信息找到相应的Session实例,若不存在 webio-session-id 头则创建新会话并分配webio-session-id
+3. 若当前请求为POST事件提交请求,则将提交的数据通过 Session.send_client_event 发送给Session
+4. 通过调用 Session.get_task_commands 获取待执行的指令序列,并通过 HttpContext 向后端设置响应数据
+
+此外,基于HTTP的会话,用户主动关闭会话时(比如关闭浏览器),后端无法立即感知,所以在HttpHandler.handle_request 中,
+还会周期性地检测会话的最后活跃时间,将一段时间内不活跃的会话视为过期,所以在HttpHandler清理过期会话并调用 Session.close 释放会话内的资源。
+
+
+基于WebSocket通信的后端的实现逻辑
+""""""""""""""""""""""""""""""""""""
+**基于WebSocket的前后端通信约定:**
+
+浏览器与后端使用一个WebSocket连接来保持一个会话,后端的指令通过JSON序列化之后的消息实时发送给前端,前端用户触发的事件数据也通过JSON序列化之后发送给后端。
+
+**代码实现**
+
+以Tornado后端为例
+
+webio_handler用于获取Tornado与前端进行通信的WebSocketHandler子类,其逻辑实现在 _webio_handler 中,由于WebSocket的有状态性,
+WebSocketHandler子类的实现比基于HTTP通信的HttpHandler要简单许多,关键部分如下:
+
+* 在WebSocket连接创建的时候初始化Session实例,并向Session对象注册了 on_task_command和on_session_close 回调,分别在新指令产生时和会话由执行单元关闭时由Session调用,
+  用于实现WebSocketHandler向前端实时发送指令
+* 在收到前端浏览器发送来的消息后,WebSocketHandler将收到的数据通过 Session.send_client_event 发送给Session
+* 在WebSocket连接关闭时,调用 Session.close 释放会话内的资源。
+
+session与执行单元(输入/输出)的交互
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+会话提供给执行单元的关键接口有:
+
+* get_current_session : 静态方法,获取当前执行单元所属的会话实例
+* get_current_task_id : 静态方法,获取当前执行单元所属的id
+* send_task_command : 向会话发送指令
+* next_client_event : 读取来自浏览器的属于当前执行单元的下一个事件
+* register_callback : 向会话注册一个回调
+
+同时,会话根据实现方式不同,还分别提供了 register_thread 和 run_async 用于启动新的执行单元。
+
+
+**回调机制**
+
+在会话中,为了能够响应用户在界面上的某些事件(比如点击了输出内容中的某个按钮),于是设计了回调机制,可以在执行单元中使用register_callback向当前会话注册回调,然后执行单元会得到一个回调ID,
+执行单元再通过相关指令让浏览器输出一些可以触发的控件,并向控件绑定回调ID,当用户触发控件后,前端将带有回调ID的 :ref:`回调事件 <callback_event>` 发回会话,会话会在专门的执行单元中或启动新执行单元中运行回调。
+
+基于线程的会话实现
+""""""""""""""""""""""""""""""""""""
+
+在基于线程的会话中,每个执行单元都是一个线程,每个执行单元通过一条消息队列从会话接收来自用户的事件消息,当执行单元所需要的事件用户还没有提交时,执行单元便会挂起。
+
+基于线程的会话使用线程ID作为执行单元的ID,在全局使用一个以线程id为key的字典来映射执行单元所属的会话实例,会话内不同执行单元的用户事件消息队列也通过执行单元ID进行索引。
+
+使用 register_thread 启动新的执行单元时,也需要为新执行单元注册用户事件消息队列。
+
+基于协程的会话实现
+""""""""""""""""""""""""""""""""""""
+在基于协程的会话中,每个执行单元都是一个由协程包装成的任务对象(Task),当会话接收来自用户的事件消息后,便激活相应的任务对象,使得协程恢复运行。
+
+由于基于协程的会话是单线程的,所以会话在激活任务对象前是通过将上下文信息保存在全局变量中来实现 get_current_session 和 get_current_task_id 方法,全局的上下文信息包含当前将要执行的会话的实例和执行单元的ID。
+
+
+Script mode的实现
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Script mode 也是基于线程的,但由于全局仅存在一个会话,所有执行单元必定全部属于这个会话,所以也无需主动调用register_thread(thread)注册线程。
+
+当PyWebIO检测到用户代码在后端Server还未启动的情况下就调用了PyWebIO交互函数时,便会启动Script mode:
+
+1. 在新线程中启动后端Server
+2. 启动浏览器打开后端Server运行的地址
+3. 在第一次与用户建立连接时初始化会话
+
+script mode的会话类继承了基于线程的会话类,并修改了部分方法:
+
+* 构造函数 : 仅允许script mode会话类被初始化一次
+* get_current_session : 直接返回全局的会话对象
+* get_current_task_id : 除了返回当前线程id,还会自动将当前线程使用 register_thread 注册到会话中
+
+相关对象的文档
+------------------------
+
+.. autoclass:: pywebio.platform.httpbased.HttpHandler
+   :members:
+
+.. autoclass:: pywebio.platform.flask.FlaskHttpContext
+   :members:
+
+.. autoclass:: pywebio.session.base.Session
+   :members:
+
 

BIN
docs/assets/architecture.png


+ 19 - 1
docs/guide.rst

@@ -602,7 +602,23 @@ Server模式与Script模式
 在Server模式下,PyWebIO会启动一个Web服务来持续性地提供服务。需要提供一个任务函数(类似于Web开发中的视图函数),当用户访问服务地址时,PyWebIO会开启一个新会话并运行任务函数。
 
 使用 `start_server() <pywebio.platform.tornado.start_server>` 来启动PyWebIO的Server模式, `start_server() <pywebio.platform.tornado.start_server>` 除了接收一个函数作为任务函数外,
-还支持传入函数列表或字典,从而使一个PyWebIO Server下可以有多个不同功能的服务,服务之间可以通过 `go_app() <pywebio.session.go_app>` 进行跳转,详细内容见函数文档。
+还支持传入函数列表或字典,从而使一个PyWebIO Server下可以有多个不同功能的服务,服务之间可以通过 `go_app() <pywebio.session.go_app>` 或 `put_link() <pywebio.output.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, task_1, task_2])  # 或 start_server({'index': index, 'task_1': task_1, 'task_2': task_2})
 
 .. attention::
 
@@ -900,6 +916,8 @@ PyWebIO的会话实现默认是基于线程的,用户每打开一个和服务
       await asyncio.gather(asyncio.sleep(1), pywebio.session.eval_js('1+1'))
       task = asyncio.create_task(pywebio.input())
 
+.. _coroutine_based_concurrency:
+
 协程会话的并发
 ^^^^^^^^^^^^^^^^
 

+ 1 - 4
docs/misc.rst

@@ -13,6 +13,7 @@
 * ``tabSize`` (int): 制表符宽度
 * ``lineWrapping`` (bool): 是否换行以显示长行
 
+完整的Codemirror选项请见 https://codemirror.net/doc/manual.html#config
 
 .. _nginx_ws_config:
 
@@ -44,10 +45,6 @@ Nginx WebSocket配置示例
 
 以上配置文件将PyWebIO的静态文件托管到 ``/tool/`` 目录下, 并将 ``/tool/io`` 反向代理到 ``localhost:5000``
 
-.. note::
-    使用以上配置文件后,您还需要在 ``webio_handler`` 或 ``start_server`` 函数中将nginx绑定的域名添加到 ``allowed_origins`` 参数指定的列表中
-
-
 PyWebIO的静态文件的路径可使用命令 ``python3 -c "import pywebio; print(pywebio.STATIC_PATH)"`` 获得,你也可以将静态文件复制到其他目录下::
 
     cp -r `python3 -c "import pywebio; print(pywebio.STATIC_PATH)"` ~/web

+ 4 - 6
docs/spec.rst

@@ -13,7 +13,7 @@ PyWebIO采用服务器-客户端架构,服务端运行任务代码,通过网
 
 **Http 通信:**
 
-* 客户端通过Http GET请求向后端轮,后端返回json序列化之后的PyWebIO消息列表
+* 客户端通过Http GET请求向后端轮,后端返回json序列化之后的PyWebIO消息列表
 
 * 当用户提交表单或者点击页面按钮后,客户端通过Http POST请求向后端提交数据
 
@@ -135,8 +135,7 @@ input_group
 
 * actions
 
-  * buttons: 选项列表。``{label:选项标签, value:选项值, [type: 按钮类型 'submit'/'reset'/'cancel'/'callback'], [disabled:是否禁止选择]}`` .
-    当 type 为 'callback' 时,value 字段表示回调函数的callback_id
+  * buttons: 选项列表。``{label:选项标签, value:选项值, [type: 按钮类型 'submit'/'reset'/'cancel'], [disabled:是否禁止选择]}`` .
 
 * file:
 
@@ -160,8 +159,7 @@ update_input
   * placeholder:
   * invalid_feedback
   * valid_feedback
-  * action_result 仅在 actions 输入项中可用,表示设置输入项显示的文本
-  * 输入项其他spec字段  // 不支持更新 on_focus on_blur inline label 字段
+  * 输入项其他spec字段  // 不支持更新 inline 和 label 字段
 
 
 close_session
@@ -223,7 +221,7 @@ popup
 命令 spec 字段:
 
 * title: 弹窗标题
-* content: 数组,元素为字符串/对象,字符串表示html
+* content: 数组,元素为字符串/对象
 * size: 弹窗窗口大小,可选值: ``large`` 、 ``normal`` 、 ``small``
 * implicit_close: 是否可以通过点击弹窗外的内容或按下 `Esc` 键来关闭弹窗
 * closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗,

+ 25 - 25
pywebio/input.py

@@ -112,7 +112,7 @@ def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=N
     :param str type: 输入类型. 可使用的常量:`TEXT` , `NUMBER` , `FLOAT` , `PASSWORD` , `URL` , `DATE` , `TIME`
 
        其中 `DATE` , `TIME` 类型在某些浏览器上不被支持,详情见 https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#Browser_compatibility
-    :param Callable validate: 输入值校验函数. 如果提供,当用户输入完毕或提交表单后校验函数将被调用.
+    :param callable validate: 输入值校验函数. 如果提供,当用户输入完毕或提交表单后校验函数将被调用.
         ``validate`` 接收输入值作为参数,当输入值有效时,返回 ``None`` ,当输入值无效时,返回错误提示字符串. 比如:
 
         .. exportable-codeblock::
@@ -126,7 +126,7 @@ def input(label='', type=TEXT, *, validate=None, name=None, value=None, action=N
                     return 'Too young'
             input('Input your age', type=NUMBER, validate=check_age)
 
-    :param name: 输入框的名字. 与 `input_group` 配合使用,用于在输入组的结果中标识不同输入项.  **在单个输入中,不可以设置该参数!**
+    :param str name: 输入框的名字. 与 `input_group` 配合使用,用于在输入组的结果中标识不同输入项.  **在单个输入中,不可以设置该参数!**
     :param str value: 输入框的初始值
     :type action: tuple(label:str, callback:callable)
     :param action: 在输入框右侧显示一个按钮,可通过点击按钮为输入框设置值。
@@ -229,9 +229,9 @@ def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, val
              placeholder=None, required=None, readonly=None, help_text=None, **other_html_attrs):
     r"""文本输入域(多行文本输入)
 
-    :param int rows: 输入文本的行数(显示的高度)。输入的文本超出设定值时会显示滚动条
-    :param int maxlength: 允许用户输入的最大字符长度 (Unicode) 。未指定表示无限长度
-    :param int minlength: 允许用户输入的最小字符长度(Unicode)
+    :param int rows: 输入框的最多可显示的文本的行数,内容超出时会显示滚动条
+    :param int maxlength: 最大允许用户输入的字符长度 (Unicode) 。未指定表示无限长度
+    :param int minlength: 最少需要用户输入的字符长度(Unicode)
     :param dict code: 通过提供 `Codemirror <https://codemirror.net/>`_ 参数让文本输入域具有代码编辑器样式:
 
         .. exportable-codeblock::
@@ -244,7 +244,7 @@ def textarea(label='', *, rows=6, code=None, maxlength=None, minlength=None, val
             })
             put_code(res, language='python')  # ..demo-only
 
-        更多配置可以参考 https://codemirror.net/doc/manual.html#config
+        :ref:`这里 <codemirror_options>` 列举了一些常用的Codemirror选项
     :param - label, validate, name, value, placeholder, required, readonly, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
     :return: 用户输入的文本
     """
@@ -289,7 +289,7 @@ def select(label='', options=None, *, multiple=None, validate=None, name=None, v
            help_text=None, **other_html_attrs):
     r"""下拉选择框。
 
-    默认单选,设置 ``multiple`` 参数后,可以多选。但都至少要选择一个选项。
+    默认单选,可以通过设置 ``multiple`` 参数来允许多选
 
     :param list options: 可选项列表。列表项的可用形式有:
 
@@ -299,7 +299,7 @@ def select(label='', options=None, *, multiple=None, validate=None, name=None, v
 
         注意:
 
-        1. ``options`` 中的 ``value`` 可以为任意可Json序列化对象
+        1. ``options`` 中的 ``value`` 可以为任意可JSON序列化对象
         2. 若 ``multiple`` 选项不为 ``True`` 则可选项列表最多仅能有一项的 ``selected`` 为 ``True``。
 
     :param bool multiple: 是否可以多选. 默认单选
@@ -307,9 +307,9 @@ def select(label='', options=None, *, multiple=None, validate=None, name=None, v
        你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
        最终选中项为 ``value`` 参数和 ``options`` 中设置的并集。
     :type value: list or str
-    :param bool required: 是否至少选择一项
+    :param bool required: 是否至少选择一项,仅在 ``multiple=True`` 时可用
     :param - label, validate, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
-    :return: 如果 ``multiple=True`` 时,返回用户选中的 ``options`` 中的值的列表;不设置 ``multiple`` 时,返回用户选中的 ``options`` 中的值
+    :return: 如果 ``multiple=True`` 时,返回用户选中的 ``options`` 中的值的列表;否则,返回用户选中的 ``options`` 中的值
     """
     assert options is not None, 'Required `options` parameter in select()'
 
@@ -326,7 +326,7 @@ def checkbox(label='', options=None, *, inline=None, validate=None, name=None, v
              **other_html_attrs):
     r"""勾选选项。可以多选,也可以不选。
 
-    :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
+    :param list options: 可选项列表。格式与 `select()` 函数的 ``options`` 参数
     :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
     :param list value: 勾选选项初始选中项。为选项值的列表。
        你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
@@ -349,13 +349,13 @@ def radio(label='', options=None, *, inline=None, validate=None, name=None, valu
           help_text=None, **other_html_attrs):
     r"""单选选项
 
-    :param list options: 可选项列表。格式与 `select` 函数的 ``options`` 参数含义一致
+    :param list options: 可选项列表。格式与 `select()` 函数的 ``options`` 参数
     :param bool inline: 是否将选项显示在一行上。默认每个选项单独占一行
     :param str value: 单选选项初始选中项的值。
        你也可以通过设置 ``options`` 列表项中的 ``selected`` 字段来设置默认选中选项。
-    :param bool required: 是否至少选择一项
+    :param bool required: 是否一定要选择一项(默认条件下用户可以不选择任何选项)
     :param - label, validate, name, help_text, other_html_attrs: 与 `input` 输入函数的同名参数含义一致
-    :return: 用户选中的选项的值
+    :return: 用户选中的选项的值, 如果用户没有选任何值,返回 ``None``
     """
     assert options is not None, 'Required `options` parameter in radio()'
 
@@ -409,16 +409,14 @@ def actions(label='', buttons=None, name=None, help_text=None):
 
     在表单上显示为一组按钮,用户点击按钮后依据按钮类型的不同有不同的表现。
 
-    当 ``actions()`` 作为 `input_group()` 的 ``inputs`` 中最后一个输入项,并且输入项中含有 ``type=submit`` 的按钮时,表单默认的提交按钮会被当前 ``actions()`` 替换
+    :param list buttons: 按钮列表。列表项的可用形式有:
 
-    :param list buttons: 选项列表。列表项的可用形式有:
-
-        * dict: ``{label:选项标签, value:选项值, [type: 按钮类型], [disabled:是否禁止选择]}`` .
+        * dict: ``{label:按钮标签, value:按钮值, [type: 按钮类型], [disabled:是否禁止选择]}`` .
           若 ``type='reset'/'cancel'`` 或 ``disabled=True`` 可省略 ``value``
         * tuple or list: ``(label, value, [type], [disabled])``
         * 单值: 此时label和value使用相同的值
 
-       其中, ``value`` 可以为任意可json序列化的对象。 ``type`` 可选值为:
+       其中, ``value`` 可以为任意可JSON序列化的对象。 ``type`` 可选值为:
 
         * ``'submit'`` : 点击按钮后,立即将整个表单提交,最终表单中本项的值为被点击按钮的 ``value`` 值。 ``'submit'`` 为 ``type`` 的默认值
         * ``'cancel'`` : 取消输入。点击按钮后,立即将整个表单提交,表单值返回 ``None``
@@ -426,9 +424,11 @@ def actions(label='', buttons=None, name=None, help_text=None):
           注意:点击 ``type=reset`` 的按钮后,并不会提交表单, ``actions()`` 调用也不会返回
 
     :param - label, name, help_text: 与 `input` 输入函数的同名参数含义一致
-    :return: 若用户点击点击 ``type=submit`` 按钮进行表单提交,返回用户点击的按钮的值;若用户点击点击 ``type=callback`` 按钮,返回值通过回调函数设置;
+    :return: 若用户点击点击 ``type=submit`` 按钮进行表单提交,返回用户点击的按钮的值;
        若用户点击 ``type=cancel`` 按钮或通过其它方式提交表单,则返回 ``None``
 
+    当 ``actions()`` 作为 `input_group()` 中的最后一个输入项、并且含有 ``type='submit'`` 的按钮时,`input_group()` 表单默认的提交按钮会被当前 ``actions()`` 替换
+
     **actions使用场景**
 
     .. _custom_form_ctrl_btn:
@@ -500,12 +500,12 @@ def file_upload(label='', accept=None, name=None, placeholder='Choose file', mul
                 max_total_size=0, required=None, help_text=None, **other_html_attrs):
     r"""文件上传。
 
-    :param accept: 单值或列表, 表示可接受的文件类型。单值或列表项支持的形式有:
+    :param accept: 单值或列表, 表示可接受的文件类型。文件类型的可用形式有:
 
         * 以 ``.`` 字符开始的文件扩展名(例如:``.jpg, .png, .doc``)。
-          注意:截本文档编写之时,微信内置浏览器还不支持这种语法
+          注意:截本文档编写之时,微信内置浏览器还不支持这种语法
         * 一个有效的 MIME 类型。
-          例如: ``application/pdf`` 、 ``audio/*`` 表示音频文件、``video/*`` 表示视频文件、``image/*`` 表示图片文件
+          例如: ``application/pdf`` 、 ``audio/*`` 表示音频文件、``video/*`` 表示视频文件、``image/*`` 表示图片文件
           参考 https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
 
     :type accept: str or list
@@ -513,7 +513,7 @@ def file_upload(label='', accept=None, name=None, placeholder='Choose file', mul
     :param bool multiple: 是否允许多文件上传
     :param int/str max_size: 单个文件的最大大小,超过限制将会禁止上传。默认为0,表示不限制上传文件的大小。
 
-       可以为数字表示的字节数,或以 `K` / `M` / `G` 结尾表示的字符串(分别表示 千字节、兆字节、吉字节,大小写不敏感)。例如:
+       ``max_size`` 值可以为数字表示的字节数,或以 `K` / `M` / `G` 结尾表示的字符串(分别表示 千字节、兆字节、吉字节,大小写不敏感)。例如:
        ``max_size=500`` , ``max_size='40K'`` , ``max_size='3M'``
 
     :param int/str max_total_size: 所有文件的最大大小,超过限制将会禁止上传。仅在 ``multiple=True`` 时可用,默认不限制上传文件的大小。 格式同 ``max_size`` 参数
@@ -563,7 +563,7 @@ def input_group(label='', inputs=None, validate=None, cancelable=False):
 
     :param str label: 输入组标签
     :param list inputs: 输入项列表。列表的内容为对单项输入函数的调用,并在单项输入函数中传入 ``name`` 参数。
-    :param Callable validate: 输入组校验函数。
+    :param callable validate: 输入组校验函数。
         函数签名:``callback(data) -> (name, error_msg)``
         ``validate`` 接收整个表单的值为参数,当校验表单值有效时,返回 ``None`` ,当某项输入值无效时,返回出错输入项的 ``name`` 值和错误提示. 比如:
 

+ 114 - 49
pywebio/output.py

@@ -196,9 +196,10 @@ def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTO
     """创建一个新的scope.
 
     :param str name: scope名
-    :param int/str container_scope: 此scope的父scope. 可以直接指定父scope名或使用 `Scope` 常量. scope不存在时,不进行任何操作.
+    :param int/str container_scope: 指定此scope的父scope. 可以直接指定父scope名或使用int索引运行时scope栈(参见 :ref:`输出函数的scope相关参数 <scope_param>`). scope不存在时,不进行任何操作.
     :param int position: 在父scope中创建此scope的位置.
-       `OutputPosition.TOP` : 在父scope的顶部创建, `OutputPosition.BOTTOM` : 在父scope的尾部创建
+       可选值: `OutputPosition.TOP` : 在父scope的顶部创建, `OutputPosition.BOTTOM`: 在父scope的底部创建。
+       也可以直接使用int来索引位置(参见 :ref:`输出函数的scope相关参数 <scope_param>`)
     :param str if_exist: 已经存在 ``name`` scope 时如何操作:
 
         - `None` 表示不进行任何操作
@@ -206,6 +207,7 @@ def set_scope(name, container_scope=Scope.Current, position=OutputPosition.BOTTO
         - `'clear'` 表示将旧scope的内容清除,不创建新scope
 
        默认为 `None`
+
     """
     if isinstance(container_scope, int):
         container_scope = get_current_session().get_scope_name(container_scope)
@@ -233,7 +235,7 @@ def get_scope(stack_idx=Scope.Current):
 def clear(scope=Scope.Current):
     """清空scope内容
 
-    :param int/str scope: 可以直接指定scope名或使用 `Scope` 常量
+    :param int/str scope: 可以直接指定scope名或使用int索引运行时scope栈(参见 :ref:`输出函数的scope相关参数 <scope_param>`)
     """
     if isinstance(scope, int):
         scope = get_current_session().get_scope_name(scope)
@@ -241,7 +243,10 @@ def clear(scope=Scope.Current):
 
 
 def remove(scope=Scope.Current):
-    """移除Scope"""
+    """移除Scope
+
+    :param int/str scope: 可以直接指定scope名或使用int索引运行时scope栈(参见 :ref:`输出函数的scope相关参数 <scope_param>`)
+    """
     if isinstance(scope, int):
         scope = get_current_session().get_scope_name(scope)
     assert scope != 'ROOT', "Can not remove `ROOT` scope."
@@ -249,16 +254,15 @@ def remove(scope=Scope.Current):
 
 
 def scroll_to(scope=Scope.Current, position=Position.TOP):
-    """scroll_to(scope, position=Position.TOP)
-
+    """
     将页面滚动到 ``scope`` Scope处
 
-    :param str/int scope: Scope名
+    :param str/int scope: 可以直接指定scope名或使用int索引运行时scope栈(参见 :ref:`输出函数的scope相关参数 <scope_param>`)
     :param str position: 将Scope置于屏幕可视区域的位置。可用值:
 
-       * ``Position.TOP`` : 滚动页面,让Scope位于屏幕可视区域顶部
-       * ``Position.MIDDLE`` : 滚动页面,让Scope位于屏幕可视区域中间
-       * ``Position.BOTTOM`` : 滚动页面,让Scope位于屏幕可视区域底部
+       * ``'top'`` : 滚动页面,让Scope位于屏幕可视区域顶部
+       * ``'middle'`` : 滚动页面,让Scope位于屏幕可视区域中间
+       * ``'bottom'`` : 滚动页面,让Scope位于屏幕可视区域底部
     """
     if isinstance(scope, int):
         scope = get_current_session().get_scope_name(scope)
@@ -298,7 +302,7 @@ def put_text(*texts, sep=' ', inline=False, scope=Scope.Current, position=Output
 
     :param texts: 要输出的内容。类型可以为任意对象,对非字符串对象会应用 `str()` 函数作为输出值。
     :param str sep: 输出分隔符
-    :param bool inline: 文本行末不换行。默认换行
+    :param bool inline: 将文本作为行内元素(文本行末不换行)。默认换行
     :param int/str scope: 内容输出的目标scope,若scope不存在,则不进行任何输出操作。
 
        可以直接指定目标Scope名,或者使用int通过索引Scope栈来确定Scope:0表示最顶层也就是ROOT Scope,-1表示当前Scope,-2表示进入当前Scope的前一个Scope,...
@@ -319,7 +323,7 @@ def put_html(html, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Outpu
 
     与支持通过Html输出内容到 `Jupyter Notebook <https://nbviewer.jupyter.org/github/ipython/ipython/blob/master/examples/IPython%20Kernel/Rich%20Output.ipynb#HTML>`_ 的库兼容。
 
-    :param html: html字符串或实现了 `IPython.display.HTML` 接口的类的实例
+    :param html: html字符串或实现了 `IPython.display.HTML` 接口的实例
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     """
     if hasattr(html, '__html__'):
@@ -403,7 +407,7 @@ class span_:
 def span(content, row=1, col=1):
     """用于在 :func:`put_table()` 和 :func:`put_grid()` 中设置内容跨单元格
 
-    :param content: 单元格内容
+    :param content: 单元格内容。可以为字符串或 ``put_xxx()`` 调用。
     :param int row: 竖直方向跨度, 即:跨行的数目
     :param int col: 水平方向跨度, 即:跨列的数目
 
@@ -570,7 +574,7 @@ def put_buttons(buttons, onclick, small=None, link_style=False, scope=Scope.Curr
                 for i in ['primary' , 'secondary' , 'success' , 'danger' , 'warning' , 'info' , 'light' , 'dark']  # ..demo-only
             ], onclick=put_text)  # ..demo-only
 
-    :type onclick: Callable / list
+    :type onclick: callable / list
     :param onclick: 按钮点击回调函数. ``onclick`` 可以是函数或者函数组成的列表.
 
        ``onclick`` 为函数时, 签名为 ``onclick(btn_value)``. ``btn_value`` 为被点击的按钮的 ``value`` 值
@@ -580,7 +584,7 @@ def put_buttons(buttons, onclick, small=None, link_style=False, scope=Scope.Curr
        Tip: 可以使用 ``functools.partial`` 来在 ``onclick`` 中保存更多上下文信息.
 
        Note: 当使用 :ref:`基于协程的会话实现 <coroutine_based_session>` 时,回调函数可以为协程函数.
-    :param bool small: 是否显示小号按钮,默认为False
+    :param bool small: 是否使用小号按钮,默认为False
     :param bool link_style: 是否将按钮显示为链接样式,默认为False
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
     :param callback_options: 回调函数的其他参数。根据选用的 session 实现有不同参数
@@ -590,8 +594,7 @@ def put_buttons(buttons, onclick, small=None, link_style=False, scope=Scope.Curr
 
        ThreadBasedSession 实现
            * serial_mode: 串行模式模式。默认为 ``False`` ,此时每次触发回调,回调函数会在新线程中立即执行。
-           对于开启了serial_mode的回调,都会在会话内的一个固定线程内执行,当会话运行此回调时,其他所有新的点击事件的回调(包括 ``serial_mode=False`` 的回调)都将排队等待当前点击事件运行完成。
-           如果回调函数运行时间很短,可以开启 ``serial_mode`` 来提高性能。
+           开启serial_mode后,该按钮的回调会在会话内的一个固定线程内串行执行,且其他所有新的点击事件的回调(包括 ``serial_mode=False`` 的回调)都将排队等待当前点击事件运行完成。如果回调函数运行时间很短,可以开启 ``serial_mode`` 来提高性能。
 
     使用示例:
 
@@ -643,12 +646,26 @@ def put_image(src, format=None, title='', width=None, height=None,
               scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """输出图片。
 
-    :param src: 图片内容. 类型可以为字符串类型的URL或者是 bytes-like object 或者为 ``PIL.Image.Image`` 实例
+    :param src: 图片内容. 可以为: 字符串类型的URL, bytes-like object 表示的图片二进制内容, ``PIL.Image.Image`` 实例
     :param str title: 图片描述
     :param str width: 图像的宽度,可以是CSS像素(数字px)或者百分比(数字%)。
     :param str height: 图像的高度,可以是CSS像素(数字px)或者百分比(数字%)。可以只指定 width 和 height 中的一个值,浏览器会根据原始图像进行缩放。
-    :param str format: 图片格式。如 ``png`` , ``jpeg`` , ``gif`` 等, 仅在 `src` 为非URL时有效
+    :param str format: 图片格式,非必须。如 ``png`` , ``jpeg`` , ``gif`` 等, 仅在 `src` 为非URL时有效
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: put_image
+        :summary: 使用`put_image()`输出图片
+
+        from pywebio import STATIC_PATH  # ..demo-only
+        img = open(STATIC_PATH + '/image/favicon_open_32.png', 'rb').read()  # ..demo-only
+        img = open('/path/to/some/image.png', 'rb').read()  # ..doc-only
+        put_image(img, width='50px')
+
+        ## ----
+        put_image('https://www.python.org/static/img/python-logo.png')
     """
     if isinstance(src, PILImage):
         format = src.format
@@ -682,6 +699,14 @@ def put_file(name, content, label=None, scope=Scope.Current, position=OutputPosi
         在PyWebIO会话(关于会话的概念见 :ref:`Server与script模式 <server_and_script_mode>` )结束后,使用 ``put_file()``
         输出的文件也将无法下载,可以在任务函数末尾处使用 `pywebio.session.hold()` 函数来将会话保持,这样在用户关闭浏览器页面前,
         文件下载将一直可用。
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: put_file
+        :summary: `put_file()`的使用
+
+        put_file('hello-world.txt', b'hello world!', 'download me')
     """
     if label is None:
         label = name
@@ -693,11 +718,11 @@ def put_file(name, content, label=None, scope=Scope.Current, position=OutputPosi
 
 def put_link(name, url=None, app=None, new_window=False, scope=Scope.Current,
              position=OutputPosition.BOTTOM) -> Output:
-    """输出链接到其他页或PyWebIO App的超链接
+    """输出链接到其他页或PyWebIO App的超链接
 
     :param str name: 链接名称
     :param str url: 链接到的页面地址
-    :param str app: 链接到的PyWebIO应用名
+    :param str app: 链接到的PyWebIO应用名。参见 :ref:`Server模式 <server_and_script_mode>`
     :param bool new_window: 是否在新窗口打开链接
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
 
@@ -720,6 +745,19 @@ def put_processbar(name, init=0, label=None, auto_close=False, scope=Scope.Curre
     :param str label: 进度条显示的标签. 默认为当前进度的百分比
     :param bool auto_close: 是否在进度完成后关闭进度条
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: put_processbar
+        :summary: `put_processbar()` usage
+
+        import time
+
+        put_processbar('bar');
+        for i in range(1, 11):
+            set_processbar('bar', i / 10)
+            time.sleep(0.1)
     """
     processbar_id = 'webio-processbar-%s' % name
     percentage = init * 100
@@ -740,6 +778,8 @@ def set_processbar(name, value, label=None):
     :param str name: 进度条名称
     :param float value: 进度条的值. 范围在 0 ~ 1 之间
     :param str label: 进度条显示的标签. 默认为当前进度的百分比
+
+    参见 `put_processbar()`
     """
     from pywebio.session import run_js
 
@@ -768,15 +808,20 @@ def put_loading(shape='border', color='dark', scope=Scope.Current, position=Outp
      `'warning'` 、`'info'`  、`'light'`  、 `'dark'` (默认)
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
 
-    .. note::
-        可以通过 :func:`style()` 设置加载提示的尺寸:
+    使用示例:
 
-        .. exportable-codeblock::
-            :name: put_loading-size
-            :summary: `put_loading()`自定义加载提示尺寸
+    .. exportable-codeblock::
+        :name: put_loading
+        :summary: `put_loading()` 使用示例
+
+        for shape in ('border', 'grow'):
+            for color in ('primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'):
+                put_text(shape, color)
+                put_loading(shape=shape, color=color)
 
-            put_loading()  # ..demo-only
-            style(put_loading(), 'width:4rem; height:4rem')
+        ## ----
+        # 可以通过 style() 设置加载提示的尺寸
+        style(put_loading(), 'width:4rem; height:4rem')
     """
     assert shape in ('border', 'grow'), "shape must in ('border', 'grow')"
     assert color in {'primary', 'secondary', 'success', 'danger', 'warning', 'info', 'light', 'dark'}
@@ -796,6 +841,24 @@ def put_collapse(title, content, open=False, scope=Scope.Current, position=Outpu
     :param content: 内容可以为字符串或 ``put_xxx`` 类输出函数的返回值,或者由它们组成的列表。
     :param bool open: 是否默认展开折叠内容。默认不展开内容
     :param int scope, position: 与 `put_text` 函数的同名参数含义一致
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: put_collapse
+        :summary: `put_collapse()` 使用示例
+
+        put_collapse('Collapse title', [
+            'text',
+            put_markdown('~~Strikethrough~~'),
+            put_table([
+                ['Commodity', 'Price'],
+                ['Apple', '5.5'],
+            ])
+        ], open=True)
+
+        ## ----
+        put_collapse('Large text', 'Awesome PyWebIO! '*30)
     """
     if not isinstance(content, (list, tuple, OutputList)):
         content = [content]
@@ -828,7 +891,7 @@ def put_scrollable(content, max_height=400, horizon_scroll=False, border=True, s
         content = [content]
 
     for item in content:
-        assert isinstance(item, (str, Output)), "put_collapse() content must be list of str/put_xxx()"
+        assert isinstance(item, (str, Output)), "put_scrollable() content must be list of str/put_xxx()"
 
     tpl = """<div style="max-height: {{max_height}}px;
             overflow-y: scroll;
@@ -897,11 +960,11 @@ def put_widget(template, data, scope=Scope.Current, position=OutputPosition.BOTT
 def put_row(content, size=None, scope=Scope.Current, position=OutputPosition.BOTTOM) -> Output:
     """使用行布局输出内容. 内容在水平方向从左往右排列成一行
 
-    :param list content: 子元素列表, 列表项为 ``put_xxx()`` 调用或者 ``None`` , ``None`` 表示空白间距
+    :param list content: 子元素列表, 列表项为 ``put_xxx()`` 调用或者 ``None`` , ``None`` 表示空白间距
     :param str size:
        | 用于指示子元素的宽度, 为空格分割的宽度值列表.
        | 宽度值需要和 ``content`` 中子元素一一对应( ``None`` 子元素也要对应宽度值).
-       | size 默认给 ``None`` 元素分配10像素宽度,剩余元素平均分配宽度.
+       | size 默认给 ``None`` 元素分配10像素宽度,并为剩余元素平均分配宽度.
 
        宽度值可用格式:
 
@@ -969,8 +1032,8 @@ def put_grid(content, cell_width='auto', cell_height='auto', cell_widths=None, c
     """使用网格布局输出内容
 
     :param content: 输出内容. ``put_xxx()`` / None 组成的二维数组, None 表示空白. 数组项可以使用 :func:`span()` 函数设置元素在网格的跨度.
-    :param str cell_width: 网格元素的宽度. 宽度值格式参考 `put_row()` 函数的 ``size`` 参数注释.
-    :param str cell_height: 网格元素的高度. 高度值格式参考 `put_row()` 函数的 ``size`` 参数注释.
+    :param str cell_width: 网格元素的宽度. 宽度值格式参考 `put_row()` 函数的 ``size`` 参数.
+    :param str cell_height: 网格元素的高度. 高度值格式参考 `put_row()` 函数的 ``size`` 参数.
     :param str cell_widths: 网格每一列的宽度. 宽度值用空格分隔. 不可以和 `cell_width` 参数同时使用. 宽度值格式参考 `put_row()` 函数的 ``size`` 参数注释.
     :param str cell_heights: 网格每一行的高度. 高度值用空格分隔. 不可以和 `cell_height` 参数同时使用. 高度值格式参考 `put_row()` 函数的 ``size`` 参数注释.
     :param str direction: 排列方向. 为 ``'row'`` 或 ``'column'`` .
@@ -1051,7 +1114,7 @@ def output(*contents):
 
     output用于对 :ref:`组合输出 <combine_output>` 中的 ``put_xxx()`` 子项进行动态修改(见下方代码示例)
 
-    :param contents: 要输出的初始内容. 元素为 ``put_xxx()`` 形式的调用或字符串,字符串会被看成HTML.
+    :param contents: 要输出的初始内容. 元素为 ``put_xxx()`` 形式的调用或字符串.
     :return: OutputHandler 实例, 实例支持的方法如下:
 
     * ``reset(*contents)`` : 重置内容为 ``contents``
@@ -1181,8 +1244,7 @@ def style(outputs, css_style) -> Union[Output, OutputList]:
 
 @safely_destruct_output_when_exp('content')
 def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closable=True):
-    """popup(title, content, size=PopupSize.NORMAL, implicit_close=True, closable=True)
-
+    """
     显示弹窗
 
     ⚠️: PyWebIO不允许同时显示多个弹窗,在显示新弹窗前,会自动关闭页面上存在的弹窗。可以使用 `close_popup()` 主动关闭弹窗
@@ -1192,15 +1254,15 @@ def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closa
     :param content: 弹窗内容. 可以为字符串或 ``put_xxx`` 类输出函数的返回值,或者为它们组成的列表。
     :param str size: 弹窗窗口大小,可选值:
 
-         * ``LARGE`` : 大尺寸
-         * ``NORMAL`` : 普通尺寸
-         * ``SMALL`` : 小尺寸
+         * ``'large'`` : 大尺寸
+         * ``'normal'`` : 普通尺寸
+         * ``'small'`` : 小尺寸
 
     :param bool implicit_close: 是否可以通过点击弹窗外的内容或按下 ``Esc`` 键来关闭弹窗
-    :param bool closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗
-       设置为 ``False`` 时弹窗仅能通过 :func:`popup_close()` 关闭, ``implicit_close`` 参数被忽略.
+    :param bool closable: 是否可由用户关闭弹窗. 默认情况下,用户可以通过点击弹窗右上角的关闭按钮来关闭弹窗
+       设置为 ``False`` 时弹窗仅能通过 :func:`popup_close()` 关闭,此时 ``implicit_close`` 参数被忽略.
 
-    支持直接传入内容、上下文管理器、装饰器三种形式的调用
+    ``popup()`` 支持直接传入内容、上下文管理器、装饰器三种形式的调用
 
     * 直接传入内容:
 
@@ -1227,13 +1289,13 @@ def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closa
         with popup('Popup title') as s:
             put_html('<h3>Popup Content</h3>')
             put_text('html: <br/>')
-            put_buttons(['clear()'], onclick=lambda _: clear(scope=s))
+            put_buttons([('clear()', s)], onclick=clear)
 
         put_text('Also work!', scope=s)
 
 
-    上下文管理器会开启一个新的输出域并返回Scope名,上下文管理器中的输出调用会显示到弹窗上。
-    上下文管理器退出后,弹窗并不会关闭,依然可以使用 ``scope`` 参数输出内容到弹窗。
+    上下文管理器会开启一个新的输出域并返回Scope名,在上下文管理器中的输出默认会显示到弹窗上。
+    上下文管理器退出后,弹窗并不会关闭,依然可以在输出函数中指定 ``scope`` 参数来输出内容到弹窗。
 
     * 作为装饰器使用:
 
@@ -1269,7 +1331,10 @@ def popup(title, content=None, size=PopupSize.NORMAL, implicit_close=True, closa
 
 
 def close_popup():
-    """关闭当前页面上正在显示的弹窗"""
+    """关闭当前页面上正在显示的弹窗
+
+    参见: `popup()`
+    """
     send_msg(cmd='close_popup')
 
 
@@ -1291,7 +1356,7 @@ def toast(content, duration=2, position='center', color='info', onclick=None):
         :summary: 使用`toast()`显示通知
 
         def show_msg():
-            put_text("Some messages...")
+            put_text("You clicked the notification.")
 
         toast('New messages', position='right', color='#2188ff', duration=0, onclick=show_msg)
 
@@ -1318,10 +1383,10 @@ def use_scope(name=None, clear=False, create_scope=True, **scope_params):
 
     参见 :ref:`用户手册-use_scope() <use_scope>`
 
-    :param name: scope名. 若为None则生成一个全局唯一的scope名.(以上下文管理器形式的调用时,上下文管理器会返回scope名)
+    :param str name: scope名. 若为None则生成一个全局唯一的scope名.(以上下文管理器形式的调用时,上下文管理器会返回scope名)
     :param bool clear: 在进入scope前是否要清除scope里的内容
     :param bool create_scope: scope不存在时是否创建scope
-    :param scope_params: 创建scope时传入set_scope()的参数. 仅在 `create_scope=True` 时有效.
+    :param scope_params: 创建scope时传入 `set_scope()` 的额外的参数. 仅在 `create_scope=True` 时有效.
 
     :Usage:
 

+ 2 - 2
pywebio/platform/aiohttp.py

@@ -102,8 +102,8 @@ def _webio_handler(applications, websocket_settings, check_origin_func=_is_same_
 
 
 def webio_handler(applications, allowed_origins=None, check_origin=None, websocket_settings=None):
-    """获取在aiohttp中运行PyWebIO任务函数的 `Request Handle <https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-handler>`_ 协程。
-    Request Handle基于WebSocket协议与浏览器进行通讯。
+    """获取在aiohttp中运行PyWebIO任务函数的 `Request Handler <https://docs.aiohttp.org/en/stable/web_quickstart.html#aiohttp-web-handler>`_ 协程。
+    Request Handler基于WebSocket协议与浏览器进行通讯。
 
     :param list/dict/callable applications: PyWebIO应用。
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。

+ 2 - 2
pywebio/platform/django.py

@@ -108,8 +108,8 @@ urlpatterns = []
 
 def start_server(applications, port=8080, host='localhost',
                  allowed_origins=None, check_origin=None,
-                 session_cleanup_interval=None,
                  session_expire_seconds=None,
+                 session_cleanup_interval=None,
                  debug=False, **django_options):
     """启动一个 Django server 将PyWebIO应用作为Web服务提供。
 
@@ -129,7 +129,7 @@ def start_server(applications, port=8080, host='localhost',
     :param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
         返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
     :param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
-    :param int session_cleanup_interval: 会话清理间隔。
+    :param int session_cleanup_interval: 会话清理间隔(秒)。服务端会周期性清理过期的会话,释放会话占用的资源
     :param bool debug: 开启 Django debug mode 和一般访问日志的记录
     :param django_options: django应用的其他设置,见 https://docs.djangoproject.com/en/3.0/ref/settings/ .
         其中 ``DEBUG`` 、 ``ALLOWED_HOSTS`` 、 ``ROOT_URLCONF`` 、 ``SECRET_KEY`` 被PyWebIO设置,无法在 ``django_options`` 中指定

+ 3 - 3
pywebio/platform/flask.py

@@ -109,8 +109,8 @@ def webio_view(applications,
 
 def start_server(applications, port=8080, host='localhost',
                  allowed_origins=None, check_origin=None,
-                 session_cleanup_interval=None,
                  session_expire_seconds=None,
+                 session_cleanup_interval=None,
                  debug=False, **flask_options):
     """启动一个 Flask server 将PyWebIO应用作为Web服务提供。
 
@@ -130,8 +130,8 @@ def start_server(applications, port=8080, host='localhost',
     :param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
         返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
     :param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
-    :param int session_cleanup_interval: 会话清理间隔。
-    :param bool debug: Flask debug mode
+    :param int session_cleanup_interval: 会话清理间隔(秒)。服务端会周期性清理过期的会话,释放会话占用的资源
+    :param bool debug: 是否开启Flask Server的debug模式,开启后,代码发生修改后服务器会自动重启。
     :param flask_options: 传递给 ``flask.Flask.run`` 函数的额外的关键字参数
         可设置项参考: https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.run
     """

+ 9 - 7
pywebio/platform/tornado.py

@@ -53,11 +53,11 @@ def _is_same_site(origin, handler: WebSocketHandler):
 
 
 def _webio_handler(applications, check_origin_func=_is_same_site):
-    """获取用于Tornado进行整合的RequestHandle类
+    """获取用于Tornado进行整合的RequestHandler
 
     :param dict applications: 任务名->任务函数 的字典
     :param callable check_origin_func: check_origin_func(origin, handler) -> bool
-    :return: Tornado RequestHandle类
+    :return: Tornado RequestHandler
     """
 
     class WSHandler(WebSocketHandler):
@@ -114,7 +114,7 @@ def _webio_handler(applications, check_origin_func=_is_same_site):
 
 
 def webio_handler(applications, allowed_origins=None, check_origin=None):
-    """获取在Tornado中运行PyWebIO应用的RequestHandle类。RequestHandle类基于WebSocket协议与浏览器进行通讯。
+    """获取在Tornado中运行PyWebIO应用的RequestHandler类。RequestHandler类基于WebSocket协议与浏览器进行通讯。
 
     :param callable/list/dict applications: PyWebIO应用。
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
@@ -122,7 +122,7 @@ def webio_handler(applications, allowed_origins=None, check_origin=None):
 
     关于各参数的详细说明见 :func:`pywebio.platform.tornado.start_server` 的同名参数。
 
-    :return: Tornado RequestHandle类
+    :return: Tornado RequestHandler
     """
     applications = make_applications(applications)
     for target in applications.values():
@@ -175,12 +175,14 @@ def start_server(applications, port=0, host='', debug=False,
 
        可以通过 ``app`` URL参数选择要运行的任务(例如访问 ``http://host:port/?app=foo`` 来运行 ``foo`` 任务),
        默认使用运行 ``index`` 任务函数,当 ``index`` 任务不存在时,PyWebIO会提供一个默认的索引页作为主页。
+       参见 :ref:`Server模式 <server_and_script_mode>`
 
        任务函数为协程函数时,使用 :ref:`基于协程的会话实现 <coroutine_based_session>` ;任务函数为普通函数时,使用基于线程的会话实现。
     :param int port: 服务监听的端口。设置为 ``0`` 时,表示自动选择可用端口。
     :param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
        通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
-    :param bool debug: Tornado Server是否开启debug模式
+    :param bool debug: 是否开启Tornado Server的debug模式,开启后,代码发生修改后服务器会自动重启。
+       详情请参阅 `tornado 文档 <https://www.tornadoweb.org/en/stable/guide/running.html#debug-mode>`_
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
         来源包含协议、域名和端口部分,允许使用 Unix shell 风格的匹配模式(全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ ):
 
@@ -190,8 +192,8 @@ def start_server(applications, port=0, host='', debug=False,
         - ``[!seq]`` 匹配任何不在seq中的字符
 
         比如 ``https://*.example.com`` 、 ``*://*.example.com``
-    :param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
-        返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
+    :param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串作为参数
+        返回 ``True/False`` 指示服务器接受/拒绝该请求。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
     :param bool auto_open_webbrowser: 当服务启动后,是否自动打开浏览器来访问服务。(该操作需要操作系统支持)
     :param int websocket_max_message_size: Tornado Server最大可接受的WebSockets消息大小。单位为字节,默认为10MiB。
     :param int websocket_ping_interval: 当被设置后,服务器会以 ``websocket_ping_interval`` 秒周期性地向每个WebSockets连接发送‘ping‘消息。

+ 88 - 9
pywebio/session/__init__.py

@@ -155,6 +155,14 @@ def download(name, content):
 
     :param str name: 下载保存为的文件名
     :param content: 文件内容. 类型为 bytes-like object
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: download
+        :summary: `download()` 使用示例
+
+        put_buttons(['Click to download'], [lambda: download('hello-world.txt', b'hello world!')])
     """
     from ..io_ctrl import send_msg
     content = b64encode(content).decode('ascii')
@@ -162,7 +170,7 @@ def download(name, content):
 
 
 def run_js(code_, **args):
-    """运行js代码.
+    """在用户浏览器中运行JavaScript代码.
 
     代码运行在浏览器的JS全局作用域中
 
@@ -180,23 +188,32 @@ def run_js(code_, **args):
 
 @chose_impl
 def eval_js(expression_, **args):
-    """执行js表达式,并获取表达式的值
+    """在用户浏览器中执行JavaScript表达式,并获取表达式的值
 
     :param str expression_: js表达式. 表达式的值需要能JSON序列化
-    :return: js表达式的值
     :param args: 传递给js代码的局部变量。变量值需要可以被json序列化
+    :return: js表达式的值
 
     注意⚠️:在 :ref:`基于协程 <coroutine_based_session>` 的会话上下文中,需要使用 ``await eval_js(expression)`` 语法来进行调用。
 
-    Example::
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: eval_js
+        :summary: `eval_js()`使用示例
 
         current_url = eval_js("window.location.href")
+        put_text(current_url)  # ..demo-only
 
+        ## ----
         function_res = eval_js('''(function(){
             var a = 1;
             a += b;
             return a;
         })()''', b=100)
+        put_text(function_res)  # ..demo-only
+
     """
     script = r"""
     (function(WebIO){
@@ -226,6 +243,8 @@ def run_async(coro_obj):
 
     :param coro_obj: 协程对象
     :return: `TaskHandle <pywebio.session.coroutinebased.TaskHandle>` 实例。 通过 TaskHandle 可以查询协程运行状态和关闭协程。
+
+    参见::ref:`协程会话的并发 <coroutine_based_concurrency>`
     """
     return get_current_session().run_async(coro_obj)
 
@@ -283,7 +302,51 @@ def defer_call(func):
 
 
 def data():
-    """获取当前会话的数据对象,用于在对象上保存一些会话相关的数据。访问数据对象不存在的属性时会返回None而不是抛出异常。
+    """获取当前会话的数据对象(session-local object)。
+
+    访问数据对象不存在的属性时会返回None而不是抛出异常。
+
+    当需要在多个函数中保存一些会话独立的数据时,使用session-local对象保存状态会比通过函数参数传递更方便。
+    以下是一个会话独立的计数器的实现示例::
+
+        def add():
+            data().cnt = (data().cnt or 0) + 1
+
+        def show():
+            put_text(data().cnt or 0)
+
+        def main():  # 会话独立的计数器
+            put_buttons(['Add counter', 'Show counter'], [add, show])
+            hold()
+
+    而通过函数参数传递状态的实现方式为::
+
+        from functools import partial
+        def add(cnt):
+            cnt[0] += 1
+
+        def show(cnt):
+            put_text(cnt[0])
+
+        def main():  # 会话独立的计数器
+            cnt = [0]  # 将计数器保存在数组中才可以实现引用传参
+            put_buttons(['Add counter', 'Show counter'], [partial(add, cnt), partial(show, cnt)])
+            hold()
+
+    当然,还可以通过函数闭包来实现相同的功能::
+
+        def main():  # 会话独立的计数器
+            cnt = 0
+
+            def add():
+                nonlocal cnt
+                cnt += 1
+
+            def show():
+                put_text(cnt)
+
+            put_buttons(['Add counter', 'Show counter'], [add, show])
+            hold()
     """
     return get_current_session().save
 
@@ -296,7 +359,7 @@ def set_env(**env_info):
     * ``title`` (str): 当前页面的标题
     * ``output_animation`` (bool): 是否启用输出动画(在输出内容时,使用过渡动画),默认启用
     * ``auto_scroll_bottom`` (bool): 是否在内容输出时将页面自动滚动到底部,默认关闭。注意,开启后,只有输出到ROOT Scope才可以触发自动滚动。
-    * ``http_pull_interval`` (int): HTTP轮询后端消息的周期(单位为毫秒,默认1000ms),仅在基于HTTP连接的会话中可用()
+    * ``http_pull_interval`` (int): HTTP轮询后端消息的周期(单位为毫秒,默认1000ms),仅在基于HTTP连接的会话(使用Flask或Django后端)中可用
 
     调用示例::
 
@@ -309,10 +372,12 @@ def set_env(**env_info):
 
 
 def go_app(name, new_window=True):
-    """跳转PyWebIO任务,仅在PyWebIO Server模式下可用
+    """在同一PyWebIO应用的不同服务之间跳转。仅在PyWebIO Server模式下可用
 
-    :param str name: PyWebIO任务名
+    :param str name: 目标 PyWebIO 任务名
     :param bool new_window: 是否在新窗口打开,默认为 `True`
+
+    参见: :ref:`Server 模式 <server_and_script_mode>`
     """
     run_js('javascript:WebIO.openApp(app, new_window)', app=name, new_window=new_window)
 
@@ -346,7 +411,7 @@ def get_info():
        * ``origin`` (str): 当前用户的页面地址. 包含 协议、主机、端口 部分. 比如 ``'http://localhost:8080'`` .
          可能为空,但保证当用户的页面地址不在当前服务器下(即 主机、端口部分和 ``server_host`` 不一致)时有值.
        * ``user_ip`` (str): 用户的ip地址.
-       * ``backend`` (str): PyWebIO使用的Web框架名. 目前可用值有 ``'tornado'`` , ``'flask'`` , ``'django'`` , ``'aiohttp'`` .
+       * ``backend`` (str): 当前PyWebIO使用的后端Server实现. 可能出现的值有 ``'tornado'`` , ``'flask'`` , ``'django'`` , ``'aiohttp'``.
        * ``request`` (object): 创建当前会话时的Web请求对象. 根据PyWebIO使用的后端Server不同,``request`` 的类型也不同:
 
             * 使用Tornado后端时, ``request`` 为
@@ -356,5 +421,19 @@ def get_info():
             * 使用aiohttp后端时, ``request`` 为 `aiohttp.web.BaseRequest <https://docs.aiohttp.org/en/stable/web_reference.html#aiohttp.web.BaseRequest>`_ 实例
 
     会话信息对象的 ``user_agent`` 属性是通过 user-agents 库进行解析生成的。参见 https://github.com/selwin/python-user-agents#usage
+
+    使用示例:
+
+    .. exportable-codeblock::
+        :name: get_info
+        :summary: `get_info()` 的使用
+
+        import json
+
+        info = get_info()
+        put_code(json.dumps({
+            k: str(v)
+            for k,v in info.items()
+        }, indent=4), 'json')
     """
     return get_current_session().info

+ 4 - 1
pywebio/session/coroutinebased.py

@@ -226,7 +226,10 @@ class CoroutineBasedSession(Session):
 
 
 class TaskHandle:
-    """协程任务句柄"""
+    """协程任务句柄
+
+    参见:`run_async() <pywebio.session.run_async>`
+    """
 
     def __init__(self, close, closed):
         self._close = close