Skip to main content

Setting up a PWA and automating workflow with Laravel and Workbox

I recently played around with the Google PWA library, Workbox. It makes managing things like service workers and caches for Progressive Web Apps a doddle, whilst also having a useful API for customising your workflow.

Here I will go through the steps of setting up your Laravel app to be a PWA and also making the workflow automated for future changes to your assets.

The first step is to set up your web app manifest, this is a JSON file that will be read by browsers when installing your PWA. Create the file below in your public directory and call it site.webmanifest

{
    "name": "My PWA App",
    "short_name": "My PWA App",
    "start_url": "/",
    "icons": [
        {
            "src": "/android-chrome-192x192.png",
            "sizes": "192x192",
            "type": "image/png"
        },
        {
            "src": "/android-chrome-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ],
    "theme_color": "#f2f2f2",
    "background_color": "#f2f2f2",
    "display": "standalone"
}

Most of this should be self-explanatory, the display value at the end determines how your PWA will be shown to the user, standalone means it will look and feel like a native app. You can find more options for the web app manifest file on the MDN page.

You can now add a link to the manifest in your site's metadata, simply add this line into your main blade template along with all of your other metadata:

<link rel="manifest" href="/site.webmanifest">

The next step is to set up your service-worker.js file, this is the main file that will dictate what strategy to take with cached assets and network requests. Create a file called service-worker.js and place it in your resources/js folder:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');
if (workbox) {

    // top-level routes we want to precache
    workbox.precaching.precacheAndRoute(['/', '/blog']);

    // injected assets by Workbox CLI
    workbox.precaching.precacheAndRoute([]);

    // match routes for homepage, blog and any sub-pages of blog
    workbox.routing.registerRoute(
        /^\/(?:(blog)?(\/.*)?)$/,
        new workbox.strategies.NetworkFirst({
            cacheName: 'static-resources',
        })
    );

    // js/css files
    workbox.routing.registerRoute(
        /\.(?:js|css)$/,
        new workbox.strategies.StaleWhileRevalidate({
            cacheName: 'static-resources',
        })
    );

    // images
    workbox.routing.registerRoute(
        // Cache image files.
        /\.(?:png|jpg|jpeg|svg|gif)$/,
        // Use the cache if it's available.
        new workbox.strategies.CacheFirst({
            // Use a custom cache name.
            cacheName: 'image-cache',
            plugins: [
                new workbox.expiration.Plugin({
                    // Cache upto 50 images.
                    maxEntries: 50,
                    // Cache for a maximum of a week.
                    maxAgeSeconds: 7 * 24 * 60 * 60,
                })
            ],
        })
    );

}

There's quite a lot going on here, so let's break it down. First, we're importing the Workbox library and checking that it is loaded and working:

importScripts('https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js');

if (workbox) {

...

}

We're then passing in some precache requests to routes and leaving an empty precache array for Workbox to inject our public assets in to later. We will be using the Workbox CLI to automatically inject all of our public assets, but it is not smart enough to know about our routes, so these need to be added separately.

// top-level routes we want to precache
workbox.precaching.precacheAndRoute(['/', '/blog']);

// injected assets by Workbox CLI
workbox.precaching.precacheAndRoute([]);

These requests are essentially telling the browser to precache the routes and assets so that they will be available offline. There is a lot more you can do here in regards to your routes, you could potentially parse your sitemap.xml file to add them dynamically.

Note: It is important to leave the second precache request as it is, as this is what the Workbox CLI will be looking for when it tries to inject assets during the build process.

Next, we need to tell the browser what strategy to take when a specific route or type of asset is requested. This is dependent on your app, but here is a combination of common strategies taken from the Workbox guides to get you started.

// match routes for homepage, blog and any sub-pages of blog
workbox.routing.registerRoute(
	/^\/(?:(blog)?(\/.*)?)$/,
	new workbox.strategies.NetworkFirst({
		cacheName: 'static-resources',
	})
);

// js/css files
workbox.routing.registerRoute(
	/\.(?:js|css)$/,
	new workbox.strategies.StaleWhileRevalidate({
		cacheName: 'static-resources',
	})
);


// images
workbox.routing.registerRoute(
	// Cache image files.
	/\.(?:png|jpg|jpeg|svg|gif)$/,
	// Use the cache if it's available.
	new workbox.strategies.CacheFirst({
		// Use a custom cache name.
		cacheName: 'image-cache',
		plugins: [
			new workbox.expiration.Plugin({
				// Cache upto 50 images.
				maxEntries: 50,
				// Cache for a maximum of a week.
				maxAgeSeconds: 7 * 24 * 60 * 60,
			})
		],
	})
);

We're using 3 different caching strategies here:

  1. NetworkFirst - Routes will try to use the network first and fall back on to the cache.
  2. StaleWhileRevalidate - JS and CSS files will first use the cache and then try to update the files from the network if available.
  3. CacheFirst - Images will favour the cache and not try to get the image from the network unless the cache has expired.

There is a lot of functionality on offer when it comes to caching strategies, to learn more, check out the Workbox docs.

Now we need to get Workbox CLI to inject our assets into our service-worker.js file. First, install the Workbox CLI.

npm install -g workbox-cli

Next, we need to set up a config file for Workbox to use when we fire the injectManifest command. Create a file called workbox.config.js in the root of your project.

module.exports = {
  "globDirectory": "public/",
  "globPatterns": [
    "**/*.{png,xml,css,ico,jpg,svg,js,json}"
  ],
  "swDest": "public\\service-worker.js",
  "swSrc": "resources/js/service-worker.js"
};

This tells Workbox to find all assets in your public folder, inject them into the service worker file you have in your resources/js folder and save the result to your public folder. We can now test this using the following command in our terminal:

workbox injectManifest workbox.config.js

If it worked, you should see the results of the build in your terminal and a new service-worker.js file created in your public. Open the file and you should see that all of your assets have been added to the empty array we created earlier.

There is a lot of customisation available here too, you could add filters or maps to the config file for it to change the result. For example, if you're versioning with Laravel Mix on your CSS and JS files, you could add the version string to your assets:

manifestTransforms: [
	(originalManifest) => {
		const manifest = originalManifest.map((entry) => {
			entry.url = entry.url.match(/app\.(?:css|js)$/g) ? `${entry.url}?id=${entry.revision.substring(0,20)}` : entry.url ;
			return entry;
		});
		const warnings = [];
		return {manifest, warnings};
	}
]

Or if your assets are served from a CDN, you could add the domain to the beginning of the string:

manifestTransforms: [
	(originalManifest) => {
		const manifest = originalManifest.map((entry) => {
			entry.url = `https://cdn.example.com/${entry.url}`;
			return entry;
		});
		const warnings = [];
		return {manifest, warnings};
	}
]

Note: For any assets that are hosted away from the main app domain you will need to allow for Cross-Origin Requests (CORS).

Now that we have a complete service-worker.js file, we can add it to our templates, insert this code before the closing body tag of your main app template.

<script>
if ('serviceWorker' in navigator) {
	window.addEventListener('load', () => {
		navigator.serviceWorker.register('/service-worker.js');
	});
}
</script>

This will register the service worker for our PWA when the page has loaded. Now let's add the command we used earlier to the end of our production build in the package.json file so that the process is automated.

 && workbox injectManifest workbox.config.js

There we have it, a PWA that registers and caches our assets automatically. PWA support still isn't great, but hopefully, it will improve now that the process of implementing them into our web apps has become quite painless.