Come funzionano i contenuti interattivi della BBC su AMP, app e Web
Pubblicato: 2022-03-10Nel team di Visual Journalism della BBC, produciamo entusiasmanti contenuti visivi, coinvolgenti e interattivi, che vanno dai calcolatori alle visualizzazioni, nuovi formati di narrazione.
Ogni applicazione è una sfida unica da produrre a sé stante, ma lo è ancora di più se si considera che dobbiamo distribuire la maggior parte dei progetti in molte lingue diverse. I nostri contenuti devono funzionare non solo sui siti Web di BBC News e Sports, ma anche su app equivalenti su iOS e Android, nonché su siti di terze parti che utilizzano contenuti BBC.
Ora considera che c'è una gamma crescente di nuove piattaforme come AMP, Facebook Instant Articles e Apple News. Ogni piattaforma ha i suoi limiti e il suo meccanismo di pubblicazione proprietario. Creare contenuti interattivi che funzionino in tutti questi ambienti è una vera sfida. Descriverò come abbiamo affrontato il problema alla BBC.
Esempio: canonico vs. AMP
Tutto questo è un po' teorico finché non lo vedi in azione, quindi approfondiamo direttamente un esempio.
Ecco un articolo della BBC contenente contenuti di Visual Journalism:
Questa è la versione canonica dell'articolo, cioè la versione predefinita, che otterrai navigando all'articolo dalla home page.
Ora diamo un'occhiata alla versione AMP dell'articolo:
Sebbene le versioni canoniche e AMP abbiano lo stesso aspetto, in realtà sono due endpoint diversi con un comportamento diverso:
- La versione canonica ti scorre nel paese prescelto quando invii il modulo.
- La versione AMP non ti scorre, poiché non puoi scorrere la pagina principale da un iframe AMP.
- La versione AMP mostra un iframe ritagliato con un pulsante "Mostra altro", a seconda delle dimensioni della finestra e della posizione di scorrimento. Questa è una caratteristica di AMP.
Oltre alle versioni canoniche e AMP di questo articolo, questo progetto è stato inviato anche all'app News, che è un'altra piattaforma con le sue complessità e limitazioni. Quindi , come supportiamo tutte queste piattaforme?
Gli strumenti sono fondamentali
Non costruiamo i nostri contenuti da zero. Abbiamo uno scaffold basato su Yeoman che utilizza Node per generare un progetto standard con un singolo comando.
I nuovi progetti vengono forniti con Webpack, SASS, implementazione e una struttura di componentizzazione pronta all'uso. L'internazionalizzazione è anche insita nei nostri progetti, utilizzando un sistema di sagomatura per Manubri. Tom Maslen ne parla in dettaglio nel suo post, 13 suggerimenti per rendere multilingue il responsive web design.
Per impostazione predefinita, funziona abbastanza bene per la compilazione per una piattaforma, ma è necessario supportare più piattaforme . Analizziamo un po' di codice.
Incorpora e autonomo
In Visual Journalism, a volte pubblichiamo il nostro contenuto all'interno di un iframe in modo che possa essere un "incorporamento" autonomo in un articolo, non influenzato dallo scripting e dallo stile globali. Un esempio di ciò è l'interattivo Donald Trump incorporato nell'esempio canonico in precedenza in questo articolo.
D'altra parte, a volte pubblichiamo il nostro contenuto come HTML grezzo. Lo facciamo solo quando abbiamo il controllo sull'intera pagina o se abbiamo bisogno di un'interazione di scorrimento davvero reattiva. Chiamiamoli rispettivamente i nostri output "embed" e "standalone".
Immaginiamo come potremmo costruire il "Un robot prenderà il tuo lavoro?" interattivo in entrambi i formati “embed” e “standalone”.
Entrambe le versioni del contenuto condividerebbero la stragrande maggioranza del loro codice, ma ci sarebbero alcune differenze cruciali nell'implementazione di JavaScript tra le due versioni.
Ad esempio, guarda il pulsante "Scopri il mio rischio di automazione". Quando l'utente preme il pulsante di invio, dovrebbe scorrere automaticamente fino ai risultati.
La versione "autonoma" del codice potrebbe assomigliare a questa:
button.on('click', (e) => { window.scrollTo(0, resultsContainer.offsetTop); });
Ma se lo stai costruendo come output "incorpora", sai che il tuo contenuto è all'interno di un iframe, quindi dovresti codificarlo in modo diverso:
// inside the iframe button.on('click', () => { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); }); // inside the host page window.addEventListener('message', (event) => { if (event.data.name === 'scroll') { window.scrollTo(0, iframe.offsetTop + event.data.offset); } });
Inoltre, cosa succede se la nostra applicazione deve andare a schermo intero? Questo è abbastanza facile se ti trovi in una pagina "autonoma":
document.body.className += ' fullscreen';
.fullscreen { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }
Se provassimo a farlo dall'interno di un "incorpora", questo stesso codice avrebbe il contenuto ridimensionato in base alla larghezza e all'altezza dell'iframe , piuttosto che al viewport:
...quindi, oltre ad applicare lo stile a schermo intero all'interno dell'iframe, dobbiamo inviare un messaggio alla pagina host per applicare lo stile all'iframe stesso:
// iframe window.parent.postMessage({ name: 'window:toggleFullScreen' }, '*'); // host page window.addEventListener('message', function () { if (event.data.name === 'window:toggleFullScreen') { document.getElementById(iframeUid).className += ' fullscreen'; } });
Questo può tradursi in un sacco di codice spaghetti quando inizi a supportare più piattaforme:
button.on('click', (e) => { if (inStandalonePage()) { window.scrollTo(0, resultsContainer.offsetTop); } else { window.parent.postMessage({ name: 'scroll', offset: resultsContainer.offsetTop }, '*'); } });
Immagina di fare un equivalente di questo per ogni interazione DOM significativa nel tuo progetto. Una volta che hai finito di rabbrividire, preparati una tazza di tè rilassante e continua a leggere.
L'astrazione è la chiave
Invece di costringere i nostri sviluppatori a gestire queste condizioni all'interno del loro codice, abbiamo creato un livello di astrazione tra il loro contenuto e l'ambiente. Chiamiamo questo livello "involucro".
Invece di interrogare direttamente il DOM o gli eventi del browser nativo, ora possiamo inoltrare la nostra richiesta tramite il modulo wrapper
.
import wrapper from 'wrapper'; button.on('click', () => { wrapper.scrollTo(resultsContainer.offsetTop); });
Ogni piattaforma ha la propria implementazione del wrapper conforme a un'interfaccia comune di metodi wrapper. Il wrapper si avvolge attorno al nostro contenuto e gestisce la complessità per noi.
L'implementazione della funzione scrollTo
da parte del wrapper autonomo è molto semplice, passando il nostro argomento direttamente a window.scrollTo
sotto il cofano.
Ora diamo un'occhiata a un wrapper separato che implementa la stessa funzionalità per l'iframe:
Il wrapper "incorpora" utilizza lo stesso argomento dell'esempio "autonomo", ma manipola il valore in modo che venga preso in considerazione l'offset iframe. Senza questa aggiunta, avremmo fatto scorrere il nostro utente da qualche parte completamente non intenzionale.
Il modello dell'involucro
L'uso dei wrapper produce un codice più pulito, più leggibile e coerente tra i progetti. Consente inoltre micro-ottimizzazioni nel tempo, poiché apportiamo miglioramenti incrementali ai wrapper per rendere i loro metodi più performanti e accessibili. Il tuo progetto può quindi beneficiare dell'esperienza di molti sviluppatori.
Allora, che aspetto ha un involucro?
Struttura dell'involucro
Ciascun wrapper comprende essenzialmente tre elementi: un modello Handlebars, un file JS del wrapper e un file SASS che denota uno stile specifico del wrapper. Inoltre, ci sono attività di compilazione che si collegano agli eventi esposti dall'impalcatura sottostante in modo che ogni wrapper sia responsabile della propria precompilazione e pulizia.
Questa è una vista semplificata del wrapper di incorporamento:
embed-wrapper/ templates/ wrapper.hbs js/ wrapper.js scss/ wrapper.scss
Il nostro scaffolding sottostante espone il tuo modello di progetto principale come un Manubrio parziale, che viene consumato dal wrapper. Ad esempio, templates/wrapper.hbs
potrebbe contenere:
<div class="bbc-news-vj-wrapper--embed"> {{>your-application}} </div>
scss/wrapper.scss
contiene uno stile specifico del wrapper che il codice dell'applicazione non dovrebbe definire da sé. Il wrapper di incorporamento, ad esempio, replica molti degli stili di BBC News all'interno dell'iframe.
Infine, js/wrapper.js
contiene l'implementazione iframed dell'API wrapper, dettagliata di seguito. Viene fornito separatamente al progetto, anziché essere compilato con il codice dell'applicazione: contrassegniamo il wrapper
come globale nel processo di compilazione del Webpack. Ciò significa che, sebbene consegniamo la nostra applicazione a più piattaforme, compiliamo il codice solo una volta.
API wrapper
L'API wrapper astrae una serie di interazioni chiave del browser. Ecco i più importanti:
scrollTo(int)
Scorre alla posizione data nella finestra attiva. Il wrapper normalizzerà l'intero fornito prima di attivare lo scorrimento in modo che la pagina host venga fatta scorrere nella posizione corretta.
getScrollPosition: int
Restituisce la posizione di scorrimento corrente (normalizzata) dell'utente. Nel caso dell'iframe, ciò significa che la posizione di scorrimento passata all'applicazione è effettivamente negativa finché l'iframe non si trova nella parte superiore della finestra. Questo è super utile e ci consente di fare cose come animare un componente solo quando viene visualizzato.
onScroll(callback)
Fornisce un hook nell'evento di scorrimento. Nel wrapper autonomo, questo essenzialmente si collega all'evento di scorrimento nativo. Nel wrapper di incorporamento, si verificherà un leggero ritardo nella ricezione dell'evento di scorrimento poiché viene passato tramite postMessage.
viewport: {height: int, width: int}
Un metodo per recuperare l'altezza e la larghezza della finestra (poiché ciò viene implementato in modo molto diverso quando viene interrogato dall'interno di un iframe).
toggleFullScreen
In modalità standalone, nascondiamo alla vista il menu e il footer della BBC e impostiamo una position: fixed
sul nostro contenuto. Nell'app News non facciamo nulla: il contenuto è già a schermo intero. Quello complicato è l'iframe, che si basa sull'applicazione di stili sia all'interno che all'esterno dell'iframe, coordinati tramite postMessage.
markPageAsLoaded
Dì al wrapper che il tuo contenuto è stato caricato. Questo è fondamentale per il funzionamento dei nostri contenuti nell'app News, che non tenterà di mostrare i nostri contenuti all'utente finché non comunicheremo esplicitamente all'app che i nostri contenuti sono pronti. Rimuove anche lo spinner di caricamento sulle versioni web dei nostri contenuti.
Elenco dei wrapper
In futuro, prevediamo di creare wrapper aggiuntivi per piattaforme di grandi dimensioni come Facebook Instant Articles e Apple News. Ad oggi abbiamo creato sei wrapper:
Involucro autonomo
La versione del nostro contenuto che dovrebbe andare in pagine autonome. Viene fornito in bundle con il marchio BBC.
Incorpora involucro
La versione iframe del nostro contenuto, che può essere inserita in modo sicuro all'interno degli articoli o da distribuire in syndication a siti non BBC, poiché manteniamo il controllo sul contenuto.
Involucro AMP
Questo è l'endpoint che viene inserito come amp-iframe
nelle pagine AMP.
Avvolgi app di notizie
Il nostro contenuto deve effettuare chiamate a un protocollo proprietario bbcvisualjournalism://
.
Involucro centrale
Contiene solo l'HTML, nessuno dei CSS o JavaScript del nostro progetto.
Involucro JSON
Una rappresentazione JSON dei nostri contenuti, per la condivisione tra i prodotti BBC.
Avvolgitori di cablaggio fino alle piattaforme
Affinché i nostri contenuti appaiano sul sito della BBC, forniamo ai giornalisti un percorso con spazi di nomi:
/include/[department]/[unique ID], eg
/include/visual-journalism/123-quiz
Il giornalista inserisce questo "percorso di inclusione" nel CMS, che salva la struttura dell'articolo nel database. Tutti i prodotti e servizi si trovano a valle di questo meccanismo di pubblicazione. Ogni piattaforma è responsabile della scelta del contenuto desiderato e della richiesta di tale contenuto da un server proxy.
Prendiamo quel Donald Trump interattivo di prima. Qui, il percorso di inclusione nel CMS è:
/include/newsspec/15996-trump-tracker/english/index
La pagina dell'articolo canonico sa che vuole la versione "incorporata" del contenuto, quindi aggiunge /embed
al percorso di inclusione:
/include/newsspec/15996-trump-tracker/english/index
/embed
…prima di richiederlo al server proxy:
https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/embed
La pagina AMP, d'altra parte, vede il percorso di inclusione e aggiunge /amp
:
/include/newsspec/15996-trump-tracker/english/index
/amp
Il renderer AMP fa un po' di magia per rendere alcuni HTML AMP che fanno riferimento al nostro contenuto, inserendo la versione /amp
come un iframe:
<amp-iframe src="https://news.files.bbci.co.uk/include/newsspec/15996-trump-tracker/english/index/amp" width="640" height="360"> <!-- some other AMP elements here --> </amp-iframe>
Ogni piattaforma supportata ha la propria versione del contenuto:
/include/newsspec/15996-trump-tracker/english/index
/amp
/include/newsspec/15996-trump-tracker/english/index
/core
/include/newsspec/15996-trump-tracker/english/index
/envelope
...e così via
Questa soluzione può essere scalata per incorporare più tipi di piattaforma man mano che si presentano.
L'astrazione è difficile
Costruire un'architettura "scrivi una volta, distribuisci ovunque" sembra abbastanza idealistico, e lo è. Affinché l'architettura del wrapper funzioni, dobbiamo essere molto severi nel lavorare all'interno dell'astrazione. Ciò significa che dobbiamo combattere la tentazione di "fare questa cosa hacky per farlo funzionare in [inserire il nome della piattaforma qui]". Vogliamo che i nostri contenuti siano completamente all'oscuro dell'ambiente in cui vengono spediti, ma è più facile a dirsi che a farsi.
Le caratteristiche della piattaforma sono difficili da configurare in modo astratto
Prima del nostro approccio di astrazione, avevamo il controllo completo su ogni aspetto del nostro output, incluso, ad esempio, il markup del nostro iframe. Se avessimo bisogno di modificare qualcosa in base al progetto, come aggiungere un attributo title
all'iframe per motivi di accessibilità, potremmo semplicemente modificare il markup.
Ora che il markup del wrapper esiste in isolamento dal progetto, l'unico modo per configurarlo sarebbe esporre un hook nello scaffold stesso. Possiamo farlo in modo relativamente semplice per le funzionalità multipiattaforma, ma esporre hook per piattaforme specifiche interrompe l'astrazione. Non vogliamo davvero esporre un'opzione di configurazione "titolo iframe" che viene utilizzata solo da un wrapper.
Potremmo nominare la proprietà in modo più generico, ad esempio title
, e quindi utilizzare questo valore come attributo del title
dell'iframe. Tuttavia, inizia a diventare difficile tenere traccia di ciò che viene utilizzato e dove, e rischiamo di atrarre la nostra configurazione al punto da non capirla più. In generale, cerchiamo di mantenere la nostra configurazione il più snella possibile, impostando solo proprietà che hanno un uso globale.
Il comportamento dei componenti può essere complesso
Sul Web, il nostro modulo sharetools emette pulsanti di condivisione sui social network che possono essere cliccati individualmente e aprono un messaggio di condivisione precompilato in una nuova finestra.
Nell'app News, non vogliamo condividere tramite il Web mobile. Se l'utente ha installato l'applicazione pertinente (ad es. Twitter), vogliamo condividere l'app stessa. Idealmente, vogliamo presentare all'utente il menu di condivisione nativo di iOS/Android, quindi lasciare che scelga l'opzione di condivisione prima di aprire l'app con un messaggio di condivisione precompilato. Possiamo attivare il menu di condivisione nativo dall'app effettuando una chiamata al protocollo proprietario bbcvisualjournalism://
.
Tuttavia, questa schermata verrà attivata toccando "Twitter" o "Facebook" nella sezione "Condividi i tuoi risultati", quindi l'utente finisce per dover fare la sua scelta due volte; la prima volta all'interno del nostro contenuto e una seconda volta sul popup nativo.
Questo è uno strano viaggio dell'utente, quindi vogliamo rimuovere le singole icone di condivisione dall'app News e mostrare invece un pulsante di condivisione generico. Siamo in grado di farlo controllando esplicitamente quale wrapper è in uso prima di eseguire il rendering del componente.
La creazione del livello di astrazione del wrapper funziona bene per i progetti nel loro insieme, ma quando la scelta del wrapper influisce sulle modifiche a livello di componente , è molto difficile mantenere un'astrazione pulita. In questo caso, abbiamo perso un po' di astrazione e abbiamo una logica di fork disordinata nel nostro codice. Per fortuna, questi casi sono pochi e rari.
Come gestiamo le funzionalità mancanti?
Mantenere l'astrazione va bene. Il nostro codice dice al wrapper cosa vuole che la piattaforma faccia, ad esempio "vai a schermo intero". Ma cosa succede se la piattaforma su cui spediamo non può effettivamente andare a schermo intero?
Il wrapper farà del suo meglio per non rompersi del tutto, ma alla fine è necessario un design che ricada con grazia su una soluzione funzionante indipendentemente dal fatto che il metodo abbia successo o meno. Dobbiamo progettare in modo difensivo.
Supponiamo di avere una sezione dei risultati contenente alcuni grafici a barre. Spesso ci piace mantenere i valori del grafico a barre su zero fino a quando i grafici non vengono visualizzati, a quel punto attiviamo l'animazione delle barre alla larghezza corretta.
Ma se non abbiamo alcun meccanismo per agganciarci alla posizione di scorrimento, come nel caso del nostro wrapper AMP, le barre rimarrebbero per sempre a zero, il che è un'esperienza completamente fuorviante.
Cerchiamo sempre più di adottare un approccio di miglioramento progressivo nei nostri progetti. Ad esempio, potremmo fornire un pulsante che sarà visibile per tutte le piattaforme per impostazione predefinita, ma che viene nascosto se il wrapper supporta lo scorrimento. In questo modo, se lo scroll non riesce ad attivare l'animazione, l'utente può comunque attivare l'animazione manualmente.
Piani per il futuro
Ci auguriamo di sviluppare nuovi wrapper per piattaforme come Apple News e Facebook Instant Articles, nonché di offrire a tutte le nuove piattaforme una versione "core" dei nostri contenuti pronta all'uso.
Speriamo anche di migliorare nel miglioramento progressivo; avere successo in questo campo significa svilupparsi in modo difensivo. Non puoi mai presumere che tutte le piattaforme ora e in futuro supporteranno una determinata interazione, ma un progetto ben progettato dovrebbe essere in grado di trasmettere il suo messaggio centrale senza cadere nel primo ostacolo tecnico.
Lavorare entro i confini dell'involucro è un po' un cambio di paradigma e sembra un po' una via di mezzo in termini di soluzione a lungo termine . Ma fino a quando il settore non maturerà su uno standard multipiattaforma, gli editori saranno costretti a implementare le proprie soluzioni o utilizzare strumenti come Distro per la conversione da piattaforma a piattaforma, oppure ignorare del tutto intere sezioni del loro pubblico.
Per noi è l'inizio dei tempi, ma finora abbiamo avuto un grande successo nell'utilizzare il modello wrapper per creare i nostri contenuti una volta e distribuirli alla miriade di piattaforme che il nostro pubblico sta attualmente utilizzando.