最近流行!JavaScriptでフロントエンドテンプレートを作ってみた
新年度、新学期、新メンバー、新体操ととにかく新しい事が多い4月の幕開けでございます。
個人的には鹿児島実業高校男子新体操を今年も推していきたいところですが、皆さまは春の訪れをどのように迎えましたでしょうか。
躍動する贅肉ことエンジニアの田中です。
最近様々な界隈から注目を集めているJavaScriptですが、今回はサクッと作ってみたJavaScriptのテンプレートを使って「フロントエンドテンプレート」の基本構造について考えてみたいと思います。
元々はWeb広告のテンプレート作成の案件をしている時に色々な事業者がテンプレート書き出しの構造を提供していたので、ちょっと自分でも書いてみたくなったのが発端です。
エラーハンドリングや環境依存をあまり考慮していないのでレガシーブラウザの方々はすいません><
とりあえず順番に書いてみる
フロントエンドエンジニアの方はもちろんの事、昔懐かしのレガシーコード愛好家の方はdocument.write();
の恐ろしさについてご存じかと思います。
loadイベントが実行された後documentがcloseした後にdocument.write()
を実行すると、勝手にdocument.open();
が実行されて画面が真っ白になるアレです。
その他にもレンダリングブロックされてしまったり、書き出しのタグのフォーマットが書きにくかったり、外部の会社に嫌味を言われたり、とにかく厄介なやつなのですが、実はWeb広告の世界では根強く残っている実装だったりします。
そこで今回は「あらかじめ書き出されたタグをテンプレートとして扱う」という事をしたいな~と思い諸々を考えてみます。
デフォルトで表示されないscriptタグを使う
scriptタグはブラウザのデフォルトの挙動として「display:none;」を充てられている事が多いです。HTML5の勧告でデフォルトのtypeは「text/javascript」ですが、こちらを適当に書き換えてしまえばJavaScriptとして実行される心配もありません。
今回は「text/tanaka-template」というとても恰好良い名前を使う事にしました。
とりあえず書いてみたテンプレートはこんな感じです。
1 2 3 4 5 6 7 |
<script type='text/tanaka-template'> <div align="center"> <p>{{TITLE}}</p> <p>{{DISCRIPTION}}</p> <img src='{{IMG_SRC}}' width='{{WIDTH}}' height='{{HEIGHT}}' /> </div> </script> |
普通にHTMLを書く感覚で書けるとても素敵な実装だと思います(自画自賛)。
テンプレートを取得する
コードがすべてを雄弁に語ってくれると信じて、あらかじめ書き出されたテンプレートを取得するscriptは以下のような感じです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
/** * テンプレートエレメントの取得処理 * @return {string} */ function() { var template, tmp; var type = 'text/tanaka-template'; if(this.opt.template) { template = this.opt.template; } else if(document.querySelector) { template = document.querySelector('script[type="' + type + '"]'); } else { template = document.getElementsByTagName('script').find(function(elm){ return elm.getAttribute('type') === type; }); } if(template.tagName !== 'SCRIPT') { template = null; } this.opt.template = template; return template; }; |
「document.querySelector」が使える環境だと書くのがとても楽ですが、そうでないならばぐるぐる回さないといけないですね。
テンプレート自体は何回も使いまわす想定でclass変数に突っ込む事にしました。
変数の取り込みについて考える
変数はテンプレートに当て込むために存在していると言ってもいいぐらいとても大切な機能だと思います。
ここの取り込みの機構についてJsonObjectで渡してkey名と同じプレースホルダーの箇所にvalueを当て込むみたいな事を想定しました。
ざっくりこんな感じですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
/** * テンプレートに変数を当てはめていく * @param {Element} template テンプレートのエレメント * @param {Object} arg 書き出しの変数とか諸々 * @return {Element} テンプレートを当て込んだ後のエレメント */ function(template, arg) { Object.keys(arg).forEach((function (key) { template = template.split('{{' + key + '}}').join(arg[key]); }).bind(this)); template = template.replace(/\n/g, ''); var tmp = document.createElement('div'); tmp.innerHTML = template; return tmp.childNodes[0]; }; |
テンプレート取得の時にscriptタグごと取得しているので中身のテンプレート部分は文字列として取得して置換していく事になります。
最後は出来上がったElementとして扱いたいのでdivタグに一度書き出してその中身を返す様に書いています。
この時に改行が入っているとうまくいかなかったので改行を置換する処理も入れました。
書き出し部分の機構を作る
あとは任意の場所に書き出せばいいだけですしElementとして取得しているので「appendChild」してあげるだけなので超簡単です。
1 2 3 4 5 6 7 8 9 |
/** * @param {Element} elm . */ function(elm) { var target; if(this.opt.target) { this.opt.target.appendChild(elm); } }; |
でもこれだとclassをnewしただけの状態の時に「その場に書き出したい!」みたいな需要に応えられないなと思いまして、以下のようにちょっと追加してみました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/** * @param {Element} elm . */ function(elm) { if(!this.opt.runFlg && !this.opt.target) { return; } var target; if(this.opt.target) { this.opt.target.appendChild(elm); } else { var script = document.getElementsByTagName('script'); target = script[script.length - 1]; target.parentNode.insertBefore(elm, target); } }; |
インスタンス生成時に書き出しを行うかどうかを「this.opt.runFlg」に持たせてfalseならば書き出しをしない!みたいな制御を想定しています。
「this.opt.target」にターゲットのElementが登録されていない時は今実行しているscriptタグを取得してそのタグの直前に書き出すみたいな感じです。同期読み込みじゃないと正しく動かないのがポイントですね。
オプションパラメータを取り込んだりとかする
classインスタンスを使う想定でインスタンス生成時の引数として内部オプションを渡す事と後からオプションの上書きが出来る機構を用意したいなと思いました。
この辺りはjQueryのプラグインとかでもよく見る構造なのであえて例示をしなくても良いかなと・・・。
プレースホルダ―の囲みタグや書き出し先の指定、テンプレートのタグなんかを引数として渡せるようにしました。
完成!
そんなこんなでさくっと作ったテンプレートエンジンはこちらのGitHubに公開しています。
READMEは書いてみたもののライセンスとか特に考えていなかったのでそのうちWTFPLのライセンス条項辺りを記載しようかなと思います。
圧縮に関してはわざわざtaskを作るのも面倒だったのでオンラインの「ClosureCompilerService」を使わせていただきました。
書き方に癖はありますが、圧縮率が他のツールと比べて圧倒的に高いので重宝しています。最低限の機能しかありませんがわずか約1.3KBのこのツールを使ってお勉強してみてはいかがでしょうか?
ちなみに過去記事の書き直しも少しずつ進めて行こうかと思っています。
以上歩く変態田中でした。