Rozdział 10. Organizacja kodu jQuery

> Dodaj do ulubionych

Wprowadzenie

Kiedy już opanujesz dodawanie drobnych funkcji do swoich stron za pomocą jQuery i zaczniesz tworzyć zaawansowane aplikacje działające po stronie klienta, będziesz musiał nauczyć się organizować swój kod. W tym rozdziale przyjrzymy się różnym sposobom jego organizacji w aplikacjach jQuery i sprawdzimy, jak działa zarządzanie zależnościami poprzez narzędzie RequireJS oraz system kompilacji.

Podstawowe pojęcia

Zanim omówimy sposoby organizacji kodu, warto zapamiętać kilka pojęć, które charakteryzują wszystkie godne polecenia techniki.

  • Kod należy dzielić na funkcjonalne jednostki — moduły, usługi itp. Umieszczenie całości kodu w wielkim bloku $(document).ready() jest kuszące, lecz staraj się tego nie robić. Przedstawiona technika znana jest ogólnie jako hermetyzacja.
  • Nie powtarzaj się. Znajduj podobieństwa w funkcjach i wykorzystuj techniki dziedziczenia, aby uniknąć powtórzeń w kodzie.
  • Mimo że biblioteka jQuery jest z natury ściśle związana z modelem DOM, aplikacje JavaScript korzystają nie tylko z tego modelu. Pamiętaj, że nie wszystkie tworzone funkcje muszą — czy powinny — mieć swoją reprezentację DOM.
  • Jednostki funkcjonalności powinny być ze sobą luźno powiązane — każda z nich powinna działać osobno, a komunikację między poszczególnymi jednostkami należy obsługiwać przez system komunikacyjny, np. poprzez zdarzenia własne lub komunikaty publikowania/subskrypcji. Kiedy to tylko możliwe, staraj się unikać bezpośredniej komunikacji pomiędzy jednostkami funkcjonalności.

Luźne powiązania mogą sprawiać szczególne problemy programistom, którzy dopiero zaczynąją swoją przygodę z pisaniem zaawansowanych aplikacji.Warto zatem już na początku wiedzieć o ich istnieniu.

Hermetyzacja

Pierwszym krokiem w kierunku organizacji kodu jest wyodrębnienie elementów aplikacji, co samo potrafi być czasem wystarczająco skutecznym rozwiązaniem.

Literały obiektowe

Literał obiektowy stanowi prawdopodobnie najłatwiejszy sposób na hermetyzację powiązanych ze sobą części kodu. Korzystając z niego nie można utworzyć prywatnych własności ani metod, ale przydaje się on do usuwania funkcji anonimowych, centralizowania opcji konfiguracji, a także ułatwia przystosowanie kodu do ponownego wykorzystania i jego refaktoryzację.

Przykład 10.1. Literał obiektowy

var mojaFunkcjonalnosc = {
	mojaWlasnosc : 'witaj',
	 
	mojaMetoda : function() {
	    console.log(mojaFunkcjonalnosc.mojaWlasnosc);
	},
	 
	init : function(ustawienia) {
	    myFunkcjonalnosc.ustawienia = ustawienia;
	},
	 
	odczytajUstawienia : function() {
	    console.log(mojaFunkcjonalnosc.ustawienia);
	}
	};
	 
	mojaFunkcjonalnosc.mojaWlasnosc; // 'witaj'
	mojaFunkcjonalnosc.mojaMetoda(); // rejestruje 'witaj'
	mojaFunkcjonalnosc.init({ foo : 'bar' });
	mojaFunkcjonalnosc.odczytajUstawienia(); // rejestruje { foo : 'bar' }

Przedstawiony powyżej literał obiektowy jest po prostu przypisanym do zmiennej obiektem. Ma on jedną własność i kilka metod. Wszystkie z nich są publiczne, zatem dowolna część aplikacji ma do nich dostęp i może je wykorzystać. W powyższym kodzie umieszczona została również metoda init, jednak nie trzeba jej wywoływać zanim obiekt będzie funkcjonalny.

Jak więc wykorzystać literał obiektowy w kodzie jQuery? Załóżmy, że napisaliśmy poniższy kod w tradycyjnym stylu biblioteki:

// kliknięcie pozycji z listy spowoduje załadowanie jakiejś treści
	// korzystając z identyfikatora pozycji z listy
	// i ukrycie treści siostrzanych pozycji
	$(document).ready(function() {
	$('#mojaFunkcjonalnosc li')
	.append('<div/>')
	.click(function() {
	  var $this = $(this);
	  var $div = $this.find('div');
	  $div.load('foo.php?item=' +
	    $this.attr('id'),
	    function() {
	      $div.show();
	      $this.siblings()
	        .find('div').hide();
	    }
	  );
	});
	});

Jeśli powyższy kod stanowiłby całość aplikacji, mógłby być pozostawiony w obecnej formie. Jeśli jednak byłby częścią większego projektu, należałoby oddzielić tę funkcjonalność od reszty kodu. Warto byłoby również przenieść adres URL z kodu do obszaru konfiguracji. Na koniec można także podzielić łańcuch, aby ułatwić późniejszą modyfikację poszczególnych części funkcjonalności.

Przykład 10.2. Wykorzystanie literału obiektowego w funkcjonalności jQuery

var mojaFunkcjonalnosc = {
	init : function(ustawienia) {
	    mojaFunkcjonalnosc.config = {
	        $pozycje : $('#mojaFunkcjonalnosc li'),
	        $kontener : $('<div class="kontener"></div>'),
	        bazowyUrl : '/foo.php?item='
	    };
	 
	    // pozwól na przedefiniowanie domyślnej konfiguracji*
	    $.extend(mojaFunkcjonalnosc.config, ustawienia);
	 
	    mojaFunkcjonalnosc.ustaw();
	},
	 
	ustaw : function() {
	    mojaFunkcjonalnosc.config.$pozycje
	        .each(mojaFunkcjonalnosc.utworzKontener)
	        .click(mojaFunkcjonalnosc.pokazPozycje);
	},
	 
	utworzKontener : function() {
	    var $i = $(this),
	        $k = mojaFunkcjonalnosc.config.$kontener.clone()
	                 .appendTo($i);
	 
	    $i.data('kontener', $k);
	},
	 
	zbudujUrl : function() {B
	    return mojaFunkcjonalnosc.config.bazowyUrl +
	           mojaFunkcjonalnosc.$obecnaPozycja.attr('id');
	},
	 
	pokazPozycje : function() {
	    var mojaFunkcjonalnosc.$obecnaPozycja = $(this);
	    mojaFunkcjonalnosc.pobierzTresc(mojaFunkcjonalnosc.pokazTresc);
	},
	 
	pobierzTresc : function(wywolaniezwrotne) {
	    var url = mojaFunkcjonalnosc.zbudujUrl();
	    mojaFunkcjonalnosc.$obecnaPozycja
	        .data('kontener').load(url, wywolaniezwrotne);
	},
	 
	pokazTresc : function() {
	    mojaFunkcjonalnosc.$obecnaPozycja
	        .data('kontener').show();
	    mojaFunkcjonalnosc.ukryjTresc();
	},
	 
	ukryjTresc : function() {
	    mojaFunkcjonalnosc.$obecnaPozycja.siblings()
	        .each(function() {
	            $(this).data('kontener').hide();
	        });
	}
	};
	 
	$(document).ready(mojaFunkcjonalnosc.init);

Na pierwszy rzut oka można zauważyć, że zaprezentowany sposób jest znacznie dłuższy od oryginału — jeśli miałaby to być jedyna funkcjonalność aplikacji, użycie literału obiektowego byłoby przesadą. Jeśli natomiast aplikacja ma składać się z większej liczby funkcji, to dzięki literałowi możemy:

  • Podzielić funkcjonalność na kilka małych metod. Jeśli w przyszłości będziemy chcieć zmienić sposób pokazywania treści od razu będzie wiadomo, gdzie dokonać zmian, co nie było tak oczywiste w oryginalnym kodzie.
  • Usunąć funkcje anonimowe.
  • Przesunąć opcje konfiguracji poza główną treść kodu i umieścić je w centralnej lokalizacji.
  • Usunąć ograniczenia łańcucha, dzięki czemu kod będzie można łatwiej poddać refaktoryzacji i przekształceniom.

W przypadku zaawansowanych funkcji literały obiektowe znaczenie poprawiają organizację długich fragmentów kodu w bloku $(document).ready() i korzystając z nich uczymy się identyfikować poszczególne elementy funkcjonalności. Nie stanowią one jednak znaczniej bardziej zaawansowanego rozwiązania niż zwykłe deklaracje funkcji wewnątrz bloku $(document).ready().

Wzorzec modułu

Wzorzec modułu jest wolny od niektórych ograniczeń literału obiektowego, np. można zachować prywatne zmienne i funkcje przy jednoczesnym udostępnianiu publicznego API.

Przykład 10.3: Wzorzec modułu

var  unkcjonalność =(function() {
	 
	// prywatne zmienne i funkcje
	var rzeczPrywatna = 'tajne',
	    rzeczPubliczna = 'jawne',
	 
	    zmienRzeczPrywatna = function() {
	        rzeczPrywatna = 'scisle tajne';
	    },
	 
	    powiedzRzeczPrywatna = function() {
	        console.log(rzeczPrywatna);
	        zmienRzeczPrywatna();
	    };
	 
	// publiczne API
	return {
	    rzeczPrywatna : rzeczPrywatna,
	    powiedzRzeczPrywatna : powiedzRzeczPrywatna
	}
	 
	})();
	 
	funkcjonalnosc.rzeczPubliczna; // 'jawne'
	 
	funkcjonalnosc.powiedzRzeczPrywatna();
	// rejestruje 'tajne' i zmienia wartosc
	// zmiennej rzeczPrywatna

W powyższym przykładzie skorzystaliśmy z samowykonującej się funkcji anonimowej, która zwraca obiekt. Wewnątrz funkcji zdefiniowaliśmy zmienne, dzięki czemu nie można do nich uzyskać dostępu spoza obszaru funkcji, chyba że zostaną umieszczone w zwracanym obiekcie. Innymi słowy żaden kod znajdujący się poza funkcją nie ma dostępu do zmiennej rzeczPrywatna ani do funkcji zmienRzeczPrywatna. Funkcja powiedzRzeczPrywatna ma natomiast dostęp do zmiennej rzeczPrywatna i funkcji zmienRzeczPrywatna, ponieważ zostały one zdefiniowane w jej zakresie.

Wzorzec ten ma duże możliwości — jak można wywnioskować z nazw zmiennych, umożliwia korzystanie z prywatnych zmiennych i funkcji przy jednoczesnym udostępnianiu tylko części API, składającej się własności i metod zwracanego obiektu.

Poniżej znajduje się zmodyfikowana wersja poprzedniego przykładu. Tym razem ilustruje ona jak można utworzyć tę samą funkcjonalność wykorzystując wzorzec moduł przy jednoczesnym udostępnianiu tylko jednej publicznej metody modułu, pokazPozycjeWgIndeksu().

Przykład 10.4: Wykorzystanie wzorca modułu w funkcjonalności jQuery

$(document).ready(function() {
	var  unkcjonalność = (function() {
	 
	    var $pozycje = $('#mojaFunkcjonalnosc li'),
	        $kontener = $('<div class="kontener"></div>'),
	        $obecnaPozycja,
	 
	        bazowyUrl = '/foo.php?item=',
	 
	        utworzKontener = function() {
	            var $i = $(this),
	                $k = $kontener.clone().appendTo($i);
	 
	            $i.data('kontener', $k);
	        },
	 
	        zbudujUrl = function() {
	            return bazowyUrl + $obecnaPozycja.attr('id');
	        },
	 
	        pokazPozycje = function() {
	            var $obecnaPozycja = $(this);
	            pobierzTresc(pokazTresc);
	        },
	 
	        pokazPozycjeWgIndeksu = function(idx) {
	            $.proxy(pokazPozycje, $pozycje.get(idx));
	        },
	 
	        pobierzTresc = function(wywolaniezwrotne) {
	            $obecnaPozycja.data('kontener').load(zbudujUrl(), wywolaniezwrotne);
	        },
	 
	        pokazTresc = function() {
	            $obecnaPozycja.data('kontener').show();
	            ukryjTresc();
	        },
	 
	        ukryjTresc = function() {
	            $obecnaPozycja.siblings()
	                .each(function() {
	                    $(this).data('kontener').hide();
	            });
	        };
	 
	    $pozycje
	        .each(utworzKontener)
	        .click(pokazPozycje);
	 
	    return { pokazPozycjeWgIndeksu : pokazPozycjeWgIndeksu };
	})();
	 
	 unkcjonalność.pokazPozycjeWgIndeksu(0);
	});

Zarządzanie zależnościami

Kiedy projekt osiągnie znaczny rozmiar, zarządzanie modułami skryptu robi się skomplikowane. Należy wówczas dzielić skrypty na sekwencje w odpowiedniej kolejności i trzeba poważnie zacząć myśleć o połączeniu skryptów w przeznaczoną do wdrożenia paczkę, tak, by do załadowania skryptów wystarczyło niewiele żądań. Kod może być również ładowany na bieżąco, po załadowaniu strony.

RequireJS, narzędzie do zarządzania zależnościami stworzone przez Jamesa Burke’a, ułatwia zarządzanie modułami skryptu oraz ich ładowanie w odpowiedniej kolejności. Z kolei dostępne w RequireJS narzędzie do optymalizacji w późniejszym etapie pracy pomaga odpowiednio połączyć skrypty bez konieczności zmiany kodu znacznikowego. Ponadto pozwala ono w łatwy sposób wczytać skrypty po załadowaniu strony, dzięki czemu pobieranie jej treści można rozłożyć w czasie.

RequireJS zawiera system modułów, pozwalający zdefiniować konkretne moduły. Użycie go nie jest jednak wymagane, aby móc korzystać z zarządzania zależnościami czy usprawnień budowy. Z czasem, jeśli w twoim kodzie znajduje się wiele modułów, które mają być ponownie wykorzystane, format modułów w RequireJS ułatwi pisanie zhermetyzowanego kodu, który może być ładowany na bieżąco. Format ten może przydać się zwłaszcza jeśli chcesz wprowadzić internacjonalizację (i18n), dokonać lokalizacji projektu w innych językach, a nawet wykorzystać usługi JSONP jako zależności. Zalety modułów RequireJS można dostrzec również w sytuacji, kiedy chcemy załadować łańcuchy HTML mając pewność, że będą one dostępne przed wykonaniem kodu.

Skąd wziąć RequireJS

Aby móc wykorzystać narzędzie RequireJS w jQuery, najłatwiej jest pobrać odpowiednią wersję tej biblioteki z wbudowanym RequireJS. W wersji tej usunięto te elementy RequireJS, które pokrywają się z funkcjami jQuery.

Korzystanie z RequireJS w jQuery

RequireJS można bardzo łatwo wykorzystać w kodzie strony: wystarczy dołączyć wersję biblioteki jQuery z wbudowanym RequireJS, a następnie dodać pliki aplikacji za pomocą funkcji require. W poniższym przykładzie przyjęto założenie, że zarówno odpowiednia wersja jQuery jak i inne skrypty znajdują się w katalogu scripts/.

Przykład 10.5. Korzystanie z RequireJS: prosty przykład

<!DOCTYPE html>
	<html>
	<head>
	    <title>jQuery+RequireJS — strona przykładowa</title>
	    <script src="scripts/require-jquery.js"></script>
	    <script>require(["app"]);</script>
	</head>
	<body>
	    <h1>jQuery+RequireJS — strona przykładowa</h1>
	</body>
	</html>

Funkcja require(["app"]) każe RequireJS załadować plik scripts/app.js. Narzędzie załaduje wszystkie zależności przekazane do funkcji require() bez rozszerzenia .js znajdujące się w tym samym katalogu, co skrypt require-jquery.js, co można jednak skonfigurować w inny sposób. Jeśli wolałbyś określić całą ścieżkę, możesz to zrobić następująco:

<script>require(["scripts/app.js"]);</script>

A co takiego znajduje się w skrypcie app.js? Kolejne wywołanie skryptu require.js, który załaduje wszystkie potrzebne na stronie skrypty i wykona operacje inicjacji. Poniższy przykładowy skrypt app.js ładuje dwie wtyczki — jquery.alpha.js i jquery.beta.js (nie odwołujemy się tutaj do prawdziwych wtyczek, to tylko przykłady). Wtyczki powinny znajdować się w tym samym katalogu co skrypt require-jquery.js:

Przykład 10.6. Prosty plik JavaScript z zależnościami

require(["jquery.alpha", "jquery.beta"], function() {
	// wtyczki jquery.alpha.js i jquery.beta.js zostały załadowane.
	$(function() {
	    $('body').alpha().beta();
	});
	});

Tworzenie możliwych do ponownego wykorzystania modułów z RequireJS

RequireJS ułatwia zdefiniowania możliwych do ponownego wykorzystania modułów poprzez metodę require.def(). Moduł RequireJS może mieć zależności, które mogą posłużyć do zdefiniowania modułu, a także zwracać wartość, np. obiekt lub funkcję, która może zostać wykorzystana przez kolejne moduły.

Jeśli moduł nie ma żadnych zależności, wówczas należy tylko przekazać jego nazwę jako pierwszy argument funkcji require.def(). Drugim argumentem będzie literał obiektowy, w którym zdefiniowane są własności modułu. Na przykład:

Przykład 10.7. Definicja modułu RequireJS, który nie ma zależności

require.def("moje/zwyklakoszulka",
	{
	    kolor: "czarny",
	    rozmiar: "uniwersalny"
	}
	);

Przykład ten będzie zapisany w pliku moje/zwyklakoszulka.js file.

Jeśli moduł ma zależności, można je określić w drugim argumencie metody require.def() (w postaci tablicy), a jako trzeci argument przekazać funkcję. Kiedy już wszystkie zależności będą załadowane, funkcja zostanie wywołana do zdefiniowania modułu. Jej argumentami będą wartości zwrócone przez zależności (w tym samym porządku, w jakim były dodawane do tablicy) i zwrócony zostanie przez nią obiekt definiujący moduł.

Przykład 10.8. Definicja modułu RequireJS z zależnościami

require.def("moje/koszulka",
	["moje/koszyk", "moje/inwentarz"],
	function(koszyk, inwentarz) {
	    //zwraca obiekt definiujący moduł "moje/koszulka"
	    return {
	        kolor: "niebieski",
	        rozmiar: "duzy"
	        dodajDoKoszyka: function() {
	            inwentarz.decrement(this);
	            koszyk.add(this);
	        }
	    }
	}
	);

W powyższym przykładzie utworzyliśmy moduł moje/koszulka, który jest zależny od modułów moje/koszyk i moje/inwentarz. Struktura plików na dysku jest następująca:

	moje/koszyk.js
	moje/inwentarz.js
	moje/koszulka.js

Funkcja definiująca moduł moje/koszulka zostanie wywołana dopiero po załadowaniu modułów moje/koszyk i moje/inwentarz, które będą jej argumentami. Należy pamiętać, by były one w takiej samej kolejności, w jakiej zostały dodane do tablicy. Obiekt zwrócony przez wywołaną funkcję będzie definiować moduł moje/koszulka. Jeśli posłużymy się tym sposobem, moduł moje/koszulka nie będzie obiektem globalnym. Zdecydowanie zaleca się tworzenie modułów lokalnych, bowiem dzięki temu na stronie może funkcjonować kilka modułów jednocześnie.

Moduły nie muszą zwracać obiektów; dozwolona jest dowolna prawidłowa wartość zwrotna funkcji.

Przykład 10.9. Definicja modułu RequireJS zwracającego funkcję

require.def("moje/tytuł",
	["moje/zaleznosc1", "moje/zaleznosc2"],
	function(zal1, zal2) {
	    // zwracamy funkcję, która zdefiniuje moduł "moje/tytuł". Funkcja pobierze lub ustawi
	    // tytuł okna.
	    return function(tytul) {
	        return tytul ? (window.tytul = tytul) : window.tytul;
	    }
	}
	);

W jednym pliku JavaScript powinien być tylko jeden moduł.

Optymalizacja kodu: narzędzie RequireJS do automatyzowania kompilacji

Po implementacji zarządzania zależnościami RequireJS, można bardzo łatwo dokonać optymalizacji strony. W tym celu pobierz plik źródłowy RequireJS i umieść go w dowolnym miejscu, najlepiej w innym katalogu niż zawierający pliki tworzonej strony. Na potrzeby tego przykładu umieściliśmy RequireJS w tym samym katalogu, co katalog webapp, który zawiera stronę HTML i katalog ze wszystkimi skryptami. Oto pełna struktura katalogów:

requirejs/ (katalog wykorzystywany do przechowywania narzędzi do automatyzowania kompilacji)
	webapp/app.html
	webapp/scripts/app.js
	webapp/scripts/require-jquery.js
	webapp/scripts/jquery.alpha.js
	webapp/scripts/jquery.beta.js

Następnie w katalogu zwierającym skrytpy require-jquery.js oraz app.js, utwórz plik o nazwie app.build.js, który będzie zawierać:

Plik konfiguracyjny RequireJS

{
	appDir: "../",
	baseUrl: "scripts/",
	dir: "../../webapp-build",
	//Jeśli chcesz, ukryj linię optimize w komentarzu
	// kod zminimalizowany przy pomocy narzędzia Closure Compiler
	// w prostym trybie optymalizacji
	optimize: "none",
	 
	modules: [
	    {
	        name: "app"
	    }
	]
	}

Korzystanie z narzędzia do automatyzowania kompilacji wymaga zainstalowanej Javy w wersji 6. Oprogramowanie to wymagane jest również przez narzędzie Closure Compiler służące do minimalizacji kodu JavaScript (jeśli lina optimize: "none" została zakomentowana).

Aby rozpocząć kompilację, przejdź do katalogu webapp/scripts i wykonaj poniższe polecenie:

	# systemy inne niż Windows
	../../ equires/build/build.sh app.build.js
	 
	# systemy Windows
	....requirejsbuildbuild.bat app.build.js

Teraz znajdujący się w katologu webapp-build skrypt app.js będzie dodatkowo zawierać wpisane bezpośrednio w kod wtyczki jquery.alpha.js i jquery.beta.js. Jeśli następnie załadujesz umieszczony w tym samym katalogu plik app.html, nie powinny pojawić się żadne żądania sieciowe dla plików jquery.alpha.js i jquery.beta.js.

Ćwiczenia

Moduł Portlet

Otwórz w przeglądarce plik /exercises/portlets.html, a także skorzystaj z pliku /exercises/js/portlets.js. Twoim zadaniem jest napisanie funkcji tworzącej portlet, która wykorzystuje wzorzec modułu, tak aby poniższy kod działał:

var myPortlet = Portlet({
	title : 'Curry',
	source : 'data/html/curry.html',
	initialState : 'open' // (otwarty) lub 'closed' (zamknięty)
	});
	 
	myPortlet.$element.appendTo('body');

Każdy portlet powinien składać się z elementu div zawierającego tytuł, treść, przycisk służący do otwierania i zamykania portletu, a także osobne przyciski do jego usuwania i odświeżania. Publiczne API portletu zwracanego przez funkcję Portlet powinno wyglądać następująco:

myPortlet.open(); // wymusza otwarcie
	myPortlet.close(); // wymusza zamknięcie
	myPortlet.toggle(); // przełącza stany (otwarty/zamknięty)
	myPortlet.refresh(); // odświeża treść
	myPortlet.destroy(); // usuwa portlet ze strony
	myPortlet.setSource('data/html/onions.html');
	// zmienia źródło

Autor: Rebecca Murphey

Źródło: http://github.com/rmurphey/jqfundamentals

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji CC BY-SA 3.0 US