JavaScript w projekcie internetowym

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.

Facebook – dodawanie aplikacji do fanpage-a

Facebook to dla developera wieczne utrapienie. Ciągłe zmiany w interfejsie, w API, dodawanie coraz to nowych funkcjonalności czy choćby permanentny redesign zmuszają do bezustannego poprawiania napisanych aplikacji. Pisząc aplikacje na facebook średnio raz na pół roku muszę być przygotowany na to, że połowę rzeczy, które nauczyłem się ostatnio implementować teraz będę musiał zrobić w zupełnie inny sposób. Czytanie tutoriali, czy wskazówek na blogach często nie ma sensu gdyż zamieszczone porady są już dawno nieaktualne. Łapię się na tym, że napisanie średnio rozbudowanej aplikacji zajmuje mniej czasu niż opublikowanie i zintegrowanie jej z facebookiem.

Dzisiaj klient zgłosił mi, że nie można dodać aplikacji do fanpage-a gdyż nigdzie nie ma przycisku „Add to my page”. Zawsze jak się wchodziło na stronę aplikacji była możliwość dodania jej do fanpage-a za pomocą jednego kliknięcia, a później ewentualnie skonfigurowanie jej tak aby wyświetlała się w zakładce, a teraz nie ma. I co? I zaczęło się rycie w dokumentacji i googlach.

W końcu znalazłem przepis na to jak dodać analogiczny przycisk do kodu samej aplikacji

<html xmlns="http://www.w3.org/1999/xhtml"
  xmlns:fb="https://www.facebook.com/2008/fbml">
  <head>
    <title>My Add to Page Dialog Page</title>
  </head>
  <body>
    <div id='fb-root'></div>
    <script src='http://connect.facebook.net/en_US/all.js'></script>
    <p><a onclick='addToPage(); return false;'>Add to Page</a></p>
    <p id='msg'></p>
 
    <script> 
      FB.init({appId: "YOUR_APP_ID", status: true, cookie: true});
 
      function addToPage() {
 
        // calling the API ...
        var obj = {
          method: 'pagetab',
          redirect_uri: 'YOUR_URL',
        };
 
        FB.ui(obj);
      }
 
    </script>
  </body>
</html>

Irytuje mnie ta polityka Facebooka niemiłosiernie ponieważ, kiedy coś przestaje działać, albo wyglądać na facebooku klient ma pretensje do mnie. Ja z kolei nie mam ochoty poprawek i modyfikacji wynikających ze zmiany flow na facebooku robić w ramach gwarancji bo to często nie są sprawy 5 minutowe.

Tworzenie obiektów w JavaScript

W JavaSripcie istnieje bazowa klasa Object, która stanowi prototyp wszystkich pozostałych obiektów JavaScript. Można jej użyć bezpośrednio do utworzenia obiektu.

    obj = new Object();
    obj.x = 1;
    obj.y = 2;

lub alternatywnie

    obj = {};
    obj.x = 1;
    obj.y = 2;

Nawiasy klamrowe stanowią semantyczny skrót podobnie jak użycie nawiasów kwadratowych (arr = []) może zastąpić arr = new Array(). Tablica czyli Array jest jednym z wielu wbudowanych w JavaScript obiektów podobnie jak Window, Document itd.

Aby utworzyć niestandardowy obiekt należy najpierw zdefinować prototyp Klasy, której instancją dany obiekt będzie. Najprościej utworzyć prototyp metodą konstruktora.

var Foo = function() {
    this.x = 1;
    this.y = 2;
    this.sum = function() {
       return this.x + this.y;
    }
}

W konstruktorze definiujemy właściwości i metody. Nową instancję tworzy się z użyciem operatora new.

    obj = new Foo();
    alert(obj.x);
    alert(obj.sum());

Do właściwości lub metod można się odwołać za pośrednictwem utworzonego w ten sposób obiektu, ale można też pominąć operację przypisania do zmiennej i wywołać metodę w sposób niemożliwy np. w PHP.

	alert(new Foo().sum());

Wywołując obj = new Foo(); powołuje się do życia instancje prototypu Foo. Można następnie wywołać metodę obj.sum();. Metodę sum można też wywołać z pominięciem przypisania obiektu do zmiennej new Foo().someMethod(); Tak czy inaczej zawsze w trzeba w pierwszej kolejności utworzyć obiekt.

Definiowanie klas metodą prototypu jest łatwe i intuicyjne, nie mniej w przypadku powoływania wielu obiektów danego prototypu zalecany jest sposób definiowania metod z użyciem prototype (nie mylić z frameworkiem prototype).

var Foo = function() { 
    this.x = 1;
    this.y = 2;
};
 
Foo.prototype.sum = function() {
   return this.x + this.y;
}

Różnica polega na tym, że w pierwszym przypadku przy powoływaniu do życia nowej instancji za każdym razem metoda „sum” tworzona jest na nowo, a w drugim przypadku jest to cały czas jedna i ta sama metoda. Zyskuję się na zaoszczędzonej pamięci co przy bardzo rozbudowanych skryptach nabiera znaczenia.

Zdefiniowanie metody z pominięciem słowa „prototype”:

var Foo = function() { };
 
Foo.sum = function(x, y) {
    return x+ y;
}

… daje możliwość wywołania metody klasy bez tworzenia obiektu Foo.sum(2,4); – to taki jakby odpowiednik metod statycznych w PHP. Analogicznie do metod statycznych w PHP użycie „this” wewnątrz takiej funkcji mija się z celem, więc gdybyśmy przez przypadek zdefiniowali:

var Foo = function() { 
    this.x = 1;
    this.y = 2;
};
 
Foo.sum = function() {
   return this.x + this.y;
}

to wywołanie Foo.sum(); zwróci „NaN” a z kolei new Foo().sum(); zakończy się błędem „TypeError on line 1: (new Foo).sum is not a function”.

W JS można też utworzyć obiekt ad hock

var Foo = {
    x: 1,
    y: 2,
    sum: function () {
        return this.x + this.y;
    }
}

W tym wypadku Foo nie jest prototypem tylko instancją prototypu Object dlatego oczywistym jest wywołanie Foo.sum(); z pominięciem operatora new.

jsTree – context content

Skryptów do wizualizacji struktur drzewiastych nie brakuje. W javascript-cie powstało tego cała masa. Ja preferuje rozwiązania oparte o [jquery](http://jquery.com/) i nawet po przyjęciu tego zawężającego kryterium mam w czym wybierać. To też wypróbowałem parę rozwiązań i ostatecznie zdecydowałem się na [jstree](http://www.jstree.com/).

Po przejrzeniu wszystkich dem prezentujących możliwości tej aplikacji (Piszę aplikacji bo coś co ma np. pluginy – nie jest już prostym skryptem), a także dokumentacji jestem naprawdę pod wrażeniem. Rozwijanie i zwijanie katalogów to banał, ale do tego dochodzi możliwość przeciągania elementów lub nawet całych gałęzi, możliwość dodawania, edytowania, usuwania, kopiowania elementów. Wspomniany skrypt jest naprawdę dobrze napisany. Daje możliwość przechwycenia wszelkich zachodzących na drzewie wydarzeń i dowolnego ich obsłużenia. Łącząc takie technologie jak ajax i np. php możemy doczytywać rozwijane katalogi lub też zapisywać wszelkie wykonane na drzewie operacje takie jak utworzenie nowego elementu lub też przeniesienie gałęzi w inne miejsce.

Wspomniałem już o pluginach. Do dyspozycji mamy kilka przydatnych rozszerzeń. **”Cookie plugin”** np. umożliwia zapisanie stanu drzewa. Dzięki temu po przeładowaniu strony wszystkie rozwinięte wcześniej gałęzie nadal takimi pozostają i nie musimy bawić się w ponowne klikanie by dojść do elementu na trzecim poziomie. **”Keyboard navigation”** umożliwia poruszanie się po drzewie przy pomocy klawiatury, a **”Checkbox plugin”** umożliwia zaznaczenie więcej niż jednego elementu poprzez kliknięcie utworzonego przy nazwie elementu checkboxa.

Jednym z ciekawszych dodatków jest w pełni konfigurowalne menu kontekstowe. **”Context menu plugin”** ma już predefiniowane ustawienie umożliwiające wykonanie takich akcji jak utworzenie podelementu oraz usunięcie lub edycja elementu bieżącego. Demo dodatkowo prezentuje w jaki sposób dodać do niego własną akcję. Menu kontekstowe ma jedną wadę, która objawia się w operze. Ponieważ pojawia się po kliknięciu na prawym przycisku myszki w Operze menu kontekstowe nie zadziała gdyż wspomniana przeglądarka nie daje możliwości nadpisania akcji prawokliku. Z tego też powodu zdecydowałem się nieco zmodyfikować tenże plugin w ten sposób, że po wybraniu danego elementu przy jego nazwie pojawia się mały plusik, którego kliknięcie otwiera menu kontekstowe.

(function ($) {
	$.extend($.tree.plugins, {
		"contextmenu" : {
			context_menu : {
				object : $("<ul id='jstree-contextmenu' class='tree-context' />"),
					data : {
					t : false,
					a : false,
					r : false
				},
				isrgtclick: false,
				defaults : {
					class_name : "hover",
					items : {
						create : {
							label	: "Create", 
							icon	: "create",
							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return 0; return TREE_OBJ.check("creatable", NODE); }, 
							action	: function (NODE, TREE_OBJ) { TREE_OBJ.create(false, TREE_OBJ.get_node(NODE[0])); },
							separator_after : true
						},
						rename : {
							label	: "Rename", 
							icon	: "rename",
							visible	: function (NODE, TREE_OBJ) { if(NODE.length != 1) return false; return TREE_OBJ.check("renameable", NODE); }, 
							action	: function (NODE, TREE_OBJ) { TREE_OBJ.rename(NODE); } 
						},
						remove : {
							label	: "Delete",
							icon	: "remove",
							visible	: function (NODE, TREE_OBJ) { var ok = true; $.each(NODE, function () { if(TREE_OBJ.check("deletable", this) == false) ok = false; return false; }); return ok; }, 
							action	: function (NODE, TREE_OBJ) { $.each(NODE, function () { TREE_OBJ.remove(this); }); } 
						}
					}
				},
				show : function(obj, t, e) {
					var opts = $.extend(true, {}, this.defaults, t.settings.plugins.contextmenu);
					obj = $(obj);
					if(obj.size() == 0) return;
					this.data.t = t;
					if(!obj.children("a:eq(0)").hasClass("clicked")) {
						this.data.a = obj;
						this.data.r = true;
						obj.children("a").addClass(opts.class_name);
						if (e) {
							e.target.blur();
						}
					}
					else { 
						this.data.r = false; 
						this.data.a = (t.selected_arr && t.selected_arr.length > 1) ? t.selected_arr : t.selected;
					}
 
					this.object.empty();
					var str = "";
					var cnt = 0;
					for(var i in opts.items) {
						if(!opts.items.hasOwnProperty(i)) continue;
						if(opts.items[i] === false) continue;
						var r = 1;
						if(typeof opts.items[i].visible == "function") r = opts.items[i].visible.call(null, this.data.a, t);
						if(r == -1) continue;
						else cnt ++;
						if(opts.items[i].separator_before === true) str += "<li class='separator'><span>&nbsp;</span></li>";
						str += '<li><a href="#" rel="' + i + '" class="' + i + ' ' + (r == 0 ? 'disabled' : '') + '">';
						if(opts.items[i].icon) str += "<ins " + (opts.items[i].icon.indexOf("/") == -1 ? " class='" + opts.items[i].icon + "' " : " style='background-image:url(\"" + opts.items[i].icon + "\");' " ) + ">&nbsp;</ins>";
						else str += "<ins>&nbsp;</ins>";
						str += "<span>" + opts.items[i].label + '</span></a></li>';
						if(opts.items[i].separator_after === true) str += "<li class='separator'><span>&nbsp;</span></li>";
					}
					var tmp = obj.children("a:visible").offset();
					this.object.attr("class","tree-context tree-" + t.settings.ui.theme_name.toString() + "-context").html(str);
					var h = this.object.height();
					var w = this.object.width();
					var x = tmp.left;
					var y = tmp.top + parseInt(obj.children("a:visible").height()) + 2;
					var max_y = $(window).height() + $(window).scrollTop();
					var max_x = $(window).width() + $(window).scrollLeft();
					if(y + h > max_y) y = Math.max( (max_y - h - 2), 0);
					if(x + w > max_x) x = Math.max( (max_x - w - 2), 0);
					this.object.css({ "left" : (x), "top" : (y) }).fadeIn("fast");
 
					if (e) {
						e.preventDefault(); 
						e.stopPropagation();
						this.isrgtclick = true;
					}
				},
				hide : function (check_isrgtclick) {
					if (check_isrgtclick == true && this.isrgtclick == false) {
						return;
					}
					this.isrgtclick = false;
					if(!this.data.t) return;
					var opts = $.extend(true, {}, this.defaults, this.data.t.settings.plugins.contextmenu);
					if(this.data.r && this.data.a) {
						this.data.a.children("a, span").removeClass(opts.class_name);
					}
					this.data = { a : false, r : false, t : false };
					this.object.fadeOut("fast");
				},
				exec : function (cmd) {
					if($.tree.plugins.contextmenu.context_menu.data.t == false) return;
					var opts = $.extend(true, {}, $.tree.plugins.contextmenu.context_menu.defaults, $.tree.plugins.contextmenu.context_menu.data.t.settings.plugins.contextmenu);
					try { opts.items[cmd].action.apply(null, [$.tree.plugins.contextmenu.context_menu.data.a, $.tree.plugins.contextmenu.context_menu.data.t]); } catch(e) { };
				},
				add : function (n, t) {
					if ($('#jstree-show-contextmenu').length == 0) {
						$(n).children('a').after('<a id="jstree-show-contextmenu" href="#">+</a>');
						$('#jstree-show-contextmenu').click(function(){
							$.tree.plugins.contextmenu.context_menu.show(n, t);
						});
					}
				},
				rem : function () {
					if ($('#jstree-show-contextmenu').length > 0) {
						$('#jstree-show-contextmenu').remove();
					}
				}
			},
			callbacks : {
				oninit : function () {
					if(!$.tree.plugins.contextmenu.css) {
						var css = '#jstree-contextmenu { display:none; position:absolute; z-index:2000; list-style-type:none; margin:0; padding:0; left:-2000px; top:-2000px; } .tree-context { margin:20px; padding:0; width:180px; border:1px solid #979797; padding:2px; background:#f5f5f5; list-style-type:none; }.tree-context li { height:22px; margin:0 0 0 27px; padding:0; background:#ffffff; border-left:1px solid #e0e0e0; }.tree-context li a { position:relative; display:block; height:22px; line-height:22px; margin:0 0 0 -28px; text-decoration:none; color:black; padding:0; }.tree-context li a ins { text-decoration:none; float:left; width:16px; height:16px; margin:0 0 0 0; background-color:#f0f0f0; border:1px solid #f0f0f0; border-width:3px 5px 3px 6px; line-height:16px; }.tree-context li a span { display:block; background:#f0f0f0; margin:0 0 0 29px; padding-left:5px; }.tree-context li.separator { background:#f0f0f0; height:2px; line-height:2px; font-size:1px; border:0; margin:0; padding:0; }.tree-context li.separator span { display:block; margin:0px 0 0px 27px; height:1px; border-top:1px solid #e0e0e0; border-left:1px solid #e0e0e0; line-height:1px; font-size:1px; background:white; }.tree-context li a:hover { border:1px solid #d8f0fa; height:20px; line-height:20px; }.tree-context li a:hover span { background:#e7f4f9; margin-left:28px; }.tree-context li a:hover ins { background-color:#e7f4f9; border-color:#e7f4f9; border-width:2px 5px 2px 5px; }.tree-context li a.disabled { color:gray; }.tree-context li a.disabled ins { }.tree-context li a.disabled:hover { border:0; height:22px; line-height:22px; }.tree-context li a.disabled:hover span { background:#f0f0f0; margin-left:29px; }.tree-context li a.disabled:hover ins { border-color:#f0f0f0; background-color:#f0f0f0; border-width:3px 5px 3px 6px; }';
						$.tree.plugins.contextmenu.css = this.add_sheet({ str : css });
					}
				},
				onrgtclk : function (n, t, e) {
					$.tree.plugins.contextmenu.context_menu.show(n, t, e);
				},
				onselect : function(n, t) {
					$.tree.plugins.contextmenu.context_menu.rem();
					$.tree.plugins.contextmenu.context_menu.add(n, t);
				},
				ondeselect : function(n, t) {
					$.tree.plugins.contextmenu.context_menu.rem();
				},
				onchange : function () { 
					$.tree.plugins.contextmenu.context_menu.hide(true);
				},
				beforedata : function () {
					$.tree.plugins.contextmenu.context_menu.hide(true);
				},
				ondestroy : function () {
					$.tree.plugins.contextmenu.context_menu.hide(true);
				}
			}
		}
	});
	$(function () {
		$.tree.plugins.contextmenu.context_menu.object.hide().appendTo("body");
		$("#jstree-contextmenu a")
			.live("click", function (event) {
				if(!$(this).hasClass("disabled")) {
					$.tree.plugins.contextmenu.context_menu.exec.apply(null, [$(this).attr("rel")]);
					$.tree.plugins.contextmenu.context_menu.hide();
				}
				event.stopPropagation();
				event.preventDefault();
				return false;
			})
		$(document).bind("mousedown", function(event) { if($(event.target).parents("#jstree-contextmenu").size() == 0) $.tree.plugins.contextmenu.context_menu.hide(); });
	});
})(jQuery);

Jeśli ktoś chce aby plusik pojawiał się tylko w Operze musi nieco zmodyfikować funkcje onselect i ondeselect dodając stosowny warunek. Przypominam, że [jquery umożliwia w łatwy sposób identyfikację przeglądarki](http://docs.jquery.com/Utilities/jQuery.browser), oczywiście jeśli przedstawia się „prawdziwym imieniem”.

Idąc dalej śladem zaspokajania własnych potrzeb napisałem też swój plugin. Nazwałem go zgodnie z przyjętą zasadą **”Context conten”** i jak się można domyślić służy on do wyświetlania określonej treści powiązanej z danym elementem. W moim konkretnym przypadku miał służyć do wyświetlania podpowiedzi pod elementem w chwili jego wybrania.

(function ($) {
	$.extend($.tree.plugins, {
		"contextcontent" : {
			contenxt_content: {
				class_name: 'jstree-contextcontent',
				show: function(NODE, TREE_OBJ) {
					var id = $(NODE).attr('id');
					var cls = this.class_name;
					$('#'+id+' > .'+cls).show();
					$('#'+id+' > .'+cls+' a').click(function() {
						window.location.href = $(this).attr('href');
					});
				},
				hide : function (NODE, TREE_OBJ) {
					var id = $(NODE).attr('id');
					var cls = this.class_name;
					$('#'+id+' > .'+cls).hide();
				}
			},
 
			callbacks : {
				oninit : function (TREE_OBJ) {
					if (TREE_OBJ.settings.plugins.contextcontent.class_name) {
						$.tree.plugins.contextcontent.contenxt_content.class_name = TREE_OBJ.settings.plugins.contextcontent.class_name;
					}
					var cls = $.tree.plugins.contextcontent.contenxt_content.class_name;
					if(!$.tree.plugins.contextcontent.css) {
 
						var css = '.'+cls+' { display: none;}';
						$.tree.plugins.contextcontent.css = this.add_sheet({ str : css });
					}
				},
				onselect : function (NODE, TREE_OBJ) {
					$.tree.plugins.contextcontent.contenxt_content.show(NODE, TREE_OBJ);
				},
				ondeselect : function(NODE, TREE_OBJ) {
					$.tree.plugins.contextcontent.contenxt_content.hide(NODE, TREE_OBJ);
				}
			}
		}
	});
})(jQuery);

Aby go właściwie użyć trzeba:

1\. Dołączyć skrypt do dokumentu html

<script type="text/javascript" src="jquery.js" charset="utf-8"></script>
<script type="text/javascript" src="jsTree/jquery.tree.js"></script>
<script type="text/javascript" src="jsTree/plugins/jquery.tree.contextcontent.js"></script>

2\. Dodać do configa jstree stosowny wpis

$(function () {
	$("#demo").tree({
		plugins : {
			contextcontent : {
				class_name: 'jstree-contextcontent'
			}
		}
	});
});

3\. No i utworzyć stosowny kod html. Proszę zwrócić uwagę na divy z atrybutem class „jstree-contextcontent”. Nazwę klasy oczywiście można zmienić pod warunkiem, że zrobi się to również w configu.

<div id="demo">
<ul>
	<li id="phtml_1" class="open">
		<a href="#"><ins>&nbsp;</ins>Root node 1</a>
		<div class="jstree-contextcontent">Root node 1 tip...</div>
		<ul>
			<li id="phtml_2">
				<a href="#"><ins>&nbsp;</ins>Child node 1</a>
				<div class="jstree-contextcontent">Child node 1 tip...</div>
			</li>
			<li id="phtml_3"><a href="#">
				<ins>&nbsp;</ins>Child node 2</a>
				<div class="jstree-contextcontent">Child node 2 tip...</div>
			</li>
			<li id="phtml_4">
				<a href="#"><ins>&nbsp;</ins>Some other child node with longer text</a>
				<div class="jstree-contextcontent">Some other child node with longer text tip...</div>
			</li>
		</ul>
	</li>
	<li id="phtml_5"><a href="#">
		<ins>&nbsp;</ins>Root node 2</a>
		<div class="jstree-contextcontent">Root node 1 tip...</div>
	</li>
</ul>
</div>