Nuxt.jsでDIっぽいことをして共通関数を作る

この記事は[ぷりぷりあぷりけーしょんず Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ppap)の16日目の記事です。 

# はじめに
Vue.jsでの処理の共通化といったら、Mixinが有名です。

しかし、asyncData関数の中では参照することができなっかたり、TSでデコレーターを使用している場合はVueインスタンスで`Mixins`クラスを継承する必要があったりと、少し不便なところもあります。

Nuxtのpluginを実装することで、DIっぽいことをしてどこでも関数が使えることが分かったので、その方法を紹介していきます。
（Nuxtで明示的にDIの機構が用意されているわけではないのでDIっぽいこと、としています。）

公式のソースはこちらです。
https://typescript.nuxtjs.org/cookbook/plugins.html#plugins

# 環境
Nuxt.js 2.10.2
TypeScript 3.7.3

※ Nuxt + TypeScript の初期構築が完了していることを前提とします。
※ [vue-property-decorator](https://github.com/kaorun343/vue-property-decorator) を使用したクラスベース、デコレータ方式で実装しています。

# contextへInjectする方法

## プラグインの作成

`/plugins`配下にTSファイルを作ります。

```contextInject.ts
import { Plugin } from '@nuxt/types'

declare module '@nuxt/types' {
  interface Context {
    $contextInjectedFunction(name: string): string
  }
}

const myPlugin: Plugin = (context) => {
  context.$contextInjectedFunction = (name: string) => name + 'さん、おはよう！'
}

export default myPlugin
```

`@nuxt/types`パッケージにある`Context`インターフェースには`$myInjectedFunction`なんてプロパティは存在しないので、declare moduleで新たに定義してあげます。

ちなみに、`Context`の中身はこのようになっています。

```app/index.d.ts
export interface Context {
  app: NuxtAppOptions
  base: string
  /**
   * @deprecated Use process.client instead
  */
  isClient: boolean
  /**
   * @deprecated Use process.server instead
  */
  isServer: boolean
  /**
   * @deprecated Use process.static instead
  */
  isStatic: boolean
  isDev: boolean
  isHMR: boolean
  route: Route
  from: Route
  store: Store<any>
  env: Record<string, any>
  params: Route['params']
  payload: any
  query: Route['query']
  req: IncomingMessage
  res: ServerResponse
  redirect(status: number, path: string, query?: Route['query']): void
  redirect(path: string, query?: Route['query']): void
  redirect(location: Location): void
  error(params: NuxtError): void
  nuxtState: NuxtState
  beforeNuxtRender(fn: (params: { Components: VueRouter['getMatchedComponents'], nuxtState: NuxtState }) => void): void
}
```

context とは、`asyncData`や`fetch`などのVueインスタンスが生成される前でもアクセスができるグローバルなオブジェクト、という認識で大丈夫かと思います。

https://ja.nuxtjs.org/api/context/

## プラグインを有効化

`nuxt.config.js`の`plugins`に追加したファイルを定義することで、context へアクセス可能なときにはいつでも関数を使用することができます。

```nuxt.congig.js
 /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts'
  ],
```

## 定義した関数を呼び出す

```/pages/sample.vue
<template>
  <div>
    <h1>{{ goodMorning }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { goodMorning: app.context.$contextInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
}
</script>
```

引数の`app`に`context.$myInjectedFunction`がInjectされているため、どのページからも関数が呼び出されるようになります。

# VueインスタンスへInjectする方法

Vueインスタンスに対してもInjectができるので紹介していきます。この場合はasyncDataからは参照することはできません。

## プラグインの作成

```/plugins/vueInstanceInject.ts
import Vue from 'vue'

declare module 'vue/types/vue' {
  interface Vue {
    $vueInjectedFunction(name: string): string
  }
}

Vue.prototype.$vueInjectedFunction = (name: string) => name + 'さん、こんにちは！'

```

今度は、`Vue`インターフェースに対して関数を追加します。

`Vue`の型定義はこのようになっています。

```vue.d.ts
export interface Vue {
  readonly $el: Element;
  readonly $options: ComponentOptions<Vue>;
  readonly $parent: Vue;
  readonly $root: Vue;
  readonly $children: Vue[];
  readonly $refs: { [key: string]: Vue | Element | Vue[] | Element[] };
  readonly $slots: { [key: string]: VNode[] | undefined };
  readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
  readonly $isServer: boolean;
  readonly $data: Record<string, any>;
  readonly $props: Record<string, any>;
  readonly $ssrContext: any;
  readonly $vnode: VNode;
  readonly $attrs: Record<string, string>;
  readonly $listeners: Record<string, Function | Function[]>;

  $mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
  $forceUpdate(): void;
  $destroy(): void;
  $set: typeof Vue.set;
  $delete: typeof Vue.delete;
  $watch(
    expOrFn: string,
    callback: (this: this, n: any, o: any) => void,
    options?: WatchOptions
  ): (() => void);
  $watch<T>(
    expOrFn: (this: this) => T,
    callback: (this: this, n: T, o: T) => void,
    options?: WatchOptions
  ): (() => void);
  $on(event: string | string[], callback: Function): this;
  $once(event: string | string[], callback: Function): this;
  $off(event?: string | string[], callback?: Function): this;
  $emit(event: string, ...args: any[]): this;
  $nextTick(callback: (this: this) => void): void;
  $nextTick(): Promise<void>;
  $createElement: CreateElement;
}
```

いつもVueコンポーネントで呼び出す関数たちが定義されています。
declare moduleで`$vueInjectedFunction`という関数を新たに追加したことになります。

## プラグインの有効化

nuxt.config.jsにプラグインを追加します。

```nuxt.config.js
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts',
    '~/plugins/vueInstanceInject.ts'
  ],
```

## 定義した関数を呼び出す

```sample.vue
<template>
  <div>
    <h1>{{ goodMorning }}</h1>
    <h1>{{ hello }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { goodMorning: app.context.$contextInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
  hello: string = ''

  created() {
    this.hello = this.$vueInjectedFunction('misaosyushi')
  }
}
</script>

```

VueインスタンスにInjectしたので、`this`でアクセスができるようになります。

# context, Vueインスタンス, VuexストアにInjectする方法

context や Vueインスタンス、Vuexストア内でも関数が必要な場合、`inject`関数を使用することで共通関数を作ることができます。

## プラグインの作成

```combinedInject.ts
import { Plugin } from '@nuxt/types'

declare module 'vue/types/vue' {
  interface Vue {
    $combinedInjectedFunction(name: string): string
  }
}

declare module '@nuxt/types' {
  interface Context {
    $combinedInjectedFunction(name: string): string
  }
}

declare module 'vuex/types/index' {
  interface Store<S> {
    $combinedInjectedFunction(name: string): string
  }
}

const myPlugin: Plugin = (context, inject) => {
  inject('combinedInjectedFunction', (name: string) => name + 'さん、おはこんばんにちは！')
}

export default myPlugin
```

新たに、`Store`インターフェースに対して共通化したい関数を定義し、`inject`関数に追加します。

`Plugin`型を見ると、`inject`の第1引数に関数名、第2引数に関数を渡せば良いことがわかります。

```types/app/index.d.ts
export type Plugin = (ctx: Context, inject: (key: string, value: any) => void) => Promise<void> | void
```


## プラグインの有効化

nuxt.config.jsにプラグインを追加します。

```nuxt.config.js
  /*
  ** Plugins to load before mounting the App
  */
  plugins: [
    '~/plugins/contextInject.ts',
    '~/plugins/vueInstanceInject.ts',
    '~/plugins/combinedInject.ts'
  ],
```

## storeを作成する

Vuexストアを使用するため、`/store`配下にindex.tsを作成します。

```/store/index.ts
export const state = () => ({
  storeMessage: ''
})

export const mutations = {
  changeValue (state: any, newValue: any) {
    state.storeMessage = this.$combinedInjectedFunction(newValue)
  }
}

```

プラグインを定義したことにより、`mutations`内の this を通して`$combinedInjectedFunction`関数が使用できるようになっています。

## 定義した関数を呼び出す

```combinedSample.vue
<template>
  <div>
    <h1>{{ contextMessage }}</h1>
    <h1>{{ vueMessage }}</h1>
    <h1>{{ $store.state.storeMessage }}</h1>
  </div>
</template>

<script lang="ts">
import { Vue, Component } from 'vue-property-decorator'

@Component({
  asyncData ({ app }) {
    return { contextMessage: app.$combinedInjectedFunction('misaosyushi') }
  }
})
export default class Sample extends Vue {
  vueMessage: string = ''

  created () {
    this.vueMessage = this.$combinedInjectedFunction('misaosyushi')
    this.$store.commit('changeValue', 'misaosyushi')
  }
}
</script>

```

これで、Context, Vueインスタンス, Vuexストア それぞれで共通関数が使えるようになっていることがわかります。

プラグインの`inject`関数を使用した場合、context の共通関数は`context.app`に注入されるため、asyncData内で`app.$combinedInjectedFunction`で参照できるようです。

公式のTIPにしれっと書いてあります。

https://typescript.nuxtjs.org/cookbook/plugins.html#usage-3

# まとめ

いままでVue.jsで共通化といったらMixin！でしたが、Nuxtの場合はプラグインのほうが実装もシンプルにできるかなと思います。

また、使用先でわざわざインポートする必要がないため使い勝手が良く、さらに型定義のおかげで補完が効くのでコーディングが捗ります。

とても便利な機能なので、試したことのない方は是非やってみてください！