23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Posted at

mixiグループ Advent Calendar 2016の16日目の記事です。

はじめに

チケットフリマNo1.チケットキャンプを運営する株式会社フンザでは、新サービスを絶賛開発中です。
新サービスをローンチするにあたって、集客の面から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>
dom.js
Vue.component('custom': {
  template: '<div class="fuga"><slot></slot>fuga</div>'
})

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

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開発においてはアリだと思います。

23
20
0

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
23
20

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?