HTML5 Storage. Techniki zaawansowane

Książka ta jest przeznaczona dla każdego, kto chciałby poznać HTML5 Storage począwszy od podstaw a skończywszy na techni

527 51 1MB

Polish Pages [46]

Report DMCA / Copyright

DOWNLOAD FILE

Polecaj historie

HTML5 Storage. Techniki zaawansowane

Citation preview

1

Spis treści 1. Wstęp..........................................................................................................................3 2. Czym jest HTML5 Storage?.......................................................................................4 2.1. Współdzielenie instancji Storage..........................................................................5 2.2. Lokacja dokumentu a współdzielenie Storage...................................................6 3. Storage API.................................................................................................................8 3.1. Rzutowanie danych............................................................................................11 3.2. Indeksy i iteracja po wpisach..............................................................................14 3.3. Zdarzenie “storage”............................................................................................16 4. Pojemność Storage...................................................................................................20 4.1. Ilość znaków a zajmowana przestrzeń...............................................................20 4.2. Liczenie zajętej przestrzeni................................................................................21 5. Rozszerzanie Storage o nowe możliwości................................................................24 5.1. Własne API........................................................................................................24 5.1.1. Fundamenty..............................................................................................24 5.1.2. Przykład użycia........................................................................................26 5.2. Przestrzenie nazw.............................................................................................26 5.2.1. Limit pojemności w przestrzeni nazw.........................................................31 5.3. Reagowanie na zmiany w Store........................................................................36 5.3.1. Usuwanie najstarszych elementów gdy Store jest pełen.........................42 5.3.2. Przykład użycia.........................................................................................43 5.3.3. Usuwanie przeterminowanych elementów...............................................43 5.3.4. Przykład użycia.........................................................................................45 6. Referencje.................................................................................................................46

Sebastian Rosik Frontend developer w firmie RST sp. z o. o. sp. k., gdzie rozwija aplikacje typu Single Page Application dla branży logistycznej. Dawniej brał udział w projektowaniu gier i aplikacji mobilnych na platformę JAVA oraz iOS. Dziś pisze aplikacje głównie w HTML5 pod przeglądarki, a także w Node.js dla rozwiązań serwerowych. Hobbystycznie tworzy grafikę 2D oraz 3D, a w wolnych chwilach zgłębia tajniki platformy Arduino. Kontakt: [email protected]

2

Wstęp

Książka ta jest przeznaczona dla każdego, kto chciałby poznać HTML5 Storage począwszy od podstaw a skończywszy na technikach zaawansowanych. W książce tej poruszone zostały techniki, które wiele razy były wykorzystywane w praktyce, zarównvo w projektach komercyjnych, jak i darmowych. HTML5 Storage jest niezwykle prosty w budowie, lecz jego dojrzałość i dostępność w wielu przeglądarkach sprawia, że znajduje on wiele bardziej złożonych zastosowań, aniżeli zwykłe przechowywanie prostych danych tekstowych. Dowiemy się  między innymi w jaki sposób korzystać  z API HTML5 Storage, w jaki sposób zapisywane są dane, czy chociażby w jaki sposób rozszerzyć API o dodatkowe możliwości. Stopniowo poruszane będą coraz to bardziej złożone problemy, oraz ich przykładowe rozwiązania w formie prostej biblioteki będącej nakładką na oryginalne API HTML5 Storage. Poniżej znajduje się mały słownik pojęć. Każde z wymienionych słów występuje dość często w książce i czasami kryje się za nim szersze znaczenie. Warto się z nimi zapoznać, by wiedzieć z czym mamy do czynienia podczas czytania. ƒƒ HTML5 Storage - Ogólna nazwa dla API przeglądarkowego do odczytu i modyfikacji danych przetrzymywanych w jednej z dwóch instancji: localStorage bądź sessionStorage. ƒƒ Instancja Storage (lub po prostu Storage) - Obiekty localStorage oraz sessionStorage. ƒƒ Dokument (DOM Document) - Strona internetowa, bądź element iframe. ƒƒ Okno, zakładka przeglądarki, popup - Okno przeglądarki, mogące być powiązane relacją z innym oknem na zasadzie dziecko - rodzic. Posiada globalny obiekt window oraz document. ƒƒ Lokacja, adres - Lokacja, inaczej adres dokumentu (URL) stanowi wspólny mianownik dla Storage. Jeżeli kilka dokumentów posiada tę samą lokacji, to mają dostęp do tej samej przestrzeni Storage (mają możliwość modyfikacji tych samych danych w Storage). Na lokację składają się  takie elementy, jak np. host, port, protokuł,  etc. Więcej na ten temat relacji dokumentów między sobą w rozdziale “Współdzielenie instancji Storage”.

3

Czym jest HTML5 Storage?

Wszystkie przeglądarki dostępne obecnie na rynku, udostępniają API JavaScript, dzięki któremy możemy przechowywać dane, które zostaną zachowane pomiędzy przeładoniami dokumentu. Dane przechowywać możemy w jednej z dwóch instancji API Storage localStorage oraz sessionStorage. Maksymalna ilość danych, jaką możemy zapisać jest niezależna dla sessionStorage i localStorage, zatem warto podczas pisania aplikacji zastanowić się jakie dane kiedy będą nam potrzebne, gdyż w przypadku limitu 5 MiB, sumarycznie będziemy mieli do dyspozycji 10 MiB, (5 MiB dla localStorage oraz 5 MiB dla sessionStorage). Sam limit różni się pomiędzy przeglądarkami - jedna przeglądarka może posiadać limit ustawiony na 5 MiB, a inna z kolei na 50 MiB. Standard nie precyzuje jaką ilość powinny domyślnie oferować przeglądarki, ani też w jaki sposób użytkownik mógłby zmodyfikować ilość zasobów jaka ma zostać na to przeznaczona [1]. W większości przypadków przeglądarki oferują obecnie (2014 r.) pojemność dla localStorage wynoszącą około 5 MiB, lecz limit ten oczywiście może ulec zmianie, w zależności od działań podjętych przez producentów przeglądarek. W przypadku HTML5 Storage, dane dzielimy na elementy (items), które posiadają postać klucz-wartość (key-value). Zarówno klucz, jak i wartość elementu zapisywane są jako typ String. Oczywiście możemy zapisywać dane o innych wartościach, ale podczas zapisu i tak zostaną one zrzutowane do typu String. Cechy localStorage Dane zapisane w localStorage pozostają w nim tak długo, aż nie zostaną usunięte przez skrypt bądź użytkownika. Po zamknięciu okna bądź przeglądarki i ponownym jego włączeniu, dane zapisane wcześniej w localStorage, nadal będą się tam znajdować. Wszystkie dokumenty, o ile znajdują się w jednej lokacji, mają dostęp do tej samej instancji localStorage. Cechy sessionStorage Dane zapisane w sessionStorage są dostępne tylko i wyłącznie podczas danej sesji (tak długo, jak długo okno przeglądarki jest otwarte). Po zamknięciu okna, dostęp do danych może zostać utracony, chyba że użytkownik ponownie otworzy zamknięte okno (dokładnie to samo okno), o ile przeglądarka oferuje taką funkcjonalność. Nie jest to oczywiście funkcjonalność, nad którą mamy jakąkolwiek kontrolę, dlatego też z góry powinnyśmy założyć, że z chwilą zamknięcia okna utracimy wszystkie informacje zapisane w sessionStorage. Warto też nadmienić, że zgodnie z założeniami standardu, przeglądarka powinna przywrócić zawartość sessionStorage w przypadku wznowienia sesji po awaryjnym zamknięciu się przeglądarki, natomiast nie mamy gwarancji, że tak właśnie się stanie.

4

Każdy z dokumentów ma swoją własną instancję sessionStorage, to znaczy, że dane zapisane w sessionStorage w jednej zakładce są niedostępne dla drugiej, mimo iż obie znajdują się pod tą samą lokacją. W przypadku dokumentów osadzonych poprzez element IFrame sprawa wygląda jednak trochę inaczej, dokument będzie współdzielił sessionStorage z dokumentem nadrzędnym (dokumentem, który posiada osadzony IFrame), pod warunkiem posiadania tej samej lokacji.

Współdzienie instancji Storage Relacje pomiędzy dokumentami względem instancji Storage mogą być dosyć skomplikowane i być przyczyną wielu problemów, jeżeli nie są obsłużone prawidłowo. Poniżej zostały przedstawione ilustracje reprezentujące relacje pomiędzy dokumentami a instancjami localStorage oraz sessionStorage w różnych sytuacjach.

Przykład 1. Cztery okna, z czego okna A, B oraz C posiadają dokumenty pod lokacją X. Dokument w oknie D jest umieszczony pod lokacją Y. Dokumenty A, B i C mają dostęp do wspólnej instancji localStorage, natomiast dokument w oknie D posiada dostęp do zupełnie innej instancji localStorage ze względu na lokacje różną od pozostałych dokumentów.

Przykład 2. Dwa okna z dokumentami A i B, każde z nich z osadzonym poprzez element iFrame dokumentem (C oraz D). Dokument A, B oraz C posiadają dostęp do wspólnej instancji localStorage ze względu na wspólną lokacje X, natomiast dokument D, który jest osadzony w dokumencie B posiada dostęp do osobnej instancji localStorage ze względu na różną lokacje.

5

Przykład 3. Przykład dostępu do instancji sessionStorage. Mamy tutaj sześć dokumentów, A, B, C, D, E oraz F z czego dokument C jest osadzony jako element iFrame w dokumencie B, a dokument E jest oknem popup (dziecko) okna D (rodzic). Dokumenty A, B, D, F posiadają dostęp do osobnych instancji sessionStorage, bez względu na to czy mają taką samą lokacje czy nie. Wyjątkiem od reguły są dokumenty posiadające relacje dziecko - rodzic z innym dokumentem. Okna C oraz E mają dostęp do tej samej instancji sessionStorage co dokumenty nadrzędne (okna B i D), pod warunkiem, że mają taką samą lokacje jak dokument nadrzędny. Z powyższych przykładów wyłania się pewna prawidłowość. Dla localStorage wszystkie dokumenty niezależnie od tego czy są dokumentami nadrzędnymi (okno) czy podrzędnymi (iframe lub okno popup) mają dostęp do tej samej instancji localStorage pod warunkiem, że znajdują się w tej samej lokacji. Dla sessionStorage sytuacja wygląda inaczej: ƒƒ Każdy z dokumentów umieszczony w oknach ma dostęp do innej instancji sessionStorage, niezależnie od tego czy współdzielą tą samą lokacje czy też nie. ƒƒ Dokumenty w elemencie iFrame, znajdujące się w innej lokacji aniżeli dokument nadrzędny, posiadają dostęp do innej instancji sessionStorage niż dokument nadrzędny. ƒƒ Wyjątek stanowią dokumenty, będące w tej samej lokacji co dokument nadrzędny, oba dokumenty posiadają dostęp do tej samej instancji sessionStorage. Zasada ta dotyczy zarówno dokumentów osadzonych poprzez element iFrame oraz dokumentów w oknach popup.

Lokacja dokumentu a współdzielenie Storage By dwa dokumenty miały dostęp do tej samej instancji Storage, muszą znajdować się w tej samej lokacji. Jeżeli adresy obu dokumentów są zgodne pod względem portu,

6

subdomeny, domeny oraz portu, to obie lokacja są takie same, zatem oba dokumenty mają dostęp do tej samej instancji Storage. Jedynym elementem adresu, który mimo swoich różnic pozwala na współdzielenie instancji Storage jest ścieżka do dokumentu. Poniżej został zilustrowany skład adresu oraz jego części, które mają wpływ na współdzielenie instancji Storage w przypadku wystąpienia potencjalnych różnić dla każdego z elementów adresu pomiędzy dwoma dokumentami.

Dokument 1

Dokument 2

Współdzielona instancja Storage

http://example.com

https://example.com

Nie

http://example.com

http://www.example.com

Nie

http://example.com

http://foobar.com

Nie

http://example.com:80

http://example.com:8081

Nie

http://example.com

http://example.com/path

Tak

Tabela 1. Współdzielenie instancji Storage dla dokumentów o różnych wartościach podstawionych pod protokół, subdomenę, domenę oraz port i ścieżkę.

7

Storage API

API dla localStorage oraz sessionStorage jest identyczne. Jak widać poniżej, jest to bardzo proste API składające się  z zaledwie kilku metod, jednak możliwości jakie nam daje, w połączeniu z innymi funkcjonalnościami przeglądarek, są o wiele większe, niż widać to na pierwszy rzut oka. W dalszych rozdziałach zobaczymy, w jaki sposób możemy rozszerzyć API o dodatkowe funkcjonalności. Nazwa metody

Argumenty

Opis

setItem key, value

Tworzy nowy bądź modyfikuje istniejący wpis. Przyjmuje dwa argumenty, klucz (key), pod którym dany element powinien zostać zapisany oraz jego wartość (value). Metoda ta zwraca undefined po zapisie do Storage.

getItem key

Zwraca wpis w Storage o podanym kluczu. Jeżeli wpis nie zostanie odnaleziony, zostanie zwrócona wartość null.

removeItem key

Usuwa istniejący wpis o podanym kluczu w Storage. Metoda ta zawsze zwraca undefined bez względu na to czy wpis został usunięty czy też w ogóle nie istniał.

clear

key index

Czyści zawartość Storage ze wszystkich zapisanych wpisów. Tak jak część powyższych metod, zwraca undefined bez względu na to czy w Storage znajdowały się jakieś dane do usunięcia czy też nie. Zwraca nazwę klucza elementu, który jest zapisany w Storage pod danym indeksem.

Tabela 2. Metody dostępne w instancji Storage

8

Nazwa właściwości

Opis

length

Ilość elementów jaka obecnie znajduje się w Storage.

*

Dowolny ciąg znaków, będacy kluczem, pod którym wcześniej zapisaliśmy element. Jeżeli odwołamy się do jakiejkolwiek właściwości, która nie jest z góry zdefiniowaną metodą (setItem, clear itp.) ani właściwością (length) to wartość ta będzie odwoływać się do zapisanego pola o kluczu, którego nazwa jest taka sama jak nazwa właściwości. Jeżeli dany element nie występuje w danej instancji Storage, właściwość ta będzie wynosić undefined. Jest to zachowanie diametralnie różne od pobierania zapisanych danych za pomocą metody getItem, gdyż w przypadku braku elementu metoda ta zwróci wartość null.

Tabela 3. Właściwości dostępne w instancji Storage

Podczas zapisu do Storage, przeglądarka może rzucić wyjątek QuotaExceededError (lub błędem odpowiednim dla danej przeglądarki), gdy pojemność instancji Storage została przekroczona bądź użytkownik wyłączył w przeglądarce Storage. Wszelkie operacje na Storage, które się nie powiodą, nie modyfikują w żaden sposób już  istniejących danych. Zatem, jeżeli podczas aktualizacji danych o kluczu “foo” w localStorage wystąpi błąd, to istniejące dane pod tym kluczem nie zostaną zmodyfikowane. Dostęp do danych oraz ich zapis może się odbywać na dwa sposoby. Pierwszy to użycie metod setItem oraz getItem. Drugi sposób to potraktowanie instancji Storage jako zwykłego obiektu i odczytanie bądź zapisanie danych przez ustawienie odpowiedniej właściwości. localStorage.setItem( “foo”, 1 ); console.log( localStorage.getItem( “foo” ) ); // Wyświetli w konsoli: // Wynik > “1” localStorage.foo = 2; console.log( localStorage.foo ); // Wyświetli w konsoli: // Wynik > “2” console.log( localStorage.getItem( “foo” ) ); // Wyświetli w konsoli: // Wynik > “2”

Podczas pobierania wartości zapisanych elementów należy pamiętać o tym, że właściwości instancji Storage mają priorytet. Dlatego też istnieje możliwość zapisania elementu o kluczu “length” jedynie poprzez użycie metody “setItem”. W przeciwnym razie próba ustawienia właściwości “length” się nie powiedzie, odczyanie właściwości “length” da nam ilość zapisanych elementów, a nie wartość, którą wcześniej ustawiliśmy.

9

Zaleca się  zatem używanie tylko i wyłącznie metod do odczytu i zapisu danych w Storage, dzięki czemu uniknie się wielu błędów. Instancja Storage reprezentuje pewne cechy obiektu hash-mapy, tablicy oraz zwykłego interfejsu. Niesie to za sobą pewne skutki, które mogą  wprowadzić  zamieszanie do naszego kodu, jeżeli nie wiemy w jaki sposób korzystać z Storage lub nie znamy jego wszystkich możliwości i ograniczeń. Do instancji Storage możemy zapisać dane na trzy różne sposoby: // Metoda localStorage.setItem( “foo”, “bar” ); // Jako indeks/klucz localStorage[ “foo” ] = “bar”; // Jako właściwość localStorage.foo = “bar”;

Oczywiście można też usunąć dany wpis na tyle samo sposobów: localStorage.removeItem( “foo” ); delete localStorage[ “foo” ]; delete localStorage.foo;

Jako że wszystkie powyższe sposoby zapisu służą do zapisu danych do Storage, nie ma sposobu na wzbogacenie localStorage o nowe funkcjonalności (brak też prototypu, który można by rozszerzyć). Przyjrzyjmy się poniższemu przykładowi: localStorage.forEach = function( fn ) { var key; for ( var i = 0; i < this.length; ++i ) { key = this.key( i ); typeof fn === “function” && fn( this.getItem( key ), key ); } } localStorage.forEach(function( value, key ) { console.log( “key: %s, value: %s”, key, value ); }); // Wyjątek w konsoli: // Uncaught TypeError: string is not a function

Powyższa próba implementacji metody forEach znanej z tablic spełznie na niczym, gdyż na podanej funkcji zostanie wywołana metoda Function.prototype.toString, a zwrócona przez nią wartość trafi do Storage pod klucz o nazwie “forEach”. Próba wywołania funkcji forEach spowoduje jedynie wystąpienie wyjątku informującego o tym że ciąg znaków nie jest funkcją. Jedynym sposobem na rozszerzenie możliwości Storage, jest stworzenie adapteru. Więcej o tym w osobnym rozdziale “Rozszerzanie Storage o nowe możliwości”.

10

Rzutowanie danych Rzutowanie danych do ciągu znaków podczas zapisu do Storage w większości przypadków będzie się ograniczało do wywołania metody setItem na instancji Storage, niemniej jednak mogą zdarzyć się  przypadki, w których będziemy potrzebować większej kontroli nad tym w jaki sposób nasze dane są zapisywane do Storage. Proces rzutowania danych jest wbrew pozorom trochę  bardziej skomplikowany i zależy od typu naszej danej, oraz od tego po czym ona dziedziczy. Poniższa tabela, przedstawia wartości jakie są zapisywane w JavaScript do localStorage oraz wartość jaka zostanie odczytana w postaci ciągu znaków. Wartość zapisywana

Wartość odczytana

”value”

”value”

1

”1”

Number.MAX_VALUE

”1.7976931348623157e+308”

null

”null”

undefined

”undefined”

{test:1}

”[object Object]”

[0,1,2]

”0,1,2”

function(){ alert(’test’); /* comment */ }

”function(){ alert(’test’); /* comment */ }”

Tabela 4. Rzutowanie danych różnego typu podczas zapisu do localStorage.

Jak widać w Tabeli 1. rzutowanie wygląda dość intuicyjnie, aż do momentu w którym nie będziemy chcieli zapisać w Storage całego obiektu. Każdy obiekt (instancja Object) będzie zapisany jako ciąg znaków “[object Object]” (dokładna wartość może, ale nie musi różnić  się  pomiędzy przeglądarkami), dzieje się tak dlatego iż  przeglądarka podczas zapisu do Storage, próbuje wywołać metodę toString na obiekcie, który chcemy zapisać. Jeżeli metoda toString istnieje to wartość przez nią zwrócona (niezależnie od typu) zostanie zapisana do Storage. Literały obiektowe w JavaScript są dość dziwnym tworem pod względem dziedziczenia metody toString (oraz innych metod). Skupmy się  na literale obiektowym dla liczb - literał ten posiada co prawda metodę toString, lecz już nie prototype. Literały jednak zachowują pewną  cechę dzieczenia, zupełnie jakby zostały utworzone poprzez konstruktor im odpowiadający, otóż gdy usuniemy funkcję toString z Number.prototype to każda liczba podczas użycia metody toString odwoła się do prototypu klasy bazowej Object.

11

Usunięcie metody toString z Number.prototype co prawda wpływa na to skąd litrał bierze swoją metodę toString, jednak w żaden sposób nie wpływa na rzutowanie do ciągów znaków np. poprzez łączenie zmiennych o różnych typach. delete Number.prototype.toString; var n = 2; console.log( 2..toString() ); // Wynik > [object Number] console.log( 2 + “a” ); // Wynik > “2”

Jeżeli do każdego z literałów bezpośrednio przypiszemy własną funkcję toString (bez modyfikowania prototypu), to zostanie ona zignorowana podczas zapisu do Storage. Wyjątek tutaj stanowi literał obiektu Function, którego metoda toString jako jedyna zostanie wykorzystywana w takiej sytuacji. var num = 1; num.toString = function() { return “[custom Number]”; }; localStorage.setItem( “test”, num ); console.log( localStorage.getItem( “test” ) ); // Wyświetli w konsoli: // Wynik > “1” var fn = function FN() { /*comment*/ } fn.toString = function() { return “[custom Function1]”; }; localStorage.setItem( “test”, fn ); console.log( localStorage.getItem( “test” ) ); // Wyświetli w konsoli: // Wynik > “[custom Function1]” var fn = new Function( “/*comment*/” ); fn.toString = function() { return “[custom Function2]”; }; localStorage.setItem( “test”, fn ); console.log( localStorage.getItem( “test” ) ); // Wynik > “[custom Function2]”

Sposobem na wywołanie funkcji toString podczas zapisu do Storage dla wszystkich typów danych jest użycie konstruktorów zamiast literałów obiektowych. Poniżej został przedstawiony przykład zapisu liczby poprzez użycie literału obiektowego oraz konstruktora new Number. Sposób ten działa dla nie tylko dla liczb, ale też dla wszystkich konstruktorów odpowiadających każdemu z literałów, gdyż  każdy z nich konstruuje obiekt, których dziedziczy po klasie Object.

12

var a = 1; a.toString = function() { console.log( “Ta wiadomość NIE zostanie wyświetlona podczas zapisu do localStorage” ); return “0”; }; var b = new Number( 1 ); b.toString = function() { console.log( “Ta wiadomość zostanie wyświetlona podczas zapisu do localStorage” ); return “0”; }; localStorage.setItem( “test1”, a ); console.log( localStorage.getItem( “test1” ) ); // Wyświetli w konsoli: // Wynik > “1” localStorage.setItem( “test2”, b ); console.log( localStorage.getItem( “test2” ) ); // Wyświetli w konsoli: // Wynik > “Ta wiadomość zostanie wyświetlona podczas zapisu do localStorage” // Wynik > “0”

Powyższy konstruktor Number (a także String, Boolean oraz wiele innych) tworzy nowy obiekt. Jeżeli nowo utworzony obiekt posiada metodę toString, zostanie ona wywołana podczas zapisu, a zwrócona wartość będzie zapisana do Storage. Wszystkie obiekty, które dziedziczą po klasie bazowej Object nie muszą posiadać zdefiniowanej metody toString, gdyż sam Object już takową posiada. Podczas zapisu do Storage przeglądarka odpyta po kolei cały łańcuch dziedziczenia, aż nie odnajdzie metody toString w najbliższym rodzicu. Jeżeli jednak metoda toString nie zostanie odnaleziona w całym łańcuchu dziedziczenia, bądź jej wartość tak naprawdę będzie czymkolwiek innym niż funkcją, przeglądarka rzuci błędem podczas zapisu do Storage. Gdy obiekt dziedziczący po klasie bazowej Object nie posiada metody toString, podczas zapisu zostanie rzucony odpowiedni wyjątek: var A = function() {}; A.prototype.toString = undefined; localStorage.setItem( “test”, new A() ); // Wynik > Uncaught TypeError: Cannot convert object to primitive value

Identycznie wygląda to w przypadku, gdy metoda toString jest czymkolwiek innym aniżeli funkcją: Object.prototype.toString = null; var A = function() {}; localStorage.setItem( “test”, new A() ); // Wynik > Uncaught TypeError: Cannot convert object to primitive value

Jeżeli podamy ciąg znaków jako argument dla new Object to zostanie wewnętrznie użyty konstruktor String, który ma własną implementację metody toString. W tym przypadku przeszukiwanie łańcucha dziedziczenia kończy się na String.prototype, przez co nie zostanie rzucony wyjątek. Poniższy przykład dotyczy ciągu znaków, ale może zostać powtórzony też dla innych typów takich jak np. Boolean czy Number:

13

Object.prototype.toString = undefined; localStorage.setItem( “test”, new Object( “string” ) );

Poniżej dziedziczymy metodę toString na przykładzie obiektu utworzonego przez konstruktor String. Identyczny proces zachodzi dla wszystkich innych obiektów dziedziczących po Object: var a = new String( “a” ); console.log( a.toString() ); // Wynik > “a” delete String.prototype.toString; console.log( a.toString() ); // Wynik > “[object String]” Object.prototype.toString = function() { return “Inheriting from Object.prototype”; } console.log( a.toString() ); // Wynik > “Inheriting from Object.prototype” delete Object.prototype.toString; console.log( a.toString() ); // Wynik > Uncaught TypeError: undefined is not a function

Indeksy i iteracja po wpisach Każdy z elementów w Storage jest dostępny nie tylko po kluczu, ale także po jego indeksie. Zapisując dane do localStorage w żaden sposób nie dostajemy informacji o tym pod jakim indeksem został dany element zapisany. Indeksy elementów w Storage wykorzystuje się  głównie do iteracji po wszystkich elementach znajdujących się  w Storage. Możemy odczytać zawartośc elementu (a dokładniej jego klucza) poprzez użycie metody key, która jako argument przyjmuje indeks, pod którym element został zapisany: // Czyścimy localStorage ze wszystkim poprzednich elementów localStorage.clear(); localStorage.setItem( ’foo’, ’bar’ ); console.log( localStorage.key( 0 ) ); // Wynik > ”foo” console.log( localStorage.key( 1 ) ); // Wynik > undefined console.log( localStorage.length ); // Wynik > 1

Storage nie posiada metody, która umożliwiałaby nam iterowanie po wszystkich elementach w danej instancji Storage (np. metody forEach, która jest dostępna dla każdej tablicy w JavaScript). Możemy jednak iterować po elementach poprzez użycie pętli. Pierwszym sposobem jest użycie pętli for, gdzie musimy znać ilość przechowywanych elementów - informacji o tym

14

dostarcza nam właściwość “length”. Posiadając informacje na temat ilości elementów, możemy w pętli odczytywać każdy z elementów poprzez odczyt klucza elementu spod danego indeksu. Indeks elementu natomiast zwiększamy w pętli od zera aż po ilość elementów w instancji Storage. localStorage.clear(); localStorage.setItem( ’a’, ’value 1’ ); localStorage.setItem( ’b’, ’value 2’ ); localStorage.setItem( ’c’, ’value 3’ ); var key; for ( var i = 0; i < localStorage.length; ++i ) { key = localStorage.key( i ); console.log( ”index: %s, key: %s, value: %s”, i, key, localStorage.getItem( key ) ); } // Wynik > index: 0, key: a, value: value 1 // Wynik > index: 1, key: b, value: value 2 // Wynik > index: 2, key: c, value: value 3

Drugim sposobem jest skorzystanie z pętli for...in, dzięki której możemy całkowicie pominąć odczytywanie kluczy poprzez indeksy. Ten sposób korzysta z alternatywnego API Storage, które umożliwia odczyt danych w Storage poprzez odwołanie się do kluczy elementów jak do zwykłych właściwości obiektów. localStorage.clear(); localStorage.setItem( ’a’, ’value 1’ ); localStorage.setItem( ’b’, ’value 2’ ); localStorage.setItem( ’c’, ’value 3’ ); for ( var key in localStorage ) { console.log( ”key: %s, value: %s”, key, localStorage.getItem( key ) ); } // Wynik > key: a, value: 1 // Wynik > key: b, value: 2 // Wynik > key: c, value: 3

Powyższe przykłady nasuwają  na myśl następujące pytanie, w jaki sposób są przydzielane indeksy do elementów i czy kolejność indeksów może się  zmienić? Przeprowadźmy mały eksperyment. Poniższy kod zapisuje do localStorage dane pod kluczem znaku ASCII (o wartości od 0 do 99), każdy ze znaków zapisywany jest w kolejności malejącej, to jest od 99 do 0. Gdy wszystkie znaki zostaną już zapisane, odczytuje całą zawartość localStorage i wyświetla ją w konsoli. localStorage.clear(); var chr = 100, key; while ( chr-- ) { localStorage.setItem( String.fromCharCode( chr ), ’CharCode: ’ + chr ); } for ( var i = 0; i < localStorage.length; ++i ) { key = localStorage.key( i ); console.log( ”key: %s, value: %s”, key, localStorage.getItem( key ) ); }

15

Kod ten uruchomiony pod różnymi przeglądarkami da różne wyniki, gdyż każda z przeglądarek różni się pod względem: ƒƒ Liczenia unikalnego hash’a dla każdego z kluczy oraz obsługi kolizji ƒƒ Mechanizmu przechowującego dane na dysku lub w pamięci ƒƒ Wewnętrznego cache Żaden standard nie mówi, w jaki sposób elementy w localStorage powinny zostać posortowane, dlatego też przeglądarki nie są zunifikowane pod tym względem. Indeks elementu jest w przeglądarkach liczony zazwyczaj z nazwy klucza. Na podstawie klucza jest liczony hash, jeżeli więcej elementów posiada taki sam hash, hash przeliczony jest jeszcze raz, tym razem innym algorytmem. Cały proces jest skomplikowany i każda z przeglądarek liczy indeks w inny sposób. Zatem kolejność elementów w localStorage może dla jednego zestawu danych być identyczna w każdej z przeglądarek, dla innego zestawu jednak różnić się diametralnie. Korzystając z indeksów w localStorage musimy pamiętać, że nie mamy żadnej gwarancji, że kolejność w jakiej zapiszemy elementy będzie odpowiadała kolejności indeksów, ani że każdy klucz będzie się już zawsze znajdował pod danym indeksem.

Zdarzenie “storage” Za każdym razem, gdy wprowadzamy zmiany w danych przetrzymywanych w Storage, występuje zdarzenie “storage” na globalnym obiekcie “window” przeglądarki. window.addEventListener( ”storage”, function( e ) { console.log( ”Storage event”, e ); });

Zdarzenie “storage” dostarcza, między innymi takich informacji jak:

16

Właściwość zdarzenia

Opis

key

Klucz, pod którym nastąpiła zmiana.

newValue

Wartość elementu o podanym kluczu.

oldValue

Poprzednia wartość jaką posiadał element o podanym kluczu bądź null, jeżeli dany element wcześniej nie istniał.

url

Adres URL, do którego Storage jest przypisany i na którym wystąpiło zdarzenie.

storageArea

Storage, dla którego wystąpiło zdarzenie.

timeStamp

Czas wystąpienia zdarzenia.

Istnieje jeden bardzo istotny fakt o którym należy zawsze pamiętać podczas pracy ze zdarzeniem “storage”, otóż nie jest ono wywoływane w oknie, w którym miało ono swoje źródło, lecz w pozostałych oknach (lub zakładkach), które współdzielą dany Storage. Oznacza to, że jeżeli mamy otwarte dwie zakładki, które posiadają  tę  samą  lokacje i w jednej z zakładek zapiszemy dane do Storage, to zdarzenie, które nas o tym poinformuje zostanie wywołane w drugiej zakładce. W pierwszej zakładce zdarzenie to nigdy nie wystąpi. Jest to zachowanie przewidziane przez standard, natomiast przeglądarki niekoniecznie się tego zachowania trzymają. Na przykład, dla przeglądarki Internet Explorer 9 oraz 10 zdarzenie to może być równie dobrze wywołane w obu zakładkach. Należy pamiętać o tym zachowaniu, gdyż w przeciwnym razie może to doprowadzić do niepotrzebnego bólu głowy i niestabilności działania aplikacji. Inną dość dużą niekompatybilnością związaną z obsługą zdarzenia “storage” pomiędzy przeglądarkami jest moment wystąpienia tego zdarzenia. Większość  przeglądarek wywołuje zdarzenia “storage” po zapisie do Storage, natomiast Internet Explorer robi to przed. Skutek tego jest taki, że jeżeli podczas wystąpienia zdarzenia “storage” odczytamy zawartość pola, które zostało zmodyfikowane, to w Internet Explorer pod tym polem będzie się znajdowała stara wartość tuż sprzed zapisu: window.addEventListener( ”storage”, function( e ) { // W IE zawartość tego pola nie będzie aktualna console.log( ”Value: ”, localStorage.getItem( e.key ) ); });

17

Poniższy skrypt jest przykładem, jak mogłaby wyglądać prosta komunikacja pomiędzy oknami przeglądarki (o ile znajdują się w tej samej lokacji). !function() { var KEY = ’__com__’; function _genID( n ) { var str = ’’, n = n || 9; while ( n-- ) { str += Math.round( 100 + Math.random() * 100 ).toString( 16 ); } return str; } var id = _genID(), listeners = []; function send( message ) { localStorage.setItem( KEY, JSON.stringify({ id: id, message: message, token: _genID() }) ); } function addListener( fn ) { if ( typeof fn === ’function’ ) { listeners.push( fn ); } } window.addEventListener( ’storage’, function( e ) { var value = e.newValue; if ( value === null ) { return; } var data = JSON.parse( value ); if ( data.id !== id ) { listeners.forEach(function( fn ) { fn( data.message, data ); }); } }); window.tabMessenger = { id: id, send: send, addListener: addListener } }();

18

Cały skrypt opiera się na bardzo prostych założeniach: ƒƒ Każde z okien generuje swoje unikalne id, które będzie rozsyłane wraz z treścią wiadomości do innych okien. ƒƒ Każde okno podczas odczytywania wiadomości porówna przesłane id ze swoim własnym id, jeżeli będą się różnić, wtedy okno przyjmie wiadomość, w przeciwnym razie wiadomość zostanie zignorowana. Jest to zabezpieczenie przed sytuacją jaka ma miejsce w przeglądarce Internet Explorer - gdy zdarzenie “storage” występuje też w oknie, w którym została dokonana zmiana w Storage. ƒƒ Ta prosta biblioteka udostępnia dwie metody, send - wysyła wiadomość do innych okien, oraz addListener - dodaje funkcję nasłuchującą wiadomości przychodzących z innych okien. ƒƒ Wiadomości są zapisywane w Storage w formacie JSON. W samej wiadomości będą przesyłane: id okna, z którego wiadomość pochodzi; treść wiadomości, która ma zostać przekazana do pozostałych okien oraz unikalny token. ƒƒ Token jest przesyłany dlatego, by umożliwić przesyłanie wiadomości o tej samej treści kilka razy pod rząd, gdyż zdarzenie “storage” jest przekazywane tylko raz w przypadku próby zapisu do Storage elementu o tej samej treści. Należy jednak pamiętać, że to jest bardzo prosty skrypt i nie jest on przystosowany do ekstremalnych przypadków, np.: rozsyłania setek wiadomości jedna po drugiej z wielu okien w tym samym czasie. Generalnie należy pamiętać o tym, że HTML5 Storage nie został stworzony w celu wymiany danych pomiędzy oknami, lecz w celu przechowywania danych. Oznacza to, że możemy wymieniać dane pomiędzy oknami, lecz nie powinniśmy się spodziewać dużej wydajności i niezawodności. Istnieje jednak mechanizm, który został stworzony z myślą o komunikacji pomiędzy oknami: HTML5 Web Messaging.

19

Pojemność Storage

Ilość znaków a zajmowana przestrzeń Ilość danych, jaką możemy zapisać do Storage, jest ściśle ustalona poprzez kodowanie znaków, jakie jest używane do przechowywania ciągów znaków. Język JavaScript używa kodowania UTF-16 do przechowywania znaków, który różni się nieznacznie od obecnie najpopularniejszego kodowania w siecii - UTF-8 [2]. Właściwości UTF-8: ƒƒ Wymagane jest minimalnie 8 bitów, by zakodować jeden znak. ƒƒ Kompatybilność z ASCII - Jeżeli dane zawierają znaki zawarte także w standardzie ASCII, to ciąg znaków UTF-8 jest w pełni kompatybilny z ASCII i może zostać odczytany przez oprogramowanie, które nie posiada wsparcia dla UTF-8. ƒƒ Zorientowany bajtowo, czyli jeden bajt to jeden znak. W przeciwnieństwie do UTF16 nie ma dwóch kolejności bajtów, lecz mimo to na początku danych nadal można spotkać bajt BOM ƒƒ Lepiej sobie radzi z błędami - pojedynczy bajt na każdy znak sprawia, że w przypadku znaków zgodnych z ASCII, jeżeli jeden z bajtów zostanie usunięty z wiadomości, to reszta znaków nadal będzie możliwa do odczytania (zachowa swoje znaczenie) Właściwości UTF-16: ƒƒ Wymagane jest minimalnie 16 bitów, by zakodować jeden znak. ƒƒ Niekompatybilny z ASCII - ze względu na różnice w ilości bitów, znaki z ASCII nie mogą zostać zaprezentowane poprzez UTF-16. ƒƒ Nie jest zorientowany bajtowo. Jeden znak wymaga pary bajtów. Co oznacza także, że w UTF-16 kolejność występowania bajtów ma znaczenie (np. 0a 01 oraz 01 0a może, ale nie musi być tym samym znakiem), dlatego rozróżniamy UTF-16LE (little endian) oraz UTF-16BE (big endian). Informacja o kolejności znaków w UTF-16 jest zazwyczaj przekazywana na początku ciągu znaków, jako pierwszy znak (BOM), lub w osobnej partii danych - meta danych. ƒƒ Jeżeli w wiadomości wystąpi błąd i jeden z bajtów zostanie pominięty, reszta wiadomości zmieni znaczenie, gdyż następujące bajty zmienią swoje pary. Należy zatem zadać  sobie pytanie, dlaczego wszelkie dane przesyłamy zazwyczaj w UTF-8 (np. plik HTML, który wysyłamy do przeglądarki), ale już dane w pamięci (JavaScript) czy na dysku (Storage) są przechowywane w formie UTF-16. Otóż musimy wziąć pod uwagę dwa problemy: Czas przesyłu danych oraz ilość zajętej przestrzeni. UTF-8 zajmuje mniej przestrzeni, ale jest trudniejszy do zdekodowania, dlatego też o wiele bardziej nadaje się  do przesyłanie danych aniżeli ich przechowania.

20

By zrekompensować problem potrzebnej pracy procesora, by zdekodować dane, są one dekodowane raz, ale do UTF-16, gdzie w takiej formie są przechowywane w pamięci operacyjnej (np. wszelkie ciągi znaków w JavaScript) lub na dysku (Storage np. w postaci bazy SQLite w wewnętrznym mechaniźmie przeglądarki). UTF-16 jest o wiele prostszy do zdekodowania, lecz ilość przestrzeni jaką mogą zająć dane nim zakodowane jest nieoptymalna do przesyłania przez sieć, natomiast w dobie pojemnych pamięci operacyjnych oraz dyskowych nie jest problemem przechowywanie tych danych. Tekst

J

A

V

A

S

C

R

I

P

T

UTF-8

4A

41

56

41

53

43

52

49

50

54

UTF-16

00 4A

00 41

00 56

00 41

00 53

00 43

00 52

00 49

00 50

00 54

Przykład zakodowania ciągu znaków za pomocą UTF-8 oraz UTF-16

Liczenie zajętej przestrzeni W przypadku Storage możemy jedynie liczyć zajętą przestrzeń, lecz w żaden sposób przeglądarki nie dają nam możliwości pobrania informacji na temat wolnej przestrzeni. Oznacza to, że w każdej chwili podczas zapisu danych możemy natrafić na limit nałożony na objętość Storage dla danej domeny. Z chwilą przekroczenia limitu nałożonego przez przeglądarkę, zostanie rzucony błąd o tym informujący (odmienny dla każdej przeglądarki, a jakże). var n = 0, s = new Array( 1024 ).join( ’1’ ); localStorage.clear(); while ( 1 ) { localStorage.setItem( n, s ); n++; } // Wynik > Uncaught QuotaExceededError: Failed to execute ’setItem’ on ’Storage’: Setting the value of ’5106’ exceeded the quota.

Powyższy przykład pochodzi z przeglądarki Google Chrome i prezentuje błąd, jaki wystąpi po przekroczeniu limitu na danym Storage. Ilość przestrzeni jaką mamy dostępną różni się dla każdej przeglądarki, obecnie (rok 2014) minimalna pojemność jaką oferuje część przeglądarek to 5 MiB. Oznacza to, że nawet jeżeli przeglądarka, na której jest uruchomiony nasz skrypt posiada większy limit, to i tak nie dowiemy się o tym, dopóki nie przekroczymy tego limitu. By poznać prawdziwą pojemność instancji Storage z jakiej korzystamy, możemy zawsze użyć argumentu siły i w pętli zapełnić całe Storage, po czym obliczyć zajętą przestrzeń, gdy wystąpi błąd związany z przekroczeniem limitu pojemności. Poniższa funkcja getStorageQuotaDirty prezentuje sposób, w jaki można to zrobić.

21

// Nasza funkcja przyjmuje dwa argumenty: // - storage - Instancja Storage, którą chcemy zbadać // - pref - prefix jakiego użyć dla kluczy // - chunkSize - długość ciągu znaków jaka ma zostać // zapisana z każdym użyciem metody // setItem, aż do zapełnienia Storage. // Domyślnie 512 znaków. function getStorageQuotaDirty( storage, pref, chunkSize ) { pref = pref || ’☺’; chunkSize = chunkSize || 512; lastChunkSize = 0; var quota = 0; var keys = []; try { var n = 0, s = new Array( chunkSize + 1 ).join( ’1’ ); while ( 1 ) {/ lastChunkSize = s.length; lastChunkSize += n.toString().length; storage.setItem( pref + n, s ); keys.push( pref + n ); n++; } } catch (e) { var key; for ( var i = 0; i < storage.length; ++i ) { key = storage.key( i ); quota += key.length; quota += storage.getItem( key ).length; } quota += lastChunkSize; } keys.forEach(function( key ) { storage.removeItem( key ); }); }

return quota;

console.log( ’Limit wynosi około: %s MiB’, getStorageQuotaDirty( localStorage ) / 1024 / 1024 );

Skrypt ten wypełnia całą wolną przestrzeń wygenerowanymi danymi, aż do momentu wystąpienia błędu o przekroczeniu dostępnego limitu. Następnie liczy ile zdołał zapełnić przestrzeni oraz dodaje do uzyskanej liczby długość ciągu znaków, którego nie udało się zapisać. Na koniec skrypt usuwa wszystkie elementeny z Storage, które sam dodał, natomiast nie usuwa danych, które istniały już wcześniej. By uniknąć sytuacji (a raczej zmniejszyć jej prawdopodobieństwo), w której nasz skrypt nadpisze istniejący już element w Storage, użyliśmy znaku ASCII “☺” jako prefixu. Mało prawdopodobne jest byśmy użyli tego (lub innego) znaku jako klucza do przechowywania wartościowych danych. Funkcja na koniec zwróci nam ilość znaków jaka się mieści w Storage. Jest to wartość przybliżona, gdyż podczas zapisu ostatniego ciągu znaków do Storage wystąpił błąd, więc jeżeli ciąg ten miał długość 516 znaków, to nie wiemy czy limit został przekroczony już przy pierwszym znaku, gdzieś w połowie czy na samym końcu. Jest to zatem też przyczyna dlaczego zapisujemy dane w małych częściach po 516 znaków, im mniejsza ilość znaków, którą zapisujemy do Storage na raz, tym precyzyjniejszy wynik badania pojemności Storage. Z drugiej strony, przy mniejszym ciągu znaków sam zapis do Storage potrwa w pętli o wiele dłużej, gdyż wywołań metody setItem nastąpi o wiele więcej nim wystąpi błąd z przekroczeniem limitu, co wiąże się z tym iż interfejs przeglądarki przestanie być responsywny na czas wywoływania pętli.

22

Więcej informacji na: it.pwn.pl

Grafika 3D czasu rzeczywistego Nowoczesny OpenGL Z książki dowiesz się:    

jak tworzyć aplikacje korzystające z grafiki trójwymiarowej tworzonej za pomocą OpenGL, w tym z nowych wersji tej biblioteki nauczysz się programować shadery w GLSL poznasz teoretyczne podstawy rachunku macierzy niezbędne w grafice trójwymiarowej poznasz model oświetlenia Phonga i nauczysz się go implementować

Powinieneś znać:   

podstawy programowania w C++ w tym programowania obiektowego podstawową wiedzę o programowaniu dla systemu Windows

Box2D: fizyczny świat w pudełku Z książki dowiesz się:    

jak napisać porządną, zgodną z obecnymi standardami, grę na swój smartfona jak zaimplementować funkcjonalność podobną do zastosowanej w sztandarowych tytułach jak tworzyć własne rozwiązania z użyciem fizyki jak wzbogacić swój program o fizykę

Powinieneś znać:   

C++ w stopniu podstawowym środowisko Microsoft Visual Studio podstawy mechaniki Newtona

OpenCL Akceleracja GPU w praktyce Z książki dowiesz się:  jak korzystać z technologii OpenCL  jak tworzyć własne jądra obliczeniowe  jak przetwarzać grafikę za pomocą wbudowanych możliwości OpenCL Powinieneś znać:  wybrane pojęcia algebry liniowej, m.in. macierze, wektory  podstawy języków C, C++ oraz Python  środowisko IDE, np. Visual Studio

Android w praktyce. Projektowanie aplikacji Z książki dowiesz się:        

jak utworzyć mobilną aplikację z systemem Android jak działa aplikacja na urządzeniu mobilnym (cykl życia) jak utworzyć projekt rozbudowanej aplikacji jak projektować podstawowe i rozbudowane GUI dla własnych aplikacji jak zaprojektować i zaimplementować własną relacyjną bazę danych SQLite jak pobierać i zapisywać dane do bazy danych SQLite z wykorzystaniem języka SQL jak tworzyć rozbudowaną grafikę 2D oraz elementy grafiki 3D jak skonfigurować i zastosować emulator urządzenia mobilnego AVD

23

Rozszerzanie Storage o nowe możliwości Własne API Jak już wcześniej zostało to opisane, nie możemy rozszerzyć bezpośrednio obiektu Storage (np. localStorage) o nowe funkcje, gdyż nowa właściwość czy metoda zostanie potraktowana jako kolejna dana do zapisu w Storage. By obejść to zachowanie, musimy najpierw opakować oryginalne API Storage w nasz obiekt z naszymi własnymi metodami i właściwościami. Po stworzeniu własnego API nie będzie możliwe zapisanie danych do Storage poprzez przypisanie właściwości do obiektu, tak jak to ma miejsce w przypadku natywnej instancji Storage. Jest to jednocześnie minus jak i plus takiego rozwiązania. Mamy, co prawda, mniejsze możliwości, ale jednak zyskujemy stabilny, z góry określony punkt dostępu i manipulacji danych. Fundamenty W pierwszej kolejności nasz cały kod umieścimy w samowywołującej się funkcji, dzięki czemu unikniemy zaśmiecenia globalnej przestrzeni. Nasza klasa będzie nosić nazwę Store, jej możliwości będą na razie niewielkie i ograniczymy się głównie do opakowania istniejącego natywnego API instancji Storage. Konstruktor naszej klasy będzie odpowiedzialny jedynie za konfigurowanie właściwości nowo utworzonego obiektu. !function() { ’strict’; var Store = function( options ) { options = options || {};

}

for ( var i in options ) { this[ i ] = options[ i ]; }

W prototypie natomiast umieścimy metody, które odwołują się do istniejących już metod dostępnych w natywnym API Storage. Dodatkowo na końcu umieścimy nową metodę “each”, które nie posiada swojego odpowiednika w istniejącym API. Jak łatwo się domyślić służy ona do iteracji po elementach znajdujących się w instancji Storage. Jako jedyny parametr będzie przyjmowana funkcja, które będzie wywołana na każdym z elementów z osoba. Podczas wywołania tej funkcji, jej argumentami będą takie dane jak: wartość elementów, nazwa klucza, pod którym element został zapisany oraz dodatkowo indeks elementu.

24

Store.prototype = { storage: window.localStorage, set: function( key, value ) { this.storage.setItem( key, value ); }, get: function( key ) { return this.storage.getItem( key ); }, remove: function( key ) { this.storage.removeItem( key ); }, clear: function() { this.storage.clear(); }, key: function( index ) { return this.storage.key( index ); },

}

each: function( fn ) { if ( typeof fn === ’function’ ) { for ( var i = 0, key; i < this.storage.length; ++i ) { key = this.storage.key( i ); fn( this.storage.getItem( key ), key, i ); } } }

Tak skonstruowaną klasę od razu wykorzystamy poprzez stworzenie dwóch jej instancji. Każda z instancji będzie odpowiadała natywnym instancją dostępnym w przeglądarce, tj.: localStorage oraz sessionStorage. Obie instancje następnie udostępnimy pod wspólnym obiektem “store” o zasięgu globalnym. window.store = { local: new Store({ storage: window.localStorage }), session: new Store({ storage: window.sessionStorage })

} }();

25

Przykład użycia store.local.set( ’foo1’, ’bar1’ ); store.local.set( ’foo2’, ’bar1’ ); store.local.set( ’foo3’, ’bar1’ ); var msg = "Item key: %s, value: %s, index: %s”; store.local.each(function( key, value, index ) { console.log( msg, key, value, index ); }); store.local.clear();

Przestrzenie nazw Załóżmy, że w naszej aplikacji umieściliśmy kilka niezależnych skryptów, z których każdy będzie musiał w pewnym momencie wyczyścić dane zapisane w Storage. Jeśli jeden ze skryptów za pomocą metody clear() wyczyści zapisane przez siebie dane, może okazać się, że dane zapisane przez drugi skrypt także zostaną usunięte, mimo iż nie powinny, a ich brak może doprowadzić do problemów w działaniu aplikacji. Natywne API Storage nie oferuje tworzenia własnych instancji Storage ani też żadnego wbudowanego mechanizmu przestrzeni nazw. Mechanizm taki uchroniłby nas przed wcześniej opisanym przypadkiem. Musimy zatem sami zadbać o taki mechanizm, który zadbałby o to żeby tylko określony zbiór danych został wyczyszczony, a nie cała zawartość instancji Storage. Główne założenia mechanizmu: ƒƒ Odrębne przestrzenie, o unikalnej nazwie ƒƒ W obrębie pojedynczej przestrzeni elementy muszą posiadać unikatowy klucz ƒƒ Klucze elementów mogą jednak występować w innych przestrzeniach ƒƒ Zmiany w jednej przestrzeni nie wpływają na zawartość innych przestrzeni By spełnić te wymagania musimy zatem zapewnić: ƒƒ Możliwość tworzenia nowych instancji klasy Store ƒƒ Wyposażyć instancje Store w unikatowe id ƒƒ Id każdej instancji będzie musiało być ciągiem znaków, zawierającym znaki 0-9, a-z, A-Z oraz _. ƒƒ By nie kolidować z istniejącymi danymi, zapisanymi poza przestrzenią nazw oferowaną przez klasę Store, każdy z elementów zapisany w Storage z pomocą klasy Store będzie posiadał prefiksowany klucz. By zmniejszyć prawdopodobieństwo wystąpienia kolizji z istniejącymi kluczami, użyjemy jako prefiksa znaku ASCII “✇” (U+2707 TAPE DRIVE). Prefiks taki zmniejszy prawodpodobieństwo wystąpienia kolizji znacząco. ƒƒ Pełna nazwa klucza będzie posiadała postać “prefix:IdInstancji:NazwaKlucza”, czyli np. “✇:myid:mykey”. Taka postać klucza będzie jednak używana prytwanie w klasie Store, natomiast manipulacja danymi poprzez udostępniony interfejs nadal będzie odbywać się za pomocą prostej nazwy klucza, np. “mykey”.

26

By rozbudować naszą klasę Store o funkcjonalność przestrzeni nazw, musimy najpierw stworzyć kilka przydatnych obiektów oraz stałych. Każdą z utworzonych instancji Store, będziemy przetrzymywać w prywatnym obiekcie _instances, niedostępnym na zewnątrz. By zapewnić  unikalność  id każdej instancji, użyjemy prostego walidatora w postaci wyrażenia regularnego. Sprawdzi ono czy podane id posiada jedynie znaki z przedziału 0-9, a-z, A-Z oraz _. !function() { ‚strict’; var _instances = {}; var ID_VALIDATOR = /\W/gi; var PREFIX = ’✇’; var SEPARATOR = ’:’; var ERR_NOID = ’Store instance needs to have a unique id’; var ERR_TKNID = ’Given Store id is already taken.’; var ERR_INVID = ’Given id is invalid. Id should match ’ + ’any alphanumeric character from the ’ + ’basic Latin alphabet, including the ’ + ’underscore [A-Za-z0-9_].’;

W obiekcie utlis, będziemy przetrzymywać funkcje do manipulowania postaciami kluczy:

ƒƒ getPrefix - Zwraca prefiks dla danej instancji Store. Przyjmuje jeden argument: id. ƒƒ addPrefix - Dodaje prefiks do podanego klucza. Przyjmuje dwa argumenty: key oraz id. ƒƒ removePrefix - Usuwa prefiks z klucza. Przyjmuje dwa argumenty: key oraz id. ƒƒ isKeyPrefixed - Sprawdza czy dany klucz posiada już nałożony prefiks. Przyjmuje dwa argumenty: key oraz id. var utils = { addPrefix: function( key, id ) { return this.getPrefix( id ) + key; }, removePrefix: function( key, id ) { return key.replace( this.getPrefix( id ), ’’ ); }, getPrefix: function( id ) { return [ PREFIX, id, ’’ ].join( SEPARATOR ); },

}

isKeyPrefixed: function( key, id ) { var p = this.getPrefix( id ); return key.substr( 0, p.length ) === p; }

Do konstruktora dodajemy walidację podanego id. Walidujemy id pod względem poprawności formatu oraz jego unikalności. Gdy będziemy już pewni, że instancja posiada poprawne i unikalne id, będziemy mogli ją zapamiętać w obiekcie _instances.

27

var Store = function( options ) { options = options || {}; for ( var i in options ) { this[ i ] = options[ i ]; } if ( typeof this.id === ’undefined’ || this.id === null ) { throw new Error( ERR_NOID ); } if ( this.id in _instances ) { throw new Error( ERR_TKNID ); } if ( ID_VALIDATOR.test( this.id ) ) { throw new Error( ERR_INVID ); } }

_instances[ this.id ] = this;

Każdą z wcześniej zaimplementowanych metod musimy wzbogacić o obsługę prefiksów. Wcześniej posiadaliśmy jedynie dwie instancje Store - “local” oraz “session”, dlatego prefiksy i unikalne id nie były koniecznie, gdyż już z założenia localStorage oraz sessionStorage posiadają niezależne zestawy kluczy, tym razem jednak umożliwimy tworzenie własnych instancji.

Świat poza jQuery W książce przedstawiono alternatywne do jQuery biblioteki skryptowe, dość powszechnie już używane w nowych projektach, których znajomość jest coraz częściej poszukiwana na rynku pracy. Z książki dowiesz się:    

jakie masz możliwości rozwoju, gdy czujesz, że jQuery to za mało jakie biblioteki skryptowe są teraz popularne jak poprawić tworzony do tej pory kod jak tworzyć strony typu Single Page App

Powinieneś znać:  

HTML (najlepiej HTML5) Java Script i jQuery

Więcej informacji na: it.pwn.pl

28

Store.prototype = { id: null, storage: window.localStorage, set: function( key, value ) { var pkey = utils.addPrefix( key, this.id ); this.storage.setItem( pkey, value ); }, get: function( key ) { var pkey = utils.addPrefix( key, this.id ); return this.storage.getItem( pkey ); }, remove: function( key ) { var pkey = utils.addPrefix( key, this.id ); this.storage.removeItem( pkey ); }, clear: function() { var self = this; this.keys().forEach(function( key ) { self.remove( key ); });

},

keys: function() { var keys = []; for ( var i = 0, key; i < this.storage.length; ++i ) { key = this.storage.key( i ); if ( utils.isKeyPrefixed( key, this.id ) ) { keys.push( utils.removePrefix( key, this.id ) ); }

} return keys; },

}

each: function( fn ) { if ( typeof fn === ’function’ ) { for ( var i = 0, key; i < this.storage.length; ++i ) { key = this.storage.key( i ); if ( utils.isKeyPrefixed( key, this.id ) ) { fn( this.storage.getItem( key ), utils.removePrefix( key, this.id ), i ); } } } }

Podczas zapisu i odczytu danych musimy dodać prefiks dla klucza. Natomiast jedynym miejscem, w którym usuwanie prefiksu z klucza może być do czegokolwiek przydatne, jest moment, w którym musimy zaprezentować na zewnątrz dany klucz w czystej postaci. Ma to miejsce podczas iteracji po wszystkich elementach należących do danej instancji, gdzie będziemy zwracać informacje na temat danego elementu w Storage. Mechanizm ten jest widoczny w dwóch metodach - keys - zwracającej zbiór wszystkich kluczy dostępnych w danej instancji, oraz each - metodzie iterującej po wszystkich elementach i wywołującej operacje na każdym z elementów. Obie metody udostępniają informację na temat danego elementu, w tym także nazwę klucza w czystej postaci, bez prefiksa ani id instancji.

29

window.store = { TYPE_LOCAL: ’local’, TYPE_SESSION: ’session’, local: new Store({ id: ’locDef’, storage: window.localStorage }), session: new Store({ id: ’sessDef’, storage: window.sessionStorage }), create: function( id, type, options ) { options = options || {}; options.id = id; if ( !type || type === store.TYPE_LOCAL ) { options.storage = window.localStorage; } else if ( type === store.TYPE_SESSION ) { options.storage = window.sessionStorage; } }

return new Store( options );

} }();

Na zewnątrz udostępniamy także możliwość tworzenia nowych instancji Store o dwóch typach: “local” oraz “session”. Całość rozwiązania posiada dwie główne cechy: 1. Możemy manipulować niezależnymi zbiorami danych. Każdy ze zbiorów znajduje się we własnej przestrzeni. Dane ze zbioru A nie mają żadnego wpływu na dane ze zbioru B, nawet jeżeli dane w zbiorze A i B posiadają identyczne nazwy kluczy. 2. Każdy ze zbiorów może potencjalnie zająć całą wolna przestrzeń, nie pozostawiając innym zbiorom żadnego miejsca na zapis danych. To samo tyczy się także danych zapisanych bezpośrednio w Storage poza określonym zbiorem. Rozwiązaniem problemu z punktu drugiego będzie wprowadzenie ograniczeń w postaci maksymalnej pojemności danej przestrzeni. Nie będzie to jednak sposób łatwy, gdyż nie będziemy mogli pozwolić sobie na przeliczanie zajętej przestrzeni poprzez iteracje po wszystkich elementach danej przestrzeni, podczas każdej zmiany w Store. Więcej o tym zagadnieniu oraz jak sobie z nim poradzić w kolejnym podrozdziale.

30

Przykład użycia wprowadzonej funkcjonalności var a = store.create( ’A’, store.TYPE_LOCAL ); var b = store.create( ’B’, store.TYPE_LOCAL ); a.set( ’test’, 1 ); b.set( ’test’, 2 ); console.log( a.get( ’test’ ) === b.get( ’test’ ) ); // false a.clear(); console.log( a.get( ’test’ ) ); // null console.log( b.get( ’test’ ) ); // ”2”

Limit pojemności w przestrzeni nazw W natywnym Storage mamy do dyspozycji określoną  pojemność na całą instancje Storage. Jako że nasza klasa Store, udostępnia już funkcjonalność przestrzeni nazw, możemy wprowadzić na bazie tej funkcjonalności obsługę limitów dla każdej instancji Store z osobna. Innymi słowy umożliwimy podział np. localStorage na 3 mniejsze przestrzenie, gdzie każda będzie miała przydzielony swój własny limit, np. jedna otrzyma limit 1 MiB a dwie pozostałe limit wynoszący 2 MiB.

31

By wprowadzić nową funkcjonalność musimy najpierw umożliwić  liczenie objętności przestrzeni, jaką aktualnie zajmuje dana instancja. Liczenie odbywać się będzie poprzez iterację po elementach danej instancji i zliczanie długości ich kluczy oraz ich wartości. Nie możemy jednak liczyć objętości przestrzeni za każdym razem, gdy zmodyfikujemy zawartość danej instancji (poprzez metody set, remove oraz clear). Każda najmniejsza zmiana wymagałaby iteracji po wszystkich elementach w danej instancji, to z kolei wiązałoby się z bardzo dużym obciążeniem pod względem wydajności. By zoptymalizować cały proces, będziemy zliczać rozmiar danych instacji Store, tylko i wyłącznie na samym początku, podczas konstruowania instancji Store. Wynik operacji będziemy przetrzymywać we właściwości size. Każda metoda modyfikująca w jakikolwiek sposób zawartość instancji, będzie sama musiała dostarczać informacji na temat różnicy w objętości przestrzeni jaką wprowadziła i sama aktualizować wcześniej zliczoną wartość size. Dzięki temu zyskamy bardzo dużo na wydajności, gdyż zamiast liczyć na nowo całą objętość, wykonując potencjalnie setki czy tysiące operacji w zależności od ilości elementów, wykonamy tylko kilka operacji poprzez drobną aktualizację już wcześniej przeliczonej wartości. Pierwszym etapem w tworzeniu nowej funkcjonalności będzie ustawienie nowych stałych. Są to odpowiednio domyślny limit przestrzeni na każdą z instancji oraz nowa treść błędu, która wystąpi podczas zapełnienia dostępnej przestrzeni. !function() { ’strict’; /* ... */ var DEFAULT_LIMIT = 1048576; // 1024 * 1024 characters var ERR_FULL = ’Store is full’; /* ... */

Kolejnym etapem jest stworzenie nowych funkcji do użytku wewnętrzego. Będą one odpowiedzialne za liczenie przestrzeni. ƒƒ isFull - Sprawdza czy instancja o danym id jest już zapełniona czy też jeszcze nie. Opcjonalnie można sprawdzić czy element, który zamierzamy zmodyfikować przekroczy dozwolony limit (poprzez podanie jego nowego oraz poprzedniego rozmiaru - jeżeli wcześniej istniał). ƒƒ addSizeCache - Dodaje podany rozmiar do instancji o danym id. ƒƒ setSizeCache - Ustawia podany rozmiar, jako argument przyjmuje id instancji. ƒƒ getItemLength - Zwraca rozmiar elementu poprzez zliczenie długości podanego klucza oraz jego wartości. Jeżeli wartość danego klucza (value), nie zostanie podana, zostanie podjęta próba odczytania jej z instancji Storage. Jeżeli element pod podanym kluczem nie istnieje, to wtedy wartość jaką funkcja zwróci będzie wynosić 0.

32

var utils = { /* ... */ isFull: function( id, new_item_value, old_item_value ) { new_item_value = new_item_value || 0; old_item_value = old_item_value || 0; var instance = _instances[ id ]; var size = instance.size + new_item_value - old_item_value; return size >= instance.maxSize; }, addSizeCache: function( id, size ) { _instances[ id ].size += size; }, setSizeCache: function( id, size ) { _instances[ id ].size = size; }, getItemLength: function( id, key, value ) { var instance = _instances[ id ]; if ( typeof value === ’undefined’ ) { value = instance.storage.getItem( key ); } if ( value === null ) { return 0; }

}

}

return key.length + value.length;

Jednyną rzeczą jako musimy dopisać do konstruktora jest wywołanie metody calcSize, dzięki czemu uzyskamy początkową wartość właściwości size. Nigdzie więcej metoda ta nie zostanie wywołana wewnętrznie, lecz udostępnimy ją na zewnątrz gdyby zaistniała potrzeba przeliczenia zawartości danej instancji jeszcze raz. Rozmiar każdej instancji Store jest liczony wewnętrznie i nie mamy wpływu na to co mogą robić inne skrypty z zawartością Storage, dlatego też w przypadku nadpisania zawartości Storage poprzez natywne API w innym skrypcie, zajdzie potrzeba przeliczenia objętości instancji Store jeszcze raz, poprzez użycie udostępnionej metody calcSize. var Store = function( options ) { /* ... */ }

this.calcSize();

W kwestii modyfikacji zawartości Storage przez inne skrypty możemy, co prawda, wykryć wszelkie takie zmiany poprzez nasłuchiwanie na zdarzeniu “storage” na obiekcie okna przeglądarki, lecz jest to sposób niepewny, gdyż zdarzenie jest wywoływane tylko w określonych sytuacjach, dlatego też nie będziemy opierać działania biblioteki na tym zdarzeniu. Więcej o zdarzeniu “storage” w rozdziale poświęconym obsłudze tego zdarzenia “Zdarzenie “storage”".

33

Do danego prototypu musimy dodać właściwości mówiące o aktualnym oraz maksymalnym dopuszczalnym rozmiarze instancji. Dodatkowo tworzymy nową metodę calcSize, która poprzez iterację po elementach danej instancji będzie liczyć rozmiar instancji. Store.prototype = { /* ... */ size: 0, maxSize: DEFAULT_LIMIT, /* ... */ calcSize: function() { this.size = 0; var self = this; this.each(function( value, key ) { self.size += utils.getItemLength( self.id, utils.addPrefix( key, self.id ), value. toString() ); }); return this.size; }

W porównaniu z poprzednią budową metody set, zmiany będą znaczące. W pierwszej kolejności musimy obliczyć rozmiar danej (długość jej klucza oraz długość jej wartości), którą chcemy zapisać, oraz rozmiar, który posiadała wcześniej (o ile istniała). Posiadając informacje na temat nowego i poprzedniego rozmiaru, będziemy mogli sprawdzić czy nie został przekroczony limit rozmiaru nałożony na daną instancję Store. Jeżeli nowy rozmiar przekroczyłby limit, zostanie rzucony błąd ERR_FULL, a sama wartość element nie zostanie zapisana w Storage. Zapis odbywa się tylko i wyłącznie wtedy, gdy limit nie zostanie podczas tej operacji przekroczony. Zachowanie to odpowiada natywnemu zachowaniu instancji Storage. Na sam koniec aktualizujemy rozmiar instancji Store poprzez zmianę wartości właściwości size. set: function( key, value ) { var pkey = utils.addPrefix( key, this.id ), old_size = utils.getItemLength( this.id, pkey ), new_size = utils.getItemLength( this.id, pkey, value ); if ( utils.isFull( this.id, new_size, old_size ) ) { throw new Error( ERR_FULL ); } this.storage.setItem( pkey, value );

}

34

this.size -= old_size; this.size += new_size;

Kolejną metodą, którą musimy zmodyfikować jest motoda remove. Tutaj zmiany jedynie ograniczają się do obliczenia nowej wartości właściwości size. remove: function( key ) { var pkey = utils.addPrefix( key, this.id ); var value = this.storage.getItem( pkey ); var item_size = value ? pkey.length + value.length : 0; this.storage.removeItem( pkey ); }

this.size -= item_size;

W przypadku metody clear, musimy na samym końcu zresetować wartość właściwości size, poprzez ustawienie jej na 0. clear: function() { var self = this; this.keys().forEach(function( key ) { self.remove( key ); }); }

this.size = 0;

Przykład użycia nowej funkcjonalności: var a = store.create( ’A’, store.TYPE_LOCAL, { maxSize: 20 }); var b = store.create( ’B’, store.TYPE_LOCAL, { maxSize: 40 }); a.clear(); b.clear(); // Poniższe wywołanie rzuci błędem, gdyż ilość // danych jaką próbujemy zapisać, przekracza // dozwolony limit dla instancji ”A”. try { a.set( ’key’, ’to much data’ ); } catch (e) { console.warn( ’Store instance ”A” is full’ ); } // Natomiast poniższe wywołanie zapisze dane // do instancji B, gdyż w przeciwieństwie // do instancji A ma ona dwukrotnie wyższy limit. try { b.set( ’key’, ’not to much data’ ); } catch (e) { console.warn( ’Store instance ”B” is full’ ); }

Gdy wprowadzimy wszystkie te zmiany, zyskamy pełną funkcjonalność przestrzeni nazw i limitów pojemności. Moglibyśmy na tym zaprzestać, ale warto też zareagować na przypadek zapełnienia całej dostępnej przestrzeni w danej instancji Store. Istnieje wiele akcji jakie można podjąć w takiej sytuacji i zależą one wyłącznie od tego, co

35

chcemy osiągnąc. Takimi akcjami mogą być, np. usunięcie najstarszego elementu, usunięcie wszystkich danych lub jakakolwiek inna akcja, która zwolniłaby chociażby trochę miejsca. By zapewnić możliwość ciągłego rozszerzania funkcjonalności Store, będziemy musieli skontruować mechanizm, który pozwoli na rejestrowanie z zewnątrz własnych akcji, które będą uruchamianie podczas wywoływania wielu metod.

Reagowanie na zmiany w Store Mechanizm, który umożliwi nam reagowanie na zmiany w Store, oprzemy na trzech podstawowych filarach: ƒƒ Każdy z elementów zapisywanych poprzez instancje Store będzie wyposażony w date i czas zapisu, czyli tzw. metadane. ƒƒ Część metod udostępnionych na zewnątrz będzie wywoływać Hooki, w różnych etapach swojego działania. Czym są Hooki (haki) zostanie wyjaśnione w dalszej części rozdziału. ƒƒ Udostępnimy możliwość rejestrowania własnych zachowań, reagujących na stan Storage, poprzez podpięcie funkcji callback pod odpowiednie Hooki. W pierwszej kolejności, skupmy się na samych metadanych. Będą one oparte na instancji Store, którą utworzymy podczas inicjalizacji naszej biblioteki. Instancja będzie posiadała id o wartości “meta” i będzie dostępna bezpośrednio z poziomu obiektu store, który ma zasięg globalny. Umożliwi nam ona przechowywanie danych dotyczących każdego z elementów zapisanych w instancjach Store utworzonych na zewnątrz. Instancja “meta” będzie głównie przeznaczona do użytku wewnętrznego, lecz nie wykluczamy jej użycia na zewnątrz. Jako że “meta” jest instancją Store, to jej id nie będzie mogło być użyte dla żadnej innej instancji - zostanie rzucony odpowiedni wyjątek przy próbie utworzenia instancji z tym id. Każdy z elementów w instancji “meta”, będzie posiadał postać: Klucz

Wartość

”id#key”

”1415557206383,1415643606383”

Na nazwę klucza składa się “id” oraz “key” - jest to id instancji Store, do której element się odnosi oraz klucz pod jakim element jest zapisany w danej instancji.

Wartość elementu w instancji “meta” zawsze składa się z dwóch liczb oddzielonych przecinkiem. Są to odpowiednio: czas utworzenia danego elementu oraz termin jego wygaśniecia. Nie używamy tutaj formatu JSON, gdyż narzuciłoby to dość duże obciążenie zarówno dla wydajności jak i zajmowanej przestrzeni. Do danych przetrzymywanych w instancji meta będziemy się dość często odwoływać, dlatego też użyjemy tak prostego formatu, jak to tylko możliwe.

36

Musimy utworzyć trzy nowe funkcje do zarządzania metadanymi. Będą one wykorzystywane w metodach set oraz remove, gdyż tylko one w bezpośredni sposób ingerują w dane zapisane poprzez instancję Store. ƒƒ getMeta - Zwraca odczytane metadane w postaci obiektu zawierającego właściwości createDate - datę utworzenia oraz expireDate - datę wygaśniecia danego elementu. ƒƒ setMeta - Zapisuje podane dane, poprzez odczytanie wlaściwosci createDate oraz expireDate z podanego obiektu i zapisanie ich w postaci liczb oddzielonych przecinkiem. ƒƒ removeMeta - Usuwa element metadanych odpowiadający podanemu id oraz kluczowi z Storage. var utils = { /* ... */ getMeta: function( id, key ) { var meta = ( store.meta.get( [ id, key ].join( ’#’ ) ) || ’’ ).split( ’,’ ); return { createDate: meta[ 0 ], expireDate: meta[ 1 ] } }, removeMeta: function( id, key ) { store.meta.remove( [ id, key ].join( ’#’ ) ); }, setMeta: function( id, key, data ) { var meta = [ data.createDate ? data.createDate : ’’, data.expireDate ? data.expireDate : ’’ ];

}

}

store.meta.set( [ id, key ].join( ’#’ ), meta.join( ’,’ ) );

W prototypie klasy Store natomiast rozszerzamy dwie metody - set oraz remove. Do metody set dodajemy trzeci argument. Będzie to obiekt, w którym będziemy mogli opcjonalnie podać czas wygaśnięcia danego elementu, po przekroczeniu którego, element ten zostanie usunięty. Dodatkowo do treści samej funkcji dopisujemy ustawianie daty utworzenia (lub też  zmodyfikowania) elementu oraz jego wygaśniecia (jeżeli podano) poprzez wywołanie funkcji utils.setMeta. Wywołanie tej funkcji uzależniamy od tego czy instancja wywołująca metodę set nie jest instancją, która przetrzymuje metadane. Jest to o tyle ważne, że w przeciwnym wypadku doprowadzilibyśmy do wywołania metody set w nieskonczoność, gdyż samo wywołanie setMeta także ustawiłoby metadane dla tego samego pola metadanych, na którym odbywa się operacja. W tym celu w metodzie remove także musimy sprawdzić czy instancją wywowłującą metodę nie jest instancja meta, gdyż dopiero wtedy możemy usunąć odpowiednie pole metadanych.

37

Store.prototype = { /* ... */ set: function( key, value, options ) { options = options || {}; /* ... */ if ( this.id !== store.meta.id ) { utils.setMeta( this.id, key, { createDate: new Date().getTime(), expireDate: options.expireDate instanceof Date && options.expireDate.getTime() }); }

},

}

remove: function( key ) { /* ... */ if ( this.id !== store.meta.id ) { utils.removeMeta( this.id, key ); } }

Na koniec utwórzmy i udostępnijmy naszą  instancję, która będzie przetrzymywać metadane. window.store = { /* ... */

}

meta: new Store({ id: ’store’, maxSize: DEFAULT_LIMIT / 4 })

Posiadając obsługę metadanych jesteśmy gotowi, by zaimplementować następny

etap, czyli tzw. “Hooks”, czyli haki - często stosowany mechanizm, pozwalający na modyfikację zachowanie aplikacji podczas jej działania. Wprowadzimy prostą obsługę  tego mechanizmu, dzięki któremu będziemy mogli reagować na zmiany zachodzące w storage, w przypadku zapełnienia którejś z instancji bądź w jakimkolwiek innym przypadku, na który będziemy chcieli zareagować. Działanie haków będzie opierało się na: 1. Zdefiniowanych miejscach o określonych nazwach, w których zostaną wywołane haki do nich przypisane. Miejsca te będą znajdować się w większości metod (np. setItem), każde z tych miejsc będzie posiadało unikalną nazwę, która pozwoli na odpalenie tylko określonych haków. 2. Poprzez udostępnioną funkcję registerBehavior będziemy mogli zarejestrować odpowiednią akcję (funkcję), która będzie wywołana podczas wystąpienia odpowiednich haków.

38

Przypisanie funkcji do odpowiednich haków, zmieni zachowanie biblioteki podczas przeprowadzania operacji na danych zapisanych w Storage. Każda z takich akcji będzie nosiła swoją własną unikalną nazwę, dzięki czemu podczas tworzenia instancji Store będziemy mogli zdecydować czy chcemy jej użyć. Funkcja registerBehavior będzie przyjmować trzy argumenty: ƒƒ behavior_name - Nazwa, pod jaką będzie zarejestrowane akcja. ƒƒ hooks - Tablica zawierająca nazwy haków, podczas których funkcja callback zostanie wywołana. ƒƒ callback - Funkcja, która zostanie wywołana podczas wystąpienia każdego z podanych haków. !function() { ’strict’; var _instances = {}; var _hooks_groups = {}; /* ... */

Funkcja registerBehavior będzie zapisywała podaną funkcję callback oraz nazwy haków w wewnętrznym obiekcie _hooks_groups pod nazwą jaką przyjmiemy dla akcji (behavior_ name), które funkcja callback ma spełniać. Nazwę każdego z zarejestrowanych zachowań będziemy dodawać do globalnie dostępnego obiektu store.behavior. Umożliwi to określenie w łatwy sposób, jakie zachowania zarejestrowane przez registerBehavior są dostępne podczas działania skryptów, korzystających z biblioteki Store, którą tworzymy. Sama funkcja registerBehavior wygląda następująco: window.store = { /* ... */ behavior: {},

}

registerBehavior: function( behavior_name, hooks, callback ) { if ( typeof behavior_name === ’string’ && typeof callback === ’function’ && Array.isArray( hooks ) && hooks.length ) { _hooks_groups[ behavior_name ] = { hooks: hooks, callback: callback } this.behavior[ behavior_name.toUpperCase() ] = behavior_name; } }

39

Natomiast do obiektu utils musimy dopisać kolejne dwie pomocne funkcje. Są to: ƒƒ callHook - Wywołuje hak o podanej nazwie dla instancji o podanym ID. Oprócz tych dwóch argumentów, może przyjąc dowolną ilość  dodatkowych argumentów, które potem są przekazywane do callbacka (gdzie jako pierwszy argument będzie przekazana referencja do instancji na jakiej odbywa się operacja), jaki został zarejestrowany dla haków, które są tutaj wywoływane. ƒƒ isHookCovered - Sprawdza, czy dla danego haka w instancji Store, zostało zarejestrowanie zachowanie (poprzez store.registerBehavior), które obsługuje m.in. podanego haka. var utils = { /* ... */ callHook: function( id, hook, __ ) { var instance = _instances[ id ]; var args = Array.prototype.slice.call( arguments, 2 ); args.unshift( instance ); for ( var behavior_name in _hooks_groups ) { if ( instance.behavior.indexOf( behavior_name ) !== -1 && _hooks_groups[ behavior_name ].hooks.indexOf( hook ) !== -1 ) { _hooks_groups[ behavior_name ].callback.apply( utils, args ); } } },

return instance.size;

isHookCovered: function( id, hook ) { var instance = _instances[ id ]; for ( var behavior_name in _hooks_groups ) { if ( instance.behavior.indexOf( behavior_name ) !== -1 && _hooks_groups[ behavior_name ].hooks.indexOf( hook ) !== -1 ) { return true; } }

}

}

return false;

Do samego konstruktora musimy dodać obsługę nowego parametru: behavior. Będzie to tablica zawierająca nazwy wszystkich zachowań, jakie mają być uruchamiane na tej instancji podczas jej działania. Mimo iż docelowo będzie to tablica, to wstępnie będziemy także akceptować postać pojedynczej wartości (typu String), która zostanie zamieniona w postać tablicy z jednym elementem. To właśnie z tego obiektu (behavior) funkcja utils.callHook będzie brała informacje na temat tego, z jakich zachowań ma ona korzystać i czy te zachowania są zarejestrowane dla podanego haka. var Store = function( options ) { /* ... */ if ( this.behavior && !Array.isArray( this.behavior ) ) { this.behavior = [ this.behavior ]; } else { this.behavior = []; } }

40

utils.callHook( this.id, ’init’ );

W prototypie natomiast rozszerzamy możliwości dwóch metod: set, get, remove oraz each. W przypadku metod get, remove oraz each jest to zmiana kosmetyczna i wymaga jedynie zdefiniowana haków w odpowiednich miejscach. Natomiast w przypadku metody set, zmiany są o wiele większe. Oprócz standardowych wywołań haków, przed i po wykonaniu metody, musimy wprowadzić hak obsługujący przypadek, gdy pojemność danej instancji zostaje zapełniona. Jak można zauważyć, w sytuacji, w której istnieje zachowanie obsługujące hak “full” (sprawdzane za pomocą funkcji utils.isHookCovered), dwa razy jest sprawdzane czy instancja Store nie została zapełniona, raz by wywołać haka “full”, a następnie zaraz po wywołaniu tego haka, by sprawdzić czy nie należy rzucić błędem o zapełnieniu całej przestrzeni instancji. Po wywołaniu haka o nazwie “full” nie mamy gwarancji, że zachowanie jakie zostanie wywołane na danej instancji faktycznie zwolni wystarczającą ilość miejsca ani czy w ogóle zrobi cokolwiek. Gdybyśmy nie sprawdzali wyniku działania zachowania przypisanego na haku “full”, moglibyśmy doprowadzić do błędnego działania biblioteki. Store.prototype = { set: function( key, value, options ) { options = options || {}; utils.callHook( this.id, ’beforeSet’ ); var pkey = utils.addPrefix( key, this.id ), old_size = utils.getItemLength( this.id, pkey ), new_size = utils.getItemLength( this.id, pkey, value ); if ( utils.isHookCovered( this.id, ’full’ ) && utils.isFull( this.id, new_size, old_size ) ) { this.size = utils.callHook( this.id, ’full’, new_size, old_size ); } if ( utils.isFull( this.id, new_size, old_size ) ) { throw new Error( ERR_FULL ); } this.storage.setItem( pkey, value ); this.size -= old_size; this.size += new_size; if ( this.id !== store.meta.id ) { utils.setMeta( key, this.id, { createDate: new Date().getTime(), expireDate: options.expireDate instanceof Date && options.expireDate.getTime() }); } },

utils.callHook( this.id, ’afterSet’ );

get: function( key, _hooks_off ) { !_hooks_off && utils.callHook( this.id, ’beforeGet’ ); /* ... */ }, remove: function( key, _hooks_off ) { !_hooks_off && utils.callHook( this.id, ’beforeRemove’ ); /* ... */ !_hooks_off && utils.callHook( this.id, ’afterRemove’ ); },

}

each: function( fn, _hooks_off ) { !_hooks_off && utils.callHook( this.id, ’beforeEach’ ); /* ... */ !_hooks_off && utils.callHook( this.id, ’afterEach’ ); }

41

Usuwanie najstarszych elementów, gdy Store jest pełen Mając już cały mechanizm haków na miejscu możemy przejść do stworzenia skryptu, który będzie modyfikował zachowanie dowolnej instancji Store. Zachowanie to będzie podpięte tylko pod hak “full”, gdyż tylko on zostanie wywołany podczas przekroczenia limitu instancji Store. W pierwszej kolejności zaimplementujemy możliwość usuwania najstarszych elementów, gdy okaże się że instancja Store, na której operujemy została zapełniona. Skrypt w pierwszej kolejności odczyta metadane i klucze każdego elementu należącego do danej instancji, następnie posortuje odczytane klucze względem daty utworzenia każdego z elementów. Skrypt będzie usuwał najstarsze elementy tak długo, aż nie zwolni się w instancji Store wystarczająco dużo miejsca by pomieścić kolejny element, który ma zostać zapisany (jego rozmiar przechowywany jest w zmiennej additionalSize). Podczas usuwania elementów zapisanych przez daną instancje Store, będziemy usuwać także odpowiadające im metadane. !function() { ’strict’; if ( typeof window.store !== ’undefined’ && typeof window.store.registerBehavior === ’function’ ) { function byTimestampDSC( a, b ) { if ( a.createDate < b.createDate ) { return 1; } else if ( a.createDate > b.createDate ) { return -1; } return 0; } window.store.registerBehavior( ’earliest’, [ ’full’ ], function( instance, newItemSize, oldItemSize ) { var self = this; var list = []; var meta; var additionalSize = newItemSize - oldItemSize; instance.each(function( value, key ) { meta = self.getMeta( instance.id, key ); list.push({ key: key, pkey: self.addPrefix( key, instance.id ), value: value, createDate: meta.createDate }); }); list.sort( byTimestampDSC ); var size = 0, removeFromIndex = null; for ( var i = 0; i < list.length; ++i ) { size += list[ i ].pkey.length + list[ i ].value.length; if ( size + additionalSize >= instance.maxSize ) { removeFromIndex = i; break; } } if ( removeFromIndex !== null ) { list.splice( removeFromIndex ).forEach(function( item ) { self.addSizeCache( instance.id, -item.pkey.length - item.value.length ) instance.storage.removeItem( item.pkey ); self.removeMeta( instance.id, item.key ); }); } });

42

} }();

Przykład użycia var instance = store.create( ’A’, store.TYPE_LOCAL, { maxSize: 40, behavior: store.behavior.EARLIEST }); instance.clear(); // Każdy z elementów zapisujemy w różnych // odstępach czasowych, tak by każdy z nich otrzymał // w metadanych różny timestamp podczas zapisu. setTimeout(function() { instance.set( ’foo1’, ’a’ ); }, 10 ); setTimeout(function() { instance.set( ’foo2’, ’bb’ ); }, 200 ); setTimeout(function() { instance.set( ’foo3’, ’ccc’ ); }, 300 ); // Podczas zapisu ostatniego elementu, // okaże się, że nie ma już dla niego miejsca. // Biblioteka zamiast rzucić błędem, // wykona akcję ustawioną na hak ”full”. // Najwcześniejsze elementy będą usuwane tak // długo, aż nie zwolni się wystarczająco dużo // miejsca dla nowego elementu. setTimeout(function() { instance.set( ’foo4’, ’dddddd’ ); }, 400 ); // Odczytu dokonujemy setTimeout(function() console.log( ’1: %s’, console.log( ’2: %s’, console.log( ’3: %s’, console.log( ’4: %s’, }, 500 );

na samym końcu { instance.get( ’foo1’ instance.get( ’foo2’ instance.get( ’foo3’ instance.get( ’foo4’

) ) ) )

); ); ); );

// // // //

null null ccc dddddd

Usuwanie przeterminowanych elementów W przeciwieństwie do ciastek, w Storage pola nie posiadają  daty ważności. Wszystkie dane jakie zapiszemy w Storage zostają tam do momentu ich usunięcia przez skrypt lub użytkownika (np. poprzez wyczyszczenie danych w samej przeglądarce). Instancje Storage nie obsługują automatycznego usuwania danych po minięciu ich daty ważności, taką funkcjonalność musimy zapewnić sami. Mechanizm usuwania przeterminowanych pól może działać na dwa różne sposoby: 1. W interwałach czasowych można sprawdzać (np. co minutę), które pola należy usunąć jest to sposób niezbyt precyzyjny i będzie sprawdzał wszystkie elementy w instancji Store co jakiś czas niezależnie od tego czy jakiś termin został już przekroczony bądź nie. 2. Można też sprawdzać, które pola powinny być usunięte tylko wtedy, gdy wydarzy się jakaś akcja modyfikująca pola bądź pobieranie danych - plus tego rozwiązania jest taki, że gdy nie wykonujemy żadnych operacji na instancji Store, to żadne elementy nie są sprawdzane pod kątem daty ważności. Rozwiązanie to ma jednak pewien narzut w przypadku modyfikacji lub odczytu znacznej ilości elementów podczas operacji na dużej ilości elementów.

43

Poniższy skrypt jest zapisem zachowania, które odpowiada pierwszemu jak i drugiemu mechanizmowi. Skrypt rejestruje dwa różne zachowania oparte na tej samej funkcji, gdzie w pierwszym zachowaniu funkcja clearExpired jest wywoływana co 5 sekund, a druga wywoływana jest tylko i wyłącznie podczas wywołania odpowiednich haków w kodzie. Warto zwrócić uwagę na fakt, że wykorzystujemy tutaj możliwość wyłączenia haków podczas wykonywania metod each oraz remove. Wyłączamy haki poprzez podanie w ostatnim parametrze wartości typu Boolean równej true. Robimy to dlatego, by nie doprowadzić do pętli wykonywanej w nieskończoność. Gdybyśmy tego nie zrobili np. metoda remove także wywołałaby odpowiadający jej hak, który właśnie jest w trakcie wykonywania. Następnie cała operacja zostałaby wykonywana ponownie, aż do momentu rzucenia stosownym błędem przez przeglądarkę (o ile dana przeglądarka byłaby w stanie to wykryć). !function() { ‘strict’; if ( typeof window.store !== ‘undefined’ && typeof window.store.registerBehavior === ‘function’ ) { var HOOKS_OFF = true; var INTERVAL_MS = 5000; var situations1 = [ ‘full’, ‘afterSet’, ‘beforeGet’, ‘afterRemove’, ‘afterEach’ ]; var situations2 = [ ‘init’ ]; function clearExpired( utils, instance, newItemSize, oldItemSize ) { var now = Date.now(); var expired = []; instance.each(function( value, key ) { meta = utils.getMeta( instance.id, key ); if ( meta.hasOwnProperty( ‘expireDate’ ) && meta.expireDate !== ‘’ && meta.expireDate