WIP: Use Flask-Assets to merge js, css and less files

Should reduce number of requests and hence load times needed for loading web interface.

TODO: Handling of empty bundles needs to be fixed
This commit is contained in:
Gina Häußge 2015-06-03 16:42:57 +02:00
parent db980689c8
commit 157b78a052
9 changed files with 246 additions and 84 deletions

View file

@ -21,6 +21,7 @@ INSTALL_REQUIRES = [
"Flask-Login==0.2.2",
"Flask-Principal==0.3.5",
"Flask-Babel==0.9",
"Flask-Assets",
"pyserial",
"netaddr",
"watchdog",

View file

@ -648,12 +648,24 @@ class BlueprintPlugin(OctoPrintPlugin, RestartNeedingPlugin):
``template_folder``, etc.
Defaults to the blueprint's ``static_folder`` and ``template_folder`` to be set to the plugin's basefolder
plus ``/static`` or respectively ``/templates``.
plus ``/static`` or respectively ``/templates``, or -- if the plugin also implements :class:`AssetPlugin` and/or
:class:`TemplatePlugin` -- the paths provided by ``get_asset_folder`` and ``get_template_folder`` respectively.
"""
import os
if isinstance(self, AssetPlugin):
static_folder = self.get_asset_folder()
else:
static_folder = os.path.join(self._basefolder, "static")
if isinstance(self, TemplatePlugin):
template_folder = self.get_template_folder()
else:
template_folder = os.path.join(self._basefolder, "templates")
return dict(
static_folder=os.path.join(self._basefolder, "static"),
template_folder=os.path.join(self._basefolder, "templates")
static_folder=static_folder,
template_folder=template_folder
)
def is_blueprint_protected(self):

View file

@ -7,10 +7,11 @@ __copyright__ = "Copyright (C) 2014 The OctoPrint Project - Released under terms
import uuid
from sockjs.tornado import SockJSRouter
from flask import Flask, g, request, session
from flask import Flask, g, request, session, Blueprint
from flask.ext.login import LoginManager, current_user
from flask.ext.principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed
from flask.ext.babel import Babel, gettext, ngettext
from flask.ext.assets import Environment, Bundle
from babel import Locale
from watchdog.observers import Observer
from collections import defaultdict
@ -24,6 +25,7 @@ SUCCESS = {}
NO_CONTENT = ("", 204)
app = Flask("octoprint")
assets = None
babel = None
debug = False
@ -232,6 +234,9 @@ class Server():
pluginLifecycleManager.add_callback("enabled", template_enabled)
pluginLifecycleManager.add_callback("disabled", template_disabled)
# setup assets
self._setup_assets()
# configure timelapse
octoprint.timelapse.configureTimelapse()
@ -240,6 +245,7 @@ class Server():
if self._debug:
events.DebugEventListener()
# setup access control
if s.getBoolean(["accessControl", "enabled"]):
userManagerName = s.get(["accessControl", "userManager"])
try:
@ -300,10 +306,14 @@ class Server():
upload_suffixes = dict(name=s.get(["server", "uploads", "nameSuffix"]), path=s.get(["server", "uploads", "pathSuffix"]))
server_routes = self._router.urls + [
# various downloads
(r"/downloads/timelapse/([^/]*\.mpg)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("timelapse"), as_attachment=True)),
(r"/downloads/files/local/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("uploads"), as_attachment=True, path_validation=util.tornado.path_validation_factory(lambda path: not os.path.basename(path).startswith("."), status_code=404))),
(r"/downloads/logs/([^/]*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("logs"), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.admin_validator))),
# camera snapshot
(r"/downloads/camera/current", util.tornado.UrlForwardHandler, dict(url=s.get(["webcam", "snapshot"]), as_attachment=True, access_validation=util.tornado.access_validation_factory(app, loginManager, util.flask.user_validator))),
# generated webassets
(r"/static/webassets/(.*)", util.tornado.LargeResponseHandler, dict(path=s.getBaseFolder("webassets")))
]
for name, hook in pluginManager.get_hooks("octoprint.server.http.routes").items():
try:
@ -610,11 +620,21 @@ class Server():
# also register any blueprints defined in BlueprintPlugins
self._register_blueprint_plugins()
# and register a blueprint for serving the static files of asset plugins which are not blueprint plugins themselves
self._register_asset_plugins()
def _register_blueprint_plugins(self):
blueprint_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.BlueprintPlugin)
for plugin in blueprint_plugins:
self._register_blueprint_plugin(plugin)
def _register_asset_plugins(self):
asset_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.AssetPlugin)
for plugin in asset_plugins:
if isinstance(plugin, octoprint.plugin.BlueprintPlugin):
continue
self._register_asset_plugin(plugin)
def _register_blueprint_plugin(self, plugin):
name = plugin._identifier
blueprint = plugin.get_blueprint()
@ -632,6 +652,100 @@ class Server():
if self._logger:
self._logger.debug("Registered API of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix))
def _register_asset_plugin(self, plugin):
name = plugin._identifier
url_prefix = "/plugin/{name}".format(name=name)
blueprint = Blueprint("plugin." + name, name, static_folder=plugin.get_asset_folder())
app.register_blueprint(blueprint, url_prefix=url_prefix)
if self._logger:
self._logger.debug("Registered assets of plugin {name} under URL prefix {url_prefix}".format(name=name, url_prefix=url_prefix))
def _setup_assets(self):
global app
global assets
global pluginManager
AdjustedEnvironment = type(Environment)(Environment.__name__, (Environment,), dict(
resolver_class=util.flask.PluginAssetResolver
))
assets = AdjustedEnvironment(app)
dynamic_assets = util.flask.collect_plugin_assets()
js_libs = [
"js/lib/jquery/jquery.min.js",
"js/lib/modernizr.custom.js",
"js/lib/lodash.min.js",
"js/lib/sprintf.min.js",
"js/lib/knockout.js",
"js/lib/knockout.mapping-latest.js",
"js/lib/babel.js",
"js/lib/avltree.js",
"js/lib/bootstrap/bootstrap.js",
"js/lib/bootstrap/bootstrap-modalmanager.js",
"js/lib/bootstrap/bootstrap-modal.js",
"js/lib/bootstrap/bootstrap-slider.js",
"js/lib/bootstrap/bootstrap-tabdrop.js",
"js/lib/jquery/jquery.ui.core.js",
"js/lib/jquery/jquery.ui.widget.js",
"js/lib/jquery/jquery.ui.mouse.js",
"js/lib/jquery/jquery.flot.js",
"js/lib/jquery/jquery.iframe-transport.js",
"js/lib/jquery/jquery.fileupload.js",
"js/lib/jquery/jquery.slimscroll.min.js",
"js/lib/jquery/jquery.qrcode.min.js",
"js/lib/sockjs-0.3.4.min.js",
"js/lib/moment-with-locales.min.js",
"js/lib/pusher.color.min.js",
"js/lib/detectmobilebrowser.js",
"js/lib/md5.min.js",
"js/lib/pnotify.min.js",
"js/lib/bootstrap-slider-knockout-binding.js",
"js/lib/loglevel.min.js"
]
js_app = dynamic_assets["js"] + [
"js/app/dataupdater.js",
"js/app/helpers.js",
"js/app/main.js",
]
css_libs = [
"css/bootstrap.min.css",
"css/bootstrap-modal.css",
"css/bootstrap-slider.css",
"css/bootstrap-tabdrop.css",
"css/font-awesome.min.css",
"css/jquery.fileupload-ui.css",
"css/pnotify.min.css"
]
css_app = []
less_app = []
for sheet, path in dynamic_assets["stylesheets"]:
if sheet == "css":
css_app.append(path)
elif sheet == "less":
less_app.append(path)
js_libs_bundle = Bundle(*js_libs, output="webassets/packed_libs.js")
js_app_bundle = Bundle(*js_app, output="webassets/package_app.js")
css_libs_bundle = Bundle(*css_libs, output="webassets/packed_libs.css")
assets.register("js_libs", js_libs_bundle)
assets.register("js_app", js_app_bundle)
assets.register("css_libs", css_libs_bundle)
if len(css_app):
css_app_bundle = Bundle(*css_app, output="webassets/packed_app.css")
assets.register("css_app", css_app_bundle)
if len(less_app):
less_app_bundle = Bundle(*less_app, output="webassets/packed_app.less")
assets.register("less_app", less_app_bundle)
class LifecycleManager(object):
def __init__(self, plugin_manager):
self._plugin_manager = plugin_manager

View file

@ -32,6 +32,9 @@ def apiKeyRequestHandler():
if _flask.request.method == 'OPTIONS' and settings().getBoolean(["api", "allowCrossOrigin"]):
return optionsAllowOrigin(_flask.request)
if _flask.request.endpoint == "static" or _flask.request.endpoint.endswith(".static"):
return
apikey = get_api_key(_flask.request)
if apikey is None:
# no api key => 401

View file

@ -10,6 +10,7 @@ import tornado.web
import flask
import flask.ext.login
import flask.ext.principal
import flask.ext.assets
import functools
import time
import uuid
@ -20,6 +21,7 @@ import netaddr
from octoprint.settings import settings
import octoprint.server
import octoprint.users
import octoprint.plugin
from werkzeug.contrib.cache import SimpleCache
@ -32,8 +34,6 @@ def enable_additional_translations(default_locale="en", additional_folders=None)
from babel import support, Locale
import flask.ext.babel
import octoprint.plugin
if additional_folders is None:
additional_folders = []
@ -411,3 +411,91 @@ def get_json_command_from_request(request, valid_commands):
return None, None, make_response("Mandatory parameter %s missing for command %s" % (parameter, command), 400)
return command, data, None
##~~ Flask-Assets resolver with plugin asset support
class PluginAssetResolver(flask.ext.assets.FlaskResolver):
def split_prefix(self, item):
if item.startswith("plugin/"):
try:
prefix, plugin, name = item.split("/", 2)
blueprint = prefix + "." + plugin
directory = flask.ext.assets.get_static_folder(self.env._app.blueprints[blueprint])
item = name
return directory, item
except (ValueError, KeyError):
pass
return flask.ext.assets.FlaskResolver.split_prefix(self, item)
def resolve_output_to_path(self, target, bundle):
if target.startswith("webassets/"):
import os
return os.path.normpath(os.path.join(settings().getBaseFolder("webassets"), target[len("webassets/"):]))
return flask.ext.assets.FlaskResolver.resolve_output_to_path(self, target, bundle)
##~~ plugin assets collector
def collect_plugin_assets(enable_gcodeviewer=True, enable_timelapse=True, preferred_stylesheet="css"):
supported_stylesheets = ("css", "less")
assets = dict(
js=[],
stylesheets=[]
)
assets["js"] = [
'js/app/viewmodels/appearance.js',
'js/app/viewmodels/connection.js',
'js/app/viewmodels/control.js',
'js/app/viewmodels/firstrun.js',
'js/app/viewmodels/files.js',
'js/app/viewmodels/loginstate.js',
'js/app/viewmodels/navigation.js',
'js/app/viewmodels/printerstate.js',
'js/app/viewmodels/printerprofiles.js',
'js/app/viewmodels/settings.js',
'js/app/viewmodels/slicing.js',
'js/app/viewmodels/temperature.js',
'js/app/viewmodels/terminal.js',
'js/app/viewmodels/users.js',
'js/app/viewmodels/log.js',
'js/app/viewmodels/usersettings.js'
]
if enable_gcodeviewer:
assets["js"] += [
'js/app/viewmodels/gcode.js',
'gcodeviewer/js/ui.js',
'gcodeviewer/js/gCodeReader.js',
'gcodeviewer/js/renderer.js'
]
if enable_timelapse:
assets["js"].append('js/app/viewmodels/timelapse.js')
if preferred_stylesheet == "less":
assets["stylesheets"].append(("less", 'less/octoprint.less'))
elif preferred_stylesheet == "css":
assets["stylesheets"].append(("css", 'css/octoprint.css'))
asset_plugins = octoprint.plugin.plugin_manager().get_implementations(octoprint.plugin.AssetPlugin)
for implementation in asset_plugins:
name = implementation._identifier
all_assets = implementation.get_assets()
if "js" in all_assets:
for asset in all_assets["js"]:
assets["js"].append('plugin/{name}/{asset}'.format(**locals()))
if preferred_stylesheet in all_assets:
for asset in all_assets[preferred_stylesheet]:
assets["stylesheets"].append((preferred_stylesheet, 'plugin/{name}/{asset}'.format(**locals())))
else:
for stylesheet in supported_stylesheets:
if not stylesheet in all_assets:
continue
for asset in all_assets[stylesheet]:
assets["stylesheets"].append((stylesheet, 'plugin/{name}/{asset}'.format(**locals())))
break
return assets

View file

@ -8,7 +8,7 @@ __copyright__ = "Copyright (C) 2015 The OctoPrint Project - Released under terms
import os
from collections import defaultdict
from flask import request, g, url_for, make_response, render_template, send_from_directory
from flask import request, g, url_for, make_response, render_template, send_from_directory, redirect
import octoprint.plugin
@ -491,19 +491,6 @@ def localeJs(locale, domain):
@app.route("/plugin_assets/<string:name>/<path:filename>")
def plugin_assets(name, filename):
asset_plugins = pluginManager.get_filtered_implementations(lambda p: p._identifier == name, octoprint.plugin.AssetPlugin)
if not asset_plugins:
return make_response("Asset not found", 404)
if len(asset_plugins) > 1:
return make_response("More than one asset provider for {name}, can't proceed".format(name=name), 500)
asset_plugin = asset_plugins[0]
asset_folder = asset_plugin.get_asset_folder()
if asset_folder is None:
return make_response("Asset not found", 404)
return send_from_directory(asset_folder, filename)
return redirect(url_for("plugin." + name + ".static", filename=filename))

View file

@ -155,7 +155,8 @@ default_settings = {
"slicingProfiles": None,
"printerProfiles": None,
"scripts": None,
"translations": None
"translations": None,
"webassets": None
},
"temperature": {
"profiles": [

View file

@ -1,48 +1,11 @@
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/modernizr.custom.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/lodash.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sprintf.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/knockout.mapping-latest.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/babel.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/avltree.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modalmanager.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-modal.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-slider.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap/bootstrap-tabdrop.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.core.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.widget.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.ui.mouse.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.flot.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.iframe-transport.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.fileupload.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.slimscroll.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/jquery/jquery.qrcode.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/sockjs-0.3.4.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/moment-with-locales.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pusher.color.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/detectmobilebrowser.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/md5.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/pnotify.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/bootstrap-slider-knockout-binding.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/loglevel.min.js') }}"></script>
{% assets "js_libs" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
<!-- Include OctoPrint files -->
<!-- TODO: merge/minimize in the future -->
{% for url in assets["js"] %}
<script type="text/javascript" src="{{ url }}"></script>
{% endfor %}
<script type="text/javascript" src="{{ url_for('static', filename='js/app/dataupdater.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/helpers.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/app/main.js') }}"></script>
<!-- /Include OctoPrint files -->
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/ui.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/gCodeReader.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='gcodeviewer/js/renderer.js') }}"></script>
{% assets "js_app" %}
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
{% endassets %}
{% if g.locale %}
<script type="text/javascript" src="{{ url_for('localeJs', locale=g.locale, domain='messages') }}"></script>

View file

@ -1,20 +1,13 @@
<link href="{{ url_for('static', filename='css/bootstrap.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-modal.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-slider.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/bootstrap-tabdrop.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/font-awesome.min.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/jquery.fileupload-ui.css') }}" rel="stylesheet" media="screen">
<link href="{{ url_for('static', filename='css/pnotify.min.css') }}" rel="stylesheet" media="screen">
{% assets "css_libs" %}
<link href="{{ ASSET_URL }}" rel="stylesheet" media="screen">
{% endassets %}
{% set lessneeded=[] %}
{% for type, url in assets["stylesheets"] %}
{% if type == "css" %}
<link href="{{ url }}" rel="stylesheet" type="text/css" media="screen">
{% elif type == "less" %}
{% do lessneeded.append(1) %}
<link href="{{ url }}" rel="stylesheet/less" type="text/css" media="screen">
{% endif %}
{% endfor %}
{% if lessneeded %}
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>
{% endif %}
{% assets "css_app" %}
<link href="{{ ASSET_URL }}" rel="stylesheet" type="text/css" media="screen">
{% endassets %}
{% assets "less_app" %}
<link href="{{ url }}" rel="stylesheet/less" type="text/css" media="screen">
{% endassets %}
<script src="{{ url_for('static', filename='js/lib/less.min.js') }}" type="text/javascript"></script>