How can I cache external URLs using service worker?

jeznag picture jeznag · Sep 11, 2016 · Viewed 12k times · Source

I've been playing with the Google Web Starter Kit (https://github.com/google/web-starter-kit) and have got a little progressive web app working but am stuck on one thing: caching static files from external CDNs. E.g. I'm using MDL icons from https://fonts.googleapis.com/icon?family=Material+Icons I can't see a way to cache the request as the service worker only responds to URLs within my app domain.

Options I see: 1. Download the file and put it in a vendor folder. Advantages: easy to set up SW cache. Disadvantages: file won't stay up to date as new icons are added (though that won't really matter as my code will only use the icons available).

  1. Use the NPM repo: https://www.npmjs.com/package/material-design-icons and use build step to copy CSS file from node_modules. Advantages: will allow auto-updating from NPM. Disadvantages: slightly more complex to set up.

  2. Some fancy proxy method that would allow me to use the SW to cache an external URL. e.g. myapp.com/loadExternal?url=https://fonts.googleapis.com/icon?family=Material+Icons

I'm leaning towards 2 right now but would be cool to know if 3 is possible.

Answer

Asim K T picture Asim K T · Feb 19, 2017

TLDR: Try Option 3. You'll thank me later.

From Google Docs:

By default, fetching a resource from a third party URL will fail if it doesn't support CORS. You can add a no-CORS option to the Request to overcome this, although this will cause an 'opaque' response, which means you won't be able to tell if the response was successful or not.

So

Option 1

Add no-cors header

var CACHE_NAME = 'my-site-cache-v1';
var urlsToPrefetch = [
  '/',
  '/styles/main.css',
  '/script/main.js',
  'https://fonts.googleapis.com/icon?family=Material+Icons'
];

self.addEventListener('install', function(event) {
  // Perform install steps
  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(function(cache) {
        console.log('Opened cache');
        // Magic is here. Look the  mode: 'no-cors' part.
        cache.addAll(urlsToPrefetch.map(function(urlToPrefetch) {
           return new Request(urlToPrefetch, { mode: 'no-cors' });
        })).then(function() {
          console.log('All resources have been fetched and cached.');
        });
      })
  );
});

As OP said, when the resource is updated, it's hard to get the latest copy in this scenario. And another issue is, as I said you won't know whether the response was a success or not.

Option 2

Or like OP said, we can create a proxy server: Something simple as (Pseudocode, not tested, Node Express code)

var request = require('request');
app.get('/library', function(req,res) {
  // read the param 
  var thirdPartyUrl = req.query.thirdPartyUrl;
  request(thirdPartyUrl).pipe(res);
});

And when you goto /library?thirdPartyUrl=https://fonts.googleapis.com/icon?family=Material+Icons should give you the response and cache it how we normally cache our response. For Ex: remove no-cors & replace urlsToPrefetch with below value:

var urlsToPrefetch = [
      '/',
      '/library?thirdPartyUrl=https://fonts.googleapis.com/icon?family=Material+Icons',
      '/library?thirdPartyUrl=https://fonts.googleapis.com/icon?family=Roboto'
    ];

Option 3

I think this is the best and easier way. Use workbox. We've tried to create PWA with and without workbox and using workbox was simple.

Read about workbox: https://developers.google.com/web/tools/workbox/

Implement a route like this after the initial setup:

workbox.routing.registerRoute(
  new RegExp('^https://third-party.example.com/images/'),
  new workbox.strategies.CacheFirst({
    cacheName: 'image-cache',
    plugins: [
      new workbox.cacheableResponse.Plugin({
        statuses: [0, 200],
      })
    ]
  })
);