Vue 3のプラグイン開発
Vue 3のプラグインについて、実用的な例とベストプラクティスを交えながら詳しく解説します。プラグインを活用することで、アプリケーション全体に機能を追加し、再利用可能なライブラリを構築できます。
プラグインとは?
プラグインは、Vueアプリケーションにグローバルな機能を追加するための仕組みです。以下のような機能を提供できます:
- グローバルメソッドやプロパティの追加
- ディレクティブ、フィルター、トランジションの追加
- コンポーネントオプションの注入
- グローバルアセットの追加
- 外部ライブラリとの統合
プラグインの基本構造
Vue 3のプラグインは、installメソッドを持つオブジェクトまたは関数として定義されます:
// オブジェクト形式
const MyPlugin = {
install(app, options) {
// プラグインの実装
}
}
// 関数形式
function MyPlugin(app, options) {
// プラグインの実装
}
基本的なプラグインの例
1. グローバルメソッドの追加
// plugins/globalMethods.js
export const GlobalMethodsPlugin = {
install(app) {
// グローバルメソッドの追加
app.config.globalProperties.$formatDate = (date) => {
return new Date(date).toLocaleDateString('ja-JP')
}
app.config.globalProperties.$formatCurrency = (amount) => {
return new Intl.NumberFormat('ja-JP', {
style: 'currency',
currency: 'JPY'
}).format(amount)
}
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { GlobalMethodsPlugin } from './plugins/globalMethods'
const app = createApp(App)
app.use(GlobalMethodsPlugin)
app.mount('#app')
<!-- App.vue -->
<template>
<div>
<p>日付: {{ $formatDate(new Date()) }}</p>
<p>価格: {{ $formatCurrency(1000) }}</p>
</div>
</template>
2. グローバルプロパティの追加
// plugins/globalProperties.js
export const GlobalPropertiesPlugin = {
install(app) {
// グローバルプロパティの追加
app.config.globalProperties.$appName = 'My Vue App'
app.config.globalProperties.$version = '1.0.0'
app.config.globalProperties.$apiUrl = 'https://api.example.com'
}
}
<template>
<div>
<h1>{{ $appName }} v{{ $version }}</h1>
<p>API URL: {{ $apiUrl }}</p>
</div>
</template>
3. カスタムディレクティブの追加
// plugins/directives.js
export const DirectivesPlugin = {
install(app) {
// フォーカスディレクティブ
app.directive('focus', {
mounted(el) {
el.focus()
}
})
// 色変更ディレクティブ
app.directive('color', {
mounted(el, binding) {
el.style.color = binding.value
},
updated(el, binding) {
el.style.color = binding.value
}
})
}
}
<template>
<div>
<input v-focus placeholder="自動でフォーカス" />
<p v-color="'red'">赤いテキスト</p>
</div>
</template>
実用的なプラグインの例
1. HTTP クライアントプラグイン
// plugins/http.js
class HttpClient {
constructor(baseURL = '') {
this.baseURL = baseURL
this.interceptors = {
request: [],
response: []
}
}
addRequestInterceptor(interceptor) {
this.interceptors.request.push(interceptor)
}
addResponseInterceptor(interceptor) {
this.interceptors.response.push(interceptor)
}
async request(url, options = {}) {
let requestConfig = {
url: this.baseURL + url,
...options
}
// リクエストインターセプターを適用
for (const interceptor of this.interceptors.request) {
requestConfig = await interceptor(requestConfig)
}
try {
const response = await fetch(requestConfig.url, {
method: requestConfig.method || 'GET',
headers: {
'Content-Type': 'application/json',
...requestConfig.headers
},
body: requestConfig.body ? JSON.stringify(requestConfig.body) : undefined
})
let responseData = await response.json()
// レスポンスインターセプターを適用
for (const interceptor of this.interceptors.response) {
responseData = await interceptor(responseData)
}
return responseData
} catch (error) {
throw error
}
}
get(url, options = {}) {
return this.request(url, { ...options, method: 'GET' })
}
post(url, data, options = {}) {
return this.request(url, { ...options, method: 'POST', body: data })
}
put(url, data, options = {}) {
return this.request(url, { ...options, method: 'PUT', body: data })
}
delete(url, options = {}) {
return this.request(url, { ...options, method: 'DELETE' })
}
}
export const HttpPlugin = {
install(app, options = {}) {
const http = new HttpClient(options.baseURL)
// グローバルプロパティとして追加
app.config.globalProperties.$http = http
// provide/inject用に提供
app.provide('http', http)
// インターセプターの設定
if (options.interceptors) {
if (options.interceptors.request) {
http.addRequestInterceptor(options.interceptors.request)
}
if (options.interceptors.response) {
http.addResponseInterceptor(options.interceptors.response)
}
}
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { HttpPlugin } from './plugins/http'
const app = createApp(App)
app.use(HttpPlugin, {
baseURL: 'https://api.example.com',
interceptors: {
request: async (config) => {
// 認証トークンを追加
const token = localStorage.getItem('token')
if (token) {
config.headers = {
...config.headers,
'Authorization': `Bearer ${token}`
}
}
return config
},
response: async (data) => {
// レスポンスの処理
console.log('Response:', data)
return data
}
}
})
app.mount('#app')
<!-- App.vue -->
<template>
<div>
<button @click="fetchUsers">ユーザーを取得</button>
<button @click="createUser">ユーザーを作成</button>
<div v-if="users.length">
<ul>
<li v-for="user in users" :key="user.id">
{{ user.name }}
</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, inject } from 'vue'
const http = inject('http')
const users = ref([])
const fetchUsers = async () => {
try {
users.value = await http.get('/users')
} catch (error) {
console.error('ユーザー取得エラー:', error)
}
}
const createUser = async () => {
try {
const newUser = await http.post('/users', {
name: '新しいユーザー',
email: 'user@example.com'
})
users.value.push(newUser)
} catch (error) {
console.error('ユーザー作成エラー:', error)
}
}
</script>
2. 状態管理プラグイン
// plugins/store.js
class SimpleStore {
constructor() {
this.state = {}
this.subscribers = []
}
setState(newState) {
this.state = { ...this.state, ...newState }
this.notifySubscribers()
}
getState() {
return this.state
}
subscribe(callback) {
this.subscribers.push(callback)
return () => {
const index = this.subscribers.indexOf(callback)
if (index > -1) {
this.subscribers.splice(index, 1)
}
}
}
notifySubscribers() {
this.subscribers.forEach(callback => callback(this.state))
}
}
export const StorePlugin = {
install(app, options = {}) {
const store = new SimpleStore()
// 初期状態を設定
if (options.initialState) {
store.setState(options.initialState)
}
// グローバルプロパティとして追加
app.config.globalProperties.$store = store
// provide/inject用に提供
app.provide('store', store)
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { StorePlugin } from './plugins/store'
const app = createApp(App)
app.use(StorePlugin, {
initialState: {
user: null,
theme: 'light',
notifications: []
}
})
app.mount('#app')
<!-- App.vue -->
<template>
<div :class="theme">
<h1>Vue Store Plugin</h1>
<p>ユーザー: {{ user?.name || '未ログイン' }}</p>
<p>テーマ: {{ theme }}</p>
<button @click="toggleTheme">テーマ切り替え</button>
<button @click="login">ログイン</button>
</div>
</template>
<script setup>
import { computed, inject } from 'vue'
const store = inject('store')
const user = computed(() => store.getState().user)
const theme = computed(() => store.getState().theme)
const toggleTheme = () => {
const newTheme = theme.value === 'light' ? 'dark' : 'light'
store.setState({ theme: newTheme })
}
const login = () => {
store.setState({
user: { name: 'テストユーザー', email: 'test@example.com' }
})
}
</script>
<style>
.light {
background-color: white;
color: black;
}
.dark {
background-color: black;
color: white;
}
</style>
3. UI コンポーネントプラグイン
// plugins/ui.js
import Toast from './components/Toast.vue'
import Modal from './components/Modal.vue'
import Loading from './components/Loading.vue'
export const UIPlugin = {
install(app) {
// グローバルコンポーネントとして登録
app.component('Toast', Toast)
app.component('Modal', Modal)
app.component('Loading', Loading)
// トースト機能の追加
app.config.globalProperties.$toast = {
success(message) {
// トースト表示の実装
console.log('Success:', message)
},
error(message) {
console.log('Error:', message)
},
info(message) {
console.log('Info:', message)
}
}
// モーダル機能の追加
app.config.globalProperties.$modal = {
show(component, props = {}) {
// モーダル表示の実装
console.log('Modal show:', component, props)
},
hide() {
console.log('Modal hide')
}
}
}
}
<!-- components/Toast.vue -->
<template>
<div v-if="visible" class="toast" :class="type">
{{ message }}
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const props = defineProps({
message: String,
type: {
type: String,
default: 'info'
},
duration: {
type: Number,
default: 3000
}
})
const visible = ref(false)
onMounted(() => {
visible.value = true
setTimeout(() => {
visible.value = false
}, props.duration)
})
</script>
<style>
.toast {
position: fixed;
top: 20px;
right: 20px;
padding: 12px 16px;
border-radius: 4px;
color: white;
z-index: 1000;
}
.toast.success {
background-color: #4caf50;
}
.toast.error {
background-color: #f44336;
}
.toast.info {
background-color: #2196f3;
}
</style>
<!-- App.vue -->
<template>
<div>
<h1>UI Plugin Demo</h1>
<button @click="showSuccessToast">成功トースト</button>
<button @click="showErrorToast">エラートースト</button>
<button @click="showModal">モーダル表示</button>
<!-- グローバルコンポーネントの使用 -->
<Toast message="テストメッセージ" type="info" />
</div>
</template>
<script setup>
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const showSuccessToast = () => {
instance.proxy.$toast.success('操作が成功しました!')
}
const showErrorToast = () => {
instance.proxy.$toast.error('エラーが発生しました')
}
const showModal = () => {
instance.proxy.$modal.show('UserModal', { userId: 123 })
}
</script>
4. ログプラグイン
// plugins/logger.js
class Logger {
constructor(options = {}) {
this.level = options.level || 'info'
this.enabled = options.enabled !== false
this.levels = {
error: 0,
warn: 1,
info: 2,
debug: 3
}
}
log(level, message, ...args) {
if (!this.enabled || this.levels[level] > this.levels[this.level]) {
return
}
const timestamp = new Date().toISOString()
const prefix = `[${timestamp}] [${level.toUpperCase()}]`
switch (level) {
case 'error':
console.error(prefix, message, ...args)
break
case 'warn':
console.warn(prefix, message, ...args)
break
case 'info':
console.info(prefix, message, ...args)
break
case 'debug':
console.debug(prefix, message, ...args)
break
default:
console.log(prefix, message, ...args)
}
}
error(message, ...args) {
this.log('error', message, ...args)
}
warn(message, ...args) {
this.log('warn', message, ...args)
}
info(message, ...args) {
this.log('info', message, ...args)
}
debug(message, ...args) {
this.log('debug', message, ...args)
}
}
export const LoggerPlugin = {
install(app, options = {}) {
const logger = new Logger(options)
// グローバルプロパティとして追加
app.config.globalProperties.$logger = logger
// provide/inject用に提供
app.provide('logger', logger)
}
}
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { LoggerPlugin } from './plugins/logger'
const app = createApp(App)
app.use(LoggerPlugin, {
level: 'debug',
enabled: process.env.NODE_ENV !== 'production'
})
app.mount('#app')
<!-- App.vue -->
<template>
<div>
<h1>Logger Plugin Demo</h1>
<button @click="testLogs">ログをテスト</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const logger = inject('logger')
const testLogs = () => {
logger.debug('デバッグメッセージ')
logger.info('情報メッセージ')
logger.warn('警告メッセージ')
logger.error('エラーメッセージ')
}
</script>
プラグインの配布とTypeScript対応
1. npm パッケージとして配布
// package.json
{
"name": "vue3-my-plugin",
"version": "1.0.0",
"description": "My Vue 3 Plugin",
"main": "dist/index.js",
"module": "dist/index.esm.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w"
},
"peerDependencies": {
"vue": "^3.0.0"
},
"devDependencies": {
"@rollup/plugin-typescript": "^8.0.0",
"rollup": "^2.0.0",
"typescript": "^4.0.0"
}
}
// src/index.ts
import { App } from 'vue'
export interface PluginOptions {
baseURL?: string
timeout?: number
retries?: number
}
export interface MyPlugin {
install(app: App, options?: PluginOptions): void
}
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$myPlugin: any
}
}
const MyPlugin: MyPlugin = {
install(app: App, options: PluginOptions = {}) {
const config = {
baseURL: options.baseURL || '',
timeout: options.timeout || 5000,
retries: options.retries || 3
}
app.config.globalProperties.$myPlugin = {
config,
// プラグインの機能
}
}
}
export default MyPlugin
// src/types.ts
export interface User {
id: number
name: string
email: string
}
export interface ApiResponse<T> {
data: T
status: number
message: string
}
2. プラグインの使用例(TypeScript)
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import MyPlugin, { PluginOptions } from 'vue3-my-plugin'
const app = createApp(App)
const options: PluginOptions = {
baseURL: 'https://api.example.com',
timeout: 10000,
retries: 5
}
app.use(MyPlugin, options)
app.mount('#app')
<!-- App.vue -->
<template>
<div>
<h1>My Plugin Demo</h1>
<button @click="usePlugin">プラグインを使用</button>
</div>
</template>
<script setup lang="ts">
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
const usePlugin = () => {
// TypeScriptで型安全に使用
const plugin = instance?.proxy?.$myPlugin
if (plugin) {
console.log('Plugin config:', plugin.config)
}
}
</script>
ベストプラクティス
1. プラグインの設計原則
// ✅ 良い例:単一責任の原則
export const ValidationPlugin = {
install(app, options) {
// バリデーション機能のみに集中
app.config.globalProperties.$validate = {
email: (email) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email),
required: (value) => value != null && value !== '',
minLength: (value, min) => value.length >= min
}
}
}
// ❌ 悪い例:複数の責任を持つ
export const BadPlugin = {
install(app, options) {
// バリデーション、HTTP、ログなど複数の機能を混在
app.config.globalProperties.$validate = { /* ... */ }
app.config.globalProperties.$http = { /* ... */ }
app.config.globalProperties.$logger = { /* ... */ }
}
}
2. エラーハンドリング
export const SafePlugin = {
install(app, options = {}) {
try {
// プラグインの初期化
const config = this.validateOptions(options)
this.initializePlugin(app, config)
} catch (error) {
console.error('Plugin initialization failed:', error)
// フォールバック処理
this.initializeFallback(app)
}
},
validateOptions(options) {
if (!options.requiredOption) {
throw new Error('requiredOption is required')
}
return options
},
initializePlugin(app, config) {
// メインの初期化処理
},
initializeFallback(app) {
// フォールバック処理
}
}
3. パフォーマンスの考慮
export const PerformancePlugin = {
install(app, options) {
// 遅延初期化
let initialized = false
app.config.globalProperties.$lazyFeature = () => {
if (!initialized) {
this.initializeHeavyFeature()
initialized = true
}
return this.heavyFeature
}
},
initializeHeavyFeature() {
// 重い処理は必要になった時のみ実行
}
}
4. プラグインのテスト
// plugins/__tests__/myPlugin.test.js
import { createApp } from 'vue'
import MyPlugin from '../myPlugin'
describe('MyPlugin', () => {
let app
beforeEach(() => {
app = createApp({})
})
test('should install plugin correctly', () => {
app.use(MyPlugin, { testOption: 'test' })
expect(app.config.globalProperties.$myPlugin).toBeDefined()
})
test('should handle options correctly', () => {
const options = { baseURL: 'https://test.com' }
app.use(MyPlugin, options)
const plugin = app.config.globalProperties.$myPlugin
expect(plugin.config.baseURL).toBe('https://test.com')
})
})
まとめ
Vue 3のプラグインは、以下の利点を提供します:
- 機能の拡張: アプリケーション全体に機能を追加
- 再利用性: 複数のプロジェクトで同じ機能を共有
- モジュール性: 機能を独立したモジュールとして管理
- 統合性: サードパーティライブラリとの統合が容易
プラグインを適切に設計・実装することで、保守性が高く、拡張性のあるVueアプリケーションを構築できます。ただし、過度なグローバル汚染は避け、適切なスコープで機能を提供することが重要です。
参考リンク
- Vue 3 プラグイン公式ドキュメント
- VueUse - 実用的なVue 3コンポーザブルのコレクション
- Vue 3 Plugin Development Guide