Vue.jsを使う際のベストプラクティスについて考える

  • 110
    いいね
  • 1
    コメント

Vue.jsは公式のドキュメントが非常に充実しており、またフォーラムでの議論やコミュニケーションもとても活発です。開発中に何か問題に遭遇した際には、ドキュメントやフォーラムに載っている情報を参照することで、多くの問題は解決できるといって差し支えないでしょう。しかしながら、現実世界のアプリケーションを開発していると、そういった情報だけでは解決が難しい個別具体的な問題や、そもそもどう実装すれば良いのかわからない、といった場面に遭遇することも多々あります。

筆者自身がVue.jsを利用してフロントエンド開発をしてきた経験に加え、Vue.jsの公式のドキュメントやサンプルプロジェクト、そしてVue.jsを利用しているOSSのプロジェクトやVue.jsのプラグインなどのソースを読んで蓄えてきたノウハウを本資料にまとめました。

「ベストプラクティス」と銘打ってはいるものの、筆者の好みや開発経験に依存する部分は大きいでしょう。本資料をより意義のあるものにするために、Vue.jsを利用して開発をする際のノウハウやプラクティスをお持ちの方は、ぜひコメントや編集リクエストを通して教えてください!

バージョン

当たり前の話のようですが、新規で開発をはじめるプロジェクトであれば、Vue.jsの最新のバージョンを使うに越したことはないでしょう。2016/12/18の時点ではv2.1.6が最新となっています。

また1系のバージョンを利用しているプロジェクトは、2系にアップグレードすることをオススメします。破壊的な変更も含まれているので、1系から2系へのアップグレードは容易ではないかもしれませんが、それでも多くのメリットを享受できるでしょう。1系から2系にアップグレードする際に利用できるマイグレーションツールがあるほか、deprecateされるAPIについての案内やアップグレードガイドも丁寧に書かれているので、それらを参照してください。

ref:

パッケージ管理ツール

Vueプラグインのリポジトリを色々と見てみると、従来通りnpmパッケージの管理にはnpmを利用しているところがほとんどのようでした。開発が盛んな一部のリポジトリでは、yarnを利用しているところも見かけましたが、数としては多くはないようです。

しかしながら、npmと比較してyarnは依存するライブラリが多いときほどインストールが速いというベンチマーク結果も出ているので、新規のプロジェクトではyarnを使わない理由はないかもしれません。CIなどでyarnを使う場合は、キャッシュ周りに気をつけて使う必要がありそうです。

ref:

ビルドツール

Vue.js公式によって用意されているvuejs-templatesには、webpackとbrowserify用のサンプルの設定ファイルと実装があります。それぞれのリポジトリのスター数を見ると、webpackの方が圧倒的に人気のようです。
Vue.jsのライブラリなどを見ても、webpackを利用しているところがほとんどですし、アセット周りの強力な機能を備えたwebpackを使うのが良さそうです。

記法

v-onv-bindを利用する際は、やはり簡潔な短縮記法を利用した書き方が好まれるようです。また複数人で開発を行うプロジェクトであれば、eslintや.editorconfigなどを利用して、記法や構文の統一化を図るのが一般的になっています。

propsの型チェック

propsを通して渡ってくる値の型チェックは、出来る限り使った方が良いでしょう。例えば正の整数を期待するpropsである場合は、Number型のチェックだけではなく、許容してはいけない値をvalidateすること(マイナスの値でないことをチェックする)なども、気づきにくいバグを減らす大きな助けになります。

// not so good
Vue.component('child', {
  props: ['age']
}) 

// good
Vue.component('child', {
  props: {
    age: {
      type: Number
    }
  }
}) 

// better
Vue.component('child', {
  props: {
    age: {
      type: Number,
      required: true,
      validator: function(value) {
        return value >= 0;
      }
    }
  }
}) 

propsの型チェックには、複数の型を指定したり、あるいはnullと指定することですべての型を許容することも可能ですが、これらは出来る限り使わないことをオススメします。どうしてもnullを指定しなければならないような場合は、ただ1つの型の値を渡せるように、コンポーネントやpropsとして渡すべきデータの構造を見直してみると良いでしょう。

ref:

ライフサイクルフックの活用

Vue.jsには便利なAPIがたくさん用意されていますが、イベント系のメソッドや細かいコンポーネントオプションを駆使する前に、実装しようとしている処理や挙動がライフサイクルフックをうまく活用することで実現できないか検討してみましょう。筆者の経験上、コンポーネント同士が思ったとおりに協調しないときは、ライフサイクルフックの使い方が悪かった、ということがよくあります。そういう場合は、得てしてwatch$emitを必要以上に複雑な形で濫用してしまっていました。ライフサイクルフックをうまく組み合わせることで、実装は自然とシンプルな形に落ち着きます。

コンポーネントが単体で存在するようなミニマルな状況下では、createdmountedのどちらのライフサイクルを利用してもあまり大きな差はないかもしれません。しかし、コンポーネント同士がpropsを通じて協調動作をするような場合では、親コンポーネントのcreatedと子コンポーネントのmountedなどのタイミングの差をよく理解してコンポーネントを組む必要があります。

new Vue({
  mounted: function() {
    console.log("Hello from parent");
  }
})

// => "Hello from parent"
Vue.component('child', {
  mounted: function() {
    console.log("Hello from child");
  }
})

new Vue({
  mounted: function() {
    console.log("Hello from parent");
  }
})

// => "Hello from child"
// => "Hello from parent"
Vue.component('child', {
  mounted: function() {
    console.log("Hello from child");
  }
})

new Vue({
  created: function() {
    console.log("Hello from parent");
  }
})

// => "Hello from parent"
// => "Hello from child"

v-forで表示された要素の削除

v-forでレンダリングされたアイテムのリストのうち、ユーザーの操作によって特定の要素を削除する、というような場面はよくあると思います。これを実現する方法は色々と考えられますが、多いのは以下の2つのアプローチのようです。

  1. 削除対象の要素(子コンポーネント)が、自身の削除を親コンポーネントに移譲するパターン(削除をイベントとして$emitし、実際の削除処理は親が行う、もしくはstoreにコミットする)
  2. 親コンポーネントの関数として削除処理を実装し、削除関数を子コンポーネントにpropsとして渡す。実際に削除する際は子コンポーネントが受け取っている関数を実行し、自身を削除する

筆者個人的には、どちらの方法がもう一方に比べて特に優れているとは思いませんが、UIやコンポーネントの分割単位の観点から見て、処理の流れがより自然な方を選ぶのが良いと思います。なお、子要素の削除に加えて、追加の処理がいろいろ付随するような場合は、2つめの方法の方が扱いやすいかもしれません。

// パターン1
Vue.component('child', {
  methods: {
    removeItem: function() {
      this.$parent.$emit('removeItem', this.index);
    }
  }
})
// パターン2
Vue.component('child', {
  props: {
    removeItem: {
      type: Function
    }
  }
})

外部ライブラリのコンポーネント化

サードパーティのライブラリ、特にUI関連のライブラリの場合は、Vueインスタンスの中から直接ライブラリを利用するのではなく、Vueのコンポーネントとしてラップすることを検討してみてください。別のコンポーネントからであっても、VueのAPIを通じて協調や命令ができるようになり、サードパーティライブラリのAPIについて関心を寄せる必要がなくなります。

また、汎用的に使いたいUIの効果(アニメーション、トランジションなど)などは、コンポーネントとディレクティブの両方を用意しておく、というのも1つの手かもしれません。こうすることで、この効果を利用する側のコンポーネントの事情に合わせて、コンポーネントとして利用するか、あるいはディレクティブを通じて利用するかを選ぶことができます。

ref:

非同期通信ライブラリ

少し前まではvue-resourceがVue.js公式の非同期通信ライブラリでしたが、現在Vue.jsが公式として提供している非同期通信ライブラリはありません。そもそもがVue.jsは外部ライブラリを組み込みやすいようにできているので、普段使っていて慣れている非同期通信ライブラリを使うのが良いでしょう。

公式からは外れたものの、vue-resourceを利用しているプロジェクトも多いようですし、JavaScript界隈の状況を見るとaxiosrequestなどが人気のようです。また、ブラウザのfetch APIを利用するのもアリでしょう(Safariではまだネイティブ実装されていないため、polyfillの利用が必要です)。

ref:

fluxアーキテクチャの導入

昨年ごろからfluxアーキテクチャがフロントエンドに新しい潮流をもたらし、Vue.jsの世界にもVuexというfluxライクなライブラリが公式から提供されています。Vuex自体は非常によく出来たライブラリではありますが、アプリケーションの規模や複雑度とよく照らし合わせて、利用するかどうかを検討するべきだと筆者は感じます。

筆者個人の感覚としては、小〜中規模のアプリケーションであれば、Vuexを導入するよりも前に、Vue.js公式ドキュメントに書かれているようなstoreパターンの導入による状態管理などを適用する方がベターだと考えています。Vuexは状態の管理や更新に秩序をもたらすものの、中途半端に導入してしまうとかえって整合性がとれなくなったり、また小さいアプリケーションの場合では、fluxアーキテクチャの導入により無用な複雑さを生んでしまう懸念もあります。

Vue.jsアドベントカレンダー2016にもある、@nekobatoさんによる構造の複雑さとVuex書き分け が非常に参考になるので、興味のある方はそちらを参照してみてください。

シングルファイルコンポーネント

ビルド環境やプロジェクトの開発環境に大きく依存しますが、シングルファイルコンポーネント(.vueファイル)を利用できる場合は、積極的に利用した方が良いでしょう。コンポーネントとは、ユーザーインターフェースの「見た目」と「振る舞い」をもとに分割した部品の単位であり、テンプレート(HTML)と装飾(CSS)、そして振る舞い(JavaScript)が1つのファイルで完結していることは理にかなっていると考えています。フロントエンド開発者とデザイナーが協働するような場合においても、シングルファイルコンポーネントは統一的な作業環境を用意してくれるので、効率良く協調作業ができると期待しています。

コンポーネントの再利用

「シングルファイルコンポーネント」の項でも書いたように、コンポーネントとは極端に言えばUIを分割した部品であり、またUIの部品はそれが所属するページの文脈化にあります。同じように見えるUIであっても、実は振る舞いが違っていたり、異なるエッジケースを持つ、といったことは少なくありません。

こういった「一見同じように見えて実は違う」部品を共通のコンポーネントとして実装してしまうと、引数やpropsによる制御に加え、コンポーネント内の条件分岐が増えることになりかねません。これは共通化されたコンポーネントを利用する側にも大きな負担を強いることになり、バグの温床にもなりうることは想像に難くありません。

こういった状況は、UIの設計を見直す良いタイミングと捉えることもできますが、いずれにしろ無理な共通化/汎用化は良い結果をもたらさないことが多いことは強調しておきます。異なるコンポーネントから共通部分を見出して共通化するよりも、一度共通化されたものをバラす方が、はるかに難しいです。ブラウザのパフォーマンスも日々向上し、ほとんどのクライアントは高速なネットワークからWebアプリケーションを利用します。多少ファイルサイズが大きくなってしまったとしても、無理な共通化は控え、コードベースを扱いやすい状態に保つことの方がメリットは大きいのではないでしょうか。コンポーネントを再利用/共通化する前には、一度共通化のpros/consや必要性をじっくりと検討してみることをオススメします。

ref:

テスト

Vue.jsアドベントカレンダー 2016にもいくつかテスト関連の記事が上がっていますが、フロントエンド開発がどんどんと複雑化してきている昨今、Vue.jsを利用したコンポーネントのユニットテストを書く必要性は高まってきていると感じています。

そのコンポーネントの実装を見ただけで、コンポーネントの動きや処理がすべて理解できるような閉じたコンポーネントであれば、わざわざテストは書かなくても良いかもしれません。しかし、コンポーネント間でpropsが受け渡しされ、それをもとにコンポーネントの挙動が変わるようなケースでは、そのコンポーネントの振る舞いが宣言的に記述されたテストコードがあると安心です。また筆者自身は、コンポーネント内で非同期通信を発行し、レスポンスの内容にもとづいてVueインスタンスに値をセットするような振る舞いをコンポーネントが持つときも、ユニットテストを書くようにしています。このあたりのテストを書くコストとそれによって受ける恩恵のバランスは難しいですが、やはりテストがあるとリファクタリングをする際の安心感は絶大です。

ref:

まとめ

Vue.jsに限らず、どんなフレームワークであっても、フレームワークの性質や癖を理解して利用することが重要であることは疑いようがありません。また、開発するアプリケーションもさまざまであり、開発者には必要とされている要件に合致した設計や実装、ライブラリの利用をすることが求められています。本資料がVue.jsを利用してフロントエンド開発をしている人、これからしようとしている人の一助になれば幸いです。

この投稿は Vue.js Advent Calendar 201618日目の記事です。