vue
vue.js
Vuex
ssr
storybook

2018年 スタートアップでSSRにVue.jsを導入してWebサービスを開発して得た設計

私がWebサービスのバージョンアップでVue.js導入することになり、取り組んでる内容についてWEBエンジニア向けに簡単に解説する記事です。
例えばjQueryのUIプラグインを沢山使ってるサーバーフレームワークで Vue.js、Vuex、Storybook を導入する際にどうすれば良いのか?などのノウハウについて記述します。

はじめに

サーバーフレームワークのテンプレートへの組み込みが今回の要件です。
つまりVue.jsでカスタムコンポーネントもどきを作り、テンプレートエンジンで使えるようにするのが今回の要件です。

フロントエンド界隈のノウハウ集は大体がビッグスケールしても耐えられるようにフロントエンドとサーバーを開発チームまで完全に分離し、サーバーは API のみ提供する設計でしょう。最近ではVue.jsがどんどん認知され、実験的にRailsに組み込む軽いサンプルが散見するでしょう。
しかし、Laravel、Rails、Django、その他各言語やフレームワークによって、導入方法やノウハウは違います。

実業務での軽い導入は、大抵軽くはありません。要求や難易度は我々フロントエンドエンジニアの想定とはかけ離れており、機能も複雑です。もし甘い設計をした途端、jQueryに匹敵、もしくは超えるであろう負債をプロジェクトに残し、継続的な開発や運用がとても困難になるはずです。
それらの助けになれたらと思います。

知見

全てのコンポーネントは単体で独立して、捨てやすく、一般的なインターフェイスに沿って作りましょう。

変な所で躓いて時間を消耗して納期に追われてる際などは品質を気にせず雑に作り、納期を間に合わせましょう。
そのコンポーネントを拡張する際に、ノウハウの溜まっている新たな状態で作り直せば良いのです。

コンポーネントを努力して使いまわすの、もうやめましょう。

リスクが高まり、保守性が下がるからです。
例えば、複数のコンポーネントで使う 特定の子コンポーネントで callbackA名の props を受け取れるようにして特定条件下で発火するようにします。
親コンポーネントA に要件が増えてに 子コンポーネントを魔改造して 対応し、親コンポーネントA では動いていたのに突然親コンポーネントB ではバグが起こり、調査するのに手間取る可能性があります。
いっその事、templateとprop、インターフェイスだけが定義されている、単体では動作しないコンポーネントに Presenter と名付けて、mixinして使ってはどうでしょうか?
これなら親コンポーネントAに要件が増えた場合でも、他のコンポーネントは影響を受けず、安心して開発ができるでしょう。

相対パスはやめましょう

webpackでトランスパイルする際、エントリポイントからの相対パスで見るので、Storybook を導入する際や、ディレクトリ構成がズレた際に全てズレるし、醜いからです。
webpackのpath resolve機能を利用し、 @/src/components/user-profile みたいに書けるようにしましょう。
可読性がグッと上がります。
またscssや画像をcomponentsでこのように取り扱いたい場合は、babelのmodule-resolverで開発時は速度の為にblobを埋め込み、本番時はトランスパイルされたアセットとして利用できます。

<Some-Component>
  <img src="@/asset/img/hoge.png">
</Some-Component>
["module-resolver", {
  "alias": {
    "@": "./application/vuejs/src"
  }
}]

状態の管理を意識しましょう。

UIコンポーネントの作成単位を意識しましょう。
汎用的な、共通コンポーネントではevent up, props downのみを利用し、storeは利用しないようにして使いやすくすると上手くいくはずです。

Store Moduleの作成単位

ページ単位で Module を作りましょう。
Store はページ単位で持つと良いでしょう。そのページ専用の全てのコンポーネントに自由にアクセスさせる事で、コードが短くなり、開発速度が上がります。また、コードも見れたものになるはずです。
しかし、store 内部に副作用を多く持ち込むのはバグの温床になるので、その副作用の発生条件を意識し、ベストプラクティスに沿ってトリガーを正しい場所で管理すると上手くいくはずです。
全ての状態の管理にstoreを利用したらvarの時代に逆戻りです。
storeはグローバル変数ではないので、管理する値を厳選しましょう。

設計する際は Endpoint、次に Store からにしましょう

UIコンポーネントはとても自由で作るのが楽しいでしょうが、そこはグッと我慢しましょう。
なぜなら、漏れや考慮ズレを最小限にするためです。
API のインターフェイスや型が変わるだけで、そのデータに依存する UI コンポーネント、利用する store のコードへの影響が大きすぎるからです。
また、特定データを操作するコンポーネントの命名を考える際にプロパティ名を流用すればとても時間短縮になるはずです。

図の見方

関係線の見方がよくわからないという方はここを参考にどうぞ

構成図

Flowchart (5).png

最終的に構成はこんな感じになりました。

構成図の解説

Bootstrap

プロジェクトに必要な初期化処理をしましょう。

Configuration

ここでは必要な設定を記載しましょう。
Vue に Vuex や各種プラグインを登録したり、コンポーネントを登録したりします。
これを作成した理由は、 Storybook のサンドボックス環境でコンポーネントを開発、登録する場合、Vuex、プラグイン、コンポーネントの登録処理など、プロジェクトと重複する 設定を再度記載する必要があったからそれを防ぐ為です。

Storybook

コンポーネントのカタログを作ります。
デザイナーと確認作業する際や、現状を把握するのにとても役立つでしょう。
各種アドオンを入れるととてもリッチになり、コンポーネントのサンドボックス開発環境としてとても優秀だと感じて使っています。
躓きポイントとしては、認証が必要なエンドポイントや、各種サーバーに要求する、ページに埋め込まれている仕様の token を回収するのに1つ1つ丁寧に mock の用意が必要な点などです。

Main

プロジェクトのエントリポイントです。
これを webpack でビルドします。
ここでは Bootstrap(初期化処理) を呼び、次に Configuration を呼び、Vueインスタンスを id="app" など適切な所にバインドする所までやります。

Presenter

template とそれを操作するための最低限の機構を用意したロジックが全くないベースコンポーネントです。
mixin などしてメソッドやロジックを登録して使いましょう。

Common Components

汎用コンポーネントです。
event-up、 props-down のみでデータのやり取りをしましょう。
store を使ったり、callback 関数への参照などは受け取ってはいけません。

Page Child Components

特定ページ専用で使われるコンポーネントです。
Page 専用 Store へのアクセス権限を持っています。

Dedicated Component

Page Child Component の部品です。
私はWidgetsディレクトリを配下に作り、その中に入れています。
沢山の部品を扱う大きめの Page Child Component になると、どうしても大きくなるので、それをただブロック単位に小分けにしたやつです。Page 専用 Store へのアクセス権限を持っています。

Page

Full SPA などを使う場合は Page を作成するはずですが、SSR などではテンプレートエンジンがこの役割の代替えになるでしょう。
なので、Page Child Component や Common Component をテンプレートエンジンで、まるで カスタムコンポーネント を使うかのように記述してあげてください。後は Vue が変換してくれるでしょう。
<cool-button>Hello World</cool-button>

Interface Adapter

くわしくはここの Interface Adapter をみてください。
役割は同じです。
フロントエンドでもとても役立つと思ったので導入してみました。
気力があれば Manager を1つ用意して、Interface Adapter をテスト時と開発時、本番時に切り替えられるようにするとより良いのではないでしょうか。

SSR フレームワークとバインドするおすすめのnpmモジュールの紹介

webpack で排出したファイル と 各種 SSR フレームワークを紐付ける際におすすめの npm モジュールは laravel-mix です。
ヒモ付がとても簡単になるので、よければ使ってみてください!

さいごに

新しい技術の導入には幅広い知識と根性が必要です。
一緒にがんばりましょう!