この記事はぷりぷりあぷりけーしょんず 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ファイルを作ります。
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
の中身はこのようになっています。
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インスタンスが生成される前でもアクセスができるグローバルなオブジェクト、という認識で大丈夫かと思います。
プラグインを有効化
nuxt.config.js
のplugins
に追加したファイルを定義することで、context へアクセス可能なときにはいつでも関数を使用することができます。
/*
** Plugins to load before mounting the App
*/
plugins: [
'~/plugins/contextInject.ts'
],
定義した関数を呼び出す
<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からは参照することはできません。
プラグインの作成
import Vue from 'vue'
declare module 'vue/types/vue' {
interface Vue {
$vueInjectedFunction(name: string): string
}
}
Vue.prototype.$vueInjectedFunction = (name: string) => name + 'さん、こんにちは!'
今度は、Vue
インターフェースに対して関数を追加します。
Vue
の型定義はこのようになっています。
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にプラグインを追加します。
/*
** Plugins to load before mounting the App
*/
plugins: [
'~/plugins/contextInject.ts',
'~/plugins/vueInstanceInject.ts'
],
定義した関数を呼び出す
<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
関数を使用することで共通関数を作ることができます。
プラグインの作成
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引数に関数を渡せば良いことがわかります。
export type Plugin = (ctx: Context, inject: (key: string, value: any) => void) => Promise<void> | void
プラグインの有効化
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を作成します。
export const state = () => ({
storeMessage: ''
})
export const mutations = {
changeValue (state: any, newValue: any) {
state.storeMessage = this.$combinedInjectedFunction(newValue)
}
}
プラグインを定義したことにより、mutations
内の this を通して$combinedInjectedFunction
関数が使用できるようになっています。
定義した関数を呼び出す
<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の場合はプラグインのほうが実装もシンプルにできるかなと思います。
また、使用先でわざわざインポートする必要がないため使い勝手が良く、さらに型定義のおかげで補完が効くのでコーディングが捗ります。
とても便利な機能なので、試したことのない方は是非やってみてください!