Vue.jsをMPAで導入してみた

[mixiグループ Advent Calendar 2016](http://qiita.com/advent-calendar/2016/mixi)の16日目の記事です。

# はじめに
チケットフリマNo1.[チケットキャンプ](https://ticketcamp.net/)を運営する株式会社フンザでは、新サービスを絶賛開発中です。
新サービスをローンチするにあたって、集客の面からSEOを無視できないことや、SPA+サーバサイドレンダリングは敷居高い(サーバサイドがDjangoフレームワークのため)などの理由から、MPAアーキテクチャで構成されています。
その際にフロントエンドのライブラリとしてVue.jsを採用しました。しかしながら、Vue.jsはSPAでの事例に比べ、MPAの例は少なく、苦労することがあったので、その知見をまとめました。

# コンポーネントにデータをどう渡すのか？
下記のような方法で実装しました。
1. ページ読み込み時にグローバル変数にJSONデータを代入する
2. その変数をコンポーネントのdataに定義する。
3. コンポーネントのdataから変数をレンダリングする

## 入力フォームの実装例
```form.html
<form>
  <input type="number" name="price" v-model="price" @change="validate">
  <div v-if="error">{{ error }}</div>
</form>

<!-- サーバサイドでHTMLを生成する際に必要なデータをJSONで渡す -->
<script>
var DATA = {
  price: 0,
  error: '金額を入力してください'
};
</script>
```

```form.js
new Vue({
  data: DATA,
  methods: {
    validate () {
      if (!parseInt(this.price, 10)) {
        this.error = '金額を入力してください'
      }
    }
  }
})
```

# ネストしたオブジェクトがバインドされない問題

実際はオブジェクトをネストした状態で渡ることもある
以下の例は、先の例とは異なり、入力してもエラーが表示されません

## 具体例
```form.html
<form>
  <input type="number" name="price" v-model="form.price" @change="validate">
  <div v-if="errors.price">{{ errors.price }}</div>
</form>

<!-- サーバサイドでHTMLを生成する際に必要なデータをJSONで渡す -->
<script>
var DATA = {
  form: {},
  errors: {}
};
</script>
```

```form.js
new Vue({
  data: DATA,
  methods: {
    validate () {
      if (!parseInt(this.form.price, 10)) {
        this.errors.price = '金額を入力してください'
      }
    }
  }
})
```

## 解決法
dataに初期値を定義すると動作するようになります。
バインドしたい変数はあらかじめ定義する必要があります。

```form.js
new Vue({
  el: 'body',
  data: {
    form: Object.assign({'price': ''}, DATA['form']),
    errors: Object.assign({'price': ''}, DATA['errors'])
  },
  methods: {
    validate () {
      if (!parseInt(this.form.price, 10)) {
        this.error = '金額を入力してください'
      }
    }
  }
})
```

# DOMを操作したい
なるべくDOMを操作したくはないものですが、スマートフォンに対応したWebアプリケーションでは、
必要になることがあるかと思います。
今回は、オーバーレイ表示でその下のスクロールを抑止する方法を紹介します。

```overlay.html
<body :class="classes">
  <div v-if="isOpeninigOverlay">
    オーバーレイ表示する
  </div>
</body>
```
```overlay.css
body.scroll-disable {
  height: 100%;
  overflow: hidden;
}
```

```overlay.js
const app = new Vue({
  el: 'body',
  data: {
    classes: [],
    isOpeninigOverlay: false,
    tempScrollTop: 0
  },
  watch: {
    isOpeninigOverlay (willOpen) {
      if (willOpen) {
        // スクロールがリセットされてしまうので、スクロール位置を保持する
        this.tempScrollTop = document.body.scrollTop
        // 下のクラスを適用するとbodyが縮まる
        this.classes.push('scroll-disable')
      } else {
        this.classes.$remove('scroll-disable')
        document.body.scrollTop = this.tempScrollTop
      }
    }
  }
})
app.isOpeninigOverlay = true
```

ポイントは`watch`を使うことで、フラグの値を変更するだけで、DOM操作を行うことができ、かつ必要な処理を一箇所にまとめられることです。

# templateを使用したコンポーネントはどういうDOMになるのか？

コンポーネントのDOMツリーが推測できないと、CSSを定義する際に困りますが、
ドキュメントにはそういった記述がないので、試してみました。

## マウントポイントとtemplateでclass属性を定義するとどうなる?

```dom.html
<div is="custome" id="hoge_id" class="hoge">
  <span>hoge</span>
</div>
```

```js:dom.js
Vue.component('custom': {
  template: '<div class="fuga"><slot></slot>fuga</div>'
})
```

結果: DOMの定義がマージされる

```html:DOM
<div is="custome" id="hoge_id" class="hoge fuga">
  <span>hoge</span>fuga
</div>
```

# 総括
## MPAであってもSPAでのベストプラクティスは活かせる
Ajaxで取得したデータもコンポーネントのdataに格納したり、入力値をバインドしたりと、コンポートネントを単一のストアとして扱うような実装になりました。
結果としては、React+Flexに近い構成になり、最終的にはコードの見通しが良くなりました。

子コンポーネントにデータを渡す場合も、当初はコンポーネントのpropsにJSONで渡していたが、親コンポーネントを経由してpropsで子コンポーネントにデータを渡す実装に変更しました。

## Vue.jsはMPAに適している?!
Vue.jsはReact.jsなどのライブラリと比較して、DOMの断片にコンポーネントを適用できる点において、MPAに適している印象です。
jQueryなどのライブラリでも同様のメリットは享受できますが、コードが肥大化していくとメンテナンスが厄介なため、モダンなMPA開発においてはアリだと思います。