Ricaricamenti persistenti dei dati tra le pagine: cookie, DB indicizzato e tutto ciò che c'è nel mezzo
Pubblicato: 2022-03-11Supponiamo che io stia visitando un sito web. Faccio clic con il pulsante destro del mouse su uno dei collegamenti di navigazione e seleziono per aprire il collegamento in una nuova finestra. Cosa dovrebbe succedere? Se sono come la maggior parte degli utenti, mi aspetto che la nuova pagina abbia lo stesso contenuto come se avessi fatto clic direttamente sul collegamento. L'unica differenza dovrebbe essere che la pagina appare in una nuova finestra. Ma se il tuo sito web è un'applicazione a pagina singola (SPA), potresti vedere risultati strani a meno che tu non abbia pianificato attentamente questo caso.
Ricordiamo che in una SPA, un tipico collegamento di navigazione è spesso un identificatore di frammento, che inizia con un cancelletto (#). Facendo clic direttamente sul collegamento non si ricarica la pagina, quindi tutti i dati archiviati nelle variabili JavaScript vengono conservati. Ma se apro il collegamento in una nuova scheda o finestra, il browser ricarica la pagina, reinizializzando tutte le variabili JavaScript. Quindi tutti gli elementi HTML associati a tali variabili verranno visualizzati in modo diverso, a meno che tu non abbia adottato misure per preservare quei dati in qualche modo.
C'è un problema simile se ricarico esplicitamente la pagina, ad esempio premendo F5. Potresti pensare che non dovrei mai aver bisogno di premere F5, perché hai impostato un meccanismo per inviare automaticamente le modifiche dal server. Ma se sono un utente tipico, puoi scommettere che ricaricherò comunque la pagina. Forse il mio browser sembra aver ridipinto lo schermo in modo errato, o voglio solo essere certo di avere le quotazioni azionarie più recenti.
Le API possono essere apolidi, l'interazione umana no
A differenza di una richiesta interna tramite un'API RESTful, l'interazione di un utente umano con un sito Web non è senza stato. Come utente web, penso alla mia visita al tuo sito come a una sessione, quasi come una telefonata. Mi aspetto che il browser ricordi i dati sulla mia sessione, nello stesso modo in cui quando chiamo la tua linea di vendita o di supporto, mi aspetto che il rappresentante ricordi ciò che è stato detto in precedenza nella chiamata.
Un ovvio esempio di dati di sessione è se ho effettuato l'accesso e, in caso affermativo, quale utente. Una volta passata attraverso una schermata di accesso, dovrei essere in grado di navigare liberamente attraverso le pagine specifiche dell'utente del sito. Se apro un collegamento in una nuova scheda o finestra e mi viene presentata un'altra schermata di accesso, non è molto facile da usare.
Un altro esempio è il contenuto del carrello in un sito di e-commerce. Se premendo F5 si svuota il carrello, è probabile che gli utenti si arrabbino.
In una tradizionale applicazione multipagina scritta in PHP, i dati della sessione verrebbero archiviati nell'array superglobale $_SESSION. Ma in una SPA, deve essere da qualche parte dal lato del cliente. Esistono quattro opzioni principali per la memorizzazione dei dati della sessione in una SPA:
- Biscotti
- Identificatore del frammento
- Archiviazione web
- IndicizzatoDB
Quattro kilobyte di cookie
I cookie sono una vecchia forma di archiviazione web nel browser. Inizialmente erano destinati a memorizzare i dati ricevuti dal server in una richiesta e rispedirli al server nelle richieste successive. Ma da JavaScript, puoi utilizzare i cookie per memorizzare praticamente qualsiasi tipo di dati, fino a un limite di dimensione di 4 KB per cookie. AngularJS offre il modulo ngCookies per la gestione dei cookie. C'è anche un pacchetto js-cookies che fornisce funzionalità simili in qualsiasi framework.
Tieni presente che qualsiasi cookie che crei verrà inviato al server ad ogni richiesta, sia che si tratti di un ricaricamento di pagina o di una richiesta Ajax. Ma se i dati della sessione principale che devi archiviare sono il token di accesso per l'utente che ha effettuato l'accesso, vuoi comunque che questo venga inviato al server ad ogni richiesta. È naturale provare a utilizzare questa trasmissione automatica di cookie come mezzo standard per specificare il token di accesso per le richieste Ajax.
Si potrebbe obiettare che l'utilizzo dei cookie in questo modo è incompatibile con l'architettura RESTful. Ma in questo caso va bene perché ogni richiesta tramite l'API è ancora senza stato, avendo alcuni input e alcuni output. È solo che uno degli input viene inviato in modo divertente, tramite un cookie. Se puoi fare in modo che la richiesta dell'API di accesso invii il token di accesso anche in un cookie, allora il tuo codice lato client non ha quasi bisogno di gestire i cookie. Ancora una volta, è solo un altro output della richiesta che viene restituito in un modo insolito.
I cookie offrono un vantaggio rispetto all'archiviazione web. Puoi fornire una casella di controllo "mantienimi connesso" nel modulo di accesso. Con la semantica, mi aspetto che se la lascio deselezionata, rimarrò connesso se ricarico la pagina o apro un collegamento in una nuova scheda o finestra, ma sono sicuro di essere disconnesso una volta chiuso il browser. Questa è un'importante funzione di sicurezza se utilizzo un computer condiviso. Come vedremo in seguito, l'archiviazione web non supporta questo comportamento.
Quindi, come potrebbe funzionare in pratica questo approccio? Supponiamo che tu stia usando LoopBack sul lato server. Hai definito un modello Persona, estendendo il modello Utente integrato, aggiungendo le proprietà che desideri mantenere per ciascun utente. Hai configurato il modello Persona per essere esposto su REST. Ora devi modificare server/server.js per ottenere il comportamento dei cookie desiderato. Di seguito è riportato server/server.js, a partire da quanto generato da slc loopback, con le modifiche marcate:
var loopback = require('loopback'); var boot = require('loopback-boot'); var app = module.exports = loopback(); app.start = function() { // start the web server return app.listen(function() { app.emit('started'); var baseUrl = app.get('url').replace(/\/$/, ''); console.log('Web server listening at: %s', baseUrl); if (app.get('loopback-component-explorer')) { var explorerPath = app.get('loopback-component-explorer').mountPath; console.log('Browse your REST API at %s%s', baseUrl, explorerPath); } }); }; // start of first change app.use(loopback.cookieParser('secret')); // end of first change // Bootstrap the application, configure models, datasources and middleware. // Sub-apps like REST API are mounted via boot scripts. boot(app, __dirname, function(err) { if (err) throw err; // start of second change app.remotes().after('Person.login', function (ctx, next) { if (ctx.result.id) { var opts = {signed: true}; if (ctx.req.body.rememberme !== false) { opts.maxAge = 1209600000; } ctx.res.cookie('authorization', ctx.result.id, opts); } next(); }); app.remotes().after('Person.logout', function (ctx, next) { ctx.res.cookie('authorization', ''); next(); }); // end of second change // start the server if `$ node server.js` if (require.main === module) app.start(); });
La prima modifica configura il parser dei cookie in modo che utilizzi "segreto" come segreto di firma dei cookie, abilitando così i cookie firmati. Devi farlo perché sebbene LoopBack cerchi un token di accesso in uno dei cookie 'authorization' o 'access_token', richiede che tale cookie sia firmato. In realtà, questo requisito è inutile. La firma di un cookie ha lo scopo di garantire che il cookie non sia stato modificato. Ma non c'è pericolo che tu modifichi il token di accesso. Dopotutto, avresti potuto inviare il token di accesso in forma non firmata, come un normale parametro. Pertanto, non devi preoccuparti che il segreto della firma del cookie sia difficile da indovinare, a meno che tu non stia utilizzando i cookie firmati per qualcos'altro.
La seconda modifica imposta un po' di post-elaborazione per i metodi Person.login e Person.logout. Per Person.login , vuoi prendere il token di accesso risultante e inviarlo al client anche come "autorizzazione" del cookie firmata. Il client può aggiungere un'altra proprietà al parametro credenziali, ricorda, indicando se rendere il cookie persistente per 2 settimane. L'impostazione predefinita è true. Il metodo di accesso stesso ignorerà questa proprietà, ma il postprocessore la verificherà.
Per Person.logout , si desidera cancellare questo cookie.
Puoi vedere subito i risultati di queste modifiche in StrongLoop API Explorer. Normalmente dopo una richiesta Person.login, dovresti copiare il token di accesso, incollarlo nel modulo in alto a destra e fare clic su Imposta token di accesso. Ma con questi cambiamenti, non devi fare nulla di tutto ciò. Il token di accesso viene automaticamente salvato come 'autorizzazione' del cookie e rinviato ad ogni richiesta successiva. Quando Explorer visualizza le intestazioni di risposta da Person.login, omette il cookie, perché JavaScript non è mai autorizzato a vedere le intestazioni di Set-Cookie. Ma stai tranquillo, il biscotto è lì.
Sul lato client, al ricaricamento della pagina vedresti se esiste l'"autorizzazione" del cookie. In tal caso, è necessario aggiornare il record dell'ID utente corrente. Probabilmente il modo più semplice per farlo è memorizzare l'ID utente in un cookie separato in caso di accesso riuscito, in modo da poterlo recuperare al ricaricamento della pagina.
L'identificatore del frammento
Mentre sto visitando un sito Web che è stato implementato come SPA, l'URL nella barra degli indirizzi del mio browser potrebbe assomigliare a "https://example.com/#/my-photos/37". La parte dell'identificatore del frammento di questo, "#/my-photos/37", è già una raccolta di informazioni sullo stato che potrebbero essere visualizzate come dati di sessione. In questo caso, probabilmente sto visualizzando una delle mie foto, quella il cui ID è 37.
Puoi decidere di incorporare altri dati di sessione all'interno dell'identificatore del frammento. Ricordiamo che nella sezione precedente, con il token di accesso memorizzato nell''autorizzazione' del cookie, era comunque necessario tenere traccia dello userId in qualche modo. Un'opzione è salvarlo in un cookie separato. Ma un altro approccio consiste nell'incorporarlo nell'identificatore del frammento. Potresti decidere che mentre sono connesso, tutte le pagine che visito avranno un identificatore di frammento che inizia con "#/u/XXX", dove XXX è l'ID utente. Quindi nell'esempio precedente, l'identificatore del frammento potrebbe essere "#/u/59/my-photos/37" se il mio userId è 59.
In teoria, potresti incorporare il token di accesso stesso nell'identificatore del frammento, evitando qualsiasi necessità di cookie o archiviazione web. Ma sarebbe una cattiva idea. Il mio token di accesso sarebbe quindi visibile nella barra degli indirizzi. Chiunque guardi alle mie spalle con una fotocamera potrebbe scattare un'istantanea dello schermo, ottenendo così l'accesso al mio account.

Un'ultima nota: è possibile configurare una SPA in modo che non utilizzi affatto identificatori di frammento. Utilizza invece URL ordinari come "http://example.com/app/dashboard" e "http://example.com/app/my-photos/37", con il server configurato per restituire l'HTML di livello superiore per il tuo SPA in risposta a una richiesta per uno qualsiasi di questi URL. La tua SPA esegue quindi il suo routing in base al percorso (ad es. "/app/dashboard" o "/app/my-photos/37") anziché all'identificatore del frammento. Intercetta i clic sui collegamenti di navigazione e utilizza History.pushState() per inviare il nuovo URL, quindi procede con l'instradamento come al solito. Ascolta anche gli eventi popstate per rilevare l'utente che fa clic sul pulsante Indietro e procede nuovamente con il routing sull'URL ripristinato. I dettagli completi su come implementarlo esulano dallo scopo di questo articolo. Ma se usi questa tecnica, ovviamente puoi memorizzare i dati della sessione nel percorso invece dell'identificatore del frammento.
Archiviazione Web
L'archiviazione Web è un meccanismo con cui JavaScript memorizza i dati all'interno del browser. Come i cookie, l'archiviazione web è separata per ciascuna origine. Ogni elemento memorizzato ha un nome e un valore, entrambi stringhe. Ma l'archiviazione web è completamente invisibile al server e offre una capacità di archiviazione molto maggiore rispetto ai cookie. Esistono due tipi di archiviazione Web: archiviazione locale e archiviazione di sessione.
Un elemento della memoria locale è visibile in tutte le schede di tutte le finestre e persiste anche dopo la chiusura del browser. A questo proposito, si comporta in qualche modo come un cookie con una data di scadenza molto lontana nel futuro. Pertanto, è adatto per memorizzare un token di accesso nel caso in cui l'utente abbia selezionato "mantienimi connesso" sul modulo di accesso.
Un elemento di archiviazione della sessione è visibile solo all'interno della scheda in cui è stato creato e scompare quando tale scheda viene chiusa. Questo rende la sua durata molto diversa da quella di qualsiasi cookie. Ricordiamo che un cookie di sessione è ancora visibile in tutte le schede di tutte le finestre.
Se utilizzi AngularJS SDK per LoopBack, il lato client utilizzerà automaticamente l'archiviazione Web per salvare sia il token di accesso che l'ID utente. Ciò accade nel servizio LoopBackAuth in js/services/lb-services.js. Utilizzerà l'archiviazione locale, a meno che il parametro RememberMe non sia false (normalmente significa che la casella di controllo "mantienimi connesso" era deselezionata), nel qual caso utilizzerà l'archiviazione della sessione.
Il risultato è che se accedo con "mantienimi connesso" deselezionato e poi apro un collegamento in una nuova scheda o finestra, non verrò registrato lì. Molto probabilmente vedrò la schermata di accesso. Puoi decidere tu stesso se questo è un comportamento accettabile. Alcuni potrebbero considerarla una bella funzionalità, in cui puoi avere diverse schede, ognuna connessa come un utente diverso. Oppure potresti decidere che quasi nessuno usa più computer condivisi, quindi puoi semplicemente omettere del tutto la casella di controllo "mantienimi connesso".
Quindi, come sarebbe la gestione dei dati della sessione se decidessi di utilizzare AngularJS SDK per LoopBack? Supponiamo di avere la stessa situazione di prima sul lato server: hai definito un modello Persona, estendendo il modello Utente e hai esposto il modello Persona su REST. Non utilizzerai i cookie, quindi non avrai bisogno di nessuna delle modifiche descritte in precedenza.
Sul lato client, da qualche parte nel tuo controller più esterno, probabilmente hai una variabile come $scope.currentUserId che contiene l'ID utente dell'utente attualmente connesso, o null se l'utente non ha effettuato l'accesso. Quindi, per gestire correttamente i ricaricamenti delle pagine, devi includi semplicemente questa istruzione nella funzione di costruzione per quel controller:
$scope.currentUserId = Person.getCurrentId();
È così facile. Aggiungi "Persona" come dipendenza del tuo controller, se non lo è già.
IndicizzatoDB
IndexedDB è una struttura più recente per la memorizzazione di grandi quantità di dati nel browser. Puoi usarlo per archiviare dati di qualsiasi tipo JavaScript, come un oggetto o una matrice, senza doverlo serializzare. Tutte le richieste sul database sono asincrone, quindi ricevi una richiamata al completamento della richiesta.
È possibile utilizzare IndexedDB per archiviare dati strutturati non correlati a dati sul server. Un esempio potrebbe essere un calendario, un elenco di cose da fare o partite salvate giocate localmente. In questo caso, l'applicazione è davvero locale e il tuo sito web è solo il veicolo per distribuirla.
Al momento, Internet Explorer e Safari supportano solo parzialmente IndexedDB. Altri browser principali lo supportano completamente. Una seria limitazione al momento, tuttavia, è che Firefox disabilita IndexedDB interamente in modalità di navigazione privata.
Come esempio concreto dell'utilizzo di IndexedDB, prendiamo l'applicazione puzzle scorrevole di Pavol Daniš e la modifichiamo per salvare lo stato del primo puzzle, il puzzle scorrevole 3x3 Basic basato sul logo AngularJS, dopo ogni mossa. Ricaricare la pagina ripristinerà quindi lo stato di questo primo puzzle.
Ho impostato un fork del repository con queste modifiche, tutte in app/js/puzzle/slidingPuzzle.js. Come puoi vedere, anche un utilizzo rudimentale di IndexedDB è piuttosto complicato. Mostrerò solo i punti salienti di seguito. Innanzitutto, la funzione di ripristino viene chiamata durante il caricamento della pagina, per aprire il database IndexedDB:
/* * Tries to restore game */ this.restore = function(scope, storekey) { this.storekey = storekey; if (this.db) { this.restore2(scope); } else if (!window.indexedDB) { console.log('SlidingPuzzle: browser does not support indexedDB'); this.shuffle(); } else { var self = this; var request = window.indexedDB.open('SlidingPuzzleDatabase'); request.onerror = function(event) { console.log('SlidingPuzzle: error opening database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onupgradeneeded = function(event) { event.target.result.createObjectStore('SlidingPuzzleStore'); }; request.onsuccess = function(event) { self.db = event.target.result; self.restore2(scope); }; } };
L'evento request.onupgradeneeded gestisce il caso in cui il database non esiste ancora. In questo caso, creiamo l'object store.
Una volta aperto il database, viene chiamata la funzione restore2 , che cerca un record con una determinata chiave (che in questo caso sarà la costante 'Base'):
/* * Tries to restore game, once database has been opened */ this.restore2 = function(scope) { var transaction = this.db.transaction('SlidingPuzzleStore'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var self = this; var request = objectStore.get(this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error reading from database, ' + request.error.name); scope.$apply(function() { self.shuffle(); }); }; request.onsuccess = function(event) { if (!request.result) { console.log('SlidingPuzzle: no saved game for ' + self.storekey); scope.$apply(function() { self.shuffle(); }); } else { scope.$apply(function() { self.grid = request.result; }); } }; }
Se tale record esiste, il suo valore sostituisce la matrice della griglia del puzzle. Se si verifica un errore nel ripristino del gioco, mescoliamo semplicemente le tessere come prima. Si noti che la griglia è una matrice 3x3 di oggetti tile, ognuno dei quali è abbastanza complesso. Il grande vantaggio di IndexedDB è che puoi archiviare e recuperare tali valori senza doverli serializzare.
Usiamo $apply per informare AngularJS che il modello è stato modificato, quindi la vista verrà aggiornata in modo appropriato. Questo perché l'aggiornamento sta avvenendo all'interno di un gestore di eventi DOM, quindi AngularJS non sarebbe altrimenti in grado di rilevare la modifica. Qualsiasi applicazione AngularJS che utilizza IndexedDB dovrà probabilmente utilizzare $apply per questo motivo.
Dopo qualsiasi azione che modificherebbe l'array della griglia, come uno spostamento da parte dell'utente, viene chiamata la funzione save che aggiunge o aggiorna il record con la chiave appropriata, in base al valore della griglia aggiornato:
/* * Tries to save game */ this.save = function() { if (!this.db) { return; } var transaction = this.db.transaction('SlidingPuzzleStore', 'readwrite'); var objectStore = transaction.objectStore('SlidingPuzzleStore'); var request = objectStore.put(this.grid, this.storekey); request.onerror = function(event) { console.log('SlidingPuzzle: error writing to database, ' + request.error.name); }; request.onsuccess = function(event) { // successful, no further action needed }; }
Le restanti modifiche sono di chiamare le funzioni di cui sopra in momenti appropriati. Puoi rivedere il commit mostrando tutte le modifiche. Nota che stiamo chiamando ripristino solo per il puzzle di base, non per i tre puzzle avanzati. Sfruttiamo il fatto che i tre puzzle avanzati hanno un attributo api, quindi per quelli ci limitiamo a mescolare il normale.
E se volessimo salvare e ripristinare anche i puzzle avanzati? Ciò richiederebbe una ristrutturazione. In ciascuno dei puzzle avanzati, l'utente può regolare il file sorgente dell'immagine e le dimensioni del puzzle. Quindi dovremmo migliorare il valore memorizzato in IndexedDB per includere queste informazioni. Ancora più importante, avremmo bisogno di un modo per aggiornarli da un ripristino. È un po' troppo per questo esempio già lungo.
Conclusione
Nella maggior parte dei casi, l'archiviazione web è la soluzione migliore per archiviare i dati della sessione. È completamente supportato da tutti i principali browser e offre una capacità di archiviazione molto maggiore rispetto ai cookie.
Utilizzeresti i cookie se il tuo server è già configurato per utilizzarli o se hai bisogno che i dati siano accessibili in tutte le schede di tutte le finestre, ma vuoi anche assicurarti che vengano eliminati alla chiusura del browser.
Utilizzi già l'identificatore del frammento per archiviare i dati della sessione specifici per quella pagina, come l'ID della foto che l'utente sta guardando. Sebbene sia possibile incorporare altri dati di sessione nell'identificatore del frammento, ciò non offre alcun vantaggio rispetto all'archiviazione Web o ai cookie.
È probabile che l'uso di IndexedDB richieda molta più codifica rispetto a qualsiasi altra tecnica. Ma se i valori che stai archiviando sono oggetti JavaScript complessi che sarebbero difficili da serializzare o se hai bisogno di un modello transazionale, allora potrebbe essere utile.