1118
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

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

はじめに

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

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

前置き

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

$ vue -V
@vue/cli 4.4.6

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

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

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

install @vue/composition-api

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

一応install後の$ git 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

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

一応軽い内容ですが、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を積極的に活用しましょう.

簡易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にはrequireddefaultの項目があり、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なものを記録し、それらが変更された際に再計算してくれます.
以下のmemolizedNumbersstate.numbersが変更された際にしか再計算されません.

<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で書いた内容に@vue/composition-apiの例と意見を単純に追加した内容になります.

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

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

悪い例:

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

改善例:

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度だけするんだな」ってのが凄く明瞭だと思います。
また、他のcomponentthis.$refs.hoge.getUsers()ってコードを書いて、そのメソッドを実行している可能性が完全になくなりましたね!

@vue/composition-api例:

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は変更される心配がないためバグを減らしてくれるとても素晴らしいものです!

悪い例:

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 {
  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例:

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内に直接式を書いた場合は逆に、「この式はどういう式か」をコードを見た人全員が推測しなければなりません。つまりハイコンテキスト、空気を読めって事です。メンテする際に大抵困ります。

悪い例:

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

@vue/composition-api例:

<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が参照できません.
そのため、counttemplate内で参照していない事がすぐにわかって、とても良いです.

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

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

悪い例:

<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-forなどにも使えますよ😊
※ 注意: v-showなどスタイルを当てる奴は反応しません

@vue/composition-api例:

<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が参照できません.
そのため、counttemplate内で参照していない事がすぐにわかって、とても良いです.

フラグを極力減らす

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

悪い例:

<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で良いでしょう。

改善例:

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

<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が参照できません.
そのため、currentStatetemplate内で参照していない事がすぐにわかって、とても良いです.

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

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

悪い例:

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

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

改善例:

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が固まり次第そちらの記事も書こうと思います.

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
1118
Help us understand the problem. What are the problem?