"Offline first" è un paradigma di sviluppo di applicazioni in cui gli sviluppatori assicurano che la funzionalità di un'app non sia influenzata da una perdita temporanea della connettività di rete. Le applicazioni Web progressive, che sembrano applicazioni native ma funzionano come applicazioni Web, sono spesso basate su questo paradigma.
Questo tutorial ti insegnerà come creare una prima applicazione offline con Node.js e un database SQLite. Iniziamo con un'introduzione alle app Web progressive.
Introduzione alla PWA
Le Progressive Web App (PWA) sono app Web che utilizzano service worker, manifest e altre funzionalità della piattaforma Web e miglioramenti progressivi per offrire agli utenti un'esperienza paragonabile alle app native.
Le PWA a volte possono superare le app native in termini di efficienza. Funzionano su richiesta e sono sempre disponibili senza consumare memoria o dati preziosi dello smartphone. Gli utenti consumano meno dati quando scelgono una PWA rispetto a una versione nativa della stessa applicazione. Possono comunque salvare la PWA nella schermata iniziale; è installabile senza la necessità di un download completo.
Cosa stiamo costruendo?
Per dimostrare la potenza delle applicazioni web progressive, creeremo una semplice applicazione per blog.
L'utente potrà interagire con esso come altre PWA, come Twitter PWA. Andiamo subito al punto.
Inizializza l'applicazione NodeJs
Sporciamoci le mani. Per iniziare, creeremo la nostra cartella del progetto con il comando seguente:
mkdir PWA && cd PWA
Quindi, inizializzeremo un'applicazione Node.js con i comandi seguenti:
npm init -y
Il comando precedente crea un package.json
file per la domanda.
Quindi, crea la seguente struttura di cartelle nella cartella del nostro progetto:
Configura un server Express
Con la configurazione dell'applicazione, installiamo Express per creare il nostro server Node.js con il comando seguente:
npm install express
Quindi, creeremo un paio di cartelle e file nella cartella pubblica:
- file css/style.css
- file js/app.js
Quindi, crea un index.js
file nella directory principale del progetto con i seguenti frammenti di codice di seguito:
const express = require("express");
const path = require("path");
const app = express();
app.use(express.static(path.join(__dirname, "public")));
app.get("/", function (req, res) {
res.sendFile(path.join(__dirname, "public/index.html"));
});
app.listen(8000, () => console.log("Server is running on Port 8000"));
Nello snippet di codice importiamo express per creare il nostro server e il percorso modulo. Abbiamo configurato la nostra app per eseguire il rendering dei nostri file statici utilizzando express.static metodo, che prende il percorso della cartella statica (pubblica), abbiamo creato il percorso principale della nostra applicazione e reso il index.html file. Quindi abbiamo configurato l'app per ascoltare la porta 8000 .
Connessione al database SQLite
Con la configurazione del server per la nostra applicazione, creiamo e colleghiamo la nostra applicazione per salvare i dettagli del nostro blog. Per iniziare, esegui il comando seguente per installare la dipendenza sqlite3.
npm install sqlite3
Quindi, nel punto di ingresso index.js
file, aggiungi il frammento di codice di seguito per creare e connettere l'applicazione a un database SQLite.
const db = new sqlite3.Database("db.sqlite", (err) => {
if (err) {
// Cannot open database
console.error(err.message);
throw err;
} else {
console.log("Connected to the SQLite database.");
}
});
Successivamente, creeremo un elenco di blog che memorizzeremo nel nostro database e renderemo in seguito sul lato client con lo snippet di codice di seguito:
let blogs = [
{
id: "1",
title: "How To Build A RESTAPI With Javascript",
avatar: "images/coffee2.jpg",
intro: "iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",
},
{
id: "2",
title: "How to Build an Offline-First Application with Node.js,"
avatar: "images/coffee2.jpg",
"iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",
},
{
id: "3",
title: "Building a Trello Clone with React DnD",
avatar: "images/coffee2.jpg",
intro: "iste odio beatae voluptas dolor praesentium illo facere optio nobis magni, aspernatur quas.",
},
];
Ogni post di blocco nella nostra applicazione avrà un id , titolo , avatar e introduzione campi.
Ora crea un nome di tabella di database blog e salva i dettagli del blog che abbiamo appena creato sopra con lo snippet di codice qui sotto:
db.run(
`CREATE TABLE blog (id INTEGER PRIMARY KEY AUTOINCREMENT, title text,avatar text,intro text)`,
(err) => {
if (err) {
// console.log(err)
// Table already created
} else {
// Table just created, creating some rows
var insert = "INSERT INTO blogs (title, avatar, intro) VALUES (?,?,?)";
blogs.map((blog) => {
db.run(insert, [
`${blog.title}`,
`${blog.avatar}`,
`${blog.intro}`,
]);
});
}
}
);
Nello snippet di codice abbiamo creato una tabella blog utilizzando db.run. Il db.run prende una query SQL come parametro, quindi scorre la nostra matrice di blog e li inseriamo nella tabella blog che abbiamo appena creato utilizzando la funzione js map.
Visualizza i record del database
Ora vediamo i record che abbiamo appena creato usando Arctype. Per visualizzare i record nel tuo database SQLite utilizzando Arctype, segui i passaggi seguenti:
- Installa Arctype
- Esegui l'applicazione con
node index.js
per creare un database - Avvia Arctype e fai clic sulla scheda SQLite
- Fai clic su Seleziona file SQLite e individua il db.sqlite file generato durante l'esecuzione del server.
- Dovresti vedere la tabella dei blog e i record che creiamo come mostrato nello screenshot qui sotto:
Rendering della pagina
A questo punto abbiamo collegato l'applicazione ad un database SQLite e inserito nel database anche alcuni record. Ora apri index.html file e aggiungi i seguenti frammenti di codice di seguito:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<link rel="stylesheet" href="css/style.css" />
<title>Blogger</title>
<link rel="manifest" href="manifest" />
</head>
<body>
<section>
<nav>
<h1>Blogger</h1>
<ul>
<li>Home</li>
<li class="active">Blog</li>
</ul>
</nav>
<div class="container"></div>
</section>
<script src="js/app.js"></script>
</body>
</html>
Abbiamo creato un semplice markup con collegamenti al nostro manifest nel file sopra, che creeremo nella prossima sezione, stili e app.js File.
Quindi, creeremo un blog percorso nel nostro index.js file per restituire i blog al lato client.
...
app.get("/blogs", (req, res) => {
res.status(200).json({
blogs,
});
});
...
Nel nostro public/js/app.js file, invieremo una richiesta get all'endpoint del blog per ottenere i blog dal nostro backend. Quindi esaminiamo i blog, scegliamo il contenitore classe e mostrarli.
let result = "";
fetch("http://localhost:8000/blogs")
.then((res) => res.json())
.then(({ rows } = data) => {
rows.forEach(({ title, avatar, intro } = rows) => {
result += `
<div class="card">
<img class="card-avatar" src="/${avatar}"/>
<h1 class="card-title">${title}</h1>
<p class="intro">${intro}</p>
<a class="card-link" href="#">Read</a>
</div>
`;
});
document.querySelector(".container").innerHTML = result;
})
.catch((e) => {
console.log(e);
});
Aggiungeremo anche uno stile alla nostra applicazione in public/css/style.css con lo snippet di codice qui sotto:
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #fdfdfd;
font-size: 1rem;
}
section {
max-width: 900px;
margin: auto;
padding: 0.5rem;
text-align: center;
}
nav {
display: flex;
justify-content: space-between;
align-items: center;
}
ul {
list-style: none;
display: flex;
}
li {
margin-right: 1rem;
}
h1 {
color: #0e9c95;
margin-bottom: 0.5rem;
}
.container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
grid-gap: 1rem;
justify-content: center;
align-items: center;
margin: auto;
padding: 1rem 0;
}
.card {
display: flex;
align-items: center;
flex-direction: column;
width: 15rem auto;
background: #fff;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23);
border-radius: 10px;
margin: auto;
overflow: hidden;
}
.card-avatar {
width: 100%;
height: 10rem;
object-fit: cover;
}
.card-title {
color: #222;
font-weight: 700;
text-transform: capitalize;
font-size: 1.1rem;
margin-top: 0.5rem;
}
.card-link {
text-decoration: none;
background: #16a0d6e7;
color: #fff;
padding: 0.3rem 1rem;
border-radius: 20px;
margin: 10px;
}
.intro {
color: #c2c5c5;
padding: 10px;
}
.active {
color: #16a0d6e7;
}
Ora apri il package.json file e aggiungi lo script di avvio.
"start": "node index.js"
A questo punto, abbiamo impostato la nostra applicazione. Ma non possiamo eseguire la nostra applicazione quando il server non è in esecuzione o quando non c'è connessione di rete per la produzione. Impostiamolo nella prossima sezione.
Ottimizzazione dell'applicazione
Dobbiamo rendere la nostra applicazione compatibile con tutte le dimensioni dello schermo. Aggiungeremo anche un colore del tema aggiungendo il markup di seguito nella sezione head del nostro index.html file.
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#16a0d6e7"/>
Crea un manifesto
Dobbiamo descrivere la nostra app e come dovrebbe comportarsi una volta installata sul dispositivo dell'utente. Possiamo farlo creando un manifest.
Crea un manifesto file nella directory principale del progetto e aggiungere le seguenti configurazioni:
{
"name": "Blogger"
"short_name": "Blogger"
"start_url": "/",
"display": "standalone",
"background_color": "#0e9c95",
"theme_color": "#16a0d6e7",
"orientation": "portrait",
"icons": []
}
Nel nostro manifest, abbiamo definito le seguenti configurazioni:
- nome :definisce il nome visualizzato dell'app.
- nome_abbreviato :Definisce il nome che verrà visualizzato sotto l'icona dell'app una volta installata.
- URL_inizio :indica al browser l'URL principale dell'applicazione.
- visualizza :indica al browser come visualizzare l'app.
- colore_sfondo: Questo definisce il colore di sfondo dell'applicazione una volta installata.
- colore_tema: Questo definisce il colore della barra di stato.
- orientamento: Definisce l'orientamento da utilizzare durante la visualizzazione dell'app.
- icone: Questo definisce le icone o le immagini di dimensioni diverse da utilizzare come icone della nostra app home.
La creazione manuale delle icone della schermata iniziale può essere un compito molto complicato, ma non preoccuparti. Sfrutteremo un modulo di terze parti noto come pwa-asset-generator per generare icone di dimensioni diverse dall'icona della nostra app principale all'interno della directory pubblica con il comando seguente:
#change directory to the public folder
cd public
#generate icons
npx pwa-asset-generator logo.png icons
Il comando precedente creerà un icone cartella all'interno della cartella pubblica con molte icone per la nostra applicazione, insieme ad alcuni JSON sul terminale che incolleremo nella nostra matrice di icone nel manifest.
L'array di icone nel nostro manifest dovrebbe assomigliare a questo:
"icons": [
{
"src": "public/icons/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any"
},
{
"src": "public/icons/manifest-icon-192.maskable.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "public/icons/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
},
{
"src": "public/icons/manifest-icon-512.maskable.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
Inoltre, il comando ha generato i collegamenti di markup alle icone generate.
Copia e incolla il markup nella sezione head del markup in public/index.html file.
Configurazione dei lavoratori del servizio
Con il nostro manifest creato, impostiamo i lavoratori del servizio. Un service worker è un pezzo di codice JavaScript che il tuo browser esegue in background in un thread separato per gestire la cache per le risorse e i dati che salvi per future richieste per abilitare il supporto offline per la tua applicazione.
Quindi crea un blogger.serviceWorker.js file nel pubblico cartella. Per l'operatore del servizio, ci sono molti eventi (push, attivazione, installazione, recupero, messaggio, sincronizzazione), ma per la dimostrazione in questo tutorial tratteremo l'installazione, attivazione, e recupera eventi. Prima di ciò, dobbiamo creare un array per archiviare tutte le risorse che abbiamo utilizzato nella nostra applicazione.
const assets = [
"/",
"css/style.css",
"js/app.js",
"/images/blog1.jpg",
"/images/blog2.jpg",
"/images/blog3.jpg,"
];
Quindi, ascolteremo l'installazione evento per registrare e salvare i nostri file statici nella cache del browser. Questo processo richiede del tempo per essere completato. Per saltare l'attesa, utilizzeremo skipWaiting().
const BLOGGER_ASSETS = "blogger-assets";
self.addEventListener("install", (installEvt) => {
installEvt.waitUntil(
caches
.open(BLOGGER_ASSETS)
.then((cache) => {
cache.addAll(assets);
})
.then(self.skipWaiting())
.catch((e) => {
console.log(e);
})
);
});
...
Quindi, è necessario svuotare la cache per rimuovere le vecchie risorse ogni volta che il lavoratore del servizio viene aggiornato. Per questo, ascolteremo l'attivazione frammento di codice qui sotto:
...
self.addEventListener("activate", function (evt) {
evt.waitUntil(
caches
.keys()
.then((keysList) => {
return Promise.all(
keysList.map((key) => {
if (key === BLOGGER_ASSETS) {
console.log(`Removed old cache from ${key}`);
return caches.delete(key);
}
})
);
})
.then(() => self.clients.claim())
);
});
Nello snippet di codice sopra, utilizziamo waitUntil metodo sul lavoratore del servizio. Questo metodo attende il completamento dell'azione, quindi controlliamo se le risorse che stiamo cercando di cancellare sono le risorse della nostra app corrente prima di eliminarle.
Successivamente, abbiamo bisogno dei file archiviati nella nostra cache per usarli.
self.addEventListener("fetch", function (evt) {
evt.respondWith(
fetch(evt.request).catch(() => {
return caches.open(BLOGGER_ASSETS).then((cache) => {
return cache.match(evt.request);
});
})
);
})
Quando viene effettuata una richiesta sulla pagina, PWA controlla la nostra cache e legge da essa se sono presenti dati nella cache piuttosto che andare in rete. Quindi, utilizzando respondWith , sovrascriviamo l'impostazione predefinita del browser e facciamo in modo che il nostro evento restituisca una promessa. Quando la cache è completa, possiamo restituire la cache corrispondente a evt.request. Quando la cache è pronta, possiamo restituire la cache che corrisponde a evt.request.
Abbiamo impostato con successo il nostro operatore di servizio. Ora rendiamolo disponibile nella nostra applicazione.
Registra il Service Worker
Ora registriamo il nostro addetto ai servizi nel nostro public/js/app.js file con lo snippet di codice qui sotto:
...
if ("serviceWorker" in navigator) {
window.addEventListener("load", function () {
navigator.serviceWorker
.register("/blogger.serviceWorker.js")
.then((res) => console.log("service worker registered"))
.catch((err) => console.log("service worker not registered", err));
});
}
Qui controlliamo se il browser della nostra applicazione supporta i service worker (ovviamente, non tutti i browser supportano i service worker), quindi registriamo il nostro file di service worker.
Ora esegui l'applicazione con il comando seguente:
npm start
Vai a localhost:8000 nel tuo browser per accedere all'app.
Controllo del faro di Google
Ora controlliamo se abbiamo impostato correttamente la nostra PWA utilizzando un controllo di Google Lighthouse. Fai clic con il pulsante destro del mouse sul browser e seleziona "ispeziona". Nelle schede di ispezione, seleziona il faro e fai clic su genera rapporto. Se tutto è andato bene con la tua applicazione, dovresti vedere un output come quello nello screenshot qui sotto:
Abbiamo creato con successo la nostra prima applicazione. Puoi anche arrestare il server per testare l'applicazione in modalità offline.
Conclusione
Le Progressive Web Apps (PWA) utilizzano le moderne API per fornire funzionalità avanzate, affidabilità e installabilità con un'unica base di codice. Consentono al tuo utente finale di utilizzare la tua applicazione indipendentemente dal fatto che disponga o meno di una connessione Internet. Dovresti sentirti libero di eseguire il fork del repository e aggiungere funzionalità aggiuntive al progetto. Buona fortuna!