TypeScript
es6
DesignPatterns
vue.js
Vuex

2018年Vue.jsとVuexを使ってる人には必ず知っていてほしいドキュメントに書かれていないコンポーネントやストア、メンテナンスの際に役立つTips

はじめに

私はVue.js with Vuexを使った業務で1画面30APIを叩く必要のある画面から、たったの数APIしか叩かないけれど、代わりにUIがとても機能的で複雑な画面まで設計し、構築しました。
現在は構築したシステムを保守・運用しており、その際に得られたノウハウを言語化し、共有出来たらと思います。

※ 記事の内容に意見がありましたら直接編集リクエストをください。
※ パフォーマンスの話はしません。

ゴール

役立つTipsを身につけコード品質を向上させる

コンポーネントのバグを減らせるTips

ほとんどのバグは変数から来ます。
もし全ての値が定数なら状態から来るバグはほとんど無くなるでしょう。
ここではこの変数や式を極力減らせるTipsを紹介したいと思います。

1. dataを極力定義しない

Vue.jsでコンポーネントを定義する際ついdata()に沢山変数を定義しちゃいますよね。
しかし、dataはいわゆるインスタンス変数です。
もしそのdataの値が定数とかpropsから算出できる値だったりした場合できるだけcomputedに移してあげましょう。
なぜならsetterがないcomputed はread onlyだからです。read onlyは変更される心配がないためバグを減らしてくれるとても素晴らしいものです!

悪い例:

export default {
  props: {
    user: {
      type: Object,
      required: true,
    },
  },
  data() {
    const birthdayDate = new Date(this.user.birthday);
    return {
      max: 5,
      birthday: `${birthdayDate.getFullYear()}/${birthdayDate.getMonth() + 1}`
    };
  },
};

悪い例ではmaxやbirthdayがtemplate内でも変更される心配があります。
また、user propの値が変わってもbirthdayは再計算されることはないでしょう。

改善例:

export default {
  computed: {
    max: () => 5,
    birthday() {
      const birthdayDate = new Date(this.user.birthday);
      return `${birthdayDate.getFullYear()}/${birthdayDate.getMonth() + 1}`;
    },
  },
};

改善例ではmaxやbirthdayがtemplate内でも変更される心配がない事がすぐにわかります。
また、user propの値が変わってもcomputedのbirthdayは正しい値をリアクティブに再計算してくれます。
改善例の方がこれはどういう値か伝わってきますね!
しかもダメな例と比べ状態(変数)を1つ減らせました!つまりバグの原因が1つ減りました!おめでとうございます!

2. template内でできるだけ式を書かない

template内でdataなどを直接変更したりイベントを発火したりするコードを直接書くことはよくありません。
それには大まかに2つの理由があります。

  • 変数を定義している場所と変更されている場所が遠すぎて視認性にすぐれない
  • methodで定義すれば式にメソッド名という形式でこのカプセル化された式はどういう意図をもった式なのかを簡単に伝えられる
    • template内に直接式を書いた場合は逆にこの式はどういう式かをコードを見た人全員が推測しなければなりません。つまりハイコンテキスト、空気を読めって事です。メンテする際に大抵困ります。

悪い例:

<template>
  <button @click="count++ && $emit('change', count)">ボタン</button>
</template>


<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
};
</script>

改善例:

<template>
  <button @click="incAndNotify()">ボタン</button>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  methods: {
    incAndNotify() {
      this.count++;
      this.$emit('change', count);
    },
  },
};
</script>

templateタグを最大限に有効活用

このようなコードをたまにみます。
お世辞でもスマートとは言い難いですね。

悪い例:

<template>
  <div>
    <button @click="inc()">ボタン</button>

    <div v-if="isSmallCount">これはとても小さな値です!</div>
    <div v-if="isSmallCount">値をもっともっと増やしてください!</div>

    <div v-if="!isSmallCount">これはとても大きな値です!</div>
    <div v-if="!isSmallCount">すごいです!</div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    isSmallCount() {
      return this.count < 5;
    },
  },
  methods: {
    inc() {
      this.count++;
    },
  },
};
</script>

改善例:

<template>
  <div>
    <button @click="inc()">ボタン</button>

    <template v-if="isSmallCount">
      <div>これはとても小さな値です!</div>
      <div>値をもっともっと増やしてください!</div>
    </template>

    <template v-else>
      <div>これはとても大きな値です!</div>
      <div>すごいです!</div>
    </template>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
    };
  },
  computed: {
    isSmallCount() {
      return this.count < 5;
    },
  },
  methods: {
    inc() {
      this.count++;
    },
  },
};
</script>

改善例では、templateタグ内にtemplateタグが使われています。
どういうことでしょうか?
一度isSmallCountが真の時にDOMがどのようにマウントされるのかを見てみましょう!

<div>
  <button @click="inc()">ボタン</button>
  <div>これはとても小さな値です!</div>
  <div>値をもっともっと増やしてください!</div>
</div>

なんてことでしょう!templateタグがどこにも見当たりません!
そうtemplateタグはなんとマウントされる際に表示されないのです!
これはv-ifなどの式を書く際にとても重宝できます!スコープはありませんが、ブロック的なものを表現できますしね!

※ もちろんv-forv-showなどにも使えますよ😊

フラグを極力減らす

通信をするコンポーネントでは通信中状態と通信していない待機状態があります。
例えばこれを2つのフラグで管理していた場合不整合が発生する可能性が出ます。
これはとても不健康な状態です。きっと将来待機状態なのに通信状態という意味不明な状態が発生するでしょう。

悪い例:

<template v-if="isInitializing">
  <div>初期化中です。。。😩</div>
</template>

<template v-else-if="isStundby">
  <div>初期化完了です😊</div>
</template>

<script>
export default {
  data() {
    return {
      isStundby: false,
      isInitializing: true,
    };
  },
};
</script>

悪い例では先程も言ったようにフラグの整合性を保証できるものが何もありませんね。
ですのでこれらを1つの状態フラグで管理するようにします。
名前は適当にcurrentStateで良いでしょう。

改善例:

<template v-if="isInitializing">
  <div>初期化中です。。。😩</div>
</template>

<template v-else-if="isStundby">
  <div>初期化完了です😊</div>
</template>

<script>
const IS_STUNDBY = 'IS_STUNDBY';
const IS_INITIALIZING = 'IS_INITIALIZING';

export default {
  data() {
    return {
      currentState: IS_STUNDBY,
    };
  },
  computed: {
    isStundby() {
      return this.currentState === IS_STUNDBY;
    },
    isInitializing() {
      return this.currentState === IS_INITIALIZING;
    },
  },
  methods: {
    toStundby() {
      this.currentState = IS_STUNDBY;
    },
    toInitializing() {
      this.currentState = IS_INITIALIZING;
    },
  },
};
</script>

やりました!改善例では変数を1つ減らし、不整合が絶対に起こり得なくなりました!
今回の例はとてもシンプルですが、画面のタブを管理する時など状態が多くなればなるほど役に立つテクニックになります。

汚いコンポーネントをスマートにするテクニック

よくレイアウトやアニメーションに関するコードと表示用コンポーネントのコードが一緒になっているコードを見ます。
大抵の場合別に良いのですが、v-forでリストを表示して、そのリストの各アイテムにも詳細情報の開閉ボタンがあり、クリックされたら詳細が開示されるなどのコードがあります。
まぁ大抵ここまでのコードを1つのコンポーネントで書くと、とてもではないですがメンテナブルなコードとは言い難いですね。バグが発生しても各アイテムの開閉状態のコードなど色んなコードが一箇所に書かれているため何が原因か特定するのにも時間がかかりますし、破壊する恐れもあります。
では、slotに渡されたアイテムの開閉だけを担当するコンポーネントと、専用のslotをいくつか用意したレイアウト用のコンポーネントを作成し、それをメインとなるコンポーネントで使ってはどうでしょうか。コードがとても見通しがよくなります。
また、メインとなるコンポーネントからはきっと大量のインスタンス変数が消えてバグが減ったかと思われます。

※ これはサンプルコードがちょっと長くなるので今度追加します。

OSSのUIライブラリを利用しましょう

1からプロダクトを作成する際にdialog、tooltip、form、validationなど他にも目眩がする程の大量のコンポーネントを作る必要があります。しかも自社でそういうのを全部書いてるときっと粒度も何もかもがバラバラでしょう。しかもレスポンシブ対応もできるだけしたい!あぁ、これはとても大変です!
そういう場合は既存のOSSからUIライブラリを拝借しましょう!
その際に重要なチェック項目として

  • 活発
  • ドキュメント、サンプルコードが豊富
  • レスポンシブ

などがあります。
ちなみによくできたUIライブラリはCSSは基本上書きできますので大抵のデザイン要件を満たせます!
それでもデザイン要件を満たすのが辛い場合はデザイナーさんと相談しましょう!このUIライブラリを使っていてできるだけこれに沿って工数を減らしたいといえばきっと協力してくれるでしょう!
ちなみに私のオススメはVuetifyです。これはとてもよくできていて、活発で、ドキュメントやサンプルコードが豊富でレスポンシブ対応です!

※ 自社専用のUIライブラリ別リポジトリで用意し、上手くメンテできるほどの潤沢なリソースがある環境は例外とします。

OSSのUIコンポーネントなどを積極的に利用しましょう

私はよく全然活発でなくてメンテされていなくても現在のニーズにあったOSSのUIコンポーネントがあった場合はとりあえず利用します。
それはとても怖いことではないでしょうか?もしそのOSSがバグっていたらと思うとゾッとするのではないですか?そう思われる方はきっと沢山いるでしょう。
しかし自分が書いてもそのコードはバグってないと保証できるのでしょうか?
かのマーク・ザッカーバーグもこう言っています。

これみて.jpg

そう、とりあえず使ってみればいいでしょう!
よく運用している途中でそのOSSのバグを見つけたりします。
もし見つけた場合は、正しく動くように利用していたOSSのコードを参考に自分で改善した奴をnpmに公開したりします。もしくは利用していたOSSへ感謝の気持ちを込めてプルリクエストを作ってあげましょう!
そう、結局は自分で書いても他の人が書いてもバグる時はバグるのでとりあえず使ってみてバグったら直しましょう!

ストアの設計

Vuexではできるだけわかりやすくシンプルにイベント名に付けてあげましょう!
それはどういう事でしょうか?いくつか例を出していきます!

悪い例:

const initialState = () => ({
  subject: '',
  text: '',
});

const actions = {
  async fetchMail({ commit }) {
    const mail = await MailApi();
    commit('setSubject', mail.subject);
    commit('setText', mail.text);
  },
};

const mutations = {
  initialize(state) {
    Object.assign(state, initialState());
  },
  setSubject(state, subject) {
    state.subject;
  },
  setText(state, text) {
    state.text;
  },
};

これはよく見るコードです。
全てのstateの各フィールドを全部カプセル化するやつですね。とても良くないです。
ただただコードが冗長になり得られるメリットは殆ど無いでしょう。むしろデバッグツールで見る際に大量のただのsetterイベントが流れて本当に探したいイベントを関しするのが困難になります。

改善例:

const initialState = () => ({
  mail: null,
});

const actions = {
  async fetchMail({ commit }) {
    const mail = await MailApi();
    commit('initializeMail', mail);
  },
};

const mutations = {
  clearMail(state) {
    Object.assign(state, initialState());
  },
  initializeMail(state, mail) {
    state.mail = mail;
  },
};

この改善例ではこのmail stateは初期化の時しか変更されないことが伝わります。
また、何を担当するかもわかるでしょう!
では、mailのデータ構造はどんな感じになるのでしょうか?それが唯一の欠点です!
しかしこの欠点はTypeScriptinterfacetypeを利用することにより改善できます!TypeScript最高ですね!

Vue.jsでスケールアウトする際のTips

はじめに

仮想DOMを利用したウェブフロントを初めて構築する際にVue.jsを選ぶ事はよくあることだと思います。
しかしjs + es6で書いている際に、コンポーネントの粒度がバラバラできっと再設計してこういう風に改善しましょう!そしてテストを導入してバグを減らしましょう!という流れが生まれるかと思います。とても自然な流れです。
しかしきっとこれを達成するためには沢山のバグをリリースしてしまうでしょう!このメソッドはこのコンポーネントにはないよ!この変数はないよ!というコンポーネントをリファクタリングした際の障害です!きっとmapState, mapActions周りも怪しいでしょう!きっと次にこう思います。リファクタリングしたけど前と同じ様にコードが動いているか全く自信がないなぁと。そう、これがall any jsの障害です!

e2eテストを導入

vue.jsのe2eテストではcypressが個人的にオススメです。とても簡単に素晴らしいe2eテストができます!
これを導入することによりリファクタリングの際に発生する不整合をある程度検知できます!

静的解析を有効化

e2eテストをしっかり作ればほとんどの事は予防できますが、未定義への参照エラー検知はもっと早い段階で検知したくなるでしょう。
TypeScriptを導入する時が来ましたね!これによって減らせるバグは沢山あるでしょう。しかし、それでもtemplate内の静的解析は全く役に立ちません。はい、render関数とjsxの出番です!これで完全に未定義参照問題は解決されました!
あなたのコードはきっと今AngularとReactを足して2で割ったようなコードになってしまうでしょう!

よく聞く反対意見

しかしきっとそこまでするならReactを使ったほうが良いじゃん!って頭の良い人は言うでしょう。
そう、はじめからReactを使って堅牢にシステムを作成していればよかったんです!高い学習コストを支払いながらね!
しかし、プロジェクトは既にVue.jsで作られています!Reactに移行するには大量のリソースが必要です!どれだけの再学習コストや再実装コストがあるのかと思うと目眩がするでしょう。しかもその間は業務に必要な新機能のリリース、改善なども必要なのです!そもそもVue.jsは雑に作るのが向いているから雑に作るべきなど、それらの主張は大きな間違いでしょう。Vue.jsは沢山の選択肢をくれるだけで、我々がその中で一番簡単な物を選んでるだけです。まるでPHPのようにね。では型付けを強化したいからC#やJava、Goに移行した方がいい、PHPは雑に作るべき!という考えは正しいのでしょうか?きっとPHPerから一笑にされるでしょう。
これの真のメリットは、プロトタイプを素早く作れ、その後事業が成功したらシステムを安定させるために素早く既存のコードを再利用して堅牢性を付与できる事です。最小のリソースでね!初めから成功するか、継続するかわからないシステムを堅牢にテストを全部書いて作成するのは本質ではないでしょう。

※ 私が触った感じだとReactで実現できることは全てVue.jsでも実現できます。コントロールドコンポーネント以外はね

最後に

如何だったでしょうか?
Vue.jsはVue.js特有の技量を求められますが、ReactやAngularもそれ特有の技量やテクニックが求められます。
しかし特にstoreや状態管理問題周りで共通する問題はとても多いので他のフレームワークのテクニック等を試してみると案外色んな発見があって楽しいと思います!