はじめに
最近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
での開発体験はまだまだ改善できるものでした.
@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
を利用して書いてみます
一切の型エラーが出ておらず、スッキリ書けて最高です.
これだ!感がありました.
一応軽い内容ですが、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
には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
が変更された際にしか再計算されません.
<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内でも使われてそうな雰囲気があります。
また、もしかしたら他のcomponent
がthis.$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度だけするんだな」ってのが凄く明瞭だと思います。
また、他のcomponent
がthis.$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
が参照できません.
そのため、count
をtemplate
内で参照していない事がすぐにわかって、とても良いです.
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
が参照できません.
そのため、count
をtemplate
内で参照していない事がすぐにわかって、とても良いです.
フラグを極力減らす
通信をするコンポーネントでは通信中状態と通信していない待機状態があります。
例えばこれを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
が参照できません.
そのため、currentState
をtemplate
内で参照していない事がすぐにわかって、とても良いです.
必須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が固まり次第そちらの記事も書こうと思います.