ログ日記

作業ログと日記とメモ

Google Closure Tools を使った開発

Closure Tools の導入記事はいくつかあるけれど、じゃー実際にアプリを作りましょうってなると迷うことが多いので、それに関するメモなどを。
Closure Tools の導入に関しては http://webos-goodies.jp/categories/closure-tools.html ここを参照。

コンパイルコンパイル済みJavaScriptの読み込みを切り替える

開発用js読み込みとコンパイル用js読み込みをサーバーサイドで制御する。
HTMLは

<!DOCTYPE html>
{if:jscompiled}
<html manifest="cache.appcache">
{else:}
<html>
{end:}
  <head>
    <meta charset="UTF-8">
    <title>My App</title>
    <flexy:include src="header.html"/>
  </head>
  <body>
    <div id="entryPoint">
    </div>

    {if:jscompiled}
    <flexy:include src="jscompiled.html"/>
    {else:}
    <flexy:include src="js.html"/>
    {end:}

  </body>
</html>

このようにし、jscompiled のフラグを見て読み込むファイルを切り替える。

<script type="text/javascript" src="js/compiled.js"></script>
<script type="text/javascript" src="js/renaming_map.js"></script>
<script type="text/javascript" src="closure-library/closure/goog/base.js"></script>
<script>goog.LOCALE='ja';</script>
<script type="text/javascript" src="js/deps.js"></script>
<script type="text/javascript">
  goog.require('myapp');
</script>

depswriter.py を実行するシェルスクリプトと closurebuilder.py を実行するシェルスクリプトを用意し

echo "jscompiled = 1" > ../myapp/config.ini

のようにして設定ファイルを生成して、HTMLを出力するテンプレートエンジンにフラグを設定する。


オフラインアプリの場合は、cache.appcacheの雛形をhtdocsの外に用意しておいて、コンパイル時には htdocs の中にコピーする。


今使っている開発用スクリプトはこんな感じ。

#!/bin/bash

./version.sh
rm -f htdocs/cache.appcache
INI=app/js.ini

if [ "$1" == "" ];then
  DIR=htdocs/closure-library
else
  DIR="$1"
fi

echo "[property]" > $INI
echo "jscompiled = 0" >> $INI

. cssfiles.sh
java -jar ../closure-stylesheets-20111230.jar \
  --output-file htdocs/css/style.css \
  --output-renaming-map htdocs/js/renaming_map.js \
  --output-renaming-map-format CLOSURE_UNCOMPILED \
  --rename DEBUG $CSS gss/*.css htdocs/js/cue/css/*.css

$DIR/closure/bin/build/depswriter.py \
  --root_with_prefix="htdocs/js ../../../js" \
  --output_file=htdocs/js/deps.js

gss ディレクトリには、アプリのスタイルシートファイルを入れる。その他外部ライブラリのcssファイルが別階層にあるなら、htdocs/js/cue/css/*.css のように指定する。


コンパイル

#!/bin/bash

./version.sh

cp -f cache.appcache htdocs/
DATETIME=`date '+%Y-%m-%d %T'`
sed -i "s/%datetime%/$DATETIME/" htdocs/cache.appcache

INI=app/js.ini

if [ "$1" == "" ];then
  DIR=htdocs/closure-library
else
  DIR="$1"
fi

echo "[property]" > $INI
echo "jscompiled = 1" >> $INI

. cssfiles.sh
java -jar ../closure-stylesheets-20111230.jar \
  --output-file htdocs/css/style.css \
  --output-renaming-map htdocs/js/renaming_map.js \
  --output-renaming-map-format CLOSURE_COMPILED \
  --rename CLOSURE $CSS gss/*.css htdocs/js/cue/css/*.css

LEVEL="--compilation_level=ADVANCED_OPTIMIZATIONS"
DEBUG="--define=goog.DEBUG=true"

$DIR/closure/bin/build/closurebuilder.py \
  --root=$DIR/ --root=htdocs/js \
  -n myapp \
  --output_mode=compiled  \
  --compiler_jar=../compiler.jar \
  --compiler_flags="$LEVEL" \
  -f "--define=goog.LOCALE='ja'" \
  -f "$DEBUG" \
  -f "--js=htdocs/js/renaming_map.js" \
  > htdocs/js/compiled.js
$DIR/closure/bin/build/closurebuilder.py \
  --root=$DIR/ --root=htdocs/js \
  -n myapp.worker \
  --output_mode=compiled  \
  --compiler_jar=../compiler.jar \
  --compiler_flags="$LEVEL" \
  -f "--define=goog.LOCALE='ja'" \
  -f "$DEBUG" \
  > htdocs/js/wkcompiled.js

通化が中途半端な箇所が多々あるが…とりあえずそのまま載せた。
wkcompiled.js はWeb Workers用。
ちなみに myapp.worker は

if (!self.COMPILED){
CLOSURE_BASE_PATH = '../closure-library/closure/goog/';

importScripts(
  CLOSURE_BASE_PATH + 'bootstrap/webworkers.js',
  CLOSURE_BASE_PATH + 'base.js',
  'deps.js');
}
goog.provide('myapp.worker');

goog.require('myapp.worker.Command');
goog.require('goog.events');

myapp.worker.message = function(e){
  try {
    var ret = myapp.worker.Command.getInstance().dispatch(e['data']);
    if (ret)
      postMessage(ret);
  }catch (err){
    postMessage(err.stack);
  }
}
goog.events.listen(self, 'message', function(e){
  myapp.worker.message(e.getBrowserEvent());
});

このようなコードを書いている。
myapp.worker.Command は普通にクラスを書けば良い。Worker生成側も

var workerFile;
if (COMPILED)
  workerFile = 'js/wkcompiled.js';
else
  workerFile = 'js/worker.js';
var worker = new Worker(workerFile);

COMPILEDで切り分けている。

画面切り替えを考える

ページの一部で Closure Library を使う場合は特に何も考えずにWeb上のサンプルなどをそのまま使えるが、画面遷移なしのフルJavaScriptページを作るにはフロントコントローラー的なものを考えないといけない。
GWTに習って EntryPoint とhistory方式でページを切り替える。

goog.provide('myapp');

goog.require('myapp.page.Controller');
...

/**
 * @constructor
 */
myapp.EntryPoint = function(){
  goog.base(this);

  ...
  // 初期化処理など
  // 最後にコントローラーの呼び出し
};
goog.inherits(myapp.EntryPoint, goog.ui.Component);
goog.addSingletonGetter(myapp.EntryPoint);
myapp.EntryPoint.getInstance();

EntryPointクラスを作って、インスタンス化までやってしまう。これでファイルを読み込むと同時に処理が開始される。
Controllerからは、enterPageとexitPageメソッドを持つクラスを継承してページクラス群を作り、Historyのtokenに応じて呼び出すようにする。

クラス設計方針を考える

普段はPHPを使ってえせMVCでやっているので、いざ本格的なMVCをやろうとすると分からないことが多い。
https://sites.google.com/site/closurelibwiki/workflow/mvc ここが参考になる。


基本的な選択肢として

  • ビューを分けるかどうか
  • コンポーネントを積み重ねていく場合に、goog.ui.Component に addChild していくか、goog.dom.createDom したものに render していくか。
  • decorate 対応するかどうか

などがある。
フルJavaScriptならHTMLを書かないので decorate は使わないとして、他の二つが悩みどころ。


手探り状態でやっているが、現状は使い捨てクラスは Component + addChild(child, true) で構築し、使いまわすクラスはきちんとDOMを生成して getElement や getContentElement を使って構築するようにしている。



ようやく処理の流れが見えてきたので基本メソッドをメモ。

render
レンダリングメソッド。createDomしてからenterDocumentを呼び出す。overrideする必要は特にない。
enterDocument
レンダリングされた後に呼び出される。イベントハンドラを定義する。※1
exitDocument
レンダリング解除された後に呼び出される。overrideする必要は特にない。
getHandler
コンポーネントイベントハンドラを取得する。独自にnew EventHandler(this)した場合と違ってexitDocument時に自動的に破棄してくれる。
createDom
DOMを生成する。setElementInternalで生成したDOMを登録する必要がある。※2
disposeInternal
オブジェクトが破棄されるときに呼び出される。メモリリーク対策でメンバ変数をnull(またはdelete?)する処理を書く。


※1 良いやり方かどうかは分からないが、アプリ固有のクラスではコンストラクタで何もせずに enterDocumentの中でで初期化処理をする方向でやっている。 addChild(component, true) を使ってコンポーネントを組み上げていく場合、レンダリング済みでないと出来ないので。
※2 使い捨てクラス&divタグで良いなら、createDomはoverrideせずに、enterDocumentの中でgetElementしてごにょごにょしても良さそう。

オフライン同期方式を考える

そろそろ未知の領域すぎて分からないことだらけ。
http://d.hatena.ne.jp/unigo/20100722/1279810052


今は

  • メインスレッドでindexeddbにdirtyフラグを付けて保存 => workerにメッセージを投げる => workerがdirtyフラグが付いているオブジェクトを取得 => xhr => メインにメッセージを投げる => イベントを投げる

という手順でやっているが、冗長な気がするし富豪的なので今後考えていく予定。