Kod JavaScript dość często jest kompresowany w celu zmniejszenia jego rozmiaru a tym samym ilości danych wysyłanych do klienta czyli przeglądarki. Kod ten oprócz oczywistych zalet czyli zmniejszonego rozmiaru ma też poważną wadę - jest kompletnie nieczytelny dla człowieka. Wynika to z faktu, iż najczęściej posiada minimalną (potrzebną do poprawnego przetworzenia przez przeglądarkę) ilość białych znaków takich jak odstępy, znaki końca linii czy wcięcia czyli wszystkie te elementy, które czynią kod czytelnym. Często jest to zabieg celowy aby zaciemnić kod skutecznie utrudniając jego analizę bądź modyfikacje. Najczęściej jednak chodzi o zwykłe zmniejszenie rozmiaru.
Co więc zrobić aby przeanalizować skompresowany kod JS? Istnieje rozwiązanie stworzone przez polskiego programistę nazwane JsDecoder dostępne na stronie http://code.gosu.pl. Jest to skrypt napisany także w JS a jego działanie polega na odkompresowaniu kodu poprzez dodanie usuniętych podczas kompresji białych znaków czyli znaków odstępu, tabulacji i końca linii. Całość działa bez problemu także dla dużych skryptów (np. kod JS do map Google - efektem jest ponad 23 tyś. linii). Przykład wykorzystania znajduje się na tej stronie. Rozwiązanie oparte jest na zasadach Open Source i dostępne za darmo na stronie autora.

Aplikacje internetowe korzystając z technologii AJAX prawie zawsze muszą wymieniać dane pomiędzy interfejsem użytkownika (JS, Flash itp) a serwerem (PHP, ASP itp).

Jak wiadomo nie można wprost przesłać np. tablicy czy obiektu z funkcji JS do skryptu PHP korzystając z obiektu XMLHttpRequest gdyż struktura takiej tablicy czy obiektu zostanie utracona. Należy więc dane jakoś przekształcić a najlepiej do pojedynczego łańcucha znaków. Dobrze byłoby gdyby po stronie przeglądarki w skryptach JS nie trzeba było korzystać z dodatkowych bibliotek niepotrzebnie powiększających ciężar strony. Ideałem byłoby gdyby format był zwięzły i w miarę czytelny. Spełnienie wszystkich tych życzeń wydaje się być nierealne a jednak!

Doskonałym rozwiązaniem okazuje się być format JSON (wymawiane jak imię "Jason" -- j?'s?n) oznacza JavaScript Object Notation czyli notacja obiektów JavaScript.
Po pierwsze niezależnie od typu dane po zakodowaniu są zawsze pojedynczym ciągiem znaków np.:

JavaScript:
  1. var jsonData = '{"a":{b:["c","d","e"],"f":{"g"}}}';

Po drugie aby odkodować format JSON wystarczy wykonać jego kod np. poprzez funkcję eval():

JavaScript:
  1. var myObject = eval(jsonData);

Po trzecie jak widać format jest zwięzły, czytelny i przez wielu uważany za bardziej naturalny niż np. XML.
Wszystkie te zalety sprawiają, że jest on bardzo często stosowany do wymiany danych nie tylko w modelu AJAX. Dla innych języków niż JS istnieją biblioteki do kodowania/dekodowania formatu JSON. Pełna ich lista znajduje się na stronie http://json.org/json-pl.html, na której można znaleźć także pełny opis tego formatu. Dla PHP istnieje nawet rozszerzenie json (wbudowane od wersji 5.2), dzięki czemu nie musimy korzystać z żadnych dodatkowych skryptów.

Format ten umożliwia przechowywanie dowolnych danych taki jak łańcuchy znaków, liczby, funkcje i całe obiekty wraz z właściwościami czy metodami dzięki czemu można bardzo wygodnie przesyłać dowolne dane pomiędzy np. skryptem PHP.odpowiadającym na żądanie funkcji JS. Warto pamiętać, iż ciągi znaków są przesyłane jako Unicode.

O ile odkodowanie danych z formatu JSON następuje poprzez funkcję eval() o tyle zakodowanie danych wymaga użycia dodatkowej funkcji, którą znaleźć można na stronie http://www.json.org/js.html. Osobiście nie jestem zadowolony z powyższego rozwiązania ponieważ polega ono na stworzeniu prototypu który "dokleja" się do danych po odkodowaniu dlatego polecam inne rozwiązanie czyli zwykłą funkcję (autor nieznany):

JavaScript:
  1. function toJsonString(o) {
  2.     var UNDEFINED
  3.     switch (typeof o) {
  4.         case 'string': return '"' + encodeJS(o) + '"'
  5.         case 'number': return String(o)
  6.         case 'object':
  7.             if(o) {
  8.                 var a = []
  9.                 if (o.constructor == Array) {
  10.                     for (var i = 0; i <o.length; i++) {
  11.                         var json = toJsonString(o[i])
  12.                         if(json != UNDEFINED) a[a.length] = json
  13.               }
  14.                     return '[' + a.join(',') + ']'
  15.                 } else if (o.constructor == Date) {
  16.                     return 'new Date(' + o.getTime() + ')'
  17.                 } else {
  18.                     for (var p in o) {
  19.                         var json = toJsonString(o[p])
  20.                         if (json != UNDEFINED) a[a.length] = (/^[A-Za-z_]\w*$/.test(p) ? ('"' + p + '":') : ('"' + encodeJS(p) + '":')) + json
  21.                     }
  22.                     return '{' + a.join(',') + '}'
  23.                 }
  24.             }
  25.             return 'null'
  26.         case 'boolean'  : return String(o)
  27.         case 'function' : return
  28.         case 'undefined': return 'null'
  29.     }
  30. }
  31. function encodeJS(s) {
  32.     return (!/[\x00-\x19\'\\]/.test(s)) ? s : s.replace(/([\\'])/g, '\\$1').replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t').replace(/[\x00-\x19]/g, '')
  33. }

W PHP wykorzystując wbudowane bądź dodane rozszerzenie json można kodować/dekodować dane poprzez:

PHP:
  1. $data = json_decode($jsonData);
  2. $jsonData = json_encode($data);

Więcej o formacie JSON:
http://json.org/json-pl.html
http://pl.wikipedia.org/wiki/JSON
http://www.hunlock.com/blogs/Mastering_JSON_(_JavaScript_Object_Notation_)

Specyfikacja obiektowego modelu dokumentu W3C DOM wprowadza nowoczesny sposób obsługi zdarzeń. Niestety model ten nie jest dostępny w przeglądarce Internet Explorer (nawet w najnowszych wersjach) i aby go wykorzystać należy dodać alternatywny sposób jego obsługi.
Funkcja dodająca zdarzenie korzysta z metody addEventListener dla przeglądarek zdognych z DOM oraz attachEvent w przypadku Interet Explorer'a:

JavaScript:
  1. function addEvent(obj, type, fn) {
  2.     if(obj.addEventListener){
  3.         obj.addEventListener(type,fn,false);
  4.         return true;
  5.     } else if(obj.attachEvent) {
  6.         obj['e'+type+fn] = fn;
  7.         obj[type+fn] = function(){obj['e'+type+fn](window.event);}
  8.         var r = obj.attachEvent("on"+type,obj[type+fn]);
  9.         return r;
  10.     } else {
  11.         return false;
  12.     }
  13. }

Funkcja usuwająca zdarzenie korzysta z metody removeEventListener dla przeglądarek zdognych z DOM oraz detachEvent w przypadku Internet Explorer'a:

JavaScript:
  1. function removeEvent(obj,type,fn) {
  2.     if(obj.removeEventListener) {
  3.         obj.removeEventListener(type,fn,false);
  4.         return true;
  5.     } else if(obj.detachEvent) {
  6.         var r = obj.detachEvent('on'+type,obj[type+fn]);
  7.         obj[type+fn] = null;
  8.         return r;
  9.     } else {
  10.         return false;
  11.     }
  12. }

Aby programowo wywołać dodane zdarzenie należy skorzystać z metody disptachEvent dla przeglądarek zgodnych z modelem DOM oraz fireEvent dla Internet Explorer'a:

JavaScript:
  1. function triggerEvent(obj, type) {
  2.     if(obj.dispatchEvent) {
  3.         var evt = document.createEvent("MouseEvents");
  4.         evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
  5.         return obj.dispatchEvent(evt);
  6.     } else if(obj.fireEvent) {
  7.         return obj.fireEvent('on'+type);
  8.     } else {
  9.         return false;
  10.     }
  11. }

Często zachodzi potrzeba zapisania pewnych danych w przeglądarce klienta aby przyśpieszyć działanie serwisu bądź mieć możliwość wykorzystania tych danych po ponownym wejściu odwiedzającego na stronę. Można w tym celu wykorzystać ciasteczka, jednak ze względu na niewielki rozmiar danych, które można tam zapisać nie są one zbyt efektywnym sposobem przechowywania danych po stronie klienta. Znacznie lepszym sposobem jest skorzystanie z obiektów do tego przeznaczonych czyli globalStorage w przeglądarce Firefox 2 i userData w przeglądarce Internet Explorer 5+.

globalStorage w Firefox 2

W przypadku przeglądarki Mozilla Firefox 2 mamy do dyspozycji obiekty sessionStorage i globalStorage. Oba obiekty umożliwiają zapisywanie dowolnych danych z pamięci przeglądarki z tą różnicą, że pierwszy z nich usuwa dane po wygaśnięciu sesji czyli po zamknięciu okna przeglądarki a w przypadku drugiego okres ważności danych jest nieskończony. Ze względu na przydatność zajmę się tylko obiektem drugim czyli globalStorage. Maksymalna ilość danych w nim zapisanych wynosi 5 MB i właśnie tyle mamy do wykorzystania. Dane zapisywane są w pliku webappsstore.sqlite w katalogu profilu Firefoksa. Wykorzystując obiekt globalStorage należy zdefiniować domenę dla jakiej dane mają być dostępne. Można użyć zarówno pełnej nazwy domeny (np. bukox.pl), tylko końcówki domeny (np. pl) lub pustego ciągu oznaczającego, iż dane będą widoczne z każdej domeny. Przykład użycia:

JavaScript:
  1. store = globalStorage['bukox.pl'];
  2. // zapis danych do obiektu
  3. store.addItem('zmienna','wartosc');
  4.  
  5. // odczyt danych z obiektu
  6. alert(store.getItem('zmienna').value);
  7.  
  8. // usuniecie danych z obiektu
  9. store.removeItem('zmienna');

Więcej o obiektach storage w FF można znaleźć na stronie:
http://developer.mozilla.org/pl/docs/DOM:Storage
a specyfikacja DOM obiektu znajduje się pod adresem:
http://www.whatwg.org/specs/web-apps/current-work/#storage.

userData w Internet Explorer

Dla przeglądarki Internet Explorer począwszy od wersji 5 istnieje model userData, który także umożliwia zapisywanie danych użytkownika do pamięci przeglądarki. Rozmiar możliwych do zapisania danych jest zależny od strefy zabezpieczeń. Dla strefy internetu wynosi od 1 MB dla domeny i 128 KB dla dokumentu. Przykład użycia:

Najpierw należy wstawić dowolny znacznik, nadać mu identyfikator i odpowiedni styl:

HTML:
  1. <div id="globalStore" style="behavior: url(#default#userdata)"></div>

Następnie poprzez zwykłe nadanie atrybutu można dodać zmienną i wartość do obiektu:

JavaScript:
  1. // zapis danych do obiektu
  2. globalStore.setAttribute('zmienna','wartosc');
  3. globalStore.save('cache');
  4.  
  5. // odczyt danych z obiektu
  6. globalStore.load('cache');
  7. alert(globalStore.getAttribute('zmienna'));
  8.  
  9. // usuniecie danych z obiektu
  10. globalStore.load('cache');
  11. globalStore.setAttribute('zmienna','');
  12. globalStore.save('cache');

Więcej informacji o obiekcie userData w IE można znaleźć pod adresem:
http://msdn2.microsoft.com/en-us/library/ms531424.aspx.
Przykłady użycia można znaleźć na stronach: http://www.webreference.com/js/column24/userdata.html i http://www.eggheadcafe.com/articles/20010615.asp.

Dla innych przeglądarek póki co nie można wykorzystać podobnych mechanizmów i pozostaje jedynie skorzystanie z ciasteczek lub ograniczenie funkcjonalności tylko do dwóch powyższych. Ze względu na fakt, iż obiekt storage stanowi część specyfikacji DOM można mieć nadzieje, że w najbliższym czasie także pozostałe przeglądarki zostaną wyposażone w obsługę obiektu storage.

Mechanizm cache opisany powyżej umożliwia przechowywanie danych po stronie klienta i ich odczyt nawet po ponownym uruchomieniu przeglądarki. Dzięki temu można stworzyć aplikację internetową, która może działać całkowicie offline. Wystarczy, że podczas normalnej pracy online dynamiczne dane są zapisywane do cache'u a po rozłączeniu z internetem dane są z niego pobierane. Dodatkowo operacje modyfikujące także mogą zostać zapamiętane i po przywróceniu połączenia z internetem dane mogą zostać zsynchronizowane z serwerem.

Poniżej znajduje się kod kompletnej klasy implementującej cache po stronie klienta działający zarówno w IE5+ jak i FF2, który automatycznie tworzy ukrytą warstwę (dla IE) oraz umożliwia kontrolę okresu ważności zapisanych danych.

JavaScript:
  1. var TCache = function(domain) {
  2.     if(typeof globalStorage != "undefined") {
  3.         // jesli FF
  4.         if(typeof domain == "undefined") {
  5.             var domain = location.host;
  6.         }
  7.         this.storage = globalStorage[domain];
  8.     } else {
  9.         // jesli IE
  10.         if(!document.getElementById('globalStore')) {
  11.             // dynamicznie dodanie warstwy dla cache
  12.             var storageObj = document.createElement('DIV');
  13.             storageObj.id = 'globalStore';
  14.             storageObj.style.behavior = 'url(#default#userdata)';
  15.             storageObj.style.display = 'none';
  16.             document.body.appendChild(storageObj);
  17.         }
  18.         this.storage = globalStore;
  19.     }
  20. }
  21. TCache.prototype = {
  22.     /**
  23.      * Magazyn danych
  24.      *
  25.      * @var object
  26.      */
  27.     storage : null,
  28.     /**
  29.      * Funkcja zapisuje cache
  30.      *
  31.      * @param string name   Nazwa zmiennej cache 
  32.      * @param mixed data    Dane do zapisania
  33.      * @param int expires   Okres waznosci danych (w sek., domyslnie bez limitu)
  34.      * @return bool
  35.      */
  36.     save : function(name, data, expires) {
  37.         if(typeof globalStorage != "undefined") {
  38.             // jesli FF
  39.             this.storage.setItem(name, data);
  40.             if(typeof expires != "undefined") {
  41.                 var _this = this;
  42.                 setTimeout(function(){this.remove(name)}, expires*1000);
  43.             }
  44.             return true;
  45.         } else if(typeof this.storage.XMLDocument != "undefined") {
  46.             // jesli IE
  47.             if(typeof expires != "undefined") {
  48.                 var oTimeNow = new Date();
  49.                 oTimeNow.setSeconds(oTimeNow.getSeconds() + expires);
  50.                 this.storage.expires = oTimeNow.toUTCString();
  51.             }
  52.             this.storage.setAttribute(name, data);
  53.             this.storage.save('cache');
  54.             return true;
  55.         } else {
  56.             return false;
  57.         }
  58.     },
  59.     /**
  60.      * Funkcja pobiera dane z cache
  61.      *
  62.      * @param string name   Nazwa zmiennej cache
  63.      * @return mixed
  64.      */
  65.     get : function(name) {
  66.         if(typeof globalStorage != "undefined") {
  67.             // jesli FF
  68.             if(this.storage.getItem(name)) {
  69.                 var result = this.storage.getItem(name).value;
  70.             }
  71.         } else if(typeof this.storage.XMLDocument != "undefined") {
  72.             // jesli IE
  73.             this.storage.load('cache');
  74.             var result = this.storage.getAttribute(name);
  75.         }
  76.         return (result) ? result : null;
  77.     },
  78.     /**
  79.      * Funkcja usuwa zmienna z cache
  80.      *
  81.      * @param string name   Nazwa zmiennej
  82.      */
  83.     remove : function(name) {
  84.         if (typeof globalStorage != "undefined") {
  85.             // jesli FF
  86.             this.storage.removeItem(name);
  87.             return true;
  88.         } else if(typeof this.storage.XMLDocument != "undefined") {
  89.             // jesli IE
  90.             this.storage.load('cache');
  91.             this.storage.setAttribute(name, '');
  92.             this.storage.save('cache');
  93.             return true;
  94.         } else {
  95.             return false;
  96.         }
  97.     }
  98. }

Sposób użycia:

JavaScript:
  1. var cache = new TCache();
  2. cache.save('imie','adam');
  3. alert(cache.get('imie'));