Implementing Service worker in Moodle
let’s start with a brief about service worker, usage, and benefits and then continue to implement in moodle as an installable web-app.
Service Worker
A service worker is a script that runs in the background of a web browser, separate from a web page. It can be used to handle background tasks, such as caching resources, intercepting network requests, and providing offline functionality.
Why Use Service Workers?
Service workers can be used to improve the performance and reliability of web applications. For example, they can be used to:
- Cache resources, so that they can be accessed more quickly when the user is offline. with HTML 5 , the browser supported the appcache through the manifest file, where we can define the resource which can be saved on user’s local device, to boost site performance, however, this has been obsolete now and the service worker is being used for that which provides interaction to handle the cache.
- Provide offline functionality or a Progressive web app, so that users can still use your application even when they don’t have an internet connection. This also helps to make your site to be installed as an app on user’s device [mobile/laptop], you can also customize the launch icon, splash screen, title bar, and color, etc
- Intercept network requests, so that you can modify them or block them altogether.
- One can also implement push notifications using that
Implementing Service Worker in Moodle
There are many guides and tutorial to learn about service worker, however, I am also mentioning the basic, which is useful to implement an installable app.
Moodle already handle the static resource cahcing through HTTP header in a very efficient manner. So, you may not get a major advantage in caching uing service worker for pure moodle, however, much beneficial if customiaztion and static resources are their.
Apart from this, you can use service worker to implement other feature.
General Architecture
- Manifest file [required for offline/installable feature]
- One entry in html head for the manifest file
- Service worker js file
- Initializing service worker js code to be available on pages
1. Manifest File
- create a mainfest.json [you can name it whatever you want] at document root of Moodle
{
"short_name": "LMS",
"name": "LMS",
"icons": [
{
"src": "/local/pwa/lms-144.png",
"type": "image/png",
"sizes": "144x144",
"purpose": "any"
},
{
"src": "/local/pwa/css/lms-192.png",
"type": "image/png",
"sizes": "192x192",
"purpose": "any"
},
{
"src": "/local/pwa/css/lms-512.png",
"type": "image/png",
"sizes": "512x512",
"purpose": "any"
}
],
"id": "/?source=pwa",
"start_url": "/?source=pwa",
"background_color": "#3367D6",
"display": "standalone",
"prefer_related_applications":false,
"scope": "/",
"theme_color": "#3367D6"
- icon should be of exact size
- most of the properties are required
- detail of the manifest is available at: https://web.dev/add-manifest/
2. One entry in html head for the manifest file
for this, either you can write a function in theme, or lib.php for any plugin which will be called in the standard head.
function <component>_<pluginame>_before_standard_html_head()
{
return ' <link rel="manifest" href="/manifest.json?x91701">';
}
OR
You can use Moodle front-end feature for this.
Site administration > appearance > additional HTML: Within HEAD (additionalhtmlhead)
3. Service worker js file
there are 2 ways to write up service worker js file code,
1- write in plane js
2- use an existing library, which wraps the strategies and functionalities in easy to use: WORKBOX
following code uses 2nd approach
/**
Import the Workbox library from the Google CDN.
It is possible to serve this locally should we want to remove the dependency on Google
See here for more info: https://developers.google.com/web/tools/workbox/modules/workbox-sw
**/
importScripts('https://storage.googleapis.com/workbox-cdn/releases/6.4.1/workbox-sw.js');
// SETTINGS
workbox.setConfig({
// set the local path is not using Google CDN
//modulePathPrefix: '/directory/to/workbox/'
// By default, workbox-sw will use the debug build for sites on localhost,
// but for any other origin it’ll use the production build. Force by setting to true.
debug: true
});
// Cache settings
const {registerRoute} = workbox.routing;
const {CacheFirst} = workbox.strategies;
const {CacheableResponse} = workbox.cacheableResponse;
const {StaleWhileRevalidate}= workbox.strategies;
workbox.core.setCacheNameDetails({
// set the default cache name prefix. each domain should be unique to stop clashes
// this is used for runtime and precaching only
prefix: 'mappv1'
});
// Using cache first strategy since the WOFF2 files are fingerprinted and this
// filename will change once a new version is created
registerRoute(
// match only with assets on the assets domain
new RegExp('.*\.woff2$'),
new CacheFirst({
cacheName: 'lms-font-cache',
plugins: [
new CacheableResponse({statuses: [0, 200]})
]
})
);
- for an example perspective, I am using the StaleWhileRevalidate approach of caching to cache the font file in the user’s browser.
- it’s up to you how and what you want to cache
- you should also save this file at the document root, name [sw.js, however, it can be anything]. Saving at the document root is required because of scope, otherwise, you can achieve that through rewrite URL
Additional Resources
https://developer.chrome.com/docs/workbox/
https://web.dev/learn/pwa/workbox/
4. Initializing service worker js code to be available on pages
the following code will show a button to the user if the app is not installed in user’s system as well as initialize the service worker file
// Registering Service worker
if ('serviceWorker' in navigator) {
navigator.serviceWorker
.register(M.cfg.wwwroot+'/sw.js', { scope: '/' })
.then(function(registration) {
console.log("Service Worker Registered");
})
.catch(function(err) {
console.log("Service Worker Failed to Register", err);
})
}
// below coed is to show install button to user
window.addEventListener("beforeinstallprompt", (e) => {
// Prevent the mini-infobar from appearing on mobile
e.preventDefault();
// Stash the event so it can be triggered later.
window.deferredPrompt = e;
console.log("Registerd event");
// Update UI notify the user they can install the PWA
window.localStorage.setItem("pwainstalled", "false");
installButtonDisplay();
});
window.addEventListener("appinstalled", (evt) => {
// Log install to analytics
console.log("INSTALL: Success");
window.localStorage.setItem("pwainstalled", "true");
});
// you may need to change this code to put the button into visible part
function installButtonDisplay() {
console.log("Installed button added at page");
var btn = document.createElement("BUTTON");
btn.setAttribute("id", "install-button");
btn.innerHTML = "Download Web-App";
btn.onclick = function() {
installPWA();
}
document.body.appendChild(btn);
}
// install web-app
function installPWA() {
if (window.deferredPrompt) {
console.log("inside window.deferredPromp if condition");
window.deferredPrompt.prompt();
window.deferredPrompt.userChoice.then((choiceResult) => {
if (choiceResult.outcome === "accepted") {
removeButton();
console.log("User accepted the install prompt");
} else {
isInstalledState(false);
console.log("User dismissed the install prompt");
}
});
}
}
// remove button
function removeButton() {
var elem = document.getElementById('install-button');
elem.parentNode.removeChild(elem);
}
The above code is to add the button ad content area, however, the browser also shows this in the address bar by default
How to debug
- as this is all Javascript and browser-based, you can use the developer toolbar to debug the issues
- In Chrome, you can use Lighthouse to analyze and figure out the issues, just click on analyze page load
If you are customizing the moodle, then service worker implementation is beneficial in aspect of User experince on desktop and mobile device, to show this as web-app as well as caching up the resources.