前回の記事はこちらです。先に読まれておくことをオススメします。
モデルに置く値、ViewModelに置く値の区別をつけよう
Vue.jsで開発するときに、モデルに置くべき値とViewModelに置くべき値を区別できればかなりキレイに設計できます。 区別の判断は少し難しいですが、「バックエンドのAPIで保存・読み込みする値のみモデルで管理する」と考えてみると勘所が分かりやすいです。 バックエンドのAPIなどが無い場合は、例えばlocalStorageに保存する値をモデルとすると良いでしょう。
前提: 記事の読み込み、保存する画面で説明します
説明のためのプロジェクトの説明をします。
記事(Post)の編集をする画面をVue.jsで作り、バックエンドのAPIからデータを取得、保存するとしましょう。 ここではAPIから以下のレスポンスがあると想定します。
{ "id": 1, "title": "タイトル", "body": "本文" }
この場合、APIのレスポンスはモデルで管理すべきです(参考: JavaScript (ES6) でAPIから受け取ったデータをモデルに入れるプラクティス - Make組ブログ)
export class Post { constructor (id, title, body="") { this.id = id this.title = title this.body = body } }
読み込み、保存する処理は以下のようにします
import axios from 'axios' import { Post } from './api' export async function getPost (id) { let res = axios.get(`/posts/${id}/`) return new Post(res.data.id, res.data.title, res.data.body) } export async function patchPost (post) { let res = axios.patch(`/posts/${post.id}/`, {body: post.body}) post.body = res.data.body return post}
単純にVueコンポーネントを作る
さて、ここでどのようにVueコンポーネントを書くべきでしょうか? 一番単純に作ると以下のようになります。
<template> <div v-if="post"> <textarea v-model="post.body"></textarea> <button @click="save">Save</button> </div> </template> <script> import * as api from './api' export default { name: 'Post', data () { return { post: null } }, methods: { async save () { await api.patchPost(this.post) } }, async mounted () { this.post = await api.getPost(1) } } </script>
モデルに書く?
ここで「値が変更されていないときは Save
ボタンを disabled
にしたい」としましょう。
この場合、値が変更されたことを検知するのはどこが良いでしょうか?
せっかくJavaScriptで class
が使えるので、以下のように getter, setter で書くとカッコイイ気がします。
が、結論から言うとあまりオススメしません。
export class Post { constructor (id, title, body="") { this.id = id this.title = title this._body = body this.changed = false } get body () { return this._body } set body (value) { this._body = value this.changed = true } }
この場合、Vueコンポーネントは以下のようになるでしょう。
changed = false
にする処理や、 :disabled="!post.changed"
が足されています。
<template> <div v-if="post"> <textarea v-model="post.body"></textarea> <button @click="save" :disabled="!post.changed">Save</button> </div> </template> <script> import * as api from './api' export default { name: 'Post', data () { return { post: null } }, methods: { async save () { await api.patchPost(this.post) post.changed = false } }, async mounted () { this.post = await api.getPost(1) } } </script>
この程度ではあまり問題になりませんが、 1つのコンポーネントでしか使わないような処理や状態をモデルに入れるとモデルが肥大化していきます 。 変更の検知をする部分はVueコンポーネントに書くほうが良いです。
「changed
はAPI(データの読み込み・保存)には関係しない。モデルに書くべきでは無いかも?」と考えてみてください。
Vueコンポーネントに書こう
モデルの定義は元に戻して(getter, setterを消して)、Vueコンポーネントを以下のように書いてみましょう。 「変更されたかどうか」という状態をVueコンポーネントに持つようになっています。
<template> <div v-if="post"> <textarea v-model="body"></textarea> <button @click="save" :disabled="!changed">Save</button> </div> </template> <script> import * as api from './api' export default { name: 'Post', data () { return { post: null, body: "" } }, methods: { async save () { this.post.body = body await api.patchPost(this.post) } }, computed: { changed () { return this.body != this.post.body } }, async mounted () { this.post = await api.getPost(1) this.body = this.post.body } } </script>
textarea
ではVueコンポーネントの body
を編集するようにして、 post.body
は触れないようにしています。
「変更されたかどうか」は changed
というcomputedで計算しています。
changed
を data
で管理するフラグにしても良いです。
methods: { update (value) { this.body = value this.changed = true } }
こう変更することで、モデルとしたPostクラスでは changed
という実装が無くなりました。
「記事」として管理すべき値はモデルに、「編集する」という責任範囲で管理すべき値はコンポーネントに置きましょう。
そうすることで、モデルの実装を減らして、細かく分けられたVueコンポーネントに実装を分散できます。
もし複数コンポーネントで状態を共有するのであれば、Vuexを使えば済みます。 もちろん今回の説明も「こっちが正解」というわけではありません。ですが、「全てモデルに書く」、「全てViewコンポーネントに書く」のでなく、適宜コンポーネントを分離して値を管理する場所を分けることで、より良い設計が自然とできるようになるでしょう。