I recently migrated my personal blog ie ShareurCodes which was created by using Laravel 5.6 into a Progressive Web App (PWA) by using WorkBox 4. The migration was much easier than I expected and took me less than 1 day. The WorkBox is a tool created by Google to ease up painfull complex caching process. This tool is very easy to integrate into your existing application without doing any breaking code changes and will save you a lot of time.
For those who are completely new to the term Progressive Web App (PWA), I will try to explain to you some of the important concepts you need to know before driving into coding. If you already know about what is PWA then feel free to skip this.
Progressive Web App (PWA) was originally proposed by Google in 2015. As the word suggests you progressively enhance the look and feel of your web application to feel like a native application. The word progressive is very important because you are not supposed to break the support for old browsers Instead, users visiting from new browsers will get enhanced user experience.
Some of the main features of PWA's are:
So the 3 words to summarize PWA's are:
Service Worker is a type of web worker supported in all modern browsers is a javascript file that runs in the background, separate from a web page ie in a separate non-blocking thread. It acts as a proxy between web application, the browser and the network, allowing you to intercept and cache network requests and take appropriate action based on the network availability.
The Service Workers are the core part of PWA, allows us to cache resources, web push notification and offline experience. It doesn't have access to DOM and other APIs like cookies, XHR, local storage and session storage etc.
Due to security reason, a service worker only runs over an HTTPS connection (localhost is an exception) and can not be used in private browsing mode.
Now if you wonder what is Web Workers, They are the most general-purpose type of workers even supported in IE10 that run in a separate thread. We use them to offload heavy works. They also don't have access to DOM API's.
The service worker registration lifecycle consists of three steps:
When a user first visits your website, the service worker file is immediately downloaded and installation is attempted. If the installation is successful, the service worker is activated. Any functionality that's inside the service worker file is not made available until the user visits another page or refresh the current page. The service worker installation event is triggered only once and it will retrigger only if the browser detects a change in service worker file.
Once installed and activated, the service worker can start intercepting network requests, caching resources and sending push notifications by listening to events emitted by the browser inside the service worker file. The browser emits the following events:
If you want to learn more about Service Worker feel free to check out this amazing article by Atta-Ur-Rehman Shah.
A caching strategy is a pattern that determines how a service worker generates a response after receiving a fetch event. The Workbox has very good support for all of them. You can refer their documentation for more info by visiting the following link.
Some of them are
1) Stale-While-Revalidate
This pattern allows you to respond to the request as quickly as possible with a cached response if available, falling back to the network request if it’s not cached. The network request is then used to update the cache.
I used this strategy to cache dynamic photos in blog contents.
2) Cache First (Cache Falling Back to Network)
If there is a Response in the cache, the Request will be fulfilled using the cached response and the network will not be used at all.
If there isn't a cached response, the Request will be fulfilled by a network request and the response will be cached so that the next request is served directly from the cache.
I used this strategy to cache the static assets files like app.css file and app.js file which will not change for a long time.
3) Network First (Network Falling Back to Cache)
For requests that are updating frequently, the network first strategy is the ideal solution. By default, it will try to fetch the latest response from the network. If the request is successful, it’ll put the response in the cache. If the network fails to return a response, the cached response will be used.
I used this approach for caching my dynamic blog pages.
4) Network Only
Here no caching, So not recommended.
5) Cache Only
This pattern is also not recommended as if the asset is not present in the cache it will not fetch an asset from the network. Use Cache First instead of this for serving static assets.
It is a simple JSON file that tells the browser about your web application and how it should behave when installed on the user's mobile device or desktop. This file is required by Chrome to show the Add to Home Screen prompt.
It includes information about the app name
, icons etc.
Now let's start coding and convert a Laravel application into a PWA.
The first thing we need to do is install Workbox CLI globally in your system. Make sure that you have the latest version of node.js and npm on your system.
npm install workbox-cli -g
Use sudo before npm if permission error occurred.
Now create an sw-base.js file in public folder of laravel application with the following content.
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js'
);
workbox.precaching.precacheAndRoute([]);
This file will be used as our base file and this file will later contain our core offline logic and dynamic asset caching.
Now create a workbox-config.js file at the root of your project ie where your composer.json file and package.json file present and add the following content.
module.exports = {
globDirectory: 'public/',
globPatterns: ['**/*.{js,css,png,jpg}','offline.html'],
swSrc: 'public/sw-base.js',
swDest: 'public/service-worker.js',
globIgnores: [
'../workbox-cli-config.js',
'photos/**'
]
};
In this file, we can see that we will precache all image, js and CSS files and offline.html inside the public folder. Use globIgnores to ignore certain files and folder. Eg you can see that we ignore all files in public/photos folder from precaching as they are dynamic assets and can be changed.
Note
Do not precache all files inside your public folder. Use globIgnores to ignore all asset files that are not loaded in all web pages. Caching unwanted files will have a negative performance result as your users will be downloading unwanted files.
Now create an offline.html file inside the public folder to show offline message when your customer is offline and webpage he requested is not in dynamic cache.
Now the final step is to add the following script in your package.json file.
"scripts": {
"generate-sw": "workbox injectManifest workbox-config.js"
},
Now you can create a service-worker.js file inside your public folder by running,
npm run generate-sw
You will use this file as your service worker file that will be registered with your application. It is very important to run this command every time you change one of your assets file inside the public folder. Eg if you change your app.css file or app.js file run this command before deploying. This will delete your old cache and replace it with new files in the user's browser.
Now you have created your service-worker.js file. But our application has no idea about this file. So you need to register this service worker file with your application by adding following code inside your app.js file if you are using Laravel Mix or at the footer of your laravel view files.
// Check that service workers are supported
if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service-worker.js');
});
}
The add to Home Screen Prompt requires two files, a manifest.json file and a service worker. Since you already have service worker you don't have to do any modifications in that file.
Now create a manifest.json file at the public folder and add the following contents.
{
"name": "Shareurcodes",
"short_name": "Shareurcodes",
"icons": [
{
"src": "/logo.png",
"sizes": "192x192",
"type": "image/png"
}
],
"start_url": "/",
"scope": ".",
"display": "standalone",
"background_color": "#fff",
"theme_color": "#7c4dff",
"description": "ShareurCodes is a code sharing site for programmers to learn, share their knowledge with younger generation",
"dir": "ltr",
"lang": "en-US"
}
I just copy and paste my manifest.json file. Modify its contents based on your application.
When you have created the manifest, add a link
tag to all the pages in your web app with the following content.
<link rel="manifest" href="/manifest.json" />
Now I also added custom link in my mobile offside navigation with 'Add to Home Screen' link. If you want to have a custom Add to Home Screen link follow the steps below.
Add this to your mobile navigation bar.
<li>
<a href="#" onclick="addToHomeScreen()">
<span class="uk-margin-small-right" data-uk-icon="icon: plus"></span>
Add to Home Screen
</a>
</li>
Now inside your app.js file or in your view file add the following js contents.
var deferredPrompt;
window.addEventListener('beforeinstallprompt', function(event) {
event.preventDefault();
deferredPrompt = event;
return false;
});
function addToHomeScreen() {
if (deferredPrompt) {
deferredPrompt.prompt();
deferredPrompt.userChoice.then(function (choiceResult) {
console.log(choiceResult.outcome);
if (choiceResult.outcome === 'dismissed') {
console.log('User cancelled installation');
} else {
console.log('User added to home screen');
}
});
deferredPrompt = null;
}
}
As you know blog images are dynamic and can change over a period of time. Moreover, we don't need to precache all the images in an application at first load as it will negatively affect initial page load speed. So the best solution is to cache all dynamic images in a page when the user visits that page so that next time we can serve the cached images faster.
The caching strategy I am using here is Stale-While-Revalidate. Inside your sw-base.js file add the following content.
workbox.routing.registerRoute(
new RegExp('/photos/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'photos',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 15
}),
new workbox.cacheableResponse.Plugin({
statuses: [200]
})
]
})
);
I restrict the maximum entries to 15 to not to over-consume users cache. Always make sure this.
Now I want my to give offline support for my website dynamic contents. Here I need to download the page every time the user visits the page but If the user is offline I need to serve with already stored content from my dynamic cache. If the user is offline and page is not in dynamic cache then I want to give him a fallback offline.html page.
The caching strategy I am using here is Network First (Network Falling Back to Cache). Inside your sw-base.js file add the following content at the bottom.
const networkFirstHandler = new workbox.strategies.NetworkFirst({
cacheName: 'dynamic',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 10
}),
new workbox.cacheableResponse.Plugin({
statuses: [200]
})
]
});
const FALLBACK_URL = workbox.precaching.getCacheKeyForURL('/offline.html');
const matcher = ({ event }) => event.request.mode === 'navigate';
const handler = args =>
networkFirstHandler
.handle(args)
.then(response => response || caches.match(FALLBACK_URL))
.catch(() => caches.match(FALLBACK_URL));
workbox.routing.registerRoute(matcher, handler);
The workbox.precaching.getCacheKeyForURL
the method is used to get the cache key of offline.html file which was already precached.
Now the complete code inside sw-base.js file is given below.
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/4.3.1/workbox-sw.js'
);
workbox.routing.registerRoute(
new RegExp('/photos/'),
new workbox.strategies.StaleWhileRevalidate({
cacheName: 'photos',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 15
}),
new workbox.cacheableResponse.Plugin({
statuses: [200]
})
]
})
);
workbox.precaching.precacheAndRoute([]);
const networkFirstHandler = new workbox.strategies.NetworkFirst({
cacheName: 'dynamic',
plugins: [
new workbox.expiration.Plugin({
maxEntries: 10
}),
new workbox.cacheableResponse.Plugin({
statuses: [200]
})
]
});
const FALLBACK_URL = workbox.precaching.getCacheKeyForURL('/offline.html');
const matcher = ({ event }) => event.request.mode === 'navigate';
const handler = args =>
networkFirstHandler
.handle(args)
.then(response => response || caches.match(FALLBACK_URL))
.catch(() => caches.match(FALLBACK_URL));
workbox.routing.registerRoute(matcher, handler);
I will cover the push notification and background synchronization part in another blog post. If anybody has any suggestions or doubts or need any help comment below and I try will respond to every one of you as early as possible.