3
5

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 3 years have passed since last update.

Vue.js初心者がやりがちなコードの書き方

Last updated at Posted at 2020-12-15

Vue.jsは学習のしやすいフレームワークであると思う。段階的に機能の導入ができるように設計されているため、必要以上の学習コストが発生しにくいということもあるが、特定の機能(例えば、算出プロパティ)を初めて学習する場合にそれを試すことが容易であることも大きい。

特定の機能を簡単に試せることは、メリットであると共に注意も必要であると思う。特に初学者は、実際のアプリケーションの中で

  • どのような状況でその機能を使えば良いのか

といった観点を把握しないままに先に進んでしまいがちで、状況に応じた適切な機能を選択できていないことが多々あるように思う。

この記事では、サンプルアプリのリファクタリングを通して、初心者がやりがちだと思うコードの書き方とその問題点を指摘していく。なお、リファクタリングは問題点に気づくための手段として行っているので、具体的なステップバイステップの安全なリファクタリング手順は記載していないことはご了承いただきたい。

目次

1. 当記事の対象読者
2. 動作環境
3. リファクタリングするサンプルアプリ
  3-1. サンプルアプリの要件
  3-2. リファクタリング前のコード
4. コードの問題点と改善方法
  4-1. 繰り返しを含むtemplate
  4-2. セットで更新しなければいけない状態
  4-3. 所属がおかしな状態
  4-4. 矛盾しうるプロパティ
  4-5. リファクタリング後のコード
5. まとめ
6. あとがき
7. 参考にしたサイト・書籍

1. 当記事の対象読者

当記事はVue.js初心者を対象とする。一通りの機能は触ったことがあり、とりあえず動作するものは作れるが、コンポーネントが上手く書けているか分からないといった場合には参考になる箇所があるかもしれない。

前提知識としてはシングルファイルコンポーネントが理解できれば良く、Vue RouterやVuexまでは踏み込まない。

また、初心者対象とはしたものの、ある程度Vue.jsの開発経験がある方にも読んでいただき、意見や間違いの指摘等をコメントいただけるとありがたい。

2. 動作環境

サンプルアプリはvue-cliでバージョン2.6.10のVue.jsアプリを作成、動作確認をおこなった。確認はしていないが3.0系で変更があった機能は使っていないので、3.0系でも問題なく動作すると思う。

2021/3/5 追記
3.0系だと一部動作しない箇所があったので修正させていただきます。

3. リファクタリングするサンプルアプリ

今回、リファクタリングの対象とするサンプルアプリは下図のようなイメージだ。
sample_app_image
とある青果店のホームページの一角に表示するものと思って欲しい。

3-1. サンプルアプリの要件

この青果店ではりんごジュースを販売しているが、りんごジュース目当ての客が増えすぎたため、ホームページ上の抽選で当選した人にのみに販売することになった。要件は以下の通りだ。

  • りんごジュースに使うりんごは日替わりで、産地や品種、値段が異なる
  • Mサイズの料金を基本料金として、 Lサイズはその1.4倍の料金とする
  • 抽選はメールアドレスをフォームに入力して、その結果がそのメールアドレスに対して送信される

あまり現実的ではない設定は多々あるが、説明用と割り切っていただけると幸いだ。

3-2. リファクタリング前のコード

このアプリは基本的に以下3つコンポーネントで構成されている。

  • App...サンプルの全体
  • MyTextBox...メールアドレスの入力フォームで使用
  • MyButton...「抽選する」ボタンで使用

次にソースコードを示す。なるべく短くするよう努めたが、それでもやや長いため、現時点ではざっと確認する程度で構わない。

なお、CSSの記述に関しては今回の目的からは逸れること、そしてコードを短く保つために外した。ただし、スタイリングに使用したタグのclass属性は、タグの意味合いを理解する補助になるため意図的に残した。

App.vue(3.リファクタリング前)
<template>
  <div id="app">
    <h1>Ringo Juice</h1>
    <div class="today-info">
      <h4 class="today-info_title">本日のりんご</h4>
      <div class="today-info_list">
        <div class="today-info_item">
          <span>産地</span>
          {{ info.prefecture }}
        </div>
        <div class="today-info_item">
          <span>品種</span>
          {{ info.variety }}
        </div>
        <div class="today-info_item">
          <span>Mサイズ</span>
          {{ info.basePrice }}</div>
        <div class="today-info_item">
          <span>Lサイズ</span>
          {{ largeSizePrice }}</div>
      </div>
    </div>
    <p class="message">
      りんごジュースは1日100名様限定です。<br />当選者のみが購入可能です。<br />
      下記フォームにメールアドレスを入力してください。<br />抽選結果が届きます。
    </p>
    <div class="form">
      <MyTextBox ref="email" placeholder="メールアドレス" @send-text="sendEmail" />
      <MyButton isSmall @click="onClick">
        抽選する
      </MyButton>
    </div>
  </div>
</template>

<script>
import MyTextBox from '@/components/MyTextBox.vue'
import MyButton from '@/components/MyButton.vue'

export default {
  name: 'app',
  components: { MyTextBox, MyButton },
  data() {
    return {
      info: { prefecture: '', variety: '', basePrice: 0 },
      largeSizePrice: 0
    }
  },
  created() {
    this.info = this.getTodayInfo()
    this.largeSizePrice = this.info.basePrice * 1.4
  },
  methods: {
    getTodayInfo() {
      // 実際はWebAPIで情報を取得するが、話の単純化のため定数を返す
      return { prefecture: '青森', variety: 'ふじ', basePrice: 300 }
    },
    onClick() {
      this.$refs.email.sendText()
    },
    sendEmail(address) {
      // 実際はWebAPIでサーバにアドレスを送信するが、話の単純化のため警告を出すだけにしている
      alert(`${address}にメールを送信しました`)
    }
  }
}
</script>
MyTextBox.vue(3.リファクタリング前)
<template>
  <input v-model="text" :placeholder="placeholder" />
</template>

<script>
export default {
  props: {
    placeholder: { type: String, default: '' }
  },
  data() {
    return {
      text: ''
    }
  },
  methods: {
    sendText() {
      this.$emit('send-text', this.text)
    }
  }
}
</script>
MyButton.vue(3.リファクタリング前)
<template>
  <button :class="btnClass" @click="$emit('click')">
    <slot />
  </button>
</template>

<script>
export default {
  props: {
    isSmall: { type: Boolean },
    isLarge: { type: Boolean }
  },
  computed: {
    btnClass() {
      if (this.isSmall) return 'small'
      return 'large'
    }
  }
}
</script>

これでコードは以上だ。

正直小さなコンポーネントばかりなので、そこまで読むのは難しくないと思う。しかし、この中にも可読性を下げる要因は散りばめられており、もっと大きなコンポーネントの中では、そういった小さな要因が積み重なって扱いづらいコンポーネントになってしまう。

2021/3/5 追記

Vue 3.0系では子コンポーネント側からイベントをemitする場合、基本的にはemitsオプションを指定する必要がある。emitsオプションを指定するとコンポーネントのインターフェースが分かりやすくなるメリットもあるが、$attrsの仕様変更のために、イベントハンドラが誤動作するのを防ぐことができる。詳しくは下記参考リンクを参照。

4. コードの問題点と改善方法

ここから上記アプリの問題点の指摘とその修正方法を記載していく。

4-1. 繰り返しを含むtemplate

まず、手始めにApp.vueの下記部分を見ていく。

App.vue(4-1.繰り返しを含むtemplate)
<template>
    <!-- ...略... -->
      <div class="today-info_list">
        <div class="today-info_item">
          <span>産地</span>
          {{ info.prefecture }}
        </div>
        <div class="today-info_item">
          <span>品種</span>
          {{ info.variety }}
        </div>
        <div class="today-info_item">
          <span>Mサイズ</span>
          {{ info.basePrice }}</div>
        <div class="today-info_item">
          <span>Lサイズ</span>
          {{ largeSizePrice }}</div>
      </div>
    <!-- ...略... -->
</template>

繰り返しは、Vue.jsに限らず一番目に着きやすいリファクタリングすべき箇所であることが多い。サンプルでは比較的単純な構造のたった4回の繰り返しだが、これがもっと大きくなってくるとコード量が増え、可読性が落ちる。また修正が必要な際は、同様の修正を複数箇所に適用しなければならず変更がしづらい。

しかし、もっと深刻な問題は**「本当に繰り返しなのか」がこのコードを書いた本人以外には分かりづらいこと**である。何回か「繰り返し」という言葉を使用してきたが、実は一箇所だけ違う箇所がある。見つけられていなければ再度コードを見返して欲しい。答えは下の折り畳み内に記載した。

Q.繰り返しではない箇所はどこか(答えはクリックして開く) A. 本当はそんなものはない。もし、時間をかけてじっくり探した人がいたら申し訳ないが、そういう人ほど次のリファクタリングの意図が理解できるだろう。

上記したような問題を解決するために、以下のファクタリングを適用する。

  • 繰り返している構造をコンポーネントとして抽出
  • 繰り返し部分にv-forを適用する

繰り返している構造をコンポーネントとして抽出

繰り返している構造を抽出したMyItemコンポーネントを作成する。

MyItem.vue(4-1.繰り返し構造をコンポーネントとして抽出)
<template>
  <div class="today-info_item">
    <span>{{ title }}</span>
    {{ value }}
  </div>
</template>

<script>
export default {
  props: {
    title: { type: String, required: true },
    value: { type: [String, Number], required: true }
  }
}
</script>

これを使用するとAppコンポーネントは次のようになる。

App.vue(4-1.MyItemを適用)
<template>
    <!-- ...略... -->
      <div class="today-info_list">
        <MyItem title="産地" :value="info.prefecture" />
        <MyItem title="品種" :value="info.variety" />
        <MyItem title="Mサイズ" :value="`${info.basePrice}円`" />
        <MyItem title="Lサイズ" :value="`${largeSizePrice}円`" />
      </div>
    <!-- ...略... -->
</template>

このようにしていれば繰り返しであることは格段に分かりやすくなる。

繰り返し部分にv-forを適用する

さて、繰り返している構造をコンポーネントとして抽出したことで、冗長な記述は大幅に減ったが、まだ検討の余地がある。シンプルにはなったが、MyItemタグを繰り返し記載している。v-forディレクティブを使用することでそのような記載をよりシンプルにすることが可能だ。

v-forをこの部分に適用するため、次のような算出プロパティを用意する

App.vue(4-1.繰り返し部分のデータを配列として扱う算出プロパティを用意)
<script>
// ...略...
export default {
  // ...略...
  computed: {
    infoItems() {
      return [
        { title: '産地', value: this.info.prefecture },
        { title: '品種', value: this.info.variety },
        { title: 'Mサイズ', value: `${this.info.basePrice}円` },
        { title: 'Lサイズ', value: `${this.largeSizePrice}円` }
      ]
    }
  },
  // ...略...
}
</script>

この算出プロパティを使用してtemplateを書き換えると次のようになる。

App.vue(4-1.繰り返し部分にv-forを適用)
<template>
    <!-- ...略... -->
      <div class="today-info_list">
        <MyItem v-for="item of infoItems" :title="item.title" :value="item.value" />
      </div>
    <!-- ...略... -->
</template>

説明のためにv-forを適用したが、今回のケースではここまでしてしまうと少しやりすぎかもしれない。しかし、繰り返している構造をコンポーネントにした場合でも、共通の属性(例えばクラス等)を指定しないといけない場合もあり、その場合はv-forを適用することでコードの記述量が減るので有用であると思う。

4-2. セットで更新しなければいけない状態

次に検討したいのはAppコンポーネントの次の部分である。

App.vue(4-2.余分な状態)
<script>
// ...略...
export default {
  // ...略...
  data() {
    return {
      info: { prefecture: '', variety: '', basePrice: 0 },
      largeSizePrice: 0
    }
  },
  created() {
    this.info = this.getTodayInfo()
    this.largeSizePrice = this.info.basePrice * 1.4
  },
  // ...略...
}
</script>

dataにはprefecturevarietybasePriceの3つのプロパティをもつオブジェクトinfolargeSizePriceが登録されている。

このうちMサイズの料金(基本料金)を表すinfobasePriceとLサイズの料金を表すlargeSizePriceの関係に注目したい。このアプリの要件には

  • Mサイズの料金を基本料金として、 Lサイズはその1.4倍の料金とする

というものがあった。これはMサイズの料金が決まれば、Lサイズの料金は自動的に決まるということであり、それぞれが独立した状態でないことを意味する。独立していない状態をdataとして登録してしまうと後々問題を引き起こす可能性がある。例えば、情報を30分に一回更新するといった要件が必要になったとする。次のようなイメージだ。

App.vue(4-2.30分に一回情報を更新する場合)
<script>
// ...略...
export default {
  // ...略...
  created() {
    this.info = this.getTodayInfo()
    this.largeSizePrice = this.info.basePrice * 1.4
    setInterval(() => {
      this.updateInfo()
    }, 30 * 60 * 1000)
  },
  methods: {
    // ...略...
    updateInfo() {
      this.info = this.getTodayInfo()
    }
  }
}
</script>

このコードの問題点はすぐに分かるだろう。updateInfo()の定義内でinfoを更新した際にlargeSizePriceがセットで更新されていない。そんな見落としはしないと思う人もいるかもしれないが、実際のアプリケーションの中では、エラー処理だったり、WebAPIから取得したデータの整形が行われていたりとupdateInfo()のコードが数十行のサイズになることもある。また他の箇所でもbasePriceが更新されるようなことがあれば、その度に忘れずlargeSizePriceを更新しなければならない。

上記したような問題を解決するために、以下のリファクタリングを適用する。

  • セットで更新しなければいけない状態は算出プロパティとして定義する

セットで更新しなければいけない状態は算出プロパティとして定義する

Vue.jsに慣れていない人は、算出プロパティを使えていないことが多々ある。この原因は憶測だが、算出プロパティがVue.js特有の機能であることや、そもそも算出プロパティを使わなくとも動くものは作れてしまうこと、あとは名前自体が何か計算が必要なときに使うものという誤解を招いていたりもしていると思う。サンプルアプリではまさに計算するために使っているが、状態に応じて決まるclassや、アイコンのパス指定などにも利用できる

さて、話が脱線してしまったが、修正自体はすごくシンプルだ。

App.vue(4-2.独立していない状態を算出プロパティとして定義)
<script>
// ...略...

export default {
  // ...略...
  data() {
    return {
      info: { prefecture: '', variety: '', basePrice: 0 }
    }
  },
  computed: {
    infoItems() {/* ...略... */},
    largeSizePrice() {
      return this.info.basePrice * 1.4
    }
  },
  created() {
    this.info = this.getTodayInfo()
  },
 // ...略...
}
</script>

これでbasePriceが更新されたときは自動的にlargeSizePriceが再計算される。

4-3. 所属がおかしな状態

次にMyButtonコンポーネントを検討する。上で記載したコードを再掲する。

MyTextBox.vue(4-3.最初に記載したものの再掲)
<template>
  <input v-model="text" :placeholder="placeholder" />
</template>

<script>
export default {
  props: {
    placeholder: { type: String, default: '' }
  },
  data() {
    return {
      text: ''
    }
  },
  methods: {
    sendText() {
      this.$emit('send-text', this.text)
    }
  }
}
</script>

このコンポーネントのメソッドsendText()は親コンポーネント(MyTextBoxを使う側、すなわちAppコンポーネント)で使用されることを想定して定義されている。

Appコンポーネントを見てみるとtemplateのMyTextBoxの呼び出し箇所でref属性が指定されており、onClickメソッド内でsendText()を呼び出していることが分かる。

App.vue(4-3.子コンポーネントのメソッド呼び出し)
<template>
   <!-- ...略... -->
      <MyTextBox ref="email" placeholder="メールアドレス" @send-text="sendEmail" />
      <MyButton isSmall @click="onClick">
        抽選する
      </MyButton>
   <!-- ...略... -->
</template>

<script>
// ...略...
export default {
  // ...略...
  methods: {
    getTodayInfo() {/* 省略 */},
    onClick() {
      this.$refs.email.sendText()
    },
    sendEmail(address) {/* 省略 */}
  }
}
</script>

このようなことをしてしまうと、コンポーネント同士は密結合になり、コンポーネントを超えた影響を考慮しなければならなくなってしまう。クラスのアクセス修飾子のように外部からのアクセスを制限できる仕組みがあれば良いが、恐らくVue.jsのコンポーネントにはそのような仕組みはないので、そもそも外部のコンポーネントの状態へのアクセスやメソッドの使用をしないように注意する他なさそうだ。

上記した問題を解決するために、以下のリファクタリングを適用する

  • 適切なコンポーネントに状態を移動

適切なコンポーネントに状態を移動

そもそも親コンポーネントで使用するメソッドであれば、親コンポーネント内で定義すれば良いのだが、そうなっていないのは入力フォームの状態をMyTextBox側で持ってしまっていることに原因がある。そこで、親コンポーネント側から入力フォームの状態を受け取るようにし、変更があれば親コンポーネントにイベント送信して通知するように修正する。

MyTextBox.vue(4-3.入力フォームの状態はプロパティとして受け取る)
<template>
  <input :value="text" :placeholder="placeholder" @input="onInput" />
</template>

<script>
export default {
  props: {
    text: { type: String, required: true },
    placeholder: { type: String, default: '' }
  },
  methods: {
    onInput(event) {
      this.$emit('input', event.target.value)
    }
  }
}
</script>

Appコンポーネントでは入力フォームの状態を定義し、MyTextBoxpropsとして渡し、変更イベントを受け取り次第状態を更新するようにする。

App.vue(4-3.入力フォームの状態を管理)
<template>
  <!-- ...略... -->
    <div class="form">
      <MyTextBox
        :text="address"
        placeholder="メールアドレス"
        @input="onInput"
      />
      <MyButton isSmall @click="onClick">
        抽選する
      </MyButton>
    </div>
  </div>
</template>

<script>
// ...略...
export default {
  name: 'app',
  components: {/* ...略... */},
  data() {
    return {
      info: {/* ...略... */},
      address: '' // <-- 入力フォーム用の状態を追加
    }
  },
  computed: {/* ...略... */},
  created() {/* ...略... */},
  methods: {
    getTodayInfo() {/* ...略... */},
    onClick() {
      alert(`${this.address}にメールを送信しました`) // <-- ボタンクリック時に子コンポーネントは介さないように修正
    },
    onInput(text) {
      this.address = text // <-- 入力フォームに変更があれば状態を変更
    }
  }
}
</script>

ところで、なぜこのアプリの実装者は最初にMyTextBoxの方に入力フォームの状態を定義したのだろうか。考えられる理由としては**Appコンポーネント側に状態を増やしたくなかったから**ということがあげられる。

今回の例では分かりづらいかもしれないが、複数のコンポーネント間でデータの共有をしようと思うとどうしても上位のコンポーネント側に状態が集まってしまいがちで、それを解決するために子側に状態を持たせてしまうことがある。しかし、コードの見かけ上状態が減ったように見えても、その状態が必要であることには変わりはない。コンポーネントの責務を分離するためにも、原則、状態は適切なコンポーネントに配置すべきだと思う。コンポーネントで使う状態が増えすぎてしまうような場合にはVuex等の状態管理ライブラリを導入するのも手だ。

さて、もっと簡潔な記載にするためMyTextBoxv-modelを適用したいと思うかもしれない。

App.vue(MyTextBoxにv-modelを適用)
<MyTextBox v-model="address" placeholder="メールアドレス" />

カスタムコンポーネントではv-modelを使用する場合、Vue 2.xのデフォルトではvalueをプロパティとして渡し、inputイベントによって値を更新するようになっているので、textをプロパティとして渡したい場合は下記のような修正が必要だ。

MyTextBox.vue(4-3.v-modelを使うための修正)
<template>
  <input :value="text" :placeholder="placeholder" @input="onInput" />
</template>

<script>
export default {
  model: { prop: 'text' }, // <-- これを追記
  props: {
    text: {/* ...略 ... */},
    placeholder: {/* ...略... */}
  },
  methods: {/* ...略... */}
}
</script>

2021/3/15 追記

Vue 3.0系ではデフォルトではmodelValueをプロパティとして渡し、update:modelValueイベントによって値を更新するようになっている。コンポーネントのmodelオプションは廃止されたので、textをプロパティとして渡したい場合はv-modelの引数として指定する。

App.vue(MyTextBoxにv-modelを適用)
<template>
  <MyTextBox v-model:text="address" placeholder="メールアドレス" />
</template>

また、onInputメソッドでemitするイベントはupdate:textとする必要がある。

4-4. 矛盾しうるプロパティ

最後に見ていくのはMyButtonコンポーネントのプロパティ部分だ。

MyButton.vue(4-4.矛盾しうるプロパティ)
<template>
  <!-- ...略... -->
</template>

<script>
export default {
  props: {
    isSmall: { type: Boolean },
    isLarge: { type: Boolean }
  },
  computed: {/* ...略 */}
}

指定できるのは、サイズ変更のための属性である。真偽値のプロパティは、コンポーネントを使う側では<MyButton isLarge>クリック</MyButton>のように値なしで属性を記載するだけで、:isLarge="true"と指定したと見なされるため、若干使い勝手が良いという側面がある。

一方で、<MyButton isSmall isLarge>クリック</MyButton>のように意味が矛盾するような指定もできてしまうので注意が必要だ。それ自体が直接問題を引き起こすようなことは少ないかもしれないが、デフォルト値の指定がしづらかったり、両方指定された場合の適用優先度を把握しなければならなかったりすることで、可読性が落ちる可能性がある。

上記問題を解決するために、以下のリファクタリングを適用する

  • 同種のプロパティをまとめる

同種のプロパティをまとめる

isSmallisLargeがともにサイズに関連するプロパティであったがために、矛盾が生じる指定ができてしまっていたので、これらをサイズを指定するためのプロパティにまとめてしまえば良い。

MyButton.vue(4-4.サイズ指定のプロパティをまとめる)
<template>
  <!-- ...略... -->
</template>

<script>
export default {
  props: {
    size: {
      type: String,
      default: 'small',
      validator: val => ['small', 'large'].includes(val)
    }
  },
  computed: {
    btnClass() {
      if (this.size === 'small') return 'small'
      return 'large'
    }
  }
}
</script>

validatorの記述は若干冗長だが、sizeというプロパティがどのような値を想定しているかのドキュメント代わりにもなるので、あった方が良いだろう。

参考程度に載せておくが、コンポーネントライブラリのVeutifyのv-btnコンポーネントは、まさにこのリファクタリングを適用する前のようなプロパティ指定でサイズ変更できるようになっている。

Vuetifyのボタンの例
<v-btn large>クリック</v-btn>

他のリファクタリングでも言えることだが、必ずしもこれが正解というものはなく、メリット・デメリットを考慮した上でどうすべきかという判断が必要であることは注意していただきたい。

4-5. リファクタリング後のコード

これで、適用したいリファクタリングは全て行った。改めてリファクタリング後のコードを記載すると次のようになる。

App.vue(4-5.リファクタリング後)
<template>
  <div id="app">
    <h1>Ringo Juice</h1>
    <div class="today-info">
      <h4 class="today-info_title">本日のりんご</h4>
      <div class="today-info_list">
        <MyItem v-for="item of infoItems" :title="item.title" :value="item.value" />
      </div>
    </div>
    <p class="message">
      りんごジュースは1日100名様限定です。<br />当選者のみが購入可能です。<br />
      下記フォームにメールアドレスを入力してください。<br />抽選結果が届きます。
    </p>
    <div class="form">
      <MyTextBox v-model="address" placeholder="メールアドレス" />
      <MyButton size="small" @click="onClick">
        抽選する
      </MyButton>
    </div>
  </div>
</template>

<script>
import MyTextBox from '@/components/MyTextBox.vue'
import MyButton from '@/components/MyButton.vue'
import MyItem from '@/components/MyItem.vue'

export default {
  name: 'app',
  components: { MyTextBox, MyButton, MyItem },
  data() {
    return {
      info: { prefecture: '', variety: '', basePrice: 0 },
      address: ''
    }
  },
  computed: {
    infoItems() {
      return [
        { title: '産地', value: this.info.prefecture },
        { title: '品種', value: this.info.variety },
        { title: 'Mサイズ', value: `${this.info.basePrice}円` },
        { title: 'Lサイズ', value: `${this.largeSizePrice}円` }
      ]
    },
    largeSizePrice() {
      return this.info.basePrice * 1.4
    }
  },
  created() {
    this.info = this.getTodayInfo()
  },
  methods: {
    getTodayInfo() {
      return { prefecture: '青森', variety: 'ふじ', basePrice: 300 }
    },
    onClick() {
      alert(`${this.address}にメールを送信しました`)
    }
  }
}
</script>
MyItem.vue(4-5.リファクタリング後)
<template>
  <div class="today-info_item">
    <span>{{ title }}</span>
    {{ value }}
  </div>
</template>

<script>
export default {
  props: {
    title: { type: String, required: true },
    value: { type: [String, Number], required: true }
  }
}
</script>
MyTextBox.vue(4-5.リファクタリング後)
<template>
  <input :value="text" :placeholder="placeholder" @input="onInput" />
</template>

<script>
export default {
  model: { prop: 'text' },
  props: {
    text: { type: String, required: true },
    placeholder: { type: String, default: '' }
  },
  methods: {
    onInput(event) {
      this.$emit('input', event.target.value)
    }
  }
}
</script>
MyButton.vue(4-5.リファクタリング後)
<template>
  <button :class="btnClass" @click="$emit('click')">
    <slot />
  </button>
</template>

<script>
export default {
  props: {
    size: {
      type: String,
      default: 'small',
      validator: val => ['small', 'large'].includes(val)
    }
  },
  computed: {
    btnClass() {
      if (this.size === 'small') return 'small'
      return 'large'
    }
  }
}
</script>

さて、リファクタリング前と比べていかがだろうか。正直、劇的な変化はないのでがっかりした方もいるかもしれない。しかし、今回紹介したような問題のある書き方を避け、避けられなくとも問題点を理解した上で、コンポーネントを書いていくことができれば、より大規模なアプリを開発する上で役に立つ場面があると信じている。

5. まとめ

今回見てきた、初心者がやりがちだと思うコードの書き方の問題点とその解決方法を改めてまとめる。

繰り返しを含むtemplate

  • 問題点
    • コード量が単純に増え可読性が落ちる
    • 同様の修正が複数箇所で必要になり、変更しづらい
    • そもそも繰り返しなのかが、書いた本人以外に分かりづらい
  • 解決方法
    • 繰り返しの構造をコンポーネントとして抽出する
    • 繰り返している箇所にv-forを適用する

セットで更新しなければいけない状態

  • 問題点
    • セットで更新しなければならないことを忘れてしまいがち
  • 解決方法
    • セットで更新しなければいけない状態は算出プロパティとして定義する

所属がおかしな状態

  • 問題点
    • コンポーネント同士が密結合になり、コンポーネント外の影響を考慮しなければならなくなる
  • 解決方法
    • 適切なコンポーネントに状態を移動する
    • (場合によっては、Vuex等状態管理ライブラリの使用を検討する)

矛盾しうるプロパティ

  • 問題点
    • デフォルト値の指定がしづらい
    • 矛盾する指定をしたときに、どちらが優先して適用されるか分かりづらい
  • 解決方法
    • 同種のプロパティをまとめる

6. あとがき

局所的に問題点の指摘やその解決方法は記載できたかと思うが、いかに扱いやすいコンポーネントになったかという説明ができなかったので、時間があれば補足したい。特にコンポーネントのテストはしやすくなっているはずで、そういった説明も加えられると良いかなと思う。

7. 参考にしたサイト・書籍

  • Vue.js
  • Vuetify
  • Element
  • Martin Fowler (2018) Refactoring: Improving the Design of Existing Code, 2nd Edition, Pearson Education Inc
3
5
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
3
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?