XULJet – vytváříme desktopové aplikace v JavaScriptu

Pokud používáte prohlížeč nebo poštovní klient od Mozilly, používáte XUL. XUL je totiž jazyk, v němž je popsáno vykreslování prvků uživatelského rozhraní – všechny tlačítka, panely, menu… V článku si ukážeme základy práce s tímto jazykem a jeden JavaScriptový framework, který vaši práci usnadní.

Dnes je naprosto běžné, že se webové aplikace prosazují i na místech, kde to dříve bylo nepředstavitelné. Přesto stále mají řadu omezení daných jednak bezpečnostními limity a jednak tím, že HTML nebylo vytvořeno pro popisování uživatelských rozhraní. Proto bylo učiněno již nespočetně pokusů vytvořit značkovací jazyk, který by se k tomuto účelu hodil lépe.

My zaměříme svou pozornost na značkovací jazyk XUL, který vyvinula Mozilla Foundation a který také používá pro popis a práci s uživatelským rozhraním u svých aplikací. XUL těží z provázanosti s ostatními webovými standardy, jako je JavaScript, CSS či DOM a práce s ním je srovnatelná s používáním HTML. Jako příklad si ukážeme jednoduchý XUL dokument, který popisuje malé okno s menu, stavovým řádkem a tlačítkem, které mění obsah zobrazovaného textového řádku

<?xml version="1.0"  encoding="UTF-8"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
<window
  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
  title="Hello World!"
  width="165"
  height="138">
  <vbox flex="1">
    <toolbox>
      <menubar>
        <menu label="File" accesskey="f">
          <menupopup>
            <menuitem label="Close" oncommand="window.close()"/>
          </menupopup>
        </menu>
      </menubar>
    </toolbox>
    <vbox align="center" pack="center" flex="1">
      <description id="desc">Press the button</description>
      <button label="OK" oncommand="document.getElementById('desc').value='Hello World!'"/>
    </vbox>
    <statusbar>
      <statusbarpanel flex="1" label="Ready..."/>
    </statusbar>
  </vbox>
</window>

Jak můžete vidět, tento dokument se v principu neliší od běžné HTML stránky. I JavaScriptové operace nad DOM, jako je getElementById, fungují stejně a dokonce při použití XML jmenných prostorů lze do takového okna vsadit i běžný HTML kód. K zobrazení tohoto dokumentu také postačí webový prohlížeč vybavený jádrem Gecko. Lze tedy nechat webový server místo HTML kódu generovat XUL a pokud vašim návštěvníkům nebude vadit, že musí používat Firefox, což by třeba u intranetu vadit nemělo, můžete si užívat poměrně velké škály XUL elementů. Protože prvky, jako záložky, rozdělovače a stromy nejsou emulovány pomocí JavaScriptu a HTML, jsou podstatně rychlejší a měly by odpovídat vzhledu a chování běžnému na dané platformě.

Na rozsáhlou demonstraci možností XUL se můžete podívat (ve Firefoxu) na stránce http://www.he­vanet.com/acor­bin/xul/top.xul. Přestože může vypadat možnost nahrazení HTML XULem lákavě, doporučoval bych každému dvakrát se rozmyslet, než to zkusí, protože takto používaný XUL má řadu omezení, temných zákoutí a chyb.

XULRunner

Rozumnější příležitost k práci s XULem nabízí program Mozilla XULRunner, což je v podstatě osamostatněné Gecko. Ten dokonce umožňuje pomocí XPCOM využívat v XUL aplikacích např. Javu či Python. My ale tuto možnost pomineme a zaměříme se aplikace, které pro svůj běh využívají pouze JavaScript, což je pro XULRunner nejpřirozenější alternativa. Začít s takovou aplikací je jednoduché, v podstatě potřebujete čtyři jednoduché (často jednořádkové) soubory s metadaty v určené adresářové struktuře. Jsou to:

application.ini
defaults/preferences/prefs.js
chrome/chrome.manifest
chrome/content/main.xul

Jejich obsah zde nebudeme podrobněji rozebírat, protože je budete mít k dispozici už předpřipravené, bližší informace můžete najít zde. Spuštění aplikace pak proběhne jednoduše tak, že XULRunneru předáme jako argument cestu souboru application.ini naší aplikace:

xulrunner application.ini

Znamená to, že s naší aplikací musíme distribuovat i XULRunner. Ten je naštěstí běžnou součástí distribucí a není to přebujelý moloch, takže i na Windows se obejde bez instalace a archiv s ním má cca 10 MB. Protože Firefox je vlastně také zakuklený XULRunner, jdou XULRunner aplikace pomocí argumentu -app předhodit i jemu, i když jejich chování se pak v některých ohledech liší. Bohužel v současné době není implementována možnost zadat XULRunneru aplikaci v komprimované formě (třeba jako *.jar).

Aplikace pro XULRunner mají řadu možností, které jsou pro webové aplikace z bezpečnostních důvodů nedostupné. Mohou spouštět lokální procesy, pracovat s lokálními soubory, vytvářet síťová spojení s libovolnými zdroji, pracovat s klávesovými zkratkám, používat nativní souborové dialogy, používat více vláken atd.

Jako ukázku poměrně rozsáhlé desktopové aplikace napsané v JavaScriptu na běhovém prostředí XULRunneru může posloužit projekt Pencil, což je nástroj na tvorbu diagramů a prototypování uživatelských rozhraní. Pencil využívá toho, že krom vkládání HTML mohou XULRunner aplikace snadno pracovat i s SVG.

JavaScript více či méně dobře ovládá prakticky každý webový vývojář, práce s XULem je pro webové vývojáře také přirozená. JavaScript je díky konkurenci webových prohlížečů jedním z nejrychlejších dynamicky typovaných jazyků současnosti a XULRunner běží na všech platformách, na nichž běží Firefox. Proč tedy nejsme doslova zasypáni desktopovými aplikacemi napsanými v JavaScriptu? 

Žádná kaše se nejí tak horká, jak se uvaří, a ani práce s XULem není tak snadná a elegantní, jak by se na první pohled mohlo zdát. Dnes už je situace podstatně lepší, ale dříve museli vývojáři používající XULRunner neustále bojovat s chybami a nedodělky, které museli ošetřovat nestandardními zásahy, u nichž se člověk mohl jen modlit, jestli s další verzí XULRunneru nebo Firefoxu budou ještě fungovat. A byla zde ještě velká pravděpodobnost, že se nebudou na všech platformách chovat úplně stejně. Nedá se ani počítat s žádnou velkou podporou integrovaných vývojových prostředí, které by usnadňovaly práci a orientaci v projektech s množstvím XUL souborů s vloženým JavaScriptem a samostatnými zdrojovými soubory s logikou. Navíc oblíbené JavaScriptové knihovny s XULem prakticky nepočítají. A dokud (i pokud) se tento typ aplikací nerozšíří, nedá se čekat, že by se situace v těchto oblastech zásadně změnila.

XULJet

Prohlášení: Autor článku je zároveň autorem dále popisovaného frameworku XULJet.

XULJet je malý mladý open source framework pro JavaScript, který si klade za cíl vývoj aplikací běžících na XULRunneru pokud možno co nejvíce usnadnit. Používá systém nezávislých komponent popsaných přímo v JavaScriptu. Komponenty nejsou jednotlivá tlačítka, ale určité logické celky prvků, které mají daný význam, stav a chování. Komponenty mohou obsahovat další komponenty, být vzájemně nahrazovány, otevírány v samostatných oknech atd.

Pro generování XUL kódu má každý tag svou metodu s neomezeným počtem argumentů, které se jako první argument volitelně předávají atributy elementu. Například XUL kód

<description id="desc">Press the button</description>

se vygeneruje pomocí kódu

description({id: "desc"}, "Press the button")

XULJet využívá knihovnu Prototype (ve verzi 1.7), což není knihovna, která by se momentálně držela na výsluní přízně mas, ale pro daný účel se hodí o něco lepe než konkurence.

Základní třída, od které se odvozují vlastní komponenty, se jmenuje XULComponent a odvozené komponenty by měly definovat vlastní metodu renderOn, která se stará o vykreslení obsahu komponenty. Ta přijímá jako argument instanci třídy XULCanvas, což je třída, která definuje metody pro vykreslování XUL tagů výše popsaným způsobem.

renderOn: function(xul)
{
  var descId = XULId();
  with (xul)
  {
    description({id: descriptionId}, "Press the button")
    button({label: "OK", oncommand: "alert('Hello World!')"})
  }
}

Funkce XULId() vrací unikátní identifikátor. Aby jednotlivé komponenty byly co nejuniverzálněji použitelné, měly by být identifikátory generovány dynamicky, aby stejná komponenta mohla být zobrazena v okně několikrát. V uvedeném příkladě jsme definovali akci tlačítka pomocí řetězce s JavaScriptem, ale mnohem lepší by bylo mít možnost použít na daném místě referenci na metodu nebo uzávěr. XULJet pro tento případ používá funkci $C(). Její jméno bylo zvoleno pro zachování konvencí knihovny Prototype a písmeno C znamená closure (uzávěr). Ta udělá to, že předaný uzávěr zavede do registru pod unikátním jménem a vrátí kód, který ji v tomto řetězci vyhledá a spustí.

button({label: "OK", oncommand: $C(function() {
  alert("Hello World!")
})})

Samotná funkce $C má dva problémy. První souvisí s tím, že při každém překreslení nebo nahrazení komponenty se zaregistrují nové uzávěry a staré zůstanou zbytečně viset v paměti. Druhý je složitější a souvisí s fungováním uzávěrů v JavaScriptu. Pokud v uzávěru používáte referenci na this, nebude this referencovat aktuální komponentu, jak by se dalo očekávat, ale objekt okna. To lze obejít buď tak, že na začátku vykreslovací metody definujeme proměnnou

var self = this;

a následně všude, kde bychom použili this, použijeme self, nebo použijeme metodu třídy XULComponent jménem $C(). Ta se postará mechanismem běžným u knihovny Prototype o svázání this s aktuální komponentou a navíc řekne komponentě, že pro ni byl zaregistrován uzávěr, takže ho může při svém zrušení nebo při překreslení obsahu předhodit krvavé tlamě garbage collectoru. Lepší kód tedy vypadá takto:

button({label: "OK", oncommand: this.$C(function() {
   alert("Hello World!")
})})

Říkal někdo, že Lisp je závorkové peklo? Ani tento příklad není zcela korektní. Tentokrát je problém ve funkci alert. Z dobrých důvodů jsou totiž objekty všech komponent vytvářeny v kontextu hlavního okna aplikace a v tomto kontextu jsou vykonávány i všechny uzávěry. Ostatní okna se používají pouze pro vykreslování. Proto každá komponenta vlastní referenci na okno, v němž je zobrazena, a tak funkce jako alert, confirm nebo třeba $ (funkce knihovny Prototype pro vyhledání objektu pomocí identifikátoru) mají v třídě XULComponent svoji obdobu, která ji deleguje na skutečné okno komponenty.

button({label: "OK", oncommand: this.$C(function() {
   this.alert("Hello World!")
})})

Samozřejmě všude, kde používáme uzávěry, mohou být i reference na funkce. Uzávěry předávané jako argument metodě $C mají jeden volitelný argument. Je jím samotný element, jehož se událost týká. To se hodí například u elementu pro výběr barvy.

colorpicker({onselect: this.$C(function(colorPicker) {
  this.color = colorPicker.color
})})

Nyní máme dost informací pro to, abychom si ukázali, jak výše uvedený příklad XUL dokumentu vypadá jako aplikace napsaná pomocí frameworku XULJet.

var HelloWorld = Class.create(XULComponent,
{
  initialize: function($super, aWindow)
  {
    $super(aWindow);
    this.message = "Hello World!";
  },
  renderOn: function(xul)
  {
    var descriptionId = XULId();

    with (xul)
    {
      vbox({flex: 1},
        toolbox(
          menubar(
            menu({label: "File", accesskey: "f"},
              menupopup(
                menuitem({label: "Close", oncommand: "window.close()"}))))),
        vbox({align: "center", pack: "center", flex: 1},
          description({id: descriptionId}, "Press the button"),
          button({label: "OK", oncommand: this.$C(function() {
            this.$(descriptionId).value = this.message})})),

        statusbar(
          statusbarpanel({flex: 1, label: 'Ready...'})))
    }
  }
});
function main()
{
  var rootComponent = new HelloWorld(window);
  window.setTitle("XULJet");
  rootComponent.beMainWindowComponent();
}

Nejdříve jsme definovali třídu HelloWorld jako potomka třídy XULComponent. V konstruktoru jsme inicializovali lokální proměnnou message. Všimněte si, že třída přijímá jako argument konstruktoru okno. Argument $super je způsob, jakým framework Prototype umožňuje inicializaci v kontextu předka.

Metoda renderOn se stará o vykreslení komponenty. Funkce main je vstupní bod do aplikace. V ní jsme vytvořili instanci naší komponenty, přiřadili titulek okna a určili, tuto komponentu jako kořenovou komponentu hlavního okna. Okna jsou instancemi třídy XULWindow (přesněji řečeno, objekty oken jsou rozšířeny rozhraním této třídy).

Sekce

Komponenty mohou obnovovat svůj obsah, být nahrazovány jinými či odstraňovány (metody refresh, replace, remove). Občas jsou ale komponenty příliš velkým kanonem na vrabce. Občas je potřeba pouze překreslit jen část komponenty. Pro takový případ má XULJet tzv. sekce, které se ohraničují pomocí metod sectionStart a sectionEnd. Náhrada obsahu sekce se dělá pomocí metody refreshSection, která přijímá jako první argument identifikátor sekce a jako druhý XULCanvas, do kterého se provede vykreslení náhradního obsahu.

var sectionId = XULId();
with (xul)
{
  sectionStart(sectionId)
  description("Press the button")
  sectionEnd(sectionId)
  button({oncommand: this.$C(function() {
    this.refreshSection(sectionId, function(_xul) {
      _xul.description("Hello World!")
    })
  })})
}

Složitější vykreslování

Pokud potřebujete rozdělit třídu renderOn do více metod, můžete použít metodu insert, která vytváří subcanvas a zajistí i správné vázání this.

O něco složitější je metoda collect, která se používá na vykreslování kolekcí. Jako argument přijímá zpracovávanou kolekci a jako druhý uzávěr nebo odkaz na metodu, která jako argumenty přijímá subcanvas, právě zpracovávanou položku a nepovinně i index položky.

this.collect(this.data, function(_xul, item, index) {
  _xul.listitem(
    label({value: item.name}),
    label({value: item.sex}),
    label({value: item.color}),
    label({value: item.desc}))
})

Formuláře

XUL jako takový žádné formuláře nemá. Hromadné zpracování vstupních polí se ale hodí, proto XULJet obdobu formulářů definuje. Tvoří se podobně jako sekce a to pomocí metod formStart a formEnd. Jednotlivé vstupní elementy (jako textová pole či checkboxy) se vážou na datové objekty pomocí metody on.

(textbox()).on(this.data, "name")

Při vykreslení formuláře se ze svázaného objektu načte aktuální hodnota. Při zpracování formuláře pomocí metody processForm se změní obsah svázaného datového objektu (zde pomocí výrazu this.data[„name“] = value). Obsah vstupních elementů lze získat i bez ovlivnění svázaných objektů pomocí metody formValue, což se může hodit pro validaci. Třída XULCanvas navíc definuje několik pomocných metod (jako formlistbox), které zjednodušují zpracování komponent, u nichž se pracuje s kolekcemi.

formlistbox({seltype: "multiple", rows: 6}, this.collection, this, this.data, "items")

Vložené HTML a SVG, tisk

Kromě třídy XULCanvas má XULJet i obdobné třídy HTMLCanvas a SVGCanvas, které umožňují generovat pomocí JavaScriptu i HTML a SVG kód a vkládat jej do XUL dokumentů. Subcanvas pro tyto značkovací jazyky se tvoří pomocí metod HTML a SVG.

  with (xul.HTML())
  {
    div({style: "text-align: center; color: red; border: 1px solid red"},
      h1("XULJet"))
  }

Subcanvasy lze výhodně využít i pro generování tiskových sestav. V tomto případě je vytvořen rámec s prázdnou HTML stránkou, do které se vloží vygenerovaný obsah a zadá se tisk. XULRunner má lepší možnosti nastavování vlastností tisku než je tomu u webových aplikací.

var html = new HTMLCanvas();
with (html)
{
  h1("Printed page")
  p("Lorem ipsum dolor sit amet...")
}
with (html.SVG())
{
  svg({width: "120px", height: "120px"},
    circle({r: 50, cx: 60, cy: 60, style: "stroke: red; fill: none; stroke-width: 20" }),
    line({x1: 33, y1: 93, x2: 93, y2: 23, style: "stroke: red; stroke-width: 20"}))
}
this.print(html)

Soubory, databáze, sítě…

Samotný XULJet tyto oblasti zatím nijak neřeší, takže je nutné je provádět přímo pomocí komponent Mozilly. Např. takto vypadá zjištění domovského adresáře:

var dirService = Components.classes["@mozilla.org/file/directory_service;1"].
                 getService(Components.interfaces.nsIProperties);
var homeDirFile = dirService.get("Home", Components.interfaces.nsIFile);
var homeDirPath = homeDirFile.path;

Co se týče databází, je nejpřirozenější použít CouchDB, protože vrací výsledky ve formátu JSON, čímž odpadá přemapovávání na objekty JavaScriptu, a dá se s ní snadno komunikovat pomocí AJAXu.

Při rozdělení aplikace na více samostatných modulů lze zdrojový soubor v JavaScriptu vložit do projektu nejjednodušeji prostým přidání dalšího tagu script do souboru main.xul. Ovšem elegantnější řešení je použít funkci loadJS, díky které nemusíme obsah souboru main.xul upravovat zvlášť pro každý nový projekt.

Běhové prostředí Mozilla XULRunner se přirozeně nehodí na všechny typy programů, obzvláště pokud chceme zůstat pouze u JavaScriptu. Nicméně bohatá paleta GUI ovládacích prvků jazyka XUL, snadná práce s vektorovou grafikou a podobnost s webovými aplikacemi dělá z XULRunneru zajímavou alternativu k ostatním platformám určeným pro vývoj desktopových aplikací. Vedle node.js tak dává JavaScriptu další šanci proniknout mimo svou hlavní doménu scriptovacího jazyka.

Pavel Křivánek je nezávislý konzultant specializující se na Smalltalk a framework Seaside. Jako open-source vývojář se soustředí především a remodularizaci projektů Squeak a Pharo.

Komentáře: 9

Přehled komentářů

heptau XUL aplikace ve web prohlizeci
imploder Použití na webu
Pavel Křivánek Re: Použití na webu
imploder Re: Použití na webu
Pavel Křivánek Re: Použití na webu
imploder Re: Použití na webu
p.franc Remote XUL
imploder Re: Remote XUL
Pavel Křivánek Re: Remote XUL
Zdroj: https://www.zdrojak.cz/?p=3377