插件基础

理解插件的工作原理,是开发高质量插件的前提。本节深入探讨插件的内部机制和基本结构。

插件的基本结构

Vue 插件有两种定义方式:

对象形式

const myPlugin = {
  install(Vue, options) {
    console.log('安装插件')
  }
}

函数形式

function myPlugin(Vue, options) {
  console.log('安装插件')
}

两种形式效果相同,Vue.use() 会自动处理。

install 方法

install 方法是插件的核心,接收两个参数:

  • Vue:Vue 构造函数,用于添加全局功能
  • options:用户传入的配置选项
const myPlugin = {
  install(Vue, options = {}) {
    const defaultOptions = {
      prefix: 'my',
      debug: false
    }
    
    const config = { ...defaultOptions, ...options }
    
    if (config.debug) {
      console.log('插件配置:', config)
    }
  }
}

Vue.use(myPlugin, { debug: true })

Vue.use 方法

Vue.use 的工作原理:

Vue.use = function(plugin, ...options) {
  const installedPlugins = this._installedPlugins || (this._installedPlugins = [])
  
  if (installedPlugins.indexOf(plugin) > -1) {
    return this
  }
  
  const args = [this, ...options]
  
  if (typeof plugin.install === 'function') {
    plugin.install.apply(plugin, args)
  } else if (typeof plugin === 'function') {
    plugin.apply(null, args)
  }
  
  installedPlugins.push(plugin)
  
  return this
}

自动安装

插件通常会在检测到全局 Vue 时自动安装:

if (typeof window !== 'undefined' && window.Vue) {
  window.Vue.use(plugin)
}

这样用户可以通过 <script> 标签直接引入,无需手动调用 Vue.use()

插件能添加的内容

全局方法

const utilsPlugin = {
  install(Vue) {
    Vue.utils = {
      formatDate(date, format) {
        return date.toLocaleDateString()
      },
      debounce(fn, delay) {
        let timer = null
        return function(...args) {
          clearTimeout(timer)
          timer = setTimeout(() => fn.apply(this, args), delay)
        }
      }
    }
  }
}

Vue.use(utilsPlugin)

console.log(Vue.utils.formatDate(new Date()))

实例方法

const messagePlugin = {
  install(Vue) {
    Vue.prototype.$message = {
      show(msg) {
        alert(msg)
      },
      success(msg) {
        this.show(`✓ ${msg}`)
      },
      error(msg) {
        this.show(`✗ ${msg}`)
      }
    }
  }
}

Vue.use(messagePlugin)

this.$message.success('操作成功')

全局指令

const directivePlugin = {
  install(Vue) {
    Vue.directive('focus', {
      inserted(el) {
        el.focus()
      }
    })
    
    Vue.directive('click-outside', {
      bind(el, binding) {
        el.__clickOutside__ = function(event) {
          if (!(el === event.target || el.contains(event.target))) {
            binding.value(event)
          }
        }
        document.addEventListener('click', el.__clickOutside__)
      },
      unbind(el) {
        document.removeEventListener('click', el.__clickOutside__)
        delete el.__clickOutside__
      }
    })
  }
}

全局过滤器

const filterPlugin = {
  install(Vue) {
    Vue.filter('currency', function(value, symbol = '¥') {
      return symbol + Number(value).toFixed(2)
    })
    
    Vue.filter('truncate', function(value, length = 50) {
      if (value.length <= length) return value
      return value.substring(0, length) + '...'
    })
  }
}

全局混入

const mixinPlugin = {
  install(Vue) {
    Vue.mixin({
      created() {
        console.log(`[${this.$options.name}] created`)
      }
    })
  }
}

谨慎使用全局混入

全局混入会影响所有组件,可能导致意外的副作用。只在确实需要时使用。

注册组件

const componentsPlugin = {
  install(Vue) {
    const components = {
      MyButton,
      MyInput,
      MyModal
    }
    
    Object.keys(components).forEach(name => {
      Vue.component(name, components[name])
    })
  }
}

插件配置

默认配置

const plugin = {
  install(Vue, options = {}) {
    const defaultConfig = {
      theme: 'light',
      locale: 'zh-CN',
      size: 'medium'
    }
    
    const config = { ...defaultConfig, ...options }
    
    Vue.prototype.$config = config
  }
}

Vue.use(plugin, {
  theme: 'dark',
  size: 'large'
})

配置响应式

const plugin = {
  install(Vue, options = {}) {
    const reactiveConfig = new Vue({
      data() {
        return {
          theme: options.theme || 'light',
          locale: options.locale || 'zh-CN'
        }
      }
    })
    
    Vue.prototype.$config = reactiveConfig
    
    Vue.prototype.$setTheme = function(theme) {
      this.$config.theme = theme
    }
  }
}

插件命名规范

方法命名

使用 $ 前缀区分 Vue 插件添加的方法和原生方法:

Vue.prototype.$http = axios
Vue.prototype.$loading = loadingService
Vue.prototype.$notify = notificationService

组件命名

使用统一前缀避免冲突:

Vue.component('MyButton', Button)
Vue.component('MyInput', Input)
Vue.component('MyModal', Modal)

插件检测

检查插件是否已安装:

if (Vue._installedPlugins && Vue._installedPlugins.includes(myPlugin)) {
  console.log('插件已安装')
}

实战示例:存储插件

const storagePlugin = {
  install(Vue, options = {}) {
    const prefix = options.prefix || 'app_'
    
    const storage = {
      get(key) {
        const value = localStorage.getItem(prefix + key)
        try {
          return JSON.parse(value)
        } catch {
          return value
        }
      },
      
      set(key, value) {
        const serialized = typeof value === 'object' 
          ? JSON.stringify(value) 
          : String(value)
        localStorage.setItem(prefix + key, serialized)
      },
      
      remove(key) {
        localStorage.removeItem(prefix + key)
      },
      
      clear() {
        Object.keys(localStorage)
          .filter(k => k.startsWith(prefix))
          .forEach(k => localStorage.removeItem(k))
      }
    }
    
    Vue.prototype.$storage = storage
    Vue.storage = storage
  }
}

Vue.use(storagePlugin, { prefix: 'myapp_' })

this.$storage.set('user', { name: '张三', age: 25 })
const user = this.$storage.get('user')

小结

插件基础要点:

  1. install 方法:插件的核心,接收 Vue 和 options
  2. Vue.use:安装插件,自动去重
  3. 添加内容:方法、指令、过滤器、组件、混入
  4. 命名规范:使用 $ 前缀和统一前缀