From 340d960b93bb9cc0ec91664d05992143cf94d265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Rivet?= Date: Mon, 28 Oct 2019 14:06:25 -0400 Subject: [PATCH] Improve resource caching (#973) --- .../webpack-dash-dynamic-import/package.json | 2 +- .../webpack-dash-dynamic-import/src/index.js | 63 ++++++++++++++++--- CHANGELOG.md | 3 +- dash/dash.py | 34 ++++++++-- dash/fingerprint.py | 31 +++++++++ tests/unit/test_fingerprint.py | 54 ++++++++++++++++ tests/unit/test_resources.py | 10 +-- 7 files changed, 175 insertions(+), 22 deletions(-) create mode 100644 dash/fingerprint.py create mode 100644 tests/unit/test_fingerprint.py diff --git a/@plotly/webpack-dash-dynamic-import/package.json b/@plotly/webpack-dash-dynamic-import/package.json index 8a0ec41b5f..789b1e6d0e 100644 --- a/@plotly/webpack-dash-dynamic-import/package.json +++ b/@plotly/webpack-dash-dynamic-import/package.json @@ -1,6 +1,6 @@ { "name": "@plotly/webpack-dash-dynamic-import", - "version": "1.0.0", + "version": "1.1.0", "description": "Webpack Plugin for Dynamic Import in Dash", "repository": { "type": "git", diff --git a/@plotly/webpack-dash-dynamic-import/src/index.js b/@plotly/webpack-dash-dynamic-import/src/index.js index 3f930a3298..5a1dc014ca 100644 --- a/@plotly/webpack-dash-dynamic-import/src/index.js +++ b/@plotly/webpack-dash-dynamic-import/src/index.js @@ -1,13 +1,35 @@ -const resolveImportSource = `\ +const fs = require('fs'); + +function getFingerprint() { + const package = fs.readFileSync('./package.json'); + const packageJson = JSON.parse(package); + + const timestamp = Math.round(Date.now() / 1000); + const version = packageJson.version.replace(/[.]/g, '_'); + + return `"v${version}m${timestamp}"`; +} + +const resolveImportSource = () => `\ +const getCurrentScript = function() { + let script = document.currentScript; + if (!script) { + /* Shim for IE11 and below */ + /* Do not take into account async scripts and inline scripts */ + const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; }); + script = scripts.slice(-1)[0]; + } + + return script; +}; + +const isLocalScript = function(script) { + return /\/_dash-components-suite\//.test(script.src); +}; + Object.defineProperty(__webpack_require__, 'p', { get: (function () { - let script = document.currentScript; - if (!script) { - /* Shim for IE11 and below */ - /* Do not take into account async scripts and inline scripts */ - const scripts = Array.from(document.getElementsByTagName('script')).filter(function(s) { return !s.async && !s.text && !s.textContent; }); - script = scripts.slice(-1)[0]; - } + let script = getCurrentScript(); var url = script.src.split('/').slice(0, -1).join('/') + '/'; @@ -15,7 +37,28 @@ Object.defineProperty(__webpack_require__, 'p', { return url; }; })() -});` +}); + +const __jsonpScriptSrc__ = jsonpScriptSrc; +jsonpScriptSrc = function(chunkId) { + let script = getCurrentScript(); + let isLocal = isLocalScript(script); + + let src = __jsonpScriptSrc__(chunkId); + + if(!isLocal) { + return src; + } + + const srcFragments = src.split('/'); + const fileFragments = srcFragments.slice(-1)[0].split('.'); + + fileFragments.splice(1, 0, ${getFingerprint()}); + srcFragments.splice(-1, 1, fileFragments.join('.')) + + return srcFragments.join('/'); +}; +` class WebpackDashDynamicImport { apply(compiler) { @@ -23,7 +66,7 @@ class WebpackDashDynamicImport { compilation.mainTemplate.hooks.requireExtensions.tap('WebpackDashDynamicImport > RequireExtensions', (source, chunk, hash) => { return [ source, - resolveImportSource + resolveImportSource() ] }); }); diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f46a28667..ebc971e73e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,10 +5,11 @@ This project adheres to [Semantic Versioning](http://semver.org/). ## Unreleased ### Added - [#964](https://github.com/plotly/dash/pull/964) Adds support for preventing -updates in clientside functions. +updates in clientside functions. - Reject all updates with `throw window.dash_clientside.PreventUpdate;` - Reject a single output by returning `window.dash_clientside.no_update` - [#899](https://github.com/plotly/dash/pull/899) Add support for async dependencies and components +- [#973](https://github.com/plotly/dash/pull/973) Adds support for resource caching and adds a fallback caching mechanism through etag ## [1.4.1] - 2019-10-17 ### Fixed diff --git a/dash/dash.py b/dash/dash.py index 0c402b2108..ddb64bec80 100644 --- a/dash/dash.py +++ b/dash/dash.py @@ -24,6 +24,7 @@ import dash_renderer from .dependencies import Input, Output, State +from .fingerprint import build_fingerprint, check_fingerprint from .resources import Scripts, Css from .development.base_component import Component, ComponentRegistry from . import exceptions @@ -541,12 +542,14 @@ def _relative_url_path(relative_package_path="", namespace=""): modified = int(os.stat(module_path).st_mtime) - return "{}_dash-component-suites/{}/{}?v={}&m={}".format( + return "{}_dash-component-suites/{}/{}".format( self.config.requests_pathname_prefix, namespace, - relative_package_path, - importlib.import_module(namespace).__version__, - modified, + build_fingerprint( + relative_package_path, + importlib.import_module(namespace).__version__, + modified, + ), ) srcs = [] @@ -676,6 +679,10 @@ def _generate_meta_html(self): # Serve the JS bundles for each package def serve_component_suites(self, package_name, path_in_package_dist): + path_in_package_dist, has_fingerprint = check_fingerprint( + path_in_package_dist + ) + if package_name not in self.registered_paths: raise exceptions.DependencyException( "Error loading dependency.\n" @@ -711,11 +718,28 @@ def serve_component_suites(self, package_name, path_in_package_dist): package.__path__, ) - return flask.Response( + response = flask.Response( pkgutil.get_data(package_name, path_in_package_dist), mimetype=mimetype, ) + if has_fingerprint: + # Fingerprinted resources are good forever (1 year) + # No need for ETag as the fingerprint changes with each build + response.cache_control.max_age = 31536000 # 1 year + else: + # Non-fingerprinted resources are given an ETag that + # will be used / check on future requests + response.add_etag() + tag = response.get_etag()[0] + + request_etag = flask.request.headers.get('If-None-Match') + + if '"{}"'.format(tag) == request_etag: + response = flask.Response(None, status=304) + + return response + def index(self, *args, **kwargs): # pylint: disable=unused-argument scripts = self._generate_scripts_html() css = self._generate_css_dist_html() diff --git a/dash/fingerprint.py b/dash/fingerprint.py new file mode 100644 index 0000000000..0c67e31ea5 --- /dev/null +++ b/dash/fingerprint.py @@ -0,0 +1,31 @@ +import re + +build_regex = re.compile(r'^(?P[\w@-]+)(?P.*)$') + +check_regex = re.compile( + r'^(?P.*)[.]v[\w-]+m[0-9a-fA-F]+(?P(?:(?:(?