10 minuti di lettura
Quando una WebApp viene caricata dentro una WebView, all’utente sembra di avere davanti un’unica applicazione. Il login è lo stesso, la navigazione sembra continua e una schermata web può aprire una funzione nativa come la lettura NFC.
Tecnicamente, però, esistono due ambienti distinti:
– la **WebApp**, eseguita dal motore JavaScript della pagina;
– la **shell React Native**, che controlla la `WebView` e può accedere alle API native del dispositivo.
La pagina web non può chiamare direttamente una funzione React Native. Allo stesso modo, il codice nativo non può manipolare il DOM come farebbe uno script eseguito nella pagina. Serve quindi un contratto di comunicazione esplicito: un piccolo canale attraverso il quale i due ambienti si scambiano comandi, risultati ed errori.
Nel caso di myGenius, questo canale è necessario perché la WebApp gestisce la maggior parte dell’interfaccia HR, mentre la shell nativa si occupa delle funzioni che richiedono accesso al dispositivo: NFC, permessi, navigazione nativa, download e gestione della sessione.
Il punto importante è questo: il bridge non è un dettaglio tecnico aggiunto alla fine. È parte dell’architettura del prodotto.
Perché WebView e React Native hanno bisogno di un bridge
Una `WebView` è un browser incorporato. Carica un URL, esegue HTML, CSS e JavaScript, gestisce cookie e navigazione, ma resta isolata dal codice React Native che la contiene.
Questa separazione è utile: la WebApp può evolvere senza riscrivere ogni schermata in nativo e la shell può concentrarsi sulle capacità del telefono. Ma crea anche un confine che va progettato.
Immaginiamo alcuni flussi tipici:
– l’utente tocca «Timbratura NFC» nella WebApp;
– la WebApp chiede alla shell di avviare la scansione;
– React Native verifica che NFC sia attivo e avvia il servizio nativo;
– il risultato della lettura torna alla WebApp;
– la WebApp aggiorna l’interfaccia e completa il flusso.
Senza un bridge, ogni ambiente potrebbe solo eseguire il proprio codice. Con un bridge, la WebApp invia un’intenzione — per esempio `nfcOn` — e la shell decide come realizzarla sul dispositivo.
È una distinzione importante: la WebApp non dovrebbe sapere come funziona `NfcManager`, così come la shell non dovrebbe conoscere la logica interna di ogni form web. I due lati si parlano attraverso messaggi, non attraverso import diretti o dipendenze nascoste.
I due versi della comunicazione
`react-native-webview` espone due direzioni differenti. Confonderle è uno degli errori più comuni quando si implementa il primo bridge.
| Direzione | API | Ricevente |
|—|—|—|
| WebApp → React Native | `window.ReactNativeWebView.postMessage(data)` | prop `onMessage` della `WebView` |
| React Native → WebApp | `webViewRef.current.postMessage(data)` | evento `message` nella pagina |
| React Native → WebApp, con esecuzione di codice | `injectJavaScript(script)` | JavaScript della pagina |
La documentazione di `react-native-webview` specifica che il valore inviato con `postMessage` deve essere una stringa. Per trasmettere un oggetto, quindi, il formato più semplice e prevedibile è JSON.
Dalla WebApp alla shell nativa
Il codice eseguito nella pagina può inviare un messaggio così:
function requestNative(message) {
const bridge = window.ReactNativeWebView;
if (!bridge) {
return false;
}
bridge.postMessage(JSON.stringify(message));
return true;
}
requestNative({
version: 1,
id: 'request-123',
type: 'nfc',
action: 'nfcOn',
});
Sul lato React Native, il messaggio arriva nell’handler assegnato alla prop `onMessage`:
const handleMessage = (event) => {
const rawMessage = event.nativeEvent.data;
const message = JSON.parse(rawMessage);
// Validazione e dispatch del messaggio
};
<WebView onMessage={handleMessage} />;
L’oggetto `window.ReactNativeWebView` viene reso disponibile dalla libreria quando la `WebView` è configurata per gestire `onMessage`. Fuori dalla shell — per esempio aprendo la WebApp in Safari o Chrome — quell’oggetto può non esistere. Per questo il controllo iniziale è utile: permette alla stessa pagina di funzionare anche fuori dall’app, almeno per le parti che non richiedono il bridge.
Dalla shell nativa alla WebApp
Il percorso inverso parte da un riferimento alla `WebView`:
const sendToWeb = (webViewRef, message) => {
const webView = webViewRef.current;
if (!webView) {
return false;
}
webView.postMessage(JSON.stringify(message));
return true;
};
La pagina web deve ascoltare l’evento `message`:
window.addEventListener('message', (event) => {
const message = JSON.parse(event.data);
if (message.type === 'nfc' && message.action === 'tagRead') {
updateStampingView(message.payload);
}
});
Gli esempi ufficiali della libreria mostrano l’ascolto tramite `window` e, per compatibilità tra piattaforme e versioni, anche tramite `document`. La scelta va verificata sui dispositivi supportati e standardizzata nella WebApp: registrare indiscriminatamente entrambi gli handler può causare doppie elaborazioni se non esiste un identificativo del messaggio.
Nel progetto myGenius, il riferimento viene mantenuto dalla `WebViewScreen` ed esposto al genitore tramite `useImperativeHandle`. Il componente nativo può così ricevere messaggi dalla pagina tramite `onMessage` e inviarne di nuovi senza esporre alla WebApp l’implementazione della `WebView`.
La differenza tra postMessage e window.ReactNativeWebView.postMessage
I nomi sono simili, ma le API appartengono a livelli diversi.
window.postMessage
`window.postMessage` è una Web API standard. Serve a far comunicare contesti browser distinti, come una finestra e un iframe, anche quando appartengono a origini diverse. Il destinatario riceve un `MessageEvent` con proprietà come `data`, `origin` e `source`.
Non è, da sola, l’API che collega una pagina a React Native. Chiamare questo metodo non invia automaticamente un evento alla prop `onMessage` della `WebView`.
window.ReactNativeWebView.postMessage
`window.ReactNativeWebView.postMessage` è l’API esposta da `react-native-webview` dentro la pagina caricata. Il suo destinatario è la shell React Native e il valore inviato diventa `event.nativeEvent.data` nell’handler nativo.
È quindi il metodo da usare per il percorso **WebApp → React Native**.
webViewRef.current.postMessage
Questo è il metodo chiamato dal codice React Native sul riferimento della `WebView`. Il messaggio viene consegnato al contesto JavaScript della pagina e deve essere gestito con un listener per l’evento `message`.
È il metodo da usare per il percorso **React Native → WebApp**.
injectJavaScript è un’altra cosa
`injectJavaScript` esegue uno script nella pagina. È utile per operazioni come aggiornare il DOM, impostare una variabile o dispatchare un evento custom. Non sostituisce un protocollo di messaggi:
– eseguire una stringa di codice accoppia la shell all’implementazione interna della pagina;
– un messaggio descrive invece un’intenzione con una struttura che può essere validata;
– gli script iniettati diventano più difficili da versionare e testare quando crescono.
In myGenius l’iniezione viene usata, per esempio, per comunicare alla WebApp la versione dell’app installata. Per comandi ricorrenti — NFC, navigazione, errori o cookie — il messaggio strutturato è una base più manutenibile.
I limiti del canale
Il bridge è piccolo e funzionale, ma non è un bus di messaggistica completo. Alcuni limiti vanno accettati e gestiti esplicitamente.
1. Il trasporto non conosce il contratto
Per la libreria, `{“type”:”nfc”}` e `”ciao”` sono semplicemente stringhe. Non esiste una validazione automatica di `type`, `action` o `payload`.
Se la WebApp invia `nfcOn`, `nfc-on` o un payload incompleto, il problema emerge solo durante l’esecuzione. Il bridge trasporta dati: non decide se quei dati abbiano senso.
2. Non esiste una risposta automatica
`postMessage` non restituisce il risultato dell’operazione. Se la WebApp avvia una scansione NFC, deve ricevere un secondo messaggio quando l’operazione termina.
Per gestire flussi asincroni servono almeno:
– un `id` univoco per correlare richiesta e risposta;
– uno stato `pending`, `success` o `error`;
– un timeout sul lato che attende la risposta;
– una regola per messaggi duplicati o arrivati fuori ordine.
3. La pagina può essere ricaricata
Una navigazione, un refresh o la ricreazione della `WebView` può cancellare lo stato JavaScript della WebApp. Un messaggio inviato prima che la pagina abbia installato i propri listener può andare perso.
Per questo i messaggi importanti non dovrebbero essere inviati «una volta sola» senza una conferma. Un handshake `ready` tra pagina e shell è spesso sufficiente per sapere quando il canale è pronto.
4. Il formato stringa nasconde gli errori
`JSON.parse` può fallire. Anche un JSON valido può avere un tipo sconosciuto o un payload non conforme. Un `try/catch` evita il crash, ma non risolve il problema di prodotto: bisogna decidere come segnalare il messaggio non valido e come osservare l’errore senza registrare dati sensibili.
5. Il bridge non sostituisce il controllo della navigazione
Se la `WebView` può navigare verso un’origine non prevista, la pagina caricata potrebbe tentare di usare il canale nativo. Limitare gli URL ammessi e separare i link esterni sono quindi misure architetturali, non solo configurazioni di contorno.
Come tipizzare i messaggi
Il pattern più semplice è usare un envelope comune e un’unione discriminata. Ogni messaggio contiene almeno:
– `version`: versione del protocollo;
– `id`: identificativo della richiesta;
– `type`: area funzionale;
– `action`: operazione richiesta;
– `payload`: dati dell’operazione, quando necessari.
In TypeScript il contratto può essere espresso così:
type BridgeMessage =
| {
version: 1;
id: string;
type: 'screen';
action: 'leaves' | 'attendance' | 'sickness-archive';
payload?: never;
}
| {
version: 1;
id: string;
type: 'nfc';
action: 'nfcOn' | 'nfcOff' | 'timestampSuccess' | 'errorBeep';
payload?: unknown;
}
| {
version: 1;
id: string;
type: 'download';
action: 'document';
payload: { id: string };
};
Il vantaggio non è solo l’autocompletamento. Quando il codice usa `type` e `action` come discriminanti, il compilatore può segnalare un’azione non prevista o un payload mancante.
Il controllo a runtime resta comunque obbligatorio: i tipi di TypeScript scompaiono dopo la compilazione e non possono impedire a una pagina aggiornata male, a una versione precedente della WebApp o a un input manipolato di inviare dati errati.
Un decoder minimale dovrebbe quindi validare il confine:
function decodeMessage(rawMessage) {
if (typeof rawMessage !== 'string') {
return null;
}
try {
const message = JSON.parse(rawMessage);
if (
!message ||
message.version !== 1 ||
typeof message.id !== 'string' ||
typeof message.type !== 'string'
) {
return null;
}
return message;
} catch {
return null;
}
}
In un progetto JavaScript lo stesso contratto può essere documentato con JSDoc e verificato con `// @ts-check`, oppure con uno schema runtime condiviso tra WebApp e shell. La soluzione migliore dipende dal modo in cui i due progetti vengono distribuiti, ma il principio non cambia: un messaggio deve avere una forma pubblica, versionata e verificabile.
Il ruolo del messageController
Una volta superati i primi due o tre messaggi, mettere tutta la logica direttamente in `WebViewScreen` diventa scomodo. La schermata dovrebbe occuparsi del rendering, del loading e del riferimento imperativo. Non dovrebbe sapere come si cancella una sessione, come si avvia NFC o quale rotta aprire per l’archivio malattie.
Per questo serve un controller nel livello app-shell.
Nel progetto myGenius il flusso è separato in questo modo:
1. `AppStack` possiede il riferimento alla `WebView` e collega la prop `onMessage`;
2. `WebViewScreen` renderizza la `WebView` ed espone i metodi imperativi necessari;
3. `messageController` riceve l’evento, interpreta il JSON e decide quale capacità nativa attivare;
4. i servizi dedicati eseguono l’operazione concreta;
5. `messageHelper` invia alla WebApp risultati o errori attraverso il riferimento alla `WebView`.
L’handler attuale riconosce diverse aree funzionali:
– `jwt_token` e `login` per la sessione;
– `download` per i documenti;
– `nfc` per scansione, suoni e navigazione verso il badge elettronico;
– `permissions` per i permessi del dispositivo;
– `screen` per aprire le feature native di ferie, presenze e malattia;
– `cookies` per sincronizzare un cookie con il dominio della WebApp;
– `pullToRefresh` per controllare il gesto nativo della `WebView`.
Questa centralizzazione ha un vantaggio concreto: la WebApp invia un comando, ma non deve conoscere `NfcManager`, `CookieManager`, React Navigation o il Keychain. La shell conserva la responsabilità delle proprie integrazioni native.
Il controller, però, non dovrebbe diventare un nuovo monolite. La sua responsabilità ideale è limitata a:
– decodificare e validare il messaggio;
– verificare che `type` e `action` siano supportati;
– scegliere l’handler corretto;
– restituire un esito coerente;
– applicare una policy comune per errori e log.
Le operazioni di dominio restano in servizi separati. Una possibile evoluzione è passare da una lunga sequenza di condizioni a una mappa di handler:
const handlers = {
screen: handleScreenMessage,
nfc: handleNfcMessage,
permissions: handlePermissionMessage,
download: handleDownloadMessage,
};
async function dispatchMessage(message, context) {
const handler = handlers[message.type];
if (!handler) {
return sendUnsupportedMessage(context.webViewRef, message);
}
return handler(message, context);
}
Non è necessario introdurre questa struttura prima di avere un problema reale. La decisione importante viene prima: un solo ingresso per i messaggi, un contratto pubblico e handler che non si mescolano al rendering.
Sicurezza e controllo della WebView
Un bridge permette di chiamare funzioni native da una pagina web. Questa possibilità va trattata come una superficie sensibile.
Caricare solo origini previste
`originWhitelist` non dovrebbe essere considerato un lasciapassare generico in produzione. Una configurazione come `[‘*’]` è ampia e va valutata con attenzione. La shell dovrebbe limitare la navigazione al dominio applicativo e aprire i link esterni con il sistema operativo, quando il caso d’uso lo richiede.
Nel progetto myGenius esiste anche un controllo su `onShouldStartLoadWithRequest` che distingue il dominio interno dagli schemi esterni come `mailto:`, `tel:` e `whatsapp:`. Le due misure hanno scopi complementari: la whitelist configura gli URL ammessi, mentre l’handler può applicare regole di navigazione più specifiche.
Validare ogni messaggio
Un messaggio ricevuto non è automaticamente autorizzato solo perché arriva dalla `WebView`. Prima di eseguire un’azione nativa bisogna controllare:
– che la pagina corrente sia un’origine prevista;
– che il JSON abbia la versione e la struttura corrette;
– che `type` e `action` appartengano alla allowlist;
– che il payload contenga solo i dati necessari;
– che l’azione sia compatibile con lo stato corrente dell’app.
Il controllo dell’origine e il controllo dello schema risolvono problemi diversi: il primo riguarda *chi* sta parlando, il secondo *che cosa* sta chiedendo.
Non inviare segreti senza una ragione precisa
Token JWT, cookie e dati dei dipendenti non dovrebbero finire nei log del bridge. Se un token deve attraversare il confine per completare un flusso di autenticazione, il passaggio va documentato, limitato e protetto.
Nel design di myGenius il controller riceve un `jwt_token` e lo salva nel Keychain tramite un servizio dedicato. È una scelta funzionale al flusso di sessione, ma rende ancora più importante non accettare messaggi da pagine non attendibili e non stampare il payload completo durante il debug.
Limitare i privilegi del bridge
Non ogni messaggio deve poter navigare, leggere dati locali o invocare hardware. Le azioni vanno esposte con il principio del privilegio minimo: ogni `type` abilita solo le operazioni necessarie alla propria feature.
Domande frequenti
window.postMessage e `window.ReactNativeWebView.postMessage` sono la stessa cosa?
No. `window.postMessage` è una Web API per la comunicazione tra contesti browser. `window.ReactNativeWebView.postMessage` è il metodo esposto da `react-native-webview` per inviare una stringa dalla pagina all’handler `onMessage` di React Native.
Come si invia un oggetto alla WebView?
Si serializza in JSON su entrambi i versi:
webViewRef.current.postMessage(JSON.stringify({
version: 1,
id: 'response-123',
type: 'error',
payload: { code: 'NFC_DISABLED' },
}));
La pagina riceve una stringa in `event.data` e deve fare il parsing prima di usare il valore.
Il bridge è una connessione sincrona?
No. `postMessage` invia un evento, ma non restituisce il risultato dell’operazione. Per le operazioni asincrone bisogna progettare una coppia richiesta-risposta, con `id`, stato e gestione del timeout.
TypeScript è sufficiente per proteggere i messaggi?
No. TypeScript aiuta durante lo sviluppo, ma i messaggi arrivano a runtime come stringhe. Serve anche una validazione runtime, soprattutto quando WebApp e app nativa possono essere aggiornate in momenti diversi.
Quando conviene usare injectJavaScript?
È utile per una piccola modifica o per l’iniezione di uno script controllato, per esempio comunicare la versione dell’app alla pagina. Per comandi applicativi ripetuti è preferibile un protocollo a messaggi: è più esplicito, validabile e osservabile.
Conclusione
Una WebApp dentro React Native non è un unico runtime: è un sistema composto da due ambienti che devono accordarsi su un protocollo. Il bridge basato su `postMessage` è il punto di contatto, ma il metodo da solo non risolve il problema.
La comunicazione diventa affidabile quando sono chiari:
– la direzione del messaggio;
– il formato dei dati;
– il momento in cui il canale è pronto;
– la gestione di risposta, errore e timeout;
– le origini autorizzate;
– il confine di responsabilità tra WebApp e shell nativa.
Nel progetto myGenius questa responsabilità è raccolta nel `messageController`, che fa da orchestratore tra la pagina web e i servizi nativi. È una scelta che mantiene la `WebViewScreen` semplice e lascia alla shell il controllo delle capacità del dispositivo.
Nel prossimo articolo entreremo nel dettaglio dell’implementazione del `messageController`: parsing, dispatch su `type` e `action`, navigazione, NFC, cookie, download e pattern per aggiungere nuovi messaggi senza trasformare il bridge in un punto fragile dell’applicazione.




