Vorig jaar vroeg een klant of zijn webapplicatie ook offline kon werken. Niet als een native app in de app store, maar installeerbaar via de browser, met een icoontje op het homescreen en basisgebruik zonder internetverbinding. Progressive Web App dus. Het project draaide al op Laravel met een Vue.js frontend, wat een prima uitgangspunt was. Wat ik niet voorzag, is dat de echte uitdagingen niet in de technologiekeuze zaten, maar in alle randgevallen die je pas tegenkomt als je er middenin zit.
Progressive Web Apps combineren het beste van web en native apps, maar de implementatie vereist veel meer dan alleen een manifest file en een service worker toevoegen. Mijn ervaring met dit project leerde me dat elk detail belangrijk is, van caching strategieën tot push notificaties en deployment overwegingen. Werkende offline functionaliteit betekent dat je elke mogelijke scenario moet doordenken waarin een gebruiker de app wil gebruiken zonder stabiele internetverbinding.
De service worker als kern
Het fundament van een PWA is de service worker: een JavaScript-bestand dat de browser als aparte thread draait, los van de webpagina. Het kan netwerkrequests onderscheppen, bepalen wat er gecacht wordt en offline-functionaliteit bieden als er geen verbinding is. Voor een nieuw project schrijf je dat handmatig; voor een bestaande app koos ik voor Workbox, een library van Google die een service worker genereert op basis van je buildproces. Handmatig een service worker schrijven geeft je meer controle, maar Workbox bespaart weken aan development tijd en zorgt voor veel minder bugs.
npm install workbox-cli
workbox generateSW
Het resultaat ziet er zo uit:
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst } from 'workbox-strategies';
precacheAndRoute(self.__WB_MANIFEST);
registerRoute(
({ url }) => url.pathname === '/',
new CacheFirst({
cacheName: 'root',
}),
);
registerRoute(
({ url }) => url.pathname === '/about',
new CacheFirst({
cacheName: 'about',
}),
);
Caching strategieën zijn cruciaal voor een goede gebruikerservaring. CacheFirst levert altijd de gecachte versie op, tenzij er niets in de cache zit. Voor statische assets is dat ideaal, maar voor pagina's met dynamische content wil je NetworkFirst, anders zien gebruikers verouderde data. Mijn eerste implementatie had te veel routes op CacheFirst staan, waarna gebruikers nieuws zagen van twee dagen oud. Het duurde een week voordat ik die bug ontdekte, omdat ik tijdens development altijd de cache leegmaakte.
Service workers draaien los van de hoofdthread, wat betekent dat ze kunnen blijven functioneren zelfs als de gebruiker de tab sluit. Dit maakt ze perfect voor achtergrondtaken zoals het syncen van data wanneer er weer internetverbinding is, maar het betekent ook dat debugging complexer wordt. Chrome DevTools heeft goede service worker debugging tools, maar het blijft moeilijker dan gewone JavaScript debuggen omdat de service worker in een aparte context draait.
Frontend architectuur met Vue.js
Vue Router was al aanwezig in het project, wat bij PWA's een voordeel is: client-side routing en service workers werken goed samen, zolang je de navigatie-fallback correct configureert. Single Page Applications zijn bijna een vereiste voor PWA's omdat je anders bij elke pageload een volledige server roundtrip nodig hebt. De basisopzet van main.js bleef grotendeels ongewijzigd, maar ik moest wel extra logica toevoegen voor het registreren van de service worker:
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import './plugins/tailwindcss'
Vue.config.productionTip = false
new Vue({
router,
render: h => h(App)
}).$mount('#app')
Router configuratie vereist extra aandacht bij PWA development. History mode is essentieel voor een app-achtige ervaring, maar het betekent dat je server elke route moet kunnen afhandelen:
import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './views/Home.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'home',
component: Home
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
Gebruik altijd mode: 'history' en zorg dat de server elke route terugverwijst naar index.html. Zonder die server-side fallback krijg je 404's bij een directe link of refresh op een subpagina. Dit is precies het soort bug dat je pas ontdekt nadat je de app hebt gedeeld met iemand anders die een deep link probeert te openen. Laravel maakt dit makkelijk met een catch-all route die alle onbekende URLs naar de Vue app stuurt.
Vuex werd cruciaal voor het beheren van offline state. Wanneer de app offline gaat, moet je data lokaal opslaan en synchroniseren zodra er weer internetverbinding is. IndexedDB via een Vuex plugin biedt persistente storage die veel groter kan zijn dan localStorage. Het opzetten van een goede offline-first architectuur betekent dat je elke CRUD operatie moet kunnen buffereren en later synchroniseren.
Backend integratie en CSRF
De Laravel backend fungeerde als JSON API, wat perfect werkt voor PWA architectuur. Laravel's API routes geven je een schone scheiding tussen je frontend en backend logica. De routedefinitie en controller waren standaard, maar de API responses moesten wel worden aangepast voor offline scenarios:
Route::get('/', function () {
return view('welcome');
});
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class HomeController extends Controller
{
public function index()
{
return view('home');
}
}
CSRF-bescherming werd een uitdaging. Laravel's standaard CSRF protection werkt via sessiecookies, maar in een PWA die offline kan gaan, moet je een robuustere aanpak hebben. Laravel Sanctum is hiervoor mijn standaardkeuze: het beheert CSRF-tokens via een cookie-flow zodat de Vue.js frontend veilig POST-requests kan sturen zonder de tokens handmatig te hoeven meegeven in elke request. Sanctum werkt ook goed met SPA's omdat het stateless is en geen server-side sessies vereist.
API versioning werd belangrijker dan verwacht omdat PWA's kunnen blijven draaien met oude cached versies. Gebruikers updaten niet automatisch naar de nieuwste versie van je app zoals bij native apps. Een expliciete versioning strategie in je API endpoints voorkomt compatibiliteitsproblemen tussen oude cached frontend code en nieuwe backend functionaliteit.
Push notificaties
Push notificaties zijn een van de krachtigste features van PWA's, maar ook een van de meest complexe om goed te implementeren. De klant wilde dat gebruikers berichten konden ontvangen ook als de app op de achtergrond stond. Daarvoor gebruikte ik de web-push library:
npm install web-push
VAPID sleutels zijn vereist voor server identificatie bij push services. Elke browser (Chrome, Firefox, Safari) heeft zijn eigen push service, maar het web-push protocol is gestandaardiseerd. Het genereren van VAPID sleutels doe je één keer per applicatie, en de public key moet je delen met de browser voor subscription. De private key gebruik je server-side om berichten te versturen naar de push service van de browser.
Gebruikerstoestemming is een kritiek punt bij push notificaties. Browsers tonen een prominente prompt die gebruikers vaak weigeren als ze niet begrijpen waarom ze notificaties zouden willen ontvangen. Timing is alles: vraag pas om toestemming nadat de gebruiker waarde heeft ervaren van je app en een duidelijke reden heeft om notificaties te willen. Een goede strategie is om eerst een in-app prompt te tonen die uitlegt welke notificaties je wilt sturen, en dan pas de browser permission te vragen.
Service worker event handling voor push notificaties vereist zorgvuldige implementatie. De service worker moet push events oppakken ook wanneer de app niet open is, de notification tonen met de juiste titel en body text, en click events afhandelen om de gebruiker naar de juiste pagina te navigeren. Debugging van push notificaties is lastig omdat je het volledige traject moet testen: server sending, push service delivery, service worker handling en notification display.
Deploy en containerisatie
Deployment van PWA's heeft unieke uitdagingen vergeleken met traditionele web apps. Service worker caching kan updates tegenhouden als je niet oppast. Browsers cachen de service worker zelf ook, waarna updates niet altijd doorkwamen. Workbox lost dit op via content hashing in de bestandsnaam, zodat elke nieuwe build als een nieuw bestand herkend wordt. Force refresh helpt niet altijd omdat de service worker los van de pagina draait.
Cache invalidation werd mijn grootste hoofdpijn. Wanneer je een nieuwe versie deployed, moeten alle clients hun cache updaten, maar dat gebeurt niet automatisch. Workbox heeft een skipWaiting optie die nieuwe service workers meteen activeert, maar dat kan leiden tot inconsistenties als de gebruiker de app op dat moment gebruikt. Een betere aanpak is om gebruikers te laten weten dat er een update beschikbaar is en ze de keuze te geven wanneer ze willen updaten.
FROM php:7.4-fpm
RUN apt-get update && apt-get install -y libfreetype6-dev libjpeg62-turbo-dev libmcrypt-dev libpng-dev
RUN docker-php-ext-install -j$(nproc) pdo_mysql
COPY . /var/www/html
WORKDIR /var/www/html
RUN composer install
EXPOSE 80
CMD ["php", "-S", "0.0.0.0:80", "-t", "/var/www/html/public"]
Docker containers voor PWA's moeten geconfigureerd worden voor zowel de Laravel backend als de gecompileerde Vue.js assets. Het bouwen van de frontend assets kan het best gebeuren in een multi-stage Docker build om de finale container klein te houden. Node.js is alleen nodig tijdens de build phase, niet in productie. HTTPS is verplicht voor service workers en push notificaties, dus zorg dat je SSL certificaten correct geconfigureerd hebt in je deployment pipeline.
Monitoring van PWA's vereist andere metrics dan traditionele websites. Je wilt weten hoeveel gebruikers je app hebben geïnstalleerd, hoe vaak ze offline gaan, welke features ze offline gebruiken en hoe vaak push notificaties worden geopend. Google Analytics heeft speciale PWA events, maar ik gebruik meestal een combinatie van server-side logging en client-side analytics om een compleet beeld te krijgen van hoe gebruikers de app daadwerkelijk gebruiken.
Een jaar later draait de app nog steeds soepel en hebben we meer dan duizend active installs. De offline functionaliteit wordt intensief gebruikt, vooral door gebruikers met slechte internetverbindingen. PWA's zijn niet geschikt voor elke use case, maar voor content-heavy apps met gebruikers die regelmatig offline gaan, is het een krachtige oplossing die veel dichter bij native apps komt dan ik oorspronkelijk had verwacht.