Client-Server Code Sharing: putting the "synergy" in Synergy
Update This article likely requires a bit of updating to work the the newest version of Synergy. The general ideas are still as described below.
Developing the client and server sides of a web app in two different languages is a well recognized problem. Many people would like to develop both sides in the same language. This allows developers to think in one language without needing to switch programming paradigm gears constantly (e.g. class-based to prototype/functional-based). Another goal is sharing code between the client and server. Form validation and something like a blog comment preview are just two examples where code sharing would be useful.
Often developers have knowledge in a server-side language and not in JavaScript. But JavaScript is the only practical option for the client-side. One solution is to write the client-side code in some other language and compile it to JavaScript. The idea of debugging buggy generated JavaScript code in buggy Internet Explorer, Firefox, Safari, Opera, etc is not a fine idea at all. Add on top of that the usual fact that generated code tends to be quite a bit bigger and performs slower than hand-written code and a picture of a clever but non-ideal web development environment starts to emerge.
If one language is the goal, then the other option is to develop the server-side of the application in JavaScript. This gives the developer the ability to develop in the code that will need debugging in the browser. The developer can work in a powerful, dynamic language: JavaScript. Of course, JavaScript is the language of choice for web programming because, after all, ECMAScript is "the language of the web".
The remainder of this article looks at how to share code between the client and the server using the Synergy framework. This is one of the big ticket items for Synergy and all you need is one little symbolic link.
Sharing Blog Comment Preview
I'm writing my blog as the guinea pig Synergy application. One thing I don't like about my current third-party blog application is that there is no comment preview. Some of you have commented that you don't like that either. Below is a simplified version of how I'm sharing the comment HTML rendering on the client and the server.
The directory structure of the blog app module is
-
blog/
- bin/
- config/
- db/
-
lib/
- actions/
- bootstrap.js
- config/
- models/
-
views/
- publicLayoutHtml.ejs
- articleHtml.ejs
- commentHtml.ejs
- commentHelper.js
- log/
- test/
- tmp/
-
www/
-
js/
- public.js
-
src/
-
js/
- public.js
- lib@ -> ../../lib
-
js/
-
js/
Note that blog/www/src/lib is a symbolic link to blog/lib and that makes all the code in our application public. This is ok for development but not production. That problem goes away at the bottom of the article.
The rest of this explanation will help if you understand how the Synergy ejs views and build process works and how the blog/www/js and blog/www/src directories relate. Before anyone mentions it, this is a simplified explaination so the code below may not seem production worthy. It is simple to hopefully make it easier to understand the various indirections that make client-server code sharing easy with Synergy.
File: blog/lib/views/publicLayoutHtml.ejs
The important line in this file is the one that script line that sources /js/public.js. This resolves to the file blog/www/js/public.js.
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Peter's blog</title>
<script src="/js/public.js" type="text/javascript"></script>
</head>
<body>
<%= c.content %>
</body>
</html>
File: blog/lib/views/articleHtml.ejs
This file is used only server-side to render and article and its comments. It does, however, use the blog/lib/views/commentHtml.ejs file that will be shared on both the client and server sides.
<h1><%= c.article.title() %></h2>
<%= c.article.body() %>
<%= c.article.comments().map(function(comment) {return commentHtml({comment:comment});}).join('') %>
<div id="preview"></div>
<form action="/comment/create" method="POST" name="commentForm">
<p>Name: <input type="text" name="name" onkeyup="preview()"></p>
<p>Comment: <textarea rows="10" cols="50" onkeyup="preview()"></textarea>
<p><input type="submit" value="save"></p>
</form>
File: blog/lib/views/commentHtml.ejs
This file renders a comment as HTML and is used when rendering an article page on the server-side. This file also needs to be shared with the client for a comment preview. Note this file is an .ejs file and must be compiled before being sent to the browser.
<p><%= sanitize(c.comment.name()) %></p>
<div><%= sanitize(c.comment.body()) %></div>
File: blog/lib/views/commentHelpers.js
This file also needs to be shared with the client as it is used in comment rendering. Note this file is just plain JavaScript with a .js extension.
function sanitize(str) {
return string.replace('<', '<').replace('>', '>');
}
File: blog/www/src/js/public.js
Client-specific code. Admittedly this is rough around the edges and sharing the actual Comment model with the client may better and allow for form validation code sharing. That will come in time but doesn't require any more Synergy framework infrastructure. The infrastructure described in this article is enough.
function preview() {
document.getElementById('preview').innerHTML = commentHtml({
name: function() {return document.forms.commentForm.elements.name.value;},
comment: function() {return document.forms.commentForm.elements.name.value;}
});
}
File: blog/www/js/public.js
I call these types types of files in blog/www/js and blog/www/css "build targets". They specify the files that will be concatenated and minified for production. In development they simply load other files. This allows developing in smaller files and so that browser error line numbers match with source code files. They also make it so no manual compile step in necessary when developing the app.
Note this file includes files in blog/lib by using the symbolic link blog/www/src/lib. This is the one piece of the puzzle that allows for code sharing.
document.write('<script src="/src/js/public.js" type="text/javascript"><\/script>');
document.write('<script src="/src/lib/views/commentHelpers.js" type="text/javascript"><\/script>');
document.write('<script src="/src/lib/views/commentHtml.ejs" type="text/javascript"><\/script>');
How it works
The public layout includes the build target file. The build target file includes the client-side specific code and, through the symbolic link, some of the server-side code. The Synergy web server will serve files though the symbolic link in non-production environments. It will also dynamically compile any linked .ejs files to JavaScript before sending them to the client.
"Foul!" you cry. "Compiling templates down to JavaScript? Didn't you write that is a bad idea near the beginning of this article?" Yes I did but template compilation to JavaScript is not as complex as something like compiling Java application logic to JavaScript. If this slight impurity bothers you then you can simply write all your shared code directly in JavaScript just like the blog/lib/views/commentHelpers.js file has been.
When the blog application is built for production, as I've described, the build targets are actually built and contain the code from the files they include. The blog/www/src directory and its contained symbolic link are deleted and the web server wouldn't follow the symbolic link anyway in production. So any security leak of serving server-side code is gone for two reasons.
So now Synergy has a light weight system for powerful sharing of server-side code with the client. All the benefits of sharing code thanks to a single symbolic link! Pretty neat, eh?
Comments
Have something to write? Comment on this article.
feed