はじめに
私はVue.js with Vuexを使った業務で1画面30APIを叩く必要のある画面から、たったの数APIしか叩かないけれど、代わりにUIがとても機能的で複雑な画面まで設計し、構築しました。
もちろん、Vue.jsのコンポーネントシステムをフル活用し、Vuexを入れていないプロジェクトの経験もあります。
現在は構築したシステムを保守・運用しています。
また、勉強の為にReact、 Angular、 最近はElm等にも少し触れています。
その際に得られたノウハウ、Vue.jsが他のライバルフレームワークと比べた際に現時点で本当に勝っている利点やノウハウを言語化し、共有出来たらと思います。
※ 記事の内容に意見がありましたら直接編集リクエストをください。
Vue.js含めどのフレームワークも利点・欠点があります
せっかくなので自分が把握しているVue.jsの利点と欠点を説明しようと思います。
利点
Vue.jsの武器は単一ファイルコンポーネントとそのシンタックスです!
コンポーネントシステムやファイルの粒度は思想は違えどReactでも似たような事はできますし、Angularでも似たような事はできますが、Vue.jsの場合は1つのコンポーネントを作るのに必要なファイルがたったの1つであり、そのファイルの中に慣れ親しんだhtmlで描画部分を書き、慣れ親しんだ普通のjavascriptでコンポーネントに必要な処理や表示ロジックを書け、css in js, css module, sass, stylus等、好きな方法でそのコンポーネントにだけ当たるスタイルを書けます。
これが他のフレームワークと比べて圧倒的に見やすく、学習していない状態でもなんとなく触れるレベルでわかりやすく、シンプルでしょう。
利点を妨げる要素
Vuexはできる限り入れないほうが良いよという話
Vuexを入れるメリット
- Vuexを入れるとFluxライクなアーキテクチャが採用でき、model層でのデータ管理が可能になります。
- 設計の仕方によってはmodel同士の結合度等を自由に操作でき、かなり疎結合にもできます。
また、これはテストしやすいです。 - etc...
デメリット
- model同士の結合度をコントロールするのはかなりの設計面の技術力が要求され、苦しんでいる話をよく耳にします。
- そこそこjavascript力も要求されます。
- Vue本来の1ファイルで完結するような圧倒的な見通しの良さがなくなってしまいます。
- model層が発生するとそれだけ複雑になりますし、イベントが誰から発行されたのかの管理も難しいです。
- Typescriptを入れない場合は、Vuexとのミスバインドもよく発生しますし、Vuexのロードマップにはこれから破壊的な変更が沢山入る事が記されています。
- Typescript対応も凄く困難が多いです。
- Vueは実はReactのようなコントロールドコンポーネントが作れません。dataがこれだから、表示されている値はこれという保証が一切ないです。
- Stateが代入構文しか受け付けない為、Object.assignですり抜ける以外にStateでUnionTypeを使う方法がないです
Vue + Vuexの感想
もちろん部分的に状態管理が本当に必要な箇所があれば入れるのは良いと思いますが、本当に最低限の利用に留めた方が良いと思います。
しかし現時点で全体的にVuexを入れるとなると、Vue.jsの長所であるSFCの恩恵が大分減るので、他のフレームワーク使ったほうが良いと思いました。
欠点
顕在化した業界主流とのズレ
現時点では、ある程度の中・大規模開発ではTypescriptを利用するのが業界の主流です。
Vue.jsの場合だとここで1歩遅れを取っていて、その理由を以下に上げます。
-
vueファイルはオリジナルファイル形式であるため、エディターでvueのサポートをするのが大変なのです。2019年3月1日時点だと、vscodeではvueファイルのtsのバージョンと本当に入れているtypescriptのバージョンに差分がでています。もしかしたら自分が使っているのよりも良いvueの構文解析ツールがあるかもしれませんが、このvueファイルのメンテナンスや出来に影響を受ける開発者さんは多いでしょう。また、構文解析ツールを作る人達の大変な声をよく耳にします。
-
ファイルの拡張子がvueであり、オリジナル構文なため、何がそのファイルからexportされているか既存の解析ツールの資産だとわかりません。例えば
import Hoge from Hoge.vue
とした場合、現時点だと、とりあえずvueインスタンス又はvueのコンポーネントオプションがdefaultでexportされているよ!ぐらいに型をつけるのが精一杯でしょう。
改善
Vue.jsの今後
Vue.jsのロードマップはここにあります。
https://github.com/vuejs/roadmap
Evan Youさんが言うには
- Vue.jsをTypescriptで作り直し、vueのtemplate内でも型は効くようになるし、tsxのサポートも強化するようです。
- どうしようもない部分にanyとかの型を付けていたのもきっと消えると期待しています。
-
import Hoge from Hoge.vue
とした時もちゃんと型がついているのを期待していいのではないかと思います。 - Vuexでよく型が辛いと言われますが、その部分も改善すると思われます。
- VueのClass Base Componentをネイティブにサポートするなどの細かい追加もあります。
なので、Vue.js + Typescriptをしっかりとやりたい場合はVue.js3のリリースを待つのが良いのではないかとおもいます。
フロントを新しく作る場合どのフレームワークで作ればいいの?
個人的には
- javascriptがそこまで得意でもないフロント初学者 => Vue.js
- Typescript + classベースの構文が好き => Angular
- 関数型の構文が好きな場合はReact
- バックエンドをScala、Haskellとかで書いてたことがあり、且つ好き => Elm
とかでいいんじゃないのだろうかと思います。
まとめ
確かに受けれる恩恵もありますが、もしVuexを全面的に入れるぐらいなら現時点だと他のフレームワークで書いたほうが良いのは間違いないでしょう。
Vue.jsの最も協力な武器であるSFCの質を上げるTips
ほとんどのバグは変数から来ます。
もし全ての値が定数なら状態から来るバグはほとんど無くなるでしょう。
ここではこの変数や式を極力減らしたり、見通しを更によくするTipsを紹介したいと思います。
htmlやvue標準で用意されているタグやコンポーネント名と同じ名前のコンポーネント名を付けない
もしhtmlやvue標準で用意されているタグやコンポーネント名と同じ名前のコンポーネント名を付けたい場合は、The
prefixを付けるなどして、必ず別名にしてください。
参考リンク
自分が以前遭遇したバグに、次のようなものがあります。
プロジェクト全体にprettierでフォーマットをかけて、CIでもprettier必須にしたところ、ヘッダー部分とか色んなコンポーネントが表示されなくなり、アプリが一時的に壊れました。
それの原因は、Header部分のコンポーネントをHeader
と書いており、prettierによってheader
に修正されたことでした。
これにより、htmlのheaderタグがただ表示されるようになってしまいました。
他にも同じようなコンポーネントにいくつか出会いましたが、この命名方式はそのようなバグから逃れる為です。
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()
ってコードを書いて、そのメソッドを実行している可能性が完全になくなりましたね!
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つ減りました!おめでとうございます!
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
タグを最大限に有効活用
次のような、同じ 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などスタイルを当てる奴は反応しません
フラグを極力減らす
通信をするコンポーネントでは通信中状態と通信していない待機状態があります。
例えばこれを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つ減らし、不整合が絶対に起こり得なくなりました!
今回の例はとてもシンプルですが、画面のタブを管理する時など状態が多くなればなるほど役に立つテクニックになります。
必須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をバインドし忘れてもエラーがでるのですぐに気付けますね!
汚いコンポーネントをスマートにするテクニック
レイアウトやアニメーションに関するコードと表示用コンポーネントのコードが一緒になっているコード等が対象です。
大抵の場合別に良いのですが、v-for
でリストを表示して、そのリストの各アイテムにも詳細情報の開閉ボタンがあり、クリックされたら詳細が開示されるなどのコードがあります。
まぁ大抵ここまでのコードを1つのコンポーネントで書くと、とてもではないですがメンテナブルなコードとは言い難いですね。バグが発生しても各アイテムの開閉状態のコードなど色んなコードが一箇所に書かれているため何が原因か特定するのにも時間がかかりますし、破壊する恐れもあります。
では、slot
に渡されたアイテムの開閉だけを担当するコンポーネントと、専用のslot
をいくつか用意したレイアウト用のコンポーネントを作成し、それをメインとなるコンポーネントで使ってはどうでしょうか。コードがとても見通しがよくなります。
また、メインとなるコンポーネントからはきっと大量のインスタンス変数が消えてバグが減ったかと思われます。
一応これは開閉だけするコンポーネントのサンプルです。
これは自分がフォークして作ったライブラリで、メンテしてないのでお世辞にも綺麗なコードとは言えませんが、参考程度に。
https://github.com/kahirokunn/vue-slide-up-down-component
OSSのUIライブラリを利用しましょう
1からプロダクトを作成する際にdialog、tooltip、form、validationなど他にも目眩がする程の大量のコンポーネントを作る必要があります。しかも自社でそういうのを全部書いてるときっと粒度も何もかもがバラバラでしょう。しかもレスポンシブ対応もできるだけしたい!あぁ、これはとても大変です!
そういう場合は既存のOSSからUIライブラリを拝借しましょう!
その際に重要なチェック項目として
- 活発
- ドキュメント、サンプルコードが豊富
- レスポンシブ
などがあります。
ちなみに、よくできたUIライブラリは基本的にCSSを上書きできますので、大抵のデザイン要件を満たせます!
それでもデザイン要件を満たすのが辛い場合はデザイナーさんと相談しましょう!「このUIライブラリを使っていてできるだけこれに沿って工数を減らしたい」といえばきっと協力してくれるでしょう!
ちなみに私のオススメはVuetifyです。これはとてもよくできていて、開発が活発で、ドキュメントやサンプルコードが豊富でレスポンシブ対応です!
※ 自社専用のUIライブラリ別リポジトリで用意し、上手くメンテできるほどの潤沢なリソースがある環境は例外とします。
OSSのUIコンポーネントなどを積極的に利用しましょう
私はよく、開発が全然活発でなくメンテされていないが現在のニーズにあったOSSのUIコンポーネントがあった場合は、とりあえず利用します。
それはとても怖いことではないでしょうか?もしそのOSSがバグっていたらと思うとゾッとするのではないですか?そう思われる方はきっと沢山いるでしょう。
しかし自分が書いてもそのコードはバグってないと保証できるのでしょうか?
かのマーク・ザッカーバーグもこう言っています。
そう、とりあえず使ってみればいいでしょう!
よく運用している途中でそのOSSのバグを見つけたりします。
もし見つけた場合は、利用していたOSSのコードを参考に、正しく動くように自分で改善した奴をnpmに公開したりします。もしくは利用していたOSSへ感謝の気持ちを込めてプルリクエストを作ってあげましょう!
そう、結局は自分で書いても他の人が書いてもバグる時はバグるので、とりあえず使ってみてバグったら直しましょう!
最後に
如何だったでしょうか?
最近あまり記事を書いていないので久しぶりに前の記事の延長として書いてみました。