Utrzymujące się dane na wszystkich stronach ponownie ładowanych: pliki cookie, indeksowana baza danych i wszystko pomiędzy

Opublikowany: 2022-03-11

Załóżmy, że odwiedzam witrynę internetową. Klikam prawym przyciskiem myszy jedno z łączy nawigacyjnych i wybieram otwarcie łącza w nowym oknie. Co powinno się stać? Jeśli jestem jak większość użytkowników, oczekuję, że nowa strona będzie miała taką samą treść, jak gdybym bezpośrednio kliknął link. Jedyną różnicą powinno być to, że strona pojawia się w nowym oknie. Ale jeśli Twoja witryna internetowa jest aplikacją jednostronicową (SPA), możesz zobaczyć dziwne wyniki, chyba że dokładnie zaplanowałeś taki przypadek.

Przypomnijmy, że w SPA typowym łączem nawigacyjnym jest często identyfikator fragmentu, rozpoczynający się od znaku hash (#). Bezpośrednie kliknięcie linku nie powoduje przeładowania strony, więc wszystkie dane przechowywane w zmiennych JavaScript zostają zachowane. Ale jeśli otworzę link w nowej karcie lub oknie, przeglądarka ponownie załaduje stronę, ponownie inicjując wszystkie zmienne JavaScript. Tak więc wszystkie elementy HTML powiązane z tymi zmiennymi będą wyświetlane inaczej, chyba że podjąłeś kroki, aby w jakiś sposób zachować te dane.

Utrzymujące się dane na wszystkich stronach ponownie ładowanych: pliki cookie, indeksowana baza danych i wszystko pomiędzy

Utrzymujące się dane na wszystkich stronach ponownie ładowanych: pliki cookie, indeksowana baza danych i wszystko pomiędzy
Ćwierkać

Podobny problem pojawia się, jeśli jawnie przeładuję stronę, na przykład przez naciśnięcie F5. Możesz pomyśleć, że nigdy nie powinienem naciskać F5, ponieważ masz skonfigurowany mechanizm automatycznego wypychania zmian z serwera. Ale jeśli jestem typowym użytkownikiem, możesz się założyć, że nadal załaduję stronę. Może wygląda na to, że moja przeglądarka nieprawidłowo przemalowała ekran, albo po prostu chcę mieć pewność, że mam najnowsze notowania giełdowe.

Interfejsy API mogą być bezstanowe, a interakcja między ludźmi nie

W przeciwieństwie do żądania wewnętrznego za pośrednictwem interfejsu API RESTful, interakcja użytkownika z witryną internetową nie jest bezstanowa. Jako internauta myślę o mojej wizycie w Twojej witrynie jak o sesji, prawie jak o rozmowie telefonicznej. Oczekuję, że przeglądarka zapamięta dane o mojej sesji, w taki sam sposób, w jaki dzwonię do Twojej linii sprzedaży lub wsparcia, oczekuję, że przedstawiciel zapamięta to, co zostało powiedziane wcześniej w rozmowie.

Oczywistym przykładem danych sesji jest to, czy jestem zalogowany, a jeśli tak, to jaki użytkownik. Po przejściu przez ekran logowania powinienem móc swobodnie poruszać się po stronach witryny dla poszczególnych użytkowników. Jeśli otworzę link w nowej karcie lub oknie i pojawi się inny ekran logowania, nie jest to zbyt przyjazne dla użytkownika.

Innym przykładem jest zawartość koszyka w witrynie e-commerce. Jeśli wciśnięcie F5 opróżni koszyk, użytkownicy mogą się zdenerwować.

W tradycyjnej wielostronicowej aplikacji napisanej w PHP dane sesji byłyby przechowywane w superglobalnej tablicy $_SESSION. Ale w SPA musi być gdzieś po stronie klienta. Istnieją cztery główne opcje przechowywania danych sesji w SPA:

  • Ciasteczka
  • Identyfikator fragmentu
  • Przechowywanie w sieci
  • Indeksowana baza danych

Cztery kilobajty plików cookie

Pliki cookie to starsza forma przechowywania stron internetowych w przeglądarce. Pierwotnie były przeznaczone do przechowywania danych otrzymanych z serwera w jednym żądaniu i odsyłania ich z powrotem do serwera w kolejnych żądaniach. Ale z JavaScript, możesz używać plików cookie do przechowywania prawie każdego rodzaju danych, do limitu rozmiaru 4 KB na plik cookie. AngularJS oferuje moduł ngCookies do zarządzania plikami cookie. Istnieje również pakiet js-cookies, który zapewnia podobną funkcjonalność w dowolnym frameworku.

Pamiętaj, że każdy plik cookie, który utworzysz, zostanie wysłany na serwer przy każdym żądaniu, niezależnie od tego, czy jest to przeładowanie strony, czy żądanie Ajax. Ale jeśli główne dane sesji, które musisz przechowywać, to token dostępu dla zalogowanego użytkownika, i tak chcesz, aby były one wysyłane do serwera przy każdym żądaniu. To naturalne, że spróbujesz użyć tej automatycznej transmisji plików cookie jako standardowego sposobu określania tokena dostępu dla żądań Ajax.

Możesz argumentować, że używanie plików cookie w ten sposób jest niezgodne z architekturą RESTful. Ale w tym przypadku jest to w porządku, ponieważ każde żądanie za pośrednictwem interfejsu API jest nadal bezstanowe, ma pewne dane wejściowe i niektóre wyjścia. Tyle, że jeden z wejść jest wysyłany w zabawny sposób, za pośrednictwem pliku cookie. Jeśli możesz zorganizować żądanie logowania API, aby wysłać token dostępu również z powrotem w pliku cookie, to twój kod po stronie klienta prawie wcale nie musi zajmować się plikami cookie. Ponownie, jest to po prostu kolejne wyjście z żądania zwracanego w nietypowy sposób.

Pliki cookie oferują jedną przewagę nad przechowywaniem w sieci. Możesz zaznaczyć pole wyboru „nie wylogowuj mnie” w formularzu logowania. Jeśli chodzi o semantykę, spodziewam się, że jeśli zostawię to niezaznaczone, pozostanę zalogowany, jeśli przeładuję stronę lub otworzę link w nowej karcie lub oknie, ale mam gwarancję, że wyloguję się po zamknięciu przeglądarki. Jest to ważna funkcja bezpieczeństwa, jeśli korzystam ze współdzielonego komputera. Jak zobaczymy później, przechowywanie w sieci WWW nie obsługuje tego zachowania.

Jak więc takie podejście może działać w praktyce? Załóżmy, że używasz LoopBack po stronie serwera. Zdefiniowano model Person, rozszerzając wbudowany model User, dodając właściwości, które chcesz zachować dla każdego użytkownika. Skonfigurowałeś model Person tak, aby był udostępniany przez REST. Teraz musisz dostosować plik server/server.js, aby osiągnąć pożądane zachowanie plików cookie. Poniżej znajduje się server/server.js, zaczynając od tego, co zostało wygenerowane przez slc loopback, z zaznaczonymi zmianami:

 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(); });

Pierwsza zmiana konfiguruje analizator plików cookie tak, aby używał „tajnego” jako klucza tajnego podpisywania plików cookie, umożliwiając w ten sposób podpisane pliki cookie. Musisz to zrobić, ponieważ chociaż LoopBack szuka tokena dostępu w jednym z plików cookie „autoryzacja” lub „access_token”, wymaga podpisania takiego pliku cookie. Właściwie ten wymóg jest bezcelowy. Podpisanie pliku cookie ma na celu zapewnienie, że plik cookie nie został zmodyfikowany. Ale nie ma niebezpieczeństwa, że ​​zmodyfikujesz token dostępu. W końcu mogłeś wysłać token dostępu w postaci niepodpisanej, jako zwykły parametr. Dzięki temu nie musisz się martwić, że sekret podpisywania plików cookie będzie trudny do odgadnięcia, chyba że używasz podpisanych plików cookie do czegoś innego.

Druga zmiana konfiguruje przetwarzanie końcowe dla metod Person.login i Person.logout. W przypadku Person.login chcesz pobrać otrzymany token dostępu i wysłać go również do klienta jako podpisany plik cookie „autoryzacja”. Klient może dodać jeszcze jedną właściwość do parametru poświadczeń, zapamiętaj, wskazując, czy plik cookie ma być trwały przez 2 tygodnie. Wartość domyślna to prawda. Sama metoda logowania zignoruje tę właściwość, ale postprocesor ją sprawdzi.

W przypadku Person.logout chcesz wyczyścić ten plik cookie.

Wyniki tych zmian można zobaczyć od razu w Eksploratorze interfejsu API StrongLoop. Zwykle po żądaniu Person.login musiałbyś skopiować token dostępu, wkleić go do formularza w prawym górnym rogu i kliknąć Ustaw token dostępu. Ale dzięki tym zmianom nie musisz tego robić. Token dostępu jest automatycznie zapisywany jako „autoryzacja” pliku cookie i odsyłany przy każdym kolejnym żądaniu. Gdy Eksplorator wyświetla nagłówki odpowiedzi z Person.login, pomija plik cookie, ponieważ JavaScript nigdy nie może zobaczyć nagłówków Set-Cookie. Zapewniamy jednak, że ciasteczko tam jest.

Po stronie klienta, przy przeładowaniu strony, zobaczysz, czy istnieje „autoryzacja” pliku cookie. Jeśli tak, musisz zaktualizować swój rekord bieżącego identyfikatora użytkownika. Prawdopodobnie najłatwiejszym sposobem na to jest przechowywanie identyfikatora użytkownika w osobnym pliku cookie po pomyślnym zalogowaniu, dzięki czemu można go odzyskać podczas ponownego ładowania strony.

Identyfikator fragmentu

Gdy odwiedzam witrynę internetową, która została zaimplementowana jako SPA, adres URL w pasku adresu mojej przeglądarki może wyglądać mniej więcej tak: „https://example.com/#/my-photos/37”. Część tego identyfikatora fragmentu, „#/my-photos/37”, jest już zbiorem informacji o stanie, które można przeglądać jako dane sesji. W tym przypadku prawdopodobnie oglądam jedno z moich zdjęć, to o ID 37.

Możesz zdecydować się na osadzenie innych danych sesji w identyfikatorze fragmentu. Przypomnijmy, że w poprzedniej sekcji, z tokenem dostępu przechowywanym w „autoryzacji” pliku cookie, nadal trzeba było jakoś śledzić identyfikator użytkownika. Jedną z opcji jest zapisanie go w osobnym pliku cookie. Ale innym podejściem jest osadzenie go w identyfikatorze fragmentu. Możesz zdecydować, że gdy jestem zalogowany, wszystkie odwiedzane przeze mnie strony będą miały identyfikator fragmentu zaczynający się od „#/u/XXX”, gdzie XXX to identyfikator użytkownika. W poprzednim przykładzie identyfikatorem fragmentu może być „#/u/59/moje-zdjęcia/37”, jeśli mój identyfikator użytkownika to 59.

Teoretycznie możesz osadzić sam token dostępu w identyfikatorze fragmentu, unikając potrzeby korzystania z plików cookie lub przechowywania w sieci. Ale to byłby zły pomysł. Mój token dostępu byłby wtedy widoczny na pasku adresu. Każdy, kto patrzył mi przez ramię z aparatem, mógł zrobić zrzut ekranu, uzyskując w ten sposób dostęp do mojego konta.

Ostatnia uwaga: możliwe jest skonfigurowanie SPA tak, aby w ogóle nie używało identyfikatorów fragmentów. Zamiast tego używa zwykłych adresów URL, takich jak „http://example.com/app/dashboard” i „http://example.com/app/my-photos/37”, z serwerem skonfigurowanym tak, aby zwracał kod HTML najwyższego poziomu dla Twojego SPA w odpowiedzi na żądanie dowolnego z tych adresów URL. Twój SPA następnie wykonuje swój routing na podstawie ścieżki (np. „/app/dashboard” lub „/app/my-photos/37”) zamiast identyfikatora fragmentu. Przechwytuje kliknięcia linków nawigacyjnych i używa History.pushState() do wysłania nowego adresu URL, a następnie kontynuuje routing jak zwykle. Nasłuchuje również zdarzeń popstate, aby wykryć użytkownika klikającego przycisk Wstecz i ponownie kontynuuje routing na przywróconym adresie URL. Pełne informacje o tym, jak to zaimplementować, wykraczają poza zakres tego artykułu. Ale jeśli użyjesz tej techniki, to oczywiście możesz przechowywać dane sesji w ścieżce zamiast identyfikatora fragmentu.

Pamięć internetowa

Magazyn sieciowy to mechanizm JavaScript do przechowywania danych w przeglądarce. Podobnie jak pliki cookie, przechowywanie w sieci jest oddzielne dla każdego źródła. Każdy przechowywany element ma nazwę i wartość, które są ciągami. Ale przechowywanie w sieci jest całkowicie niewidoczne dla serwera i oferuje znacznie większą pojemność niż pliki cookie. Istnieją dwa typy przechowywania danych w sieci WWW: pamięć lokalna i pamięć sesji.

Element pamięci lokalnej jest widoczny na wszystkich kartach wszystkich okien i pozostaje nawet po zamknięciu przeglądarki. Pod tym względem zachowuje się trochę jak plik cookie z datą ważności bardzo odległą w przyszłości. Dzięki temu nadaje się do przechowywania tokena dostępu w przypadku, gdy użytkownik zaznaczył „nie wylogowuj mnie” w formularzu logowania.

Element przechowywania sesji jest widoczny tylko na karcie, na której został utworzony, i znika po zamknięciu tej karty. To sprawia, że ​​jego żywotność bardzo różni się od czasu życia każdego pliku cookie. Przypomnij sobie, że plik cookie sesji jest nadal widoczny na wszystkich kartach wszystkich okien.

Jeśli używasz zestawu SDK AngularJS dla LoopBack, strona klienta automatycznie użyje magazynu internetowego do zapisania zarówno tokenu dostępu, jak i identyfikatora użytkownika. Dzieje się tak w usłudze LoopBackAuth w js/services/lb-services.js. Będzie korzystać z pamięci lokalnej, chyba że parametr RememberMe ma wartość false (zwykle oznacza to, że pole wyboru „nie wylogowuj mnie” było niezaznaczone), w którym to przypadku użyje pamięci sesji.

Powoduje to, że jeśli zaloguję się z odznaczoną opcją „nie wylogowuj mnie”, a następnie otworzę link w nowej karcie lub oknie, nie będę tam zalogowany. Najprawdopodobniej zobaczę ekran logowania. Możesz sam zdecydować, czy jest to akceptowalne zachowanie. Niektórzy mogą uznać to za fajną funkcję, w której możesz mieć kilka zakładek, z których każda jest zalogowana jako inny użytkownik. Możesz też zdecydować, że prawie nikt nie używa już współdzielonych komputerów, więc możesz po prostu całkowicie pominąć pole wyboru „nie wylogowuj mnie”.

Jak więc wyglądałaby obsługa danych sesji, jeśli zdecydujesz się skorzystać z AngularJS SDK dla LoopBack? Załóżmy, że po stronie serwera jest taka sama sytuacja, jak poprzednio: zdefiniowałeś model Person, rozszerzając model User, i udostępniłeś model Person przez REST. Nie będziesz używać plików cookie, więc nie będziesz potrzebować żadnych zmian opisanych wcześniej.

Po stronie klienta, gdzieś w zewnętrznym kontrolerze, prawdopodobnie masz zmienną taką jak $scope.currentUserId, która przechowuje identyfikator użytkownika aktualnie zalogowanego użytkownika, lub null, jeśli użytkownik nie jest zalogowany. po prostu umieść tę instrukcję w funkcji konstruktora dla tego kontrolera:

 $scope.currentUserId = Person.getCurrentId();

To jest takie proste. Dodaj „Person” jako zależność kontrolera, jeśli jeszcze nie jest.

Indeksowana baza danych

IndexedDB to nowsza funkcja do przechowywania dużych ilości danych w przeglądarce. Możesz go używać do przechowywania danych dowolnego typu JavaScript, takiego jak obiekt lub tablica, bez konieczności ich serializacji. Wszystkie żądania skierowane do bazy danych są asynchroniczne, więc po zakończeniu żądania otrzymasz wywołanie zwrotne.

Możesz użyć IndexedDB do przechowywania uporządkowanych danych, które nie są powiązane z żadnymi danymi na serwerze. Przykładem może być kalendarz, lista rzeczy do zrobienia lub zapisane gry, które są odtwarzane lokalnie. W tym przypadku aplikacja jest tak naprawdę lokalna, a Twoja witryna internetowa jest tylko narzędziem do jej dostarczania.

Obecnie Internet Explorer i Safari mają tylko częściową obsługę IndexedDB. Inne główne przeglądarki w pełni go obsługują. Jednym z poważnych ograniczeń w tej chwili jest jednak to, że Firefox całkowicie wyłącza IndexedDB w trybie przeglądania prywatnego.

Jako konkretny przykład użycia IndexedDB, weźmy aplikację łamigłówek przesuwnych autorstwa Pavola Daniša i dostosujmy ją, aby po każdym ruchu zapisać stan pierwszej łamigłówki, podstawowej łamigłówki przesuwnej 3x3 opartej na logo AngularJS. Ponowne załadowanie strony przywróci stan pierwszej układanki.

Skonfigurowałem rozwidlenie repozytorium z tymi zmianami, z których wszystkie znajdują się w app/js/puzzle/slidingPuzzle.js. Jak widać, nawet podstawowe użycie IndexedDB jest dość skomplikowane. Poniżej pokażę tylko najważniejsze informacje. Po pierwsze, funkcja restore jest wywoływana podczas ładowania strony, aby otworzyć bazę 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); }; } };

Zdarzenie request.onupgradeneeded obsługuje przypadek, w którym baza danych jeszcze nie istnieje. W tym przypadku tworzymy składnicę obiektów.

Po otwarciu bazy danych wywoływana jest funkcja restore2 , która szuka rekordu z podanym kluczem (którym właściwie będzie stała 'Basic' w tym przypadku):

 /* * 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; }); } }; }

Jeśli taki rekord istnieje, jego wartość zastępuje tablicę siatki układanki. Jeśli wystąpi błąd podczas przywracania gry, po prostu tasujemy kafelki jak poprzednio. Zauważ, że siatka to tablica 3x3 obiektów kafelkowych, z których każdy jest dość złożony. Wielką zaletą IndexedDB jest to, że możesz przechowywać i pobierać takie wartości bez konieczności ich serializacji.

Używamy $apply , aby poinformować AngularJS, że model został zmieniony, więc widok zostanie odpowiednio zaktualizowany. Dzieje się tak, ponieważ aktualizacja odbywa się w module obsługi zdarzeń DOM, więc AngularJS nie byłby w stanie wykryć zmiany. Z tego powodu każda aplikacja AngularJS korzystająca z IndexedDB prawdopodobnie będzie musiała użyć $apply.

Po każdej akcji, która zmieniłaby tablicę siatki, np. ruchu przez użytkownika, wywoływana jest funkcja save, która dodaje lub aktualizuje rekord o odpowiedni klucz, na podstawie zaktualizowanej wartości siatki:

 /* * 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 }; }

Pozostałe zmiany mają wywołać powyższe funkcje w odpowiednich momentach. Możesz przejrzeć zatwierdzenie, pokazując wszystkie zmiany. Pamiętaj, że przywracamy tylko podstawową łamigłówkę, a nie trzy zaawansowane łamigłówki. Wykorzystujemy fakt, że trzy zaawansowane łamigłówki mają atrybut api, więc dla nich po prostu wykonujemy normalne tasowanie.

Co by było, gdybyśmy chcieli również zapisać i przywrócić zaawansowane łamigłówki? To wymagałoby pewnej restrukturyzacji. W każdej z zaawansowanych łamigłówek użytkownik może dostosować plik źródłowy obrazu oraz wymiary łamigłówki. Musielibyśmy więc ulepszyć wartość przechowywaną w IndexedDB, aby zawierała te informacje. Co ważniejsze, potrzebowalibyśmy sposobu na zaktualizowanie ich po przywróceniu. To trochę za dużo jak na ten i tak długi przykład.

Wniosek

W większości przypadków przechowywanie danych w sieci jest najlepszym sposobem na przechowywanie danych sesji. Jest w pełni obsługiwany przez wszystkie główne przeglądarki i oferuje znacznie większą pojemność niż pliki cookie.

Używasz plików cookie, jeśli Twój serwer jest już skonfigurowany do ich używania lub jeśli chcesz, aby dane były dostępne na wszystkich kartach wszystkich okien, ale chcesz również mieć pewność, że zostaną usunięte po zamknięciu przeglądarki.

Używasz już identyfikatora fragmentu do przechowywania danych sesji specyficznych dla tej strony, takich jak identyfikator zdjęcia, które ogląda użytkownik. Chociaż możesz osadzić inne dane sesji w identyfikatorze fragmentu, tak naprawdę nie oferuje to żadnej przewagi nad przechowywaniem w sieci lub plikami cookie.

Korzystanie z IndexedDB prawdopodobnie będzie wymagało znacznie więcej kodowania niż jakakolwiek inna technika. Ale jeśli wartości, które przechowujesz, są złożonymi obiektami JavaScript, które byłyby trudne do serializacji, lub jeśli potrzebujesz modelu transakcyjnego, może się to opłacać.