24: PWA & native apps

Een Phlo-app is al een website. Dezelfde build wordt geleverd als een installeerbare Progressive Web App zonder extra tooling, en dezelfde weboutput kan worden verpakt in een dunne native shell voor de app-winkels. Er is geen aparte "mobiele build": één bron, twee extra distributievormen.

24.1: Het manifest

Een PWA heeft een manifest nodig dat de app beschrijft: naam, iconen, hoe het wordt gestart. Phlo levert een manifest resource die de route en de bediening beheert; jouw app declareert alleen de body. Lijst manifest in je resources (data/app.json), en injecteer vervolgens de body vanuit app.phlo:

prop %manifest.body => arr(
	id: '/',
	name: %app->title,
	short_name: 'App',
	description: %app->description,
	start_url: '/',
	scope: '/',
	display: 'standalone',
	orientation: 'any',
	theme_color: %app->themeColor,
	background_color: '#0e131b',
	icons: [
		obj(src: '/icons/app-192.png', sizes: '192x192', type: 'image/png', purpose: 'any'),
		obj(src: '/icons/app-512.png', sizes: '512x512', type: 'image/png', purpose: 'any'),
		obj(src: '/icons/app-192.png', sizes: '192x192', type: 'image/png', purpose: 'maskable'),
		obj(src: '/icons/app-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable'),
	],
)

De resource levert GET /manifest.json als application/manifest+json met een korte cache, en %manifest->head rendert de <link rel="manifest"> tag voor je head view. Omdat de body een luie prop is, leest het de live app status (een per-taal name, de actieve themakleur) op het moment van de aanvraag.

Een app met meer dan één manifest (een pro-variant, een per-sectie PWA) houdt zijn eigen routes en levert elke body via dezelfde helper; de standaard route van de resource treedt op de achtergrond wanneer er geen %manifest.body is gedeclareerd:

route GET pro manifest.json => manifest::output(manifests::body(pro: true))

Twee bodyvelden bepalen de installeerbaarheid: display: 'standalone' en een paar maskable iconen zijn wat Chrome en Safari überhaupt een installprompt laten aanbieden. Zonder beide is het manifest geldig, maar wordt de app nooit installeerbaar. En stel id expliciet in ('/'): het verankert de geïnstalleerde identiteit aan de oorsprong, zodat een latere wijziging van start_url de geïnstalleerde app niet in een andere verandert.

24.2: De service worker

Installeerbaar betekent ook offline-capabel. Dien de worker zelf via een route aan zodat de naam van de cache de app-versie kan bevatten, waardoor oude caches bij elke uitrol ongeldig worden:

route GET sw.js {
	%res->header('Cache-Control', 'no-cache')
	output(strtr((string)file_get_contents(data.'sw.js.tpl'), ['{{VERSION}}' => %app->version]), type: 'text/javascript')
}

output() is de algemene response helper: het stelt het contenttype en Content-Length in, codeert een array automatisch naar JSON en behandelt Content-Disposition voor downloads. Gebruik %res-> alleen voor wat het niet dekt, zoals de extra Cache-Control header hier.

De template cachet de app shell bij installatie, verwijdert verouderde caches bij activatie, en serveert bij ophalen navigaties netwerk-eerst met een offline fallback naar de gecachete shell:

const CACHE = 'app-{{VERSION}}'
const SHELL = ['/', '/app.js?{{VERSION}}', '/app.css?{{VERSION}}', '/manifest.json']

self.addEventListener('install', e => {
	e.waitUntil(caches.open(CACHE).then(c => c.addAll(SHELL)).then(() => self.skipWaiting()))
})
self.addEventListener('activate', e => {
	e.waitUntil(caches.keys()
		.then(keys => Promise.all(keys.filter(k => k !== CACHE).map(k => caches.delete(k))))
		.then(() => self.clients.claim()))
})
self.addEventListener('fetch', e => {
	if (e.request.method !== 'GET') return
	if (e.request.mode === 'navigate') return e.respondWith(fetch(e.request).catch(() => caches.match('/')))
})

Les. Een algemene fetch handler breekt de SPA-engine. Phlo's .async navigatie leest en schrijft de DOM rechtstreeks vanuit een apply() antwoord; het verwacht nooit een volledige HTML-pagina terug. Cache.match negeert headers, dus als de worker de gecachte / shell voor iedere aanvraag naar / serveert, krijgt een async aanvraag een volledige HTML-document terug waar het een DOM-patch verwachtte, en de pagina corrumpeert zichzelf. Sluit async verkeer expliciet uit, door de header te controleren die de engine bij elke async oproep verzendt: ```js if (e.request.headers.get('X-Requested-With') === 'phlo') return


Vallen door (geen `respondWith`) laat de browser de aanvraag normaal afhandelen, ongecached. Hetzelfde
geldt voor elk eindpunt dat altijd live het netwerk moet bereiken: een WebSocket-upgrade, een byte-range media
aanvraag, alles wat de cache anders zou onderscheppen en stilletjes zou corrumperen.

Registreer het één keer, vroeg:

if ('serviceWorker' in navigator) navigator.serviceWorker.register('/sw.js')

24.3: Wat Capacitor toevoegt

Een PWA dekt installeren-naar-startscherm, offline shell, en push als je het toevoegt, allemaal vanuit dezelfde deployment. Capacitor dekt wat een browser tabblad niet kan: een app-store vermelding, en native APIs die een achtergrond browser tabblad niet betrouwbaar kan bereiken, zoals achtergrond geolocatie, een wake lock die overleeft dat het besturingssysteem een achtergrondtabblad agressief opschort, of native tekst-naar-spraak.

Capacitor bundelt geen kopie van je app in de binaire. Het wijst een dunne native shell naar je draaiende site:

{
	"appId": "com.example.app",
	"appName": "App",
	"webDir": "www",
	"server": { "url": "https://app.example.com", "cleartext": false }
}

De binaire is een WebView die je live, gedeployde Phlo-app laadt: geen aparte mobiele build-pijplijn om in sync te houden, geen gebundelde assets die verouderen. Stuur een wijziging op de normale manier (zie het hoofdstuk Deployment) en elke geïnstalleerde kopie pakt het op bij de volgende lancering, precies zoals een website.

Voeg platforms en native plugins toe indien nodig:

npm install @capacitor/core @capacitor/android @capacitor/geolocation
npx cap add android
npx cap sync
npx cap open android

Roep een native plugin aan vanuit je Phlo frontend-script precies zoals elke andere browser-API: het is een extra mogelijkheid die de pagina kan bereiken, onder bepaalde voorwaarden, wanneer deze draait binnen de native shell.

import { Geolocation } from '@capacitor/geolocation'
const pos = await Geolocation.getCurrentPosition()

npx cap open android opent het gegenereerde project in Android Studio voor een store-signed build. Hetzelfde cap add ios pad geldt voor iOS, mits je een Mac en Xcode hebt; de configuratie en het pluginverhaal zijn identiek, alleen het native project verandert.

24.4: Één app, drie vormen

Een Phlo-app is in de eerste plaats een website. Het wordt installeerbaar met een manifest en een service worker, zonder wijzigingen in de build-stap. Het wordt een winkelvermelding met Capacitor, zonder een fork van de app, geen tweede codebase om synchroon te houden, gewoon een dunne schil die naar dezelfde deployment wijst en een kleine set native plugins die zijn verbonden waar de browser echt niet kan volgen. De browsertab, de geïnstalleerde PWA en de native binary zijn dezelfde app op drie punten van één distributiespectrum, niet drie dingen om te onderhouden.

We gebruiken essentiële cookies om deze site te laten werken. Met uw toestemming gebruiken we ook analytics om de site te verbeteren.