- Vue.js
Vueプラグインの作り方|app.useとinstall・グローバルプロパティ
Vueのプラグインとは、install メソッドを持つオブジェクト(または install の役割をする関数)で、app.use(プラグイン) と呼ぶだけでアプリ全体に機能を追加できる仕組みです。 install の第1引数にアプリのインスタンス(app)が渡されるので、その中でグローバルコンポーネントの登録、ディレクティブの登録、provide、グローバルプロパティの追加をまとめて行えます。
この記事では、install の中でできる4つのことを一覧表にし、それぞれ最小コードと実行結果を載せます。そのうえで、script setup からはグローバルプロパティに直接触れられない点を実際のエラーで示し、provide と inject を包んだ use 関数に InjectionKey で型を付ける書き方、ComponentCustomProperties の型拡張の全文、app.use を2回呼んだときの警告文、inject が undefined になる場面の再現、Vitest の global.plugins を使ったテストまで、動かした結果つきで解説します。
確認環境:Ubuntu 24.04 / Node.js 22.22.2 / Vue 3.5.43 / Vitest 5.0.3 + jsdom 30.1.1 + @vue/test-utils 2.5.1 / vue-tsc 3.3.12(TypeScript 6.0.3)。画面の出力は jsdom 上でマウントして確かめています。
Vueプラグインの早見表
| やりたいこと | 書き方 |
|---|---|
| プラグインを定義する(オブジェクト) | const myPlugin: Plugin = { install(app, options) { … } } |
| プラグインを定義する(関数) | const myPlugin: Plugin = (app, options) => { … } |
| プラグインを入れる | app.use(myPlugin)(app.mount より前に呼ぶ) |
| オプションを渡す | app.use(myPlugin, { locale: 'ja-JP' }) → install の第2引数に届く |
| オプションに型を付ける | Plugin<[options?: MyOptions]> |
| 続けて書く | createApp(App).use(a).use(b).mount('#app')(use は app を返す) |
install の中でできることは、次の4つです。
| install の中で呼ぶもの | 追加されるもの | 使う側の書き方 | script setup での使いやすさ |
|---|---|---|---|
app.component('BaseBadge', BaseBadge) |
グローバルコンポーネント | テンプレートに BaseBadge タグを書く(import なし) |
そのまま使える |
app.directive('focus', vFocus) |
グローバルディレクティブ | テンプレートに v-focus |
そのまま使える |
app.provide(key, value) |
アプリ全体への provide | inject(key)、またはそれを包んだ use 関数 |
script でもテンプレートでも使える |
app.config.globalProperties.$fmt = … |
グローバルプロパティ | テンプレートの $fmt、Options API の this.$fmt |
テンプレートだけ(script からは直接読めない) |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vueプラグインの作り方|app.useとinstall・グローバルプロパティ」
installの中でできる4つを最小コードで動かす
4つそれぞれについて、プラグインの定義から app.use、マウント、出力までを1ファイルにまとめた最小の例です。テンプレートを文字列で書いているため、Vitest の設定で vue を実行時コンパイラつきのビルド(vue/dist/vue.esm-bundler.js)に向けています。
// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js' } },
test: { environment: 'jsdom' },
})
1. app.component:グローバルコンポーネントを登録する
// min/component.ts
import { createApp, defineComponent, h } from 'vue'
import type { Plugin } from 'vue'
const AppTag = defineComponent({
props: { text: { type: String, required: true } },
setup: (props) => () => h('span', { class: 'tag' }, props.text),
})
const tagPlugin: Plugin = {
install(app) {
app.component('AppTag', AppTag)
},
}
const app = createApp({ template: `<AppTag text="新着" />` })
app.use(tagPlugin)
app.mount(document.body.appendChild(document.createElement('div')))
console.log('app.component →', document.body.lastElementChild?.innerHTML)
app.component → <span class="tag">新着</span>
ルートのテンプレートは AppTag を import していませんが、プラグインが登録したので描画されました。UI 部品をまとめて配るライブラリは、この方法で複数のコンポーネントを一度に登録します。
2. app.directive:グローバルディレクティブを登録する
// min/directive.ts
import { createApp } from 'vue'
import type { Plugin } from 'vue'
// install だけなら、関数をそのままプラグインにできる
const upperPlugin: Plugin = (app) => {
app.directive('upper', (el: HTMLElement) => {
el.textContent = (el.textContent ?? '').toUpperCase()
})
}
const app = createApp({ template: `<p v-upper>vue plugin</p>` })
app.use(upperPlugin)
app.mount(document.body.appendChild(document.createElement('div')))
console.log('app.directive →', document.body.lastElementChild?.innerHTML)
app.directive → <p>VUE PLUGIN</p>
この例では、プラグインをオブジェクトではなく関数で書いています。install しか持たないプラグインは、関数をそのままプラグインとして渡せます。
3. app.provide:アプリ全体に値を配る
// min/provide.ts
import { createApp, defineComponent, h, inject } from 'vue'
import type { InjectionKey, Plugin } from 'vue'
const siteNameKey: InjectionKey<string> = Symbol('siteName')
const sitePlugin: Plugin<[name: string]> = {
install(app, name) {
app.provide(siteNameKey, name)
},
}
const Root = defineComponent({
setup() {
const siteName = inject(siteNameKey)
return () => h('h1', siteName)
},
})
const app = createApp(Root)
app.use(sitePlugin, '備品管理')
app.mount(document.body.appendChild(document.createElement('div')))
console.log('app.provide →', document.body.lastElementChild?.innerHTML)
app.provide → <h1>備品管理</h1>
app.use(sitePlugin, '備品管理') の第2引数が、install の第2引数 name に届いています。app.provide した値は、そのアプリのすべてのコンポーネントから inject で受け取れます。
4. app.config.globalProperties:テンプレートのどこからでも読めるプロパティ
// min/global.ts
import { createApp } from 'vue'
import type { Plugin } from 'vue'
const yenPlugin: Plugin = {
install(app) {
app.config.globalProperties.$yen = (value: number) => `${value.toLocaleString('ja-JP')}円`
},
}
const app = createApp({ template: `<p>{{ $yen(12800) }}</p>` })
app.use(yenPlugin)
app.mount(document.body.appendChild(document.createElement('div')))
console.log('globalProperties →', document.body.lastElementChild?.innerHTML)
globalProperties → <p>12,800円</p>
$yen はどのコンポーネントのテンプレートからも呼べます。名前の先頭に $ を付けるのは、コンポーネント自身のデータやメソッドと区別しやすくするための慣習です。
4つを組み合わせたプラグインの完成形
ここからは、4つをすべて使った「表示の整形」プラグインを例にします。数値や日付を、指定したロケールの書式で文字列にする機能を配ります。
<!-- plugins/BaseBadge.vue -->
<script setup lang="ts">
defineProps<{ label: string }>()
</script>
<template>
<span class="badge">{{ label }}</span>
</template>
// plugins/formatter.ts
import { inject } from 'vue'
import type { Directive, InjectionKey, Plugin } from 'vue'
import BaseBadge from './BaseBadge.vue'
// ---- プラグインが配るもの ----
export interface FormatterOptions {
locale?: string
unit?: string
}
export interface Formatter {
readonly locale: string
number(value: number): string
price(value: number): string
date(value: Date): string
}
export function createFormatter(options: FormatterOptions = {}): Formatter {
const { locale = 'ja-JP', unit = '円' } = options
const numberFormat = new Intl.NumberFormat(locale)
const dateFormat = new Intl.DateTimeFormat(locale, { dateStyle: 'long', timeZone: 'Asia/Tokyo' })
return {
locale,
number: (value) => numberFormat.format(value),
price: (value) => `${numberFormat.format(value)}${unit}`,
date: (value) => dateFormat.format(value),
}
}
const vFocus: Directive<HTMLInputElement> = {
mounted: (el) => el.focus(),
}
// ---- provide / inject 用のキーと use 関数 ----
export const formatterKey: InjectionKey<Formatter> = Symbol('formatter')
export function useFormatter(): Formatter {
const formatter = inject(formatterKey)
if (!formatter) {
throw new Error('useFormatter() を使う前に app.use(formatterPlugin) を呼んでください')
}
return formatter
}
// ---- プラグイン本体 ----
export const formatterPlugin: Plugin<[options?: FormatterOptions]> = {
install(app, options) {
const formatter = createFormatter(options)
app.component('BaseBadge', BaseBadge) // 1. グローバルコンポーネント
app.directive('focus', vFocus) // 2. グローバルディレクティブ
app.provide(formatterKey, formatter) // 3. script setup 向け(inject で受け取る)
app.config.globalProperties.$fmt = formatter // 4. テンプレート・Options API 向け
},
}
// ---- 型の拡張 ----
declare module 'vue' {
interface ComponentCustomProperties {
$fmt: Formatter
}
interface GlobalComponents {
BaseBadge: typeof BaseBadge
}
interface GlobalDirectives {
vFocus: typeof vFocus
}
}
このファイルのポイントは次の4つです。
Plugin<[options?: FormatterOptions]>でオプションの型を決めている。app.use(formatterPlugin, { … })の第2引数が型チェックされます。- 同じ formatter を、provide と globalProperties の両方に渡している。 script setup では use 関数、テンプレートと Options API では
$fmtと、書きやすいほうを選べます。 - InjectionKey とそれを包んだ use 関数を一緒に export している。 使う側は
useFormatter()を呼ぶだけで、キーを意識せずに型つきの値を受け取れます。 - 型の拡張(declare module 'vue')を同じファイルに置いている。 プラグインを import したプロジェクトでは、
$fmtや BaseBadge、v-focus に型が付きます。
プラグインを入れる:main.ts
// main.ts
import { createApp } from 'vue'
import OrderSummary from './OrderSummary.vue'
import { formatterPlugin } from './plugins/formatter'
const app = createApp(OrderSummary, { amount: 12800, orderedAt: new Date('2026-10-03T09:00:00+09:00') })
app.use(formatterPlugin, { locale: 'ja-JP', unit: '円' })
app.mount('#app')
app.use は app.mount より前に呼びます。第2引数以降が、そのまま install の第2引数以降になります。
使う側のコンポーネント
<!-- OrderSummary.vue -->
<script setup lang="ts">
import { computed } from 'vue'
import { useFormatter } from './plugins/formatter'
const props = defineProps<{ amount: number; orderedAt: Date }>()
const fmt = useFormatter()
const total = computed(() => fmt.price(props.amount))
</script>
<template>
<section>
<BaseBadge label="注文" />
<input v-focus placeholder="メモ" />
<p id="total">合計: {{ total }}</p>
<p id="date">注文日: {{ $fmt.date(orderedAt) }}</p>
<p id="locale">ロケール: {{ $fmt.locale }}</p>
</section>
</template>
jsdom 上でマウントした結果の HTML です。
<section><span class="badge">注文</span><input placeholder="メモ">
<p id="total">合計: 12,800円</p>
<p id="date">注文日: 2026年10月3日</p>
<p id="locale">ロケール: ja-JP</p>
</section>
BaseBadge(app.component)、v-focus(app.directive。マウント後の activeElement は INPUT でした)、useFormatter(app.provide)、$fmt(globalProperties)の4つが、すべて1つのプラグインから届いています。
Options API のコンポーネントでは、this.$fmt で同じものを読めます。
<!-- OptionsSummary.vue -->
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: { amount: { type: Number, required: true } },
computed: {
total(): string {
return this.$fmt.price(this.amount) // Options API は this から読める
},
},
})
</script>
<template>
<p id="opt">合計: {{ total }}</p>
</template>
<p id="opt">合計: 500円</p>
script setupからはグローバルプロパティに直接触れられない
テンプレートで $fmt と書けるので、script setup の中でも同じように書けそうに見えます。試してみます。
<!-- ScriptNg.vue -->
<script setup lang="ts">
// テンプレートと同じ感覚で書いてみる
const label = $fmt.price(12800)
</script>
<template>
<p>{{ label }}</p>
</template>
マウントしたときのエラーと、vue-tsc の出力です。
ReferenceError: $fmt is not defined
a3/ScriptNg.vue(3,15): error TS2304: Cannot find name '$fmt'.
グローバルプロパティは「コンポーネントのインスタンスのプロパティ」として追加されるもので、JavaScript のグローバル変数ではありません。 テンプレートの中の $fmt や Options API の this.$fmt は、インスタンスを通して読んでいます。script setup には this がないため、同じ書き方では届きません。
そこで、script setup では provide した値を use 関数で受け取ります。
<!-- ScriptOk.vue -->
<script setup lang="ts">
import { useFormatter } from './plugins/formatter'
const fmt = useFormatter()
const label = fmt.price(12800)
</script>
<template>
<p>{{ label }} / {{ $fmt.price(500) }}</p>
</template>
use 関数版 → 12,800円 / 500円
script の中では fmt.price(12800)、テンプレートでは $fmt.price(500) と、どちらの経路でも同じ formatter が使えています。
globalPropertiesとprovideの判断表
| 観点 | globalProperties($fmt) |
provide + use 関数(useFormatter()) |
|---|---|---|
| テンプレートで使う | 何も書かずに $fmt で使える |
setup で受け取った変数を使う |
| script setup の中で使う | 直接は読めない | 使える |
| Options API で使う | this.$fmt |
inject オプションで受け取る |
| 型の付け方 | ComponentCustomProperties を拡張 | InjectionKey の型引数 |
| どこから来たかの追いやすさ | 名前だけなので、定義を探す必要がある | import をたどれば定義に着く |
| 名前が重なったとき | コンポーネント自身のプロパティが優先される | Symbol のキーなので重ならない |
| テストでの差し替え | global.mocks |
global.provide、または別オプションのプラグイン |
新しく書くコードが script setup 中心なら、provide + use 関数を基本にします。 テンプレートで頻繁に使う整形処理や、Options API のコンポーネントが多いアプリでは、globalProperties も一緒に登録しておくと書きやすくなります。Vue の公式ガイドには、グローバルプロパティは控えめに使うこと、数が増えるとどのプラグインが入れたものかが追いにくくなることが書かれています。
「名前が重なったとき」の行は、次のコードで確かめました。data に title を持つコンポーネントへ、同じ名前の globalProperties を登録しています。
// priority.ts
import { createApp, defineComponent } from 'vue'
const Own = defineComponent({
data: () => ({ title: 'コンポーネントの title' }),
template: `<p>{{ title }} / {{ siteName }}</p>`,
})
const el = document.body.appendChild(document.createElement('div'))
createApp(Own)
.use((app) => {
app.config.globalProperties.title = 'グローバルの title'
app.config.globalProperties.siteName = 'グローバルの siteName'
})
.mount(el)
console.log('優先順位 →', el.textContent)
優先順位 → コンポーネントの title / グローバルの siteName
TypeScriptの型を付ける:InjectionKeyとComponentCustomProperties
use関数とInjectionKey
InjectionKey<Formatter> は、「このキーで provide / inject される値は Formatter 型」という情報を持った Symbol です。先ほどの formatter.ts から、関係する部分を抜き出すと次の形になります。
// plugins/formatter.ts(抜粋)
export const formatterKey: InjectionKey<Formatter> = Symbol('formatter')
export function useFormatter(): Formatter {
const formatter = inject(formatterKey)
if (!formatter) {
throw new Error('useFormatter() を使う前に app.use(formatterPlugin) を呼んでください')
}
return formatter
}
inject(formatterKey) の戻り値は、Formatter | undefined 型になります。プラグインが入っていない場合に undefined になりうるためです。use 関数の中で undefined を調べてエラーにしておくと、使う側は毎回 undefined を気にせず Formatter 型として扱えます。
use 関数を通さずに inject を直接使い、さらにテンプレートでメソッド名を打ち間違えた例を vue-tsc に通します。
<!-- TypeNg.vue -->
<script setup lang="ts">
import { inject } from 'vue'
import { formatterKey } from './plugins/formatter'
const fmt = inject(formatterKey)
const label = fmt.price(12800)
</script>
<template>
<p>{{ label }} / {{ $fmt.prise(500) }}</p>
</template>
a3/TypeNg.vue(6,15): error TS18048: 'fmt' is possibly 'undefined'.
a3/TypeNg.vue(10,28): error TS2551: Property 'prise' does not exist on type 'Formatter'. Did you mean 'price'?
1行目が「undefined かもしれない」という指摘、2行目がテンプレートの $fmt.prise に対する指摘です。$fmt に型が付いているので、テンプレートの中の打ち間違いにも「price ではありませんか」と候補が出ます。
ComponentCustomPropertiesの型拡張
$fmt に型が付くのは、formatter.ts の末尾にある次の部分のおかげです。
// plugins/formatter.ts(抜粋)
declare module 'vue' {
interface ComponentCustomProperties {
$fmt: Formatter
}
interface GlobalComponents {
BaseBadge: typeof BaseBadge
}
interface GlobalDirectives {
vFocus: typeof vFocus
}
}
| 拡張するインターフェイス | 型が付く対象 |
|---|---|
| ComponentCustomProperties | テンプレートの $fmt、Options API の this.$fmt |
| GlobalComponents | app.component で登録したコンポーネントの props |
| GlobalDirectives | app.directive で登録したディレクティブの値 |
この部分を消して vue-tsc を実行すると、$fmt を使っている箇所すべてでエラーになりました(長い型名は途中を省いています)。
a3/OptionsSummary.vue(8,19): error TS2339: Property '$fmt' does not exist on type 'CreateComponentPublicInstanceWithMixins<…>'.
a3/OrderSummary.vue(16,26): error TS2339: Property '$fmt' does not exist on type '{ amount: number; orderedAt: Date; … }'.
a3/ScriptOk.vue(9,23): error TS2339: Property '$fmt' does not exist on type 'ComponentPublicInstance<…>'.
型拡張を別ファイルに分ける場合の注意点が、公式ガイドに書かれています。declare module 'vue' を書くファイルは、トップレベルに import か export が1つ以上あるモジュールにします。 import するものがなければ、次のように export {} を1行置きます。
// types/vue-shim.d.ts
export {}
declare module 'vue' {
interface ComponentCustomProperties {
$appName: string
}
}
モジュールになっていないファイルに書くと、拡張ではなく元の型の上書きになってしまいます。上の formatter.ts は import と export があるので、そのまま拡張として働きます。
オプションの型チェック
Plugin<[options?: FormatterOptions]> と書いておくと、app.use の第2引数もチェックされます。
// type-ng.ts
import { createApp } from 'vue'
import OrderSummary from './OrderSummary.vue'
import { formatterPlugin } from './plugins/formatter'
const app = createApp(OrderSummary)
app.use(formatterPlugin, { locale: 123 })
app.use(formatterPlugin, { lang: 'ja-JP' })
a3/type-ng.ts(6,5): error TS2769: No overload matches this call.
Argument of type '[{ locale: number; }]' is not assignable to parameter of type '[options?: FormatterOptions | undefined]'.
Type '{ locale: number; }' is not assignable to type 'FormatterOptions'.
Types of property 'locale' are incompatible.
Type 'number' is not assignable to type 'string'.
a3/type-ng.ts(7,28): error TS2769: No overload matches this call.
Argument of type '[{ lang: string; }]' is not assignable to parameter of type '[options?: FormatterOptions | undefined]'.
Object literal may only specify known properties, and 'lang' does not exist in type 'FormatterOptions'.
(それぞれ、続きの「Overload 2 of 2 …」の行は省いています。)値の型の違いも、オプション名の打ち間違いも、app.use を書いた時点で見つかります。
app.useを2回呼んだときの警告
同じプラグインを2回 app.use すると、どうなるかを確かめます。
// twice.ts
import { createApp, h } from 'vue'
import type { Plugin } from 'vue'
let count = 0
const counterPlugin: Plugin<[options: { name: string }]> = {
install(app, options) {
count++
app.config.globalProperties.$name = options.name
},
}
const app = createApp({ render: () => h('p') })
app.use(counterPlugin, { name: '1回目' })
app.use(counterPlugin, { name: '2回目' })
console.log(`install の実行回数 = ${count} / $name = ${app.config.globalProperties.$name}`)
install の実行回数 = 1 / $name = 1回目
[Vue warn]: Plugin has already been applied to target app.
install は1回しか実行されず、2回目の呼び出しは警告を出して何もしません。 2回目に渡したオプション(「2回目」)も反映されず、$name は「1回目」のままです。公式の API リファレンスにも、同じプラグインに対して app.use が複数回呼ばれた場合、プラグインは一度だけインストールされると書かれています。
| 状況 | 起きること | 対応 |
|---|---|---|
| 同じ app に同じプラグインを2回 use | 2回目は警告を出して何もしない | use を1か所にまとめる |
| オプションを変えてもう一度 use | 2回目のオプションは反映されない | 最初の use で必要なオプションを渡す |
| テストごとに別の設定にしたい | 同じ app では切り替わらない | テストごとに mount して、別の app にプラグインを渡す(後述) |
injectがundefinedになる場面の再現と直し方
プラグインが provide した値を受け取れず、undefined になる場面は主に3つあります。警告文と一緒に再現します。
(a) app.useを呼んでいない
プラグインを入れずに inject(formatterKey) を呼ぶコンポーネントをマウントした結果です。
(a) app.use(formatterPlugin) を呼んでいない
結果: undefined
警告: [Vue warn]: injection "Symbol(formatter)" not found.
キーに付けた説明(Symbol('formatter') の 'formatter')が警告文に出るので、どのプラグインが入っていないかがわかります。 Symbol を作るときは、説明の文字列を付けておくと調べやすくなります。直し方は、main.ts に app.use(formatterPlugin) を足すことです。use 関数を通していれば、警告に加えて「useFormatter() を使う前に app.use(formatterPlugin) を呼んでください」というエラーで知らせてくれます。
(b) setupの外でinjectを呼んでいる
<!-- InjectNg.vue -->
<script setup lang="ts">
import { inject, ref } from 'vue'
import { formatterKey } from './plugins/formatter'
const label = ref('')
function onClick() {
// setup の外(イベントハンドラーの中)で inject を呼ぶ
const fmt = inject(formatterKey)
label.value = fmt ? fmt.price(12800) : 'fmt は undefined'
}
</script>
<template>
<button @click="onClick">計算</button>
<p>{{ label }}</p>
</template>
(b) イベントハンドラーの中で inject
結果: fmt は undefined
警告: [Vue warn]: inject() can only be used inside setup() or functional components.
プラグインは入っているのに undefined になりました。inject は、setup の実行中(script setup の直下)でだけ働きます。 クリックのハンドラーは setup の実行が終わった後に呼ばれるので、どのコンポーネントから呼ばれたかを Vue が判断できません。
直し方は、setup の直下で1回だけ受け取り、変数に入れておくことです。
<!-- InjectOk.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useFormatter } from './plugins/formatter'
const fmt = useFormatter() // setup の直下で1回だけ受け取る
const label = ref('')
function onClick() {
label.value = fmt.price(12800)
}
</script>
<template>
<button @click="onClick">計算</button>
<p>{{ label }}</p>
</template>
(b) の直し方: setup 直下で useFormatter
結果: 12,800円
警告: (警告なし)
(c) コンポーネントの外でinjectを呼んでいる
ルーターの処理や、ストアの定義など、コンポーネントではない場所から provide した値を使いたいこともあります。そのまま inject を呼ぶと (b) と同じ警告になりますが、app.runWithContext(Vue 3.3 以降)の中で呼ぶと、app.provide した値を受け取れます。
// outside.ts
import { createApp, h, inject } from 'vue'
import { formatterKey, formatterPlugin } from './plugins/formatter'
const app = createApp({ render: () => h('p') })
app.use(formatterPlugin)
// コンポーネントの外でそのまま inject を呼ぶ
console.log('そのまま inject →', inject(formatterKey))
// app.runWithContext の中なら、app.provide した値を受け取れる
const label = app.runWithContext(() => inject(formatterKey)?.price(12800))
console.log('runWithContext →', label)
そのまま inject → undefined
runWithContext → 12,800円
[Vue warn]: inject() can only be used inside setup() or functional components.
(警告は1行目の「そのまま inject」に対するものです。)
| 場面 | 警告文 | 直し方 |
|---|---|---|
| (a) プラグインを入れていない | injection "Symbol(formatter)" not found. | app.use(formatterPlugin) を mount の前に呼ぶ |
| (b) イベントハンドラーの中で inject | inject() can only be used inside setup() or functional components. | setup の直下で受け取って変数に入れる |
| (c) コンポーネントの外で inject | 同上 | app.runWithContext(() => inject(key)) |
Vitestでプラグインを使うコンポーネントをテストする
プラグインに頼るコンポーネントをテストするときは、Vue Test Utils の mount の global.plugins にプラグインを渡します。mount のたびに新しいアプリが作られるので、テストごとに別のオプションで入れられます。
// summary.test.ts
import { expect, test } from 'vitest'
import { mount } from '@vue/test-utils'
import OrderSummary from './OrderSummary.vue'
import OptionsSummary from './OptionsSummary.vue'
import { formatterPlugin } from './plugins/formatter'
const props = { amount: 12800, orderedAt: new Date('2026-10-03T09:00:00+09:00') }
test('プラグインを global.plugins に渡す', () => {
const wrapper = mount(OrderSummary, {
props,
attachTo: document.body,
global: { plugins: [formatterPlugin] },
})
console.log(wrapper.html())
console.log('activeElement =', document.activeElement?.tagName)
expect(wrapper.get('#total').text()).toBe('合計: 12,800円')
expect(wrapper.get('#date').text()).toBe('注文日: 2026年10月3日')
})
test('オプションつきは [プラグイン, オプション] の配列で渡す', () => {
const wrapper = mount(OrderSummary, {
props,
global: { plugins: [[formatterPlugin, { locale: 'en-US', unit: ' JPY' }]] },
})
console.log(wrapper.get('#total').text(), '/', wrapper.get('#date').text(), '/', wrapper.get('#locale').text())
expect(wrapper.get('#date').text()).toBe('注文日: October 3, 2026')
})
test('Options API は this.$fmt で読める', () => {
const wrapper = mount(OptionsSummary, { props: { amount: 500 }, global: { plugins: [formatterPlugin] } })
console.log(wrapper.html())
expect(wrapper.text()).toBe('合計: 500円')
})
test('プラグインを渡さないと use 関数が知らせてくれる', () => {
expect(() => mount(OrderSummary, { props })).toThrowError('useFormatter() を使う前に app.use(formatterPlugin) を呼んでください')
})
✓ a3/summary.test.ts > プラグインを global.plugins に渡す 53ms
✓ a3/summary.test.ts > オプションつきは [プラグイン, オプション] の配列で渡す 6ms
✓ a3/summary.test.ts > Options API は this.$fmt で読める 3ms
✓ a3/summary.test.ts > プラグインを渡さないと use 関数が知らせてくれる 5ms
Test Files 1 passed (1)
Tests 4 passed (4)
2つ目のテストで出力した文字列は次のとおりで、オプションが反映されています。
合計: 12,800 JPY / 注文日: October 3, 2026 / ロケール: en-US
| やりたいこと | 書き方 |
|---|---|
| プラグインを入れる | global: { plugins: [formatterPlugin] } |
| オプションつきで入れる | global: { plugins: [[formatterPlugin, { locale: 'en-US' }]] } |
| すべてのテストに入れておく | セットアップファイルで config.global.plugins = [formatterPlugin] |
4つ目のテストでは、プラグインを渡さずにマウントしています。このとき標準エラーには、4つの機能それぞれについて警告が出ました(コンポーネントの位置を示す行は省いています)。
[Vue warn]: injection "Symbol(formatter)" not found.
[Vue warn]: Failed to resolve component: BaseBadge
If this is a native custom element, make sure to exclude it from component resolution via compilerOptions.isCustomElement.
[Vue warn]: Failed to resolve directive: focus
[Vue warn]: Property "$fmt" was accessed during render but is not defined on instance.
テストでこの4種類の警告を見かけたら、プラグインを global.plugins に渡し忘れている合図です。
すべてのテストにあらかじめ入れておく
ほとんどのテストで同じプラグインを使うなら、セットアップファイルで config.global.plugins に入れておくと、mount ごとの指定を省けます。
// setup.ts
import { config } from '@vue/test-utils'
import { formatterPlugin } from './plugins/formatter'
// すべてのテストの mount に、あらかじめプラグインを入れておく
config.global.plugins = [formatterPlugin]
// vitest.config.ts(setupFiles を追加)
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
resolve: { alias: { vue: 'vue/dist/vue.esm-bundler.js' } },
test: { environment: 'jsdom', setupFiles: ['./setup.ts'] },
})
この設定で、mount(ScriptOk) と global を何も書かずにマウントしたテストが通り、出力は次のとおりでした。
config.global → 12,800円 / 500円
1つ注意点があります。この設定のまま先ほどの summary.test.ts を実行すると、2つ目と4つ目のテストが通らなくなりました。
[Vue warn]: Plugin has already been applied to target app.
AssertionError: expected '注文日: 2026年10月3日' to be '注文日: October 3, 2026' // Object.is equality
AssertionError: expected [Function] to throw an error
config.global.plugins に入れたプラグインが先に入るため、mount 側で同じプラグインを別のオプションで渡しても「2回目の use」になり、反映されません。 テストごとにオプションを変えたいプラグインや、「入っていない場合」を確かめたいプラグインは、共通の設定に入れず、mount ごとに global.plugins で渡します。
プラグインにするかどうかの判断表
| やりたいこと | 向いている方法 | 理由 |
|---|---|---|
| アプリ全体で1つの設定や機能を共有する(整形、翻訳、通知、設定値) | プラグイン(provide + use 関数) | app.use の1行で入り、テストでも差し替えやすい |
| UI 部品やディレクティブをまとめて配る | プラグイン(app.component / app.directive) | 使う側の登録が1行で済む |
| 複数のアプリやリポジトリで同じ機能を使う | プラグインとしてパッケージにする | install とオプションという共通の入口になる |
| 1つの画面の中だけで状態を共有する | 親コンポーネントで provide | アプリ全体に広げる必要がない |
| 状態を持たない関数を使い回す | 関数を export して import する | プラグインにしなくても足りる |
| コンポーネントごとの状態と処理を再利用する | コンポーザブル | app に登録するものがない |
「app に何かを登録する必要があるか」が判断の分かれ目です。 登録するものがなければ、ふつうの関数やコンポーザブルの export で十分です。なお、すべてのコンポーネントに処理を混ぜる app.mixin は、公式の API リファレンスで非推奨とされ、代わりにコンポーザブルを使うよう案内されています。
つまずきやすい箇所と直し方
| 起きること | 理由 | 直し方 |
|---|---|---|
script setup で $fmt is not defined |
グローバルプロパティはインスタンスのプロパティ | use 関数で受け取る |
Property '$fmt' does not exist の型エラー |
ComponentCustomProperties を拡張していない | declare module 'vue' で型を足す |
| 型拡張を書いたら他の型がおかしくなった | ファイルがモジュールになっていない | export {} を1行足す |
| Plugin has already been applied の警告 | 同じプラグインを2回 use している | use を1か所にまとめる |
| injection not found の警告 | app.use を呼んでいない |
mount より前に use する |
| inject() can only be used inside setup() の警告 | イベントハンドラーの中で inject している | setup の直下で受け取る |
| テストで Failed to resolve component | テストの mount にプラグインを渡していない | global.plugins に渡す |
よくある質問
Q. プラグインはオブジェクトと関数のどちらで書くのがよいですか? A. 動きは同じです。install だけで足りるなら関数が短く書けます。プラグインに名前やバージョンなどの情報を持たせたい場合は、install メソッドを持つオブジェクトにします。
Q. globalProperties は使わないほうがよいのですか?
A. 使ってかまいません。テンプレートで頻繁に使う整形処理などは、$fmt と書けると手軽です。数を絞り、script setup 向けに use 関数も一緒に用意しておくと、どちらの書き方のコンポーネントからも使いやすくなります。
Q. Vue 2 の Vue.prototype.$xxx はどう書き換えますか?
A. 公式の API リファレンスで、app.config.globalProperties は Vue 2 の Vue.prototype の置き換えと説明されています。Vue.prototype.$fmt = … は、プラグインの install の中で app.config.globalProperties.$fmt = … と書きます。Vue.use(…) は app.use(…) になります。
Q. プラグインの中から別のプラグインの値を使えますか?
A. install の中では app.config.globalProperties を読めるので、先に use されたプラグインが追加したプロパティを参照できます。順序に頼る形になるため、必要なものはオプションとして受け取る設計にすると、入れる順番を気にせずに済みます。
まとめ
- プラグインは install を持つオブジェクトか関数。
app.use(プラグイン, オプション)で入れ、オプションは install の第2引数に届く - install の中でできるのは4つ:app.component、app.directive、app.provide、app.config.globalProperties
- グローバルプロパティはテンプレートと Options API 向け。script setup の中では
$fmt is not definedになるので、provide + use 関数で受け取る - InjectionKey と use 関数をセットで export すると、使う側は undefined を気にせず型つきで使える
$fmtの型は ComponentCustomProperties の拡張で付ける。別ファイルに書くときはexport {}でモジュールにする- 同じプラグインを2回 use しても install は1回だけ。「Plugin has already been applied to target app.」の警告が出る
- テストでは
global.pluginsにプラグインを渡す。オプションつきは[プラグイン, オプション]の配列で渡す
参考
- Vue.js 公式ガイド「プラグイン」 https://ja.vuejs.org/guide/reusability/plugins
- Vue.js API リファレンス(app.use・app.provide・app.config.globalProperties など) https://ja.vuejs.org/api/application
- Vue.js 公式ガイド TypeScript と Options API のページ(グローバルなプロパティの拡張) https://ja.vuejs.org/guide/typescript/options-api
- Vue Test Utils API リファレンス https://test-utils.vuejs.org/api/
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Vueプラグインの作り方|app.useとinstall・グローバルプロパティ」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。