Using plugins

When using Workbox, you might want to manipulate a request and a response as it's being fetched or cached. Workbox plugins allow you to add additional behaviors to your service worker with minimal extra boilerplate. They can be packaged up and reused in your own projects, or released publicly for others to use as well.

Workbox provides a number of plugins out of the box available for us, and—if you're the crafty sort—you can write custom plugins tailored to your application's requirements.

Available Workbox plugins

Workbox offers the following official plugins for use in your service worker:

Workbox plugins—whether they be one of the plugins listed above, or a custom plugin—are used with a Workbox strategy by adding an instance of the plugin to the strategy's plugins property:

import {registerRoute} from 'workbox-routing';
import {CacheFirst} from 'workbox-strategies';
import {ExpirationPlugin} from 'workbox-expiration';

registerRoute(
  ({request}) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
    plugins: [
      new ExpirationPlugin({
        maxEntries: 60,
        maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Days
      }),
    ],
  })
);

Methods for custom plugins

A Workbox plugin needs to implement one or more callback functions. When you add a plugin to a Strategy, the callback functions are automatically run at the right time. The Strategy passes your callback function relevant information about the current request and/or response, giving your plugin the context it needs to take action. The following callback functions are supported:

  • cacheWillUpdate: Called before a Response is used to update a cache. In this method, the response can be changed before it's added to the cache, or you can return null to avoid updating the cache entirely.
  • cacheDidUpdate: Called when a new entry is added to a cache or if an existing entry is updated. Plugins that use this method may be useful when you want to perform an action after a cache update.
  • cacheKeyWillBeUsed: Called before a request is used as a cache key. This occurs for both cache lookups (when mode is 'read') and cache writes (when mode is 'write'). This callback is handy if you need to override or normalize URLs prior to using them to access caches.
  • cachedResponseWillBeUsed: This is called just before a response from a cache is used, which allows you to examine that response. At this point in time, you could either return a different response, or return null.
  • requestWillFetch: Called whenever a request is about to go to the network. Useful when you need to change the Request just before it goes to the network.
  • fetchDidFail: Called when a network request fails, most likely due to an absence of network connectivity, and will not fire when the browser has a network connection, but receives an error (for example, 404 Not Found).
  • fetchDidSucceed: Called whenever a network request succeeds, regardless of the HTTP response code.
  • handlerWillStart: Called before any handler logic starts running, which is useful if you need to set the initial handler state. For example, if you wanted to know how long the handler took to generate a response, you could make a note of the start time in this callback.
  • handlerWillRespond: Called before the strategy's handle() method returns a response, which is helpful if you need to modify a response before returning it to a RouteHandler or other custom logic.
  • handlerDidRespond: Called after the strategy's handle() method returns a response. This is when it might be useful to record any final response details (for example, after changes made by other plugins).
  • handlerDidComplete: Called after all extend lifetime promises added to the event from the invocation of the strategy have settled. This is helpful if you need to report on any data that needs to wait until the handler is done in order to calculate stuff like cache hit status, cache latency, network latency, and other useful information.
  • handlerDidError: Called if the handler can't provide a valid response from from any source, which is the optimal time to provide some sort of fallback response as an alternative to failing outright.

All of these callback are async, and therefore will require await to be used whenever a cache or fetch event reaches the relevant point for the callback concerned.

If a plugin used all of the above callbacks, this would be the resulting code:

const myPlugin = {
  cacheWillUpdate: async ({request, response, event, state}) => {
    // Return `response`, a different `Response` object, or `null`.
    return response;
  },
  cacheDidUpdate: async ({
    cacheName,
    request,
    oldResponse,
    newResponse,
    event,
    state,
  }) => {
    // No return expected
    // Note: `newResponse.bodyUsed` is `true` when this is called,
    // meaning the body has already been read. If you need access to
    // the body of the fresh response, use a technique like:
    // const freshResponse = await caches.match(request, {cacheName});
  },
  cacheKeyWillBeUsed: async ({request, mode, params, event, state}) => {
    // `request` is the `Request` object that would otherwise be used as the cache key.
    // `mode` is either 'read' or 'write'.
    // Return either a string, or a `Request` whose `url` property will be used as the cache key.
    // Returning the original `request` will make this a no-op.
    return request;
  },
  cachedResponseWillBeUsed: async ({
    cacheName,
    request,
    matchOptions,
    cachedResponse,
    event,
    state,
  }) => {
    // Return `cachedResponse`, a different `Response` object, or null.
    return cachedResponse;
  },
  requestWillFetch: async ({request, event, state}) => {
    // Return `request` or a different `Request` object.
    return request;
  },
  fetchDidFail: async ({originalRequest, request, error, event, state}) => {
    // No return expected.
    // Note: `originalRequest` is the browser's request, `request` is the
    // request after being passed through plugins with
    // `requestWillFetch` callbacks, and `error` is the exception that caused
    // the underlying `fetch()` to fail.
  },
  fetchDidSucceed: async ({request, response, event, state}) => {
    // Return `response` to use the network response as-is,
    // or alternatively create and return a new `Response` object.
    return response;
  },
  handlerWillStart: async ({request, event, state}) => {
    // No return expected.
    // Can set initial handler state here.
  },
  handlerWillRespond: async ({request, response, event, state}) => {
    // Return `response` or a different `Response` object.
    return response;
  },
  handlerDidRespond: async ({request, response, event, state}) => {
    // No return expected.
    // Can record final response details here.
  },
  handlerDidComplete: async ({request, response, error, event, state}) => {
    // No return expected.
    // Can report any data here.
  },
  handlerDidError: async ({request, event, error, state}) => {
    // Return a `Response` to use as a fallback, or `null`.
    return fallbackResponse;
  },
};

The event object available in the callbacks listed above is the original event that triggered the fetch or cache action. Sometimes, there will not be an original event, so your code should check if it exists before referencing it.

All plugin callbacks are also passed a state object, which is unique to a particular plugin and the strategy it invokes. This means you can write plugins where one callback can conditionally perform a task based on what another callback in the same plugin did (for example, compute the difference between running requestWillFetch() and fetchDidSucceed() or fetchDidFail()).

Third-party plugins

If you develop a plugin and you think it has use outside of your project, we encourage you to publish it as a module! Below is a short list of community-provided Workbox plugins:

You may be able to find more community-provided Workbox plugins by searching in npm's repository.

Finally, if you've built a Workbox plugin that you'd like to share, add the workbox-plugin keyword when you publish it. If you do, let us know on Twitter @WorkboxJS!