17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ぷりぷりあぷりけーしょんずAdvent Calendar 2019

Day 16

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

Last updated at Posted at 2019-12-15

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2019の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 を使用したクラスベース、デコレータ方式で実装しています。

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 とは、asyncDatafetchなどのVueインスタンスが生成される前でもアクセスができるグローバルなオブジェクト、という認識で大丈夫かと思います。

プラグインを有効化

nuxt.config.jspluginsに追加したファイルを定義することで、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>

引数のappcontext.$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にしれっと書いてあります。

まとめ

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

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

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

17
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
17
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?