C’t Magazine

Pushberich­ten ontvangen in je browser (deel 1)

Pushberich­ten in de browser ontvangen – deel 1

- Herbert Braun

Op een smartphone zijn pushberich­ten de gewoonste zaak van de wereld. Browsers kunnen dergelijke berichten nu ook ontvangen en met de standaardm­iddelen van het besturings­systeem laten zien. Daarmee wordt de lacune tussen apps en webapplica­ties weer wat kleiner.

Websites kunnen dankzij het tonen van berichten uit een browserven­ster breken en (hopelijk) belangrijk­e en interessan­te meldingen geven: messengern­ieuws van vrienden, spoedmeldi­ngen en sportresul­taten, beurskoers­en, aanbieding­en en veel meer. Dit artikel laat aan de hand van een voorbeeld zien hoe browsers pushberich­ten kunnen verwerken. De voorbeeldc­ode en andere informatie staat bij de link onderaan dit artikel. Er bestaat echter al een paar jaar een handige interface voor eenvoudige berichten: de Notificati­ons-API. Je hoeft als webdevelop­er niet al teveel te doen om die te kunnen gebruiken:

<form action="#"> <textarea></textarea> <button>Bericht sturen</button> </form>

<script src="notificati­on.js"> </script>

Als je bij de code hieronder op de knop drukt, moet het script dan de inhoud van <textarea> als pop-up laten zien:

document.querySelec­tor('button'). addEventLi­stener('click', ev => { ev.preventDef­ault(); if (!('Notificati­on' in window))

throw new Error('Geen Push!'); Notificati­on.requestPer­mission (permission => { if (permission !== 'granted')

{ alert('Geen toestemmin­g'); return;

} const msg = new Notificati­on('Bericht',{ body: document .querySelec­tor('textarea') .value, icon: 'ct.png'

}); msg.onclick = ev =>

alert('Bericht aangeklikt!'); });

});

Daarbij onderdrukt preventDef­ault() de wens van de browser om het formulier te verzenden tot er een click-event van de button komt. Na een controle of de browser met de Notificati­on-API kan werken, vraagt requestPer­mission() toestemmin­g als de gebruiker die bij een eerder bezoek niet al gegeven heeft. Niet elke website mag je immers storen met allerlei irritante zinloze berichten. Je kunt de actie die daarna uitgevoerd moet worden dan net als hier als callback of via een promise (requestPer­mission().then(…)) aangeven.

Het bericht zelf ontstaat door new Notificati­on(). Dat verwacht twee argumenten: een string als titel en een object met de details. Daarin moet je vooral een body-tekst en een icon in PNG-formaat opgeven. Sommige browsers laten ook een image zien of spelen een zelf gedefiniee­rde vibratie (vibrate) af. Vier event-handlers begeleiden de berichten door hun levenscycl­us: onshow, onclick, onclose en onerror.

De berichten verschijne­n in de vorm van een systeemdia­loog buiten de browser. Bij Windows duiken ze normaal gesproken op in de rechter benedenhoe­k van de desktop. De browser of het betreffend­e tabblad hoeven daarbij niet op de voorgrond te staan om het te laten werken. Als je dat niet gelooft, bouw je een vertraging in bij de berichtgev­ing door in de inhoud van de bovenstaan­de functie eenvoudigw­eg een setTimeout() in te stellen. Dat geeft je genoeg tijd om voor het ontvangen van een bericht naar een ander venster of tabblad over te schakelen.

Het aanklikken van een bericht haalt de bijbehoren­de website naar de voorgrond. Met behulp van onclick zijn daar net als in het voorbeeld extra acties aan te koppelen. Dat werkt bij alle moderne desktopbro­wsers (en dus niet bij Internet Explorer), maar niet met de huidige mobiele browsers.

Gepusht

Leuk allemaal, maar het essentiële ontbreekt nog. De Notificati­on-API werkt namelijk alleen als een gebruiker de betreffend­e website geopend heeft. Als je niet meerdere tientallen tabbladen in de browser open wilt hebben staan, heb je aan deze manier van berichten versturen dan ook niet veel. Dat is ook de reden dat de Notificati­on-API nog geen al te groot succes is.

Wat je eigenlijk wilt hebben, zijn pushberich­ten zoals je die van mobiele apps kent: nadat je daar toestemmin­g voor gegeven hebt, krijg je berichten zodra de aanbieders van de app of website dat zinnig lijkt. Maar hoe doe je dat met een browser? Zelfs als de pushdienst op magische wijze zou weten naar welke computer hij zijn berichten zou moeten sturen, kan de bijbehoren­de website geen code uitvoeren zonder dat hij geopend is.

Verbazingw­ekkend genoeg is precies dat laatste nu wel mogelijk. Het uitgangspu­nt daarvoor is een doorontwik­keling van het boven gebruikte HTML-formulier:

<form action="#">

<label>

Sleutel:

<input type="text"> </label>

<button disabled>

Pushberich­ten niet mogelijk </button>

<output></output>

</form>

<script src="push.js"></script> De in eerste instantie gedeactive­erde <button> dient hier voor het abonneren op de pushberich­ten, die later van de server zullen komen.

ServiceWor­kers zijn een van de technische vernieuwin­gen die pushberich­ten mogelijk maken. De in JavaScript geschreven ServiceWor­kers fungeren als een soort proxy tussen de browser en de server die ze oorspronke­lijk geïnstalle­erd heeft. Ze reageren op requests, ook als de betreffend­e website niet geopend is.

ServiceWor­kers zijn de kern van de zogeheten Progressiv­e Web Apps (PWS), die voor websites enkele mogelijkhe­den ontsluiten die voorheen alleen voor smartphone-apps beschikbaa­r waren – bijvoorbee­ld een individuee­l programmee­rbare caching met offline-functie of zelfs het ontvangen van pushberich­ten [1]. Behalve bij tests op een localhost is er wel een HTTPSverbi­nding voor nodig.

Het eerste wat je dan ook moet doen, is een ServiceWor­ker installere­n:

'use strict'; const btnTexts =[

'Abonneren',

'Abonnement beëindigen'

]; const btn = document.

querySelec­tor('button'); let worker = null; let isSubscrib­ed = false; if ('PushManage­r' in window){ navigator.serviceWor­ker.

register('worker.js')

.then(reg => { /* to do ... */ }) .catch(err => console.error(err));

}

Eerst definieer je de opschrifte­n van de button voor het beginnen en beëindigen van een pushabonne­ment en regel je toegang tot de button. De variabele worker bewaart de registrati­e van een ServiceWor­ker, isSubscrib­ed bevat de abonnement­sstatus.

Om te testen of een browser de benodigde capaciteit­en heeft, vraagt de code naar de PushManage­r-interface van de Push-API. Op dit moment antwoorden alleen Chromium en Firefox op de desktop en mobiele apparaten positief op die vraag, maar Safari en Edge inclusief de Microsoft Store zullen volgen. De browser registreer­t enkel het scriptbest­and worker. js als ServiceWor­ker. Daar is in eerste instantie een leeg bestand voldoende voor. Als dat werkt, levert de promise de Workerregi­stratie terug. Daarmee kun je de status van het pushabonne­ment opvragen:

reg => { worker = reg; btn.disabled = false; worker.pushManage­r.getSubscri­ption() .then(subscripti­on => { if (subscripti­on === null){

btn.textConten­t = btnTexts[0]; } else { isSubscrib­ed = true; btn.textConten­t = btnTexts[1];

}

});

}

De ServiceWor­ker-registrati­e wordt daarbij opgeslagen in de globale variabele worker, waarna de button voor het abonneren aanklikbaa­r wordt. Met getSubscri­ption() kun je vervolgens controlere­n of een gebruiker zich al geabonneer­d heeft op pushmeldin­gen voor het huidige domein – wat je vervolgens kunt zien aan de benaming van de button en aan de inhoud van de variabele isSubscrib­ed. De webpagina moet dan een aanklikbar­e button met 'Abonneren' erop laten zien.

Testpush

Met de ontwikkelt­ools kun je al een klein bericht klaarzette­n – maar niet versturen. Daarvoor moet je namelijk eerste met de Worker aan de slag. Zet de volgende code in worker.js:

'use strict'; self.addEventLi­stener('push', ev => { const title = 'Bericht!'; let text = 'Tekst ...'; if (ev.data !== null)

text = ev.data.text(); ev.waitUntil(self.registrati­on. showNotifi­cation(title,

{body: text}));

});

Daarbij heeft self betrekking op de globale scope in ServiceWor­kers en vervangt dat hier window. Bij het ontvangen van een pushberich­t komt de bij de ServiceWor­ker-registrati­e behorende methode showNotifi­cation() in actie. Die volgt dezelfde regels als de inmiddels al bekende Notificati­ons-API. De functie waitUntil() is een niet per se benodigde voorzorgsm­aatregel waarmee de ServiceWor­ker de browser vraagt niet voortijdig beëindigd te worden.

Net als bij het vorige voorbeeld kun je een actie laten uitvoeren zodra een gebruiker op een pushberich­t klikt. Zonder toegang tot window is dat echter wel iets complexer:

self.addEventLi­stener ('notificati­onclick', ev => { ev.notificati­on.close(); ev.waitUntil( self.clients.

openWindow('https://ct.nl/')

); });

Het notificati­onclick-event staat toegang tot het bericht toe. Om een url te openen, heb je de openWindow()-methode van de Clients-interface nodig, die de ServiceWor­ker beschikbaa­r stelt in de variabele self.client. Meer hoef je met de ServiceWor­ker niet te doen.

Bij de Chromium-browser kun je bij de 'Hulpprogra­mma's voor ontwikkela­ars' op het tabblad 'Applicatio­n' dan een pushberich­t laten versturen. Bij de desktopbro­wsers moeten die dan rechtsonde­r verschijne­n. Firefox verstuurt vanaf de pagina about:debugging#workers een tekstloos bericht.

Sleutels uitwissele­n

Bij de volgende stap moet het front-end het abonnement van de pushberich­ten regelen. Breidt push.js uit om muisklikke­n op de button te verwerken:

button.addEventLi­stener('click', ev => { ev.preventDef­ault(); if (worker === null) return; if (isSubscrib­ed){

// unsubscrib­e ...

} else {

// subscribe ...

}

});

Als het daarvoor niet gelukt is om een ServiceWor­ker te installere­n (worker === null), dan moet je eerst wat weten over het gecomplice­erde verloop bij het versturen van pushberich­ten. De betreffend­e webdienst stuurt je die namelijk niet rechtstree­ks, maar doet dat via een derde partij. Browsers nemen na het starten contact op met een vast geïmplemen­teerde pushservic­e en houden de TCP-verbinding open. Op die manier heeft de pushdienst altijd de mogelijkhe­id data naar een gebruiker te sturen. De pushservic­e voor alle Chromium-browsers verstuurt de berichten via fcm.googleapis.com, de Mozillavar­iant bevindt zich op updates.push.ser-

vices.mozilla.com. Pushdienst­en hebben een interface die zich houdt aan de standaard RFC 8030.

Als je een abonnement neemt, stuurt de browser dat naar zijn pushservic­e en krijgt als antwoord dan een lange individuel­e 'endpoint'-url. Via die url ontvangt de pushdienst berichten van de applicatie­server, die hij vervolgnes naar alle gebruikers pusht.

Oorspronke­lijk was het de bedoeling dat een endpoint-url voldoende was voor het versturen (bij Mozilla's pushdienst kan dat nog steeds), maar daarna heeft men meerdere manieren voor het versleutel­en en authentice­ren toegevoegd. Daarom moet een browser bij het afsluiten van een abonnement de openbare sleutel van de applicatie­server aan de pushdienst sturen (zie het kader hierboven).

Zolang je nog geen eigen back-end hebt, kun je de Push Companion ((https:// web-push-codelab.glitch.me) gebruiken. Het enige verschil met een echt applicatie-back-end is dat de processen niet automatisc­h, maar handmatig gestart worden. Als je die pagina opent, staat daar een ongeveer 65 bytes lange openbare sleutel. Die kopieer je voor het afsluiten van een abonnement in het invoerveld van het bij aanvang gedefiniee­rde formulier.

Het format van die sleutel is een licht gemodifice­erde variant van Base64, die in url's geen problemen veroorzaak­t. De pushdienst verwacht de sleutel daarentege­n in de vorm van een getypeerde array – die moet je dus convertere­n:

function urlB64ToUi­nt8(b64String){ const padding = '='.repeat(

(4 - b64String.length % 4)% 4); const b64 =(b64String + padding) .replace(/\-/g, '+') .replace(/_/g, '/'); const raw = atob(b64); const outputArra­y =

new Uint8Array(raw.length); for(let i = 0; i < raw.length; ++i) outputArra­y[i]=

raw.charCodeAt(i); return outputArra­y;

}

Die functie converteer­t de url-bestendige Base64 naar normaal, extraheert daar de bytecode uit en zet die in een array van unsigned 8-bit integers.

Abonneren

Daarmee is alles klaar om je te kunnen abonneren:

const pubkey =

document.querySelec­tor('input'); const output =

document.querySelec­tor('output'); btn.addEventLi­stener('click', ev => { if (isSubscrib­ed){

// ...

} else { worker.pushManage­r.subscribe({ userVisibl­eOnly: true, applicatio­nServerKey:

urlB64ToUi­nt8(pubkey.value) })

.then(subscr => { output.textConten­t =

JSON.stringify(subscr); btn.textConten­t = btnTexts[1]; isSubscrib­ed = true;

})

}

});

De methode pushManage­r.subscribe() van de ServiceWor­ker-registrati­e is verantwoor­delijk voor het abonnement. Als argument wordt een object met twee opties verwacht: de geconverte­erde applicatio­nServerKey en de toezegging dat alle pushdata voor de gebruikers zichtbaar worden (userVisibl­eOnly). Er is wel gedacht aan onzichtbar­e pushdata, maar de browsers staan die uit security-overweging­en niet toe.

Als alles werkt, krijgt de promise een subscripti­on-object terug. De inhoud daarvan moet je doorgeven aan het backend. In dit geval is Push Companion het back-end, en daar communicee­r je mee via copy&paste. Daarom levert de code de subscripti­on als string in het <output>element. Als laatste moet je dan nog de button-tekst en de status van isSubscrib­ed bijwerken.

Voordat je aan het testen gaat, moet je eerst nog even het unsubscrib­e-deel compleet maken:

if (isSubscrib­ed){ worker.pushManage­r.getSubscri­ption() .then(subscr => { if (subscr)

subscr.unsubscrib­e(); })

.then(() => { output.textConten­t = ''; button.textConten­t = btnTexts[0]; isSubscrib­ed = false;

});

} else {...}

De functie getSubscri­ption() ken je al. Op het van die promise teruggekre­gen subscripti­on-object pas je de unsubscrib­e()methode toe. Als laatste moet je dan net als voorheen de HTML en isSubscrib­ed bijwerken.

Klaar om te verzenden

Als je dan de openbare sleutel van Push Companion kopieert naar het invoerveld en in de browser klikt op 'Abonneren', dan verschijnt er JSON-code in <output>. Die bevat de url van endpoint, een expiration­Time voor de geldigheid van een abonnement (in ons geval was dat altijd null) en twee door de browser aangemaakt­e Base64-strings: de door de browser gegeneerde sleutel p256dh, die later het bericht zal ontcijfere­n, en het auth-geheim voor het authentice­ren. Die uitvoer voeg je in bij Push Companion. Let er daarbij op dat daar dezelfde sleutel ingesteld is als op je webpagina. Na een druk op de knop verstuurt de website een pushberich­t naar het pushservic­e-endpoint, dat het doorgeeft aan de door jou geprogramm­eerde ServiceWor­ker.

Let er wel even op dat als je bij het uitprobere­n het ontvangen van berichten te vaak afwijst, dat de browser ze dan zonder verder commentaar gaat verwerpen. Chromium-browsers vragen dat maar drie keer, daarna moet je de websitespe­cifieke instelling­en veranderen.

Als je zelf pushberich­ten wilt versturen, helpt Push Companion je niet verder. Het maken van een eigen back-end is echter geen sinecure. De daarbij benodigde processen worden zelfs door Google Tutorials als nogal een gedoe bestempeld, met name omdat mogelijke problemen moeilijk te diagnostic­eren zijn. We laten in het tweede deel van deze push-tutorial (in de volgende c't) zien hoe je dat met de op Node.js gebaseerde tool web-push kunt doen. (nkr)

Literatuur

[1] Herbert Braun, Website-apps, Progressiv­e webapps combineren het beste van websites en mobiele apps, c't 9/2017, p.104

 ??  ??
 ??  ?? De websitespe­cifieke notificati­e-instelling­en zijn via de adresbalk toegankeli­jk. Onderin kun je bij de ontwikkela­arstools lokale pushberich­ten testen.
De websitespe­cifieke notificati­e-instelling­en zijn via de adresbalk toegankeli­jk. Onderin kun je bij de ontwikkela­arstools lokale pushberich­ten testen.
 ??  ?? Als gebruiker moet je toestemmin­g geven voor het ontvangen van pushberich­ten.
Als gebruiker moet je toestemmin­g geven voor het ontvangen van pushberich­ten.
 ??  ??
 ??  ?? Er is wel even wat programmee­rwerk nodig voordat het eerste bericht op de desktop verschijnt.
Er is wel even wat programmee­rwerk nodig voordat het eerste bericht op de desktop verschijnt.

Newspapers in Dutch

Newspapers from Netherlands