Ractive.js入門
これは、jsCafe18で話したSlideです。
概要
これから、「Ractive.js」という
js上でReactiveなUIを
とてもsimpleにbuildするためのlibraryの話をします
目次
- reactive programming
- ractive.js
- 他のframeworkとの比較
- Backboneに入れてみる
そもそもReactive Programmingとは?
古くは、2003年頃から日本のblogでも幾つか言及されてる模様
去年、更に色々と動きが出てきた。
- 2013/7 ガートナーの2013年のreportで扱われてる
- 2013/9 リアクティブマニフェストが出た。これ訳されてる方がいた。
- 2013/9 infoQの記事見ると大体の流れや概要が分かる
バズりと意味の広がり
- なんか徐々にバズり感がある
- 他のバズりワード(cloud等)と同じように意味が広義に、より抽象的になってきてる
- マニフェストにあるような広義な意味でのReactiveは一旦今日は置いておく
Reactive programmingの例
2010年に素晴らしい記事を書かれてるmaoeのブログを参考にjsで書かせて頂くと
var a = 1; var b = a + 1; a = 10; console.log(b) // => 11
- aの値の変化からbの値が動的に変化した。
- ※勿論、今のjsはこんな動きしない。
- 一見、何言ってるか意味分からないかもしれないが、以下のExcelのセル計算と同じimage
- A1セルに
=B2+C3
と書くと - A1セルの内容はB2とC3が加算されて、動的に書き換えられる。
- A1セルに
結局Reactive programmingとは
- 時間や外部入力と共に変化する値や計算を、
- プログラムするのではなく、
- システム側が見えない所で反応(reaction)してくれるもの
用途は、色々あるようだが、GUIプログラミングに向いてる。
ここまで、maoeのブログの記事の要約。ありがとうございました。
GUIとReactive
- Wikipedia見てみると、一例としてMVCアーキテクチャが書かれてる。
- リアクティブがModelとViewの相互連携を可能にする
- こう書くとデータバインディングを連想するが。
- stackoverflow:データバインディングとリアクティブの違いは何?
- Reactive:汎用的な概念
- data bindings:Reactiveな視点で作られた機能
- stackoverflow:データバインディングとリアクティブの違いは何?
Reactive Programmingとjavascript
jsでのReactive関連libはRxJS/bacon.js/knockout/Meteor等色々ある。
アプローチも様々。
偶々、「BackboneとReactive」で検索してると
なんか、Backbone本家のGithubで、面白そうなissueひっかかった。
いい加減な訳
Q: Ractive.jsとBackbone使うのって良いIdeaじゃない? 我々のアプリはHandlebarsとBackbone使ってる。 幾つかのViewの中に維持の必要な多くの要素があるがお互い非常に強く結びついている。 これはsubviewに分ける事を困難にしている 私はDOM要素を操作する事やレガシーコードをリファクタリングする事に疲れた。 そこで、Ractive.jsを見つけた。 これは、これらの問題を解決してくれるために開発されている。 それは、Angularみたいだが、ただAngular程にクレージーではない。 Angularは開発中の殆どの問題を壊してくれるように思う。それは素晴らしい事かもしれない。 しかし、私のようないくらかの人々にとって学習する事が厳しい。 そこで、私の質問は、次の通り。 ・Backboneの中で、Ractive.jsと一緒にtemplateを使う形で置換するのは良いのでは?
Ractive.jsとは
theguardian.comで新しいアプリを作るために開発されたらしい(去年2013年の夏頃)
- ReactiveなUIをビルドするjsのライブラリ
- 特定のフレームワークを強制させる事はしない
- なので、Backboneと組み合わせようが自由(実際、公式pageにbackboneとの連携例もあがってる)
つづり
- ちなみに、つづりは、「ract」ive.js
- reactiveでないので、ご注意を
- React / reactive.js等等同じ様な名前の物がいっぱいある
情報源
特徴
公式pageによると
- データバインディング
- 美しい宣言的なシンタックス
- イベントハンドリング
- 泣かせたり髪をかきむしったりさせない
- 柔軟でパフォーマンスの高いアニメや移動ができる
根本的にDOM操作のアプローチと異なる。
angularあるでしょ
- 非常にangularに近い感じ。
- 実際、かなり意識されて作られている
- 開発時の呼称がAnglebarsだったらしい(Angular + Handlebars) ※ロゴまであった。
- 大きな違いは、DI等特殊な事をractive.jsはやってない事
- ractiveは非常にsimpleな文法になっていて覚える事は少ない(1hでtutorial終わる)
knockoutがあるでしょ
- ractive.js = databindできる軽量のlibrary
- 立ち位置がknockoutそっくり
- ここの記事のコメントでも戦ってる
- 作者はknockoutの文法が気に入らないらしい(言うだけあって、ractiveはkoと比べてもsimple)
- ここの比較記事によると
- ractiveは、internalなDOM持っていて全要素のre-renderせず、DOMを1つずつでなくグループでまとめ非同期に更新できる所が違うらしい
- 後、そもそもractiveはMV* FWでないので、ModelとかViewとかいう言葉が出てこない
data-bindingsの2つの方式
- dirty-checking(ex. angular)
- 前の値比較,変化あれば変更eventをfire
- simpleなobjectを扱えてsimpleな点がメリットだが、処理として無駄多い
- change listeners(ex.knockout)
- 動的な処理をする関数の中にある監視objを追跡してその関数を入れるといった事が必要(依存性追跡)
- simpleなobjectでなく決まった口から呼ぶ必要があり、また、個別にfireするので複数まとめて処理する時に効率的でない
- 他、色々メリデメがある。※参考:stackoverflow slide
- ractive.jsは後者(ちなみにまとめて処理は出来る※多分updateの話)(また、後述するが依存性追跡の仕組みはちょっと違う)
要するに
- Ractive.js = 複雑な所がないAngular
- Ractive.js = knockoutと立ち位置が近く、仕組みも近いが、もっとsimple
ractive.jsを始める
公式pageのsampleをやってみるとこんな感じ(gist)
js
var ractive = new Ractive({ el: "#result", template: "#myTemplate", data: { user: '太郎', messages: { total: 11, unread: 4 } } });
sample(html側)
- 出力先
<div id="result"> </div>
- テンプレート
<script id='myTemplate' type="text/ractive"> <p>こんにちわ{{user}} さん。 <strong>{{messages.unread}} 件の未読メッセージがあります。</strong> </p> </script>
sampleを動かす
- sampleを動かすと、templateが出力されている
- console上で、以下をやってみるとbrowserの値も変わってるはず
ractive.set('messages.unread', 5);
sampleのjsを見てみる
var ractive = new Ractive({ // el : 出力先の要素 el: "#result", // template : 表示の際元となるテンプレート template: "#myTemplate", // data : Reactiveに値を変えて行きたいデータの固まり data: { user: '太郎', messages: { total: 11, unread: 4 } } });
- ぱっと見もごちゃごちゃしてなく、たった3つのプロパティしかない。
sampleのtemplateを見てみる
<script id='myTemplate' type="text/ractive"> <p>こんにちわ{{user}} さん。 <strong>{{messages.unread}} 件の未読メッセージがあります。</strong> </p> </script>
- mustacheの書き方を採用している
-
{{ }}
で変数等 - この例には書いてないが、mustacheの他の文法がそのまま使える(#等)
-
- jsで定義していたdataの中身を変数として使ってる
user
とmessages.unread
consoleでやった事を振り返る
ractive.set('messages.unread', 5);
- 作ったinstanceのdata内の変数に値をsetしてる
- 単純に内部の値が変わるだけでなく、画面上の要素も変わってる
ractiveのデータの取得変更の整理
- 取得したい
ractive.get( ‘messages.unread’ )
- 変更したい
- 方法1)前述の
ractive.set('messages.unread', 5);
- 方法2)
reactive.update(‘message’)
- 方法1)前述の
変更の方法2の例
var msg = ractive.get("messages"); msg.unread = 9; ractive.update("messages");
仕組み
-
- Ractiveは、jsonみたいなツリーの中でtemplateをparse
- 式を見るとAbstractSyntaxTreeを作る(参照を抜き出す)
- mustacheをrenderする時参照を解決
Ractive.parse( '{{i+1}}' ); // { // t: 2, // mustacheの型 // x: { // 式 // r: [ "i" ], // 式の中の参照 // s: "${0}+1" // 参照を含む式 // } // }
仕組み
- 単純な文字展開(遅いeval繰り返し)する仕組みと比べて冗長なようだが、
- 関数を一度で作るだけで済む
- メモリに効果的だし、パフォーマンスも良い
dynamic attributeの使い道
- 例えば、cssの切り替えもできる
<p style='color: {{color}}; font-size: {{size}}em; font-family: {{font}};'> {{greeting}} {{recipient}}! </p>
関数
- 勿論、関数も追加できる。
- 例えば、数値を整形するformat関数を作る
<p>The population of {{country}} is {{ format(population) }}.</p>
data: { country: 'the UK', population: 62641000, format: function ( num ) { if ( num > 1000000000 ) return ( num / 1000000000 ).toFixed( 1 ) + ' billion'; if ( num > 1000000 ) return ( num / 1000000 ).toFixed( 1 ) + ' million'; if ( num > 1000 ) return ( Math.floor( num / 1000 ) ) + ',' + ( num % 1000 ); return num; }
イベント
- 1クッション挟む(proxy event)
- html側の属性に
on-イベントハンドラ名=‘イベント名’
- js側で
reactive.on(‘イベント名’, function(event) {})
- html側の属性に
<button on-click='activate'>Activate!</button>
ractive.on( 'activate', function ( event ) { alert( 'Activating!' ); });
mustache系で出来る事
mustacheで出来る一通りの事がRactive.jsでも出来る
// true,falseチェック = # {{#signedIn}} <p>You’ve logged in</p> {{/signedIn}} // ※これで、ractive.data.signedIn = trueの時に、表示される
// not = ^
{{^signedIn}}<!-- not-signed-in block -->{{/signedIn}}
// これで、ractive.data.signedIn = falseの時に、表示される
list操作
これもmustache
{{#people}} <tr> <td>{{name}}</td> <td>{{age}}</td> </tr> {{/people}}
// reactiveの中 data : { people : [ {name : “taro”, age : 18}, {name : “hanako”, age : 20} ] }
listに追加
people.push(newPerson);
他にpop/shift/等準備されてる
tutorial
ここまで書いた内容がtutorialに書かれてるので、良ければ。(1hで終わる)
BackboneとRactive.js
公式pageの例にもある位、setで使うのに問題は無さげ。
- Backbone adapterを使うと、ModelとViewを繋いでくれるみたい
- src見ると、Model/Collectionのchangeイベントにひっかけてractive.setしてる
色んな妄想
- (´-`).。oO(これ、listも勝手にsyncしてるからMarionetteのCollectionViewとか全部要らなくなるんじゃね?
- (´-`).。oO(これ、viewにそのまま変数貼付けれるからViewのstate管理が綺麗になるんじゃね?
- (´-`).。oO(これ、jsから見て完全にDOM分かれてるからテストし易いようなimage。
Backbone的に可能性を感じる
backboneと一緒に書いてみた
- 要するに、BackboneがAngularっぽくなった。
ractive.TaskListView = Ractive.extend({ template: "#task-list-template", init: function(options){ this.set('taskList', []); this.on({ onClickTaskItem: function(e, item){ item["finished"] = true; this.update("taskList"); } }); }, pushNewTask: function(task){ this.get('taskList').push(task.attributes); }
書いてみたhtml
<div id="task-list"> <script id="task-list-template" type="javascript/ractive"> <ul> {{#taskList}} <li on-click="onClickTaskItem:{{this}}" style="{{.finished ? 'text-decoration:line-through;' : ''}}"> {{name}} </li> {{/taskList}} </ul> </script> </div>
変わった事
- Before:Backboneで親子階層書くのが手間(Marionette使ってもやっぱり大変)
- After:ractiveのmustacheのlist表示で一発
- Before:BackboneでView内のstate管理(Modelにする程ではないような物)が大変だった
- After:ractiveのtemplate内の分岐で解決
- Before:BackboneのView内でDOM操作が絡まる(Marionette.View.ui使ってもカオス)
- After:ractiveはデータだけ触れば良いのでごちゃごちゃしない(test書き易そう)
もう一度言うけど
- Angularでよくね?
- DI等キモい所が無いから良い
- knockoutでよくね?
- knockoutとBackbone繋ぐ例は少ないが、(knockback?そこまで要らない)ractiveはMV*でもなく、小さいので簡単に入れれる。(公式でも例あるし。)
- ractiveの方が見た目綺麗。
個人的な感想(良い所)
- 要するに、elとtemplateとdataの3つとmustacheなので、すげーsimple。不可解な所が無い
- Backbone
- Marionetteもstickitも要らなくなるかも…
- Backbone.Viewと併用するなら$elの生成timing考えないといけないかもだが、ractiveでeventもまとめて書ける事等から、そもそもBackbone.View要らなくなりそう(routerとmodel通信部分とevent連携だけBackboneから借りる??)
駄目な所
- 個人的にmustache自体キモい(普段ejs使ってる事もあり)
- mustacheへの独自機能追加もキモい(
../
で親階層辿れる??何言ってるの?) - templateエンジンを自分で選べないのは苦痛
- mustacheへの独自機能追加もキモい(
- 情報量の少なさと使ってる人の少なさ
- knockoutとの棲み分けをもっと明確にした方が良いと思う
もう少しRactive.js使ってみる事にする
おしまい。
Backbone.jsとMarionette.jsで発表してきた
BackboneとMarionetteの発表してきた。
結構現実的な内容で、色々個人的に試行錯誤している所を含んでおり、多分賛否ありそう。
まあ、こういうドロドロした話もたまには有っても良いかと。
Backboneで直面する問題って皆大体似てるので、
俺の話が正解でなくても何かヒントが得られれば光栄です。
気になった所
書いていて、改めて気になった事は、
- statusの落としどころ
- これは、plugin作るしかないな。
- テストダブルとのつきあい方
などなど。
また、いつか、ブログに書く。
思った事
twitterのfollowerさんと会えたり、
関係会社や競合(!?)さんのエンジニアと話せて楽しかった。
Backbone書いてる人と話してみると、
やっぱり同じ様な悩みかかえてて、
「あるあるー!」で盛り上がった。
こういうのパターン化出来るんだろうな。
振り返ってみて
結局、このブログで書いてきた事の総集編みたいなslideで頭の整理が出来て良かった。
骨には肉を付けていかないとね。
linuxカーネルのセマフォについて
アルゴリズムとデータ構造についてOSSから勉強する会 という勉強会に行ってきた。
これは、とある掲示板の書き込みにOSSで使われてるアルゴリズムが書かれている物があって、
これを元に、皆で読み解いて行こうという企画。
カーネル読むという意味でも、アルゴリズムの勉強という意味でも、
とても良い勉強会。
面白かった。
まとめ
自分のチームはセマフォやった。
まあ、色々と不慣れだったし分からない所も多かったが、勉強になった。
以下、発表したメモに加筆した。
概要
- セマフォ(semaphore)とは
- 複数プロセスによるアクセス制御に使われる仕組みの事
- 任意の個数の資源を扱うcounting semaphoreと、値が0,1しかないbinary semaphoreがある。
- 今日見たのは前者のcounting semaphore。この話を書いて行く。
- ちなみに、後者がMutexになる(同等の機能を持つ)らしい
counting semaphoreって?
この仕組みが面白いのだが、
例えば、自習室に10個の机があって、
入り口の綺麗なお姉さんが、
机の数と同じ10枚のカードを持っていたとする。
(綺麗なお姉さんじゃなくてもいいけど、いきなりIRQの話でてきても嫌になると思うので、
想像の中で、優木まおみみたいなお姉さんがいたとしよう!もう結婚したけど。)
- 利用する学生から見た時
- 自習室に入りたければ、1枚カードを取って空いてる机に座る。
- 自習室から退出したければ、自分の持ってる1枚のカードを返す。
- もし、自習室のカードが全部、無くなった時は、自習室の前で並ぶ。(この辺り、正しくは後で例示する。一旦、とにかく待ち行列を作るイメージで。)
- 入り口の綺麗なお姉さん(優木まおみ似)
- 入り口のお姉さんは、「残り」のカードの枚数で、あと机が何個余っているのか知る。
この時、学生がプロセスにあたり、
お姉さんがカードを管理しているおかげで、自習室に人があふれない。
以下、ソースの説明も、この例を引き続き使う。
ソース
ヘッダファイルから。
https://github.com/mirrors/linux-2.6/blob/master/include/linux/semaphore.h
構造体として、以下を持っている
struct semaphore { raw_spinlock_t lock; unsigned int count; struct list_head wait_list; };
- raw_spinlock_t
- スピンロック:ロック獲得するまでに単純にループして定期的にチェックするロック
- このロックに必要な情報を入れてるっぽい
- count
- 自習室のカードの「残り」枚数
- wait_list
- 満員の時に入り口で優木まおみの前で並ぶ行列
.cソース
https://github.com/mirrors/linux-2.6/blob/master/kernel/locking/semaphore.c
メインは以下の2種類の関数。
- 入室=カード枚数が1枚減る
- downという関数を実行する
- 退出=カードが1枚戻ってくる=カード枚数が1枚増える
- upという関数を実行する
downのvarionation
downは幾つかvariationがある。(down/down_interruptib/down_killable ..etc)
基本的には、殆ど一緒だが、違いは、
満室で待ってる時の対応が違うっぽい。
例えば、down_interruptibleは、満室で待ってる時に、シグナルを受けたら待つの止めるが、
down_killableは、満室で待ってる時に、fatalなシグナルを受けたら中断する。
こんな感じの違いがある。
以下より、(今はDeprecatedなのだが、基本的な動きは全部変わらないので)down関数を見る。
down 関数
こんな流れ
- spin lockのIRQ上のflagを保存
- カウントダウン
- カウンタが1以上:カウントダウン
(count—)
- カウンタが0:__down_common関数を叩く(status渡す)
- down_common関数:満室時の待ち処理
- カウンタが1以上:カウントダウン
- spin lockのIRQ上のflagを戻す
void down(struct semaphore *sem) { unsigned long flags; raw_spin_lock_irqsave(&sem->lock, flags); if (likely(sem->count > 0)) sem->count--; else __down(sem); raw_spin_unlock_irqrestore(&sem->lock, flags); }
まずは、raw_spin_lock_irqsaveを見てみる。
raw_spin_lock_irqsave
#define raw_spin_lock_irqsave(lock, flags) \ do { \ typecheck(unsigned long, flags); \ flags = _raw_spin_lock_irqsave(lock); \ } while (0)
一見、ループっぽいのだが、do while(0)って、これだった。
http://www.jpcert.or.jp/sc-rules/c-pre10-c.html
ただの固まり。
そもそもIRQとは
この関数名にある、IRQは何なのかというと、
IRQ = Interrupt ReQuestの略で、割り込み要求の事。
- 割り込み要求とはハードウェア(keyboard等)がCPUを呼び出す時に起きる。
- ソフトウェア側でも例外発生時に、割り込み要求を出す事がある。
- 要求を受け付ける場合は、現在実行中の処理を中断して優先的に割り込み要求を実施する
以下のコマンドでIRQを見る事ができる
$ cat /proc/interrupts CPU0 CPU1 CPU2 0: 132 0 0 IO-APIC-edge timer 1: 7 0 0 IO-APIC-edge i8042 8: 0 0 0 IO-APIC-edge rtc0 9: 0 0 0 IO-APIC-fasteoi acpi 10: 133723985 0 0 IO-APIC-fasteoi virtio0, virtio2 11: 14239496 0 0 IO-APIC-fasteoi uhci_hcd:usb1, virtio1, virtio3 12: 102 0 0 IO-APIC-edge i8042 14: 0 0 0 IO-APIC-edge ata_piix 15: 62 0 0 IO-APIC-edge ata_piix NMI: 0 0 0 Non-maskable interrupts LOC: 3207352625 2940858960 2935259295 Local timer interrupts SPU: 0 0 0 Spurious interrupts PMI: 0 0 0 Performance monitoring interrupts PND: 0 0 0 Performance pending work RES: 71463290 73636621 72198537 Rescheduling interrupts CAL: 291 552 648 Function call interrupts TLB: 1270192 1591796 1461926 TLB shootdowns TRM: 0 0 0 Thermal event interrupts THR: 0 0 0 Threshold APIC interrupts MCE: 0 0 0 Machine check exceptions MCP: 79072 79072 79072 Machine check polls ERR: 0 MIS: 0
これが、例示していた自習室の、linuxカーネル内の実際のイメージ。
カーネルはこのIRQを管理するために、セマフォを使ってるみたい。
raw_spin_lock_irqsaveは、このIRQに一旦、保存してるっぽい。
中身を更にほじると、
raw_spin_lock_irqsave
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock) { unsigned long flags; local_irq_save(flags); preempt_disable(); spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); /* * On lockdep we dont want the hand-coded irq-enable of * do_raw_spin_lock_flags() code, because lockdep assumes * that interrupts are not re-enabled during lock-acquire: */ #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); #else do_raw_spin_lock_flags(lock, &flags); #endif return flags; }
- flagをIRQ保存
- 生のスピンロックさせるためのflagを立ててるっぽい
今、これで、IRQに自分のプロセスを示すフラグを登録した状態。
奥迄深入りするのは、ここまでにして、down関数の話に戻る。
次に、メインとなる、カウントダウンをする。
カウントダウン処理
今、IRQを1つ占有したので、カウントダウンしないといけない。
元の例で言うと、自習室の机を1つ取ったので、カードを1枚ひかないといけない。
- もし、まだカードがあれば、
- 単純にカードを引いて(カウントダウン)
- もし、カードが無ければ
- down_common実行する(これが満室時の待ち処理)
※なので、このロジックを正しく例示すると、満室だろうが学生は自習室にとにかく入っていて、自分の名前を勝手に部屋に落書きした後、自習室カードの奪い合いをしてるイメージになる。(カードを貰わないと、自習は始められない)
__down_common
大まかな流れ
- 待ち行列に自分を入れる
- 無限ループ (
for(;;){ }
)- 一旦、unlockして、少し待ってから、その後、ロックし直す。
- もし、semaphore_waiterが実行中になったら、抜ける。
この無限ループしてる所が、スピンロックの実体と思われる。
static inline int __sched __down_common(struct semaphore *sem, long state, long timeout) { struct task_struct *task = current; struct semaphore_waiter waiter; list_add_tail(&waiter.list, &sem->wait_list); waiter.task = task; waiter.up = false; for (;;) { if (signal_pending_state(state, task)) goto interrupted; if (unlikely(timeout <= 0)) goto timed_out; __set_task_state(task, state); raw_spin_unlock_irq(&sem->lock); timeout = schedule_timeout(timeout); raw_spin_lock_irq(&sem->lock); if (waiter.up) return 0; }
unlock_irqrestore
この後、IRQに登録したflagをrestore
static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock, unsigned long flags) { spin_release(&lock->dep_map, 1, _RET_IP_); do_raw_spin_unlock(lock); local_irq_restore(flags); preempt_enable(); }
おしまい
カーネルは非常に奥が深く、奥にもぐっていくとどこまでも行けそう。
ちょっと、よく分からない所も多々あったのだが、
せっかく、色々調べたので、残しておく。
また、次もこのイベント行こうと思う。
Backbone.jsのテストの悩みとその解決
経緯
Backbone.jsでTDD書く時に、
Model/CollectionはServerSideのようなイメージで書けるのだが、
Viewのテストがfrontend特有の事があり、結構、悩ましい。
ここでは、Backbone.Viewを中心に、
テストで悩ましい所をどう解決していくか、試行錯誤している所等書く。
※どちらかというと、初めにどう書いて行くか?のTDD寄りの話。
ちなみに、環境は、
- Backbone
- Marionette.js
- Test
- mocha
- chai
- sinon.js
を使ってる。
概要
- Backbone.Viewのテストの基本形
- 悩み1: javascript vs html
- 答え:DOM分離
- 悩み2:検証方法が素直に書けない
- 答え:テストダブル
- イベント
- サーバー通信
- アニメ
- local変数
- 答え:テストダブル
- Backboneのテスト現実解
Viewのテストの基本形
初めに、Viewのテストの基本形から。
describe("TodoView", function() { // ①初期化 beforeEach(function() { this.view = new TodoView(); }); // ②メソッド毎にテスト describe(“methods”, function() { it(“should xxxx“, function() { // this.view.yourMethod(); // ③検証 // expect(this.view.ui.title.is(':visible')).to.be.true; }); }); });
こんな感じで書いて行く。
- ①初期化
- view毎回newするの面倒なのでbeforeで。(大体、皆一緒)(initializeの中身をテストするなら、ちょっと調整が必要)
- 場合によってはrender入れてもいいかと。(Marionetteとか。この時、ネスト必要。)
- ②メソッド単位で切り分け
- 関数の実行(eventを経由しない方が良い※後述)
- 検証
- ③検証
- 大抵Viewの$elをchk($(selector)書かない方が良い※後述)
こういう基本的な形を、各環境のgeneratorのはくテンプレートや、
面倒くさければ、editorのsnippetsに登録すると良い。
ここからが、本題。
色んなお悩みを書いて行く。
悩み1
jsだけでなく、html/DOM迄考えなくては行けない事
サーバーサイドのテスト
サーバーサイドのTDDは、以下のたった2つのソースだけを行き来して書けば良かった。
- ターゲットソース
- テストソース
とても、シンプルだし、何よりTDDに集中できる。
実は恵まれていた。
BacboneもModelやCollectionは、同じイメージで書けるのだがViewは少し違う。
Backbone.Viewのテスト
BackboneのViewのテストは、ターゲットとテストソース以外に、
- ターゲットのhtmlや
- 場合に寄ってはテスト用のhtml等のファイルも
行き来が必要になったりする。これらも含めてTDDしようとすると、以下の問題に直面する。
問題1:htmlとターゲットソースだけで完結してしまう問題
実際にhtml上で動かさないと分からない事、調整しないと行けない事も多々あるので、
ファイル横断していく内に、html <=> ターゲットソースの行き来がメインになってしまい、
開発フローの中でのTDDの必然性が薄れてテスト習慣がつかなくなる。
問題2:htmlがデザイン変更等から多くのテスト変更を伴う問題
htmlはデザイン等頻繁に変わる事あるので、
テスト書いてもDOM操作を中心に頻繁に調整が必要になる。
(expext($(‘#aaa’).text()).toBe(‘aaa’)みたいなの一個ずつ直す必要あるし、
test用のでかいtemplateとか準備してるとマジ地獄。
実装直すなら、テストも直すのは当然で正しいのだが、量が多いのが問題。)
問題3:集中力の問題
単純に対象ソースとテストソース見比べれば良かったサーバーサイドのTDDと比べて、
frontendのTDDはあっちこっちファイル見比べてTDD以外の考慮が多いので、
TDDしようとする集中力とやる気を削ぐ。
この集中できないのが一番問題。
jsのTDDに集中するには。
TDD続けるには、とにかく、作業ファイルを
- ターゲットソースと、
- テストソースの
2つだけにするのが良いと思う。
そのために、DOM/htmlを分離する
(TDDする事と、DOM調整する作業を分ける)
DOM/html分離
- DOM
- テストコードにDOMセレクタ書かない
- テストコードからは、Marionette.View.ui経由で掴む
- view.events経由せず、直接関数実行
- Template
- test用fixture template出来るだけ使わない(修正が面倒)
- 最悪、使っても、1,2行の断片程度で
- ターゲットとなるViewも整理する
- 元のViewは、el内だけを扱う。(el以外への変更はtriggerして、別Viewに)
- View内でelを指定しない。相対Viewにする。(これで、テスト用template要らなくなるはず)(Marionetteに沿って書くとRegion使うので、そうなってるはず。)
※ちなみに、勿論、最終的にDOMも貫通したテストは必要で、
それをこのレベルで書くなら、事後的に追加して書いていけば良いし、
書かないなら、別のレベルのテストで確認が必要。
DOM/htmlを完全にスルーするという話ではない。
TDDみたいに、始めてTest書く時にはDOM意識しなくていいのでは?という話。
sample
// View var YourView = Backbone.Marionette.View.extend({ // elは指定しない // ui定義して、DOMアクセスは全てここからにする(変更時、ここを起点に直す) // ※省略してるけどMarionette.Viewならthis.bindUIElements忘れずに ui: { menu: “#menu” }, onClickShowMenuButton: function(){ this.ui.menu.show(); // mocha // test用のfixture template出来るだけ使わない before(function(){ this.view = new YourView()} it(‘should show menu’, function(){ // event経由せず直接メソッド実行 this.view.onClickShowMenuButton(); // ui経由でDOMを掴む expect(this.view.ui.menu.is(‘:visible’)).to.be.true;
悩み2
検証を素直に書けない
Backbone.Viewでちょっとして、html操作であれば、
簡単にテスト書けるのだが、例えばevent挟むと、
何書いていいかわからなくなる。
image的には関数実行後に、素直に
expect(this.view).haveCalledEvent('changeName') // 注意:ただのimageです
と書けたらいいのに。 勿論、書けない。
イベント以外にも、困った事に素直に検証書けない所がいっぱいある
何があるか?整理するためViewに書いてる事を整理すると
Viewに書いてる主要な事
Viewに大体こんな事書いているんじゃない?
この中で、instance変数の変更はサーバーサイドと同じ。悩まない。
メソッド呼び出しも、spy/stubs使えそうなのは、すぐimage湧く通り。
window.confirmも、this.stub(window, "confirm").returns(false);
でいい。
DOM操作は、jQueryでチェックし直せば良い(それぞれのTestFW上でjQuery操作補助するライブラリあるので、それ使うと楽)
この辺りは普通にイメージの湧く通り。
問題は、それ以外の残った所。
残った要素
- イベント
- サーバー通信
- アニメ
- local変数(model等)
これら、全部、色んな事情で結局テストダブルが必要。
イベント
- イベント掴むために、testdouble必要
- event用spyを準備して
- 実行されたかチェックしないといけない
// before this.eventSpy = sinon.spy(); this.view.on("jikken", this.eventSpy); // it this.view.onClickTriggerButton(); expect(this.eventSpy.called).to.be.true;
サーバ通信のfake
- そのまま書くと、
- テストに検証に非同期callback深くなるし、
- POST/PUT後の検証方法もそれぞれ考えないと行けないし、
- 問題有ったときの原因の切り分けも面倒くさい。
- という事で、これもテストダブル必要
- 色々やり方ある
$.ajaxにspy
describe('onClickSave', function(){ beforeEach(function(){ this.ajaxSpy = sinon.spy(Backbone.$, 'ajax'); }); it('should save', function(){ this.view.onClickSaveButton(); expect(this.ajaxSpy.called).to.be.true; }); });
saveにstubs
- stubsのyield(yieldsTo)使うと成功/失敗も掴める
// before this.stub = sinon.stub(this.view.model, "save"); this.stub.yieldsTo('success', {name: "taro"}); // it expect(this.stub.called).to.be.true;
アニメ
- 何秒か待っても書けるが、テスト時間かかる
- sinonのfakeTimer使うとすぐ終わる。
- useFakeTimers()してtick(ミリ秒)するだけ
// before this.fakeTimer = sinon.useFakeTimers(); // it this.view.onClickHideAnime(); this.fakeTimer.tick(2000); expect(this.view.ui.animeButton.is(":visible")).to.be.false;
local変数のチェック
- よく、local変数でYourModelをnewしてsaveしてみたいな事書く
- ここでmockが役立つ!
- モデル.prototypeにmockすると、関数内local変数も検証可
// Backbone側 var Task = Backbone.Model.extend({ url: "/tasks" }); var TestView = Backbone.View.extend({ saveTask: function(){ var task = new Task(); // <--外から掴めない task.save(); }}); // mocha側 // before this.view = new TestView(); this.mock = sinon.mock(Task.prototype); this.expect = this.mock.expects('save'); // it() this.view.saveTask(); expect(this.mock.verify()).to.be.true;
Viewのテストで思った事
これで、一通り、書けるハズ(多分)
しっかし、Backboneのテストはコストかかるなー。
- Backbone.View(特にmarionette)の関数=小粒になる
- 最小単位のUTが効果的と思えない(あまり複雑なlogic無い。関数Aといえば命令Bする!はい終わり!みたいなので、bugが入り込む余地が少ない)
- sinon書くの結構大変。その苦労して書くコストに対して効果が見合わない気がする
- 最小単位のUTより、関数同士の組み合わせやstatus変化の方がbuggy
- initialize => events => 関数Aだけでなく、
- initialize => events => 関数B => status変更 => 関数Aの時どうなる?とか
- 非同期fetchしてる間にタブ切り替えてどうなる?とか
- UTをやらないという話をしているのではないが、UTとしての網羅性に注力するより、ComponentTest/(whitebox寄り)結合テストに注力した方がいいかも
現実解
- Backboneとテスト
- TDDでとりあえずの動作をぱーっと書いて実装する
- その後、UTの網羅性より、ComponentTest/(white寄り)結合テストに時間かける方がいい気がする
参考
- blog
- 書籍
- slide
- source
- BackboneTesting本のcode ※100点とは思わないけど、一番参考になる。
jsでTDD!MochaとChaiとsinon.js入門
※この記事は社内勉強会向けの資料の下書きです。書きなぐりの下書きで見直すと最後の方の文書がヤバいので、いつか書き直します。読み辛い所は申し訳ないです。
概要
- TDD
- テスト自動化とTDDを整理
- TDDとBDDの違い
- Test Framework in javascript
- QUnit/jasmine/mochaについて、違いやメリデメを知る
- mocha
- 基本的な書き方
- アサーションライブラリのメリデメを整理する
- chai
- 記述形式の違い整理
- 基本文法
- sinonjs
- spy
- stubs
- mock
TDD
Test Driven Development
- テスト駆動開発
- by ケントベック
特徴
- xUnit系/BDD系のテストフレームワーク使う
- テストするコードも実装
- テストファースト
- 実装の後にテストするのではなく、テストを先に書いて実装する
- サイクル
- Red(失敗) => Green(通過) => Refactoring
- 仮実装 => 三角測量 => 明白な実装
よくある勘違い
- TDDはテスト手法...ではない
- これを真面目に考えて、通常のテスト観点でテストすると、膨大な量のcodeを書く事になり、TDDがコスト要因になる。よくある失敗例。
- また「何を書いていいのか分からない」状態になる。これもよくある失敗例。test firstで、通常のテスト観点は書けない。
それでは、TDDとは?
- TDD は結果としてテストをしているだけ。
- TDDの本質は、設計や分析
- あるコンポーネントのインタフェースや構造を整理していく為の方法
- 多くの人が誤解するので、整理する為にBDDなんて言葉も生まれた(後述)
kent beck says
- 実は、TDDの話の大元のKent Beckさんも、著書の中で、さらっと大事な事を言ってる
TDDの皮肉の1つは、TDDがテスト技法ではない(カニンガム考案)ことである。 TDDは分析技法および設計技法であり、実際には開発のすべてのアクティビティを 構造化するための技法である。 (Kent Beck テスト駆動開発入門 P199)
TDDとテスト自動化
- 通常のテスト観点で一生懸命テスト自動化していく人達もいる。
- これらも間違えではない
- ただ、元々のTDDの話とはまた違う話。
- (違う話だが、この事をTDDと言う人もいる)
- ここに2つの話がある点に注意する必要がある。
- テストを自動化する話 = テストの話
- TDDの話 = 設計の話(結果としてテストが残る)
- 注意すべきは、test firstの話。これはTDDの話(設計の話)であって、テストの話ではない。(=> test firstでテスト観点でテスト書けない)
- ややこしい事に、現実、実際の現場のソースも両者ごっちゃで書いて行く事になると思う。
- テスト自動化は多くの場合(全てではないが)コストが高い。自動化のメリットがあるのか良く考えるべき
TDDとBDDの違い
- TDDからBDDへ
- TDDは冒頭で話した通り、テストと勘違いすると破綻する
- TDDは設計を意識するべきで、この誤解を解くためTDDのT(テスト)をB(Behaiver:振る舞い)に変えたものがBDD
- BDD
- TDDと比べて、書くべきソースに差はない
- 但し、上述の誤解の無いように、ツール側でソースやメッセージの中に振る舞いを前面に出して書けるようにしている。
- なお、ここで話しているのは、狭義な意味でのBDD。これは、TDDと同じレベルの話だが、ちなみに、広義な意味でのBDDの話もある。広義な意味でのBDDは、cucumberのようにstory形式で書くATDDの話とrspecのようにspecification形式で書くTDDの話と2つに分かれる。ここでは、TDDと比べるだけなので、狭義な意味のBDDしか扱わない。
- BDD後のTDD
- BDD登場後、TDD系のxUnit系ツールもBDDっぽく改良されつつある
- 振る舞い書く/ネスト出来る/実際の値と期待値の順番が自然言語と同じ等、BDDの特徴だった機能もTDD系のツールにも入ってきた。
- 従って、特に後発のTDD/BDDツールの機能の差は殆どない。言葉が違うだけ。mochaも同じ。
jsのUT周辺のTestFrameworks
ここ数年で主流が、コロコロ変わってきた。
- 数年前:QUnit
- 去年くらいまで:jasmine
- rails系とangular系はまだjasmine多い印象(angularはオリジナルなのだが)
- にしても、以前と比べて見なくなってきた。
今年あたり:mocha周りが熱かった
- アサーションライブラリは、chai/expect.js/should.jsを筆頭に幾つか分かれる
- mocha自体、とても汎用的(TDDもBDDもいけるの)で、拡張性も高いので、もうそろそろこの辺りで落ち着く?
ちなみに、DailyJSの2013surveyによると、まだjasmineが1位だった
- Jasmineが30%
- Mochaが27%
- QUnit 16%
どれを使う?
- 最近の新しいprojectで多いのは?
- やっぱりmochaが多い気がする
- 機能的には?
- mochaの非同期の簡潔さは大きい
- mocha自体は汎用的なので、どこまでもライブラリ追加して強力なツールに出来る。
- jasmineは一通り必要な物は揃ってる(All in One package)
- QUnitは大分機能が少ない(逆に覚える事も少ない)
- レガシーIEでtestできる?
- 学習コストは?
- QUnit < jasmine < mocha の順に学習量は多くなる。
TestFWを整理
あまりこだわりなければ、とりえあず、今はmochaで。
mocha
Mochaはnodeやブラウザ上で動かすjsのtest framework
- nodeやブラウザでjsを実行可能
- レポート出力も可能
- 遅いテストの検出等のテスト支援機能ある
- アサーションやテストダブルライブラリは別途準備が必要
メリデメ
- メリット
- styleを選べる(TDD or BDD/ mockライブラリも好きな物使える)
- 非同期のコードが綺麗
- デメリット
- 自分で別途ライブラリ組み合わせる必要あるという手間。
- しかも、今後、その別途ライブラリも廃り流行でてくるんだろうな(これは、特にjs周辺は仕方ないか)
アサーションが別ってどういう事?
そもそもアサーションとは
- そもそもアサーションとは、以下の例で言うと、検証部分の事(should.equal)
[1,2,3].indexOf(5).should.equal(-1);
- ここを別のライブラリに頼ってる
- ライブラリ毎に記述が違う
- = 記述を選べる
アサーションの選択肢
選択肢(の中から代表的な物)
- should.js:rspec(BDD)みたい
user.should.have.property('name', 'tj');
- expect.js:jasmine(BDD)みたい
expect(window.r).to.be(undefined);
- chai:柔軟に書ける(BDD/TDD)
- 上記、sholdやexpectいずれも使える。他に、xUnit(TDD)みたいな
assert.equal(foo, 'bar');
も書ける
- 上記、sholdやexpectいずれも使える。他に、xUnit(TDD)みたいな
- should.js:rspec(BDD)みたい
参考データ(2013/11/30現在の①github star数と②github更新状況)
- should.js
- ①1241 ☆
- ②昨日
- expect.js
- ①524 ☆
- ②半年前(更新停滞してる)
- chai
- ①870 ☆
- ②昨日
- should.js
現状、chaiが良さそう。(shouldはそもそもの問題もある※後述)
install
// 黒い画面から npm install -g mocha // htmlから使う bower install mocha
テスト実行
$ mocha
- 代表的なオプション
-R
レポート出力(後述)-g
マッチするテストだけ実行する-w
js変更を監視して実行する-d
デバッグモード(debugger
で停止、ステップ実行できる)--recursive
サブディレクトリ以下も実行する--reporters
使えるreporterを表示する
mochaの基本
- インタフェースはTDD/BDD選べる
- 幾つか見る限り、殆ど皆BDDで書いてる気がする。
- mochaの言うTDDとBDDの違い
- 冒頭の通りの事情で、単に表現の違い程度
BDD
// 公式ページより describe('Array', function(){ before(function(){ // ... }); describe('#indexOf()', function(){ it('should return -1 when not present', function(){ [1,2,3].indexOf(4).should.equal(-1); }); }); });
TDD
// 公式ページより suite('Array', function(){ setup(function(){ // ... }); suite('#indexOf()', function(){ test('should return -1 when not present', function(){ assert.equal(-1, [1,2,3].indexOf(4)); }); }); });
BDD style
- describe
- ネスト管理可能な階層。ディレクトリの様な物。
- it
- 検証内容を書く。
- before/beforeEach
- 前提条件
- after/afterEach
- 後処理
beforeとbeforeEach
- before
- その階層で一度しか実行しない
- beforeEach
- その下の階層でit毎に毎回実行される
'use strict'; (function () { describe('before test', function () { before(function(){ console.log('before'); }); beforeEach(function(){ console.log('beforeEach'); }); it('should show console.log', function () { console.log('first'); }); it('should show console.log', function () { console.log('second'); }); }); })(); // 以下の順番でconsole.log出力される // before => beforeEach => first => beforeEach => second
非同期コードの書き方
- itのfunctionの第1引数に用意されている関数を掴んでおく
it(‘should show’, function(done){
- 非同期のテストが終わったタイミングで上記の関数を実行する
done()
// 例 describe('User', function(){ describe('#save', function(){ it('should save', function(done){ var user = new User(‘taro’); user.save({}, {success: function(user) { expect(user.isNew()).to.be.not.ok done(); }); }) }) })
- beforeEachにも書く事が出来る
beforeEach(function(done){ var user = new User(‘yamada’); user.save({success: done}); })
Chai
アサーション用のライブラリ should形式/expect形式/assert形式から選べる
// 公式ページより // Should形式 chai.should(); foo.should.be.a('string'); // Expect形式 var expect = chai.expect; expect(foo).to.be.a('string'); // Assert形式 var assert = chai.assert; assert.typeOf(foo, 'string');
どれがいい?
should形式
- メリット
- 英語文法として自然なので設計を考え易い
- デメリット
expect形式
- assertよりはexpectが良いかも
- TDD=設計なので、語の選択として、assert(検証)よりexpect(期待)の方が仕様を書く事に意識がいく(といっても、俺ら日本人なのでそこまで感じないが)
- そもそも、mochaでassert書いてる人あんまり見ない。
という事で、expectオススメ
install
npm install chai
基本的な文法
イメージ
expect(対象オブジェクト).つなぎ目.演算子.期待値
つなぎ目(language chain)
可読性を上げるためのつなぎ目となるgetter。特にテストには関係ない。
- to
- be
- been
- is
- that
- and
- have
- with
- at
- of
- same
英語文法的に、expect to 〜に併せて、
expect(object).to.be.値という形が多い。
期待値
以降、見ての通り。補足必要な所だけコメント付けた。
// .ok expect('everthing').to.be.ok; // trueらしければtrue // .true expect(true).be.true; // trueの時のみtrue // .empty expect([]).to.be.empty; // .a(type) expect(‘test’).to.be.a(‘string’); // .string(string) expect('foobar').to.have.string('bar'); // .closeTo(expected, delta) expect(1.1).to.be.closeTo(1, 0.3); // 0.7から1.3までの値であれば、true!
演算子
// .not expect(foo).to.not.equal('bar'); // .equal(value) 厳密等価演算子(===)で確認 expect('hello').to.equal('hello'); // .eql(value) deeply equal。object等も再起的に値チェックしてくれる expect({ foo: 'bar' }).to.eql({ foo: 'bar' }); // .match(regexp) expect('foobar').to.match(/^foo/); // .exist (nullかundefinedではない事を検証) expect(foo).to.not.exist;
その他
// .throw(constructor) expect(fn).to.throw(ReferenceError); // .respondTo(method) Klass.prototype.bar = function(){}; expect(Klass).to.respondTo(‘bar’); //objectやclassがメソッド返すかチェック // .itself Klass.baz = function(){}; expect(Klass).itself.to.respondTo(‘baz’); // static functionを返すかチェック // .satisfy(method) 関数内でtrueになるかチェック expect(1).to.satisfy(function(num) { return num > 0; });
sinon.js
テストダブル(スタブ、モック等)を提供してくれるライブラリ。
テストダブルとは
テストダブルとは
Targetとなるソースが外部componentに依存している時に、
この外部コンポーネントに起因する何かしらの理由により、
テストが困難な時に、外部componentを置き換える仕組み。
※例:まだ、サーバーサイド出来ていないから、仮の関数に置換する等。
[ Target ] <= [ TestCode ] | [ 外部component ] ※これを置き換える
sinon.jsはspyとstubsとmocksを提供してくれる
spy
[ Target ] <= [ TestCode ] | <— spy 関数呼び出しを監視して記録 = 間接出力を保存 [ 外部component ]
spyは、存在する関数をラップして、関数の実行時の以下の情報を保持
- 引数の値
- 戻り値
- this
- 例外
なお、存在する関数をspyした時、元の関数は通常通り振る舞う。
test("test should call subscribers on publish": function () { var callback = sinon.spy(); PubSub.subscribe("message", callback); PubSub.publishSync("message"); assertTrue(callback.called); }
spy作成
// 基本形 var spy = sinon.spy(); // 既存の関数myFuncを置き換える var spy = sinon.spy(myFunc); // 既存のobject.methodを置き換える。 var spy = sinon.spy(object, “method”); // ※object.method.restore()でオリジナルメソッドに置き換えれる
spy API
// spyが呼ばれたらtrue spy.called // 1度でもspyが呼ばれたらtrue spy.calledOnce // spyが指定の引数で一度でもspyが呼ばれたらtrue spy.calledWith(arg1, arg2, …); // 一度でも例外を投げたらtrue spy.threw(); // 一度でも指定の値を返したらtrue spy.returned(obj);
プロパティ関係
// thisを返す。spy.thisValues[0]で1件目の呼び出し時のthisが入ってる。 spy.thisValues // 引数を返す。 spy.args // 戻り値を返す spy.returnValues
※spy.getCall(n)書くと、n番目に呼ばれたspyが帰って来る。ここからも、thisValueやargsを取得出来る。
stubs
次はスタブ
[ Target ] <= [ TestCode ] | [ stubs ] 代わりの値を返信 = 間接入力をset [ 外部component ]
stubsは作られる前の関数の振る舞いを定義するために使う。
spyと異なり、存在する関数をラッピングしていた場合、元の関数は呼ばれない。
例えば、強制的に例外を発生させたかったり、ajaxさせたくない時等で使う
test("test should call all subscribers, even if there are exceptions" : function(){ var message = 'an example message'; var error = 'an example error message'; var stub = sinon.stub().throws(); var spy1 = sinon.spy(); PubSub.subscribe(message, stub); PubSub.subscribe(message, spy1); PubSub.publishSync(message, undefined); assert(spy1.called); assert(stub.calledBefore(spy1)); }
stubs作成
var stub = sinon.stub(); var stub = sinon.stub(object, ‘method’); // stub関数で置き換えるobject.method.restore()で元の関数をrestore。 var stub = sinon.stub(object, “method”, func); // object.methoをspyでラップしたfunc関数で置換する
stubs API
// 与えられた引数の時だけメソッドをstubsする stub.withArgs(arg1 [, arg2, … ]); // 指定の戻り値を返す stub.returnsArg(obj); // 例外を返す stub.throws();
mocks
最後にモック
[ Target ] <= [ TestCode ] | [ mock 期待値と結果 => 検証 ※間接出力を検証する ] [ 外部component ]
モックはstubsの機能に加えて、検証する機能を持っている
もし検証で使わなければ、failになる。
// 公式ページより sinon.mock(jQuery).expects("ajax").atLeast(2).atMost(5); jQuery.ajax.verify();
mocks作成
var mock = sinon.mock(obj); // obj.methodをmock関数で上書き var expectation = mock.expects(‘method’); // 全てのmockメソッドをリストア mock.restore(); // 全てのexpectationsを検証 mock.verify();
Expectation
// 例 sinon.mock(jQuery).expects("ajax").atLeast(2).atMost(5); jQuery.ajax.verify();
var expectation = sinon.expectation.create([methodName]); var expectation = sinon.mock(); // 最低呼び出し回数 expectation.atLeast(number); // 一度でも呼ばれる事を期待 expectation.once(); // n回呼ばれる事を期待 expectation.exactly(number);
この後、
backboneでTDDする話をする予定なのだけど、この記事はここまで。
追記:この記事、未だに見て頂けているので、追記すると、 この記事書いた直後位で、jasmine2が出てきて、 非同期テストでdone書けるようになりました。 そのため、mochaの優位性が薄れてきました。 個人的に、mocha使う=chai使う=BDD使う=ランゲージチェインが気持ち悪く、 また、mocha使うと調べるときもchaiやらmochaやら色んなページ行き来しないといけないので、 最近は、jasmine2がオススメです。
Rest経由のparseを使うBackbone.Viewを自作する
parse.comはiOS/android/Backbone.js等の各clientが準備されているが、
Restの口も準備されているので、実質、どの言語からも叩く事が出来る。
公式pageの内容をまとめて試してみて、
最後にBackboneのViewに組み込んでPluginにしてみた。
そこまでのメモ
メリデメ
先にメリデメまとめると、
- メリット
- とにかく気軽(Backend全く準備が要らない)
- 1ヶ月100万requestまでは無料
- デメリット
- 少し気になる程、遅い。(後述)
- Parse固有の事がある(relation等)ので、これで書いてしまうと、ベンダーロックされる
- queryがmongoDB丸だしなので、これも書いてしまうと、mongoDBから抜け出すのが大変
いずれにせよ、単純なテーブル構成でとりあえず作って見た、的なもの
※ハッカソンや個人制作アプリやモック制作等。
で良ければ、これで十分。
※勿論、parseをproductで使う予定があるのであれば、そのまま作って行けば何も問題は無い。プロダクト事例もいっぱいあるしね。
http header
ヘッダにApplicationIdとRestApiKeyが必要
- それぞれparse.com上の設定画面からコピペしておく
X-Parse-Application-Id: Your App Id X-Parse-REST-API-Key: Your REST API Key
※urlのhostにBasic認証のID/パスワード書く形でも上記のIDとKeyを書けるみたい。
URL + HTTPメソッド
endpointのapi.parse.comに対して、以下のURLを組み立てる
http://api.parse.com/<version>/classes/<className>/<objectId>
上記の
< >
=自分で記述するparamater- version
- 今とりあえず、
1
でいいみたい。
- 今とりあえず、
- className
- Parse側で保存したいリソース名。テーブル名。要するにmongoDBのコレクション名。
- objectId
- Parseで管理している識別子。要するに、mongoDBのid。ある特定のリソースを指定したい時に、記述する。(次のHTTPメソッドに詳しく書く)
- version
HTTP メソッド
- 新規作成:POST
- objectId付けない。
- 取得:GET
- 全件取得はobjectId付けない。
- 1件取得はobjectId付ける。
- 更新:PUT
- objectId付けて、何を更新するか指定が必要。
- 削除:DELETE
- objecjtId付けて、何を削除するか指定が必要。
- 新規作成:POST
URL+httpメソッドの例
例えば、tasksというクラスに対するCRUD操作は以下になる。
# Create POST http://api.parse.com/1/classes/tasks/ # Read(全件取得) GET http://api.parse.com/1/classes/tasks/ # Read(1件取得) GET http://api.parse.com/1/classes/tasks/abCDefGHij # Update PUT http://api.parse.com/1/classes/tasks/abCDefGHij # Delete DELETE http://api.parse.com/1/classes/tasks/abCDefGHij
curlで試す
curlで試してみる
新規作成
curl -X POST \ -H "X-Parse-Application-Id: YourApplicationId" \ -H "X-Parse-REST-API-Key: YourRestApiKey" \ -H "Content-Type: application/json" \ -d '{"score":1337,"playerName":"Sean Plott","cheatMode":false}' \ https://api.parse.com/1/classes/GameScore
- もし、存在しないclassName指定すると、新規にclass自体を作ってくれる
- 但し、その時、結構、遅い(今やったら8secかかった)
- 既にあるclassNameであれば、1,2sec程(それでも遅いですね)
取得
- 以下で全件取得してみる
curl -X GET \ -H "X-Parse-Application-Id: YourApplicationId" \ -H "X-Parse-REST-API-Key: YourRestApiKey" \ https://api.parse.com/1/classes/GameScore/
- 例えば、2件同じデータを入れてみたときの出力結果
{"results":[ {"score":1337, "playerName":"Sean Plott", "cheatMode":false, "createdAt":"2013-12-13T13:24:07.037Z", "updatedAt":"2013-12-13T13:24:07.037Z", "objectId":"ilCXeuD0Iv"}, {"score":1337, "playerName":"Sean Plott", "cheatMode":false, "createdAt":"2013-12-13T13:24:32.211Z", "updatedAt":"2013-12-13T13:24:32.211Z", "objectId":"io5sO9G2gQ"}]}
- 1件取得したければ、上記のobjectIdをurlの最後のpathに入れる
更新
- 上記の一番上のデータを更新してみる
curl -X PUT \ -H "X-Parse-Application-Id: YourApplicationId" \ -H "X-Parse-REST-API-Key: YourRestApiKey" \ -H "Content-Type: application/json" \ -d '{"score":73453}' \ https://api.parse.com/1/classes/GameScore/ilCXeuD0Iv
- うーん時間かかる(2sec程)
- 更新をした後で、改めて、GETで、取得してみると、更新されてる。
- なんか、CRUDで更新が一番、時間かかるっぽい
- 何度も更新していくと、落ち着いてくるので、mongodbのinplace updateはみ出て、padding factor調整してるっぽい動き
BackboneでRestのparseを使う
ここからが、本題。
やりたい事
Parse.comでBackbone.js用のClientがあるのに、これがイマイチ。現実的に使えない。
やりたいのは、単純にBackbone.syncのバックエンドとしてparse使えればいいだけなのに。
parse付属のBackboneクライアントの何がイマイチなのか?
古いBackboneをforkして、かなり独自に実装してしまってる
- Backbone.Objectって?
- 中見るとBackbone.Modelから大分違うソースになってる
- validation周りの動きが変。
- Backbone.Syncすら無い。
- イベントsyncとかrequestとか動きが変(無い)
結論、これに別のプラグインとか突っ込んで行くと色々問題があって、結局使えない。
多分、あえて独自実装にして、ベンダーロックインさせたいんだろうけど、
出来れば、Backbone本体+αでどうにかしたい。
そうしたら、他のpluginも使えるし。
Backbone.Model内でどうにかする
という事で書いてみた
var Task = Backbone.Model.extend({ urlRoot: "https://api.parse.com/1/classes/tasks" }); var myTask = new Task(); myTask.set({"name": "jikken"}); myTask.save({}, { headers:{ 'X-Parse-Application-Id': ‘YourAppId’, 'X-Parse-REST-API-Key': ‘YourRestAPIKey’ }});
parseのRESTの使い方に沿って、単にヘッダー追加しただけ。
なお、ssl入れないと通らないので注意。
plugin化
毎回、こんなView書いて行ったら大変なので、
これをpluginとして作って見た。
https://github.com/lxyuma/backbone-parse-sync
Backbone.Viewを拡張してる。
使い方
githubに書いた通りだけど、
- parseHeadersにid/keyをセット
- Model/CollectionのuseParse:trueにすればいいだけ。
<script src="dist/backbone-parse-sync.min.js"></script> <script> // settings window.parseHeaders = { ApplicationId: "*****************************", RESTAPIKey: "*****************************" }; // Model TestModel = Backbone.Model.extend({ useParse: true, urlRoot: "/blogs" // this means using parse 'blogs' object }); testObject = new TestModel(); testObject.set({'name': 'test'}); testObject.save(); </script>
これで、各CRUD処理書くと、parseに対してRESTしてCRUDする。
本体ソース
短いので、貼付けるが、前に書いたソースと大体同じ様な感じで、
- ヘッダー追加してsyncして
- parseする時に、json調整してる
sync: function(){ if (this.useParse === true) { _.extend(_.last(arguments), { url: "https://api.parse.com/" + this.parseVersion + "/classes/" + _.result(this, "url"), headers: { 'X-Parse-Application-Id': window.parseHeaders.ApplicationId, 'X-Parse-REST-API-Key' : window.parseHeaders.RESTAPIKey} }); } return Backbone.sync.apply(this, arguments); }, parse: function(resp, options) { if (this.useParse === true && _.has(resp, "results")){ return resp.results; } else { return resp; } }
おしまい
まあ、クライアント側にkey/id貼付けてるのも気になるので、
お遊びや検証/学習やモック作成やハッカソン等で
上記plugin等使えればいいかと。
ImageMagickでjpegの縦横(exifのOrientation)情報を元に画像を回転する
やりたい事
元々、iphoneのsafariで、写真uploadできる機能を作りたかった。 ※ちなみに、iOS6からsafariでも画像up可
upする時に、jpegに入ってる重力の方向を元に自動で画像を回転して保存したい。
概要
この記事では以下の話を書いて行く。
- jpegのexif情報について説明
- ImageMagickの画像変換処理(auto_orient)で楽する
- iOSのsafariの特殊な動作
※ImageMagickのruby用のクライアントRmagick使うが、他言語も殆ど同じなんじゃないかと
jpegのexif
画像ファイルのフォーマットとして、exif(Exchangeable digicame-exif file format)規格がある。
Exifは一般的なjpeg/tiffファイルフォーマットに準拠している。
このExifには、タグが幾つか準備されていて、そこに、以下の様な情報が入ってる。(沢山あるので一部のみ)
- 画像の幅(imageWidth)
- 画素の並び(Orientation)
- 解像度(XResolution)
- メーカ(Make)
- 撮影者(Artist)
- exifdpointer(露出時間/F値/絞り/露出補正/シャッタースピード等の情報へのポインタ)
- GPSInfoIFDPointer(GPS情報へのポインタ)
※ちなみに、先日のPC遠隔操作事件で、真犯人からの猫の写真が江ノ島だと特定できたのは、このExif情報から、だそう。(link)
今から話すのは、上記、上から二つ目のOrientationの話。
なお、以下のサイトにExif規格についての詳しい説明がある。(参考にさせて頂いた)
Orientation
jpegの中のOrientationという属性に、一桁の数値が入っていて、
それぞれ、方向を示していて、以下の回転操作が必要。
- 1:そのまま
- 2:水平反転
- 3:180度回転
- 4:180度回転して水平反転
- 5:時計回りに90度回転して水平反転
- 6:時計回りに90度回転
- 7:時計回りに270度回転して水平反転
- 8:時計回りに270度回転
※水平反転が必要になるケースは、おそらく、自撮りする時に、左右反転してると思われる(未確認だが)
詳しくは、以下。
RMagickでorientation
RMagick(rubyのImageMagick用クライアント)を使ってOrientation見てみる。
image = File.open('/***/your.jpg').read m_image = Magick::Image.from_blob(image).shift # 自分で、exif属性ほじっても取れるが m_image.get_exif_by_entry("Orientation") #=> [["Orientation", "1"]] # orientationでも取得できる。Typeも分かり易いので、こっちが良い。 m_image.orientation #=> TopLeftOrientation=1 # ちなみに、ここで表示される型のクラスの値を見てみると、以下が定義されている Magick::OrientationType.values # => [ # UndefinedOrientation=0, # TopLeftOrientation=1, # TopRightOrientation=2, # BottomRightOrientation=3, # BottomLeftOrientation=4, # LeftTopOrientation=5, # RightTopOrientation=6, # RightBottomOrientation=7, # LeftBottomOrientation=8, # TopLeftOrientation=1, # TopLeftOrientation=1, # TopLeftOrientation=1]
回転を実装する。
さて、ここからが、本題。
皆、ガリガリ実装してるから、俺も書かなきゃ行けないのかと思ってた。
rubyでガリガリ実装して、ブラウザ上ではバッチリ動いた後、
iOS7のsafariで動かすと、なんか、方向がおかしい。
色々、試した結果、どうやら、iOS7は、safari上への画像表示時に、
exif情報を元に、画像を勝手に回転しているみたい。
※しかも、その時、一部のcssを無視してくれるので、表示が若干崩れる(^ ^ #)
なので、画像を回転した後に、ちゃんと、orientationを1にしないと、意図した表示にならない。
inner_img.orientation = Magick::OrientationType.values[1]
自力で書くなら、上記忘れずに。
※そもそも、facebookや他の大手サービスでは、セキュリティやプライバシの観点からExif情報を削除している。家で撮ったら家の緯度経度ももろばれにもなるので、そもそも削除するべき。
auto_orient
所で、調べて行くと、どうやら、rmagickには、auto_orientというメソッドがある。
これを使うと、勝手に回転してくれるらしい。(結局、ガリガリ自分で実装しなくて良かった)
m_image.auto_orient!
# auto_orientとauto_orient!(破壊的メソッド)がある。
どうやら、ImageMagickのオプションにある、--auto-orientと同じ動きをしてるっぽい。
※ImageMagick側のドキュメントによると、--auto-orientはresetもすると書いているので、上記のorientation=1も書かなくて良いかも(これは未確認)
まとめ
- iOSのsafariはOrientationを元に勝手に画像を回転してしまうっぽい
- 画像を回転したら、Orientationのresetを忘れずに
- そもそも、Exif情報には緯度経度等含まれるので、削除するべき
- ImageMagickにはauto-orientという自動で適切に回転してくれるオプションもある
以上。