Vue.jsのcomponentを使ってみた


最近結構流行ってきているようなので、Vue.js使ってみました。

私はangularJSはあまり好きになれず、backbone派なJSエンジニアですが、ちょっとした小規模なWebアプリを作成する際にbackboneは少し煩わしいところがあるのは確かです。

vue.jsはangularをシンプルにしたものというイメージで、学習コストも低く、公式のドキュメントに掲載されているガイドは英語ですが1日あれば全て読めると思います。

これを使えば小規模なwebアプリの開発を効率化できそうな予感がしたので、使用してみました。(MVCとかMVVMとかそういうことを語るつもりはありません。)

余談ですが、私がaugularをあんまり好きではないのは、その豊富なディレクティブの機能が故にJSPを彷彿させたからです。
前職では結構JSPを書いてて、メンテナンスの面でかなり大変な思いをしたからです。もちろんシンプルなロジックをテンプレート側に組み込むことはコード量を劇的に減らせる可能性があるため賛成ですが、あまりに複雑なロジックをテンプレートにもってしまうと、ロジックが分散されメンテナンス不能なコードになりやすいので注意が必要です。

ところがどっこい、vue.jsの一通りの機能を使ってみてると、その直感的なシンプルさ故にこちらはaugularとは違って好きになれそうな気がしました。

今回はv-componentを使用した例をとりあげてvue.jsをの使いやすさをご紹介しようと思います。

1. 準備
installはbowerで以下を一発。scriptを適宜読み込む。
bower install vue --save

以下、2, 3, 4ではリスト形式のコンポーネントを親とし、リスト内の一つずつのアイテムを子componentとしてv-repeatを用いて表示する例です。

2. 親vueの定義

new Vue({
    el: '#app',
    data: {
        isInitialized: false,
        result: {list: []}
    },
    created: function() {
        var that = this;
        $.ajax({
            type: "GET",
            url: "/api/something/list",
            dataType: "json",
            success: function(list) {
                // 成功時の処理としてリスト形式の結果を代入する
                // e.g. [{"title": "hoge", "text": "hogehoge"}, ...]
                that.result.list = list;
            },
            complete: function() {
                that.isInitialized = true;
            }
        });
    },
    methods: {
        showDetail: function(id, $e){
            // アイテムがクリックされた時のハンドラ
        }
    }
});

実際のアプリを想定して、リストで表示する用のデータは非同期にjsonで取得します。親のVueのcreated中でxhrします。($.ajaxはjqueryでもzeptoでも良い)

今回の例では初期化時にデーターがないので(非同期でデータを取得しているから)、モデルの中で空のresultとresult.listを定義し、初めのレンダリング時の空オブジェクトに対するbinding実行を防いでいます。この空オブジェクトの定義が無いままHTML上で.(ドット)アクセスしてしまうと、warningがコンソール上に出ると思います。

3. 子vue componentの定義

    Vue.component('result-item', {
        template: "#result-item",
        created: function(){
            if(!this.$parent.isInitialized){ return; }
            // HTMLで親がresult.listをv-repeatに指定した際に、listの一つのアイテムがこのコンポーネントのモデルになる
            this.contentHTML =  '<span>' + this.text + '</span>';
        }
    });

repeatされる子要素となるcomponentのcreatedは親のcreatedの直後にまず一回呼ばれます。それは、既に子componentのdataに値があればbindingされているviewを更新していくためのものです。
親のモデルのresult.listを代入すると、そのタイミングで自動的にviewが更新されるため、repeatごとに子componentが生成されていきます。

今回は、最初の親が出来上がったタイミングで呼ばれるcreatedもしくはcomputedでレンダリング処理は行う必要は無いので、データが追加された時にのみ処理を行わせるように”this.$parent.isInitialized”と親のモデルをチェックしています。

4. v-componentとv-repeatを使用したHTMLの定義
html内では各コンポーネントのモデルにドットでアクセスして、ビューとのbindingをしていきます。

vueの一番の肝はここで、template内で対応しているVue modelにドットアクセスで指定することでバインディングが自動的にされていきます。また、今回は詳しくは説明しませんがcomputedを使うことで、あたかも通常のモデルの1プロパティとして、あらかじめ定義したgetterとsetterを呼ぶこともできます。filterもとても使えるので気になる方はvuejsのドキュメントへ。

    <div id="app">
        <div v-show="result.list.length">
            <ul>
                <li v-on="click: showDetail('#/detail/'+ $index, $event)" v-component="result-item" v-repeat="result.list"></li>
            </ul>
        </div>
    </div>
  <script id="result-item" type="x-template">
     <span>{{title}}</span>
     {{{contentHTML}}}
  </script>

上記の例で、親コンポーネントをリストとし、v-componentで子コンポーネントを一つのアイテムとして指定し、v-repeatでiterateしています。親のinitialize時に非同期にリスト形式のデータがロードされると、そのタイミングで自動的にビューが更新されていきます。
※{{{contentHTML}}}と三つ括弧でくくるとHTMLがescapeされません。使用する際にはXSSに注意が必要です。

以上、vue.jsを使った例ではコードがとても簡潔になり、直感的で期待された動作がされているのではないかと思います。

directorjs等を使ったルーティングの機能を取り込むと、少し複雑なアプリケーションでもこのような形で簡潔なコードで書くことができます。
あと、このcomponentの機能はweb componentを見据えて設計されているようで、polyfills無しで使えるところを売りにしているみたいです。

あとは個人的にこのようなbindingがどのように実装されているのかも気になります。基本的にはhtmlをパースして、キャッシュしたnodeに対して、独自のフック用のgetter/setterをモデル変更時に適用しているような形だと思いますが。これらについてはまた次回ですかね。