Web無料相談会2018冬
Web無料相談会2018冬
2016.11.14

JavaScriptとHTMLを分離するために。lodash.jsを使ってデータからHTMLを動的生成する方法

いなば

こんにちは、フロントエンドエンジニアのいなばです。

SPA (シングルページアプリケーション)やpjaxを使ったWebサイトなどはもちろん、静的なWebサイトでも、ページの遷移をせずにHTML要素を動的に生成するケースが多くなりましたね。

今回は、lodash.jsというライブラリのもつtemplate関数を使ってHTMLを生成する方法を紹介します。

静的サイトでよくあるHTMLを動的生成するケース

社内の案件のソースコードレビューの場でもよく出てくるのですが、
以下のようなケースで、オブジェクトなどのデータからHTMLを動的に生成したいことがよくあるのではないでしょうか?

  • clickされた要素によって、モーダルの中身を書き換える
  • 任意のタグやカテゴリなどで絞込む時に、一覧のHTMLを書き換える
  • ajaxで外部から取得したデータを使って、画面の一部を書き換える

昔ながらの HTMLを動的生成する手法

ひと昔前は、書き換えたい要素に対してHTML文字列をinnerHTMLするのが、HTMLを動的に生成する方法として一般的だったのではないかと思います。

var obj = {
  hoge: 'hoge',
  bar: 'bar',
  foo: 'foo'
};
var target = document.getElementById('target');
var html = '<ul>';
for (var key in obj) {
  html += '<li>' + obj[key] + '</li>';
}
html += '</ul>';
target.innerHTML = html;

最近ソースコードレビューした事例 A

clickした要素によってモーダルの中身を書き換えるケースです。
HTMLは属性値や中身を空にしてマークアップします。

<div class="modal" id="modal">
    <div class="modal-inner">
        <h1 class="modal-heading"></h1>
        <div class="modal-body">
            <img class="modal-thumbnail" src="" height="200" width="140" alt="">
            <dl class="modal-detail">
                <dt class="modal-detail-title"></dt>
                <dd class="modal-detail-author"></dd>
            </dl>
        </div>
    </div>
</div>

clickされた要素のdata属性を使って、モーダルの中身のHTMLの属性値や中身を書き換えます。

let $modal = $('#modal');
let render = (data) => {
    $modal.find('.modal-heading').html(data.name);
    $modal.find('.modal-thumbnail').attr('src', data.img);
    $modal.find('.modal-detail-title').html(data.detail.title);
    $modal.find('.modal-detail-author').html(data.detail.author);
};

$list = $('#js-list');
$list.on('click', '.js-modal-trigger', function(e) {
    e.preventDefault();
    let data = this.dataset;
    render(data);
    // この後にモーダルを開く処理を実行
});

最近ソースコードレビューした事例 B

こちらはajaxで取得したデータをもとにHTMLを組み立てるケースでした。
JavaScript側でHTMLテンプレートを持つ手法ですが、ES6のテンプレートリテラルを使うことでいくらかすっきりしていますね。

let render = (data) => {
    return `<li>
        <article>
            <a href="${data.url}">
                <div>
                    <div style="background-image: url(${data.image})"></div>
                </div>
                <div>
                    <div>
                        <h1>
                            <span>${data.title}</span>
                        </h1>
                    </div>
                </div>
            </a>
            <p>${data.publishAt}</p>
        </article>
    </li>`;
};

JavaScriptコード内にHTMLを持ちたくない

上の2つの例では、JavaScript側がHTMLの構造に依存しています。
HTML側の修正頻度は高いので、都度JavaScriptをいじらなくて良いほうが修正コストは低くなりますね。

また、HTMLとJavaScriptが分離できていると見通しがよいですし、JavaScriptがシンプルになります。

lodash.templateを使う

そこで今回は、HTMLとJavaScriptを分離するためにlodash.templateを使ってみようと思います。
今ではVue.js, Riot.jsなどViewを簡単に扱える便利なライブラリもありますが、ライブラリ自体のファイルサイズも小さくはないので、機能要件がそこまで複雑でない場合、自分はよくlodash.templateを使うことが多いです。

lodash.jsとは?

配列操作などの便利なメソッドが詰まったライブラリです。

https://lodash.com/

最近では、babelのプラグイン(babel-plugin-transform-runtime)やpollfilライブラリ(core-js)などでES6で追加されたネイティブの関数を先行して使うことができるため、lodash.jsが必要になるケースは今後なくなってくるかとは思います。

もっともシンプルな使用例

下記が、lodash.templateの最もシンプルな使用例です。

まず、scriptタグでHTMLテンプレートを定義します。
次に、lodash.template関数にテンプレートとして使用したいHTML文字列を渡したものを、変数(compile)にキャッシュします。
最後に、先ほどキャッシュしたcompile関数に任意のオブジェクトを渡すとHTML文字列が得られるので、HTMLを追加したい要素に対してinnerHTMLしています。

See the Pen QKmKPx by inaba (@s-inaba) on CodePen.

JavaScriptとHTMLを分離することができたので、HTML側で修正があったとしてもHTMLの修正だけで済みますね。

テンプレート内で使える構文

テンプレート内ではfor文やif文を使うことができます。

See the Pen gwZwJR by inaba (@s-inaba) on CodePen.

Docs

https://lodash.com/docs/4.16.2#template

EJS内でlodash.templateを使う

EJSのテンプレートの中でlodash.templateを使うと、デリミタがバッティングしてしまうのでEJSのコンパイルができません。
下記はデリミタを変更するコードになります。

See the Pen jrzrgv by inaba (@s-inaba) on CodePen.

Docs

https://lodash.com/docs/4.16.2#templateSettings

デリミタのデフォルトはソースコードのこのあたりにあります。

https://github.com/lodash/lodash/blob/master/lodash.js#L130

Ajaxと組み合わせる

QiitaのAPIを使ってクリックした要素のdata属性をもとに、特定のタグがついた一覧を取得してHTMLを生成するようにしてみました。

See the Pen gwZLYx by inaba (@s-inaba) on CodePen.

lodash.templateを使う時に注意すること

lodash.templateを使えばHTMLの生成が簡単におこなえますが、いくつか気をつけなくてはいけないことがあります。

SEO的に必要な要素のレンダリング

GoogleBotがJavaScriptを解釈できるようになりましたが、bingなどJavaScriptを解釈できないクローラーはまだあります。
これはVue.js、Riot.jsなどでも言えますが、最初から必要なHTML要素は極力サーバーサイド側でレンダリングするべきです。

動的生成した要素へのイベント設置

先ほどのajaxを使ったデモを少し修正して、生成した一覧の子要素にclickイベントがつくようにしてみました。

HTMLが書きかわる度に、一覧の子要素にclickイベントを設置していますが、これはよくないコードです。

See the Pen QKmGLB by inaba (@s-inaba) on CodePen.

イベントデリゲートを使いましょう

一覧の子要素が書きかわる度にclickイベントをつけるのではなく、一覧の親要素($target)にイベントをつけて監視するようにしましょう。

$targetの子要素(.js-item)が100個でも200個でもイベントの数は1つですみますし、HTMLが書きかわる際にイベントを削除する手間も不要になります。また、メモリリークを起こすリスクも下がります。

$($target).on('click', '.js-item', function(e) {
  e.preventDefault();
  let target = this;
  let id = target.dataset.id;
  alert(id);
});

終わりに

LIGのフロントエンドチームでは週に2回、立候補者がテーマを自由に決めて発表する社内勉強会を開催しています。

その会で行った案件のソースコードレビューで、HTML要素を動的に生成するケースがたまたま続き、また、社内のフロントエンドの間でlodash.templateの認知度があまりなかったので、社内向け勉強会で発表した内容をブログにしてみました。

Vue.js、Riot.jsなどを使うほどでもない要件のときの参考になれば幸いです。