@kyanny's blog

My life. Opinions are my own.

ペパボ社内の有志で JavaScript ソースコードリーディング勉強会をやった (3)

三回目。今回は SNBinder というテンプレートエンジンのライブラリを読んだ。

https://github.com/snakajima/SNBinder
https://github.com/a2chub/SNBinder

二つ目のレポジトリは README が和訳されてる fork で、使い方を把握するのにとても助かった。

このテンプレートエンジンの特徴は、まぁぐぐれば解説記事があるのでそっちを読めばいいんだけど一応書いておくと、サーバからデータだけじゃなくてテンプレート文字列も受け取ってしまおう、という点にある。テンプレートを受け取る関数のコールバック関数でデータを受け取り、そのまたコールバック関数で実際のレンダリングを行う(要するに innerHMTL へ代入してる)という。そしてさらに面白いのが部分テンプレートの仕組みで、 {%} というデリミタで区切られた所定のフォーマットのでっかい文字列を分解して、一部分だけをテンプレートとして扱える。つまりサーバサイドではすべての部分テンプレートを結合したでっかいテンプレート集合の文字列を返してやり、あとはクライアントサイドでパーツごとに分解して必要な部分だけ使う、という使い方を想定しているっぽい。これは面白いアイデアだ。例が $('body').html() と、 body 空でもいいんですよという感じになっているのも割り切っている。しかしなにぶんそういう特殊さがあるので、既存の js コードがたくさんあるところで一部分だけこれを採用する、とかだとかえって使いづらいかもしれない(サーバサイドからテンプレート返さなきゃ普通に使えるけどわざわざこれを選ぶ必然性は薄い)

  • 関数リテラルを定義して直後に実行するイディオム
var SNBinder = (function(){})();
  • cache, handlers の実体を外部から隠蔽する(クロージャ的なもの?)
  • return {} で返してるオブジェクトがグローバルネームスペースの SNBinder の実体となる
var handlers = {
  isDebug : function(){},
  ...
};
return {
  isDebug : function(){
    return handlers.isDebug();
  },
  ...
};
  • handlers と init
    • login とか error とかコールバック関数を初期化時にカスタマイズできる(コード読まないと使い方よくわからない)
  • キャッシュの仕組み (cache[url])
    • get の結果をキャッシュする(オブジェクトを key value store として使う)
    • return {} の外側で cache を定義しておいて、アクセス用メソッドからは直接 cache オブジェクトを触らせない(cache そのものにオブジェクト以外の値を代入されたりする問題を避けている?)
  • get_sections, get_named_sections
    • {%}
    • このライブラリの特徴的な部分
    • こんな感じでひとつの文字列のなかに {%} というデリミタで区切って部分テンプレートをまとめて書いておける
{%}header{%}
<header>$(.h)</header>
{%}footer{%}
<footer>$(.f)</footer>
    • {%}header{%} -> header という名前の部分テンプレート -> 実際のテンプレート文字列は
      ($.h)
      になる(get_named_sections のなかで dict (辞書)というオブジェクトに key value ペアで保存している)
    • .split('{%}').slice(1) -> 配列の先頭を切り落として残りを返す(なんかきもい、 shift と正反対の動きみたいな)
  • get
    • キャッシュの仕組み、キャッシュを無視する仕組みなど
    • ここでも関数リテラルを (function(){})() で定義即実行(なぜここでやる必要があるのか?変数のスコープ的な問題なんだろうか)
    • _attempt は setTimeout とか retry のときに再帰的に呼ぶ必要があるので普通の関数として定義したあと _attempt() で実行している
    • isJson フラグ(JSON として eval するかどうか)
  • post は get とほぼ同じでキャッシュ機構がないくらいの差
  • evaluate
    • JSON 文字列を eval してオブジェクトに戻してるだけ
    • こういう風にやるのかー
var obj;
eval("obj="+json);
    • try catch で eval できなかった場合のハンドリング(alert はどうなの・・・)
    • return {}; で空のオブジェクトを返す(呼び出し側が、この関数がオブジェクトを返すことを期待してるので、型エラーにならないようにしてる?)
  • escape
    • XSS とかならないようにタグと解釈される文字とかを文字参照実体参照に置き換えてる
    • エディタのシンタックスハイライト対策っぽいものがあった //'
  • compile
    • ここがテンプレート処理のキモ
    • _templatize と _func
    • _templatize はテンプレート文字列を受け取って記法の部分を文字列置換して '"<b>Hello, " + SNBinder.escape("" + row.$1) + "!</b>"' みたいな文字列を返す関数
    • row とかは変数なのであとで値が渡されたときに評価されてデータの値となる
    • _func は値を受け取ってさっきの置換されたあとのテンプレート式?文字列?を評価して返す関数
    • 実際の評価は eval でやっている
    • で _func を返すので compile は「テンプレート文字列を受け取って置換処理をして関数を返す。この関数にデータを渡して実行すると実際の評価が行われて値が埋め込まれた文字列が返る」ということだ
  • bind は _compile して _func の実行結果を返してるだけ
  • bind_rowset もだいたい同じで、データの配列を受け取り要素ごとにテンプレート評価をして最後に文字列連結して一個の大きい文字列を返す
  • ここまでで終わり!
  • __trailing__ : null がおもしろかった(IE でオブジェクトリテラルの最後のキーバリューの後ろにカンマがあるとエラーになるのでカンマの取り忘れ防止に意味のないキーバリューを一番最後に書いておくということ)
    • これを IE 作ってたひとがやっているあたりがシュールだ

なんか compile のあたりをもうちょっとわかりやすく説明できるようにならないと、ちゃんとは理解できてないんだろうなーという気がする。ぼんやりとはわかってる感じなんだけど、具体的にどの時点でどういう式?文字列?になっているのかが曖昧で、フロー図を穴埋めしろというテストをされたら答えられないと思う。

今回は行数も短いし、あんまりトリッキーな(みててハテナとなるような)ことはやってない印象を受けた。けっこう突っ込んでじっくり読んだけどきっちり一時間で読みきれた。上でぼんやりとかかいたばっかりだけど前回前々回に比べるとかなりちゃんと理解できたと思う。いままでのところ一番読み終えたあとの充実感があった。

最後に KPT

Keep

  • 開催できた
  • SNBinder を読み終えた

Problem

  • スケジュールを押さえるのに失敗した(でかい外部ディスプレイがある会議室がとれなかった)
  • 周知も不十分で、別件ありで参加できなかったひとが多かった

Try

  • スケジュール押さえやすくする -> 社内 SNS のグループを作ってもらったのでそっちでイベント立てる方式にする
  • 今後は社内 SNS のほうにも「まとめ」的な情報を集約していく(とはいえおれはここに書いてリンクはるとかにするとおもう)