浏览代码

feat: support fetch frontend resource from CDN

wangweimin 4 年之前
父节点
当前提交
a5d0d694ce

+ 3 - 0
pywebio/exceptions.py

@@ -17,3 +17,6 @@ class SessionClosedException(SessionException):
 class SessionNotFoundException(SessionException):
     """会话未找到异常"""
 
+
+class PyWebIOWarning(UserWarning):
+    pass

+ 16 - 8
pywebio/platform/aiohttp.py

@@ -9,7 +9,7 @@ from urllib.parse import urlparse
 from aiohttp import web
 
 from .tornado import open_webbrowser_on_server_started
-from .utils import make_applications, render_page
+from .utils import make_applications, render_page, cdn_validation
 from ..session import CoroutineBasedSession, ThreadBasedSession, register_session_implement_for_target, Session
 from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction
@@ -37,10 +37,11 @@ def _is_same_site(origin, host):
     return origin == host
 
 
-def _webio_handler(applications, websocket_settings, check_origin_func=_is_same_site):
+def _webio_handler(applications, cdn, websocket_settings, check_origin_func=_is_same_site):
     """获取用于Tornado进行整合的RequestHandle类
 
     :param dict applications: 任务名->任务函数 的映射
+    :param bool/str cdn: 是否从CDN加载前端静态资源. 支持传入URL来自定义CDN地址。
     :param callable check_origin_func: check_origin_func(origin, handler) -> bool
     :return: Tornado RequestHandle类
     """
@@ -58,7 +59,7 @@ def _webio_handler(applications, websocket_settings, check_origin_func=_is_same_
 
             app_name = request.query.getone('app', 'index')
             app = applications.get(app_name) or applications['index']
-            html = render_page(app, protocol='ws')
+            html = render_page(app, protocol='ws', cdn=cdn)
             return web.Response(body=html, content_type='text/html')
 
         ws = web.WebSocketResponse(**websocket_settings)
@@ -111,11 +112,13 @@ def _webio_handler(applications, websocket_settings, check_origin_func=_is_same_
     return wshandle
 
 
-def webio_handler(applications, allowed_origins=None, check_origin=None, websocket_settings=None):
+def webio_handler(applications, cdn=True, allowed_origins=None, check_origin=None, websocket_settings=None):
     """获取在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 bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。设置成 ``False`` 时会从PyWebIO应用部署URL的同级目录下加载静态资源。
+       支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
     :param callable check_origin: 请求来源检查函数。
     :param dict websocket_settings: 创建 aiohttp WebSocketResponse 时使用的参数。见 https://docs.aiohttp.org/en/stable/web_reference.html#websocketresponse
@@ -130,12 +133,14 @@ def webio_handler(applications, allowed_origins=None, check_origin=None, websock
 
     websocket_settings = websocket_settings or {}
 
+    cdn = cdn_validation(cdn, 'error')
+
     if check_origin is None:
         check_origin_func = partial(_check_origin, allowed_origins=allowed_origins or [])
     else:
         check_origin_func = lambda origin, handler: _is_same_site(origin, handler) or check_origin(origin)
 
-    return _webio_handler(applications=applications,
+    return _webio_handler(applications=applications,cdn=cdn,
                           check_origin_func=check_origin_func,
                           websocket_settings=websocket_settings)
 
@@ -158,7 +163,7 @@ def static_routes(prefix='/'):
 
 
 def start_server(applications, port=0, host='', debug=False,
-                 allowed_origins=None, check_origin=None,
+                 cdn=True, allowed_origins=None, check_origin=None,
                  auto_open_webbrowser=False,
                  websocket_settings=None,
                  **aiohttp_settings):
@@ -169,6 +174,7 @@ def start_server(applications, port=0, host='', debug=False,
     :param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
         通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
     :param bool debug: 是否开启asyncio的Debug模式
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
     :param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
     :param bool auto_open_webbrowser: 当服务启动后,是否自动打开浏览器来访问服务。(该操作需要操作系统支持)
@@ -183,8 +189,10 @@ def start_server(applications, port=0, host='', debug=False,
     if port == 0:
         port = get_free_port()
 
-    handler = webio_handler(applications, allowed_origins=allowed_origins, check_origin=check_origin,
-                            websocket_settings=websocket_settings)
+    cdn = cdn_validation(cdn, 'warn')
+
+    handler = webio_handler(applications, cdn=cdn, allowed_origins=allowed_origins,
+                            check_origin=check_origin, websocket_settings=websocket_settings)
 
     app = web.Application(**aiohttp_settings)
     app.router.add_routes([web.get('/', handler)])

+ 11 - 5
pywebio/platform/django.py

@@ -6,7 +6,7 @@ import os
 from django.http import HttpResponse, HttpRequest
 
 from .httpbased import HttpContext, HttpHandler, run_event_loop
-from .utils import make_applications
+from .utils import make_applications, cdn_validation
 from ..utils import STATIC_PATH, iscoroutinefunction, isgeneratorfunction, get_free_port
 
 logger = logging.getLogger(__name__)
@@ -72,7 +72,7 @@ class DjangoHttpContext(HttpContext):
         return self.request.META.get('REMOTE_ADDR')
 
 
-def webio_view(applications,
+def webio_view(applications, cdn=True,
                session_expire_seconds=None,
                session_cleanup_interval=None,
                allowed_origins=None, check_origin=None):
@@ -80,6 +80,8 @@ def webio_view(applications,
     基于http请求与前端进行通讯
 
     :param list/dict/callable applications: PyWebIO应用。
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。设置成 ``False`` 时会从PyWebIO应用部署URL的同级目录下加载静态资源。
+       支持传入自定义的URL来指定静态资源的部署地址
     :param int session_expire_seconds: 会话不活跃过期时间。
     :param int session_cleanup_interval: 会话清理间隔。
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
@@ -89,7 +91,8 @@ def webio_view(applications,
 
     :return: Django视图函数
     """
-    handler = HttpHandler(applications=applications,
+    cdn = cdn_validation(cdn, 'error')
+    handler = HttpHandler(applications=applications, cdn=cdn,
                           session_expire_seconds=session_expire_seconds,
                           session_cleanup_interval=session_cleanup_interval,
                           allowed_origins=allowed_origins, check_origin=check_origin)
@@ -107,7 +110,7 @@ def webio_view(applications,
 urlpatterns = []
 
 
-def start_server(applications, port=8080, host='localhost',
+def start_server(applications, port=8080, host='localhost', cdn=True,
                  allowed_origins=None, check_origin=None,
                  session_expire_seconds=None,
                  session_cleanup_interval=None,
@@ -118,6 +121,7 @@ def start_server(applications, port=8080, host='localhost',
     :param int port: 服务监听的端口。设置为 ``0`` 时,表示自动选择可用端口。
     :param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
         通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
     :param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
     :param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
@@ -140,6 +144,8 @@ def start_server(applications, port=8080, host='localhost',
     if not host:
         host = '0.0.0.0'
 
+    cdn = cdn_validation(cdn, 'warn')
+
     django_options.update(dict(
         DEBUG=debug,
         ALLOWED_HOSTS=["*"],  # Disable host header validation
@@ -170,7 +176,7 @@ def start_server(applications, port=8080, host='localhost',
     settings.configure(**django_options)
 
     webio_view_func = webio_view(
-        applications=applications,
+        applications=applications, cdn=cdn,
         session_expire_seconds=session_expire_seconds,
         session_cleanup_interval=session_cleanup_interval,
         allowed_origins=allowed_origins,

+ 12 - 5
pywebio/platform/flask.py

@@ -11,7 +11,7 @@ import threading
 from flask import Flask, request, send_from_directory, Response
 
 from .httpbased import HttpContext, HttpHandler, run_event_loop
-from .utils import make_applications
+from .utils import make_applications, cdn_validation
 from ..utils import STATIC_PATH, iscoroutinefunction, isgeneratorfunction
 from ..utils import get_free_port
 
@@ -78,13 +78,15 @@ class FlaskHttpContext(HttpContext):
         return request.remote_addr
 
 
-def webio_view(applications,
+def webio_view(applications, cdn=True,
                session_expire_seconds=None,
                session_cleanup_interval=None,
                allowed_origins=None, check_origin=None):
     """获取在Flask中运行PyWebIO任务的视图函数。基于http请求与前端页面进行通讯
 
     :param list/dict/callable applications: PyWebIO应用。
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。设置成 ``False`` 时会从PyWebIO应用部署URL的同级目录下加载静态资源。
+       支持传入自定义的URL来指定静态资源的部署地址
     :param int session_expire_seconds: 会话不活跃过期时间。
     :param int session_cleanup_interval: 会话清理间隔。
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
@@ -95,7 +97,9 @@ def webio_view(applications,
     :return: Flask视图函数
     """
 
-    handler = HttpHandler(applications=applications,
+    cdn = cdn_validation(cdn, 'error')
+
+    handler = HttpHandler(applications=applications, cdn=cdn,
                           session_expire_seconds=session_expire_seconds,
                           session_cleanup_interval=session_cleanup_interval,
                           allowed_origins=allowed_origins, check_origin=check_origin)
@@ -108,7 +112,7 @@ def webio_view(applications,
     return view_func
 
 
-def start_server(applications, port=8080, host='localhost',
+def start_server(applications, port=8080, host='localhost', cdn=True,
                  allowed_origins=None, check_origin=None,
                  session_expire_seconds=None,
                  session_cleanup_interval=None,
@@ -119,6 +123,7 @@ def start_server(applications, port=8080, host='localhost',
     :param int port: 服务监听的端口。设置为 ``0`` 时,表示自动选择可用端口。
     :param str host: 服务绑定的地址。 ``host`` 可以是IP地址或者为hostname。如果为hostname,服务会监听所有与该hostname关联的IP地址。
         通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``allowed_origins`` 参数
     :param callable check_origin: 请求来源检查函数。格式同 :func:`pywebio.platform.tornado.start_server` 的 ``check_origin`` 参数
     :param int session_expire_seconds: 会话过期时间。若 session_expire_seconds 秒内没有收到客户端的请求,则认为会话过期。
@@ -133,9 +138,11 @@ def start_server(applications, port=8080, host='localhost',
     if port == 0:
         port = get_free_port()
 
+    cdn = cdn_validation(cdn, 'warn')
+
     app = Flask(__name__)
     app.add_url_rule('/', 'webio_view', webio_view(
-        applications=applications,
+        applications=applications, cdn=cdn,
         session_expire_seconds=session_expire_seconds,
         session_cleanup_interval=session_cleanup_interval,
         allowed_origins=allowed_origins,

+ 4 - 2
pywebio/platform/httpbased.py

@@ -176,7 +176,7 @@ class HttpHandler:
         if 'webio-session-id' not in request_headers:
             app_name = context.request_url_parameter('app', 'index')
             app = self.applications.get(app_name) or self.applications['index']
-            html = render_page(app, protocol='http')
+            html = render_page(app, protocol='http', cdn=self.cdn)
             context.set_content(html)
             return context.get_response()
 
@@ -230,13 +230,14 @@ class HttpHandler:
 
         return context.get_response()
 
-    def __init__(self, applications,
+    def __init__(self, applications, cdn,
                  session_expire_seconds=None,
                  session_cleanup_interval=None,
                  allowed_origins=None, check_origin=None):
         """获取用于与后端实现进行整合的view函数,基于http请求与前端进行通讯
 
         :param list/dict/callable applications: PyWebIO应用. 可以是任务函数或者任务函数的字典或列表。
+        :param bool/str cdn: 是否从CDN加载前端静态资源. 支持传入URL来自定义CDN地址。
         :param int session_expire_seconds: 会话不活跃过期时间。
         :param int session_cleanup_interval: 会话清理间隔。
         :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
@@ -255,6 +256,7 @@ class HttpHandler:
 
         cls = type(self)
 
+        self.cdn = cdn
         self.applications = make_applications(applications)
         self.check_origin = check_origin
         self.session_expire_seconds = session_expire_seconds or cls.DEFAULT_SESSION_EXPIRE_SECONDS

+ 20 - 8
pywebio/platform/tornado.py

@@ -14,11 +14,11 @@ import tornado.ioloop
 from tornado.web import StaticFileHandler
 from tornado.websocket import WebSocketHandler
 
+from .utils import make_applications, render_page, cdn_validation
 from ..session import CoroutineBasedSession, ThreadBasedSession, ScriptModeSession, \
     register_session_implement_for_target, Session
 from ..session.base import get_session_info_from_headers
 from ..utils import get_free_port, wait_host_port, STATIC_PATH, iscoroutinefunction, isgeneratorfunction, check_webio_js
-from .utils import make_applications, render_page
 
 logger = logging.getLogger(__name__)
 
@@ -52,10 +52,11 @@ def _is_same_site(origin, handler: WebSocketHandler):
     return origin == host
 
 
-def _webio_handler(applications, check_origin_func=_is_same_site):
+def _webio_handler(applications, cdn, check_origin_func=_is_same_site):
     """获取用于Tornado进行整合的RequestHandler类
 
     :param dict applications: 任务名->任务函数 的字典
+    :param bool/str cdn:
     :param callable check_origin_func: check_origin_func(origin, handler) -> bool
     :return: Tornado RequestHandler类
     """
@@ -72,7 +73,7 @@ def _webio_handler(applications, check_origin_func=_is_same_site):
 
                 app_name = self.get_query_argument('app', 'index')
                 app = applications.get(app_name) or applications['index']
-                html = render_page(app, protocol='ws')
+                html = render_page(app, protocol='ws', cdn=cdn)
                 return self.write(html)
             else:
                 await super().get()
@@ -128,10 +129,12 @@ def _webio_handler(applications, check_origin_func=_is_same_site):
     return WSHandler
 
 
-def webio_handler(applications, allowed_origins=None, check_origin=None):
+def webio_handler(applications, cdn=True, allowed_origins=None, check_origin=None):
     """获取在Tornado中运行PyWebIO应用的RequestHandler类。RequestHandler类基于WebSocket协议与浏览器进行通讯。
 
     :param callable/list/dict applications: PyWebIO应用。
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。设置成 ``False`` 时会从PyWebIO应用部署URL的同级目录下加载静态资源。
+       支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
     :param callable check_origin: 请求来源检查函数。
 
@@ -143,12 +146,14 @@ def webio_handler(applications, allowed_origins=None, check_origin=None):
     for target in applications.values():
         register_session_implement_for_target(target)
 
+    cdn = cdn_validation(cdn, 'error')
+
     if check_origin is None:
         check_origin_func = partial(_check_origin, allowed_origins=allowed_origins or [])
     else:
         check_origin_func = lambda origin, handler: _is_same_site(origin, handler) or check_origin(origin)
 
-    return _webio_handler(applications=applications, check_origin_func=check_origin_func)
+    return _webio_handler(applications=applications, cdn=cdn, check_origin_func=check_origin_func)
 
 
 async def open_webbrowser_on_server_started(host, port):
@@ -173,7 +178,8 @@ def _setup_server(webio_handler, port=0, host='', **tornado_app_settings):
     return server, port
 
 
-def start_server(applications, port=0, host='', debug=False,
+def start_server(applications, port=0, host='',
+                 debug=False, cdn=True,
                  allowed_origins=None, check_origin=None,
                  auto_open_webbrowser=False,
                  websocket_max_message_size=None,
@@ -198,6 +204,7 @@ def start_server(applications, port=0, host='', debug=False,
        通过设置 ``host`` 为空字符串或 ``None`` 来将服务绑定到所有可用的地址上。
     :param bool debug: 是否开启Tornado Server的debug模式,开启后,代码发生修改后服务器会自动重启。
        详情请参阅 `tornado 文档 <https://www.tornadoweb.org/en/stable/guide/running.html#debug-mode>`_
+    :param bool/str cdn: 是否从CDN加载前端静态资源,默认为 ``True`` 。支持传入自定义的URL来指定静态资源的部署地址
     :param list allowed_origins: 除当前域名外,服务器还允许的请求的来源列表。
         来源包含协议、域名和端口部分,允许使用 Unix shell 风格的匹配模式(全部规则参见 `Python文档 <https://docs.python.org/zh-tw/3/library/fnmatch.html>`_ ):
 
@@ -218,6 +225,9 @@ def start_server(applications, port=0, host='', debug=False,
         内收到‘pong’消息,应用会将连接关闭。默认的超时时间为 ``websocket_ping_interval`` 的三倍。
     :param tornado_app_settings: 传递给 ``tornado.web.Application`` 构造函数的额外的关键字参数
         可设置项参考: https://www.tornadoweb.org/en/stable/web.html#tornado.web.Application.settings
+
+    .. versionadded:: 1.1
+        The *cdn* parameter.
     """
     kwargs = locals()
     global _ioloop
@@ -228,7 +238,9 @@ def start_server(applications, port=0, host='', debug=False,
         if kwargs[opt] is not None:
             tornado_app_settings[opt] = kwargs[opt]
 
-    handler = webio_handler(applications, allowed_origins=allowed_origins, check_origin=check_origin)
+    cdn = cdn_validation(cdn, 'warn')
+
+    handler = webio_handler(applications, cdn, allowed_origins=allowed_origins, check_origin=check_origin)
     _, port = _setup_server(webio_handler=handler, port=port, host=host, **tornado_app_settings)
 
     print('Listen on %s:%s' % (host or '0.0.0.0', port))
@@ -248,7 +260,7 @@ def start_server_in_current_thread_session():
 
     mock_apps = dict(index=lambda: None)
 
-    class SingleSessionWSHandler(_webio_handler(applications=mock_apps)):
+    class SingleSessionWSHandler(_webio_handler(applications=mock_apps, cdn=False)):
         session = None
         instance = None
 

+ 22 - 22
pywebio/platform/tpl/index.html

@@ -7,12 +7,12 @@
     <meta name="description" content="{{ description }}">
     <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">
     <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">
-    <link rel="stylesheet" href="css/markdown.min.css">
-    <link rel="stylesheet" href="css/bootstrap.min.css">
-    <link rel="stylesheet" href="css/codemirror.min.css">
-    <link rel="stylesheet" href="codemirror/base16-light.min.css">
-    <link rel="stylesheet" href="css/toastify.min.css">
-    <link rel="stylesheet" href="css/app.css">
+    <link rel="stylesheet" href="{{ base_url }}css/markdown.min.css">
+    <link rel="stylesheet" href="{{ base_url }}css/bootstrap.min.css">
+    <link rel="stylesheet" href="{{ base_url }}css/codemirror.min.css">
+    <link rel="stylesheet" href="{{ base_url }}codemirror/base16-light.min.css">
+    <link rel="stylesheet" href="{{ base_url }}css/toastify.min.css">
+    <link rel="stylesheet" href="{{ base_url }}css/app.css">
 </head>
 <body>
 <div class="pywebio">
@@ -34,24 +34,24 @@
     Powered by <a href="https://github.com/wang0618/PyWebIO" target="_blank">PyWebIO</a>
 </footer>
 
-<script src="js/mustache.min.js"></script>  <!--template system-->
-<script src="js/codemirror.min.js"></script>  <!--code textarea editor-->
-<script src="codemirror/matchbrackets.js"></script>  <!--codemirror plugin-->
-<script src="codemirror/python.js"></script> <!--codemirror python language support-->
-<script src="codemirror/loadmode.js"></script> <!--codemirror plugin: auto load mode-->
-<script src="codemirror/active-line.js"></script> <!--codemirror plugin: auto load mode-->
-<script src="js/prism.min.js"></script>  <!-- markdown code highlight -->
-<script src="js/FileSaver.min.js"></script>  <!-- saving files on the client-side -->
-<script src="js/jquery.min.js"></script>
-<script src="js/popper.min.js"></script>  <!-- tooltip engine -->
-<script src="js/bootstrap.min.js"></script>
-<script src="js/toastify.min.js"></script> <!-- toast -->
-<script src="js/bs-custom-file-input.min.js"></script> <!-- bootstrap custom file input-->
-<script src="js/purify.min.js"></script>  <!-- XSS sanitizer -->
+<script src="{{ base_url }}js/mustache.min.js"></script>  <!--template system-->
+<script src="{{ base_url }}js/codemirror.min.js"></script>  <!--code textarea editor-->
+<script src="{{ base_url }}codemirror/matchbrackets.js"></script>  <!--codemirror plugin-->
+<script src="{{ base_url }}codemirror/python.js"></script> <!--codemirror python language support-->
+<script src="{{ base_url }}codemirror/loadmode.js"></script> <!--codemirror plugin: auto load mode-->
+<script src="{{ base_url }}codemirror/active-line.js"></script> <!--codemirror plugin: auto load mode-->
+<script src="{{ base_url }}js/prism.min.js"></script>  <!-- markdown code highlight -->
+<script src="{{ base_url }}js/FileSaver.min.js"></script>  <!-- saving files on the client-side -->
+<script src="{{ base_url }}js/jquery.min.js"></script>
+<script src="{{ base_url }}js/popper.min.js"></script>  <!-- tooltip engine -->
+<script src="{{ base_url }}js/bootstrap.min.js"></script>
+<script src="{{ base_url }}js/toastify.min.js"></script> <!-- toast -->
+<script src="{{ base_url }}js/bs-custom-file-input.min.js"></script> <!-- bootstrap custom file input-->
+<script src="{{ base_url }}js/purify.min.js"></script>  <!-- XSS sanitizer -->
 
-<script src="js/pywebio.min.js"></script>
+<script src="{{ base_url }}js/pywebio.min.js"></script>
 
-<script src="js/require.min.js"></script> <!-- JS module loader -->
+<script src="{{ base_url }}js/require.min.js"></script> <!-- JS module loader -->
 {% if script %}
 <script>
 

+ 34 - 4
pywebio/platform/utils.py

@@ -6,7 +6,12 @@ from os import path
 
 from tornado import template
 
-from ..utils import isgeneratorfunction, iscoroutinefunction, get_function_name, get_function_doc, get_function_seo_info
+from ..__version__ import __version__ as version
+from ..exceptions import PyWebIOWarning
+from ..utils import isgeneratorfunction, iscoroutinefunction, get_function_name, get_function_doc,\
+    get_function_seo_info
+
+DEFAULT_CDN = "https://cdn.jsdelivr.net/gh/wang0618/PyWebIO-assets@v{version}/"
 
 AppMeta = namedtuple('App', 'title description')
 
@@ -14,18 +19,43 @@ _here_dir = path.dirname(path.abspath(__file__))
 _index_page_tpl = template.Template(open(path.join(_here_dir, 'tpl', 'index.html')).read())
 
 
-def render_page(app, protocol):
+def render_page(app, protocol, cdn):
     """渲染前端页面的HTML框架, 支持SEO
 
     :param callable app: PyWebIO app
     :param str protocol: 'ws'/'http'
-    :return: bytes
+    :param bool/str cdn: Whether to use CDN, also accept string as custom CDN URL
+    :return: bytes content of rendered page
     """
     assert protocol in ('ws', 'http')
     meta = parse_app_metadata(app)
+    if cdn is True:
+        cdn = DEFAULT_CDN.format(version=version)
+    elif not cdn:
+        cdn = ''
     return _index_page_tpl.generate(title=meta.title or 'PyWebIO Application',
                                     description=meta.description, protocol=protocol,
-                                    script=True, content='')
+                                    script=True, content='', base_url=cdn)
+
+
+def cdn_validation(cdn, level='warn'):
+    """CDN availability check
+
+    :param bool/str cdn: cdn parameter
+    :param level: warn or error
+    """
+    assert level in ('warn', 'error')
+
+    if cdn is True and 'dev' in version:
+        if level == 'warn':
+            import warnings
+            warnings.warn("Default CDN is not supported in dev version. Ignore the CDN setting", PyWebIOWarning,
+                          stacklevel=3)
+            return False
+        else:
+            raise ValueError("Default CDN is not supported in dev version. Please host static files by yourself.")
+
+    return cdn
 
 
 def parse_app_metadata(func):

+ 1 - 1
test/1.basic.py

@@ -38,7 +38,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1', auto_open_webbrowser=False)
+    start_server(target, port=8080, host='127.0.0.1', auto_open_webbrowser=False, cdn=False)
 
 
 if __name__ == '__main__':

+ 4 - 4
test/10.aiohttp_multiple_session_impliment.py

@@ -38,7 +38,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(1)
     template.save_output(browser, '10.aiohttp_multiple_session_impliment_p1.html')
 
-    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2')
+    browser.get('http://localhost:8080/io2?_pywebio_debug=1')
     template.test_output(browser)
     time.sleep(1)
     template.test_input(browser)
@@ -51,12 +51,12 @@ def start_test_server():
     pywebio.enable_debug()
 
     app = web.Application()
-    app.add_routes([web.get('/io', webio_handler(target))])
-    app.add_routes([web.get('/io2', webio_handler(async_target))])
+    app.add_routes([web.get('/io', webio_handler(target, cdn=False))])
+    app.add_routes([web.get('/io2', webio_handler(async_target, cdn=False))])
     app.add_routes(static_routes())
 
     web.run_app(app, host='127.0.0.1', port=8080)
 
 
 if __name__ == '__main__':
-    util.run_test(start_test_server, test)
+    util.run_test(start_test_server, test, 'http://localhost:8080/io?_pywebio_debug=1')

+ 5 - 2
test/11.charts.py

@@ -188,6 +188,9 @@ def pyecharts():
     from pyecharts.charts import Polar
     from pyecharts.charts import HeatMap
     from pyecharts.charts import Tree
+    from pyecharts.globals import CurrentConfig
+
+    CurrentConfig.ONLINE_HOST = "https://cdn.jsdelivr.net/gh/pyecharts/pyecharts-assets@master/assets/"
 
     r1 = ['草莓', '芒果', '葡萄', '雪梨', '西瓜', '柠檬', '车厘子']
     r2 = [127, 33, 110, 29, 146, 121, 36]
@@ -346,7 +349,7 @@ def target():
     from bokeh.io import output_notebook
     from bokeh.io import show
 
-    output_notebook(verbose=False, notebook_type='pywebio')
+    output_notebook(verbose=True, notebook_type='pywebio')
 
     put_markdown('# Bokeh')
 
@@ -379,7 +382,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(target, port=8080, auto_open_webbrowser=False)
+    start_server(target, port=8080, auto_open_webbrowser=False, cdn=False)
 
 
 if __name__ == '__main__':

+ 3 - 3
test/12.cors.py

@@ -57,7 +57,7 @@ def start_test_server():
     util.start_static_server()
 
     try:
-        tornado_server(target, port=8080, host='127.0.0.1', allowed_origins=['http://localhost:5000'])
+        tornado_server(target, port=8080, host='127.0.0.1', allowed_origins=['http://localhost:5000'], cdn=False)
     except:
         print('tornado_server exit')
 
@@ -65,12 +65,12 @@ def start_test_server():
     asyncio.set_event_loop(loop)
 
     try:
-        aiohttp_server(target, port=8081, host='127.0.0.1', allowed_origins=['http://localhost:5000'])
+        aiohttp_server(target, port=8081, host='127.0.0.1', allowed_origins=['http://localhost:5000'], cdn=False)
     except:
         print('aiohttp_server exit')
 
     try:
-        flask_server(target, port=8082, host='127.0.0.1', allowed_origins=['http://localhost:5000'])
+        flask_server(target, port=8082, host='127.0.0.1', allowed_origins=['http://localhost:5000'], cdn=False)
     except:
         print('flask_server exit')
 

+ 1 - 1
test/13.misc.py

@@ -176,7 +176,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server([corobased, partial(threadbased)], port=8080, host='127.0.0.1', debug=True)
+    start_server([corobased, partial(threadbased)], port=8080, host='127.0.0.1', debug=True, cdn=False)
 
 
 if __name__ == '__main__':

+ 4 - 4
test/14.django_multiple_session_impliment.py

@@ -35,7 +35,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(1)
     template.save_output(browser, '14.flask_multiple_session_impliment_p1.html')
 
-    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2&_pywebio_http_pull_interval=400')
+    browser.get('http://localhost:8080/app2?_pywebio_debug=1&_pywebio_http_pull_interval=400')
     template.test_output(browser)
     time.sleep(1)
     template.test_input(browser)
@@ -90,8 +90,8 @@ def start_test_server():
     settings.configure(**django_options)
 
     urlpatterns = [
-        path(r"io", webio_view(target)),
-        path(r"io2", webio_view(async_target)),
+        path(r"app", webio_view(target, cdn=False)),
+        path(r"app2", webio_view(async_target, cdn=False)),
         path(r'', partial(serve, path='index.html'), {'document_root': STATIC_PATH}),
         path(r'<path:path>', serve, {'document_root': STATIC_PATH}),
     ]
@@ -107,4 +107,4 @@ def start_test_server():
 
 if __name__ == '__main__':
     util.run_test(start_test_server, test,
-                  address='http://localhost:8080?_pywebio_debug=1&_pywebio_http_pull_interval=400')
+                  address='http://localhost:8080/app?_pywebio_debug=1&_pywebio_http_pull_interval=400')

+ 1 - 1
test/3.django_backend.py

@@ -34,7 +34,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 def start_test_server():
     pywebio.enable_debug()
 
-    start_server(target, port=8080, host='127.0.0.1')
+    start_server(target, port=8080, host='127.0.0.1', cdn=False)
 
 
 if __name__ == '__main__':

+ 1 - 1
test/4.flask_backend.py

@@ -33,7 +33,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1')
+    start_server(target, port=8080, host='127.0.0.1', cdn=False)
 
 
 if __name__ == '__main__':

+ 1 - 1
test/5.coroutine_based_session.py

@@ -35,7 +35,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1')
+    start_server(target, port=8080, host='127.0.0.1', cdn=False)
 
 
 if __name__ == '__main__':

+ 1 - 1
test/6.flask_coroutine.py

@@ -34,7 +34,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 
 def start_test_server():
     pywebio.enable_debug()
-    start_server(target, port=8080, host='127.0.0.1')
+    start_server(target, port=8080, host='127.0.0.1', cdn=False)
 
 
 if __name__ == '__main__':

+ 3 - 3
test/7.multiple_session_impliment.py

@@ -35,7 +35,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(1)
     template.save_output(browser, '7.multiple_session_impliment_p1.html')
 
-    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2')
+    browser.get('http://localhost:8080/io2?_pywebio_debug=1')
     template.test_output(browser)
     time.sleep(1)
     template.test_input(browser)
@@ -53,8 +53,8 @@ def start_test_server():
     from pywebio import STATIC_PATH
 
     application = tornado.web.Application([
-        (r"/io", webio_handler(async_target)),
-        (r"/io2", webio_handler(target)),
+        (r"/", webio_handler(async_target, cdn=False)),
+        (r"/io2", webio_handler(target, cdn=False)),
         (r"/(.*)", tornado.web.StaticFileHandler,
          {"path": STATIC_PATH, 'default_filename': 'index.html'})
     ])

+ 4 - 4
test/8.flask_multiple_session_impliment.py

@@ -36,7 +36,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
     time.sleep(1)
     template.save_output(browser, '8.flask_multiple_session_impliment_p1.html')
 
-    browser.get('http://localhost:8080?_pywebio_debug=1&pywebio_api=io2&_pywebio_http_pull_interval=400')
+    browser.get('http://localhost:8080/io2?_pywebio_debug=1&_pywebio_http_pull_interval=400')
     template.test_output(browser)
     time.sleep(1)
     template.test_input(browser)
@@ -54,8 +54,8 @@ def start_test_server():
     import logging
 
     app = Flask(__name__)
-    app.add_url_rule('/io', 'webio_view', webio_view(target), methods=['GET', 'POST', 'OPTIONS'])
-    app.add_url_rule('/io2', 'webio_view_async_target', webio_view(async_target), methods=['GET', 'POST', 'OPTIONS'])
+    app.add_url_rule('/io', 'webio_view', webio_view(target, cdn=False), methods=['GET', 'POST', 'OPTIONS'])
+    app.add_url_rule('/io2', 'webio_view_async_target', webio_view(async_target, cdn=False), methods=['GET', 'POST', 'OPTIONS'])
 
     @app.route('/')
     @app.route('/<path:static_file>')
@@ -70,4 +70,4 @@ def start_test_server():
 
 
 if __name__ == '__main__':
-    util.run_test(start_test_server, test, address='http://localhost:8080?_pywebio_debug=1&_pywebio_http_pull_interval=400')
+    util.run_test(start_test_server, test, address='http://localhost:8080/io?_pywebio_debug=1&_pywebio_http_pull_interval=400')

+ 1 - 1
test/9.aiohttp_backend.py

@@ -34,7 +34,7 @@ def test(server_proc: subprocess.Popen, browser: Chrome):
 def start_test_server():
     pywebio.enable_debug()
 
-    start_server(target, port=8080, host='127.0.0.1')
+    start_server(target, port=8080, host='127.0.0.1', cdn=False)
 
 
 if __name__ == '__main__':