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

# はじめに

最近[iCARE](https://www.icare.jpn.com)さんの所で`Vue.js`を一緒にやらせていただいているのですが、フロントの技術スタックが`GraphQL` + `Vue.js` + `TypeScript`で開発しており、そこでのVue.jsの開発体験がかなり良く、iCareさんの詳細なノウハウを公開しても良いと言っていただけたので、言語化し、整理して、共有出来たらと思います.

※ いつも通り記事の内容に意見がありましたら直接編集リクエストをください

## 前置き

今回のサンプルは`@vue/cli`を利用し、プリセットはTypeScriptだけいれときました. versionは`2020年7月16日`時点の最新`4.4.6`です.

```bash
$ vue -V
@vue/cli 4.4.6
```

リポジトリはこちらから見れます.
https://github.com/kahirokunn/vue-ts-sample

`Vue.js` + `TypeScript`の対応状況は日々改善されています.
以前までは`.vue`を利用しても`Vue.extend`を利用してコンポーネントを開発する場合はこのシンプルなコードでも型のエラーが出てしまい、`TypeScript`での開発体験はまだまだ改善できるものでした.
[![Image from Gyazo](https://i.gyazo.com/f52be85693b18d9782d8c718ef3e8291.gif)](https://gyazo.com/f52be85693b18d9782d8c718ef3e8291)

`@vue/composition-api`を利用すれば`TypeScript`の体験を向上できます.
早速`@vue/composition-api`をインストールします.

### install `@vue/composition-api`

インストール方法はこちらのリポジトリに書いてあります.
https://github.com/vuejs/composition-api

一応install後の`$ git diff`を貼り付けておきます.

```diff
diff --git a/package.json b/package.json
index 406eb0c..80a17a6 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
     "lint": "vue-cli-service lint"
   },
   "dependencies": {
+    "@vue/composition-api": "^1.0.0-beta.3",
     "core-js": "^3.6.4",
     "vue": "^2.6.11",
     "vue-class-component": "^7.2.2",
diff --git a/src/main.ts b/src/main.ts
index 1f5f073..f43868c 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,6 +1,9 @@
 import Vue from "vue";
 import App from "./App.vue";
 import router from "./router";
+import VueCompositionAPI from '@vue/composition-api';
+
+Vue.use(VueCompositionAPI);

 Vue.config.productionTip = false;
```

### before after
先程型エラーが出ていたApp.vueと全く同じように動作するコードを今度は`@vue/composition-api`を利用して書いてみます

[![Image from Gyazo](https://i.gyazo.com/f0706d034cb3dbfaa9316f4d3b1d673d.gif)](https://gyazo.com/f0706d034cb3dbfaa9316f4d3b1d673d)

一切の型エラーが出ておらず、スッキリ書けて最高です.
これだ!感がありました.

一応軽い内容ですが、composition apiに関して興味ある方はよければこちらのスライドもどうぞ
https://slides.com/kahirokunn/composition-api

## Editorでの開発状況

基本veturを使います.
vscodeの場合だと拡張から一発でinstallできるので最高ですね.
一応vscodeで`TypeScript`使う場合は、プロジェクト内の`TypeScript`を使うように設定しましょう(マスト.
また、veturの実験的機能のtemplate内での参照検査をonにすると、多くの場合でtemplate内で存在しないものへの参照をすることが減ります.
`"vetur.experimental.templateInterpolationService": true`

## CIでの検査

### 型検査

CIではどのようにして型検査をすれば良いでしょうか？
`TypeScript`のプロジェクトでは基本的に`$ tsc --noEmit`をすれば、型検査ができます.
しかし、`altJS`である`Vue.js`ではそれが使えません.

そのため、webpack等で検査させるという手段を取ることが一応可能です.
例えば以下のようなプラグインを活用すれば可能です.
https://github.com/TypeStrong/fork-ts-checker-webpack-plugin

しかし、webpackはあくまでバンドラーですので、型検査は軽量な別のツールに任せたいです.
そこで、`vue-type-check`を利用します.
https://www.npmjs.com/package/vue-type-check
https://github.com/Yuyz0112/vue-type-check
これは、vueの構文解析をしてくれる`vue-language-server`を活用しているライブラリで、自前で構文解析をしていません.
そのため、vueの新たな仕様への追従が簡単に可能です.

また、段階的に`TypeScript`に移行しているプロジェクトでも使えるように、以下のプルリクにより、柔軟に解析対象を絞り込めるoption等も用意してあります.
https://github.com/Yuyz0112/vue-type-check/pull/8

これは私の参加しているプロジェクトで上手く動いてくれており、型システムによる多くの単体テストの簡略化を実現してくれています.

## GraphQL
GraphQLを書く時は[graphql-code-generator](https://github.com/dotansimha/graphql-code-generator)を積極的に活用しましょう.

https://github.com/dotansimha/graphql-code-generator/tree/master/packages/plugins/typescript/vue-apollo

## 簡易Runtime Error Checkとスクリーンショットテスト

e2eテストとは別に、Storybookを起動し、そこでエラーが発生していないか検査しています.
互換性のない修正をした場合はエラーが発生し、CIを通しません.
同時にそれぞれのコンポーネントに対してスクリーンショットを取っており、以前の結果と比べた際にズレが会った場合はCIを通しません.
これのおかげでコードのリファクタや移行がしやすくて助かっています.

## 現時点での`Vue.js`の型検査でできないこと 1

`vue-language-server`で検査できるのは、`template`内である程度までの変数の参照のみです.
現時点では`props`とデータの型が正しく紐付けているかの検査はできてません.
こればかりは`vue-language-server`の改善をするしかないです.
自信のある方は`tsx`を頑張って活用すればその辺も改善できるかもしれませんが、`Vue.js`での`tsx`の型のデファクトはまだ用意されていないため、現時点では`tsx`の信頼性は`template`と良い勝負です.

## 現時点での`Vue.js`の型検査でできないこと 2

やはり`emit`がくせ者ですね.
`emit`を型安全に扱いたい場合は、現時点では`Vue.js`の型を拡張しないと無理です. (運用でカバーは型安全じゃない


## Propsの型について

`Props`には`required`や`default`の項目があり、Component内部での型と外から入れるべきPropsの型に差があります.
それを手動でメンテナンスするのにストレスを覚える方は多いと思います.
そのため、`@icare-jp/vue-props-type`を作成しました.

https://github.com/icare-jp-oss/vue-props-type
https://www.npmjs.com/package/@icare-jp/vue-props-type

これを活用することにより、簡単にComponent内部での型と外から入れるべきPropsの型を生成できます.
それを紐付け、参照の連鎖を起こすことにより、より型安全な開発が可能です.

## Vue.jsのcomputedの素晴らしさ

Vue.jsのcomputedは、計算時に参照されたreactiveなものを記録し、それらが変更された際に再計算してくれます.
以下の`memolizedNumbers`は`state.numbers`が変更された際にしか再計算されません.

```vue
<template>
  <div id="app">
    {{ state.numbers }}<br/>
    {{ memolizedNumbers }}
  </div>
</template>

<script>
import { defineComponent, reactive, computed } from "@vue/composition-api";

export default defineComponent({
  name: "App",
  setup() {
    const state = reactive({ numbers: [1,2,3,4,5] });
    const memolizedNumbers = computed(() => state.numbers.map(n => n * n));
    return {
      state,
      memolizedNumbers
    };
  }
});
</script>
```

Reactの場合だと、これら依存してる値が変化した場合再計算をするように指定します.
あれは中々良い体験ではないし、油断するとcommit時に依存を追加されて`useEffect`が循環してAPIコールの永久機関を作成してしまう事もあるのですが、パフォチュするのにはやはりうまく設定するのが必須で、`Vue.js`の場合だとこの辺のメモ化等の体験がかなり良いと思います.

---

以下は[2019年版Vue.jsを使ってる人には必ず知っていてほしいVue.jsの武器とドキュメントに書かれていないコンポーネントやメンテナンスの際に役立つTips](https://qiita.com/kahirokunn/items/6b4834b9a13406535f32)で書いた内容に`@vue/composition-api`の例と意見を単純に追加した内容になります.

## templateで必要ないmethodは極力vueに定義しない

これは、どのmethodがそのcomponentで使われているのかを簡単に把握する為です。

悪い例:

```javascript
export default {
  data() {
    return {
      users: [],
      loading: true
    };
  },
  mounted() {
    this.getUsers();
  },
  methods: {
    async getUsers() {
      const { data } = await this.$axios.get('/api/users');

      this.users = data.users;
      this.loading = false;
    }
  }
};
```

例えばこちら、getUsersがなんかtemplate内でも使われてそうな雰囲気があります。
また、もしかしたら他の```component```が```this.$refs.hoge.getUsers()```ってコードを書いて、そのメソッドを実行している可能性もあります。
チーム開発で「それはしていない。このメソッドは使ってない！消せる！」って言い切るには、全文検索するしか今の所方法がないです。このTipsを適用すればそのような不安もある程度緩和できると思います。

改善例:

```javascript
async function getUsers(axios) {
  const { data } = await axios.get('/api/users');
  return data.users
}

export default {
  data() {
    return {
      users: [],
      loading: true
    };
  },
  async mounted() {
    this.users = await getUsers(this.$axios);
    this.loading = false;
  },
};
```

改善例では、「このコンポーネントはmount時にデータ取得を1度だけするんだな」ってのが凄く明瞭だと思います。
また、他の```component```が```this.$refs.hoge.getUsers()```ってコードを書いて、そのメソッドを実行している可能性が完全になくなりましたね！

@vue/composition-api例:

```javascript
async function getUsers(axios) {
  const { data } = await axios.get('/api/users');
  return data.users
}

export default defineComponent({
  setup(_, context) {
    const state = reactive({
      users: [],
      loading: true
    })
    onMounted(async () => {
      this.users = await getUsers(context.root.$axios);
      this.loading = false;
    })
    return {
      state
    }
  },
});
```

## dataを極力定義しない

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

悪い例:

```js
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は再計算されることはないでしょう。

改善例:

```js
export default {
  props: {
    user: {
      type: Object,
      required: true,
    },
  },
  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つ減りました！おめでとうございます！

@vue/composition-api例:

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

## ```template```内でできるだけ式を書かない

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

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

悪い例:

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


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

改善例:

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

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

@vue/composition-api例:

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

<script>
export default {
  setup(_, context) {
    const state = reactive({
      count: 0,
    })
    return {
      incAndNotify() {
        state.count++;
        context.emit('change', state.count);
      }
    }
  },
};
</script>
```

`@vue/composition-api`の例では`template`内で`count`が参照できません.
そのため、`count`を`template`内で参照していない事がすぐにわかって、とても良いです.

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

次のような、同じ `v-if="..."` が複数回書かれているコードをたまにみます。
お世辞でもスマートとは言い難いですね。

悪い例:

```vue
<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>
```

改善例:

```vue
<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がどのようにマウントされるのかを見てみましょう！

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

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

※ もちろん```v-for```などにも使えますよ😊
※ 注意: v-showなどスタイルを当てる奴は反応しません

@vue/composition-api例:

```vue
<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 defineComponent({
  setup() {
    const state = reactive({
      count: 0,
    })

    return {
      isSmallCount: computed(() => state.count < 5),
      inc() {
        state.count++;
      }
    }
  },
});
</script>
```

`@vue/composition-api`の例では`template`内で`count`が参照できません.
そのため、`count`を`template`内で参照していない事がすぐにわかって、とても良いです.

## フラグを極力減らす

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

悪い例:

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

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

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

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

改善例:

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

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

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

export default {
  data() {
    return {
      currentState: IS_STANDBY,
    };
  },
  computed: {
    isStandby() {
      return this.currentState === IS_STANDBY;
    },
    isInitializing() {
      return this.currentState === IS_INITIALIZING;
    },
  },
  methods: {
    toStandby() {
      this.currentState = IS_STANDBY;
    },
    toInitializing() {
      this.currentState = IS_INITIALIZING;
    },
  },
};
</script>
```

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

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

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

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

export default defineComponent({
  setup() {
    const state = reactive({
      currentState: IS_STANDBY,
    })
    return {
      isStandby: computed(() => state.currentState === IS_STANDBY),
      isInitializing: computed(() => state.currentState === IS_INITIALIZING),
      toStandby() {
        state.currentState = IS_STANDBY;
      },
      toInitializing() {
        state.currentState = IS_INITIALIZING;
      },
    }
  },
});
</script>
```

`@vue/composition-api`の例では`template`内で`currentState`が参照できません.
そのため、`currentState`を`template`内で参照していない事がすぐにわかって、とても良いです.

## 必須```props```に必ず```required: true```を付ける

必須```props```には必ず```required: true```を付けるべきです。
たまにあるのが、必須propsなのにdefault値だけを指定して```required: true```を設定しないケースです。

悪い例:

```javascript
export default {
  props: {
    account: { type: Object, default: null },
    shokai: { type: Object, default: null }
  },
};
```

なぜこれが悪いのかというと、account propに何か入れて欲しいのに、入れ忘れた場合でも一切警告がでなく、バグ発見が困難になるからです。
また、「本当に任意であるpropがどれなのか」を見分ける難易度が上がってしまいます。

改善例:

```javascript
export default {
  props: {
    // account: { type: Object, default: null },
    // shokai: { type: Object, default: null }
    account: { type: Object, required: true },
    shokai: { type: Object, required: true }
  },
};
```

やりました！
改善例ではaccountにpropをバインドし忘れてもエラーがでるのですぐに気付けますね！

## 最後に

如何だったでしょうか？
最近あまり`Vue.js`の記事を書いていないので久しぶりに前の記事の延長として書いてみました.
Vue3が固まり次第そちらの記事も書こうと思います.