Edited at

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

More than 1 year has passed since last update.


記事概要

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で実装。


前準備 - 利用モジュールの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