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 => メインにメッセージを投げる => イベントを投げる
という手順でやっているが、冗長な気がするし富豪的なので今後考えていく予定。