Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make the PWA code more modern and readable #1207

Merged
merged 4 commits into from
Mar 5, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 90 additions & 93 deletions files/en-us/web/progressive_web_apps/app_structure/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -120,61 +120,61 @@ <h3 id="The_main_app_JavaScript">The main app JavaScript</h3>

<p>The app.js file does a few things we will look into closely in the next articles. First of all it generates the content based on this template:</p>

<pre class="brush: js">var template = "&lt;article&gt;\n\
&lt;img src='data/img/SLUG.jpg' alt='NAME'&gt;\n\
&lt;h3&gt;#POS. NAME&lt;/h3&gt;\n\
&lt;ul&gt;\n\
&lt;li&gt;&lt;span&gt;Author:&lt;/span&gt; &lt;strong&gt;AUTHOR&lt;/strong&gt;&lt;/li&gt;\n\
&lt;li&gt;&lt;span&gt;Twitter:&lt;/span&gt; &lt;a href='https://twitter.com/TWITTER'&gt;@TWITTER&lt;/a&gt;&lt;/li&gt;\n\
&lt;li&gt;&lt;span&gt;Website:&lt;/span&gt; &lt;a href='http://WEBSITE/'&gt;WEBSITE&lt;/a&gt;&lt;/li&gt;\n\
&lt;li&gt;&lt;span&gt;GitHub:&lt;/span&gt; &lt;a href='https://GITHUB'&gt;GITHUB&lt;/a&gt;&lt;/li&gt;\n\
&lt;li&gt;&lt;span&gt;More:&lt;/span&gt; &lt;a href='http://js13kgames.com/entries/SLUG'&gt;js13kgames.com/entries/SLUG&lt;/a&gt;&lt;/li&gt;\n\
&lt;/ul&gt;\n\
&lt;/article&gt;";
var content = '';
for(var i=0; i&lt;games.length; i++) {
var entry = template.replace(/POS/g,(i+1))
.replace(/SLUG/g,games[i].slug)
.replace(/NAME/g,games[i].name)
.replace(/AUTHOR/g,games[i].author)
.replace(/TWITTER/g,games[i].twitter)
.replace(/WEBSITE/g,games[i].website)
.replace(/GITHUB/g,games[i].github);
entry = entry.replace('&lt;a href=\'http:///\'&gt;&lt;/a&gt;','-');
content += entry;
};
<pre class="brush: js">const template = `&lt;article&gt;
&lt;img src='data/img/placeholder.png' data-src='data/img/SLUG.jpg' alt='NAME'&gt;
&lt;h3&gt;#POS. NAME&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;span&gt;Author:&lt;/span&gt; &lt;strong&gt;AUTHOR&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Twitter:&lt;/span&gt; &lt;a href='https://twitter.com/TWITTER'&gt;@TWITTER&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;Website:&lt;/span&gt; &lt;a href='http://WEBSITE/'&gt;WEBSITE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;GitHub:&lt;/span&gt; &lt;a href='https://GITHUB'&gt;GITHUB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;span&gt;More:&lt;/span&gt; &lt;a href='http://js13kgames.com/entries/SLUG'&gt;js13kgames.com/entries/SLUG&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/article&gt;`;
let content = '';
for (let i = 0; i &lt; games.length; i++) {
let entry = template.replace(/POS/g, (i + 1))
.replace(/SLUG/g, games[i].slug)
.replace(/NAME/g, games[i].name)
.replace(/AUTHOR/g, games[i].author)
.replace(/TWITTER/g, games[i].twitter)
.replace(/WEBSITE/g, games[i].website)
.replace(/GITHUB/g, games[i].github);
entry = entry.replace('&lt;a href=\'http:///\'&gt;&lt;/a&gt;', '-');
content += entry;
}
document.getElementById('content').innerHTML = content;</pre>

<p>Next, it registers a service worker:</p>

<pre class="brush: js">if('serviceWorker' in navigator) {
navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
navigator.serviceWorker.register('/pwa-examples/js13kpwa/sw.js');
};</pre>

<p>The next code block requests permission for notifications when a button is clicked:</p>

<pre class="brush: js">var button = document.getElementById("notifications");
button.addEventListener('click', function(e) {
Notification.requestPermission().then(function(result) {
if(result === 'granted') {
randomNotification();
}
});
<pre class="brush: js">const button = document.getElementById('notifications');
button.addEventListener('click', () =&gt; {
Notification.requestPermission().then((result) =&gt; {
if (result === 'granted') {
randomNotification();
}
});
});</pre>

<p>The last block creates notifications that display a randomly-selected item from the games list:</p>

<pre class="brush: js">function randomNotification() {
var randomItem = Math.floor(Math.random()*games.length);
var notifTitle = games[randomItem].name;
var notifBody = 'Created by '+games[randomItem].author+'.';
var notifImg = 'data/img/'+games[randomItem].slug+'.jpg';
var options = {
body: notifBody,
icon: notifImg
}
var notif = new Notification(notifTitle, options);
setTimeout(randomNotification, 30000);
const randomItem = Math.floor(Math.random() * games.length);
const notifTitle = games[randomItem].name;
const notifBody = `Created by ${games[randomItem].author}.`;
const notifImg = `data/img/${games[randomItem].slug}.jpg`;
const options = {
body: notifBody,
icon: notifImg,
};
new Notification(notifTitle, options);
setTimeout(randomNotification, 30000);
}</pre>

<h3 id="The_service_worker">The service worker</h3>
Expand All @@ -185,8 +185,8 @@ <h3 id="The_service_worker">The service worker</h3>

<p>Next, it creates a list of all the files to be cached, both from the app shell and the content:</p>

<pre class="brush: js">var cacheName = 'js13kPWA-v1';
var appShellFiles = [
<pre class="brush: js">const cacheName = 'js13kPWA-v1';
const appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
Expand All @@ -204,73 +204,70 @@ <h3 id="The_service_worker">The service worker</h3>
'/pwa-examples/js13kpwa/icons/icon-168.png',
'/pwa-examples/js13kpwa/icons/icon-192.png',
'/pwa-examples/js13kpwa/icons/icon-256.png',
'/pwa-examples/js13kpwa/icons/icon-512.png'
'/pwa-examples/js13kpwa/icons/icon-512.png',
];
var gamesImages = [];
for(var i=0; i&lt;games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
const gamesImages = [];
for (let i = 0; i &lt; games.length; i++) {
gamesImages.push(`data/img/${games[i].slug}.jpg`);
}
var contentToCache = appShellFiles.concat(gamesImages);</pre>
const contentToCache = appShellFiles.concat(gamesImages);</pre>

<p>The next block installs the service worker, which then actually caches all the files contained in the above list:</p>

<pre class="brush: js">self.addEventListener('install', function(e) {
<pre class="brush: js">self.addEventListener('install', (e) =&gt; {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
e.waitUntil((async () =&gt; {
const cache = await caches.open(cacheName);
console.log('[Service Worker] Caching all: app shell and content');
await cache.addAll(contentToCache);
})());
});</pre>

<p>Last of all, the service worker fetches content from the cache if it is available there, providing offline functionality:</p>

<pre class="brush: js">self.addEventListener('fetch', function(e) {
e.respondWith(
caches.match(e.request).then(function(r) {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then(function(response) {
return caches.open(cacheName).then(function(cache) {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
<pre class="brush: js">self.addEventListener('fetch', (e) =&gt; {
e.respondWith((async () =&gt; {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (r) { return r; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})());
});</pre>

<h3 id="The_JavaScript_data">The JavaScript data</h3>

<p>The games data is present in the data folder in a form of a JavaScript object (<a href="https://github.com/mdn/pwa-examples/blob/master/js13kpwa/data/games.js">games.js</a>):</p>

<pre class="brush: js">var games = [
{
slug: 'lost-in-cyberspace',
name: 'Lost in Cyberspace',
author: 'Zosia and Bartek',
twitter: 'bartaz',
website: '',
github: 'github.com/bartaz/lost-in-cyberspace'
},
{
slug: 'vernissage',
name: 'Vernissage',
author: 'Platane',
twitter: 'platane_',
website: 'github.com/Platane',
github: 'github.com/Platane/js13k-2017'
},
// ...
{
slug: 'emma-3d',
name: 'Emma-3D',
author: 'Prateek Roushan',
twitter: '',
website: '',
github: 'github.com/coderprateek/Emma-3D'
}
{
slug: 'lost-in-cyberspace',
name: 'Lost in Cyberspace',
author: 'Zosia and Bartek',
twitter: 'bartaz',
website: '',
github: 'github.com/bartaz/lost-in-cyberspace'
},
{
slug: 'vernissage',
name: 'Vernissage',
author: 'Platane',
twitter: 'platane_',
website: 'github.com/Platane',
github: 'github.com/Platane/js13k-2017'
},
// ...
{
slug: 'emma-3d',
name: 'Emma-3D',
author: 'Prateek Roushan',
twitter: '',
website: '',
github: 'github.com/coderprateek/Emma-3D'
}
];</pre>

<p>Every entry has its own image in the data/img folder. This is our content, loaded into the content section with JavaScript.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ <h3 id="Registering_the_Service_Worker">Registering the Service Worker</h3>
<p><strong>NOTE</strong> : We're using the <a href="http://es6-features.org/">es6</a> <strong>arrow functions</strong> syntax in the Service Worker Implementation</p>

<pre class="brush: js">if('serviceWorker' in navigator) {
navigator.serviceWorker.register('./pwa-examples/js13kpwa/sw.js');
navigator.serviceWorker.register('./pwa-examples/js13kpwa/sw.js');
};</pre>

<p>If the service worker API is supported in the browser, it is registered against the site using the {{domxref("ServiceWorkerContainer.register()")}} method. Its contents reside in the sw.js file, and can be executed after the registration is successful. It's the only piece of Service Worker code that sits inside the app.js file; everything else that is Service Worker-specific is written in the sw.js file itself.</p>
Expand All @@ -58,15 +58,15 @@ <h4 id="Installation">Installation</h4>
<p>The API allows us to add event listeners for key events we are interested in — the first one is the <code>install</code> event:</p>

<pre class="brush: js">self.addEventListener('install', (e) =&gt; {
console.log('[Service Worker] Install');
console.log('[Service Worker] Install');
});</pre>

<p>In the <code>install</code> listener, we can initialize the cache and add files to it for offline use. Our js13kPWA app does exactly that.</p>

<p>First, a variable for storing the cache name is created, and the app shell files are listed in one array.</p>

<pre class="brush: js">var cacheName = 'js13kPWA-v1';
var appShellFiles = [
<pre class="brush: js">const cacheName = 'js13kPWA-v1';
const appShellFiles = [
'/pwa-examples/js13kpwa/',
'/pwa-examples/js13kpwa/index.html',
'/pwa-examples/js13kpwa/app.js',
Expand All @@ -89,22 +89,21 @@ <h4 id="Installation">Installation</h4>

<p>Next, the links to images to be loaded along with the content from the data/games.js file are generated in the second array. After that, both arrays are merged using the {{jsxref("Array.prototype.concat()")}} function.</p>

<pre class="brush: js">var gamesImages = [];
for(var i=0; i&lt;games.length; i++) {
gamesImages.push('data/img/'+games[i].slug+'.jpg');
<pre class="brush: js">const gamesImages = [];
for (let i = 0; i &lt; games.length; i++) {
gamesImages.push(`data/img/${games[i].slug}.jpg`);
}
var contentToCache = appShellFiles.concat(gamesImages);</pre>
const contentToCache = appShellFiles.concat(gamesImages);</pre>

<p>Then we can manage the <code>install</code> event itself:</p>

<pre class="brush: js">self.addEventListener('install', (e) =&gt; {
console.log('[Service Worker] Install');
e.waitUntil(
caches.open(cacheName).then((cache) =&gt; {
console.log('[Service Worker] Caching all: app shell and content');
return cache.addAll(contentToCache);
})
);
e.waitUntil((async () =&gt; {
const cache = await caches.open(cacheName);
console.log('[Service Worker] Caching all: app shell and content');
await cache.addAll(contentToCache);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was returned before (I think, return await is not needed, since async functions always return a Promise by definition).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but that's precisely what what confusing about the previous syntax, we returned the promised to await it, but we don't do anything with the resolved value

With this refactoring, the install event awaits for the caching of assets, but we don't return anything we won't use, which is clearer imho

Btw addAll returns undefined so this is a strictly ISO refactoring

})());
});</pre>

<p>There are two things that need an explanation here: what {{domxref("ExtendableEvent.waitUntil")}} does, and what the {{domxref("Cache","caches")}} object is.</p>
Expand All @@ -126,26 +125,24 @@ <h3 id="Responding_to_fetches">Responding to fetches</h3>
<p>We also have a <code>fetch</code> event at our disposal, which fires every time an HTTP request is fired off from our app. This is very useful, as it allows us to intercept requests and respond to them with custom responses. Here is a simple usage example:</p>

<pre class="brush: js">self.addEventListener('fetch', (e) =&gt; {
console.log('[Service Worker] Fetched resource '+e.request.url);
console.log(`[Service Worker] Fetched resource ${e.request.url}`);
});</pre>

<p>The response can be anything we want: the requested file, its cached copy, or a piece of JavaScript code that will do something specific — the possibilities are endless.</p>

<p>In our example app, we serve content from the cache instead of the network as long as the resource is actually in the cache. We do this whether the app is online or offline. If the file is not in the cache, the app adds it there first before then serving it:</p>

<pre class="brush: js">self.addEventListener('fetch', (e) =&gt; {
e.respondWith(
caches.match(e.request).then((r) =&gt; {
console.log('[Service Worker] Fetching resource: '+e.request.url);
return r || fetch(e.request).then((response) =&gt; {
return caches.open(cacheName).then((cache) =&gt; {
console.log('[Service Worker] Caching new resource: '+e.request.url);
cache.put(e.request, response.clone());
return response;
});
});
})
);
e.respondWith((async () =&gt; {
const r = await caches.match(e.request);
console.log(`[Service Worker] Fetching resource: ${e.request.url}`);
if (r) { return r; }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🙌

const response = await fetch(e.request);
const cache = await caches.open(cacheName);
console.log(`[Service Worker] Caching new resource: ${e.request.url}`);
cache.put(e.request, response.clone());
return response;
})());
});</pre>

<p>Here, we respond to the fetch event with a function that tries to find the resource in the cache and return the response if it's there. If not, we use another fetch request to fetch it from the network, then store the response in the cache so it will be available there next time it is requested.</p>
Expand All @@ -167,11 +164,10 @@ <h2 id="Updates">Updates</h2>
// ...

self.addEventListener('install', (e) =&gt; {
e.waitUntil(
caches.open('js13kPWA-v2').then((cache) =&gt; {
return cache.addAll(contentToCache);
})
);
e.waitUntil((async () =&gt; {
const cache = await caches.open(cacheName);
Ryuno-Ki marked this conversation as resolved.
Show resolved Hide resolved
await cache.addAll(contentToCache);
})());
});</pre>

<p>A new service worker is installed in the background, and the previous one (v1) works correctly up until there are no pages using it — the new Service Worker is then activated and takes over management of the page from the old one.</p>
Expand All @@ -181,15 +177,13 @@ <h2 id="Clearing_the_cache">Clearing the cache</h2>
<p>Remember the <code>activate</code> event we skipped? It can be used to clear out the old cache we don't need anymore:</p>

<pre class="brush: js">self.addEventListener('activate', (e) =&gt; {
e.waitUntil(
caches.keys().then((keyList) =&gt; {
return Promise.all(keyList.map((key) =&gt; {
if(key !== cacheName) {
return caches.delete(key);
}
}));
})
);
e.waitUntil((async () =&gt; {
const keyList = await caches.keys();
await Promise.all(keyList.map((key) =&gt; {
if (key === cacheName) { return; }
await caches.delete(key);
Ryuno-Ki marked this conversation as resolved.
Show resolved Hide resolved
}))
})());
});</pre>

<p>This ensures we have only the files we need in the cache, so we don't leave any garbage behind; the <a href="/en-US/docs/Web/API/IndexedDB_API/Browser_storage_limits_and_eviction_criteria">available cache space in the browser is limited</a>, so it is a good idea to clean up after ourselves.</p>
Expand Down
Loading