mixiグループ Advent Calendar 2016の16日目の記事です。
はじめに
チケットフリマNo1.チケットキャンプを運営する株式会社フンザでは、新サービスを絶賛開発中です。
新サービスをローンチするにあたって、集客の面からSEOを無視できないことや、SPA+サーバサイドレンダリングは敷居高い(サーバサイドがDjangoフレームワークのため)などの理由から、MPAアーキテクチャで構成されています。
その際にフロントエンドのライブラリとしてVue.jsを採用しました。しかしながら、Vue.jsはSPAでの事例に比べ、MPAの例は少なく、苦労することがあったので、その知見をまとめました。
コンポーネントにデータをどう渡すのか?
下記のような方法で実装しました。
- ページ読み込み時にグローバル変数にJSONデータを代入する
- その変数をコンポーネントのdataに定義する。
- コンポーネントのdataから変数をレンダリングする
入力フォームの実装例
<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>
new Vue({
data: DATA,
methods: {
validate () {
if (!parseInt(this.price, 10)) {
this.error = '金額を入力してください'
}
}
}
})
ネストしたオブジェクトがバインドされない問題
実際はオブジェクトをネストした状態で渡ることもある
以下の例は、先の例とは異なり、入力してもエラーが表示されません
具体例
<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>
new Vue({
data: DATA,
methods: {
validate () {
if (!parseInt(this.form.price, 10)) {
this.errors.price = '金額を入力してください'
}
}
}
})
解決法
dataに初期値を定義すると動作するようになります。
バインドしたい変数はあらかじめ定義する必要があります。
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アプリケーションでは、
必要になることがあるかと思います。
今回は、オーバーレイ表示でその下のスクロールを抑止する方法を紹介します。
<body :class="classes">
<div v-if="isOpeninigOverlay">
オーバーレイ表示する
</div>
</body>
body.scroll-disable {
height: 100%;
overflow: hidden;
}
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属性を定義するとどうなる?
<div is="custome" id="hoge_id" class="hoge">
<span>hoge</span>
</div>
Vue.component('custom': {
template: '<div class="fuga"><slot></slot>fuga</div>'
})
結果: 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開発においてはアリだと思います。