大阪からこんにちは、福山健(@kenfdev)です!
先日「v-kansai Vue.js/Nuxt.js meetup #2」で「Nuxt.jsのinjectでインジェクトしてみる話」という内容で登壇させていただきました。その内容について記事にも残しておこうと思います。
スライドに関しては以下で公開しています。
speakerdeck.com
前提知識
- 基本的なNuxt.jsの機能
- Vuexをなんとなく知っている
- TypeScriptがなんとなく読める
Nuxt.jsの inject
皆さん、Nuxt.jsには inject
という機能があるのをご存知でしょうか?意外と知られていないんじゃないかと思うのですが、公式ドキュメントの「統合された注入」にも記載されているNuxt.jsのプラグインの機能です。この inject
を使うことで、Nuxt.jsのアプリケーションで、いろいろな場所から共通で利用したい関数や値を(グローバル変数にすることなく)呼び出すことができます。「グローバル変数や、外部モジュールから import
で読み込んじゃえばいいんじゃないの?」と思われるかもしれませんが、自分の興味のあるコンテキスト内(例えばある関数Aの中身)に、差し替え可能な状態で関数や値が用意されていると、何かと便利です(特にtestabilityの観点で)。このトピックと関連が深いのが「Dependency Injection(依存性の注入)」です。
Dependency Injection?
Dependency Injectionが何なのか。何がうれしいのか。と思われる方もいると思いますが、それを話し始めるとこの記事が(ただでさえ長いのに)すごく長くなってしまうので、ぜひ「dependency injection 何がうれしい」と ググってみて ください。先人たちの素晴らしい記事が何個も出てくると思います。
Demoアプリ
inject
を実際に小さなアプリで使ったものを上図のようなアーキテクチャで公開しています。
github.com
このアプリはVue.jsとNuxt.jsのGitHubのスター数をGitHubAPIから取得して合算したものを画面に表示するという至ってシンプルなものです。大きな特徴としては「状態管理パターンとはなんですか?」で紹介されているVuexの登場人物に加えて、「Gateway」という存在を追加しています。これはActionsから直接 axios
などを使ってリクエストを投げるのではなく、Gatewayさんに依頼して(ワンクッション置いて)リクエストを投げるという意味です。
プラグインの書き方
プラグインの書き方は、次のような記述を plugins/dependencies.ts
というファイル(TypeScriptを使用しています)に書きます。
export default (context, inject) => {
const environment = process.env.environment || 'development';
let gitGateway: IGitGateway;
if (environment === 'offline') {
gitGateway = new FakeGitGateway();
} else {
gitGateway = new GitHubGateway(axios);
}
const deps: Dependencies = {
gitGateway,
};
inject('deps', deps);
};
inject('deps', deps)
としているところがミソで、これでアプリケーションの様々な場所から $deps
として呼べるようになります。
$deps
の呼び方
上の設定で用意した $deps
は大きく3箇所から呼ぶことができます。
- Nuxt.jsの
context
- Vueのインスタンス
- VuexのActionの中(store自身)
Nuxt.jsの context
Nuxt.jsの context
は様々な場所で参照できますが、例えば pages
の fetch の第一引数にも渡されてきます。次のようにアクセスできます:
fetch(context) {
},
Vueのインスタンス
Vueのインスタンスからは、例えば methods
内から呼び出すことができます。インスタンス自身なので this
から参照できます。
methods: {
onLoad() {
},
},
VuexのActionの中
今回のパターンで使いたい、Actionから参照する方法です。 store
自身にも $deps
が注入されているので、次のように参照できます。
export const actions = {
async [actionTypes.FETCH_STARS]({ commit }) {
},
};
このように、様々な場所から $deps
が呼び出せるようになります。
何がうれしいのか?
では、 $deps
が呼び出せるようになって何がうれしいかというと、ソフトウェアアーキテクチャを考えると色々と例があると思いますが、以下の2点を紹介したいと思います。
- Vuex内が要件の変化に強くなる
- 開発モードみたいな機能を追加できる
Vuex内が要件の変化に強くなる
今回の要件は「リポジトリのスター数をとってきて表示する」というものだったとします。リポジトリがどこにあるかもわからなければ、その数もわからなかったとしましょう。「まだわからないので開発もしない」わけではなく、詳細はともかく、要件は決まっているところから着手していくことができます。
「どうやってスター数をとってくるか」はGatewayさんにおまかせします。それ以上の詳細は今は気にしません。図で表すと次の部分は作ることができるということです。
TypeScriptで表現した場合、Gatewayさんは IGitGateway
として例えば次のように interface
を定義することができます。
export interface IGitStarsResult {
count: number;
}
export interface IGitGateway {
fetchStars(): Promise<IGitStarsResult>;
}
ということで、VuexのActionは次のような実装で書くことができます。
export const actions: ActionTree<State, State> = {
async [actionTypes.FETCH_STARS]({ commit }) {
const { gitGateway } = this.$deps as Dependencies;
commit(mutationTypes.SET_LOADING, true);
const { count } = await gitGateway.fetchStars();
commit(mutationTypes.SET_LOADING, false);
commit(mutationTypes.SET_COUNT, count);
},
};
TypeScriptを使っていて as
を使うのは極力避けたいのですが、今の所 $deps
に型情報を簡単に反映させるには as Dependencies
が良いと思って使っちゃっています。こうすることで次のように補完も効いてちょっと安心して開発ができます。
では、「どうやってスターをとってくるか」が不明な状態で、実際の gitGateway
はどう実装すればいいのでしょう?という疑問がわいてくると思うのですが、わからないうちは次のようにスタブを作っちゃいましょう。 IGitGateway
を満たしつつ、1.5秒後に {count: 2}
を返す FakeGitGateway
を次のように実装できます:
export class FakeGitGateway implements IGitGateway {
fetchStars(): Promise<IGitStarsResult> {
return new Promise(resolve => {
setTimeout(() => {
resolve({ count: 2 });
}, 1500);
});
}
}
これを plugins/dependencies.ts
で差し込むことで、Action内で参照する gitGateway
は FakeGitGateway
のインスタンスになります。
export default (context, inject) => {
const gitGateway = new FakeGitGateway();
const deps: Dependencies = {
gitGateway,
};
inject('deps', deps);
};
以上で、スター数は2で固定となってしまいますが、Vuexの中身の実装は先行して作り切っちゃうことができます。
後になって「GitHubからスター数をとってきて」という詳細がきまったときに、 GitHubGateway
を実装して、 FakeGitGateway
と差し替えることで、Vuex内は何も変えずに最終型を作り上げることができます。
export default (context, inject) => {
const gitGateway = new GitHubGateway();
const deps: Dependencies = {
gitGateway,
};
inject('deps', deps);
};
GitLabからスター数をとってきたい場合であれば GitLabGateway
を実装すればいいですし、GitHubとGitLab両方から取得する場合も、そのようなGatewayを作って差し込めばOKです。
アーキテクチャ寄りな話になってしまいましたが、 inject
を使ったメリットの一つとして紹介しました。
開発モードみたいな機能を追加できる
次に、上の差し替えに関連するのですが、Nuxt.jsの 環境変数 と組み合わせることで、 npm run offline
のように、インターネットに通信しにいかないモードを作ることもできます。次のように plugins/dependencies.ts
を実装することで、実行時のGateway切り替えが可能になります。
export default (context, inject) => {
const environment = process.env.environment || 'development';
let gitGateway: IGitGateway;
if (environment === 'offline') {
gitGateway = new FakeGitGateway();
} else {
gitGateway = new GitHubGateway(axios);
}
const deps: Dependencies = {
gitGateway,
};
inject('deps', deps);
};
こういうモードを分けることで、Integration Test時にGatewayを差し替えたり、あるいは「API側がバグっていてフロントの開発ができなくなる」というような状況を回避することもできたりするので、意外とDX(Developer Experience)の向上に貢献するのではと思います。
気になった点
「うれしかった」と感じた点はいったんここまでとして、やってみて「気になった」点について2点ほど共有したいと思います。
Lazy Loadはされるのか?
中規模くらいなアプリケーションであればさほど気にしなくてもいいのかもしれませんが、 現時点でプラグインのLazy LoadというものはNuxt.jsには無いという認識です。なので、 plugins/dependencies.ts
が肥大化すると、そこに関連するコードはすべてメインのbundleに含まれることになると思います。アプリケーションが大規模になっていくと、bundleのサイズも無視できなくなってくるので、結構致命的な問題じゃないかな、と思っています。ここらへんは今後Nuxt.js的に改善されていくかもしれないので要チェックです。
$deps
はVuexのStoreで参照できれば十分では?
Vuexを使うアプリであれば、ビジネスロジックに関わる部分はAction内にほとんど収まるはずなので、StoreのAction内でだけ this.$deps
が参照できたら十分では?と思いました。 injectのコード を見ても、 store
に $deps
を代入してるだけなので、なんら難しいことはしてなさそうです。Vuexのモジュールは 動的に登録 することができるので、Lazy Loadしながら、 store
の中に注入していけるのではと思います。
まとめ
最後に inject
を使ってのまとめです。
- 中規模なアプリ(明確な線引はできませんが、パフォーマンスとの兼ね合いでしょうか)であれば
inject
を使ってのDIは便利に使えそう
- ↑に関連して、プラグインもLazy Loadできるような仕組みがNuxt.jsにできれば、
plugins/dependencies.ts
も複数ファイルに分割して大規模なアプリでも一つのファイルが肥大化しちゃう問題は回避できそう
- 実は
store
でしか inject
されたものを使わないかもしれないので、 store
にさえ自分で入れてしまえば使わなくても良いのかもしれない
- とは言え、↑でいろいろと言ってますがNuxt.js側でこの
inject
の仕組みを用意してくれているのはうれしい。今後の改善にも期待したい
という感じです!
ながーい記事になってしまいましたがここまで読んでいただきありがとうございます。
サイダスではこんな感じにDXを追求しながら堅牢なソフトウェアを作っていく方法について語り合える仲間も絶賛募集中です!
www.wantedly.com
ではでは!