This page provides an overview of the Jaxer sample chat application and an introduction to the technology behind it.
Contents |
Introduction
Your Jaxer installation includes a demo of an online multi-room chat application. This demo application includes much of the major functionality that you would find in a typical online chat app:
- Creating a new user account
- Logging in to chat
- Creating a new chat room
- Entering a chat room
- Chatting with other users
- Exiting a chat room
- Logging out of chat
This section briefly explains how the demo chat application works from a user perspective. After learning how a user would use chat, you'll learn about the Jaxer architecture and how to use it to create an application similar to chat.
Viewing the sample application
Your Jaxer installation (whether as a standalone Jaxer Package or in Aptana Studio) includes this simple chat application.
To access the chat application from within Aptana Studio:
- In the Samples View, expand the Aptana Jaxer folder.
- Select the chat example.
- Click either the Preview Sample button
to do a quick preview of the sample, or click the Import Sample button
to import the sample as a project into your workspace.
To access the chat application from a standalone Jaxer Package installation:
- Navigate to your Jaxer installation folder, and double-click the StartServers.bat file.
- In your web browser, navigate to the following URL: http://localhost:8081/aptana/
- From the column on the left, click the Apps and Tools link.
- Click the Chat link.
You can now experiment with creating new users, new chat rooms, and chatting in the demo application (shown below):
Creating a new account
As with many web applications, new chat users need to create new accounts before they can log into and use chat. To create a new user account:
- On the main chat screen, click the Create new account… link to go to the new account screen.
- On the new account screen, type your user information:
- In the New username field, type your username.
- In the New password field, type your password (at least 6 characters).
- In the Confirm password field, re-type your password.
- Click the Create button to create your new account.
The Jaxer chat room creates your new account and automatically logs you in to chat.
What just happened?
When you request the chat application's page, and when you login, a series of events takes place. Some of these events happen during page preparation, some happen locally in your browser, and some involve communicating back to the Jaxer server and — through the Jaxer server framework — to the database and the file system.
Jaxer page lifecycle
The following diagram shows a simple overview of the Jaxer page processing chain:
Lifecycle events
The following list describes what happens when Jaxer processes a web page:
- The client (browser) sends a request to Apache, which either handles the request itself or delegates it to a handler such as PHP, Ruby, Java, etc.
- Apache either retrieves the static HTML document from disk, or receives a dynamic HTML document from the handler.
- Jaxer acts as an output (post-process) filter that receives the document from Apache, and starts to parse and execute it, just as a browser would, but on the server side. The DOM is created, JavaScript code designated to run on the server is executed, and so on until the entire document is consumed.
- The result is a DOM modified by the Jaxer Framework and by the developer: in particular, server-side client-callable functions are automatically replaced by proxies. Some important side-effects include storing designated JavaScript functions as callbacks and persisting session-type data.
- The new DOM is serialized (without any server-side JavaScript) as a new HTML document and streamed out to the client as usual.
- The client receives the HTML document and the processing continues, recreating the DOM from the provided HTML and this time executing the client-side JavaScript remaining on the page as normal.
The client-side JavaScript served to the browser may contain proxy function stubs, that correspond to (and have the same name as) certain server-side functions tagged by the developer to be callable from the client. By using the proxy architecture, Jaxer is able to provide powerful server-side capabilities accessible from the client in an entirely native and seamless manner.
The next section examines these events in more detail.
Initial page load
The initial page load processing for Jaxer is similar to other web architectures, in that the request to the browser is passed though both a web server (in this case Apache) and an Application server layer (in this case the Jaxer apache handler/filter) before being presented to the client browser.
Processing details
Preparing the database schema
When the Jaxer server receives the page to process, several behind the scenes actions take place, among them:
- Jaxer performs any server side activity, such as database access.
- The page is automatically instrumented with server callback capabilities by binding certain client functionality to server-side scripts.
The enabling mechanism for controlling the server callback functionality is the runat attribute, an additional attribute that Jaxer recognizes on in-line or external script tags.
Even without the presence of runat attributes on your script tags, Jaxer will still take certain actions on you code, such as persisting session-type data and inserting the Aptana namespace object into the DOM.
Examine the code of the first page of HTML sent, index.html, to see what it does:
The first script tag loads the file setup.js.
[html,N]
<script type="text/javascript" src="jaxer-include/setup.js" runat="server"></script>
The runat attribute provided to the script tag tells the Jaxer server that the contents of this script should execute server side and not be presented to the client browser. This means that if you view the source on the index page after it loads in your browser, you will not see any of the contents of the setup.js file (shown below).
[javascript,1]
function() {
if (Jaxer.application.get("isInitialized")) return;
var sql;
sql = "CREATE TABLE IF NOT EXISTS users " + "( id INTEGER PRIMARY KEY AUTO_INCREMENT" + ", username VARCHAR(255) NOT NULL" + ", password VARCHAR(255) NOT NULL" + ", created DATETIME NOT NULL" + ", last_login DATETIME NOT NULL" + ", UNIQUE (username)" + ")";
Jaxer.DB.execute(sql);
sql = "CREATE TABLE IF NOT EXISTS messages " + "( id INTEGER PRIMARY KEY AUTO_INCREMENT" + ", room_id INTEGER NOT NULL" + ", sent DATETIME NOT NULL" + ", user_id INTEGER NOT NULL" + ", contents TEXT NOT NULL" + ")";
Jaxer.DB.execute(sql);
Jaxer.application.set("isInitialized", true);
})();
The script file setup.js checks the application level container object and looks for a stored variable called isInitialized. If it already exists, the application has already been run and can return control to the original page index.html. However, if the variable isInitialized is not found, this is the first time the application has been run, and some SQL DDL is executed to bootstrap the database, which creates the required schema to run the application. After the SQL executes and the database is prepared, the isInitialized flag is set to true, and the processing returns to the index page.
You now have a schema ready for use by the application. Note that by using the Jaxer framework modules, you can accomplish all of this using only JavaScript.
Adding server- and client-side functionality
The next script tag that you encounter uses the runat='both' value to tell the server that the code is to be run on both the browser and the server. The server-side functions will also be cached in association with this page so that they can be used by any server-side callback functions.
[html,1]
<script type="text/javascript" runat="both">
function $(id) { return document.getElementById(id); } function fromTemplate(idTarget, idSource) { $(idTarget).innerHTML = $(idSource).innerHTML.replace(/(\W)\$id\=/g, "$1id="); }
function showRooms(rooms) { if (rooms == null) { $("rooms").innerHTML = "(Not logged in)"; } else { var htmlStrings = []; for (var i=0; i<rooms.length; i++) { var room = rooms[i]; var html = $("roomTemplate").innerHTML.replace(/\$(\w+)\$/g, function(match, keyword) { return room[keyword]; });; htmlStrings.push(html); } $("rooms").innerHTML = "\n" + htmlStrings.join("\n") + "\n"; } }
</script>
The functions in this script block will be needed on both the server and the client in order to populate the dynamic section of the DOM server-side for the initial page presentation, and then to update the DOM client-side as needed in response to user actions. Note that the same DOM manipulation code can be used on both client and server sides, so the DOM is consistent and the code is shorter and more maintainable.
The next script we encounter is only needed server-side:
[html,N]
<script type="text/javascript" src="jaxer-include/_authentication.js" runat="server"></script>
The script _authentication.js contains some functions for use both now during page preparation and later during callbacks. These functions will be cached automatically by using runat='server'. (If you know you won't need them during callbacks, and you'd like to optimize performance and memory a bit, you can use runat='server-nocache' instead.)
[javascript]
function getAuthenticatedUser()
{
var username = Jaxer.session.get("username");
if (typeof username == "undefined") return null;
var rs = Jaxer.DB.execute("SELECT * FROM users WHERE username = ?", [username]);
if (rs.rows.length == 0)
{
return null;
}
return rs.rows[0];
}
function makeAuthenticated(user) { Jaxer.session.set("username", user.username); }
function initAuthentication() { try { var user = getAuthenticatedUser(); if (user) { fromTemplate('loginComponent', 'loginAuthenticated'); $('authenticatedUsername').innerHTML = user.username; Jaxer.clientData.isAuthenticated = true; } else { fromTemplate('loginComponent', 'loginRequest'); Jaxer.clientData.isAuthenticated = false; } } catch (e) { Jaxer.Log.error("Error trying to get authenticated user: " + e); } }
This code was encapsulated in an external .js file since it will be used on multiple pages where authentication is needed, guaranteeing that the same logic will be used consistently. Note that it only runs on the server, and in fact cannot be called from client-side JavaScript functions, for security reasons. Next we'll see an example of code that should be called from the client.
Specifying where your code runs
This really gets to the power of the Jaxer architecture: by being able to specify whether the code runs on the server or the client or both, and allowing the server-side functions to be seamlessly called from the client, we can build highly consistent code that crosses the client/server boundary with little effort, thus avoiding the requirement in other web stacks to use templating constructs, emit JavaScript from a different language, create handler pages for your server-side callbacks, etc. In Jaxer it is all DOM and JavaScript.
The next script is an in-line script with a runat attribute set to execute on the server.
[html]
<script type="text/javascript" runat="server">
function getRooms() { var user = getAuthenticatedUser(); if (user == null) return null; var rs = Jaxer.DB.execute("SELECT MIN(sent) AS started, MAX(sent) AS last, room_id FROM messages GROUP BY room_id"); // Fix up dates because SQLite does not (reliably?) return the declared column type on aggregates for (var i=0, len = rs.rows.length; i<len; i++) { var row = rs.rows[i]; if (typeof row.started == "number") row.started = new Date(row.started); if (typeof row.last == "number") row.last = new Date(row.last); } return rs.rows; } getRooms.proxy = true;
function prepareDOM() { var rooms = getRooms(); showRooms(rooms); }
</script> One of the contained functions getRooms() adds a Jaxer-specific property on the function, getRooms.proxy = true; so it can be called from the browser. We could have used an alternate syntax: getRooms.runat = "server-proxy";.
The prepareDom() function is invoked later on the page by being bound to the special Jaxer event onserverload. This event is fired when the server has completed loading of the page and has a DOM available for manipulation by JavaScript code.
The onserverload event is the server-side equivalent of the traditional client side onload.
[html,N]
<body onserverload="initAuthentication(); prepareDOM()">
At this point the authentication is initialized, and the prepareDom() function is called. This will insert rows displaying any existing chat rooms into the DOM contents of the home page before sending the page to the client. It does this by querying the database table messages (lines 7-9) and returning a distinct set of rooms as well as information on when each room was opened and when the last message was posted. This content also contains a hyperlink to enter the chatroom (shown below).
Now let's turn to the callback mechanism used by the Jaxer chat app.
Callbacks made easy
When you enter the application for the first time, no room will be listed and you will have to create a new room.
However, before you can create any new rooms, you first have to login to chat. If this is your first chat visit, you will need to create a new account before you can login.
The next two sections discuss what happens on both the client and the server when you login to chat (assuming that you already have an account created). Here is a detailed diagram of the process flow:
Image:JaxerProcess-Callback.png
On the client
The chat login process make extensive use of callbacks from the client to the server to provide the services required to authenticate users. The following code demonstrates some of the unique capabilites of the Jaxer server with respect to remote function invocation or callbacks.
The login function (located in _login.js) invokes a local (client-side) function checkCredentials(username, password) which is the proxy for the previously-shown server-side checkCredentials function.
[javascript,1,(5)]function login()
{
var username = $('username').value;
var password = $('password').value;
var username = checkCredentials(username, password);
if (username != "")
{
fromTemplate('loginComponent', 'loginAuthenticated');
setTimeout("$('authenticatedUsername').innerHTML = '" + username + "'", 0);
changeAuthentication(true);
}
else
{
$('loginMessage').innerHTML = "Sorry, try again";
}
}
login.runat = "client";
The Jaxer-inserted proxy function for checkCredentials utilizes the methods available from the Framework namespace Jaxer.Callback. The important point to note here is that the call signature for checkCredentials will be exactly the same as the server function shown later in the example.
[javascript]function checkCredentials()
{
return Jaxer.Callback.invokeFunction.call(null, "checkCredentials", arguments);
}
On the server
The checkCredentials function can only truly execute on the server, but since it's marked to be proxied, the client will also get a function called checkCredentials (shown above) which simply provides a remote wrapper for the server code. Internally it does this by serializing its arguments to a JSON format and passing them to the server, where they are deserialized and the server-side function is invoked. When the server-side function completes, its result is serialized to JSON and then passed back to the client, where it's deserialized and returned as the result of the proxy as if the proxy function had carried out the processing.
[javascript]function checkCredentials(username, password)
{
var rs = Jaxer.DB.execute("SELECT * FROM users WHERE username = ? AND password = ?",
[username, password]);
if (rs.rows.length == 0)
{
return "";
}
var user = rs.rows[0];
makeAuthenticated(user);
return user.username;
}
checkCredentials.proxy = true;
One point worth noting is that this is not asynchronous, and the client will wait for the response. For every server-side function to be proxied Jaxer emits two proxies into the client-bound page: one for synchronous remote invocation and the other for asynchronous remote invocation. The latter is named the same as the former, but with Async appended to its name, for example checkCredentials and checkCredentialsAsync.
This callback architecture is the key to providing the seamless client server communications without any of the complexity of the more traditional XHR techniques nomally associated with Ajax functionality. With Jaxer it is all just Javascript and the boundary between client and server can be smooth and simple.
Inside the Jaxer chat application
You should now be familiar with the functionality of the Jaxer chat application. The next section discusses some of the key technical differences between how a typical web application is constructed and the way the demo chat application uses the unique capabilities of the Jaxer Server.
Architectural distinction
The main architectural difference of the Jaxer server from other page composition engines, such as PHP, JSP and ASP, is that both the client side and server side use essentially the same platform. This means that you can use familiar DOM manipulation techniques with standard JavaScript tools and libraries on both the client and server side. As a developer, you benefit from this approach by no longer having to deal with complex paradigms, such as emitting JavaScript from Java. Using Jaxer, you can even invoke server-side functionality directly from the client in a completely seamless manner.
The Jaxer server wraps typical server-side functionality within a tightly controlled namespace (the Jaxer object) to prevent variable and scope conflict with other Javascript libraries and tools.
Side-by-side comparison
The following diagram illustrates how Jaxer can replace the common server technologies of most dynamic web applications.
With the Jaxer server, you can perform all the usual activities associated with web applications, such as database access, session management, error logging and file system access.
In the next few sections of this document, you will learn about some of the unique features of the Jaxer Server and examine at how they were implemented for the Jaxer chat application.
Jaxer runat and proxy reference
One of the initial differences you will notice between the Jaxer source and typical web applications is the addition of the runat attributes to the script tags in the HTML markup. These attributes tell the Jaxer server how to manage the script code. Because the server can run JavaScript on the server-side, client-side, or both, and can inject proxies into the client, this meta information instructs the server how to handle the scripts appropriately.
Basic runat values
The following table describes the standard runat attributes that are applied to the <script> tag.
value | description |
---|---|
client |
The functions and code contained in the script block will run in the client browser only. This functions exactly as a regular script block. This is the default value of the runat attribute, so usually you'll omit it for script blocks intended for the client. Its main use is to override the runat attribute of a specific function within a server-side script block. Note: if a script block has runat = "client" (or no runat attribute), it will not run at all server-side, so you cannot override the runat behaviors of individual functions from within this block. |
server |
The functions and code contained in the script will run on the server only. Any functions defined within the script block will be cached in association with this page. These functions are not directly callable from the client, but they can be called during callback processing by other server-side functions. These script blocks will not be presented to the client browser. |
both |
The functions and code contained in the script will run on both the client and the server. Any functions defined within the script block will be cached in association with this page. The server-side functions are not directly callable from the client, but they can be called during callback processing by other server-side functions. |
Example The following code snippet is an excerpt from the index.html page in the Jaxer chat application folder. This example shows an external script being included on the page for server-side-only execution.
[html,N]<script type="text/javascript" src="jaxer-include/_login.js" runat="server"></script>
Advanced runat values
The following runat values can also be used on the script tags. We expect most use cases would be covered by the basic attributes shown above.
value | description |
---|---|
server-proxy | Same as the basic 'server' target except ALL the functions will be proxied by default |
server-nocache | Same as the basic 'server' target except NONE of the functions will be cached by default |
both-proxy | Same as the basic 'both' target except ALL the functions will be proxied by default |
both-nocache | Same as the basic 'both' target except NONE of the functions will be cached by default |
Programmatic runat configuration
Jaxer is aware of some special function object properties that can be declared on individual function objects to control how they are managed. When these are specified, the property value will override the containing script block's runat setting for the individual function. This allows more granular control and prevents the need to break scripts out into separate blocks or files depending on their desired runat value.
property | description |
---|---|
proxy |
Server-side functions can be declared to be proxied so they are callable from the client side. This is achieved by specifying a proxy property on the function object. The possible values for this property are true or false. This is only required for enabling the proxying of the function. By default, in a <script runat="server"> block, the functions are not proxied. Note that if a function is not proxied, it isn't just that proxies are not inserted into the client to facilitate calling it: it's actually marked as not callable on the server, so hacking the client to try to call the function on the server will not work. |
runat |
Takes the same values as the <script> tag runat attributes. |
Example
The _login.js file referenced in the example above contains some functions that explicitly override the runat='server' directive specified on the script tag used to load the file.
In this snippet the function will proxied:
[javascript,1,(13)]
function checkCredentials(username, password)
{
var rs = Jaxer.DB.execute("SELECT * FROM users WHERE username = ? AND password = ?",
[username, password]);
if (rs.rows.length == 0)
{
return "";
}
var user = rs.rows[0];
makeAuthenticated(user);
return user.username;
}
checkCredentials.proxy = true;
In this snippet the function will run client side:
[javascript,1,(17)]
function login()
{
var username = $('username').value;
var password = $('password').value;
var username = checkCredentials(username, password);
if (username != "")
{
fromTemplate('loginComponent', 'loginAuthenticated');
setTimeout("$('authenticatedUsername').innerHTML = '" + username + "'", 0);
changeAuthentication(true);
}
else
{
$('loginMessage').innerHTML = "Sorry, try again";
}
}
login.runat = "client";
Recommended style
The following illustrates one simple way of using the runat and proxy options in a typical code scenario. We choose to group all the server-side code in one script block, and explicitly designate a subset of function to be proxied. Then all client-side code goes in a different script block (where there isn't even the option of programatically changing it by setting a different runat or proxy value). Of course you may choose a different way of organizing your code if that makes more sense. And for large amounts of code, it may also make sense to extract the code into (reusable) external JavaScript files.
[html, 1, (1,15,19)]
<script type="text/javascript" runat="server">
function setPassword(username, newPassword) { // put code in here to directly set the password of a given username // this code should not be callable from the client }
function changePassword(username, oldPassword, newPassword) { // put code in here to first verify the submitted password, // and then -- if successful -- call setPassword to actually make changes // this code should be callable from the client } changePassword.proxy = true;
</script>
<script type="text/javascript">
function submitPassword() { // put client-side code here to grab the username and old and new passwords // and call changePassword on the server }
</script>
Alternate Syntax
Jaxer provides a useful convenience array inside the Jaxer namespace to allow the proxy functions to be declared in a single group within your Javascript code:
[javascript,N]
Jaxer.proxies = [myFunc1, myFunc2, "myFunction"]; // ... Jaxer.proxies.push(myFunc3, "myFunction4");
The value of Jaxer.proxies at the end of server-side page processing (i.e. when proxies are about to be inserted) determines which server-side functions will have their proxy property be set to true. You can also use this to remove all proxied functions by setting the value to null.
Note: Jaxer.proxies is NOT a complete collection of the functions being proxied by the server, it is just a convenient way to express the myFunc.proxy = true; syntax for multiple function references.
<jaxer:include> reference
Jaxer provides several special <jaxer:include> tags for implementing server-side include statements.
Why use server-side includes?
If you are managing a web site that has more than a few pages, you may find that making changes to all those pages can be tedious, particularly if you are trying to maintain a standard functionality or look across all of your pages.
Using an include file for a header and/or a footer can reduce the burden of these updates. You just have to make one footer file, and then include it into each page with the include SSI command. The <jaxer:include> element can determine what file to include by the specified src attribute. The value of the src attribute should be a URL relative to the document being served, or an absolute URL starting with a / representing the web server's document root. Alternatively, you can use a path attribute instead of the src attribute; its value is an absolute path in the filesystem. In any case, the included file must be on the same server as the page being served.
Jaxer also provides a <jaxer:includeJS> element that takes a src or path attribute as well as a runat attribute, and acts much like a script element with a src attribute. It acts like the <jaxer:include> element but wraps the contents of the included file in a JavaScript script element.
Finally, Jaxer also provides Jaxer.load(src) to programatically load and evaluate (server-side) a JavaScript file.
Examples
The following examples show how to include a file from the server's local file system as part of the current document.
[html,N]<jaxer:include src="_login.html"></aptana:include>
[html,N]<jaxer:includeJS src="setup.js" runat="server"></aptana:includeJS>
[javascript,N]Jaxer.load("setup.js")
This functionality is similar to the php include directive or the Apache #include functionality.