CYDAS Developer's Blog

サイダス技術者ブログ

Nuxt.jsのinjectを使ってDIする

大阪からこんにちは、福山健@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-demo-app
injectを使った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 は様々な場所で参照できますが、例えば pagesfetch の第一引数にも渡されてきます。次のようにアクセスできます:

fetch(context) {
  // context.app.$deps.gitGateway
  // ...
},

Vueのインスタンス

Vueのインスタンスからは、例えば methods 内から呼び出すことができます。インスタンス自身なので this から参照できます。

methods: {
  onLoad() {
    // this.$deps.gitGateway
    // ...
  },
},

VuexのActionの中

今回のパターンで使いたい、Actionから参照する方法です。 store 自身にも $deps が注入されているので、次のように参照できます。

export const actions = {
  async [actionTypes.FETCH_STARS]({ commit }) {
    // this.$deps.gitGateway
    // ...
  },
};

このように、様々な場所から $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; // $depsからgitGatewayを参照

    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 が良いと思って使っちゃっています。こうすることで次のように補完も効いてちょっと安心して開発ができます。

code-completion
Action内でのコード補完

では、「どうやってスターをとってくるか」が不明な状態で、実際の 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内で参照する gitGatewayFakeGitGateway のインスタンスになります。

// plugins/dependencies.ts
export default (context, inject) => {
  const gitGateway = new FakeGitGateway();
  const deps: Dependencies = {
    gitGateway,
  };

  inject('deps', deps);
};

以上で、スター数は2で固定となってしまいますが、Vuexの中身の実装は先行して作り切っちゃうことができます。

後になって「GitHubからスター数をとってきて」という詳細がきまったときに、 GitHubGateway を実装して、 FakeGitGateway と差し替えることで、Vuex内は何も変えずに最終型を作り上げることができます。

// plugins/dependencies.ts
export default (context, inject) => {
  // const gitGateway = new FakeGitGateway();
  const gitGateway = new GitHubGateway();
  const deps: Dependencies = {
    gitGateway,
  };

  inject('deps', deps);
};

GitLabからスター数をとってきたい場合であれば GitLabGateway を実装すればいいですし、GitHubとGitLab両方から取得する場合も、そのようなGatewayを作って差し込めばOKです。

要件の変化に強いVuex

アーキテクチャ寄りな話になってしまいましたが、 inject を使ったメリットの一つとして紹介しました。

開発モードみたいな機能を追加できる

次に、上の差し替えに関連するのですが、Nuxt.jsの 環境変数 と組み合わせることで、 npm run offline のように、インターネットに通信しにいかないモードを作ることもできます。次のように plugins/dependencies.ts を実装することで、実行時のGateway切り替えが可能になります。

// plugins/dependencies.ts
export default (context, inject) => {
  const environment = process.env.environment || 'development';

  let gitGateway: IGitGateway;
  if (environment === 'offline') {
    // 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

ではでは!