Pre-caching assets with a service-worker

Hi everyone,

I’m trying to add a service worker to a Phoenix 1.5 app to display an offline page when the user has no network connectivity. To do this I need to cache some static assets (css, fonts and an image) along with the html page itself.

Following this cookbook, I would need something like

self.addEventListener('install', function(event) {
  event.waitUntil(
    caches.open('pre-cache:v1').then(function(cache) {
      return cache.addAll([
        '/css/app.css',
        '/images/logo.svg',
        '/fonts/some-font.woff2',
        '/offline.html',
      ]);
    })
  );
});

Since I’m using Phoenix, my static assets are “digested” using mix phx.digest when the app is deployed to production, so I need to cache the files using their hashed names. I also want the service worker to work locally, where the filenames do not contain a cache-busting hash. I also would like to avoid adding a build step. Something like workbox would require to run their cli after mix phx.digest has run, so it’s quite awkward to make it work in conjunction with webpack to get reloading etc. when working locally (I did something like this in the past and it was not great to work with).

Basically I need a way of getting the name of some assets, with the hash in production, and without the hash locally, which is exactly what Routes.static_path/2 does, and put the result inside a javascript file.

So what I came up with it to generate the service-worker.js file using EEx by doing this:

# router.ex
  scope "/", MyApp do                                                                                         
    get "/service-worker.js", SomeController, :service_worker                                                     
  end  
# some_controller.ex
def service_worker(conn, _) do
  conn
  |> put_resp_content_type("application/javascript")
  |> render("service_worker.js")
end
  # service_worker.js.eex
...
  caches.open("pre-cache:v1").then(cache => cache.addAll([                                                                                                                  
    "<%= Routes.static_path(@conn, "/css/app.css") %>",                                                                                            
    "<%= Routes.static_path(@conn, "/images/logo.svg") %>",
    "<%= Routes.static_path(@conn, "/fonts/some-font.woff2") %>" 
    "/offline.html",                                                    
  ]));   
...     

It seems to work nicely, but I don’t think that I’ve ever seen EEx being used to render anything but html, so I’m wondering if I missed something here (security issues, or a simpler way of doing this).

Thanks!

1 Like

Hi @kimlai, did you end up using this method successfully as described? I’ve realised the need to re-develop part of my LiveView site as an offline-first app for field work in poor network conditions, so I’d also have to integrate with a front-end framework, local storage and Channels for bidirectional syncing, but that would all be implemented in their own cached scripts as well. Any pitfalls you’ve come across?

I use the same strategy for the ServiceWorker on an offline-first production Rails + JS application with good results since years. While somewhat uncommon, there is no issue in rendering a JS file with EEx when needed.

Of course, LiveView needs some special care when it comes to offline behavior, and depending on your needs, being it a server-side tech, it might not be the best option if offline-first is a strong requirement.

1 Like

This is quite true. The strategy will be to migrate the architecture from LiveView-centric server side rendering to client-side from Phoenix endpoint (dead view), with Phoenix Channels for the data-synchronization and event-handling reactivity while the app is online, and a light-weight templating engine for the DOM rendering (currently trialling lit-html).