Whoa hold on! What are you talking about?
Trust me it's possible... Alright the title was a little catchy I know it is not actually native more of like a native feeling. But hold on don't go away yet. I have a question! What are your needs in this native app?
Does it includes one of the following?
- Integration with other apps
- Hardware specific feature
- System settings
Then yes native like apps are not for you. Native apps are mostly developed due to hardware specific feature.
But if you don't need any of the above then this blog is for you.
So what is it you hyped me for? 🤔
I'm talking about Hybrid apps, specificly it's called Progressive Web Application, PWA for short. It is developed in JavaScript. It is also...
- A small cross-platform app
- Less network traffic in comparison with websites
- The cool way to develop applications on the Web!
Yes PWAs are running in browsers which are cross platform so you don't have to learn multiple languages or pay multiple developer teams to build an app for every one.
Do you want to see a live demo of a PWA?
This sounds promising right? Well it is until your user uses the browser with PWA support.
Safari, Firefox and Opera doesn't support it on desktop. On mobile almost every browsers support it except Opera. Yes I know Firefox has an extension which will grant you access to the features of PWA, but not everybody uses extensions or know how do they work.
Good news is most users use chrome for browsing:
PWA uses most features of ServiceWorker API
So if your user's browser doesn't support PWA API it may have some good features still.
SIDE NOTE: My opionon is web will take over more users in the near future due to it's cross availability. Also we web developers are waiting for browsers to implement more features securely. And let the backward compatibilites behind from 1980s and 1990s (like WebGL 1.0). Tell me what you think about this in the comments please.
Talking about features what do we have?
- Every API your browser support! (USB, Bluetooth, HID, File, etc..)
- Caching this will reduce traffic a lot, pointless requests just to load the view of your app are gone with cache!
- Offline usability, because of cached data!
- Your users can decide to use your app with or withouth installing it.
- The app can send notifications to your users (push notifications are possible too)!
- You can run the app with a native app feeling so you don't see the search bar and arrows etc. there are multiple options fullscreen, standalone or minimal ui.
- Desktop icon. Cool right!? 😎
What do you need to run a PWA? 🚀
- Manifest file
- Service Worker
- Any kind of web app developed or bundled in JavaScript
Manifest file
It is a json file describing basic settings for your app, like how should it be displayed, what name, icon and theme color you want. There is a deep description about it on web.dev .
Service Worker script
A script running in the background. It caches data and will refresh the content off that cache when it is needed. When is it needed? Whenever you want it to, for example if there is any bytes whom changed in your Service Worker file or on every Monday. But you have the power in your hand to tell the app. The following code can be found at Google's developer site. Also web.dev has a pretty good explanation with a game helping to visualize the process of the service worker.
The Code
Main.js
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/SW.js').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);
});
});
}
Only fires when browser supports ServiceWorker API:
if ('serviceWorker' in navigator) {
On page load we register the SW.js file which is a Promise with parameters of 2 callbacks (success, error):
navigator.serviceWorker.register('/SW.js')
.then(function(registration) {
// Do smh on success
}, function(err) {
// Do smh on error
});
SW.js
var CACHE_NAME = "my-site-cache-v1";
var urlsToCache = ["/", "/styles/main.css", "/script/main.js"];
self.addEventListener("install", function (event) {
// Perform install steps
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
});
self.addEventListener("fetch", function (event) {
event.respondWith(
caches.match(event.request).then(function (response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request).then(function (response) {
// Check if we received a valid response
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
// IMPORTANT: Clone the response. A response is a stream
// and because we want the browser to consume the response
// as well as the cache consuming the response, we need
// to clone it so we have two streams.
var responseToCache = response.clone();
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, responseToCache);
});
return response;
});
})
);
});
self.addEventListener("activate", function (event) {
console.log("Cache is updated!");
});
We need to specify a name for the cache file(s):
var CACHE_NAME = "my-site-cache-v1";
Also specify the caching files:
var urlsToCache = ["/", "/styles/main.css", "/script/main.js"];
The Installing process it has an event listener which will be called on install:
self.addEventListener("install", function (event) {
We wait until our Promise fullfills. caches.open will create a cache with our specified name, which will be available in the promise callback. Then we can add all our file names to be cached in an array. And that cache will be returned.
event.waitUntil(
caches.open(CACHE_NAME).then(function (cache) {
console.log("Opened cache");
return cache.addAll(urlsToCache);
})
);
NOTE: If any of the files are failed to be cached the installation process it will fail. For now the error handling doesn't handling this in the API.
Get the offline content calls the fetch event listener:
self.addEventListener("fetch", function (event) {
We need to repsond to the event:
event.respondWith(
Check with a promise if the requested cache exists, if yes return it.
caches.match(event.request).then(function (response) {
// Cache hit - return response
if (response) {
return response;
}
If the requested cache is not available we can make a network request by returning a fetch pulling the request.
return fetch(event.request);
If the requested cache is not available and you want to cumultatively cache those files. Make a network request with fetch and in the promise we can cache them. An error handling is needed here, in case the files failed to load we return them immediatelly:
return fetch(event.request).then(function (response) {
// Check if we received a valid response if not return instantly
if (!response || response.status !== 200 || response.type !== "basic") {
return response;
}
// Create another stream
var responseToCache = response.clone();
caches.open(CACHE_NAME).then(function (cache) {
cache.put(event.request, responseToCache);
});
return response;
});
IMPORTANT: Clone the response with response.clone(). A response is a stream and because we want the browser to consume the response as well as the cache consuming the response, we need to clone it so we have two streams.
The cache in the above code happens the following we open the cache by name and then put the file with the cache.put(file, stream):
cache.put(event.request, responseToCache);
Make it look fancy
You have the possibility to create
- installers for your app
- custom installer button
Installer.js
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
});
btnAdd.addEventListener('click', (e) => {
// hide our user interface that shows our A2HS button
btnAdd.style.display = 'none';
// Show the prompt
deferredPrompt.prompt();
// Wait for the user to respond to the prompt
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
deferredPrompt = null;
});
});
You have 2 event listeners "beforeinstallprompt" and "appinstalled". If the app is not installed then the "beforeinstallprompt" will be fired and the event in the parameter will be the property which we can later prompt so lets save and call it now deferredprompt because we need it later when the user presses the install button:
let deferredPrompt;
window.addEventListener('beforeinstallprompt', (e) => {
// Prevent Chrome 67 and earlier from automatically showing the prompt
e.preventDefault();
// Stash the event so it can be triggered later.
deferredPrompt = e;
});
NOTE: If you don't want the installer to fire immediatelly call preventDefault() on the event. (We are creating an installer flow here so we gon do that.)
In the event listener of the button we can call the prompt() and a prompt will popup:
deferredPrompt.prompt();
We can listen the outcome of the prompt and do anything with it, maybe greet the user or appreciate the installation. the result will be the choiceResult parameter's outcome property. Here we only log it to console:
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
});
To prevent installation fire again we empty the deferredprompt variable with null after choiceResult received:
deferredPrompt = null;
🎉 Now you should have a working PWA 🎉
PLUS: With a state management library like React you have to store the deferredprompt in the state like so:
App.jsx
import React, { Component } from "react";
import { checkInstallPWA, installPwa } from "path to dir/PWA.js";
class App extends Component {
constructor(props) {
super(props);
this.state = {
deferredPrompt: false,
}
checkInstallPWA((param) => this.setState({ deferredPrompt: param }));
}
render() {
const { deferredPrompt } = this.state;
return (
<main className="App">
{deferredpropmt && (
<button className="install" onClick={() => installPwa(deferredPrompt)}>
install
</button>
)}
</main>
}
}
PWA.js
export const checkInstallPWA = (func) => {
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
func(e);
console.log("PWA: Installable");
});
window.addEventListener("appinstalled", (evt) => {
func(false);
console.log("PWA INSTALL: Success");
});
};
export const installPwa = (deferredPrompt) => {
deferredPrompt.prompt();
deferredPrompt.userChoice
.then((choiceResult) => {
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the A2HS prompt');
} else {
console.log('User dismissed the A2HS prompt');
}
});
}
This was a long read I know but thanks for reading trough!
Please leave your opinion in the comments and thanks for all the awesome readers who claps/likes/loves my post.