SiNBLOG

140文字に入らないことを、極稀に書くBlog

Closure Templatesを使ってみた!

Google Closure Toolsの1つClosure Templatesを試してみました。
Closure Templates  |  Google Developers

Closure Templates単体でも使えますが、私はClosure Libraryと共に使っています。
Java版とJavaScript版がありますが、私が使ったのはJavaScript版です。

Closure TemplatesはTemplatesのためのファイルを作成し、それをコンパイルすることでjsができあがります。
まずは、Templateのためのファイルです。
book.soy

{namespace templates.book}

/**
 * Book.
 * @param isbn The isbn of the book.
 * @param name The name of the book.
 * @param link The link of the book.
 */
{template .book}
  <div class="book">
    <div class="{css amazon}"><a href="{$link}">amazon</a></div>
    <div class="{css isbn}">{$isbn}</div>
    <div class="{css name}">{$name}</div>
  </div>
{/template}

/**
 * Book List.
 * @param bookList
 */
{template .bookList}
  {foreach $book in $bookList}
    {call .book}
      {param isbn: $book.isbn /}
      {param name: $book.name /}
      {param link: $book.link /}
    {/call}
  {ifempty}
    No book list.
  {/foreach}
{/template}

templateのためのファイルは、soyという拡張子で作成します。
$xxxがtemplate変数となり、値を受け取る事ができるようになっています。
また、上記の例だと2つのtemplateを定義しています。
1つ目が{template .book}です。
中身は殆ど無く、divで囲って値を表示しているだけです。
2つ目は{template .bookList}です。
こっちは、foreachでループするようになっています。
また、中で{template .book}をcallしています。
このように、複数のtemplateを組み合わせることも可能です。


soyファイルが出来たら、SoyToJsSrcCompiler.jarで、Jsへとコンパイルします。
コマンドから実行可能ですが、Eclipseを使っているので、Antを利用してみました。
build.xml

<?xml version="1.0"?>
<project basedir="." default="soy_to_javascript">
	<target name="book">
		<property name="target" value="../index.html" />
		<antcall target="_create_soy" />
	</target>

	<target name="soy_to_javascript">
		<java jar="../../../../lib/SoyToJsSrcCompiler.jar" fork="true">
			<arg value="--outputPathFormat" />
			<arg value="../scripts/templates.js" />
			<arg value="--shouldProvideRequireSoyNamespaces" />
                        <arg value="book.soy" />
		</java>
	</target>
</project>

Closure TemplateがJavaScriptのテンプレートエンジンでいい気がしてきた - Lush Life
こちらを参考に作ったのですが、何故か自動では動いてくれず・・・。
自動で動かせるようになったら、また追記したいと思います。


因みにbook.soyをJsにコンパイルすると、以下のようになります。
templates.js

// This file was automatically generated from book.soy.
// Please don't edit this file by hand.

goog.provide('templates.book');

goog.require('soy');
goog.require('soy.StringBuilder');


templates.book.book = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  output.append('<div class="book"><div class="amazon"><a href="', soy.$$escapeHtml(opt_data.link), '">amazon</a></div><div class="isbn">', soy.$$escapeHtml(opt_data.isbn), '</div><div class="name">', soy.$$escapeHtml(opt_data.name), '</div></div>');
  return opt_sb ? '' : output.toString();
};


templates.book.bookList = function(opt_data, opt_sb) {
  var output = opt_sb || new soy.StringBuilder();
  var bookList26 = opt_data.bookList;
  var bookListLen26 = bookList26.length;
  if (bookListLen26 > 0) {
    for (var bookIndex26 = 0; bookIndex26 < bookListLen26; bookIndex26++) {
      var bookData26 = bookList26[bookIndex26];
      templates.book.book({isbn: bookData26.isbn, name: bookData26.name, link: bookData26.link}, output);
    }
  } else {
    output.append('No book list.');
  }
  return opt_sb ? '' : output.toString();
};

ただのJsなので、デバッグ時にはこの中にconsole.log()とかを埋め込めば、普通に実行されます。


次にtemplateを呼び出している箇所です。
app.js

goog.provide('xhrmanagerSample.App');
goog.require('goog.soy');
goog.require('templates.book');

goog.scope(function(){
    var gdom = goog.dom;
    var gsoy = goog.soy;

    /**
     * ajax get book list compete.
     * @param {Object} e
     */
    xhrmanagerSample.App.prototype.onGetBookListComplete = function(e){
        var bookList = gdom.getElement('book-list');
        gdom.removeChildren(bookList); //domの中身を吹き飛ばしている
        
        var data = {
            bookList: []
        };
        for (var i = 0, len = e['bookList'].length; i < len; i++) {
            data.bookList[i] = {
                isbn: e['bookList'][i]['isbn'],
                name: e['bookList'][i]['name'],
                link: e['bookList'][i]['link']
            };
        }
        gdom.append(bookList, gsoy.renderAsElement(templates.book.bookList, data));
    };
});

猛烈に省略しているのですが、呼び出しているの上記の部分です。
ajaxで取ってきたjsonの値を渡しているのですが、ここで少し困ったことがあります。
Closure Libraryを使っているため、最終的にClosure Compilerでコンパイルを行います。
その時に、templateの中の変数名なども短縮されます。
しかし、ajaxで取ってきたjsonの中の名前は短縮されませんので、一致しません。
そのため、上記のようにJsでリマッピングしてやります。
この辺りは、どうすれば良いのかさっぱり分からなかったので、伊藤殿に聞いてしまいました・・・。
https://plus.google.com/u/0/114061805681880927213/posts/TbB36g3PdNY
伊藤殿、いつもありがとうございますorz


因みにClosure Compilerをかけた後だと、{template .bookList}は以下のようになっていました。

function Ic(a, b) {
    var c = b || new V, d = a.Hb, f = d.length;
Uncaught TypeError: Cannot read property 'length' of undefined
    if (0 < f)
        for (var e = 0; e < f; e++) {
            var h = d[e], l = h.ic, p = h.name, h = h.link;
            (c || new V).append('<div class="book"><div class="', "amazon", '"><a href="', Dc(h), '">amazon</a></div><div class="', "isbn", '">', Dc(l), '</div><div class="', "name", '">', Dc(p), "</div></div>")
        }
    else
        c.append("No book.");
    return b ? "" : c.toString()
};

opt_data.bookListだったのものa.Hbに。
bookList26.lengthだったものが、d.lengthになっています。
ajaxで取得したjsonをそのまま渡していた時は、a.Hbでundefinedになっていまいました。
そのため、上記のようなリマッピングをしてやったのですね。


また、このエントリーでは色々と省略している部分があるので、伊藤殿のBlogも合わせて見ることをお勧めします。
Closure Stylesheets で CSS を最適化する (2) - WebOS Goodies


他にもこんなBlogを書いている方もいらっしゃいました。
Closure Templatesのオートエスケープが最強すぎる件 - teppeis blog

私はClosure Libraryと組み合わせて使っていますが、単体で使っても悪くないかもしれませんね。
jQuery.template()だとjspでは$がかち合って使えませんが、Closure TemplateならばJsにコンパイルされた後は、独自の文字はありませんので。


最後に、今回のサンプルの全ソースです。
book.soy
Google Code Archive - Long-term storage for Google Code Project Hosting.
build.xml
Google Code Archive - Long-term storage for Google Code Project Hosting.
app.js
Google Code Archive - Long-term storage for Google Code Project Hosting.


一応、動くものはこちらです。
ViewBookListボタンを押すと、本の一覧が出てくるだけですが・・・。
ログイン - Google アカウント