WebDriverJs 〜果たされた約束〜
相変わらずSEO無視気味のうざいブログタイトルシリーズです。
WebDriverJs というのがあって、SeleniumのJavaScriptドライバです。npm で簡単に入れられます。
$ npm install selenium-webdriver
https://code.google.com/p/selenium/wiki/WebDriverJs
WebDriverJsは私の中で話題のPromiseを全面的にサポートしています。
それで、SeleniumはPromiseと非常に相性がいいと思うんです。
Seleniumはブラウザ自体の操作や状態を知るのは得意ですが、スクリプトで変化したページの状態を知るのは苦手な部分があります。例えばこれは得意です:ページ遷移後、ページが表示されたタイミングに、ページタイトルを確認してテストする。でも、私たちが実際にテストしたいのは、もっとこういうものです:ページ内のボタンをクリックしたら、XHR通信が発生し、サーバの健康状態にもよるが、2秒後ぐらいにレスポンスが返り、ページ内の複数のDOMが書き換えられるので、その一連の動作をテストしたい。
XHR後でかつレンダリングが終わったタイミングというのは、確かに知りにくいです。XHR通信の到着直後にテストをしてもいけない。レンダリングが終わった直後にテストをしてもいけない。他でもなく、テストしたい場所がテストできる状態になってからテストしたいのです。これは、ブラウザAPIでは検出しにくい情報です。もっと緩やかな(人間の視点に近い)条件でステップを進行できないものかな? と私はかれこれ中学生くらいの時からずっと考えていました。嘘です。
それで、方法がありました。Promiseを用いたSeleniumテストです。PromiseといえばJavaScriptです。JavaScriptは私が唯一書ける言語です。ラッキーです。
Promiseは、非同期テストに最適です。Promiseは処理の塊をステップとして扱い、それらを勝手に順序よく実行してくれます。ステップを外に出し、複数のテストでステップを共有することもできます。ステップをネストさせれば(たぶんWebDriverJsでもできると思うんだが)、よくある一連の操作を1つにまとめて使うこともできます。テストがスケールするかどうかは、それが増えて来た時に必ず重要になるでしょう。Promiseなら、理解し易い形で処理を増やして行くことができるでしょう。
例えば、こういう変なページがあったとしましょう。今回これをテストします。
タイトルは無視してください。作り直すのが面倒です。この画面をしばらく眺め、pushボタンをクリックしたくなったら(貴方はきっとなる)クリックします。すると2秒後に・・・
successと表示されます。わざと2秒もかかるようにしてみました。サーバのコードはこちらです。
// Node server app.get('/timeout', function(req, res) { setTimeout(function() { res.end(JSON.stringify({ success: 1 })); }, 2000); });
1. クリックする、2. success と表示される。それでは、この動作を実際にテストをしてみます。mochaしか使えませんのでmochaを使います。CoffeeScriptが書けますのでCoffeeScriptで書きます。
# CoffeeScript assert = require("assert") webdriver = require("selenium-webdriver") buildDriver = -> d = new webdriver.Builder().usingServer("http://localhost:4444/wd/hub").withCapabilities(browserName: "firefox").build() d.manage().window().setSize 350, 200 d describe "strange page", -> it "should display 'success' on click", (done) -> # DOMセレクタたち。 BUTTON = webdriver.By.id("b") BOX = webdriver.By.id("box") # :3000にアクセスする。 d = buildDriver() d.get "http://localhost:3000/" # クリックして、"success" と表示されるまで最大4秒まで待つ。 d.findElement(BUTTON).click() d.wait (-> # この条件式が何度も実行される d.findElement(BOX).getText().then (text) -> text is "success" ), 4000, "OMG" # 一応テストしてみる。 d.findElement(BOX).getText() .then((text) -> assert.equal text, "success") # 100%通るはず。 # どこかでエラーが起きたら、ここに来る。 .then(null, (reason) -> d.quit().then -> throw new Error(reason) ) # 問題なければテストを終了。 .then -> d.quit().then -> done()
WebDriverJs は.click()や.findElement() などの関数をコールすると、Promiseオブジェクトを返します。でもthenしていないのは、コールするだけで、Promiseがスケジュールに自動的に追加されて行くからです。つまり、d.then(function(){ return d.findElement(BOX).getText() }).then(...) とも書けますが、代わりに上記のようにも淡々とスケジュールを登録していくことができます。これは npm q には無い考え方でした。
重要なのは driver.wait(condition, ms, message) です。第1引数に、Booleanを返すFunctionを渡しましょう。すると、何度も何度もこの関数が呼ばれ、trueになるまで、ステップを待機する事ができます。つまり、任意の条件を作り出し、ページがその状態になるまでステップを止めることができます。今回の2秒かかるレスポンスのテストでは、condition関数が13回呼ばれていました。
会社の人が言っていましたが、この「何度も叩く」仕組みはテストフレームワークのJasmineの非同期テストと同じ仕組みのようです。合理的です。mochaには恐らく無く、mochaでは非同期後にユーザーが done() を実行することでテストが完了する仕組みを採用しています。それで十分だし、Promiseのresolveの考え方が好きなので私は気に入っています。
$ mocha ----------------------------- ⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅⋅✈ ----------------------------- 1 test complete (6 seconds) $
ビィィュュュォォァァァ・・・・。着陸成功です。ブラウザを立ち上げたりするのに時間がかかりましたが、Promiseのおかげで最短に近いスピードだと思います。
あとは特に書くこともないですが、mochaのデフォルトタイムアウトは2000ミリ秒なので mocha.opts に --timeout 10000 などと書いておきましょう。
PromiseとDeferredを表面的に実装してみた。
promiseにはpromise/A という小さなルールがあって、それに則って皆ライブラリを作っているみたいだけど、それはいいからとにかくthen したくなった。
主に、promiseオブジェクトのありかたが本家を無視しています。promiseオブジェクトは、thenを一度しか呼べないルールなんじゃないか。で、thenしたら 新たなpromiseオブジェクトをnew してreturn するんじゃないかと思ってる(全部詳細未確認)。
TODO: あとでちゃんと kriskowal/q のソースよむ。
var util = require('util'); /** * @constructor */ var Promise = function(startValue) { this.startValue = startValue; this.chain = []; }; // thenすると、ステップを蓄積していく。 Promise.prototype.then = function(f) { this.chain.push(f); return this; }; // doneすると、最後のステップを挿入したあと、実行を開始する。 Promise.prototype.done = function(f) { this.then(f).call_(this.startValue); }; /** * @protected */ Promise.prototype.call_ = function(val) { var me = this; // nextTickから実行開始。 process.nextTick(function() { var rv = me.chain.shift()(val); if (me.chain.length != 0) { // 蓄積されたステップを、nextTick ごとにcallしていく。 me.call_(rv); } }); }; (new Promise).then(function() { return 'a'; }).then(function(a) { return a + 'b'; }).then(function(b) { return b + 'c'; }).done(function(out) { console.log(out); // 'abc' }); /** * @constructor * @extends {Promise} */ var Deferred = function() { Promise.call(this); }; util.inherits(Deferred, Promise); /** * @protected */ Deferred.prototype.call_ = function() { var me = this; var args = Array.prototype.slice.call(arguments); // 次の関数を呼ぶための準備をする。doneなら無視。 if (me.chain.length >= 2) { args.unshift(function next() { if (me.chain.length > 0) { // next が呼ばれたら、次のステップに移れるように。 me.call_.apply(me, arguments); } }); } // nextTickから実行開始。 process.nextTick(function() { // 蓄積した最初の関数を実行。 me.chain.shift().apply(null, args); }); }; (new Deferred).then(function(next) { setTimeout(function() { next('a'); }, 800); }).then(function(next, a) { util.print(a); setTimeout(function() { next('--', 'b'); }, 800); }).then(function(next, and, b) { util.print(and + b); setTimeout(function() { next('c'); }, 800); }).done(function(c) { console.log(c); }); // 'a', '--b', 'c' と非同期的に出力される
苛烈!マングースは3度噛む
node.js のMongoDBドライバに Mongoose というものがあります。これの model.update 関数のコールバックの動きが分かりにくかったので、注意して様子を見てみました。
まず、ネイティブのREPLの動きを再確認します。
$ mongo MongoDB shell version: 2.2.1 connecting to: test // 新規データベースを作成 > use myBalanceBook switched to db myBalanceBook // 文書の作成 > db.girls.insert({name: 'honda tsubasa', spent: 500}) // 1つめの文書を作成 > db.girls.insert({name: 'mizuhara kiko', spent: 1500}) // 2つめ > db.girls.insert({name: 'nounen rena', spent: 20}) // 3つめ // 文書の書き換え > db.girls.update({name: 'nounen rena'}, {xxx: 'xxx'}) // 選択した1つの文書を上書き > db.girls.find() { "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 } { "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "name" : "mizuhara kiko", "spent" : 1500 } { "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" } // 上書かれてしまった文書 // 文書にキーを追加 > db.girls.update({name: 'mizuhara kiko'}, {$set: {age: 22}}) // 選択した文書に、ageキーを追加 > db.girls.find() { "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 } { "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" } { "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 } // ageキーが追加された文書 // upsert(あればupdate、無ければinsert) > db.girls.update({name: 'kojima fujiko'}, {name: 'kojima fujiko', spent: 4}, true) // upsert(第3引数がtrue) > db.girls.find() { "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 500 } { "_id" : ObjectId("516a84f543b0013848d9a9ae"), "xxx" : "xxx" } { "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 1500 } { "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 4 } // upsert(insert)された文書 // multi update (複数の文書を書き換え) > db.girls.update({}, {$inc: {spent: 1000}}, false, true) // multi(第4引数がtrue):セレクタにヒットする全ての文書に対し、変更を加える // (ここでは、spentキーの値を1000インクリメントする) > db.girls.find() // 全ての文書のspentキーの値が1000増えている { "_id" : ObjectId("516a84a543b0013848d9a9ac"), "name" : "honda tsubasa", "spent" : 1500 } { "_id" : ObjectId("516a84f543b0013848d9a9ae"), "spent" : 1000, "xxx" : "xxx" } { "_id" : ObjectId("516a84ce43b0013848d9a9ad"), "age" : 22, "name" : "mizuhara kiko", "spent" : 2500 } { "_id" : ObjectId("516a87be9a5e6b80d7b75284"), "name" : "kojima fujiko", "spent" : 1004 } >
注意すべき点はここです。
- updateは、デフォルトでは1件のみを書き換える
- multiフラグ(第4引数)がtrueの場合、セレクタ(第1引数)にヒットした全ての文書を書き換える
それでは、NodeからMongoDBを操作するドライバmongoose の update関数の動きを見て行きます。
ドキュメント: http://mongoosejs.com/docs/api.html#model_Model.update
予め、上記データベースのgirlsコレクションを操作するためのGirlModelを作成しておきます。
var mongoose = require('mongoose'); mongoose.connect('mongodb://localhost/myBalanceBook'); var GirlModel = mongoose.model('girls', { name: String, spent: Number, cantAfford: Boolean, age: Number })
それでは文書1件に対し、spentキーの値をインクリメントします。
GirlModel.update({name: 'mizuhara kiko'}, {$inc: {spent: 2000}}, function(err, numberAffected, raw) { console.log(err); // null console.log(numberAffected); // 1 console.log(raw); // { // updatedExisting: true, // n: 1, // connectionId: 48, // err: null, // ok: 1 // } });
1件の文書を操作しました。コールバック関数の引数には何が渡って来るのでしょうか?
第1引数からは、通常のNodeのコールバックと同じように、エラーが起きた場合エラーオブジェクトが来ます。
第2引数は、操作した文書の件数です。通常は0 か 1 が来るはずです。後述するmultiフラグをtrueにしない限り、書き換えは最大1件に対してしか行わないからです。
第3引数は、rawとありますから、生データのようです。このうち、
- updatedExisting: 何か1つでもセレクタにヒットしたらtrue
- n: 操作された文書の件数(コールバックの第2引数numberAffectedと同じ)
- connectionId: 知りません。どうせリクエストのUIDか何かでしょう。
- err: rawで返されるエラーオブジェクトのようです。
- ok: オッケーなら1、ダメなら0 のようです。試しにnumberAffected がゼロの結果でも、 ok は 1 でした。errと一体何が違うんだ。
続いて、mongooseから複数の文書をupdateしてみます。
GirlModel.update({ spent: { $gt: 50 } }, { $set: {cantAfford: true} }, { multi: true }, function(err, numberAffected, raw) { console.log(err); // null console.log(numberAffected); // 4 console.log(raw); // { // updatedExisting: true, // n: 4, // connectionId: 69, // err: null, // ok: 1 // } });
mongooseでは、multiなどのオプションを第3引数にオブジェクトでまとめて渡します。そのためコールバック関数は第4引数になります。multiフラグをtrueにしたので、操作された文書の件数:numberAffected が 4 になっているのが分かります。ふむふむ。
書いていて気づいたのですが、言うほど分かりにくくはなかったです。
npm q べんり
qはPromises/A提案ベースの非同期管理ユーティリティです。
https://github.com/kriskowal/q/wiki/API-Reference
と思ってたら、今はPromises/A+ というものがあるようです。
http://promises-aplus.github.io/promises-spec/
qがあれば、非同期処理が増えてインデントが増えて読みにくくなったり、クロージャ構造で変数を参照し過ぎてリファクタリングしにくいコードになるのを、ある程度防ぐことができます。
以下は、同期処理と非同期処理を順番に処理していく例です。
var Q = require('q'); var step1 = function(v) { var d = Q.defer(); setTimeout(function() { d.resolve(v); }, 100); return d.promise; }; var step2 = function(v) { console.log(v); }; var step3 = function() { var d = Q.defer(); setTimeout(function() { d.resolve('ohh'); }, 800); return d.promise; }; // 同期的に 'yeah' という文字列を取得するところからスタート Q.when('yeah') // 非同期処理。引数に 'yeah' が渡ってくる。ここでは、100ms 後に、もういちど文字列 'yeah' を返す。 .then(step1) // 同期処理。前のステップが終わってから実行される処理。ここでは、渡ってきた値を出力する。 .then(step2) // 再び非同期処理。今度は、800ms後に文字列 'ohh' を渡す。 .then(step3) // お終い。最後のステップで返される文字列 'ohh' が渡ってくる。 .done(function(v) { console.log(v, '... done. '); });
上の例では非同期処理がただのsetTimeoutですが、これが「ファイルアクセス」や「HTTPリクエスト」等の非同期だったとして考えてみてください。
このように、返り値であるpromiseオブジェクトのthenメソッドで連ねていくと、1本道のステップを同期・非同期に関係無く簡単に作ることができます。
更に、qは「非同期処理を並列実行し、2つの処理が終わった直後に、2つの返り値を利用して処理を続行する」という複雑な処理も、簡単に書くことができます。
var Q = require('q'); // ステップ1のA。2000ms 後に、文字列を返す。 var step1A = function() { var d = Q.defer(); setTimeout(function() { d.resolve('result of step1A'); }, 2000); return d.promise; }; // ステップ1のB。700ms 後に、文字列を返す。 var step1B = function() { var d = Q.defer(); setTimeout(function() { d.resolve('result of step1B'); }, 700); return d.promise; }; // ステップ2。並列処理の結果を出力する。 var step2 = function(step1A_result, step1B_result) { console.log(step1A_result + ', ' + step1B_result); } // ステップ1A と、ステップ1B を並列実行する。 Q.allResolved([step1A(), step1B()]) // 両方の処理が終わったらすぐに、ステップ2 を実行する。 .spread(step2);
これを自前でやろうとすると、おかしなコードになりがちです。2つの並列処理の最後に、お互いの処理が既に終わっているかどうかをチェックし、trueなら第3の処理を続行。しかし2つの処理結果の引数を渡しにくいし、順序も分かりにくいし、コードも重複しがちです。さらに、並列処理が10個だとどうでしょうか? きっと何もかもが嫌になり家に帰って10時間眠りたくなるでしょう。qがあれば、安全かつ最速でステップを進めることができます。
安全に、と言えば、qはステップの途中に処理を離脱するコードも1カ所に書いておけるのも魅力です。
var Q = require('q'); // ランダムで、OKあるいはNGになるステップを作成。 var okOrNgStep = function() { var d = Q.defer(); var timeout = Math.random() * 1000; setTimeout(function() { // 1:1 の確率で、OK あるいは NG と判断します。 timeout > 500 ? d.resolve('More than 500!') : d.reject('timeout <= 500..'); }, timeout); return d.promise; }; Q.when() // OKあるいはNGになるステップを実行。 .then(okOrNgStep) // OKなら、こちらにステップが進む。 .then(function(msg) { console.log('OK! ', msg); }) // NGなら、こちらにステップが進む。 .fail(function(msg) { console.log('NG... ', msg); }) // 最後には必ずここに来る。 .done(function() { console.log('... done.'); });
これは、ステップのどこかで起きた例外のハンドリングのためにとても有用です。仮に10ステップの長いフローがあったと考えてみてください。もしこの機能が無ければ、全てのステップの中に if (err) handleError(err); と書いておかねばならないでしょう。
q以外にも、非同期ユーティリティは他にもたくさんあるようです(https://github.com/joyent/node/wiki/modules#wiki-async-flow)。しかし私が触ったいくつかのnpmモジュールは qを使っていたので、比較的広く導入されているんじゃないかと思います。
個人的には、引数の取り方が柔軟過ぎる気もしますが、それほど問題にならないのかも知れません。
あなたは非同期管理をどうしていますか?
sass のmixin の省略記法、= と +
sass 3.2.0 でmixinの省略記法が導入されたようです。
=box-shadow($properties) -webkit-box-shadow: $properties -moz-box-shadow: $properties box-shadow: $properties .my-box +box-shadow(2px 2px 2px #000)
上のsassをコンパイル↓
.my-box { -webkit-box-shadow: 2px 2px 2px black; -moz-box-shadow: 2px 2px 2px black; box-shadow: 2px 2px 2px black; }
いい!
参考:
Soy(Closure Template)で Object をイテレートする
keys() が去年の夏にサポートされたようです。
https://groups.google.com/d/msg/closure-templates-discuss/plF5xSdYCqs/Va1Eh_oVGv4J
/** * @param obj */ {template .printKeyValue} {foreach $key in keys($obj)} key...{$key} value...{$obj.key} or {$obj[$key]} {/foreach} {/template}
Sencha Touch 2.1 のコードの雰囲気
この記事は、Sencha Advent Calendar 2012の12月08日の記事です。
残念ながら僕はSencha Touch のことを良く知らないので、初期化の流れとコードの雰囲気をぐだぐだと探索していきたいとおもいます。
ファイルを書き出します。
$ pwd /Users/nomorerolling/git/sencha-touch-2.1 $ sencha generate app GS ../GS/public/ Sencha Cmd v3.0.0.250 [INF] init-properties: [INF] init-sencha-command: [INF] init: ... [INF] generate-app: $ cd ../GS/public $ ls -a drwxr-xr-x 11 nomorerolling nomorerolling 374 12 8 14:54 . drwxr-xr-x 8 nomorerolling nomorerolling 272 12 8 14:56 .. drwxr-xr-x 4 nomorerolling nomorerolling 136 12 8 14:54 .sencha drwxr-xr-x 7 nomorerolling nomorerolling 238 12 8 14:54 app -rw-r--r-- 1 nomorerolling nomorerolling 1368 12 8 14:54 app.js -rw-r--r-- 1 nomorerolling nomorerolling 4972 12 8 14:54 app.json -rw-r--r-- 1 nomorerolling nomorerolling 1234 12 8 14:54 build.xml -rw-r--r-- 1 nomorerolling nomorerolling 1740 12 8 14:54 index.html -rw-r--r-- 1 nomorerolling nomorerolling 5040 12 8 14:54 packager.json drwxr-xr-x 7 nomorerolling nomorerolling 238 12 8 14:54 resources drwxr-xr-x 11 nomorerolling nomorerolling 374 12 8 14:54 touch
サーバを起動してindex.html にアクセスし、ページが表示されることを確認。
ですが、何も分からないです。何がどうなってこのページが表示されてるのか。
ページのソースを見てみたら
<head> ... <!-- The line below must be kept intact for Sencha Command to build your application --> <script id="microloader" type="text/javascript" src="touch/microloader/development.js"></script> </head>
スクリプトはこれだけ。あー、分かってきました。開発時モードなわけで、development.jsさんが、ファイルのパスを読み込んで、scriptタグを書き出すパターンですね。
じゃあ、ファイルパスはどこに書いてあるんでしょうか?
// touch/microloader/development.js var xhr = new XMLHttpRequest(); xhr.open('GET', 'app.json', false); xhr.send(null); var options = eval("(" + xhr.responseText + ")"), scripts = options.js || [], styleSheets = options.css || [], i, ln, path;
app.json に書いてあるみたいですね。同期通信、見慣れないな。
で、app.json を見てみると
"js": [ { "path": "touch/sencha-touch.js" }, { "path": "app.js", "bundle": true, /* Indicates that all class dependencies are concatenated into this file when build */ "update": "delta" } ], ... "css": [ { "path": "resources/css/app.css", "update": "delta" } ],
ありました。js[index].path。
ちょっと分かって来ました。現在開発モードで、読むファイルはこの順番。
1. development.js
2. app.json
3. touch/sencha-touch.js(15,000行)
4. app.js(50行)
次は、と。短い方から読もう。
app.js はExt.application 関数を実行しているだけ。巨大なオブジェクトを引数に与える。このobject ばっかりの感じがSencha っぽいイメージ。どこもかしこもオブジェクトリテラル。
Ext.application({ name: 'GS', requires: [ 'Ext.MessageBox' ], views: ['Main'],
ところで、全然関係ないけど、オプションを渡すたびに毎回オブジェクトを生成するのって遅くないんですかね?つまり、コンストラクタに渡すオプションオブジェクトを、毎回生成するよりも、どこか外に定義しておいて、使う時に参照するほうが速いんじゃないですかね?と思って。調べてみました。
https://gist.github.com/4239149
毎回オブジェクトを生成する方が、2倍速かった。なんで!?予想外。検証何か間違ってる?Node.js 使ったからかな?スコープを辿るのにもたついてるからか?・・・コンパイラがproductionコードをどうしているかは知らないが、とにかく、どうもSenchaのオブジェクトを逐次生成するスタイルは結構速そうだ。
まあいいや。app.jsで実行してる、Ext.application を見てみる。/touch/sencha-touch.js の9596行目。短い関数に、長いコメント。中で、Ext.app.Application をnew してるなぁ。
application: function(config) { ... requires = Ext.Array.from(config.requires); config.requires = ['Ext.app.Application']; onReady = config.onReady; scope = config.scope; config.onReady = function() { config.requires = requires; new Ext.app.Application(config); if (onReady) { onReady.call(scope); } }; Ext.setup(config); },
引数でわたって来たconfig.requires を、後で使うように細工してる。onReady とかscopre とかも。引数を渡したとたん、これだもんな。これがSenchaスタイルってわけですか。使い回すオブジェクトの値が、みるみるうちに書き換えられていく。で、新しいonReady のリスナらしき関数がセットされてる。この、config.onReady ってだれがcall するんだろう? というわけで、Ext.setup を見る。/touch/sencha-touch.js 9226行目。
Ext.setup長い・・200行くらいある・・とりあえず、コメントから。
/** * Ext.setup() is the entry-point to initialize a Sencha Touch application. Note that if your application makes * use of MVC architecture, use {@link Ext#application} instead.
Ext.setup は、Ext.application と同じアプリケーションの初期化関数で、ユーザーはExt.setup を使ってもいいけど、MVCしたいならExt.application しちゃえば?と言ってるみたい。Ext.application はExt.setupをラップしてたのか。だからさっき引数オブジェクトに変な細工を。
で、今知りたいのは、Ext.setup が onReady をcall してるかどうか。どこかでcallしてるよね?
setup: function(config) { var ... emptyFn = Ext.emptyFn, onReady = config.onReady || emptyFn, onUpdated = config.onUpdated || emptyFn, scope = config.scope, requires = Ext.Array.from(config.requires), ... delete config.requires; delete config.onReady; delete config.onUpdated; delete config.scope;
・・・ん、ちょっと関係無い所が気になる。delete してますよね。何でいちいち消してるんでしょう? きっと、変数config が参照するオブジェクトは存続し続けるので(new Ext.app.Applicationの引数にも使い回されてたし)オブジェクトのキーの中でも、生きてると分かりにくくなるキーや、生きてるとまずいキーを、不要になったそばから消してるんでしょうね。
管理を怠ると、すぐに分かりにくくなってしまいそうですね。1つのオブジェクトが、あらゆる初期化関数の引数に内部で使い回されています。デバッグで死ねそう。「この値、どこで入った!?」「この値、知らない間に書き変わってる!」などなど。ただSencha がカオスなのではなく、Javascriptがそうなんでしょうが。で、オブジェクトを使い回すなら、上のコードのように明示的に不要なキーをdelete していくのが、生き残るために非常に大事そう。そういうセルフサービス感がJavascript、面白いです。
で。onReady を探していたんだった。
Ext.onReady の値(関数)が魔術のごとく入れ替わっていくのは見て見ぬ振りをしておきつつ、onDocumentReady を待ち、Ext.require がスクリプトを読み込み(Ext.factoryConfig の中身は重要そうだな)、viewport 関連のセットアップを終えて、Loader#onReady、8383行目。
// duplicate definition (documented above) onReady: function(fn, scope, withDomReady, options) { var oldFn;
ここで、Ext.setup にわたってきたonReadyが、documentReady と相談しながら、どちらもOKだったらようやくcall されるようだ。
このあと、new Ext.app.Application(config) され、app.js で Ext.application に渡した onReady が実行される。初期化の流れと、Sencha の雰囲気がちょっとだけ分かった気がする。
実は、最初はExt.define のコードを読んでいたのですが、最後まで良くわからないまま終わってしまいました。。Sencha 、奥が深そうです。Sencha Advent Calendar はまだまだ空きがあるようですので、興味のあるかたはぜひ執筆してください!