Kilka miesięcy temu na łamach Internet Makera opisywaliśmy ciekawy framework JavaScript, służący do tworzenia zaawansowanych interfejsów przypominających wyglądem i zachowaniem interfejsy okien w systemach operacyjnych. Dzisiejszy artykuł ma za zadanie pokazać rozwiązania kilku problemów, na które natkniesz się pracując z większymi projektami ExtJS.
W swojej pracy zawodowej wykorzystałem ExtJS do tworzenia projektu, którego na co dzień używa około 1000 użytkowników, w tym do kilkudziesięciu użytkowników jednocześnie. Ponieważ w systemie przechowywane są dane z około 30 składnic danych, ich synchronizacja w trybie rzeczywistym była sporym wyzwaniem. Dlatego w tej części artykułu poświęcę temu tematowi sporo uwagi.
Na koniec dowiesz się, w jaki sposób uniknąć łatwego do przeoczenia, ale irytującego błędu w kodzie, który w przeglądarce Internet Explorer całkowicie uniemożliwia uruchomienie aplikacji napisanej z wykorzystaniem w ExtJS. Dalsze sztuczki w drugiej części artykułu już niebawem.
Synchronizacja dużej ilości danych z serwera
W poprzednim artykule (http://serwis.magazynyinternetowe.pl/artykul/4486,1,2036,framework_ext_js_-_nowoczesne_interfejsy_z_wykorzystaniem_javascript.html) dotyczącym ExtJS opisany został mechanizm obsługi składnicy danych (ang. store), czyli obiektów, w których przechowywane są dane tabelaryczne. Te stanowią później podstawę dla budowy gridów, czyli formatek o szerokiej funkcjonalności, od prezentacji prostych tabel, aż po zaawansowaną funkcjonalność, zbliżającą się w kierunku arkuszy kalkulacyjnych.
Jedną z ciekawszych właściwości takich składnic danych jest ich zdolność do automatycznej synchronizacji z serwerem. Aby skorzystać z takiej możliwości należy w czasie definiowania składnicy podać adres URL z danymi otrzymanymi z serwera. Możesz to zrobić na przykład w taki sposób:
var store = new Ext.data.Store({
url: \'produkty.xml\', autoLoad: true, reader: new Ext.data.XmlReader({record: \'pozycja\'}, [\'produkt\',\'cena\',\'ilosc\', \'dostawa\']
)
});
Powyższy fragment kodu będzie współpracować z dokumentem XML, którego struktura została zaprezentowana w artykule wprowadzającym do ExtJS (TU LINK PACIInternet Maker nr 3/08). Kod można dowolnie dostosowywać do własnych potrzeb.
Dzięki zastosowanej instrukcji autoLoad natychmiast po zainicjowaniu składnicy, system połączy się z adresem o wskazanym URL (w tym przypadku jest to adres względny) i na podstawie pobranego XML wypełni składnicę danymi. Za każdym razem, gdy konieczna będzie aktualizacja danych można wywołać programowo instrukcję:
store.reload();
Ponieważ jednak system zgodnie z założeniem miał jak najbardziej przypominać aplikację okienkową, istotne było, aby dane aktualizowane były automatycznie, bez konieczności ręcznego przeładowania strony. Dlatego zdecydowaliśmy się na ponowne pobieranie danych ze składnic w określonym interwale czasowym, który wynosił początkowo 10 sekund.
Aby zaimplementować automatyczną aktualizację danych, wykorzystany został obiekt klasy Ext.util.TaskRunner. Obiekt taki posiada metodę start(), uruchamiający wykonanie zadania przekazanego w parametrze. Oto sposób uruchamiania TaskRunnera:
var runner = new Ext.util.TaskRunner();
runner.start(task);
Należy do niego przekazać zadanie zapisane w zmiennej task. Przy okazji definiowania zadania, ważne są dwa klucze: run oraz interval. Pierwszy pozwala wskazać funkcję, która będzie realizowana w momencie wywołania zadania przez TaskRunnera, natomiast interval ustala odstęp czasowy w jakim zadanie będzie wywoływane (podany w milisekundach). A więc poniższy kod spowoduje przeładowanie składnicy danych w odstępie 10 sekund:
var task = {
run: function(){
store.reload(); }, interval: 10000 //10 sekund
}
Takie rozwiązanie sprawia, że dane w systemie będą zawsze aktualne. Nawet jeśli inny użytkownik wprowadzi nowy dokument do systemu, pozostałe zalogowane osoby będą widzieć zmiany w okresie czasu nie przekraczającym 10 sekund.
Problem pojawia się jednak wraz z przyrostem ilości obsługiwanych składnic. W swojej pracy tworzyłem system wspierający obsługę księgowości, konieczne było więc zbieranie wielu danych bardzo różnego rodzaju – od faktur VAT, poprzez dokumenty kosztowe, nazwy kontrahentów, katalogi produktów, listy rachunków do umów, a skończywszy na liście użytkowników uprawnionych do korzystania z systemu i wielu innych mniej lub bardziej widocznych na pierwszy rzut oka informacji.
I teraz, jeśli takie składnice byłyby odświeżane co 5-10 sekund, oznaczało by to, że w każdej sekundzie jest nawet kilka zapytań jednocześnie do serwera. Od zaledwie jednego użytkownika! Oznacza to po pierwsze spadek szybkości działania aplikacji po stronie klienta, jako że JavaScript nie należy do szybkich języków – intensywne przetwarzanie wciąż nowych danych potrafiło by dość szybko doprowadzić do zauważalnego spadku komfortu korzystania z przeglądarki.
Jeszcze większym problemem jest jednak aspekt serwerowy. Do systemu, który stworzyliśmy dostęp posiada blisko 1000 osób ze struktur organizacyjnych naszego klienta na terenie całej Polski. W godzinach szczytu z systemu korzysta od kilkunastu do kilkudziesięciu użytkowników jednocześnie (i to pomimo że po 10 minutach bezczynności użytkownicy są wylogowywani).
Oznacza to setki zapytań na sekundę, a ponieważ mamy do czynienia z systemem wspierającym księgowość, który z założenia przechowuje dane o skomplikowanej strukturze, generowanie odpowiedzi na niektóre zapytania wymaga często intensywnych operacji na całej bazie danych. Czyli w różnych tabelach, z wielokrotnymi złączeniami, a następnie dalszą obróbką po stronie skryptów. Wszystko to oznacza, że platforma sprzętowa z całą pewnością odmówi posłuszeństwa i system stanie.
To poważny problem. Istnieje jednak proste remedium. Większość danych wcale nie musi być aktualizowana co 10 sekund. Owszem, aby zachować dynamizm uaktualniania systemu w środowisku w którym pracuje wielu użytkowników, należy nieustannie sprawdzać czy pojawiły się nowe informacje. Nie musi to jednak oznaczać pobierania każdorazowo wszystkich dostępnych danych. Dla przykładu – nowa faktura nie jest dodawana do systemu z częstotliwością 10 sekund, tylko znacznie rzadziej. To oznacza, że nie ma potrzeby pobierania w takim odstępie czasu listy wszystkich faktur. Zamiast tego można wysłać zapytanie o to, czy pojawiły się jakieś zmiany na liście, a jeśli tak – dopiero wysłać kolejne zapytanie o jej aktualizację.
Ponadto, jeśli składnic danych jest dużo, zamiast wysyłać dla każdej z nich osobne zapytanie, można je wszystkie połączyć w jedno. W tworzonym systemie założono, że realizowane jest jedno zapytanie w cyklicznym, krótkim odstępie czasu (np. 5 sekund). Dodatkowo to samo zapytanie dokonuje sprawdzenie czy wygasła sesja użytkownika. Jeśli tak to w warstwie ExtJS jest dokonywane przejście do ekranu ponownego logowania.
Jeśli chodzi o sposób implementacji takiego rozwiązania, po stronie serwera praktycznie całość została przerzucona na mechanizm bazodanowy – wybrana do obsługi baza danych PostgreSQL daje tutaj znacznie potężniejsze możliwości niż system MySQL.
Natomiast po stronie ExtJS stworzona została jedna dodatkowa składnica, która przyjmuje przesyłane XML-em informacje o tym które dodatkowe składnice należy zaktualizować. Ponieważ składnica w ExtJS ma dwa wymiary, przyjęliśmy że zawiera tylko jeden wiersz i tyle kolumn, ile innych składnic objętych jest monitoringiem. Oto jak wygląda to w praktyce:
var task = {
run: function()
{
// Wykonuj tylko jeśli otrzymalismy XML z jakimis danymi
if (storeLogOff.getCount()!= 0 )
{
if (storeLogOff.getAt(0).data[\'faktury_vat\'] == \'true\' )
{
storeListaFaktur.reload();
storeListaPropozycji.reload();
}
if (storeLogOff.getAt(0).data[\'towary\'] == \'true\')
{
storeFakturaTowary.reload();
storeTowaryWszystkie.reload();
}
if (storeLogOff.getAt(0).data[\'kontrahenci\'] == \'true\')
{
storeKontrahenci.reload();
}
// Sprawdzenie czy sesja jest aktywna
if (storeLogOff.getAt(0).data[\'wyloguj\'] == 0) //sesja aktywna
{
storeLogOff.reload();
}
else if (storeLogOff.getAt(0).data[\'wyloguj\'] == 1) // sesja nieaktywna
{
Ext.Msg.alert(\'Status\', storeLogOff.getAt(0).data[\'responseMsg\']);
document.location.href = \'logout.php\';
}
}
},
interval: 5000 // Powtarzaj co 5 sekund
}
var runner = new Ext.util.TaskRunner();
runner.start(task);
Przy dużej ilości danych w często zmieniających się składnicach można pokusić się o to, aby pójść krok dalej – nie przeładowywać całej składnicy gdy pojawi się modyfikacja, a jedynie przesyłać informacje aktualizujące. W nadmiarowej kolumnie można dodać informację o tym, czy dany wiersz jest dodawany, usuwany, czy zmieniany – za pomocą JavaScript można na tej podstawie dokonać programowej modyfikacji zawartości składnicy danych. Pozostawimy to jednak inwencji własnej zainteresowanym czytelnikom.
Komunikat o wczytywaniu danych
Jak już zostało wspomniane, wraz z rozrostem systemu zaczynało przybywać danych do ściągnięcia. Oznaczało to nie tylko konieczność dążenia do jak najszerszej optymalizacji tego mechanizmu, ale w pierwszej kolejności przede wszystkim do wyświetlenia komunikatu o tym, że określone dane są wczytywane. Podnosi to użyteczność serwisu tworzonego z wykorzystaniem ExtJS. Użytkownik wie, że to, iż wciąż nie widzi jeszcze wszystkiego na ekranie jest związane z tym, że część danych wciąż jest pobierana z serwera.
W przypadku wspomnianego systemu wspierającego księgowość postanowiliśmy wykorzystać pasek postępu (ang. progress bar) dla celów informacji o tym, że proces komunikacji z serwerem wciąż trwa. Pasek postępu w ExtJS działa domyślnie w sposób zapętlony – po dotarciu paska symbolizującego ładowanie do prawego skrajnego brzegu, jest on zerowany i wczytuje się od nowa.
Przypomina to nieco pasek postępu ładowania stron ze starszych wersji przeglądarek Internet Explorer. Ponieważ pasek jest zapętlony, świetnie sprawdza się tam, gdzie czas ładowania ani stopień zaawansowania procesu nie są znane.
Wdrożenie takiego paska postępu jest bardzo proste. W miejscu, gdzie wykonywana jest operacja, która może zająć trochę czasu – na przykład wywołanie załadowania danych do składnicy, należy uaktywnić pasek:
wait_progress = Ext.MessageBox.wait( \"Proszę czekać, trwa wczytywanie danych.\",\"Informacja\",{id:\'propmsg\'});
Wywoływana jest metoda wait() klasy Ext.MessageBox. Jako parametry przyjmuje ona kolejno – treść komunikatu, tytuł okna oraz ustawienia dodatkowe. W omawianym systemie ustawiono tylko wartość id, która definiuje identyfikator obiektu w strukturze DOM. Dzięki temu pasek postępu zawsze będzie jedną i tą samą warstwą, niezależnie od tego ile razy zostanie wywołany.
Tak wywołana metoda zwróci obiekt paska zadań, który zapisany został do zmiennej wait_progress. Od tej chwili pasek postępu już działa. Pasek będzie działać \”w kółko\”, aż do momentu gdy nastąpi jego programowe wyłączenie:
wait_progress.hide();
W rzeczywistości warto zadać sobie pytanie kiedy tak naprawdę warto wywoływać pasek, a kiedy go chować? Jeśli przykładowo jest on wykorzystywany przy obsłudze wspomnianych składnic danych, powinniśmy uruchamiać informację o ładowaniu danych w momencie inicjowania komunikacji z serwerem, a kończyć gdy komunikacja się zakończy. Zwykle jednak nie można przewidzieć jaki czas będzie ona trwać. Jak więc to zrealizować?
Jest to idealne zadanie do realizacji z wykorzystaniem zdarzeń. Można tu bowiem wykorzystać dostarczone w ramach ExtJS zdarzenia \”beforeload\” i \”load\” obsługujące składnice danych. Zdarzenia te są wywoływane kolejno – przed zainicjowaniem transmisji z serwerem oraz po załadowaniu danych. Oto przykład:
// Tu należy zdefiniować składnicę danych
var store = new Ext.data.Store({ [...] });
// Zmienna wait_progress powinna mieć szerszy zasięg
var wait_progress;
// Zdarzenie beforeload - pokazuje pasek
store.addListener(\'beforeload\', function() {
wait_progress = Ext.MessageBox.wait( \"Proszę czekać, trwa wczytywanie danych.\",\"Informacja\",{id:\'propmsg\'});
});
// Zdarzenie load - ukrywa pasek
store.addListener(\'load\', function() {
wait_progress.hide();
});
Zastosowanie mechanizmu paska postępu ma jeszcze jedną cenną właściwość. Pasek wczytywania jest wrzucany na pierwszy plan i uniemożliwia użytkownikowi korzystanie z tego co znajduje się w tle.
Jest to o tyle ważne, że niejednokrotnie przed wdrożeniem tego mechanizmu zdarzało się, że użytkownicy klikali w opcje programu, które działały błędnie, dopóki nie zakończyło się pobieranie wszystkich danych. To potrafiło powodować lawinę dodatkowych błędów. Problem ustał po wdrożeniu rozwiązania z paskiem postępu.
Na koniec: uważaj na Internet Explorera
Drobnym, ale dokuczliwym problemem przy wdrażaniu projektów wykorzystujących ExtJS (ale także innych frameworków opierających się na JavaScript) może być nieznaczna odmienność składniowa w interpretacji kodu JavaScript przeglądarki Internet Explorer i pozostałej dwójki – Firefoxa i Opery. Ponieważ programiści tworzący nasz system pracują na co dzień z wykorzystaniem dwóch ostatnich przeglądarek, pewnego dnia nie lada konsternację wywołał fakt, że w czasie testów kolejnej wersji systemu pod Internet Explorerem okazało się… że wyświetla się tylko pusta strona z lakonicznym i niewiele mówiącym opisem błędu JavaScript. Wskazywany przez debugger błąd był daleki od rzeczywistego źródła problemów.
Co się okazało? Wszystkiemu winien był mały, wydawać by się mogło – niewiele znaczący przecinek. Zwróć uwagę na poniższy fragment kodu:
var tree = new Tree.TreePanel({
fitToFrame: true,
loader: new Tree.TreeLoader({requestMethod: \'GET\',dataUrl:\'tree.xml\'}),
rootVisible: false,
title: \"Drzewo\",
});
Na samym końcu pozostawiony został przecinek – tuż przed zamknięciem nawiasów. Błąd taki teoretycznie nie powinien się zdarzyć, nie ma bowiem sensu stawianie przecinka w takim miejscu. W praktyce jednak przy przekopiowywaniu fragmentów kodu łatwo zapomnieć o jego usunięciu. Ponieważ tak pozostawiony przecinek nie stanowi problemu ani dla przeglądarki Firefox, ani dla Opery – w których najczęściej pisze się projekty, błąd nie zostanie zauważony od razu.
Jednak już dla Internet Explorera będzie to błąd składniowy, który uniemożliwi wykonanie skryptu. Dlatego warto uważać na pozostawiony przecinek, a także regularnie testować swój projekt pod IE. Gdy projekt się rozrośnie do kilkudziesięciu tysięcy linii – znalezienie miejsca, w którym popełniono taki lapsus nie będzie łatwe.
Ciąg dalszy sztuczek z ExtJS już niebawem.