Skip to content

Commit 4052a39

Browse files
committed
feat: dynamically generate homepage html
* SEO friendly * Easier integration with web frameworks * No need to send a probe request first when start a new session
1 parent 9606951 commit 4052a39

15 files changed

+374
-84
lines changed

MANIFEST.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
recursive-include demos *.py
22
prune docs
3-
graft pywebio/html
3+
graft pywebio/html
4+
graft pywebio/platform/tpl

pywebio/html/index.html

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
<!doctype html>
2-
<html lang="zh">
2+
<html lang="">
33
<head>
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
6-
<title>PyWebIO</title>
7-
<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAwklEQVQ4T63TvU5CQRCG4WcwMfEuqOgNtQ2Nd4CxV2LHtVhJ0N7AHdjQUBtrrLwLA4ks2Rx+/Qucw3Y78807M7sz4ft5dq6mI7RQX7o/JCNzfdfetkNifRk6k9wLN9jYdxMkyZPQ1faZXYUwB/OCix8V/W4Y4zJDCsBAX7jdM7iQJY+udELu+cTrP2X/xU2+NMPAg3B3UPaVOOmFoQkapQC8Z8AUpyUBs6MAKrZQ+RErf2PlQTrKKK8gpZdpewgOXOcFTTxEjYwMoIkAAAAASUVORK5CYII=" id="favicon32">
8-
<link rel="icon" type="image/png" sizes="16x16" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABmUlEQVRYR82XK0wDQRCGv21TUUUJGBwGDBggGCSGBIcAWnBAgsNAgkKhSMDgCA8HtEXgSDBIDC9DDRgcpoSiKo52yea49DiutMttsz27M/98N7s7OyNo9tujgxSTwDiCIaAXSH27l4AXJA/AFSUuWOajGWnR0ChLP3HWkWSAZEN716CM4JQKW6R5+sunPkCeJJJNBCtAosnAQTMHyS6CDWYoh2mEAxzTR4JzYOCfgYNuBRymmOc5uPAbIMswMS6BbkPBPZkiVSZIc+/X/Qng/vl1C4LXIBzG/JmoAag9hxuDaa+XwAIw6p2JGkCObQSrhtMeLifZYZY1tegCqKsW4zHCadfldqgyqK6oC3DGIZIFXZVI9oIjplkUqArXyatGkYkU1+dc5p0eQY4MghNTqlo6kjkFsI9gScvRlLHkQJDnFhgxpampc6cAikCXpqMp8zcF8AnETSlq6lTaAsD6Flg+hNavofVCZL0UW3+M2uI5VhBWGxIFYL0lUxBWm1KviFttyz0Iq4OJB2F1NPO/qdaG0+DD3qLx/AuMVJFhmC8dSgAAAABJRU5ErkJggg==" id="favicon16">
6+
<title>PyWebIO Application</title>
7+
<link rel="icon" type="image/png" sizes="32x32"
8+
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAwklEQVQ4T63TvU5CQRCG4WcwMfEuqOgNtQ2Nd4CxV2LHtVhJ0N7AHdjQUBtrrLwLA4ks2Rx+/Qucw3Y78807M7sz4ft5dq6mI7RQX7o/JCNzfdfetkNifRk6k9wLN9jYdxMkyZPQ1faZXYUwB/OCix8V/W4Y4zJDCsBAX7jdM7iQJY+udELu+cTrP2X/xU2+NMPAg3B3UPaVOOmFoQkapQC8Z8AUpyUBs6MAKrZQ+RErf2PlQTrKKK8gpZdpewgOXOcFTTxEjYwMoIkAAAAASUVORK5CYII="
9+
id="favicon32">
10+
<link rel="icon" type="image/png" sizes="16x16"
11+
href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAABmUlEQVRYR82XK0wDQRCGv21TUUUJGBwGDBggGCSGBIcAWnBAgsNAgkKhSMDgCA8HtEXgSDBIDC9DDRgcpoSiKo52yea49DiutMttsz27M/98N7s7OyNo9tujgxSTwDiCIaAXSH27l4AXJA/AFSUuWOajGWnR0ChLP3HWkWSAZEN716CM4JQKW6R5+sunPkCeJJJNBCtAosnAQTMHyS6CDWYoh2mEAxzTR4JzYOCfgYNuBRymmOc5uPAbIMswMS6BbkPBPZkiVSZIc+/X/Qng/vl1C4LXIBzG/JmoAag9hxuDaa+XwAIw6p2JGkCObQSrhtMeLifZYZY1tegCqKsW4zHCadfldqgyqK6oC3DGIZIFXZVI9oIjplkUqArXyatGkYkU1+dc5p0eQY4MghNTqlo6kjkFsI9gScvRlLHkQJDnFhgxpampc6cAikCXpqMp8zcF8AnETSlq6lTaAsD6Flg+hNavofVCZL0UW3+M2uI5VhBWGxIFYL0lUxBWm1KviFttyz0Iq4OJB2F1NPO/qdaG0+DD3qLx/AuMVJFhmC8dSgAAAABJRU5ErkJggg=="
12+
id="favicon16">
913
<link rel="stylesheet" href="css/markdown.min.css">
1014
<link rel="stylesheet" href="css/bootstrap.min.css">
1115
<link rel="stylesheet" href="css/codemirror.min.css">
@@ -87,12 +91,18 @@
8791
});
8892

8993
const urlparams = new URLSearchParams(window.location.search);
90-
let config = {
91-
debug: urlparams.get('_pywebio_debug'),
92-
outputAnimation: !urlparams.get('_pywebio_disable_animate'),
93-
httpPullInterval: parseInt(urlparams.get('_pywebio_http_pull_interval') || 1000)
94-
};
95-
WebIO.startWebIOClient($('#markdown-body'), $('#input-cards'), urlparams.get('app') || 'index', config);
94+
WebIO.startWebIOClient({
95+
output_container_elem: $('#markdown-body'),
96+
input_container_elem: $('#input-cards'),
97+
backend_address: urlparams.get('pywebio_api') || './io',
98+
app_name: urlparams.get('app') || 'index',
99+
protocol: 'auto',
100+
runtime_config: {
101+
debug: urlparams.get('_pywebio_debug'),
102+
outputAnimation: !urlparams.get('_pywebio_disable_animate'),
103+
httpPullInterval: parseInt(urlparams.get('_pywebio_http_pull_interval') || 1000)
104+
},
105+
});
96106

97107
</script>
98108

pywebio/platform/aiohttp.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from aiohttp import web
1010

1111
from .tornado import open_webbrowser_on_server_started
12-
from .utils import make_applications
12+
from .utils import make_applications, render_page
1313
from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, Session
1414
from ..session.base import get_session_info_from_headers
1515
from ..utils import get_free_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction
@@ -51,6 +51,16 @@ async def wshandle(request: web.Request):
5151
if origin and not check_origin_func(origin=origin, host=request.host):
5252
return web.Response(status=403, text="Cross origin websockets not allowed")
5353

54+
if request.headers.get("Upgrade", "").lower() != "websocket":
55+
# Backward compatible
56+
if request.query.getone('test', ''):
57+
return web.Response(text="")
58+
59+
app_name = request.query.getone('app', 'index')
60+
app = applications.get(app_name) or applications['index']
61+
html = render_page(app, protocol='ws')
62+
return web.Response(body=html, content_type='text/html')
63+
5464
ws = web.WebSocketResponse(**websocket_settings)
5565
await ws.prepare(request)
5666

@@ -159,17 +169,8 @@ def start_server(applications, port=0, host='', debug=False,
159169
:param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
160170
通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
161171
:param bool debug: 是否开启asyncio的Debug模式
162-
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
163-
来源包含协议、域名和端口部分,允许使用 Unix shell 风格的匹配模式(全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ ):
164-
165-
- ``*`` 为通配符
166-
- ``?`` 匹配单个字符
167-
- ``[seq]`` 匹配seq中的字符
168-
- ``[!seq]`` 匹配不在seq中的字符
169-
170-
比如 ``https://*.example.com`` 、 ``*://*.example.com``
171-
:param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
172-
返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
172+
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
173+
:param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
173174
:param bool auto_open_webbrowser: 当服务启动后,是否自动打开浏览器来访问服务。(该操作需要操作系统支持)
174175
:param dict websocket_settings: 创建 aiohttp WebSocketResponse 时使用的参数。见 https://docs.aiohttp.org/en/stable/web_reference.html#websocketresponse
175176
:param aiohttp_settings: 需要传给 aiohttp Application 的参数。可用参数见 https://docs.aiohttp.org/en/stable/web_reference.html#application
@@ -186,7 +187,7 @@ def start_server(applications, port=0, host='', debug=False,
186187
websocket_settings=websocket_settings)
187188

188189
app = web.Application(**aiohttp_settings)
189-
app.router.add_routes([web.get('/io', handler)])
190+
app.router.add_routes([web.get('/', handler)])
190191
app.router.add_routes(static_routes())
191192

192193
if auto_open_webbrowser:

pywebio/platform/django.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ def set_content(self, content, json_type=False):
5656
:param content:
5757
:param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
5858
"""
59+
# self.response.content accept str and byte
5960
if json_type:
6061
self.set_header('content-type', 'application/json')
6162
self.response.content = json.dumps(content)
@@ -117,17 +118,8 @@ def start_server(applications, port=8080, host='localhost',
117118
:param int port: 服务监听的端口。设置为 ``0`` 时,表示自动选择可用端口。
118119
:param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
119120
通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
120-
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
121-
来源包含协议、域名和端口部分,允许使用 Unix shell 风格的匹配模式(全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ ):
122-
123-
- ``*`` 为通配符
124-
- ``?`` 匹配单个字符
125-
- ``[seq]`` 匹配seq中的字符
126-
- ``[!seq]`` 匹配不在seq中的字符
127-
128-
比如 ``https://*.example.com`` 、 ``*://*.example.com``
129-
:param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
130-
返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
121+
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
122+
:param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
131123
:param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
132124
:param int session_cleanup_interval: 会话清理间隔(秒)。服务端会周期性清理过期的会话,释放会话占用的资源。
133125
:param bool debug: 开启 Django debug mode 和一般访问日志的记录
@@ -186,8 +178,7 @@ def start_server(applications, port=8080, host='localhost',
186178
)
187179

188180
urlpatterns = [
189-
path(r"io", webio_view_func),
190-
path(r'', partial(serve, path='index.html'), {'document_root': STATIC_PATH}),
181+
path(r"", webio_view_func),
191182
path(r'<path:path>', serve, {'document_root': STATIC_PATH}),
192183
]
193184

pywebio/platform/flask.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ def set_content(self, content, json_type=False):
6262
:param content:
6363
:param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
6464
"""
65+
# self.response.data accept str and bytes
6566
if json_type:
6667
self.set_header('content-type', 'application/json')
6768
self.response.data = json.dumps(content)
@@ -118,17 +119,8 @@ def start_server(applications, port=8080, host='localhost',
118119
:param int port: 服务监听的端口。设置为 ``0`` 时,表示自动选择可用端口。
119120
:param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
120121
通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
121-
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
122-
来源包含协议、域名和端口部分,允许使用 Unix shell 风格的匹配模式(全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ ):
123-
124-
- ``*`` 为通配符
125-
- ``?`` 匹配单个字符
126-
- ``[seq]`` 匹配seq中的字符
127-
- ``[!seq]`` 匹配不在seq中的字符
128-
129-
比如 ``https://*.example.com`` 、 ``*://*.example.com``
130-
:param callable check_origin: 请求来源检查函数。接收请求来源(包含协议、域名和端口部分)字符串,
131-
返回 ``True/False`` 。若设置了 ``check_origin`` , ``allowed_origins`` 参数将被忽略
122+
:param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
123+
:param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
132124
:param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
133125
:param int session_cleanup_interval: 会话清理间隔(秒)。服务端会周期性清理过期的会话,释放会话占用的资源。
134126
:param bool debug: 是否开启Flask Server的debug模式,开启后,代码发生修改后服务器会自动重启。
@@ -142,17 +134,16 @@ def start_server(applications, port=8080, host='localhost',
142134
port = get_free_port()
143135

144136
app = Flask(__name__)
145-
app.add_url_rule('/io', 'webio_view', webio_view(
137+
app.add_url_rule('/', 'webio_view', webio_view(
146138
applications=applications,
147139
session_expire_seconds=session_expire_seconds,
148140
session_cleanup_interval=session_cleanup_interval,
149141
allowed_origins=allowed_origins,
150142
check_origin=check_origin
151143
), methods=['GET', 'POST', 'OPTIONS'])
152144

153-
@app.route('/')
154145
@app.route('/<path:static_file>')
155-
def serve_static_file(static_file='index.html'):
146+
def serve_static_file(static_file):
156147
return send_from_directory(STATIC_PATH, static_file)
157148

158149
has_coro_target = any(iscoroutinefunction(target) or isgeneratorfunction(target) for

pywebio/platform/httpbased.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
import time
2323

24-
from .utils import make_applications
24+
from .utils import make_applications, render_page
2525
from ..session import CoroutineBasedSession, Session, ThreadBasedSession, register_session_implement_for_target
2626
from ..session.base import get_session_info_from_headers
2727
from ..utils import random_str, LRUDict, isgeneratorfunction, iscoroutinefunction
@@ -63,13 +63,13 @@ def set_status(self, status):
6363
def set_content(self, content, json_type=False):
6464
"""设置响应的内容。方法应该仅被调用一次
6565
66-
:param content:
66+
:param str/bytes/json-able content:
6767
:param bool json_type: content是否要序列化成json格式,并将 content-type 设置为application/json
6868
"""
6969
pass
7070

7171
def get_response(self):
72-
"""获取当前的响应对象,用于在私图函数中返回"""
72+
"""获取当前的响应对象,用于在视图函数中返回"""
7373
pass
7474

7575
def get_client_ip(self):
@@ -166,16 +166,24 @@ def handle_request(self, context: HttpContext):
166166

167167
if request_headers.get('Origin'): # set headers for CORS request
168168
self._process_cors(context)
169+
# CORS process end ############################
169170

170-
if context.request_url_parameter('test'): # 测试接口,当会话使用给予http的backend时,返回 ok
171+
if context.request_url_parameter('test'): # 测试接口,当会话使用基于http的backend时,返回 ok
171172
context.set_content('ok')
172173
return context.get_response()
173-
# CORS process end ############################
174+
175+
# 对首页HTML的请求
176+
if 'webio-session-id' not in request_headers:
177+
app_name = context.request_url_parameter('app', 'index')
178+
app = self.applications.get(app_name) or self.applications['index']
179+
html = render_page(app, protocol='http')
180+
context.set_content(html)
181+
return context.get_response()
174182

175183
webio_session_id = None
176184

177-
# webio-session-id 的请求头为空时,创建新 Session
178-
if 'webio-session-id' not in request_headers or not request_headers['webio-session-id']:
185+
# 初始请求,创建新 Session
186+
if not request_headers['webio-session-id'] or request_headers['webio-session-id'] == 'NEW':
179187
if context.request_method() == 'POST': # 不能在POST请求中创建Session,防止CSRF攻击
180188
context.set_status(403)
181189
return context.get_response()

pywebio/platform/tornado.py

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
register_session_implement_for_target, Session
1919
from ..session.base import get_session_info_from_headers
2020
from ..utils import get_free_port, wait_host_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction
21-
from .utils import make_applications
21+
from .utils import make_applications, render_page
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -62,6 +62,20 @@ def _webio_handler(applications, check_origin_func=_is_same_site):
6262

6363
class WSHandler(WebSocketHandler):
6464

65+
async def get(self, *args, **kwargs) -> None:
66+
# It's a simple http GET request
67+
if self.request.headers.get("Upgrade", "").lower() != "websocket":
68+
# Backward compatible
69+
if self.get_query_argument('test', ''):
70+
return self.write('')
71+
72+
app_name = self.get_query_argument('app', 'index')
73+
app = applications.get(app_name) or applications['index']
74+
html = render_page(app, protocol='ws')
75+
return self.write(html)
76+
else:
77+
await super().get()
78+
6579
def check_origin(self, origin):
6680
return check_origin_func(origin=origin, handler=self)
6781

@@ -150,7 +164,7 @@ def _setup_server(webio_handler, port=0, host='', **tornado_app_settings):
150164
if port == 0:
151165
port = get_free_port()
152166

153-
handlers = [(r"/io", webio_handler),
167+
handlers = [(r"/", webio_handler),
154168
(r"/(.*)", StaticFileHandler, {"path": STATIC_PATH, 'default_filename': 'index.html'})]
155169

156170
app = tornado.web.Application(handlers=handlers, **tornado_app_settings)
@@ -231,7 +245,9 @@ def start_server_in_current_thread_session():
231245
websocket_conn_opened = threading.Event()
232246
thread = threading.current_thread()
233247

234-
class SingleSessionWSHandler(_webio_handler(applications={})):
248+
mock_apps = dict(index=lambda: None)
249+
250+
class SingleSessionWSHandler(_webio_handler(applications=mock_apps)):
235251
session = None
236252
instance = None
237253

0 commit comments

Comments
 (0)