Jeff Liu, Ingeniør, Potix
Corporation
February 18, 2008
Innhold |
Begrepet "Ajax" ble først introdusert av Jesse James Garrett den 18. februar 2005 i hans innlegg "Ajax: en ny måte å utvikle web applikasjoner på." Fra da av har Ajax blitt et svært populært tema i miljø som driver med utvikling av web applikasjoner. I de siste 2 årene har det dukket opp mange Ajax-rammeverk til bruk for utviklingsmiljøene. Disse rammeverkene varierer fra rene JavaScript-baserte rammeverk, så som Yahoo! UI (YUI) til Flash-baserte rammeverk som Adobes AIR (Adobe Integrated Runtime). Men ett viktig konsept blir ofte oversett av media og den generelle leser: er rammeverket server-basert eller klient-basert? Denne artikkelen forsøker å avklare noen forskjeller ved å bruke reelle kode-eksempler fra ZK og GWT
Kort fortalt er den viktige forskjellen mellom et server- eller klient-basert rammeverk hvor applikasjonene kjører. I et server-basert rammeverk ligger applikasjonen på serveren og all prosessering skjer her. Klientens nettleser brukes kun for brukergrensesnittet.
Dette er helt forskjellig fra applikasjoner som prosesseres på klient-siden, som f.eks. i GWT-rammeverket, hvor JavaScript kjører i nettleseren.
Ved første øyekast kan utviklere tenke at disse rammeverk er ganske like - begge gjør det mulig å utvikle i Java, og begge har støtte for Ajax brukergrensesnitt-komponenter. Men graver en litt dypere ned i mekanismene bak rammeverkene så vil en se at ZK og GWT fungerer på to helt forskjellige måter. I hovedsak kompilerer GWT Java-kode til JavaScript slik at applikasjonen kan kjøre i klientens nettleser i stedet for på serveren. GWT applikasjoner snakker med serveren kun når data-overføring er nødvendig. ZK-rammeverket er helt forskjellig fra GWT: applikasjonen kjører på serveren og ZK tar seg kun av brukergrensesnittet.
Forskjellen mellom de to rammeverkene ZK og GWT vil bli demonstrert ved å se på koden som trengs for å lage en enkel Google kartoppslag applikasjon.
Anta at kundens krav er:
La oss anta at du allerede har implementert en Java-klasse "Locator". Den vil ta en tekst som input og returnere den nødvendige informasjon. Det eneste som gjenstår er å lage brukergrensesnittet og data-uthentingen. La oss se på koden fra begge rammeverkene.
Gmap.zul:
<?xml version="1.0" encoding="utf-8"?>
<zk>
<script src="http://maps.google.com/maps?file=api&v=2&key=KEY"
type="text/javascript">
</script>
<zscript>
import com.macroselfian.gps.Locator;
public void locate(){
String location = tb.getValue();
double[] pos = Locator.locate(location);
mymap.panTo(pos[0], pos[1]);
mymap.setZoom(16);
ginfo.setOpen(true);
ginfo.setLat(pos[0]);
ginfo.setLng(pos[1]);
ginfo.setContent(Locator.getInfo(location));
mymap.openInfo(ginfo);
}
</zscript>
<window>
<div align="center">
<separator spacing="50px" />
<vbox>
<label value="Macroselfian Google Map Locator"/>
<hbox width="600px">
<textbox width="550px" id="tb"/>
<button width="50px" onClick="locate();" label="Search"/>
</hbox>
<gmaps id="mymap" zoom="16" ...>
<ginfo id="ginfo"/>
</gmaps>
</vbox>
</div>
</window>
</zk>
Klient-side filer:
Server-side filer:
Kode-eksempler:
Map.java:
…
public class Map implements EntryPoint {
…
public void onModuleLoad() {
…
mapWidget = new GMap2Widget("400", "600");
gmaps = mapWidget.getGmap();
gmaps.addControl(GControl.GMapTypeControl());
gmaps.addControl(GControl.GLargeMapControl());
gmaps.setZoom(16);
VerticalPanel vPanel = new VerticalPanel();
HorizontalPanel hPanel = new HorizontalPanel();
hPanel.add(location);
hPanel.add(search);
vPanel.add(title);
vPanel.add(hPanel);
vPanel.add(mapWidget);
RootPanel.get().add(vPanel);
}
public class ClickListenerImpl implements ClickListener{
public void onClick(Widget widget) {
LocationGeneratorAsync async =
LocationGenerator.Util.getInstance();
async.locate(location.getText(), new LocationCallback());
}
}
public class LocationCallback implements AsyncCallback {
public void onFailure(Throwable error) {
response.setText("Ops..!");
}
public void onSuccess(Object resp) {
LocatorData data = (LocatorData)resp;
double lat = data.getLat();
double lng= data.getLng();
String inf = data.getInfo();
Label info = new Label(inf);
info.setStyleName("map-info");
GLatLng pos = new GLatLng(lat,lng);
gmaps.setCenter(pos);
gmaps.openInfoWindow(pos,info);
}
}
}
LocationGenerator.java:
public interface LocationGenerator extends RemoteService {
public static final String SERVICE_URI = "/locationgenerator";
public static class Util {
public static LocationGeneratorAsync getInstance() {
LocationGeneratorAsync instance =
(LocationGeneratorAsync)GWT.create(LocationGenerator.class);
ServiceDefTarget target = (ServiceDefTarget) instance;
target.setServiceEntryPoint(GWT.getModuleBaseURL()SERVICE_URI);
return instance;
}
}
public LocatorData locate(String location);
}
LocationGeneratorAsync.java:
public interface LocationGeneratorAsync {
public void locate(String location, AsyncCallback callback);
}
LocatorData.java:
public class LocatorData implements IsSerializable {
private double _lng;
private double _lat;
private String _info;
private String _title;
public LocatorData(double lat, double lng, String info,…) {
_lng = lng;
_lat = lat;
_info = info;
_title = title;
}
…
}
LocationGeneraotrImpl.java:
public class LocationGeneratorImpl extends RemoteServiceServlet implements LocationGenerator {
public LocatorData locate(String location) {
com.macroselfian.gps.Locator locator =
new com.macroselfian.gps.Locator(location);
LocatorData data = new
LocatorData(locator.getLat(),locator.getLng(),locator.getInfo(),
locator.getTitle());
return data;
}
}
Ved å bruke ZK rammeverket ligger komponentens tilstand på serveren. En av fordelene med en server-basert applikasjon er at koden for data uthenting og behandling av forretningslogikk er "rett fram". Siden komponentenes tilstand ligger på serveren blir dataprosessering og forespørsler til forretningslogikken gjort direkte uten noen ekstra grensesnitt.
Dette er i motsetning til de RPC - remote procedure call - som kreves når GWT applikasjoner trenger dataoverføring fra serveren. I Map.java, når async.locate(...) kalles idet onClick hendelsen avfyres, så er "callback" objektet LocationCallback nødvendig for å prosessere de returnerte data. Hvis du har litt erfaring med JavaScript programmering så vil du raskt finne ut at dette er ganskje likt det å kalle XMLHttpRequest funksjoner i JavaScript. Og nå har du trolig funnet ut at ved å bruke det server-baserte rammeverket til ZK så trenger ikke utviklerne å skrive noe kode for RPC kall og håndtering av returnerte data.
Her er et eksempel på kode for å oppnå samme funksjonalitet i et annet klient-orientert rammeverk som heter DWR.
Web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
<servlet>
<servlet-name>dwr-invoker</servlet-name>
<servlet-class>
org.directwebremoting.servlet.DwrServlet
</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>true</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>dwr-invoker</servlet-name>
<url-pattern>/dwr/*</url-pattern>
</servlet-mapping>
</web-app>
Map.htm
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<script type="text/javascript" src="dwr/engine.js"></script>
<script type="text/javascript" src="dwr/util.js"></script>
<script type='text/javascript' src='dwr/interface/locator.js'> </script>
<script src="http://maps.google.com/maps?file=api&v=2" type="text/javascript"></script>
<script type="text/javascript" src="http://www.google.com/jsapi?key=ABCDEFG"></script>
</head>
<body>
<script language="javascript" >
var lng, lat, map;
function goto(location){
locator.locate(location, callBackLat);
}
function callBackLat(data){
lat = data[0];
lng = data[1];
load();
}
function load(){
if (GBrowserIsCompatible()) {
var level = 16;
map = new GMap2(document.getElementById("map_canvas"));
map.setCenter(new GLatLng(lat, lng), level);
map.addControl(new GSmallMapControl());
map.addControl(new GMapTypeControl());
var point = new GLatLng(lat,lng);
setGmarker();
setGinfo();
}
}
function setGmarker(){
var point = new GLatLng(lat,lng);
map.addOverlay(new GMarker(point));
}
function setGinfo(){
locator.getInfo(location, callBackGinfo);
}
function callBackGinfo(data){
var point = new GLatLng(lat,lng);
span_element = document.createElement("span");
span_element.setAttribute("style","font-size:11px;font-family:verdana;");
txt_node = document.createTextNode(data);
span_element.appendChild(txt_node);
map.openInfoWindow(point,span_element);
}
</script>
<input id="location" type="textbox" onchange="goto(this.value);" />
<div id="map_canvas" style="width: 500px; height: 300px"></div>
</body>
</html>
Både GWT og DWR er klient-side baserte rammeverk, men DWR er ganske forskjellig fra GWT. Det trengs ikke så mye kode i DWR som i GWT. Derimot, på grunn av fleksibiliteten i DWR, må utviklerne selv ta seg av ulikheter mellom nettlesere. Kort sagt gir DWR bare "broen" mellom klient og server.
Enhver utvikler vil ha møtt på problemet med å vise hoved- og detalj-informasjon. For eksempel hvis brukeren klikker på et navn i en liste - og detaljene bak det valgte navnet skal vises i et detalj-vindu. ZK har en veldig kjekk løsning: data-binding med annotasjon. Vil du ha mer informasjon om data-binding med annotasjon, les "Y-Grid Support Drag-Drop and DataBinding"
For å løse samme oppgave i GWT må utviklerne lage RPC kall, finne hvilken linje som skal oppdateres og så videre. Det er ingen dum ide, men du må gjøre all den kodejobben selv.
ygrid-databinding2.zul
<?init class="org.zkforge.yuiext.zkplus.databind.AnnotateDataBinderInit" ?>
<window xmlns:y="http://www.zkoss.org/2007/yui" width="500px">
<zscript src="Person.zs"/>
<y:grid height="200px" model="@{persons}" selectedItem="@{selected}">
<y:columns>
<y:column label="First Name"/>
<y:column label="Last Name"/>
<y:column label="Full Name"/>
</y:columns>
<y:rows>
<y:row self="@{each=person}">
<y:label value="@{person.firstName}"/>
<y:label value="@{person.lastName}"/>
<y:label value="@{person.fullName}"/>
</y:row>
</y:rows>
</y:grid>
<!-- show the detail of the selected person -->
<textbox value="@{selected.firstName}"/>
<textbox value="@{selected.lastName}"/>
<label value="@{selected.fullName}"/>
<zscript>
//init each person
setupPerson(Person person, int j) {
person.setFirstName("First "+j);
person.setLastName("Last "+j);
}
//prepare the example persons List
int count = 30;
List persons = new ArrayList();
for(int j= 0; j < count; ++j) {
Person personx = new Person();
if(j==0)
selected = personx;
setupPerson(personx, j);
persons.add(personx);
}
</zscript>
</window>
Klient-side filer:
Server-side filer:
Kode-eksempler:
FooExt.java:
package test.gwtExt.client;
import com.google.gwt.core.client.EntryPoint;
…
public class FooExt implements EntryPoint {
…
public void onModuleLoad() {
_response = new Label();
_first = new TextBox();
_first.setName("first");
_first.addChangeListener(new OnTextChangeListenerImpl());
_last = new TextBox();
_last.setName("last");
_last.addChangeListener(new OnTextChangeListenerImpl());
DataSourceGeneratorAsync async =
DataSourceGenerator.Util.getInstance();
async.getData(new DataCallback());
RootPanel.get().add(_first);
RootPanel.get().add(_last);
RootPanel.get().add(_response);
}
public class DataCallback implements AsyncCallback {
public void onFailure(Throwable error) {
…
}
public void onSuccess(Object resp) {
Object[][] data = (Object[][])resp;
createGrid(data);
}
}
public void createGrid(Object[][] data){
MemoryProxy proxy = new MemoryProxy(data);
_recordDef = new RecordDef(
new FieldDef[]{
new StringFieldDef("first"),
new StringFieldDef("last"),
new StringFieldDef("full")
}
);
ArrayReader reader = new ArrayReader(_recordDef);
Store store = new Store(proxy, reader);
store.load();
ColumnModel columnModel = new ColumnModel(new ColumnConfig[]{
new ColumnConfig() {
{
setHeader("First Name");
setWidth(160);
setSortable(true);
setLocked(false);
setDataIndex("first");
}
},
new ColumnConfig() {
{
setHeader("Last Name");
setWidth(100);
setSortable(true);
setDataIndex("last");
}
},
new ColumnConfig(){
{
setHeader("Full Name");
setWidth(260);
setRenderer(new Renderer(){
public String render(…);
_grid = new Grid("grid-example1", "460px", "300px", store, …);
_grid.addGridRowListener(new RowClickListener());
_grid.render();
}
public class RowClickListener implements GridRowListener{
public void onRowClick(Grid grid, int rowIndex, EventObject e) {
FieldDef[] fs = _recordDef.getFields();
Record record = grid.getStore().getAt(rowIndex);
_first.setText(record.getAsString(fs[0].getName()));
_last.setText(record.getAsString(fs[1].getName()));
_selectedRow = rowIndex;
}
…
}
public class OnTextChangeListenerImpl implements ChangeListener {
public void onChange(Widget widget) {
/*
* TODO: Modify data in server side by RPC
*/
TextBox target = (TextBox)widget;
Record r = _grid.getStore().getAt(_selectedRow);
if(target.getName().equals("first")){
r.set("first", target.getText());
}else if(target.getName().equals("last")){
r.set("last",target.getText());
}
}
}
}
DataSourceGenerator.java:
public interface DataSourceGenerator extends RemoteService {
public static final String SERVICE_URI = "/datasourcegenerator";
public static class Util {
public static DataSourceGeneratorAsync getInstance() {
DataSourceGeneratorAsync instance =
(DataSourceGeneratorAsync)GWT.create(DataSourceGenerator.class);
ServiceDefTarget target = (ServiceDefTarget) instance;
target.setServiceEntryPoint(GWT.getModuleBaseURL() + SERVICE_URI);
return instance;
}
}
public String[][] getData();
}
DataSourceGeneratorAsync.java:
public interface DataSourceGeneratorAsync {
public void getData(AsyncCallback callback);
}
DataSourceGeneratorImpl.java:
public class DataSourceGeneratorImpl extends RemoteServiceServlet implements DataSourceGenerator {
public String[][] getData() {
org.zkoss.demo.DataSource ds = new org.zkoss.demo.DataSource();
return ds.getData();
}
}
Ygrid-livedata.zul:
<window xmlns:y="http://www.zkoss.org/2007/yui" title="Y-Grid Live Data" width="200px" border="normal">
<zscript><![CDATA[
String[] data = new String[200];
for(int j=0; j < data.length; ++j) {
data[j] = "option "+j;
}
ListModel strset = new SimpleListModel(data);
]]></zscript>
<y:grid height="200px" model="${strset}">
<y:columns>
<y:column label="options"/>
</y:columns>
</y:grid>
</window>
Her finner du noen artikler om ZK Mobile
Her finner du noen artikler om ZK Android
I prosessen med å samle inn informasjon for denne artikkelen fant jeg at det er tusenvis av artikler som sammenligner Ajax rammeverk. Mange prøver å overbevise leseren om at ett rammeverk er bedre enn et annet og truer Ajax markedet med å bli et Nullsum-spill. (en situasjon der summen av alle deltakeres tap og gevinst er til enhver tid lik null.)
Personlig ser jeg et annet perspektiv. Etter å ha sett på både server-baserte og klient-baserte rammeverk, blir det å sammenligne dem som å sammenligne epler og appelsiner. Begge rammeverk har som utgangspunkt at det skal være enklest mulig for utviklerne å lage Ajax-baserte applikasjoner, men de har helt forskjellige angrepsvinkler.
For meg tilbyr begge typer rammeverk flotte redskapskasser, men det viktige spørsmål er hvilket problem en egenlig forsøker å løse. Finn rett redskap til rett jobb. For tunge, data-baserte applikasjoner og prosjekt som krever en høy grad av sikkerhet så foretrekker jeg et server-basert rammeverk. Hvis applikasjonen krever stilige klient-side funksjoner og mindre server aksess, så kan et klient-basert rammeverk bli mitt valg.