0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Vue 3のプラグイン開発について調べてみました

Posted at

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アプリケーションを構築できます。ただし、過度なグローバル汚染は避け、適切なスコープで機能を提供することが重要です。

参考リンク

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?