"Offline first" er et applikationsudviklingsparadigme, hvor udviklere sikrer, at en apps funktionalitet er upåvirket af et midlertidigt tab af netværksforbindelse. Progressive webapplikationer, der føles som native applikationer, men kører som webapplikationer, er ofte bygget på dette paradigme.
Denne tutorial vil lære dig, hvordan du bygger en offline-first-applikation med Node.js og en SQLite-database. Lad os starte med en introduktion til progressive webapps.
Introduktion til PWA
Progressive Web Apps (PWA'er) er webapps, der bruger servicearbejdere, manifester og andre webplatformsfunktioner og progressive forbedringer for at give brugerne en oplevelse, der kan sammenlignes med native apps.
PWA'er kan nogle gange overgå native apps med hensyn til effektivitet. De fungerer efter behov og er altid tilgængelige uden at forbruge værdifuld smartphonehukommelse eller data. Brugere bruger mindre data, når de vælger en PWA frem for en native version af den samme applikation. De kan stadig gemme PWA'en på deres startskærm; det kan installeres uden behov for en fuld download.
Hvad bygger vi?
For at demonstrere styrken ved progressive webapplikationer bygger vi en simpel blogapplikation.
Brugeren vil være i stand til at interagere med den ligesom andre PWA'er, såsom Twitter PWA. Lad os komme lige til det.
Initialiser NodeJs applikation
Lad os få hænderne snavsede. For at komme i gang opretter vi vores projektmappe med kommandoen nedenfor:
mkdir PWA && cd PWA
Derefter initialiserer vi en Node.js-applikation med kommandoerne nedenfor:
npm init -y
Ovenstående kommando opretter en package.json
fil til ansøgningen.
Opret derefter følgende mappestruktur i vores projektmappe:
Konfigurer en Express-server
Med vores applikationsopsætning, lad os installere Express for at skabe vores Node.js-server med kommandoen nedenfor:
npm install express
Derefter opretter vi et par mapper og filer i den offentlige mappe:
- css/style.css-fil
- js/app.js-fil
Opret derefter en index.js
fil i projektets rodmappe med følgende kodestykker nedenfor:
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"));
I kodestykket importerer vi express at oprette vores server og stien modul. Vi konfigurerede vores app til at gengive vores statiske filer ved hjælp af express.static metode, som tager stien til den statiske mappe (offentlig), har vi oprettet rodruten for vores applikation og gengivet index.html fil. Derefter konfigurerede vi appen til at lytte til port 8000 .
Opret forbindelse til SQLite-database
Med serveropsætningen til vores applikation, lad os oprette og forbinde vores applikation for at gemme vores blogoplysninger. For at komme i gang skal du køre kommandoen nedenfor for at installere sqlite3-afhængigheden.
npm install sqlite3
Derefter i indgangspunktet index.js
fil, skal du tilføje kodestykket nedenfor for at oprette og forbinde applikationen til en SQLite-database.
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.");
}
});
Dernæst opretter vi en liste over blogs, som vi gemmer i vores database og senere gengiver til klientsiden med nedenstående kodestykke:
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.",
},
];
Hvert blokindlæg i vores applikation vil have et id , titel , avatar og intro felter.
Opret nu et databasetabelnavn blogs og gem de blogoplysninger, vi lige har oprettet ovenfor, med kodestykket nedenfor:
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}`,
]);
});
}
}
);
I kodestykket oprettede vi en tabel blogs ved hjælp af db.run. db.run metoden tager en SQL-forespørgsel som en parameter, så går vi gennem vores række af blogs og indsætter dem i den blog-tabel, vi lige har oprettet ved hjælp af js-kortfunktionen.
Se databaseregistreringer
Lad os nu se de poster, vi lige har oprettet ved hjælp af Arctype. For at se posterne i din SQLite-database ved hjælp af Arctype, følg nedenstående trin:
- Installer Arctype
- Kør applikationen med
node index.js
at oprette en database - Start Arctype, og klik på SQLite-fanen
- Klik på Vælg SQLite-fil knappen, og find db.sqlite fil genereret, da du kørte serveren.
- Du bør se blogtabellen og de poster, vi opretter, som vist på skærmbilledet nedenfor:
Gengiv siden
På dette tidspunkt har vi forbundet applikationen til en SQLite-database og også indsat nogle poster i databasen. Åbn nu index.html fil og tilføj følgende kodestykker nedenfor:
<!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>
Vi oprettede en simpel opmærkning med links til vores manifest i ovenstående fil, som vi vil oprette i næste afsnit, stile og app.js filer.
Derefter opretter vi en blogs rute i vores index.js fil for at returnere blogs til klientsiden.
...
app.get("/blogs", (req, res) => {
res.status(200).json({
blogs,
});
});
...
I vores public/js/app.js fil, sender vi en get-anmodning til blogslutpunktet for at hente bloggene fra vores backend. Så går vi gennem bloggene, målretter mod containeren klasse og vise dem.
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);
});
Vi tilføjer også lidt styling til vores applikation i public/css/style.css med kodestykket nedenfor:
* {
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;
}
Åbn nu package.json fil og tilføj startscriptet.
"start": "node index.js"
På dette tidspunkt har vi konfigureret vores applikation. Men vi kan ikke køre vores applikation, når serveren ikke kører, eller når der ikke er nogen netværksforbindelse til produktion. Lad os sætte det op i næste afsnit.
Optimering af applikation
Vi skal gøre vores applikation kompatibel med alle skærmstørrelser. Vi tilføjer også en temafarve ved at tilføje markeringen nedenfor i hovedafsnittet i vores index.html fil.
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#16a0d6e7"/>
Opret et manifest
Vi skal beskrive vores app, og hvordan den skal opføre sig, når den er installeret på brugerens enhed. Det kan vi gøre ved at skabe et manifest.
Opret et manifest fil i projektets rodbibliotek og tilføj følgende konfigurationer:
{
"name": "Blogger"
"short_name": "Blogger"
"start_url": "/",
"display": "standalone",
"background_color": "#0e9c95",
"theme_color": "#16a0d6e7",
"orientation": "portrait",
"icons": []
}
I vores manifest definerede vi følgende konfigurationer:
- navn :Dette definerer appens visningsnavn.
- short_name :Dette definerer det navn, der vil blive vist under app-ikonet, når det er installeret.
- start_url :Dette fortæller browseren programmets rod-URL.
- skærm :Dette fortæller browseren, hvordan appen skal vises.
- baggrundsfarve: Dette definerer baggrundsfarven for programmet, når det er installeret.
- tema_farve: Dette definerer farven på statuslinjen.
- retning: Dette definerer den retning, der skal bruges under appvisningen.
- ikoner: Dette definerer de ikoner eller billeder i forskellige størrelser, der skal bruges som vores app-hjemmeikoner.
At oprette vores startskærmsikoner manuelt kan være en meget kompliceret opgave, men ikke at bekymre dig. Vi vil drage fordel af et tredjepartsmodul kendt som pwa-asset-generator til at generere ikoner i forskellige størrelser fra vores hovedappikon inde i den offentlige mappe med kommandoen nedenfor:
#change directory to the public folder
cd public
#generate icons
npx pwa-asset-generator logo.png icons
Ovenstående kommando vil skabe et ikon mappe inde i den offentlige mappe med mange ikoner til vores applikation sammen med nogle JSON på terminalen, som vi vil indsætte i vores ikon-array i manifestet.
Ikonarrayet i vores manifest skulle se sådan ud:
"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"
}
]
Kommandoen genererede også markup-links til de genererede ikoner.
Kopiér og indsæt markeringen i hovedsektionen af markeringen i public/index.html fil.
Konfigurationsservicemedarbejdere
Med vores manifest oprettet, lad os konfigurere servicemedarbejderne. En service worker er et stykke JavaScript-kode, som din browser kører i baggrunden i en separat tråd for at håndtere cachen for aktiver og data, som du gemmer til fremtidige anmodninger for at aktivere offline-understøttelse af din applikation.
Så opret en blogger.serviceWorker.js fil i offentligheden folder. For servicearbejderen er der mange hændelser (push, aktiver, installer, hent, besked, synkronisering), men til demonstrationen i denne vejledning dækker vi installer, aktiver, og hent begivenheder. Før det skal vi oprette et array til at gemme alle de aktiver, vi brugte i vores applikation.
const assets = [
"/",
"css/style.css",
"js/app.js",
"/images/blog1.jpg",
"/images/blog2.jpg",
"/images/blog3.jpg,"
];
Derefter lytter vi til installationen begivenhed for at registrere og gemme vores statiske filer i browserens cache. Denne proces tager noget tid at fuldføre. For at springe ventetiden over, bruger vi 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);
})
);
});
...
Derefter skal vi rydde cachen for at fjerne de gamle aktiver, hver gang servicearbejderen opdateres. Til det lytter vi til aktiver kodestykke nedenfor:
...
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())
);
});
I ovenstående kodestykke bruger vi waitUntil metode på servicemedarbejderen. Denne metode venter på, at handlingen er færdig, og derefter tjekker vi, om de aktiver, vi forsøger at rydde, er aktiver i vores nuværende app, før vi sletter dem.
Dernæst har vi brug for filerne gemt i vores cache for at bruge dem.
self.addEventListener("fetch", function (evt) {
evt.respondWith(
fetch(evt.request).catch(() => {
return caches.open(BLOGGER_ASSETS).then((cache) => {
return cache.match(evt.request);
});
})
);
})
Når der kommer en anmodning på siden, vil PWA tjekke vores cache og læse fra den, hvis der er data i cachen i stedet for at gå til netværket. Brug derefter svarer med metode, tilsidesætter vi browserens standard og får vores begivenhed til at returnere et løfte. Når cachen er færdig, kan vi returnere cachen svarende til evt.request. Når cachen er klar, kan vi returnere den cache, der matcher evt.request.
Vi har med succes oprettet vores servicemedarbejder. Lad os nu gøre det tilgængeligt i vores applikation.
Registrer Service Worker
Lad os nu registrere vores servicemedarbejder i vores public/js/app.js fil med nedenstående kodestykke:
...
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));
});
}
Her tjekker vi, om vores applikations browser understøtter servicearbejdere (selvfølgelig understøtter ikke alle browsere servicearbejdere), og registrer derefter vores servicearbejderfil.
Kør nu applikationen med kommandoen nedenfor:
npm start
Gå til localhost:8000 i din browser for at få adgang til appen.
Google Lighthouse Check
Lad os nu tjekke, om vi konfigurerer vores PWA korrekt ved hjælp af et Google Lighthouse-tjek. Højreklik på din browser og vælg "inspicer". På inspektionsfanerne skal du vælge fyrtårn og klikke på generer rapport. Hvis alt gik godt med din applikation, skulle du se et output som det på skærmbilledet nedenfor:
Vi har med succes oprettet vores første ansøgning. Du kan også stoppe serveren for at teste applikationen i offlinetilstand.
Konklusion
Progressive Web Apps (PWA) bruger moderne API'er til at give forbedrede muligheder, pålidelighed og installerbarhed med en enkelt kodebase. De giver din slutbruger mulighed for at bruge din applikation, uanset om de har en internetforbindelse eller ej. Du skal være velkommen til at fordele depotet og tilføje yderligere funktioner til projektet. Held og lykke!