How to make sure a cached page has its corresponding assets cached? - service-worker

tl;dr. My Service Worker is caching HTML pages and CSS files in different versions. Going offline: since I have to limit the number of files I’m caching, how can I make sure that, for each HTML page in the cache, the versioned CSS files it needs are also in the cache? I need to delete old CSS files at some point, and they have no direct relation with the HTML files.
I’m trying to turn a traditional website into a PWA (sort of), implementing caching strategies with a Service Worker (I’m using Workbox but the question is supposed to be more generalist).
I’m caching pages as I navigate through the website (network-first strategy), in order to make them available offline.
I’m also caching a limited number of CSS and JS assets with a cache-first strategy. The URLs pointing to them are already "cachebusted" using a timestamp embedded in the filename: front.320472502.css for instance. Because of the cachebusting technique already in place, I only need/want to keep a small number of assets in this cache.
Now here’s the issue I’m having. Let’s suppose I cached page /contact which referenced /front.123.css (hence was also cached). As I navigate to other pages, CSS has changed several times in the meantime, and my CSS cache now might contain only /front.455.css and /front.456.css. If I’m going offline now, trying to load /contact will successfully retrieve the contents of the page, but the CSS will fail to load because it’s not in the cache anymore, and it will render an unstyled content page.
Either I keep versions of my CSS in cache for a long time, which is not ideal, or I try to purge cached CSS only if it is not required by any of the cached pages. But how would you go about that? Looping through the cached pages, looking for the front.123.css string?
Another solution might be to give back an offline page rather than an unstyled content page, but I’m not sure if it is doable, since the worker responds with the HTML before knowing what assets it will need.

The "best" solution here is to use precaching (either via Workbox, or via some other way of generating a build-time manifest), and making sure that all of your HTML and subresources are cached and expired atomically. You don't have to worry about version mismatches or cache misses if you can precache everything.
That being said, precaching everything isn't always a viable option, if your site relies on a lot of dynamic, server-rendered content, or if you have a lot of distinct HTML pages, or if you have a larger variety of subresources, many of which are only required on a subset of pages.
If you want to go with the runtime caching approach, I'd recommend a technique along the lines of what's described in "Smarter runtime caching of hashed assets". That uses a custom Workbox plugin to handle cache expiration and finding a "best-effort" cache match for a given subresource when the network is unavailable. The main difficulty in generalizing that code is that you need to use a consistent naming scheme for your hashes, and write some utility functions to programmatically translate a hashed URL into the "base" URL.
In the interest of providing some code along with this answer, here's a version of the plugin that I currently use. You'll need to customize it as described above for your hashing scheme, though.
import {WorkboxPlugin} from 'workbox-core';
import {HASH_CHARS} from './path/to/constants';
function getOriginalFilename(hashedFilename: string): string {
return hashedFilename.substring(HASH_CHARS + 1);
}
function parseFilenameFromURL(url: string): string {
const urlObject = new URL(url);
return urlObject.pathname.split('/').pop();
}
function filterPredicate(
hashedURL: string,
potentialMatchURL: string,
): boolean {
const hashedFilename = parseFilenameFromURL(hashedURL);
const hashedFilenameOfPotentialMatch =
parseFilenameFromURL(potentialMatchURL);
return (
getOriginalFilename(hashedFilename) ===
getOriginalFilename(hashedFilenameOfPotentialMatch)
);
}
export const revisionedAssetsPlugin: WorkboxPlugin = {
cachedResponseWillBeUsed: async ({cacheName, cachedResponse, state}) => {
state.cacheName = cacheName;
return cachedResponse;
},
cacheDidUpdate: async ({cacheName, request}) => {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
for (const key of keys) {
if (filterPredicate(request.url, key.url) && request.url !== key.url) {
await cache.delete(key);
}
}
},
handlerDidError: async ({request, state}) => {
if (state.cacheName) {
const cache = await caches.open(state.cacheName);
const keys = await cache.keys();
for (const key of keys) {
if (filterPredicate(request.url, key.url)) {
return cache.match(key);
}
}
}
},
};

Related

Progressive web app launched in standalone does not detect site updates

I've a Progressive web app that is added on my home screen. I've chosen standalone run type, I've a service-worker running in it.
All working perfectly, only one doubt: if I update my site (with its relative service-worker, I can see updates if I load it directly into browser, but if I launch it by home added link I see always the old site.
There is a way to request updates when launching my site in standalone mode?
I think you are asking "is there a way to dynamically update my precached assets without updating my service worker?"
Yes!
I have been working on an upgrade to the JSON cache strategy here -> https://serviceworke.rs/json-cache.html We will publish this soon!
After digging I've discovered the simple solution, I report it for others.
iOS does not support service-workers, so that is not the problem.
iOS keep in cache many resources, so the solution is to add a parameter to the various imports like so:
The best solution to ensure updates is to add an hash of imported as the parameter.
Alternatively we can use a timestamp to ensure that resources are ALWAYS updated.
To attach this parameter we can import these resources injecting them with javascript, like so
/**
* inietta uno script nella pagina
*/
function appendScript(url) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.src = url;
script.async = false; // async false to wait for previous file loading
head.appendChild(script);
}
// create parameter with date
var currVersion = '?v=' + new Date().getTime();
// get head html element
var head = document.getElementsByTagName("head")[0];
// append script
appendScript('app.js' + currVersion);
// append css
head.insertAdjacentHTML('beforeend', '<link rel="stylesheet" type="text/css" href="style.css' + currVersion + '">');

Bundling, but per user

We have javascript files which get bundled and compressed using the normal asp.net mvc mechanism.
We also have some javascript files which get transformed via httphandlers to deal with phrases, colour schemes, etc. At present these are simply linked in, could these be compressed and bundled but at the user level?
Unfortunately we can't group these easily, but even if we could we couldn't do it within a global.ascx file without a lot of rejigging. I mention this as it's not simply a case of having bundle1 = french, bundle2=german, etc
Compression I'm assuming could be done via IIS and static compression, but bundling?
thanks
There is no easy way to do this.
The easiest I can see is to skip the whole Bundling and Minification that is shipped with MVC 5.
Handle it yourself. Generate CSS for your user and have it go through this piece of code:
public static string RemoveWhiteSpaceFromStylesheets(string body)
{
body = Regex.Replace(body, #"[a-zA-Z]+#", "#");
body = Regex.Replace(body, #"[\n\r]+\s*", string.Empty);
body = Regex.Replace(body, #"\s+", " ");
body = Regex.Replace(body, #"\s?([:,;{}])\s?", "$1");
body = body.Replace(";}", "}");
body = Regex.Replace(body, #"([\s:]0)(px|pt|%|em)", "$1");
// Remove comments from CSS
body = Regex.Replace(body, #"/\*[\d\D]*?\*/", string.Empty);
return body;
}
Or any CSS minifier for that matter. Just make sure to include proper caching tag for your user and you won't even have to regenerate it too much.
Code taken from Mads Kristensen

Override web page's javascript function using firefox addon sdk

I'm trying to override a JS function named replaceMe in the web page from my add-on's content script, but I see that the original function implementation always gets executed.
Original HTML contains the following function definition:
function replaceMe()
{
alert('original');
}
I'm trying to override it my add-on like (main.js):
tabs.activeTab.attach({
contentScriptFile: self.data.url("replacerContent.js")
});
Here's what my replacerContent.js looks like:
this.replaceMe = function()
{
alert('overridden');
}
However, when I run my addon, I always see the text original being alerted, meaning the redefinition in replacerContent.js never took effect. Can you let me know why? replaceMe not being a privileged method, I should be allowed to override, eh?
This is because there is an intentional security between web content and content scripts. If you want to communicate between web content and you have control over the web page as well, you should use postMessage.
If you don't have control over the web page, there is a hacky workaround. In your content script you can access the window object of the page directly via the global variable unsafeWindow:
var aliased = unsafeWindow.somefunction;
unsafeWindow.somefunction = function(args) {
// do stuff
aliased(args);
}
There are two main caveats to this:
this is unsafe, so you should never trust data that comes from the page.
we have never considered the unsafeWindow hack and have plans to remove it and replace it with a safer api.
Rather than relying on unsafeWindow hack, consider using the DOM.
You can create a page script from a content script:
var script = 'rwt=function()();';
document.addEventListener('DOMContentLoaded', function() {
var scriptEl = document.createElement('script');
scriptEl.textContent = script;
document.head.appendChild(scriptEl);
});
The benefit of this approach is that you can use it in environments without unsafeWindow, e. g. chrome extensions.
You can then use postMessage or DOM events to communicate between the page script and the content script.

Pure Dart DOM possibility?

I am learning Dart and suddenly had an epiphany (or possibly, an epiphany):
Can I write a Dart web app where the "view" is done 100% in Dart?
I'm talking: absolutely no (none/zero/nadda) HTML files (.html). 100% Dart code. Something like:
class SigninView {
LabelElement signinLabel;
InputElement emailTextField;
InputElement passwordTextField;
ButtonElement signinButton;
// constructors, getters, setters, etc.
// Perhaps called from inside constructor...
void initUI() {
signinLabel = new LabelElement();
signinLabel.innerHTML = "<span class=\"blah\">Please sign in</span>";
emailTextField = new InputElement();
emailTextField.innerHTML = "<input type=\"text\" name=\"fizz\" placeholder=\"Email\"/>";
// ...etc.
// htmlFactory would be something I'd need to write myself (?)
String html = htmlFactory.newHTML(signinLabel, emailTextField, ...);
querySelector("#someDivTag").innerHTML = html;
}
}
In theory (that is, my intentions with the above code), as soon as the SigninView is created, it initializes a bunch of DOM elements and populates someDivTag with them.
Is this possible? If so am I "doing it right", or is there a different/preferred/standardized approach to this?
Does this introduce any additional/potential caveats (memory leaks), performance or security issues that I should be aware of?
If I were to adopt this strategy throughout my whole app, can I assume the app would be quicker to download (less HTML text), but slower to execute (dynamic DOM element creation)? If so, is there a way to somehow instantiate all the DOM elements my app will need up front (slowing down initial download time), and then only make certain elements visible as I wish to render different views/screens (thus speeding up execution time)?
You need an HTML file with the script tags for the Dart startup.
Anything else can be done in Dart.

display photos in grails vs playframework

I have been doing some tests using Grails framework and now I'm trying to do something similar in playframework.
Basically, I want to display some pictures, but to hide (in order to be able to avoid any crawling and to be able to change the hosting ) the pictures path.
The gsp page:
<g:each in="${images}" var="img">
<img class="thumbnail" src='${createLink(controller: "images", action: "displayImage", params:[img: img.name])}'/>
</g:each>
The controller:
def displayImage() {
File image = new File(IMAGES_DIR.absolutePath +'/' + params.img)
if(!image.exists()) {
response.status = 404
} else {
response.setContentType("application/jpg")
OutputStream out = response.getOutputStream();
out.write(image.bytes);
out.close();
}
}
The html generated page it looks like:
<img class="thumbnail" src='/myhost/images/displayImage?img=blabla.jpg' />
My questions:
Is this a best way to do it ?
Regarding the performance ?
Is this slower than juste displaying the pictures using http ?
Can I do something like this in Playframework ? If yes, how ?
Thanks.
C.C.
For static and public resources most probably using raw HTTP server will be fastest solution, so I don't think it's required to "drag" it through Java controller. Actually you can do it with Play very similar, but even easier - as Play allows yo to return a File as a response body directly ie (written from top of my head):
public static Result displayImage(String imagePath) {
File image = new File(SOME_CONFIGURED_FOLDER_WITH_IMAGES +'/' + imagePath)
if(!image.exists()) return notFound();
return ok(image).as("image/jpg");
}
Anyway, you should use it only if:
You are not gonna to use additional HTTP server (remember, that Play has built in one?)
You need to bring some access control
You want to perform some operations, ie. scaling, cropping etc. (in such case, IMHO it's also better to use Play only for creating the thumbnail, and serve it with common HTTP server...)
Thank's to this approach:
You don't waste processor's resources, as HTTP server just need to serve the file which is stored on disk, instead of rewriting it to the Result.
Your app can concentrate on other dynamic operations so it's faster.
You can (and should) use typical webmaster's techniques for optimizing serving static contents, like cookie free domains, advanced caching headers etc. (ofc you can do that also within Play controller, but...)

Resources