Chrome 拡張の 2ch ブラウザを作った

クライアントで少しリッチなアプリが作れるようになりたいと思って、 chrome 拡張 で 2ch ブラウザを作ってみた。

f:id:hatz48:20131222133006p:plain

開発周り

Chrome 拡張は js/css/html での開発になるので、それぞれ TypeScript/less/underscore-template を Grunt でビルドするようにした。 サーバーサイドはないので、デプロイツール等はなし。 Grunt では本当はテストも走らせる予定だったのだが、まだテストが書いてない。。

クライアントサイドMVC

Backbone.js を使っていたのだけれど、いつくつかの理由から何も使わずに MVC(P?) っぽくかいてみることにした。基本的はに Backbone.js のやり方をまねて、Model の役割を一部サービスクラスに分離した感じ。

Service層導入の経緯

Backbone はいろんな書き方ができるので、Backboneが悪いとかじゃなくて自分の書き方がよくなかったのだろうけど、コントローラー同士のやり取りがうまく書けなかった。(Backbone.View をコントローラーとしてます)

あるコントローラーが管理しているDOMのイベントに応じて、別のコントローラーが管理しているDOMを変更する、というようなこと(例えばスレッド一覧をクリックされたときにスレッドの内容を新しいタブとして開く)をしたい場合どうするか。

  1. 上位のコントローラーに管理させる
  2. コントローラー同士に参照関係をもたせる
  3. コントローラー同士に Pub/Sub 関係をもたせる
  4. DOM の(カスタム)イベントを介してやり取りする
  5. Model のイベントを介してやり取りする
  6. URLを変更する

Backbone を使う場合こんな感じだと思う。1 は管理する範囲が大きくなるのがイヤで、2 コントローラー同士を疎結合にしたいのでイヤな感じ。3 はイベントの把握が煩雑になる。

4 のDOMを介すっていうのは悪くないと思うんだけど、コントローラー同士が同じDOMを購読している必要があるのでやっぱり1つのコントローラーの責務が大きくなりがち。

いろいろ挙げたけど実際にBackboneでやることが多いパターンは 5 か 6 だと思う。ただ URL を変更するやり方は Chrome 拡張ではうまくいかなかったのと、コントローラー同士のやりとりをするときに必ずURLを変更をするわけにもいかないだろうと思う。最後、model のイベント経由にしなかったのはなんでか。

Model のイベントを経由する -> しない

model はいろんな場所で参照されるので、 model がイベントの発行者だとどこでイベントが発行されるのか把握しづらくないだろうか。

// コントローラーAでmodelを購読
this.listenTo(model, 'change:hoge', this.onChangeHoge);
...
// どこかでmodelのイベントが発行される
model.set('hoge':'new value');

こんな感じでカジュアルにイベントが発行されるのだけど、この「どこか」がどこになり得るのか、極端な話 template の中ということもありえる。まぁそれはさすがにないだろうけど、規模が大きくなるほどイベントの流れが把握しにくくなる。

もう一つ、modelはインスタンス化される数が多い。つまりイベントの発行者が多いわけで、「どこで」「どいつ」がイベントを発行しているのかちゃんと把握するのがむずかしい。なんか二回実行されてる、みたいなバグがでたりする。

いやいや設計ちゃんとしろよ、みたいな話なのかもしれないけどそれより model をイベント発行者じゃなくすことを選んだ。

サービスクラスが何をするか

  • イベントの発行
  • IO 処理

IO 処理もサービス層の責務にした。IO完了時にイベント発行する必要があるからというのと、 どこからイベント発行されるのか把握しにくいのがイヤなのと同様に、IO 処理もどこから行われるのかわからないのはイヤだし、model にやらせるべきじゃないと思っている。

処理の流れ

  1. ユーザーの入力
  2. DOMがイベントを発行
  3. コントローラーが購読して、サービスにモデルを渡して処理を委譲
  4. サービスが処理をおこない、イベントを発行
  5. コントローラーがサービスのイベントを購読して、ビューを変更

コントローラーがビューを変更しているけど、Backbone.View みたいな感じ。 別にビューをつくってサービスを購読するようにすると良いのかもしれない。 モデルは基本的にデータを持つ/持っているデータを操作するだけになる。

まとめ

コントローラー同士の処理の流れがわかりやすくなって、規模が大きくなったときに全体が把握しやすい。 細かいところでダメなところはけっこうあって、例えばコントローラーがサービスに依存しているので、コンポーネント化がしにくそうとか、ポリモーフィズムがうまくない(model.fetch だとクラスごとの処理が行えるが、service.fetch(model) とする場合には fetch の中で分岐する必要がある)とか。コンポーネント化したい部分はサービスを使わずにDOMのカスタムイベントで完結するとよさそう。実際に jquery ui は自然に組み込めた。

Backbone.Event を拡張してここで言うサービスを実装すればいいので、別にBackboneを辞める必要はなかったかも。

あとこのやり方は実は URL を変更するやり方と同じで、Backbone.Router が URL を扱うサービスになってる。URLを扱う以外の部分もサービスにしたというだけの話だった。 Angular.js には Service というのがあるようなので勉強してみたい。 ネイティブのGUIアプリ作ったことないのでそっちも勉強してみたい。

ソースは github においてあります


2ch ビューワーとして

  • レスのポップアップ周りは適当なのでまだちゃんと作らないと見づらいし、タブ表示の切り替えがモッサリしていたりして実用するにはもう少しがんばる必要がありそう。
  • 最初は拡張じゃなくて Chrome packaged apps (Chrome apps になる前のやつ)で作っていたのだけれど、なぜか manifest が通らなくて拡張に変更した。packaged apps は古いからもう新規に登録できないとかなのだろうか??誰か知っている人いたら教えてほしい。