技術をかじる猫

適当に気になった技術や言語、思ったこと考えた事など。

SPAするほどでもない時に気軽に使う JavaScript framework, Vue と Knockout 比較

ちょっと鯖欲しかったので Activator new で作って試す。
nodejs で勉強するチャンスだったかもしれないが、時間もなかったのでとりあえず知ってるやつで。

とりあえず、読めるようにしておく。
build.sbt

libraryDependencies ++= Seq(
  jdbc,
  cache,
  ws,
  "org.webjars" %% "webjars-play" % "2.5.0",
  "org.webjars" % "vue" % "2.1.3",          // これと
  "org.webjars.npm" % "knockout" % "3.2.0", // これ
  "org.scalatestplus.play" %% "scalatestplus-play" % "1.5.1" % Test
)

単純フォーム=画面バインディング

Knockout.js

    <div id="knockout-binding">
        <input id="k_text" type="text" value="" data-bind="textInput: ktext" />
        <p data-bind="text: ktext"></p>
    </div>

    <script type="text/javascript" src='@routes.Assets.versioned("lib/knockout/build/output/knockout-latest.debug.js")'></script>
    <script type="text/javascript">
        var myKoData = {
            ktext: ko.observable('Example')
        };
        ko.applyBindings(myKoData);
    </script>

まずは単純バインディング。テキストを変更すると、リアルタイムに p タグメッセージが変わる。

シンプルといえばシンプルだが、オブジェクトに「ko.observable」を用いているのが引っかかる。
これは、値を変更した際に更新をライブラリに通知する役割があり、それを受けて他のバインディング箇所を更新しているらしい。

ちなみに、一度バインドした画面内でもう一度 ko.applyBindings(myKoData) を実行するとエラーが発生する。

  • knockout-latest.debug.js:2786 Uncaught Error: You cannot apply bindings multiple times to the same element.

つまり、バインド時点で必要なデータは揃ってる必要がある。

Vue.js

    <div id="vue-simple-binding">
        <input id="v_text" type="text" value="" v-model="vtext" />
        <p>{{ vtext }}</p>
    </div>
    <script type="text/javascript" src='@routes.Assets.versioned("lib/vue/vue.js")'></script>
    <script>
        var myVueData = {
            vtext: "Example"
        };

        var vueData = new Vue({
            el: "#vue-simple-binding",
            data: myVueData
        });
    </script>

全く同じ仕様の動作実装。

Vue オブジェクトにデータを食わせるところが異なる。
knockout は画面全体に対するバインドだが、Vueel パラメータでバインドの範囲を制限する。

これは、画面の部品ごとに別々のバインドを実行できるということだろう。

存在しないオブジェクトをバインドし、後から値を設定する

動かないパターン。
これはオブジェクトの値が変化したことをコンポーネントが認識できない。

    <div id="knockout-binding">
        <input type="button" onclick="myKoData.after = 'example';" />
        <p data-bind="text: after"></p>
    </div>

    <script type="text/javascript" src='@routes.Assets.versioned("lib/knockout/build/output/knockout-latest.debug.js")'></script>
    <script type="text/javascript">
        var myKoData = { };
        ko.applyBindings(myKoData);
    </script>

エラー内容は、下記の通り。つまり、事前に設定したものしかバインドできないと言っている。

  • knockout-latest.debug.js:2890 Uncaught ReferenceError: Unable to process binding "text: function (){return after }"

動作可能なものは下記。事前にフィールドを用意しておくのだ。

    <script type="text/javascript">
        var myKoData = { after: ko.observable(""); };
        ko.applyBindings(myKoData);
    </script>

Vue.js

おおよそ想像はつくと思うが、こっちもエラーがでる。

    <div id="vue-simple-binding">
        <input id="v_text" type="button" onclick="vueData.vtext = 'Example';"  />
        <p>{{ vtext }}</p>
    </div>
    <script type="text/javascript" src='@routes.Assets.versioned("lib/vue/vue.js")'></script>
    <script>
        var myVueData = {};

        var vueData = new Vue({
            el: "#vue-simple-binding",
            data: myVueData
        });
    </script>

vue.js:515 [Vue warn]: Property or method "vtext" is not defined on the instance but referenced during render. Make sure to declare reactive data properties in the data option. (found in root instance)

こっちも事前にプロパティは用意しなければならない。
エラー内容が Vue の方が親切だ。

    <script>
        var myVueData = { vtext: null };

        var vueData = new Vue({
            el: "#vue-simple-binding",
            data: myVueData
        });
    </script>

配列にオブジェクトバインディング

二つのフレームワークは共に、事前にプロパティを設定して置かなければならなかった。
まぁわからない話ではない。

しかし、 ajax リクエストで遅延取得するようなデータのバインディングを処理できなければ用途は限られる。

knockout.js

    <div id="knockout-binding">
        <input type="button" onclick="myKoData.list([ { name: 'Example' } ]);" />
        <ul data-bind="foreach: list">
            <li data-bind="text: name"></li>
        </ul>
    </div>

    <script type="text/javascript" src='@routes.Assets.versioned("lib/knockout/build/output/knockout-latest.debug.js")'></script>
    <script type="text/javascript">
        var myKoData = { list: ko.observable([]) };
        ko.applyBindings(myKoData);
    </script>

結果は成功。
では要素の追加はどうか?

<input type="button" onclick="myKoData.list.push({ name: 'Example' });" />

に変更して行ってみるが、結果は失敗。

(index):19 Uncaught TypeError: myKoData.list.push is not a function

ko.observableを実行した際には、getter, setter 実装が追加されるため、そもそも論配列ではないのだという。
まぁ当然か。

定義を var myKoData = { list: ko.observableArray() }; に変更すると成功する。要は、knockout の独自定義配列だ。
では、さらにクエスチョン。

配列そのものの入れ替えは効くのか?というと、基本その手段は用意されないらしい。

ドキュメント | Knockout.js 日本語ドキュメント

Vue.js

    <div id="vue-simple-binding">
        <input id="v_text" type="button" onclick="vueData.list = [{ name: 'Example' }];"  />
        <ul>
            <li v-for="data in list">{{ data.name }}</li>
        </ul>
    </div>
    <script type="text/javascript" src='@routes.Assets.versioned("lib/vue/vue.js")'></script>
    <script>
        var vueData = new Vue({
            el: "#vue-simple-binding",
            data: { list: [] }
        });
    </script>

はい成功。
ボタン動作を push に変えてみる。

<input id="v_text" type="button" onclick="vueData.list.push({ name: 'Example' });" />

これも成功。

まとめ

どっちのフレームワークも、書き方に癖がある。

  • Knockout は独自オブジェクトを使うのが気になる
  • VueJS は、HTML 正式互換ではない attribute を使ってるのが気になる(HTML で任意属性は「data-XXXX」だけである)。

画面が単純であれば、どっちを汚すかの世界だと思われる。

一方で、画面に複数の独立したコンポーネントを定義したい場合、Knockout 側は部品ごとに独立して JS をおくことができない。
これは1画面1バインドでしか対応していないためだ。

もし、複数の定義が欲しい場合は、 コンポーネント 構文が必要とのこと。
割と大げさだ。
再利用せず、1画面内で複数の独立した部品の定義をするなら Vue の方がシンプルだろう。

Vue にもコンポーネント はあるが、これはあくまで再利用可能なコンポーネントを指す。

因みに、実行速度は、単純に書くとknockout の方が早い。これは、Vue は画面内の HTML を取り込んでメモリ内部コンパイルを実行するためだ。
事前コンパイルしておけば、そのロスは減るので、速度差はほぼ意識せずに済むレベルにはなる。
とはいえここも判断材料だろう。