[js] Closure Tools用コンパイルツール plovr について

plovrはClosure Toolsに依存するJSとClosureTemplateのSoyファイルのローカルコンパイルデーモン。

普通にClosure LibraryやClosure Templateを使う時は 作成→コンパイル→コンパイル後のソースで動作確認 という手順になるが、
plovrを使うと、作成→コンパイル前後のソースで動作確認→コンパイル という手順に変わる。

作りながらコンパイル後の動作確認ができる。というのが主たる恩恵だが、それだけじゃなく、
必要になった時だけJSファイルを読み込めば良いように、JSファイルをメインとモジュールに分割して作りたい場合にも使えるツールです。

導入までの流れ

  1. plover.jarを入手する(ダウンロード・インストール)
  2. config.jsonを作成する (設定ファイルの作成)
  3. 表示用のhtmlにあるgoog/base.jsとdeps.jsのscriptタグのsrcを
    ploverサーバのURLに置き換える。(リアルタイムコンパイル)
  4. モジュールを作る
  5. ブラウザで確認する
    アクセス先はplovrのサーバーではない ので注意

ダウンロード・インストール

チェックアウト・自前ビルドする場合はsvnではなくhgコマンドなので、
Command Not Foundなら先にMercurialへの対応が必要でござる。
ビルドはファイル一式をzip圧縮して拡張子をjarに変えればおkらしい…

とっととjar欲しいという場合はダウンロードリストから落として
任意のフォルダに入れるだけでインストール終了。

必要環境は Java 1.6. だそうです。

設定ファイルの作成

設定ファイルを作る。JSONにしなければならないが、ファイル名はなんでもいい。

リポジトリにあるサンプルの設定ファイルは実にすっきりした内容になっているが、
設定出来る項目はかなり豊富で、CompilerやTemplateの設定がまとめて出来るようになっている。
設定項目一覧

JSONなのでJSコメントを入れるとinValidになる。
サンプルの設定ファイルを改変するときはコメントを全部除去すること。

最低限必要な設定項目

{
  "id": "integration-test",
  "paths": "testdata",
  "inputs": "testdata/example/main.js",
  "output-charset": "UTF-8"
}
  • id
    srciptタグのsrc属性で使うクエリパラメータidの値として使用するもの。
    plovrは複数の設定ファイルをインスタンス化出来るので、idの値はユニークなものにしなければならない。
  • paths
    {string|Array.}で、コンパイル対象になるソースがあるディレクトリを指定する。
  • inputs
    {string|Array.}で、コンパイルに含めるJSファイルを指定する。

モジュール分割する場合の設定

modulesというキーの値として、モジュール名とモジュールの定義をオブジェクトリテラルで書いておく。

{    
    "modules": {
        "core": {
            "inputs": "src/core/js/base.js",
            "deps": []
        },
        "main": {
            "inputs": "src/main/js/pf/main/init.js",
            "deps": ["core"]
        },
        "mypage": {
            "inputs": "src/mypage/js/pf/mypage/index.js",
            "deps": ["core"]
        }
    },
    "module-output-path": "build/js/%s.js",
    "module-production-uri": "../build/js/%s.js"
}

inputsは上で解説したのと同じコンパイル対象になるファイルへのパス。
depsは依存しているモジュールの設定で、これを空にしたものはルートモジュールになる。
(1つ以上空にしておくとエラーになる)
modulesを使う場合必ず1つルートに設定しておかなければならない。
ルートにしたJSファイルをコンパイルしたものがページロード時に最初に読み込ませるJSファイルになります。

modulesを設定している時のrequireしたクラスの扱い

1つのファイルでしかrequireされていない
→ コンパイルでrequire元のソース(inputsの)とまとめられる

いくつかのファイルでrequireされている
→ コンパイルで依存している(depsに指定された)ソースとまとめられる。

例1

foo.js、bar.js、hoge.jsで sample.Klass をrequireしている。

//config
{    
    "modules": {
        "core": {
            "inputs": "core.js",
            "deps": []
        },
        "foo": {
            "inputs": "foo.js",
            "deps": ["core"]
        },
        "bar": {
            "inputs": "bar.js",
            "deps": ["core"]
        },
        "hoge": {
            "inputs": "hoge.js",
            "deps": ["bar"]
        }

    }
}

コンパイル後
core.js ← sample.Klass
foo.js
bar.js
hoge.js

例2

bar.js、hoge.jsで sample.Klass をrequireしている。

//config
{    
    "modules": {
        "core": {
            "inputs": "core.js",
            "deps": []
        },
        "foo": {
            "inputs": "foo.js",
            "deps": ["core"]
        },
        "bar": {
            "inputs": "bar.js",
            "deps": ["core"]
        },
        "hoge": {
            "inputs": "hoge.js",
            "deps": ["core", "bar"]
        }

    }
}

コンパイル後
core.js
foo.js
bar.js ← sample.Klass
hoge.js

モジュール化ガイド

参考:plovrでJava Scriptのモジュール分割 – プログラム番長

仕組みはclosure libraryにmoduleパッケージとしてもともと用意されている。
plovrのコンフィグでmodules設定を行うと、コンパイル後のソースでmoduleパッケージを利用出来るように分割してくれるという感じなので、
モジュール分割するなら最初からmoduleパッケージのクラスを利用して作っておく必要がある。

コアモジュールに必須の処理

参考:http://code.google.com/p/plovr/source/browse/testdata/modules/app_init.js

plovrのコンフィグファイルに書いたJSONは、コンパイルすると PLOVR_MODULE_\* という変数名でコアモジュールの先頭に出力される。
コアモジュールでは必ずこのplovrが出力した設定をModuleManagerにセットしなければならない。

var moduleManager = goog.module.ModuleManager.getInstance();
var moduleLoader = new goog.module.ModuleLoader();

moduleLoader.setDebugMode(!!goog.global['PLOVR_MODULE_USE_DEBUG_MODE']);
moduleManager.setLoader(moduleLoader);
moduleManager.setAllModuleInfo(goog.global['PLOVR_MODULE_INFO']);
moduleManager.setModuleUris(goog.global['PLOVR_MODULE_URIS']);

サブモジュールに必須の処理

参考:http://code.google.com/p/plovr/source/browse/testdata/modules/api_init.js

コア以外は全てサブモジュールになる。
モジュールマネージャーが読み込み完了を把握するために、サブモジュールでは必ずModuleManagerのsetLoadedメソッドを実行しなければならない。

//hoge.js
goog.provide('sample.Hoge');

/////いろいろな処理/////

goog.module.ModuleManager.getInstance().setLoaded('hoge');

setLoadedに指定するのはモジュールのID。
modules設定のオブジェクトリテラルで使用するモジュール名と同じにしておけば良い。

これを忘れたモジュールは永久に読み込みが終わらない扱いをされる。

サブモジュールを読み込む処理

使えるメソッドはloadexecOnLoadの2つ。

//load
goog.module.ModuleManager.getInstance().load('hoge');

loadは単純にidで指定されたモジュールを読み込むだけだが、
戻り値がgoog.async.Deferredなので、
acyncパッケージを利用した処理にも使える。

//execOnLoad
goog.module.ModuleManager.getInstance().execOnLoad('hoge', function(){});

execOnLoadはモジュール読み込み完了後に実行する関数を指定する事が出来る。
戻り値はgoog.module.ModuleLoadCallbackのインスタンスで、
execOnLoadの引数で渡した関数がセットされている。

ボタンを押したらモジュールを読み込んで実行するサンプル

/**
 * Loads a module.
 * @param {string} id The id of the module
 * @param {Function} func The funciton to execute
 * @param {Object=} opt_obj The "this" object for func.
 */
app.require = function(id, func, opt_obj) {
  var manager = goog.module.ModuleManager.getInstance();
  var info = manager.getModuleInfo(id);
  
  //moduleInfoがない = goog.global['PLOVR_MODULE_INFO'] に値がない = 設定されてない
  if (!info) {
    throw new Error(id + 'は設定されていないモジュールです');
  }
  
  //モジュールファイルの最後の方で
  //goog.module.ModuleManager.getInstance().setLoaded('hoge');
  //が書いてないとどちらも実行されない
  if(info.isLoaded()) {//読み込み済み
    func.call(opt_obj);
  } else {//読み込まれていない
    manager.execOnLoad(id, func, opt_obj);
  }
};

//最初の1文字を大文字にする
var capitaliseFirstLetter = function(string){
  return string.charAt(0).toUpperCase() + string.slice(1);
};

goog.events.listen(goog.dom.getElement('button'), goog.events.EventType.CLICK, function(e) {
  //data-module="hoge" -> hoge.js
  var moduleName = goog.dom.dataset.get(e.target, 'module');
  
  app.require(moduleName, function() {
    //app.Hoge
    var module = new app.[capitaliseFirstLetter(moduleName)]();
    //app.Hoge.prototype.main
    module.main();
  }, this);
});

リアルタイムコンパイル

  1. 表示用htmlを用意する(もしくは、既にあるindex.htmlなどを使う)
    goog/base.jsとdeps.jsのscriptタグがある場合は消しておくこと。
  2. scriptタグのsrcでplovrのサーバーを指定する。(だけでいい)
    <script src="http://localhost:9810/id=ID_FROM_CONFIG_FILE"></script>
    

    idはplovrの設定ファイルで定義したやつ。

  3. コマンド叩いて起動。その際scriptタグのパラメータに書いたのと同じIDを持つ設定ファイルを指定する。
    
    java -jar bin/plovr.jar serve plovr_config.json
    
    
  4. ブラウザで2のhtmlファイルを表示する

modeオプション

//Syntax
http://localhost:9810/compile?id=ID_FROM_CONFIG_FILE&mode=ADVANCED
  • RAW
    base.jsとdeps.jsを読み込んだときと同じような状態。完全なる未圧縮。
  • WHITESPACE
    コンパイルレベルを WHITESPACE_ONLY に設定する
  • SIMPLE
    コンパイルレベルを SIMPLE_OPTIMIZATIONS に設定する
  • ADVANCED
    コンパイルレベルを ADVANCED_OPTIMIZATIONS に設定する

RAWモードを使用する場合は設定に “output-charset”: “UTF-8” が必須になる。

ビルド

コマンドを叩かないと圧縮ファイルを作ってくれない。


java -jar bin/plovr.jar build plovr_config.json

コンパイルしたファイル名の設定

module-output-path がコンパイル後のファイル名で、
module-production-uri はコアモジュールの先頭に追加されるPLOVR_MODULE_URISで、各モジュールのファイルパスを指定するもの。
%sはmodulesで設定してあるモジュール名に置き換わる。

{
  "modules":{ ... 省略 ... },
  "module-output-path": "build/js/%s.js",
  "module-production-uri": "../build/js/%s.js"
}

SoyWeb

SoyWebはsoyファイルをコンパイルしなくてもhtmlファイルで確認できるようにしてくれるplovr内蔵ツール。

起動コマンド


java -jar bin/plovr.jar soyweb --dir src

確認したいsoyファイルが src/hoge/soy/main.soy だとすると、
ブラウザでのアクセス先は htttp://localhost:9811/hoge/soy/main.html になる。

SoyWeb用ダミーデータの設定

パラメーターを使っているテンプレートを確認した時空っぽにならないように、
SoyWeb用ダミーデータ設定がsoyファイル内でできるようになっている。

/**
 * soyテンプレート
 * @param title
 * @param items
 */
{template .list}
<h1>{$title}</h1>
<div>
  {foreach $item in $items}
    <div class="{css task-item}">
      <div style="margin-left: {$item.indent * 24}px">
        {$item.name}
      </div>
    </div>
  {ifempty}
    <div class="{css tasks-complete}">
      Nothing to do!
    </div>
  {/foreach}
</div>
{/template}

/**
 * @param content
 */
{template .demoPage}
<!doctype>
<html>
<head>
  <link rel="stylesheet" href="tasks.css">
</head>
<body>
  {$content|noAutoescape}
</body>
</html>
{/template}
/**
 * SoyWeb用のダミーデータ
 */
{template .soyweb}
{call .demoPage} ←{template .demoPage}を実行
  {param content} ←$param
    {call .list}←{template .list}を実行
      {param title: 'Food Shopping' /}
      {param items: [
        ['indent': 0, 'name': 'cheese' ],
        ['indent': 0, 'name': 'crackers' ],
        ['indent': 0, 'name': 'condiments' ],
        ['indent': 1, 'name': 'ketchup' ],
        ['indent': 1, 'name': 'mayo' ],
      ] /}
    {/call}
  {/param}
{/call}
{/template}
  • {template .soyweb}
    SoyWeb用テンプレート定義
  • {call _.method_}
    テンプレートメソッド実行
  • {param _paramName_:_paramData_}
    パラメーターに渡すデータの定義

URLのクエリで渡した値をテンプレートパラメータで使用する

コマンドオプション –unsafe をつけて起動する。


java -jar bin/plovr.jar soyweb --dir src --unsafe

URLクエリの値 テンプレートに渡される値
?option=true {option: true}
?option=TRUE {option: “TRUE”}
?option1=true&option2=false {option1: true, option2: false}
?option=null {option: null}
?option=foo {option: “foo”}
?option=%22foo%22 {option: “foo”}
?option=true&option=10 {option: 10}

クエリにJavaScriptが含まれている場合は拒否される
(エディタの関係で半角英数以外の文字列を含めています)

http://localhost:9811/settings.html?accessToken=%3Cscript%3Ea lert('gotcha')%3C/script%3E
//=>Refused to execute a JavaScript script. Source code of script found within request.

compile以外のモード

ブラウザで http://localhost:9810/compile?id=ID_FROM_CONFIG_FILE にアクセスすると表示される。

  • Module dependency diagram
    http://localhost:9810/modules?id=ID_FROM_CONFIG_FILE
  • Compilation output for root module
    http://localhost:9810/module/ID_FROM_CONFIG_FILE/CORE_MODULE_NAME
  • List of files in root module
    http://localhost:9810/list?id=ID_FROM_CONFIG_FILE&module=CORE_MODULE_NAME
  • List of tests
    http://localhost:9810/test/ID_FROM_CONFIG_FILE/list
  • Test runner
    http://localhost:9810/test/ID_FROM_CONFIG_FILE/all

List of testsとTest runnerはJsUnitテストを実行するモード。

参考文献