Java front-end webpagina’s zijn server-side georiënteerd (.jsp’s en HTML templating). Dit is jarenlang voldoende geweest, omdat webpagina’s niet client-side mogelijk waren door grote browser incompatibiliteit wat betreft JavaScript en HTML/CSS.
Niels Baloe
Tegenwoordig is deze browser incompatibiliteit er echter niet meer en kun je een webpagina gerust volledig client-side maken: back to basics. Ook kun je tegenwoordig client-side JavaScript gebruiken voor interactie afhandeling, omdat JavaScript in alle browsers (ES5, 97.88% http://caniuse.com/#feat=es5 ) hetzelfde is. Het grote voordeel van client-side webpagina’s is dat je de server ontlast van continu dynamische webpagina genereren. Geen wonder dat men overstapt van zware Java HTML-templating frameworks naar JavaScript frameworks.
JavaScript is alleen nog niet zo volwassen als Java, hoewel het daar wel snel naar toe gaat (zie Tabel 1). JavaScript verandert momenteel door de populariteit van server Node.js in een Java of C# variant compleet met classes, excepties, lambda notatie, workerthreads, annotaties, futures, librarybeheer en nog veel meer. Het is appels met peren vergelijken, maar in deze tabel zie je de startdatum van relatief vergelijkbare features. Merk op dat in de Java wereld heel veel (uiteindelijk) is gestandaardiseerd in de taal zelf, terwijl dit voor JavaScript nu nog kiezen is uit duizenden losse libraries.
Wat | Javascript | sinds | Java | sinds |
worker threads | ES5 | 2009 | Java 1.02 | 1997 |
compiler checks | lint (node-jslint) | 2011 | Java 1.02 | 1997 |
.jar-achtige packaging | requireJS | 2011 | Java 1.02 | 1997 |
bytecode | asm.js | 2013 | Java 1.02 | 1997 |
strong-typed | typescript | 2014 | Java 1.02 | 1997 |
classes | modules, ES6 | 2015 | Java 1.02 | 1997 |
unit testing | mocha (jasmine, tape) | 2011 | junit | 1998 |
librarybeheer | npm | 2011 | maven | 2002 |
dependency injection | gulp-inject / inversify / … | 2014 | Spring DI | 2004 |
annotaties | decorators, ES7 | ? | Java 5 | 2005 |
futures | promises, ES6 | 2015 | Java 8 | 2014 |
lambda notatie | arrow functies, ES6 | 2015 | Java 8 | 2014 |
async-await (C#) | ES7 | ? | ? | ? |
Stel nou dat je het volwassen Java gebruikt als front-end taal in plaats van JavaScript. Hoe zou dat eruit zien? Die vraag probeert VertxUI te beantwoorden.
Fluent
Omdat tegenwoordig puur HTML met CSS genoeg is om een webpagina te bouwen, biedt VertxUI behalve low-level toegang ook een lichtgewicht fluent API -genaamd Fluent– die puur HTML met CSS vergemakkelijkt. Fluent werkt met methodenamen die gelijk zijn aan de HTML-tag en het eerste argument is altijd de CSS class. VertxUI heeft dus geen templates, maar alleen Java code. Bijvoorbeeld listing 1 is een menu voor Bootstrap (een veelgebruikte CSS library van Twitter).
Resultaat:
<ul class=”nav navbar-hav”>
<li class=”active”><a href=”#home”>Home</a>
<li><a href=”#about”>About</a>
</ul>
Code:
…
Fluent ul = body.ul(“nav navbar-nav”);
ul.li(“active”).a(null, “Home”, “#home”, controller::onMenuHome);
ul.li(null).a(null, “About”, “#about”, controller::onMenuAbout);
In listing 1 is de variabele body een static import van klasse Fluent, net zoals je console, document en window kunt gebruiken. Een instantie van klasse Controller (‘controller::’) zorgt voor event-afhandeling.
Wat hier niet is weergegeven, is dat er in dit volledige voorbeeld ook een klasse Store is die een lijst van klasse Model beheert. Deze traditionele separation-of-concern genaamd ‘MVC’ model-view-controller(-store) opzet hoef je niet te gebruiken, maar is onmisbaar bij JUnit tests voor het mocken.
Natuurlijk kun je ook inline lambda methodes schrijven als je bezig bent om je project op te zetten. Je ziet in het volgende Bootstrap voorbeeld Listing 2 ook .css() en .att(), waarmee je volledig controle hebt over de HTML:
Resultaat:
<div class=”form-group”>
<label style=”font-size: 200%” for=”n”>Wat</label>
<input class=”cssClass” type=”text” id=”n”>
</div>
Code:
Fluent div = body.div(“form-group”);
div.label(null, “Wat”).style(Style.fontSize, “200%”).att(Att.for, “n”);
div.input(“cssClass”, “text”).id(“n”).keydown( (fluent, keyEvent) -> {
console.log(“You typed: ” + fluent.domValue());
});
View On Model
De kracht van Fluent zit in de dynamische weergave, omdat wijzigingen automatisch en zo optimaal mogelijk worden doorgevoerd. Dit is vergelijkbaar met ReactJS (gemaakt door Facebook) die er bijvoorbeeld voor zorgt dat op Facebook niet de hele lijst van vrienden opnieuw gerenderd wordt als een online-status wijzigt. Een ViewOn<Model> maak je met .add() en heeft als argumenten een initieel model plus een methode die declareert hoe je van een model een view maakt. Bijvoorbeeld listing 3:
Resultaat:
<table>
<tbody class=”striped”>
(per persoon:)
<tr>
<td class=”fat”>*name*</td>
<td>*quantity*</td>
</tr>
</tbody>
</table>
Code:
public View {
private ViewOn<List<Person>> table;
public void start(Controller controller) {
List<Person> initialPersons = controller.getPersons();
Fluent middle=body.div();
…
table = middle.add(initialPersons, persons -> {
if (persons == null || persons.isEmpty()) {
return Span(“big”, “No people yet”);
}
Fluent result = Table().tbody(“striped”);
for (Person person : persons) {
if (person.isSubscribed()) {
result.tr(
Td(“fat”, person.getName()),
Td(null, person.getQuantity()));
}
}
return result;
});
}
public void syncPersons() {
table.sync();
}
}
De controller roept syncPersons() aan als de lijst van personen wijzigt, bijvoorbeeld na een lokale wijziging of na een wijziging die via een websocket asynchroon is binnengekomen. De ‘table.sync()’ aanroep wijzigt de visuele weergave zelf. Je kunt uiteraard ook complexe weergaves (zoals wizards) of geneste ViewOn’s schrijven, wat zeer gemakkelijk is omdat je alles declaratief uitschrijft.
Alle code in VertxUI is puur Java, dus als je liever streams gebruikt dan kan dat ook, zoals te zien is in Listing 4.
table = middle.add(initialPersons, persons -> {
if (persons == null || persons.isEmpty()) {
return Span(“big”, “No people yet”);
}
return Table().tbody(“striped”, persons
.filter(person -> person.subscribed())
.map(person -> Tr(
Td(“fat”, person.getName()),
Td(null, person.getQuantity()))));
});
View on …. State
In het Bootstrap menu voorbeeld, hoort na een menuklik de CSS “active” te wisselen naar het gekozen menu-item. Welk menu-item CSS klasse “active” heeft, is typisch een “state”. Het is handig om een “state” te herkennen en ermee om te gaan als een DTO of entiteit die alleen niet uit de database komt of wordt opgeslagen. Voor een state is ViewOn ook te gebruiken, zie listing 5:
String initState = “home”; // of iets anders als dat uit de URL blijkt
…
ViewOn<String> menu = body.add(initState, state -> {
Fluent ul = Ul(“nav navbar-nav”);
ul.li(state.equals(“home”) ? “active” : null).a(null, “Home”, “#home”, controller::onMenuHome);
ul.li(state.equals(“about”) ? “active” : null).a(null, “About”, “#about”, controller::onMenuAbout);
});
JUnit – unit testen
Omdat Fluent intern een virtuele view heeft, kun je deze ‘misbruiken’ voor JUnit testen. Dit testen gaat razendsnel, omdat geen JavaScript compilatie en geen browser (opstarten en afsluiten) nodig is. Je draait dus gewoon de test in JUnit zoals je dat kent. In de praktijk mock je meestal een Store om netwerkverkeer voor te zijn, alleen in het volgende voorbeeld (listing 6) wordt de controller aanroep uit het vorige voorbeeld gemockt met Mockito.
class Test {
@Test
public void test() {
// Fake resultaat na netwerkverkeer
List<Person> persons = new ArrayList<>();
String name = “Jan ” + Math.random();
persons.add(new Person(name, 2000, true));
// Opstartsequence
View view = new View();
Controller controller = Mockito.spy(Controller.class);
Mockito.when(controller.getPersons()).thenReturn(persons);
view.start(controller);
assertEquals(“1 rij”,1,VirtualDomSearch.getElementsByTagName(“TR”, body));
List<Fluent> tds = VirtualDomSearch.getElementsByTagName(“TD”, body);
assertTrue(“2 kolommen”, 2, tds.length());
assertTrue(“testnaam”, name, tds.get(0).txt());
}
}
JUnit – integratie testen
Als een project volwassen wordt en bijvoorbeeld externe JavaScript libraries gebruikt zijn, worden grafische integratietests onmisbaar. Hiervoor kun je met Fluent dual-language testen in een headless browser met ‘register-and-run’. Je registreert met registerJS() op een nummer welke code je wil uitvoeren en met runJS() voer je dit uit vanuit Java. Je hebt hierdoor meer controle dan met Selenium, omdat je JavaScript code kunt afwisselen met Java code en ook asserts kunt uitvoeren in de browser.
Teruggrijpend op het allereerste Bootstrap menu voorbeeld, simuleren we nu een menu-klik door rechtstreeks controller.onMenuAbout() aan te roepen. De code in Listing 7 is alles wat je nodig hebt, de compilatie naar JavaScript gebeurt on the fly:
class Test extends TestDOM {
@Test
@GwtIncompatible
public void test() throws Exception {
System.out.println(“Java”);
runJS(100);
}
@Override
public Map<Integer, Runnable> registerJS() {
Map<Integer, Runnable> result = new HashMap<>();
result.put(100, () -> testWithDOM());
return result;
}
public void testWithDOM() {
console.log(“JavaScript”);
View view = new View();
Controller controller = new Controller(new StoreEmpty(), view);
view.start(controller);
// zoek het actieve menu item
NodeList actives = document.getElementsByClassName(“active”);
assertEquals(“aantal”, actives.length(), 1);
assertTrue(“titel”, ((Element) actives.item(0).getChildNodes().at(0))
.getTextContent().equals(“Home”));
controller.onMenuAbout(null, null);
// zoek opnieuw
actives = document.getElementsByClassName(“active”);
assertEquals(“aantal”, actives.length(), 1);
assertTrue(“titel”, ((Element) actives.item(0).getChildNodes().at(0))
.getTextContent().equals(“About”));
}
}
(Merk op dat de rest in Listing 7 ook in de eerste niet-DOM test had gekund.)
VertX
Je kunt iedere back-end software gebruiken met VertxUI (zie handleiding), alleen samen met VertX biedt VertxUI diverse faciliteiten, zoals FigWheely en POJO verkeer. Ook werkt het erg gemakkelijk: start de main() en kijk naar http://localhost zonder een IDE-plugin te installeren, zonder geleur met een .war, enz.
VertX werkt net als JavaScript volledig asynchroon en met callbacks. Asynchroon betekent dat bijvoorbeeld een VertX thread niet blockt totdat alle TCP data binnen is, maar later terugkomt bij de stack als alles binnen is. Daarmee is Vertx -behalve een zeer interessant framework- meteen ook een extreem efficiënte webserver.
VertX – FigWheely
FigWheely heeft in ontwikkelmodus een websocket-verbinding met de browser, die notificaties ontvangt als er bestanden (.js, .css enz) wijzigen. Ook zorgt FigWheely ervoor dat als je een .java bestand opslaat, VertxUI hercompileert naar JavaScript en de browser notificeert, zodat je niet steeds de server of browser hoeft te herladen tijdens ontwikkelen.
FigWheely werkt -net als heel VertxUI- zonder een IDE-plugin; de compilatie naar JavaScript gebeurt bij het opstarten van de server. Ook wordt de index.html gegenereerd, maar je kunt ook een bestaande site gebruiken óf HTML als HTML template zelf zien en een (jQuery Mobile) paginastructuur in HTML vooraf uitschrijven.
VertX – POJO
VertxUI faciliteert POJO verkeer tussen server en browser voor AJAX websockets sockJS en de VertX eventBus voor strong-typed dataverkeer ondanks dat Json over de lijn gaat. Dit is zeer krachtig in combinatie met een schemaless database zoals MongoDB: het uitbreiden van een extra tabelkolom betekent slechts één regel bij de entiteit en één regel voor een extra “TD” in de View. Listing 8 is een client-side voorbeeld van een chatprogramma met een POJO ontvanger:
WebSocket socket = window.newWebSocket(“ws://localhost/chatWebsocket”);
socket.setOnmessage(event -> {
// POJO: de kleur van de pojo in een nieuwe <li>…</li>
if (POJOfy.socketReceive(urlPOJO, event, POJOMapper,
pojo -> messages.li(“colored”, “Received: ” + pojo.getColor()))) {
return;
}
// Tekst in een nieuwe <li>..</li>
messages.li(“flat”, ((MessageEvent) event).getData().toString());
});
Listing 9 betreft de server-side van hetzelfde chatprogramma dat ook een POJO kan ontvangen. Het ziet er zo simpel uit dat je je misschien niet kan voorstellen dat dit vele malen efficiënter is dan een traditionele servlet omgeving:
List<String> ids = new ArrayList<>(); // all ids
vertx.createHttpServer().websocketHandler(socket -> { // entering
String id = socket.textHandlerID();
ids.add(id);
socket.closeHandler(data -> { // leaving
ids.remove(id);
});
socket.handler(buffer -> { // receiving
if (POJOfy.socket(socket, View.urlPOJO,buffer, Dto.class,aService::handle)){
return;
}
String message = buffer.toString();
ids.forEach(id -> vertx.eventBus().send(id, message)); // broadcasting
});
}).listen(80);
Conclusie
Wat voor bokkesprongen de taal JavaScript nog zal maken, je kunt nú al met VertxUI goede testbare webpagina’s maken die 100% in Java zijn geschreven en zijn toegespitst op de zeer volwassen back-end VertX. De unieke testbaarheid alleen is al genoeg reden om VertxUI te gebruiken. JavaScript en de bijbehorende libraries zullen volwassen worden, alleen dit kan nog jaren duren en de kans is erg klein dat je nu kiest voor libraries die over een paar jaar nog bestaan. Frameworks zelf wijzigen op dit moment ook nog heftig. Denk hier bijvoorbeeld aan Angular 2, dat is totaal anders dan Angular 1.
VertxUI biedt voordelen die puur JavaScript niet biedt: POJO’s, alles strong-typed, het feit dat je met een professionele uitontwikkelde taal in een uitontwikkelde IDE werkt, kortom: Java.
De voordelen van VertxUI versus server-side pagina’s genereren zijn duidelijk: de server wordt ontlast van voortdurend pagina’s genereren, maar serveert per gebruiker éénmalig een paar tekstbestanden en is verder alleen bezig met REST calls. De view declareren op je datamodel, betekent veel minder code (wijzigingen worden vanzelf doorgevoerd) en ook veel minder potentiële visuele fouten na interactie. Tenslotte is het volgen van nieuwe front-end ontwikkelingen gemakkelijk, omdat geen eigen HTML-templating, maar juist externe bestaande HTML/CSS frameworks worden gebruikt. Ik hoop in ieder geval dat ik met het maken van VertxUI Java front-end ontwikkeling een frisse impuls geef.
Links:
VertxUI: https://github.com/nielsbaloe/vertxui
VertX: http://vertx.io/