Pages

Monday, November 30, 2009

Closure Templates and Node.JS: Server-Side Soy

Another NodeJS experiment: using Google's Closure Templates with NodeJS.

Closure Templates (aka Soy) can be used either on the server or client-side. The recommended way to use them server-side is with SoyTofu, but there is a way to use them in pure javascript on the server with our new pal, NodeJS.

Suppose we have this blog.soy template to render a simple blog post with some comments:

{namespace blog}

/**
 * Renders a post with comments.
 * @param post
 * @param comments
 */
{template .postPage}
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"
   "http://www.w3.org/TR/html4/strict.dtd">

<html lang="en">
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
 <title>blog</title>
 <meta name="generator" content="TextMate http://macromates.com/">
 <meta name="author" content="Sean McCullough">
 <!-- Date: 2009-11-28 -->
</head>
<body>
{call .post}
  {param post: $post /}
{/call}
{call .comments}
  {param comments: $comments /}
{/call}
</body>
</html>
{/template}

/**
 * Renders a Post.
 * @param post
 */
{template .post}
<h2>{$post.title}</h2>
{$post.body}
{/template}

/**
 * Renders a list of comments.
 * @param comments
 */
{template .comments}
<h3>Comments:</h3>
<ul>
  {foreach $comment in $comments}
 <li>{$comment}</li>
  {/foreach}
</ul>
{/template}

The Closure template compiler will take this input .soy file and create an output .js file that contains functions corresponding to the {template .functionName} sections above.

To compile:
java -jar SoyToJsSrcCompiler.jar --outputPathFormat templates-compiled/blog.js templates/blog.soy


The generated functions in templates-compiled/blog.js look like this:

blog.post = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<h2>', soy.$$escapeHtml(opt_data.post.title), '</h2>', soy.$$escapeHtml(opt_data.post.body));
  if (!opt_sb) return output.toString();
};

Now, if you just try to require() this generated .js file, Node will complain because it doesn't know what soy.StringBuilder() is. We can fix that by shoehorning soyutils.js into node, of course.

First we need to make soyutils.js work with Node's require mechanism. require works in conjunction with process.mixin(), so you make soy require()-able by adding this to the bottom of soyutils.js (copied into your application code directory from the closure templates distribution):

process.mixin(exports, soy); 

Then we need to require soyutils in the blog.js file (you can just paste these into the bottom of the file but it's probably better to implement this as a post-soy-compile step in a build script so you don't have to keep pasting every time you recompile the template)

var soy = require('../soyutils');

process.mixin(exports, blog);

That last process.mixin call will make the blog template functions available to other source files via require.

Now we're ready to use the soy template with our nodejs server code. You'd just require templates-compiled/blog.js and call the functions that it provides from within your event handlers (again, building on the blogging example from a previous post):

var sys = require("sys"), http = require("http"),
  blogTemplates = require("./templates-compiled/blog");

var handlers = {
  '/posts/{postId}' : {
      GET : function(request, response, args) {
        response.sendHeader(200, {"Content-Type": "text/html"});
        var commentsPromise = getCommentsPromise(args.postId);
        var postPromise = getPostPromise(args.postId);
        var templateVars = {};
        commentsPromise.addCallback(function(comments) {
          templateVars.comments = comments;
        });
        postPromise.addCallback(function(post) {
          templateVars.post = post;
        })

        var joinedPromise = join([commentsPromise, postPromise]);
        
        joinedPromise.addCallback(function() {
          var pageHtml = blogTemplates.postPage(templateVars);
          response.sendBody(pageHtml);
          response.finish();
        });
      }
    }
  }
};

This is awfully clunky. I'd like to write a directory watcher that automatically compiles recently updated .soy files, appends the require and process.mixin calls, and reloads the result into Node.

Thoughts on Closure and NodeJS


I spent a little time trying to get the closure compiler to work with Node so that you could for instance, statically verify that the template function invocation parameters match up with the declared parameter types in the .soy file. Haven't gotten enough working there to blog about yet though.

I don't know if the closure compiler optimizations would help NodeJS much, but the static analysis would probably help catch a lot of easy-to-introduce but too-tedious-to-unit-test problems that crop up when you have lots of people working on the same code base.

Also, the Closure Library contains a lot of useful packages that could be applied server-side as well.

1 comment:

  1. glad i found this as i have been working on the same thing.

    I wonder if the main people behind NodeJS are doing this. I doubt that it can be done as a Module as it really needs to be baked into the core more.

    ReplyDelete