Vue.jsでVirtual DOMを速習する

こんにちは。teratail開発チームでインターンをしている草間(@tkow)です。

jQueryはとても便利なライブラリで簡単なアプリケーションであればjQueryだけでも作れてしまうので、Web開発ではjQueryしか使ったことがない人も多いと思います。

しかし規模が大きくになるにつれて複雑になったロジックをjQueryのみで管理するのは大変です。また、selectorの一貫性を保つのが困難で、再利用が難しいコードが多くなります

そして生まれたのが、ブラウザのHTMLの表示部分と変更されるデータを結びつけておき、データの変更が生じるとそのデータが仕様されているHTMLの表示もすべて更新されるデータバインディングという概念と、その位置を特殊なタグによって管理することで埋め込まれたタグを再描画する位置と構造を記録するVirtual DOMという概念です。
近年では、React.jsが有名ですね。

今回は、軽量かつ学習コストが比較的小さいVirtual DOMライブラリVue.jsの使い方を自作のCheatSheetを用いて紹介します。

f:id:tkow:20161206141108p:plain

React.jsはjsxと呼ばれる、独自のテンプレート記法を用いて、Virtual DOMを実現していますが、Vue.jsは独自の記法を最低限に止め、カスタムタグ内の記述のほとんどが、既存のJavaScriptやHTMLの標準的な記法に沿っています。かつ、外部依存性のないライブラリなので、他のライブラリよりも比較的導入が簡単で、学習コストも少なめと大変便利なライブラリです。

また、Virtual DOMを使ったライブラリやフレームワークはほとんどが根本的な概念で共通しているので、Vue.jsを理解すればAngularやReact.jsなど、Virtual DOMを利用している他のフレームワークやライブラリを学ぶ時にも知識が活かせます

それでは、実際に基本的なVueの基本を説明していきます。

QuickTest

Create a new fiddle - JSFiddle

などのブラウザエディタを使う方も多いと思いますが,僕はライブラリを軽く試す時は、 data:text/html,<html contenteditable> をブラウザのURLバーに打ち込んでdevtoolsのスニペットで動作確認することが多いです。 (軽くじゃない時はエディタからサーバを立ち上げます)

//Chrome Snippetなどのツールでライブラリをロードして検証環境を作るスニペット
function loadCheck(url) {
  if (!url) return ;
  var scripts = document.getElementsByTagName('script');
  for (var i = scripts.length; i--;) {
    if (scripts[i].src == url) return false;
  }
  return true;
}

for(var url of ["https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.4/vue.min.js","https://code.jquery.com/jquery-2.0.0.min.js"]){
  if(loadCheck(url)){
    var script = document.createElement("script");
    script.setAttribute("src",url);
    document.body.appendChild(script);
  }
}

これをdevtoolsのコンソールで実行するとVue.jsがロードできます。

Vueクラス

new Vue()でVueインスタンスを作成します。VueインスタンスはVueコンポーネントを適用するセレクタの指定や、データバインディングしたいデータとDOMの設定を保存します。

Vue Lifecycle Event

  • Vueクラス自体が持つeventの発火タイミングはこの図が全てを表しています。イベント発火のタイミングが分からなかったら上の図を見れば解決します。それぞれのイベントに対して関数を設定できます。(よく使うのはmount関連とupdate関連のイベント)

  • mountedでイベント設定を行うことによりjQueryなどの他ライブラリのイベントトリガーをVueComponent構築時に設定できます。

データバインディング

  • data:Vueインスタンスに紐づくモデルのメンバ変数を指定します。変数名をkeyで、値をvalueで設定するとデータバインディングされます。連想配列でvalueを設定するなどプロパティのネストも可能です。data.styleやdata.classなどサーバ側に送信しないプロパティをオブジェクトで保持し、input要素などサーバへの送信が必要なデータ構造はprimitive型で保持するのが良いでしょう。これによってサーバサイドのモデルと共通なデータ構造を持つモデルをjsに受け渡しやすくなります。

  • methods:Vueインスタンスのinstanceメソッドを定義します。第一引数にeventオブジェクトがインジェクトされイベントハンドルされたDOMにもアクセスできるため、Vueコンポーネントに含まれないDOMのイベントリスナーに設定することで外部のDOMのプロパティに対してデータバインディングができます。関数の展開、eventオブジェクトを明示的に受け渡す方法についてはこちら

イベントハンドリング

  • watch:指定したプロパティのvalue変更時のコールバックを設定できます。

  • computed: keyに任意の関数名(ただし、data配下のプロパティ名と重複するとオーバーライドされてしまうため避ける)を設定し、valueに関数を指定します。この関数は、内部で使われているVueインスタンスのdataで設定されたプロパティを自動的にwatchし、それらのプロパティの変更時のコールバックとして発火します。複数のプロパティを同時に監視したいときはwatchよりcomputedを使うべきです。また、computed配下のプロパティは複数指定可能なので、関数名を変えることで同じ変数をwatchしていてもデータバインディングしたいDOMによってプロパティ更新時の動作を別々に設定することができます

DSL

ディレクティブ

ディレクティブ右辺のダブルクオテーション内はJavaScriptが式評価されます。 style属性など文字列の設定を混在させたい場合は、文字列部はシングルクオテーションで囲みます

//要jQuery
$('body').append(`<span id="test" :attr="a+n" :a="n+1" v-bind:class="'a'" :trans="trans">{{n}}</span>`);
var ins = new Vue({el:"#test",data:{n:1,a:"attr"},computed:{trans:function(){return this.a+String(this.n)}} });
//=> <span id="test" attr="attr1" a="2" trans="attr1" class="a">1</span>
ins.n++;
//=> <span id="test" attr="attr2" a="3" trans="attr2" class="a">2</span>

v-bind:(タグ属性)="(Vueプロパティ名)"

タグ属性の値をVueインスタンス値とバインドしたい時に使います。class,style,その他、何でも使えます。v-bindは省略可能です。(:class="hoge",:style="fuga")

v-model="(Vueのdataプロパティ名)"

input要素とVueに設定されているdataプロパティをバインディングすることでユーザー入力に応じてデータの変更を可能にします。

v-on:(イベント名).(オプション)="(Vueのmethodsプロパティ名)"

イベント発生時に実行される関数を設定できます。オプションにはpreventDefaultなどイベントオブジェクトのメソッドを簡潔に呼べるパターンが何種類か用意されています。@(イベント名)というシンタックスシュガーがありますがブラウザレンダリングされているDOMに対してはエラーになるので、使う際はトランスパイル前のテンプレート文字列に対してのみ使用できることに注意してください。

v-for

  • この属性を付けたタグは複製されます
  • data以下の適当なプロパティに配列または連想配列を入れておきます
  • 配列は(v,i) of、連想配列は(v,k,i) inでループできます
//要jquery
$('body').append('<div id ="repeat"><div v-for="(value, key, index) in object">{{key}},{{value}},{{index}}</div></div>');
new Vue({
  el: '#repeat',
  data: {
    object: {
      FirstName: 'John',
      LastName: 'Doe',
      Age: 30
    }
  }
})

mustache記法

インナーテキストはmustache({{}})記法でデータバインディングができます。

ディレクティブの追加

オリジナルのディレクティブを定義することもできます

Vueコンポーネント

基本

Vue.component('タグ名',{})で定義されたタグは、上位のVueインスタンスのel要素配下でに配置することで初期化時にtemplateに記述されているタグに置き換わります

Vue.component('custom',{
  template: '<div>test<div>'
});

/** before
  <div id="example">
    <custom><custom>
  </div>
*/

new Vue(
{'el':"#example"}
)

/** after
  <div id="example">
    <div>test<div>
  </div>
*/

Vueコンポーネントの連鎖

Vue.component('custom',{
  template: '<div>test<div>'
});

/** before
  <span id="example"></span>
*/

new Vue({
  el:"#example",
  template:`
    <div>
      <custom><custom>
    <div>
  `
});

/** after
  <div id="example">
    <div>test<div>
  </div>
*/

階層化したコンポーネント

Vue.component('child-child',{...});
Vue.component('child',{
  'template': //階層を積み重ねてUIを作る
   `<child-child></child-child>`
...
});
Vue.component('another-child',{...});
Vue.component('main-component',{
{ 'template':
  `<div>
    <span>コンポーネントではないタグは残る</span>
    <child class='default-class-set-ok'>
      defalutのinnerHtmlも残る(この下にchildの定義componentがrenderされる)
    </child>
    <another-child></another-child> //別のコンポーネントも共存できる
  </div>`
...
}
})
/**
 <span id="use-component"><main-component><main-component><span>
 new Vue({el:"#use-component"})
=>main-componentが連鎖的に置き換えられていく
*/

class,styleのつけ方

enable,disable時の設定

flagによってclassをつけるかつけないかを指定できます

デフォルトclassは保護される。またクラス付加の条件指定もできます

基本styleと,override

共通スタイルから一部をoverrideします(後続プロパティ優先)

componentの抽象化

propsに抽象化したプロパティを指定することができます。 これにより同じプロパティを持つComponentに対して使い回しが可能な子Componentが作成できます

参考

Vue.component('abstract-person', {
  props: ['person'],
  template: '<li>{{ person.name }}</li>'
})

/** before
  <span id="customer"></span>
  <span id="candidate"></span>
*/

new Vue({
el:'#customer',
template:`
  <ul>
    <abstract-person v-for="item of customers" v-bind:person="item">
    </abstract-person>
  </ul>`,
data:{"customers":[{name:"Alice"},{name:"Bob"}]}
})

new Vue({
el:'#candidate',
template:`
  <ul>
    <abstract-person v-for="item of candidates" v-bind:person="item">
    </abstract-person>
  </ul>`,
data:{"candidates":[{name:"Tom"},{name:"Sam"}]}
})

/** after
  <ul><li>Alice</li><li>Bob</li></ul>
  <ul><li>Tom</li><li>Sam</li></ul>
*/

全体における注意点

  • スコープが狂うのでVueインスタンス及びVueコンポーネントのプロパティを設定する際にラムダ式を使ってはいけません。

  • Vueを使ってUIを作成する場合は必ずid,class,タグによってコンパイルを実行するルートのタグをVueインスタンス初期化時に指定する必要があります。コンポーネントを使用する際にelementに指定するタグを置き替えたい場合は、vueインスタンスの初期化でtemplate要素にカスタムタグを設置しましょう。

  • new Vueのtemplateでカスタムタグをコンポーネントに置き換える場合v-forが設定されているコンポーネントを最上位にして置き換えようとするとエラーが発生します。動的なDOM追加の最上位の親は必ず一つであることが必要です。

  • new Vueのセレクタはハッシュのelキーで渡すのに対し,Componentは第一引数にいれるのでちょっと紛らわしいので、忘れているとはまってしまうことがありそうなので注意しましょう。

  • ディレクティブの右辺のダブルクオテーションの中はJavaScriptの式評価が行われるため、演算子やシングルクオテーションで囲むことで文字列+Vueによって設定されるDSLが使えます。またカレントスコープがVueインスタンス内部になるためdata,methods,computedプロパティは名前空間の指定なし(thisもいらない)で呼び出せるようになります。ダブルクオテーション内でマスタッシュ記法({{}})を呼び出せないのは、この仕様と分離させるためかもしれません。

まとめ

今回はVue.jsの基本的なUI作成に関して必要な知識をざっくりご紹介しました。

Vue.jsではこのように、DOM,Style,Model,Scriptをコンポーネントという単位で作成することによって互いの処理が干渉しづらい疎結合なUIを作成することができます。まだまだ紹介しきれていない機能(.vueファイルについて等)も多いので、その話はまた機会があれば書きたいと思います。

それでは次回もteratailブログをよろしくお願いいたします。