20

In service worker, I can define array of resource those are being cached during service worker get started mentioned below:

self.addEventListener('install', event => {
    event.waitUntil(caches.open('static-${version}')
        .then(cache => cache.addAll([
            '/styles.css',
            '/script.js'
        ]))
    );
});

How can I define a path/directory in service worker, so that instead of writing all files name, service worker pick all files from given path/directory and add all in cache?

Gul Ershad
  • 1,743
  • 2
  • 25
  • 32
Mohd Yasin
  • 427
  • 1
  • 5
  • 13

4 Answers4

6

Yes you can. I have also such kind of problem and I find cool solution using performance. Here is my sw.js:

const KEY = 'key';

self.addEventListener('install', (event) => {
    event.waitUntil(self.skipWaiting());
});

self.addEventListener('message', (event) => {
    if (event.data.type === 'CACHE_URLS') {
        event.waitUntil(
            caches.open(KEY)
                .then( (cache) => {
                    return cache.addAll(event.data.payload);
                })
        );
    }
});

Here is my main.js:

if ('serviceWorker' in navigator) {
    navigator.serviceWorker.register('/sw.js', { scope: '/' })
        .then((registration) => {
            const data = {
                type: 'CACHE_URLS',
                payload: [
                    location.href,
                    ...performance.getEntriesByType('resource').map((r) => r.name)
                ]
            };
            registration.installing.postMessage(data);
        })
        .catch((err) => console.log('SW registration FAIL:', err));
}

By this you can also add some filter which will cache specific path.

Jahongir
  • 141
  • 2
  • 7
  • SW registration FAIL: TypeError: Cannot read properties of null (reading 'postMessage') – Mashwishi May 13 '22 at 02:48
  • Mashwishi, Looks like that sw registration failed and the value of `registration` variable is null, could u please change the content of `.then` and see what error will be be printed – Jahongir May 30 '22 at 23:56
6

Yes. To cache many files from multiple directories ...
I use a helper function in the service worker that I named "getFileArray( ... )." It takes one string argument of the directory name. For multiple directories, I use an array of these in Promise.all:

let cache_name = "cache-A";
let filesToCache = [
    "https://myApp.com/",
    "index.php?launcher=true;homescreen=1",
    "manifest.json",
    "favicon.ico",
];

self.addEventListener( "install", eo => {
    self.skipWaiting();
    eo.waitUntil( filesAreCached() );
} );

///////| helper functions |/////////
function filesAreCached(){
    
    Promise.all([
        /* name the directories whose files you wish to cache */
        getFileArray( "js" ),
        getFileArray( "css" ),
        getFileArray( "images" ),
        getFileArray( "screens" ),
        getFileArray( "modals" )        
    ])
    .then( promiseArray => {
        let promisedFiles = [];
        promiseArray.forEach( array => {
            promisedFiles = promisedFiles.concat( array ) ;
        } );
        return promisedFiles;       
    }) 
    .then( promisedFiles => {
        filesToCache = filesToCache.concat( promisedFiles );
        console.log( "Cached files:", filesToCache  );
        return self.caches.open( cache_name );
    })
    .then( cache => cache.addAll( filesToCache ) );
}

/* 
 the following function calls a server script that returns an array of filenames,
 each one prepended with the directory name:  
*/
async  function getFileArray( directory ){
    let form = new FormData();
    form.append( `directory`, directory );
    let fileArray = await fetch( `php/getFileArray.php`, { method: `POST`, body: form })
    .then( response => response.json() ) ;
    
    return fileArray;
}

The PHP code (getFileArray.php) looks like this:

<?php
/*  
Motivation: To help provide an accurate list of files
for JavScript service workers to cache. Saves time,
tedium, and possible typos in doing it manually.
        
Use the POSTed directory path to return an array
that lists all the files in that directory,
less the "." and ".." entries.
Prepend the directory name to the filenames so that
we have the "full path" to each file.
Return this array as a json string.
*/   
     $directory = $_POST["directory"] ;
      /* 
       You should probably sanitize $directory of all "../" prefixes
       in order to prevent a Directory Traversal Attack. 
       Using str_replace("/", "", $directory) has been suggested.
       It throws an error but prevents the attack.
     */
     $filelistArray = scandir( "../" . $directory );
     $cleanFileArray =  array();
     foreach( $filelistArray  as $file ){
         if ( $file !== "." and $file !== ".." ){
                array_push( $cleanFileArray, $directory . "/" . $file );
         }       
     }
     $fileArrayJson = json_encode( $cleanFileArray );
     exit( $fileArrayJson );
?>

Of course, any backend language can be used.
This technique might be a little unsophisticated, but it works for me :)

3

That is not possible. The SW (or the browser, for that matter) doesn't have any clue about the files in a specific path on the web server. You have to give the names of the files you want to be cached. More on the same issue here.

Are you using some build tooling to automatically generate the list of files? If not, you most likely should :)

EDIT:

One of the most used libraries for SW tooling is Workbox. They offer both runtime-caching and precaching of assets. They also have build tooling plugins for eg. Webpack and Gulp.

Runtime-caching works by giving the asset from the cache if it exists in there and anyway updating it from the server. Basically every new asset will be initially requested from the network and then returned from the cache on subsequent requests.

EDIT2:

Yes, you can use Workbox without NPM to some extent. You need to run NPM scripts etc. to gather the filenames for files to be cached BUT you can still implement runtime-caching just by importing the Workbox.js script in your hand-written SW file.

Just by saying

importScript("https://unpkg.com/workbox-sw@2.1.0/build/importScripts/workbox-sw.prod.v2.1.0.js") 

At the top of your SW imports the latest (as of now) version of Workbox. You can see that's what happens in the runtime-caching example here too.

You can also download the .js file above and place it on your own server and import it from a relative path instead.

pate
  • 4,937
  • 1
  • 19
  • 25
  • Thank you @pate. Now am thinking to automatically generate the list of files. Can you suggest me the best way to do this. – Mohd Yasin Oct 19 '17 at 16:22
  • Can I cache at run time? – Mohd Yasin Oct 20 '17 at 02:34
  • I read workbox theory. It is also required Node and npm. It can be used without Node and npm? Any example will be very helpful. – Mohd Yasin Oct 20 '17 at 09:58
  • @MhodYasin Yes, it can be used. Check out my updated answer. – pate Oct 22 '17 at 14:56
  • Hi @pate, below i posted sample code to implement Workbox sw runtime caching. This is running without any error. Is it written perfectly or need any improvement? I need your feedback or help to improve this. – Mohd Yasin Oct 23 '17 at 14:29
0

Runtime caching using Workbox sw.

service-worker.js:

importScripts('https://unpkg.com/workbox-sw@0.0.2/build/importScripts/workbox-sw.dev.v0.0.2.js');
importScripts('https://unpkg.com/workbox-runtime-caching@1.3.0/build/importScripts/workbox-runtime-caching.prod.v1.3.0.js');
importScripts('https://unpkg.com/workbox-routing@1.3.0/build/importScripts/workbox-routing.prod.v1.3.0.js');

const assetRoute = new workbox.routing.RegExpRoute({
    regExp: new RegExp('^http://localhost:8081/jobs/static/*'),
    handler: new workbox.runtimeCaching.CacheFirst()
});

const router = new workbox.routing.Router();
//router.addFetchListener();
router.registerRoutes({routes: [assetRoute]});
router.setDefaultHandler({
    handler: new workbox.runtimeCaching.CacheFirst()
});

Script in my html file to load Servcie worker.

<script>
    if ('serviceWorker' in navigator) {
        window.addEventListener('load', function() {
            navigator.serviceWorker.register('http://localhost:8081/jobs/static/service-worker.js?v=4').then(function(registration) {
            // Registration was successful
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }, function(err) {
            // registration failed :(
            console.log('ServiceWorker registration failed: ', err);
            });
        });
    }
</script>
Mohd Yasin
  • 427
  • 1
  • 5
  • 13