JavaScript w projekcie internetowym

Podziel się z innymi!

    JavaScript jest technologią kojarzoną ze stronami WWW. Uruchamiany w przeglądarce internetowej język przez długi czas stanowił dodatek ożywiający jedynie statyczną treść. Sam przez wiele lat trzymałem się paradygmatu, że strona internetowa powinna być w pełni funkcjonalna i czytelna nawet jeśli użytkownik wyłączy w swojej przeglądarce obsługę JavaScript-u. Ta reguła jest nadal dobrze widzianą praktyką ale rzadziej przestrzeganą, gdyż pozbawiony javascript-owego dopingu interfejs użytkownika na tyle traci na wygodzie i atrakcyjności, że już mało kto decyduje się na skrajne ustawienia bezpieczeństwa.

    W chwili obecnej ilość kodu pisanego w JavaScript-cie powoli zrównuje się z ilością kodu tworzonego w językach działających po stronie serwera, a podejrzewam, że są i serwisy gdzie ją przewyższa. Programiści frontendowi zyskują coraz bardziej na znaczeniu bo też wymagania stawiane im są coraz większe. Dzisiaj pracodawcy nie szukają „koderów” potrafiących dołączyć i uruchomić cudzy, znaleziony gdzieś w sieci skrypt, którego jedynym zadaniem jest rozwijanie i zwijanie menu. Dzisiaj tworzy się rozbudowane aplikacje oparte na profesjonalnych frameworkach stworzonych przez – nierzadko duże (np. Yahoo YUI Library) – firmy i mające szerokie wsparcie wśród społeczności (np. jQuery) internetowych.

    Im bardziej złożone programy, im większa ilość skryptów – tym zarządzanie i utrzymywanie kodu JavaScript staje się trudniejsze. Fakt, że interpreterem języka JavaScript jest przeglądarka internetowa czyni sprawę jeszcze bardziej skomplikowaną gdyż musimy wziąć pod uwagę kilka dodatkowych aspektów związanych ze specyficznym środowiskiem w jakim pisane w tym języku programy są uruchamiane.

    Osadzać JavaScript w kodzie strony HTML czy wydzielać do plików js?

    Nie ma zastrzeżeń natury techninej ograniczających ilość kodu w źródle strony lub też określających co powinno być przeniesione do plików zewnętrznych. Zdrowy rozsądek oraz zasada, że należy rozdzielać logikę od warstwy wizualnej podpowiadają, że kod osadzony inline-owo powinien być jak najskromniejszy, a wszystko co nie jest specyficzne dla danej podstrony powinno zostać umieszczone w pliku.

    Przemawiają za tym zarówno względy estetyczne jak i pragmatyczne. Kod umieszczony w pliku łatwiej się debuguje, można go skompresować, poddać obfuskacji i dołączyć do innych stron. Ponadto przeglądarki domyślnie buforują pliki js – chyba, że mają inne ustawienia – co oszczędza liczbę żądań (requestów) kierowanych do serwera.

    Skrypt osadzony powinien być umieszczony w jednym miejscu. Przeplatanie kodu JavaScript między innymi elementami HTML-a dopuszczalne jest tylko w przypadku obsługi zdarzeń.

    <script type="text/javascript">
        function someFunction() { alert('OK'); }
    </script>
    <input id="somebutton" type="button" value="Call function" onclick="someFunction();" />

    Jest to już trochę archaizm ponieważ dzięki nasłuchiwaczom (ang. listener) można każde zdarzenie obsłużyć bez dodawania do tagów HTML atrybutów onclick, onmouseover itd. Nicholas C. Zakas w książce pt. „JavaScript dla webmasterów – zaawansowane programowanie” opisuje różnice w implementacji tego mechanizmu w różnych przeglądarkach. Wspominam o tym tylko jako o ciekawostce gdyż każdy z czołowych frameworków udostępnia ujednolicone api do obsługi zdarzeń.

    Przykładowy kod z użyciem jQuery pokazuje jak podpiąć funkcję pod określone zdarzenie wybranego obiektu na stronie.

    $(document).ready(function() {
        $('#somebutton').click(someFunction);
    });

    Kod ten umieszczony w nagłówku strony uruchomi się po załadowaniu całego drzewa DOM. Wynika z tego kilka niedogodności.

    Wszelkie inicjowane JavaScript-em widżety pojawią się z widocznym opóźnieniem – zostaną uruchomione dopiero po wyrenderowaniu całej strony. Niecierpliwi użytkownicy mogą zacząć klikać w kalendarzyk zanim jeszcze zostanie uruchomiony, albo zatwierdzać formularza bez podpiętych javascript-owych walidatorów. Jest to cena za czystość i porządek w kodzie.

    Czy można sterować cache-owaniem plików JavaScript w pamięci podręcznej przeglądarki?

    Podczas pierwszej wizyty na stronie WWW wszystkie jej elementy zostają pobrane i w zależności od ustawień przeglądarki oraz przesłanych nagłówków HTTP zapisane w pamięci podręcznej przeglądarki. Buforowanie (ang. cache) oszczędza liczbę żądań wysyłanych do serwera, transfer i przyśpiesza ładowanie strony. (Użyj about:cache w Firefox jeśli chcesz zobaczyć co zawiera Twój cache)

    Do sterowania zachowaniem cache-a służą nagłówki HTTP (ang. HTTP Headers). Dla treści dynamicznej najlepiej skorzystać z Cache-Control i tak aby zapobiec buforowaniu dokumentu należy wysłać następujące nagłówki.

    header("Pragma: no-cache");
    header("Cache-Control: max-age=0, no-store, no-cache, must-revalidate, post-check=0, pre-check=0");
    header("Expires: 0");

    UWAGA! Można w tym celu użyć meta tagów ale nie wszystkie przeglądarki honorują meta tagi strujące cachem i nie są one brane pod uwagę przez serwery proxy

    <head>
        <meta http-equiv="Expires" content="0" />
        <meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate" />
        <meta http-equiv="Cache-Control" content="post-check=0, pre-check=0" />
        <meta http-equiv="Pragma" content="no-cache" />
    </head>

    Przy rzadko zmieniającej się treści lepsze są inne ustawienia bufora.

    header("Cache-Control: max-age=3600, must-revalidate, post-check=600, pre-check=1800");

    przeglądarka powinna przez 600 sekund podawać dokument wyłącznie z bufora, następnie przez 1200 sekund brać dokument z bufora, ale też sprawdzać w tle czy na serwerze jest nowsza wersja (i jeżeli jest to ją zaciągnąć do kolejnego użycia). Po upłynięciu 1800 sekund od pobrania przeglądarka powinna najpierw sprawdzić czy jest nowa wersja i jeśli jest to ją zaktualizować, a następnie użyć.

    Dla elementów statycznych takich jak np. pliki JavaScript mamy do dyspozycji dwa inne nagłówki: ETag i Expires. Nagłówek ETag informuje przeglądarkę czy konkretny plik został zmieniony. Wymaga każdorazowo nawiązania połączenia z serwerem w celu odpytania o aktualność danego zasobu. Z kolei nagłówek Expires określa okres (a właściwie datę końcową) przez jaki obiekty danego rodzaju powinny być traktowane jako aktualne. Ma to tę zaletę, że Expires wyznacza datę ważności i do czasu jej minięcia przeglądarka nie będzie próbowała podejmować nawet próby weryfikacji aktualności danego pliku na serwerze.

    <FilesMatch "\.(ico|jpg|jpeg|png|gif|js|css|swf)$">
        <IfModule mod_expires.c>
            ExpiresActive on
            # 2592000 sekund = 30 dni
            ExpiresDefault A2592000
            Header append Cache-Control "public"
        </IfModule>
        # Wylaczenie naglowkow ETag
        Header unset ETag
        FileETag None
    </FilesMatch>

    Powyższy przykład prezentuje jak w htaccess zdefiniować 30 dniowy (2592000 sekund = 30 dni) okres ważności dla wybranych plików. Nagłówek ETag został wyłączony ponieważ w przypadku równoczesnego wysłania do klienta (czytaj przeglądarki) obu nagłówków, ETag ma priorytet.

    UWAGA! Jedynym sposobem na wymuszenie odświeżenie pliku z ustawionym Expires w przeglądarkach wszystkich odwiedzających jest zmiana jego nazwy.

    Zmiana nazwy pliku, który został podpięty w różnych częściach serwisu wiąże się z koniecznością dokonania poprawek w kilku miejscach w kodzie. Najłatwiej i najwygodniej zmienić nazwę foldera zawierającego elementy statyczne serwisu i jego nazwę trzymać w zmiennej. Wystarczy wtedy zmienić jedną wartość w konfigu i adresy wszystkich dynamicznie osadzonych w kodzie html obiektów zmienią się w jednej chwili. Jest to przepis, który można od biedy zastosować dla małopopularnych serwisów bowiem takie podejście ma przynajmniej dwie wady

    1. Trzeba zmieniać nazwę katalogu co jest kłopotem jeśli trzyma się statiki w repozytorium. Obejściem tego problemu jest użycie dowiązań symbolicznych.
    2. Zmiana pojedynczego obiektu dezaktualizuje wszystkie w danym folderze. Nawet jeśli zmienimy tylko favico-nkę to adresy wszystkich plików w danym katalogu się zmienią i zostaną pobrane na nowo przez przegladarki wszystkich użytkowników marnując transfer i zasoby serwera.

    Sprytniejszym rozwiązaniem jest dynamiczna zmiana nazwy pojedynczego pliku przez dodanie do niego np. czasu ostaniej modyfikacji oraz zdefiniowanie odpowiedniej regułki mode_rewrite w htaccess.

    RewriteEngine on
    RewriteRule ^(.*)\.[\d]{10}\.(css|js|jpe?g|gif|png)$ $1.$2 [L]

    Funkcja, która zajmie się modyfikowaniem nazwy pliku w kodzie dla PHP może wyglądać tak:

    /**
     *  Given a file, i.e. /js/base.js, replaces it with a string containing the
     *  file's mtime, i.e. /js/base.1221534296.js.
     *
     *  @param $file  The file to be loaded.  Must be an absolute path (i.e.
     *                starting with slash).
     */
    function auto_version($file)
    {
      if(strpos($file, '/') !== 0 || !file_exists($_SERVER['DOCUMENT_ROOT'] . $file))
        return $file;
     
      $mtime = filemtime($_SERVER['DOCUMENT_ROOT'] . $file);
      return preg_replace('{\\.([^./]+)$}', ".$mtime.\$1", $file);
    }

    sposób użycia

    <script src="<?php echo auto_version('/js/base.js'); ?>" type="text/javascript"></script>

    Analogiczny przykład dla Django wykorzystujący tę samą regułę mode_rewrite-a.

    Załóżmy, że kod jest zdefiniowany w /project/app/templatetags/helpers.py

    from django import template
    register = template.Library()
    import os, re
     
    STATIC_PATH="/path/to/templates/"
    version_cache = {}
     
    rx = re.compile(r"^(.*)\.(.*?)$")
    def version(path_string):
        try:
            if path_string in version_cache:
                mtime = version_cache[path_string]
            else:
                mtime = os.path.getmtime('%s%s' % (STATIC_PATH, path_string,))
                version_cache[path_string] = mtime
     
            return rx.sub(r"\1.%d.\2" % mtime, path_string)
        except:
            return path_string
     
    register.simple_tag(version)

    i użycie w szablonie

    {% load helpers %}
    <link rel="stylesheet" type="text/css" href="{% version '/static/css/style.css' %}">

    UWAGA! Dodanie parametru GET do nazwy pliku nie jest rekomendowanym sposobem odświeżania plików statycznych. Niektóre przeglądarki (Opera, Safari) i serwery pośredniczące nie cache-ują takich obiektów, inne z kolei (Firefox i IE) ignorują parametry.

    Najmniej inwazyjne, niewymagające interwencji ze strony programisty jest użycie mod_pagespeed, który na poziomie Apache podmienia odsyłacze do wszelkich mediów a w przypadku zmiany w pliku sam ją wykrywa i generuje inny adres.

    Czy łączyć pliki JavaScript w jeden duży plik?

    Każdy serwer ma swoją przepustowość i może przyjąć ograniczoną liczbę żądań (ang request). Żądanie http – nawet jednopikselowego obrazka – wymaga nawiązania połączenia pomiędzy przeglądarką a serwerem, przesłania danych na serwer definiujących treść żądania, rozpoznanie czy dane żądanie może być spełnione czy też nie, a następnie zwrócenie do przeglądarki żądanego zasobu lub też nagłówków z odpowiednim kodem błędu. Wszystko to absorbuje czas i angażuje zasoby serwera. Wziąwszy pod uwagę, że pobraniu każdego dokumentu HTML towarzyszą dodatkowe zapytania o tzw. media czyli grafiki, video, pliki css, js, fonty, filmy flash itd., trzeba sobie uświadomić, że wpisanie jednego krótkiego url-a na pasku adresu przeglądarki i wciśnięcie entera uruchamia przy bardziej rozbudowanych stronach całą lawinę zapytań.

    To zagadnienie zaczyna być istotne dopiero, kiedy liczba unikalnych wizyt na Twojej stronie składa się przynajmniej z pięciu cyfr. Zatem jeśli nie masz przynajmniej 10 tys. unikalnych odwiedzin lub też znacząca ich liczba jest wygenerowana przez narzędzie do fałszowania statystyk, to możesz odłożyć przeczytanie tego rozdziału na później.

    Ograniczenie liczby requestów najłatwiej osiągnąć poprzez połącznie kilku plików w jeden. Jeden plik js to rozwiązanie teoretycznie idealne, ale niepraktyczne. Jeśli na stronie używamy, jQuery, tinymce z dużą liczbą pluginów, do tego chcielibyśmy mieć fancyboxa i parę widgetów z jquery.UI i jeszcze kilka innych ciekawych bibliotek to rozmiar pliku byłby niepokojąco duży nawet gdyby miał się ładować tylko raz a potem został zbuforowany.

    Osobiście uważam, że łączenie plików w jeden większy sprawdza się dla małych skryptów podobnie jak to się ma w przypadku zestawu małych ikonek połączonych w jedną grafikę i wyświetlanych za pomocą techniki CSS Sprite polegającej na manipulacji parametrami position-background.

    Warto też rozważyć połączenie skryptów, które są używane na większości, lub też często odwiedzanych podstronach. W dużych serwisach nigdy nie ma tak, aby 100% kodu JavaScript było wykorzystywane na każdej podstronie witryny. Poza tym część programów JavaScript urychamianych jest np. w części administracyjnej serwisu. Nie ma sensu łączyć kodu uniwersalnego z kodem uruchamianym dla wąskiego grona użytkowników.

    Kodem logicznie podzielonym między pliki łatwiej się zarządza dlatego warto pomyśleć nad rozwiązaniami atomatycznie łączącymi i kompresującymi pliki JavaScript takimi jak np. jsmin

    require 'jsmin.php';
    // Output a minified version of example.js.
    echo JSMin::minify(file_get_contents('example.js') . file_get_contents('example2.js'));

    Dla Django mamy szeroki wybór asset-manager-ów, ale możemy też skorzystać z algorytmu Douglas Crockford-a, przepisanego w języku Python i zaprezentowanego w jednej z recept w serwisie stackoverflow.

    #!/usr/bin/python
    # -*- coding: utf-8 -*-
     
    import os, os.path, shutil
     
    # This code is original from jsmin by Douglas Crockford, it was translated to
    # Python by Baruch Even. The original code had the following copyright and
    # license.
    #
    # /* jsmin.c
    #    2007-05-22
    #
    # Copyright (c) 2002 Douglas Crockford  (www.crockford.com)
    #
    # Permission is hereby granted, free of charge, to any person obtaining a copy of
    # this software and associated documentation files (the "Software"), to deal in
    # the Software without restriction, including without limitation the rights to
    # use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
    # of the Software, and to permit persons to whom the Software is furnished to do
    # so, subject to the following conditions:
    #
    # The above copyright notice and this permission notice shall be included in all
    # copies or substantial portions of the Software.
    #
    # The Software shall be used for Good, not Evil.
    #
    # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
    # SOFTWARE.
    # */
     
    from StringIO import StringIO
     
    def jsmin(js):
        ins = StringIO(js)
        outs = StringIO()
        JavascriptMinify().minify(ins, outs)
        str = outs.getvalue()
        if len(str) > 0 and str[0] == '\n':
            str = str[1:]
        return str
     
    def isAlphanum(c):
        """return true if the character is a letter, digit, underscore,
               dollar sign, or non-ASCII character.
        """
        return ((c >= 'a' and c <= 'z') or (c >= '0' and c <= '9') or
                (c >= 'A' and c <= 'Z') or c == '_' or c == '$' or c == '\\' or (c is not None and ord(c) > 126));
     
    class UnterminatedComment(Exception):
        pass
     
    class UnterminatedStringLiteral(Exception):
        pass
     
    class UnterminatedRegularExpression(Exception):
        pass
     
    class JavascriptMinify(object):
     
        def _outA(self):
            self.outstream.write(self.theA)
        def _outB(self):
            self.outstream.write(self.theB)
     
        def _get(self):
            """return the next character from stdin. Watch out for lookahead. If
               the character is a control character, translate it to a space or
               linefeed.
            """
            c = self.theLookahead
            self.theLookahead = None
            if c == None:
                c = self.instream.read(1)
            if c >= ' ' or c == '\n':
                return c
            if c == '': # EOF
                return '\000'
            if c == '\r':
                return '\n'
            return ' '
     
        def _peek(self):
            self.theLookahead = self._get()
            return self.theLookahead
     
        def _next(self):
            """get the next character, excluding comments. peek() is used to see
               if an unescaped '/' is followed by a '/' or '*'.
            """
            c = self._get()
            if c == '/' and self.theA != '\\':
                p = self._peek()
                if p == '/':
                    c = self._get()
                    while c > '\n':
                        c = self._get()
                    return c
                if p == '*':
                    c = self._get()
                    while 1:
                        c = self._get()
                        if c == '*':
                            if self._peek() == '/':
                                self._get()
                                return ' '
                        if c == '\000':
                            raise UnterminatedComment()
     
            return c
     
        def _action(self, action):
            """do something! What you do is determined by the argument:
               1   Output A. Copy B to A. Get the next B.
               2   Copy B to A. Get the next B. (Delete A).
               3   Get the next B. (Delete B).
               action treats a string as a single character. Wow!
               action recognizes a regular expression if it is preceded by ( or , or =.
            """
            if action <= 1:
                self._outA()
     
            if action <= 2:
                self.theA = self.theB
                if self.theA == "'" or self.theA == '"':
                    while 1:
                        self._outA()
                        self.theA = self._get()
                        if self.theA == self.theB:
                            break
                        if self.theA <= '\n':
                            raise UnterminatedStringLiteral()
                        if self.theA == '\\':
                            self._outA()
                            self.theA = self._get()
     
     
            if action <= 3:
                self.theB = self._next()
                if self.theB == '/' and (self.theA == '(' or self.theA == ',' or
                                         self.theA == '=' or self.theA == ':' or
                                         self.theA == '[' or self.theA == '?' or
                                         self.theA == '!' or self.theA == '&' or
                                         self.theA == '|' or self.theA == ';' or
                                         self.theA == '{' or self.theA == '}' or
                                         self.theA == '\n'):
                    self._outA()
                    self._outB()
                    while 1:
                        self.theA = self._get()
                        if self.theA == '/':
                            break
                        elif self.theA == '\\':
                            self._outA()
                            self.theA = self._get()
                        elif self.theA <= '\n':
                            raise UnterminatedRegularExpression()
                        self._outA()
                    self.theB = self._next()
     
     
        def _jsmin(self):
            """Copy the input to the output, deleting the characters which are
               insignificant to JavaScript. Comments will be removed. Tabs will be
               replaced with spaces. Carriage returns will be replaced with linefeeds.
               Most spaces and linefeeds will be removed.
            """
            self.theA = '\n'
            self._action(3)
     
            while self.theA != '\000':
                if self.theA == ' ':
                    if isAlphanum(self.theB):
                        self._action(1)
                    else:
                        self._action(2)
                elif self.theA == '\n':
                    if self.theB in ['{', '[', '(', '+', '-']:
                        self._action(1)
                    elif self.theB == ' ':
                        self._action(3)
                    else:
                        if isAlphanum(self.theB):
                            self._action(1)
                        else:
                            self._action(2)
                else:
                    if self.theB == ' ':
                        if isAlphanum(self.theA):
                            self._action(1)
                        else:
                            self._action(3)
                    elif self.theB == '\n':
                        if self.theA in ['}', ']', ')', '+', '-', '"', '\'']:
                            self._action(1)
                        else:
                            if isAlphanum(self.theA):
                                self._action(1)
                            else:
                                self._action(3)
                    else:
                        self._action(1)
     
        def minify(self, instream, outstream):
            self.instream = instream
            self.outstream = outstream
            self.theA = '\n'
            self.theB = None
            self.theLookahead = None
     
            self._jsmin()
            self.instream.close()
     
    def compress(in_files, out_file, in_type='js', verbose=False,
                 temp_file='.temp'):
        temp = open(temp_file, 'w')
        for f in in_files:
            fh = open(f)
            data = fh.read() + '\n'
            fh.close()
     
            temp.write(data)
     
            print ' + %s' % f
        temp.close()
     
        out = open(out_file, 'w')
     
        jsm = JavascriptMinify()
        jsm.minify(open(temp_file,'r'), out)
     
        out.close()
     
        org_size = os.path.getsize(temp_file)
        new_size = os.path.getsize(out_file)
     
        print '=> %s' % out_file
        print 'Original: %.2f kB' % (org_size / 1024.0)
        print 'Compressed: %.2f kB' % (new_size / 1024.0)
        print 'Reduction: %.1f%%' % (float(org_size - new_size) / org_size * 100)
        print ''
     
        os.remove(temp_file)
     
    if __name__ == '__main__':
        compress(['script1.src.js','script2.src.js'], 'script.min.js')

    UWAGA! Dodatkową zaletą obfuskacji – czyli zaciemniania kodu – jest jego mniejsza objętość. Dlatego też zachęcam do tego „procederu” nie tyle w celu ochrony własności intelektualnej co minimalizacji plików js.

    Czy zaciągać pliki JavaScript z zewnętrznych serwerów?

    Istnieje wiele serwisów społecznościowych czy też wyspecjalizowanych w określonej tematyce, które oferują funkcjonalności mogące w uatrakcyjnić naszą własną stronę www. Poprzez integrację z usługą uwierzytelniania użytkowników za pośrednictwem Facebooka, wykorzystując api Soundcoluda do udostępniania muzyki lub choćby osadzając mapy Googla wzbogacamy nasz serwis o elementy, które samodzielnie byłoby o wiele trudniej zaimplementować. Większość zaawansowanych dodatków, jak również tych całkiem niepozornych jak Facebook Like wymaga dołączenia JavaScriptu. Okazuje się, że także duże javascriptowe frameworki możemy zaciągać z zewnętrznego serwera.

    Google hostuje wybrane biblioteki JavaScript i udostępnia je w wielu wersjach zachęcając do osadzenia na swoich stronach.

    Js frameworki hostowane przez google

    Jeśli spojrzymy na oficjalną stronę jQuery zobaczymy w jej źródle.

    <script src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js"></script>

    Można się zastanawiać nad zaletami i wadami takiej praktyki. Niewątpliwie warto mieć kopię podstawowej biblioteki – używanej w serwisie jako bazę i szkielet wszelkich skryptów – na swoim serwerze. Nie przeszkadza to równocześnie używać kopii tej biblioteki udostępnianej z innego serwera. Delegowanie części ruchu na zewnątrz z pewnością odciąży nieco naszego własnego Apacha. Poza tym trzeba wziąć pod uwagę, że serwer Googla jest zoptymalizowany dla treści statycznych, co gwarantuje szybkie i niezawodne działanie.

    Temat dotyczący serwerów plików statycznych CDN-ów (Content Delivery Network) jest bardzo ciekawy, ale wykracza nieco poza ramy tego opracowania, które traktuje raczej o tym co może zrobić programista i porusza sprawy całkiem podstawowe.

    Czy ładować pliki JavaScript head czy w body?

    Osadzając odnośniki do plików JavaScript w HTML-u w sposób tradycyjny …

    <script type="text/javascript" src="scripts_1.js"></script>
    <script type="text/javascript" src="scripts_2.js"></script>

    … skrypty ładowane są synchronicznie czyli po kolei. Co więcej – do czasu załadowania pliku js wstrzymywane jest renderowanie strony. Można to zasymulować wywołując w skrypcie funkcję stop, która przerywa całkowicie dalsze ładowanie HTML-a, a co za tym idzie pozostałych, osadzonych w kodzie obiektów.

    stop();

    Z tego powodu pliki JavaScript mogą stać się tzw. SPOF (Single Point Of Failure) czyli pojedynczym elementem zaburzającym działanie całości. W praktyce ma to miejsce wtedy kiedy plik JavaScript z powodu wielkości lub kłopotów na łączach długo się ładuje. Najczęściej spowalnia to po prostu załadowanie całej witryny. W najgorszym przypdaku serwer plików nie odpowiada, a przeglądarka czaka w nieskończoność na zasób lub też kod błędu.

    Testowanie SPOF na stronie WWW spowodowanego ładowaniem JavaScript

    Zob. Testing for Frontend SPOF

    Programiści często obchodzą ten problem ładując skrypty nie w nagłówku strony (czyli w head) tylko w ciele strony (w body) i to najlepiej na samym dole. Wychodzą z założenia, że jeśli nawet sypnie się JavaScript to przynajmniej cała treść, style i obrazki się załadują.

    UWAGA! Ładowanie skryptów w body jest absolutnie niepotrzebne. Wystarczy odroczyć pobieranie skryptów do czasu sparsowania całej strony. Służy do tego atrybut defer (od ang. deferred – odroczony).

    <script type="text/javascript" src="script.js" defer="defer"></script>

    Teraz funkcja strop nie spowoduje przerwania renderowania strony ani ładowania innych skryptów.

    Czy ładować pliki JavaScript synchronicznie czy asynchronicznie?

    HTML5 przyniósł wiele ciekawych innowacji także w sprawie ładowania skryptów pojawiło się coś nowego. Tag script wzbogacił się o atrybut async, który determinuje ładowanie asynchroniczne.

    <script async="async" src="scripts_1.js"></script>
    <script async="async" src="scripts_2.js"></script>

    Oba skrypty będą pobierane niezależnie od siebie i od dokumentu HTML dlatego jeśli nawet w pierwszym skrypcie wywołamy funkcję „stop” nie spowoduje to przerwania renderowania strony ani ładowania drugiego skryptu. Przewagą trybu asynchronicznego nad odraczaniem ładowania do czasu sparsowania strony HTML (patrz. atrybut defer), że skrypt może być pobierany równocześnie z samym dokumentem.

    UWAGA! Jeśli drugi skrypt będzie pobierany w trybie synchrnicznym to pierwszy także przełączy się w ten tryb. Mówiąc inaczej skrypty będą pobierane asynchronicznie jeśli mają ustawiony parametr async na true oraz wszystkie później rozpoczynane pobrania także odbywają sie w tym trybie.

    Ładowanie asynchroniczne jest idealne do inicjowania na stronie dodatkowych usług takich jak np. statystyki:

      var _gaq = _gaq || [];
      _gaq.push(['_setAccount', 'UA-XXXXX-Y']);
      _gaq.push(['_trackPageview']);
     
      (function() {
        var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
        ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
        var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
      })();

    lub facebook connect

    $(document).ready(function() {
     
        window.fbAsyncInit = function() {
            // kod realizujący logowanie na stronie via facebook
        };
        (function(d){
            // nadanie id zapobiega wielokrotnemu ładowaniu skryptu
            var js, id = 'facebook-jssdk'; if (d.getElementById(id)) {return;}
            js = d.createElement('script'); js.id = id; js.async = true;
            // brak protokołu nie jest błędem
            js.src = "//connect.facebook.net/pl_PL/all.js";
            d.getElementsByTagName('head')[0].appendChild(js);
        }(document));
    });

    UWAGA W powyższym przykładzie warto zwrócić uwagę na brak protokołu w adresie skryptu. Zostanie użyty taki sam protokół jaki został wywołany dla strony www. Zapobiega to zgłaszaniu ostrzeżeń przez przeglądarki internetowe, które reagują, kiedy strona idzie po SSL-u (https) a media są dostarczane do przeglądarki bez szyfrowania (http).

    W przypadku plików doładowywanych asynchronicznie trzeba czekać aż zostaną w całości pobrane i będą gotowe do pracy. Jeśli jesteś autorem oprogramowania możesz przyjąć, że w kodzie strony zostaną zdefiniowane jakieś zmienne (np. _gaq), albo funkcje (np. window.fbAsyncInit), które po załadowaniu pliku js zostaną w nim użyte. Tej strategi nie można użyć dla ładowanych asynchronicznie bibliotek takich jak jQuery. To nie jQuery wywoła zdefiniowany przez Ciebie kod tylko Ty chcesz użyć metod oferowanych przez ten rozbudowany framework. Dlatego musisz wykryć moment załadowania pliku i uruchomić swoje funkcje dopiero wtedy kiedy dostępny będzie obiekt jQuery.

    JavaScript jest językiem zorientowanym na zdarzenia. Między innymi obsługuje też zdarzenia związane z ładowaniem obrazków czy plików js. Także w tej kwestii IE wyróżnia się spośród innych przeglądarek autorskimi rozwiązaniami i zamiast zdarzenia load ma swoje onreadystatechange. Tak czy owak można to wykorzystać do napisania funkcji umożliwiającej dynamiczne ładowanie skryptów.

    (function(global){
        var cache = {}
        var call = function(url, callback) {
            if (typeof(callback) == "function") {
                cache[url].callbacks.push(callback);
            }
            // run only if loaded
            if (cache[url].status == 'loaded') {
                for (i in cache[url].callbacks) {
                    cache[url].callbacks[i]();
                }
                // reset callbacks
                cache[url].callbacks = [];
            }
        }
        global.loadScript = function(url, callback){
            if (url in cache) {
                call(url, callback);
                return;
            } else {
                cache[url] = {'status':'init','callbacks':[]};
            }
            var script = document.createElement("script")
            script.type = "text/javascript";
            script.async = true;
            if (script.readyState){ //IE
                script.onreadystatechange = function(){
                    if (script.readyState == "loaded" ||
                        script.readyState == "complete"){
                        script.onreadystatechange = null;
                        cache[url].status = 'loaded';
                        call(url, callback);
                    }
                };
            } else { //Others
                script.onload = function(){
                    cache[url].status = 'loaded';
                    call(url, callback);
                };
            }
            script.src = url;
            document.getElementsByTagName("head")[0].appendChild(script);
        }
    })(this);
     
     
    loadScript('//ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js'
        , function(){
            $('p').attr('style', 'border: 1px solid red');
        });

    Funkcja ta choć prosta ma dwie przewagi nad jquerową metodą $.getScript, a mianowicie nie wymaga jQuery (nie używa też do ładowania ajaxa) i dba o to aby każdy skrypt był ładowany tylko raz. Ma to duże znaczenie w dynamicznie bydowanych aplikacjach wykorzystujących wzorzec projektowy opóźnionego ładowania (ang. lazy loading pattern), którego idea zasadza się na myśli, że zasób pobieramy dopiero w chwili kiedy go potrzebujemy.

    Koncepcję wzorca opóźnionego ładowania dla języka używanego w przeglądarce trzeba nieco zmodyfikować. W końcu nie wyobrażam sobie aby pobierać TinyMce wraz z jego wszystkimi pluginami dopiero w chwili kiedy użytkownik kliknie w pole tekstowe formularza. Choć jest to technicznie możliwe to z punktu widzenia funkcjonalności byłoby z uwagi na opóźnienia totalnym nieporozumieniem. Chodzi tu raczej o dołączanie do poszczególnych podstron HTML skryptów js tylko tam gdzie są faktycznie potrzebne.

    Rozbudowane aplikację mają to do siebie, że składają się z wielu klocków, z których jeden nie musi wiedzieć nic o innym, a przynajmniej nie musi widzieć czy inny moduł załadował już wymagane biblioteki JavaScript. Co z tego, że główny kontent strony stanowi formularz, rejestracji nowego użytkownika a w nagłówku strony umieszczono formularza logowania, który prezentowany jest zresztą standardowo na wszystkich podstronach serwisu. Oba formularze nie mają ze sobą nic wspólnego. Dla każdego z nich można podjąć próbę załadowania jquery.validate. Jeśli skorzysta się z loadScript można mieć pewność, że plugin zostanie pobrany tylko raz.

    loadScript('jquery.validate.js', function(){loginFormValidation();});
    loadScript('jquery.validate.js', function(){registerFormValidation();});

    Kwestia wielokrotnego ładowania tego samego pliku została rozpracowana, jednak mamy do rozwiązania jeszcze jedną zagwozdkę, a mianowicie zależności

    Fancybox jest dodatkiem, który świetnie sprawdza się do prezentacji zdjęć na stronach internetowych. Opcjonalnie można go używać z pluginem jquery.mousewheel umożliwiającym przechodzenie pomiędzy zdjęciami w galerii poprzez ruch rolki w myszce. Zanim zainicjujemy fancyboksa byłoby dobrze aby oba pliki były już gotowe do użycia, z kolei oba pluginy wymagają wczesniejszego załadowania podstawowej biblioteki jQuery. Kolejność ładowania skryptów jest zatem istotna.

    LABjs udostępnia przyjemny interfejs zoptymalizowany dla celu asynchronicznego, łańcuchowego ładowania skryptów.

    <script type="text/javascript" src="script.js"></script>
    <script type="text/javascript">
    $LAB
        .script("jquery.js")
        .script("jquery.mousewheel.js")
        .script("jquery.fancybox.js")
        .wait(function(){
            $('a.img').fancybox();
        });
    </script>

    albo jeszcze prościej

    $LAB
        .script("jquery.js", "jquery.mousewheel.js", "jquery.fancybox.js")
        .wait(function(){
            $('a.img').fancybox();
        });

    Przy użyciu funkcji loadScript trzebaby to było zrobić w mniej zgrabny sposób.

    loadScript('jquery.js', function(){
        loadScript('jquery.mousewheel.js', function(){
            loadScript('jquery.fancybox.js', function(){
                $('a.img').fancybox();
        });
        });
    });

    LABjs ma wszystkie zalety loadScript i dokłada do tego jeszcze trochę od siebie. Szczerze polecam ją do zastosowań profesjonalnych. We wpisie Sposoby wczytywania JavaScript przeczytałem o jeszcze innych podobnych narzędziach.

    Podsumowanie

    Opisałem zaledwie kilka i to tych podstawowych aspektów pracy z JavaScriptem w projekcie aplikacji internetowej. Jak widać jest on ściśle powiązany z technologiami z którymi koegzystuje i już sam proces jego dołączania może mieć wpływ na funkcjonowanie całości jaką jest strona www.

    Wszystkich zachęcam jeszcze raz do przeczytania – przynajmniej najważniejszych moim zdaniem – twierdzeń czy wskazówek zawartych w tym artykule. Wytłuściłem je wszystkie i poprzedziłem słowem UWAGA!, tak więc nie powinno być problemu z ich odnalezieniem.

    Podziel się z innymi!

      3 Comments

      1. szagi3891

        Do asynchronicznego ładowania plików polecam bibliotekę „autoloadjs” :

        https://github.com/szagi3891/autoloadjs

        Działa ona na zasadzie wczytywania potrzebnych „modułów” wraz ze spełnieniem wszystkich wymaganych zależności. Moduły sami sobie definiujemy w pliku konfiguracyjnym. Biblioteka wczytuje skrypty nawet jeśli w bibliotece została użyta instrukcja document.write (demo pokazuje że można za jej pomocą wczytać nawet google mapy)

      2. Tone

        czy używanie zewnętrznych źródeł js obniża bezpieczeństwo protokołu ssl ?

      3. Zbigniew Heintze Post author

        Przede wszystkim jeśli serwujesz stronę po ssl-u i dołączasz zewnętrzny skrypt to powinien on być także pobierany poprzez https bo w przeciwnym przypadku przeglądarki wyświetlają użytkownikom ostrzeżenia. Inną kwestią jest to, że jeśli już z jakichś względów musisz (np. google analitics lub facebook) to dołączaj skrypty jedynie z zaufanych źródeł.

      Dodaj komentarz

      Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *