Tomcat – Clustering i Loadbalancing Widgety – część druga: WidSets
Aug 18

Widgety – małe aplikacje użytkowe lub rozrywkowe, po polsku zwane często gadżetami – można spotkać coraz częściej, zarówno w sieci – na widgetach oparte są serwisy stron startowych (personalized homepages), jak NetVibes czy iGoogle – jak i poza nią, w aplikacjach desktopowych (panel gadgetów w Windows Vista albo Apple Dashboard pod Mac OSem), a nawet w telefonie komórkowym. Każdy z wymienionych serwisów widgetów istniał jednak osobno – iGoogle, NetVibes, Apple czy Opera (która od wersji 9 także wspiera widgety) miały swoje technologie i swoje zbiory gadżetów do pobrania i wykorzystania. Niektóre z nich udostępniały te technologie wszystkim, pragnącym samemu napisać jakąś małą aplikację, tym samym powiększając bibliotekę danego serwisu, ale cały czas taki twórczy użytkownik ograniczony był tylko do jednej platformy.
Dlatego też NetVibes – pionier w dziedzinie owych stron startowych – postanowił przerobić swój MiniAPI 0.3, w którym wcześniej można było pisać moduły do zamieszczania w ich serwisie, na nowy API, który został zaprezentowany w wersji 1.0 pod nazwą Universal Widget API.

Z czym to się je?

Pierwszą i najważniejszą zaletą Universal Widget API (w skrócie UWA) jest – jak sama nazwa wskazuje – jego uniwersalność. W tej chwili napisane w nim widgety można zamieścić nie tylko na stronie NetVibes, ale także z powodzeniem zaimportować na stronach iGoogle czy Live.com, umieścić na pasku gadżetów Visty czy MAC OSa, według twórców działają też na iPhone oraz pod Operą desktopową (Opera na urządzenia mobilne jeszcze nie jest na nie gotowa), ale tych ostatnich nie udało mi się uruchomić (nie jestem użytkownikiem Opery, a jej mechanizm importowania widgetów stanowi dla mnie barierę nie do przeskoczenia), a zapowiadanych jest jeszcze więcej platform (m.in. Facebook czy wspomniana Opera Mobile). Dodatkowo taki gadżet można umieścić w ramce (iframe) na dowolnej stronie, np. na blogu.
Napisany pod UWA widget stanowi kombinację HTMLa (rzecz jasna z CSSem), JavaScripta i AJAXa – te technologie wystarczą do napisania całego, działającego widgeta. Dodatkowo możliwości widgeta można znacznie rozszerzyć, podpinając go pod zewnętrzny skrypt PHP czy JSP albo pod WebService oparty na dowolnej technologii.
Dość prostym przykładem jest czytnik RSS – w UWA można go zawrzeć w pojedynczym pliku XHTML:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:widget="http://www.netvibes.com/ns/">
<head>
  <title>RSS Reader</title>

  <link rel="stylesheet" type="text/css" href="http://www.netvibes.com/themes/uwa/style.css" />
  <script type="text/javascript" src="http://www.netvibes.com/js/UWA/load.js.php?env=Standalone"></script>

  <widget:preferences>
    <preference name="url" type="text" label="URL" defaultValue="http://j2ee.pl/feed/" />
    <preference name="limit" type="range" label="Number of items to display" defaultValue="10" step="1" min="1" max="25" />
  </widget:preferences>
  <script type="text/javascript">
    var RSSReader = {}

    RSSReader.feed = false;
    RSSReader.display = function(feed) {
      if(feed) RSSReader.feed = feed;
      var feedList = widget.createElement('ul');
      feedList.className = 'nv-feedList';
      var j = 0;
      for(var i=0; i < RSSReader.feed.items.length; i++) {
        if(j >= widget.getValue('limit')) break;
        var item = RSSReader.feed.items[i];
        var li = widget.createElement('li');
        var a = widget.createElement('a');
        a.href = item.link;
        a.target = '_blank';
        var displayTitle = item.title;
        if(search != '') {
          displayTitle = String.highlight(displayTitle, search);
        }
        a.innerHTML = displayTitle;
        var title = item.content.stripTags().truncate(255);
        a.desc = title;
        a.onmouseover = function() {
          UWA.Utils.setTooltip(this, this.desc, 250);
        }
        li.appendChild(a);
        var display = true;
        if(display) { feedList.appendChild(li); j++;
      }
    }
    widget.setBody(feedList);
  }
  widget.onLoad = function() {
    widget.log('RSSReader.onLoad');
    UWA.Data.getFeed(widget.getValue('url'), RSSReader.display);
  }
  widget.onRefresh = widget.onLoad;
</script>
</head>

<body>
  <p>Loading ...</p>
</body>
</html>

Przepis na gadżet

Rdzeniem napisanego w UWA widgeta jest pojedynczy, statyczny plik XHTML, zawierający w nagłówku odniesienie do przestrzeni nazw NetVibes:

 xmlns:widget=“http://www.netvibes.com/ns/”

Następnie w nagłówku strony należy umieścić w tagu preferences opcje widgeta – takie, które będą dostępne dla użytkownika po kliknięciu odpowiedniego linka na stronie (w zależności od platformy link ten może wyglądać inaczej):

 <widget:preferences>
  <preference name="url" type="text" label="URL" defaultValue=" http://j2ee.pl/feed/" />
  <preference name="limit" type="range" label="Number of items to display" defaultValue="7" step="1" min="1" max="10" />
 </widget:preferences>

Oczywiście, posiadanie właściwości nie jest niezbędne – można napisać widget bez żadnych opcji dostępnych dla użytkownika. Wtedy po prostu pozostawia się tag pusty:

 <widget:preferences/>

Aktualnie UWA udostępnia kilka typów opcji, opartych na rodzajach pól w HTML-owych formularzach: text, password, boolean (odpowiednik checkbox), list (odpowiednik select), range (coś jak list, tylko z automatycznie generowanymi wartościami liczbowymi) i hidden. Trwają pracę nad obsłużeniem pól typu textarea. Wartości owych właściwości mogą zostać wykorzystane we właściwym javascriptowym kodzie widgeta w dalszej części strony za pomocą metody getValue obiektu widget.

Danie główne – obiekty widget i UWA

Kod javascriptowy kręci się wokół obiektu widget, którego metody analogiczne do zdarzeń (onLoad, onRefresh i inne) zawierają lub wywołują właściwy kod programu. Najczęściej – jeśli widget nie jest tylko lokalną aplikacją (np. grą), tylko sięga do zewnętrznych źródeł danych – kod ten zawiera jedną z dostarczanych przez UWA AJAXowych metod pobierania danych. W przykładzie, w metodzie onLoad można to znaleźć w linijce:

 UWA.Data.getFeed(widget.getValue('url'), RSSReader.display);

Obiekt UWA.Data dostarcza kilka podobnych metod do wyboru. UWA.Data.getText() zwraca odpowiedź w formacie plain text, getXml() – w formacie XML, getJson() – w formacie danych JSON, getFeed() jest formą tego ostatniego, z tym, że JSONowa odpowiedź dodatkowo jest kompatybilna ze wszystkimi formatami źródeł danych – RSS, Atom i inne. Wszystkie metody pobierają dwa argumenty – adres źródła danych oraz nazwę funkcji, do której ma zostać przesłana odpowiedź (w przykładzie naszego czytnika RSS jest to metoda RSSReader.display()). Dodatkowo obiekt UWA.Data posiada metodę request(), która pobiera argumenty w postaci adresu oraz obiektu, przechowującego szczegóły żądania. Tak naprawdę wszystkie wymienione wcześniej metody (getText, getFeed itp.) są odpowiednio przeładowaną metodą request. Metody tej w czystej postaci używa się przy bardziej zaawansowanych potrzebach.
Same źródła natomiast mogą być zupełnie dowolne – pod adresem, przekazywanym do metody, może znajdować się (jak w naszym przykładzie) kanał RSS, ale może to być dowolna inna zawartość (np. skrypt PHP, ASP czy JSP) generująca odpowiedź w odpowiednim formacie. Może to być, jak już wspomniałem, skrypt specjalnie na potrzeby tego widgeta napisany.
Wracając do naszego przykładu – skoro już metoda getFeed zwróciła nam JSONowy obiekt z podanego adresu, przyjrzyjmy się metodzie, do której odpowiedź z serwera jest przekazywana. Metoda display przetwarza zawartość zwróconego obiektu feed i na jego podstawie buduje ciało strony. Używane są do tego metody obiektu widget takie, jak createElement, tworząca element HTMLowy podanego jako argument typu (np. widget.createElement(’a’) stworzy odnośnik) oraz metod obiektów reprezentujących owe elementy (addContent, setHTML, setStyle…). W ten sposób tworzona jest struktura DOM dokumentu, która, gdy już będzie gotowa, ładowana jest do ciała strony metodą widget.setBody, zastępując aktualną jego zawartość (dlatego jedyną zawartością znacznika <body> jest tymczasowy tekst „Loading…”, który znika po tym, jak metoda display przetworzy odpowiedź na żądanie).

Deser – skrypty wykonywane po stronie serwera

Jak już wspomniałem, widget może sięgać do źródeł danych specjalnie dla niego utworzonych, np. skryptów PHP. Jako przykład podam widgeta, pobierającego dane nie z jednego, a z wielu kanałów Atom (w tym przypadku – blogów z serwisu Blogger, których adresy przechowywane są w pliku myfeed.txt) i informujący o tym, kiedy każdy z nich był ostatnio aktualizowany.

Poniżej cytuję sam kod JavaScript z pliku XHTML widgeta. Nie korzysta on z żadnych ustawień (preferences) widgeta, więc obudowanie go odpowiednim kodem XHTML, wzorowanym choćby na powyższym przykładzie czytnika RSS, nie będzie trudne:

<script type="text/javascript">
var xmlHttp = false;
var blogNote = false;
var MyFeed = {
  feed : false,
  feedArray : false,
  oneFeed : false,
  processFeed : function(feed) {
    widget.log(feed);
  }
}

MyFeed.parseFeeds = function(feed) {
  if(feed) MyFeed.feed = feed;
  widget.log(feed);
  var feedList = widget.createElement('ul');
  feedList.className = 'nv-feedList';
  feedArray = new Array();
  for(var i=0;i<MyFeed.feed.list.length;i++) {
    var li = widget.createElement('li');
    var titleTxt = widget.createElement('a');
    titleTxt.className = 'myFeed_blogTitle';
    titleTxt.href = MyFeed.feed.list[i].url;
    titleTxt.target = '_blank';
    titleTxt.innerHTML = MyFeed.feed.list[i].title;
    li.appendChild(titleTxt);
    li.appendChild(widget.createElement('br'));
    var dateTxt = widget.createElement('span');
    dateTxt.className = 'myFeed_when';
    dateTxt.innerHTML = MyFeed.getWhen(MyFeed.feed.list[i].entry_when);
    li.appendChild(dateTxt);
    li.appendChild(widget.createElement('br'));
    var articleTxt = widget.createElement('a');
    articleTxt.className = 'myFeed_title';
    articleTxt.href = MyFeed.feed.list[i].url;
    articleTxt.target = '_blank';
    articleTxt.innerHTML = MyFeed.feed.list[i].entry_title;
    li.appendChild(articleTxt);
    li.appendChild(widget.createElement('br'));
    var categoriesTxt = widget.createElement('span');
    categoriesTxt.className = 'myFeed_categories';
    for(var j=0;j<MyFeed.feed.list[i].entry_cats.length;j++) {
      var categoryTxt = widget.createElement('span');
      categoryTxt.className = 'myFeed_category';
      categoryTxt.innerHTML = MyFeed.feed.list[i].entry_cats[j];
      categoriesTxt.appendChild(categoryTxt);
      if(j+1 == MyFeed.feed.list[i].entry_cats.length) {
        break;
      }
      categoriesTxt.innerHTML += ', ';
    }
    li.appendChild(categoriesTxt);
    feedList.appendChild(li);
  }
  widget.setBody(feedList);
}

MyFeed.getWhen = function(datestr) {
  var then = parseInt(datestr);
  var now = Date.parse(Date());
  var whentxt = "";
  var when = now - then;
  if(when < 3600000) {
    whentxt = ""+Math.floor(when/60000)+" minut temu";
  } else if (when < 86400000) {
    whentxt = ""+Math.floor(when/3600000)+" godzin temu";
  } else {
    whentxt = ""+Math.floor(when/86400000)+" dni temu";
  }
  return whentxt;
}

widget.onLoad = function() {
  var url = 'myfeed.php';
  UWA.Data.getJson(url, MyFeed.parseFeeds);
}
</script>

Widget ten wysyła zapytanie do specjalnie utworzonego skryptu PHP, który sam pobiera dane z kanałów i na ich podstawie generuje odpowiedź w formacie JSON, którą odsyła do aplikacji. Następnie metoda parseFeeds ze zwróconego JSONa (w specjalnie utworzonym formacie, nie tym zwracanym przez metodę getFeed – oto dowód, że w formacie JSON można przesyłać naprawdę dowolne dane, nie tylko te kompatybilne z uniwersalnym formatem kanałów RSS i podobnych).

Oto zawartość pliku myfeed.php:

<?php
error_reporting(E_ALL);
header ("Content-type: text/plain");

if(!file_exists("myfeed.txt"))
  echo "";
else {
  $file = fopen("myfeed.txt","r");
  echo " { \"list\" : ";
  $ec=0;
  while(!feof($file)) {
    if($ec == 0)
      echo " [ ";
    else
      echo " , ";
    $ec++;

    $url = chop(fgets($file));
    $xmlDoc = new DOMDocument();
    $xmlDoc->load($url);

    //get elements from "<feed>"
    $feed=$xmlDoc->getElementsByTagName('feed')->item(0);
    $feed_title = $feed->getElementsByTagName('title')->item(0)->nodeValue;
    $feed_url = $url;
    $feed_author = $feed->getElementsByTagName('author')->item(0)->firstChild->nodeValue;

    //get and output first "<item>" element
    $feed_entry=$xmlDoc->getElementsByTagName('entry')->item(0);
    $feed_entry_when = strtotime($feed_entry->getElementsByTagName('published')->item(0)->nodeValue)*1000;
    $feed_entry_cats = $feed_entry->getElementsByTagName('category');
    $feed_entry_categories = array();

    //get post categories
    for($i=0; $i<$feed_entry_cats->length; $i++) {
      $feed_entry_categories[] = $feed_entry_cats->item($i)->attributes->getNamedItem('term')->nodeValue;
    }
    $feed_entry_title = $feed_entry->getElementsByTagName('title')->item(0)->nodeValue;

    echo " { \n";
    echo "\"title\" : \"" . str_replace("\"","\\\"",$feed_title) . "\" , \n";
    echo "\"author\" : \"" . $feed_author . "\" , \n";
    echo "\"url\" : \"" . $feed_url . "\" , \n";
    echo "\"entry_when\" : \"" . $feed_entry_when . "\" , \n";
    echo "\"entry_title\" : \"" . str_replace("\"","\\\"",$feed_entry_title) . "\" , \n";
    echo "\"entry_cats\" : ";
    for($i=0; $i<count($feed_entry_categories); $i++) {
      if($i==0)
        echo " [ ";
      else
         echo " , ";
      echo "\"" . $feed_entry_categories[$i] . "\"";
    }
    echo " ] \n";
    echo " } \n";
  }
  echo " ] }";
}
fclose($file);
?>

Plik myfeed.txt, z którego powyższ skrypt czyta adresy kanałów Atom zawiera po prostu kilka adresów kanałów z kilku zaprzyjaźnionych blogów w serwisie Blogger.com.

Przydatność do spożycia

UWA jest wynalazkiem bardzo nowym, wciąż jeszcze rozwijanym i nie w pełni kompatybilnym z wszystkimi tymi platformami, którymi przechwalają się twórcy. Nie mniej jednak wyraźnie widać jego zalety i możliwości. Nie miałem możliwości przetestować jego działania pod Vistą czy MAC OSem ani na iPhone, a testy pod Operą zakończone były porażką, ale w głównych zastosowaniach – na stronach NetVibes, iGoogle czy w ramce iframe na dowolnej innej stronie – sprawdzają się bardzo dobrze. Myślę, że gdy zostanie dopracowane i przeniesione na jeszcze większą liczbę platform (telefony i urządzenia mobilne inne niż iPhone), Universal Widget API może cieszyć się zasłużonym zainteresowaniem, a programy w nim napisane mogą być w równym stopniu proste, jak przydatne.

Podziel się z innymi:
  • Wykop
  • Digg
  • del.icio.us
  • StumbleUpon
  • Slashdot

Leave a Reply

Security Code: