LoginSignup
94

More than 5 years have passed since last update.

初めてのVue.js(jQuery脳からの移行をメインに)

Last updated at Posted at 2017-04-19

記事概要

jQueryベースでWebブラウザベースのアプリ書いている人がVue.jsに手を出すとしたら、「こんな風にやってみると分かりやすいかも?」と言う話。ES5 前提です。

Vue.jsの特徴として、他のフレームワークとの混在が容易、ってのがある。なのでjQueryとの共存も可能。今回の例では「inputタグなどの入力インターフェースにのみVue.jsを適用し、設定パネルのスライドダウン/アップなどの表示系DOMの操作はjQueryでのコードをそのまま流用」という方針で組んでみた。

「Vue.jsとは何ぞや?」は下記の公式ガイドを参照のこと。
https://jp.vuejs.org/v2/guide/

Vue (発音は / v j u ? / 、 view と同様)はユーザーインターフェイスを構築するためのプログレッシブフレームワークです。他の一枚板(モノリシック: monolithic)なフレームワークとは異なり、Vue は初めから少しづつ適用していけるように設計されています。

なお、当方はMVVMに対する理解が全力で不足の状態で書いているので、そこは留意のこと。

Vue.jsの考え方

Vueを適用したブロックタグ配下において、inputタグの内容をjavascript側に閉じて(タグ側のDOMとしての操作を意識することなく)操作できる。たとえば、inputタグを変数 var param にバインドしておけば、javascript側でparamの値に対してI/Oするだけでinputタグ側に勝手にVueが反映してくれる(※双方向か、一方向かも指定可能)。

なお、html側のタグ構造もVue側のcomponent 機能を利用することでVue側で管理できる様子だけど、今回は割愛。タグの構造はhtmlで直に書き下すこととする。

// 動作イメージ:

<input value="ほげ">
var input_str = "ふが"; // この変数へのI/Oだけでinputタグは勝手に追随する。

jQueryとの共存について

Vueを適用したいブロック(例:<div id="id_app"><div>)に対して new Vue({el: '#id_app'});でバインドする。バインドしたノード配下がVueの動作範囲になる。この外側でJQueryを使うことで共存が可能。なお、Vueの動作範囲ではイベント発火はVueがハンドリングするので、jQuery側からイベントのバインドはしないこと。

以下のような構成が可能。

<BODY>
<Vueが動作するブロック></>
<jQueryが動作するブロック></>
<jQueryが動作するブロック>
 <Vueが動作するブロック></>
</>
</BODY>

サンプルコード

以下の機能を持つWebページを作成してみる。

  1. inputタグに入力した文字列を「保存」ボタンでCookieに保存
  2. inputタグに入力した文字列を「追加」ボタンでプルダウンメニュー(selectタグ)に追加
  3. プルダウンメニューから項目を選択すると、inputタグに反映される。
  4. 次回のページ読み込み時に、「保存」した文字とプルダウンメニューは、自動反映される。

※コード中に「Azure」の文字があるのは、引用元がAzure関連のソースのため。このサンプルでは特に意味はないので無視すること(後でトリミングします)。

デモ画面

サンプルコードのデモはこちら

スクリーンショット

動作の様子はこちら。
設定パネルの開閉をjQueryで、値へのjQueryからのRead/WriteをVue.jsで実装。

スクリーンショット 2017-04-18 12.54.06.png

スクリーンショット 2017-04-18 12.54.01.png

前準備 - 利用モジュールのDL

以下のモジュールを利用する。

  • Vue.js
  • jQuery2.x
  • tiny-cookie

これらのうち、Vue.jsとjQueryはCDNを利用するのでダウンロード不要。tiny-cookieについては、Node.jsのnpmコマンドが使える人はnpm install tiny-cookieから、そうでない人は https://www.npmjs.com/package/tiny-cookie からGitHubを辿って取得する。ダウンロードしたtiny-cookieモジュールを、本サンプルでは./lib/tiny-cookie/tiny-cookie.jsに配置する。

htmlファイル+CSSファイル

ユーザーへの見せ方を構成する。inputやselectタグの配置もここで記述する。それぞれのタグに、JavaScript側のどの変数をバインドして、どのイベント(クリック)に反応させるか?もここで記述する(※JavaScript側に取り込む方法もあるっぽいけど今回は割愛)。合わせて、未入力状態の警告表示や入力後にボタン表示、などもここでhtmlタグで設定できる。

Vue.jsに基づいたバインド関連で、やっていることは以下。

  • 未入力時の警告表示用タグに、v-if属性をつけて表示トリガーを指定。
  • 未入力時の状態で非表示にしたいタグ(ボタン)に、v-show属性を指定。
  • 値をバインドしたいタグに、v-model属性を用いてバインドする変数名を指定。
  • クリック動作させたいタグに、v-on:click属性を利用して発火させる関数を指定。

なお、簡単のためCSSはhtmlファイル内に記述した。

index_qiita.html
<html>
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" id="id_viewport" content="width=device-width" >

    <style>
        .error {
            font-size: 10px;
            color: red;
            font-weight: bold;
        }
        input {
            width: 240pt;
        }
        select {
            width: 240pt;
        }
    </style>


    <!-- vue.jsの読み込み -->
    <script src="https://unpkg.com/vue/dist/vue.js"></script>

    <!-- cookie容易に扱うヤツの読み込み -->
    <script src="./lib/tiny-cookie/tiny-cookie.js"></script>

    <!-- vue.jsベースで描画 -->
    <script src="./vue_main_qiita.js"></script>


    <!-- jQueryとの共存OK~♪ -->
    <script src="https://code.jquery.com/jquery-2.2.4.min.js"></script>

    <!-- jQueryベースで描画 -->
    <script src="./ui_panel_qiita.js"></script>
    <script>
        $(document).ready(function(){
            var panelbutton = bindExpandCollapsePanel( "id_ctrl_panel" )
        });
    </script>
</head>
<body>
    <div id="id_ctrl_panel">
        <!-- ※以下のdiv配下のイベントはvue.jsに捉まれるので、jQueryから操作NG -->
        <div id="app">
            <div>
                ドメイン:
                <span class="error" v-if="!azure_domain_str">ドメインを入力して「保存」を押してください</span><br>
                <input v-model="azure_domain_str">
                <button v-on:click="add_azure" v-show="azure_domain_str">保存</button><br>
            </div>
            <br>
            <div>
                デバイスキー+表示名:
                <span class="error" v-if="!device_key_str">デバイスキーを入力してください。</span>
                <span class="error" v-if="!device_name_str">表示名を入力してください。</span>
                <br>
                <input v-model="device_key_str"><br>
                <input v-model="device_name_str">
                <button v-on:click="add_device" v-show="device_key_str && device_name_str">追加</button><br>
            </div>

            <div style="display:none;"><!-- // debugger -->
                <div id="id_azure_domain">{{ azure_domain_str }}</div>
                <div id="id_device_key">{{ device_key_str }}</div>
            </div>
        </div>
    </div>
    <hr>
    <!-- jQueryとの共存のために、↑↓別々のdivとして2つのvueインスタンスとして扱う。 -->
    <div>
        <div id="app_selector">
            <select v-model="selected" v-on:change="update_inputer">
                <option v-for="item in options" v-bind:value="item.value">
                    {{ item.text }}
                </option>
            </select>
            <button v-on:click="update_chart">実行(Alert)</button><br>
            <!-- // debugger -->
            <div style="display:none;"><!-- // debugger -->
                対応するValue⇒ {{ selected }}
            </div>
        </div>
    </div>
    <br>
    <br>
    <br>
</body>
</html>

JavaScriptファイル - Vue.js

Vue.jsを利用して定義する。new Vue({});で、引数にhtmlファイル側のidなどを利用して紐づけを指定する。生成したVueのインスタンスに対してデータの入出力(この例では、inputタグの内容をSelectタグに追加する、等)をすれば、勝手にhtml側にも反映される。html側を意識しなくてよい。便利♪

なお、Vueのインスタンス生成は「htmlのDOMが描画されてから」実施する必要がある。多くのVueの例だと、htmlタグを記述し終えた後に <script>で入れてるのは、そういう背景。今回の例では、window.onloadを利用して「描画後」にVueインスタンスを生成している。

※ソース中のコメントで「後でマージ」は、このサンプルでJQuery側でイベント処理している部分(ペインの開閉)もVue側に取り込めば、Vueのインスタンスを分ける必要がなくなり「マージできる」という意図。

vue_main_qiita.js
/*
    [vue_main_qiita.js]
    encoding=UTF-8
*/


window.onload = function(){
    var items = loadItems();
    var app = new Vue({
        el: '#app',
        data: function(){
            return {
            "azure_domain_str" : loadAzureDomain(),
            "device_key_str"   : "",
            "device_name_str"  : ""
            };
        },
        methods : {
            "add_azure" : function(event){
                saveAzureDomain( this.azure_domain_str );
            },
            "add_device" : function(e){
                add_selecter_if_unique( this, app2 ); // 後でマージする。⇒this1つになる。
                saveItems( app2.options ); // 後でマージする。⇒this.optionsになる。
            }
        }
    });
    var app2 = new Vue({ // jQueryとの共存の都合で分ける。
        el: '#app_selector',
        data: function(){
            return {
            // 以下はセレクター関連
            "selected" : "", // ここは初期選択したいvalueを指定する。
            "options" : items
            };
        },
        methods : {
            "update_inputer" : function(e){
                show_item_on_inputer( this, app ); // 後でマージする。⇒this1つになる。
            },
            "update_chart" : function(e){
                update_log_viewer( app ); // 後でマージする。⇒thisになる。
            }
        }
    });
    var add_selecter_if_unique = function( src, dest ){
        var list = dest.options, n = list.length, is_unique = true;
        while( 0<n-- ){
            if( list[n].value == src.device_key_str ){
                is_unique = false;
                break;
            }
        }
        if( is_unique ){
            dest.options.push({
                value : src.device_key_str,
                text  : src.device_name_str
            });
        }
    };
    var show_item_on_inputer = function( src, dest ){
        var selected_value = src.selected;
        var list = src.options, n = list.length;
        while( 0<n-- ){
            if( list[n].value == selected_value ){
                dest.device_key_str = list[n].value;
                dest.device_name_str = list[n].text;
                break;
            }
        }
    };
    var update_log_viewer = function( src ){
        var str = src.azure_domain_str + "\r\n";
        str += src.device_key_str;
        alert( "この先でjQueryベースの関数を呼び出すこともOK。\r\n\r\n" + str )
    };
};



/**
 * Cookieを利用したデータ保存。
 *
 */
var MAX_LISTS = 7;
var COOKIE_NAME  = "AzBatteryLog_Text";
var COOKIE_VALUE = "AzBatteryLog_Value";
var COOKIE_OPTIONS = {expires: 7};
var loadItems = function(){
    var cookie = window.Cookie;
    var list = [];
    var name, value, n = MAX_LISTS;
    while( 0 < n-- ){
        name = cookie( COOKIE_NAME + n );
        value = cookie( COOKIE_VALUE + n );
        if( name && value ){
            list.push({
                "text" : name,
                "value" : value
            });
        }
    }
    return list;
};
var saveItems = function( list ){
    var cookie = window.Cookie;
    var name, value, n = MAX_LISTS;
    while( 0 < n-- ){
        if( list[n] && list[n].text && list[n].value ){
            name = cookie( COOKIE_NAME + n, list[n].text, COOKIE_OPTIONS );
            value = cookie( COOKIE_VALUE + n, list[n].value, COOKIE_OPTIONS );
        }
    }
};
var loadAzureDomain = function(){
    var cookie = window.Cookie;
    return cookie("AzBatteryLog_Domain");
}
var saveAzureDomain = function( azureStr ){
    var cookie = window.Cookie;
    cookie("AzBatteryLog_Domain", azureStr, COOKIE_OPTIONS );
};

Javascriptファイル - jQuery

こちらでは、DOM操作によるビジュアルの変化をjQueryで実装する。
上部のバーをクリックするたびに、設定用ペインをスライドダウン/スライドアップさせる。

※おそらくは、Vue.jsだけでも(もしくは関連フレームワークで)このような視覚効果も実装可能と思われる。しかし、こういう視覚効果はJQueryの得意とするところなので、そのまま残すことで移行を容易とするのが今回の方針。

ui_panel_qiita.js
/*
    [ui_panel_qiita.js]
    encoding=UTF-8
*/


var bindExpandCollapsePanel = function( idPanel ){
    var BGCOLOR = "#77ffcc";
    var CSS_EXPAND = {
        "float" : "left",
        "line-height" : "0",
        "width" :  "0",
        "height" : "0",
        "border" : "10px solid " + BGCOLOR, /* transparent */
        "border-top" : "10px solid #000000",
        "padding-bottom": "0px"
    };
    var CSS_COLLAPSE = {
        "float" : "left",
        "line-height" : "0",
        "width" :  "0",
        "height" : "0",
        "border" : "10px solid " + BGCOLOR, /* transparent */
        "border-left" : "10px solid #000000",
        "padding-bottom": "0px"
    };    
    var str = "<div id=\"\_id_expand_collapse_base\">"
    str += "<div id=\"_id_marker\"></div>";
    str += "<div id=\"_id_expand_collapse\">設定パネル</div>"
    str += "<div style=\"float: none; both: clear;\"></div>"
    str += "</div>";
    $("#"+idPanel).before(str);
    $("#"+idPanel).hide();

    $("#_id_marker").css(CSS_COLLAPSE);
    $("#_id_expand_collapse_base").css({
        "margin" : "4   px",
        "padding" : "8px",
        "cursor" : "pointer",
        "backgroundColor" : BGCOLOR
    });
    $("#_id_expand_collapse").click(function(){
        if($("#"+idPanel).is(":visible")){
            $("#"+idPanel).slideUp();
            $("#_id_marker").css(CSS_COLLAPSE);
        }else{
            $("#"+idPanel).slideDown();
            $("#_id_marker").css(CSS_EXPAND);
        }
    });
    return $("#_id_expand_collapse");
};

以上ー。

補足(MVVMとは?など自己メモ)

MVVM(Model-View-ViewModel )とは何ぞや?MVCとMVPの違いは?、については、以下のWebサイトが分かりやすかった。

雑把の UI アーキテクチャー史(MVCからMVVMへ)|プログラマーズ雑記帳
http://yohshiy.blog.fc2.com/blog-entry-215.html

ところで、上記のサンプルコードでvar app = new Vue({}); してるところは、var app = factory.create({}); した方が良いのではないか?と思えてきた。せっかくU/Iからモデルを切り離しているのだから、モデルに対するテストのし易さを考慮しないともったいない。factoryのインスタンスを切り替えてあげれば、(、、ってVueというオブジェクト自体を置き換える手もあるか)好きなようにテストできるわけで。

参照にしたサイト様

jQueryから移行するならVue.jsが良さそう
http://tech.innovation.co.jp/2017/01/13/vue.html

Vue.js(ここが公式かな)
https://jp.vuejs.org/

フォーム入力バインディング - Vue.js(上記サイトにあるガイドの一部)
https://jp.vuejs.org/v2/guide/forms.html#%E9%81%B8%E6%8A%9E

体で覚えるVue.js - インスタンスメンバ編 〜 JSおくのほそ道 #024
http://qiita.com/hosomichi/items/ebbfcc3565bcd27f344c

tiny-cookie
https://www.npmjs.com/package/tiny-cookie

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
94