Skip to content

插件开发

插件是自包含的代码,通常向 Vue 添加全局级功能。插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property
  2. 添加全局资源:指令/过滤器/过渡等
  3. 通过全局混入来添加一些组件选项
  4. 添加 app 实例方法,通过把它们添加到 app.config.globalProperties 上实现
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能

编写插件

插件是一个对象,需要暴露一个 install 方法,或者是一个函数,它会被作为 install 方法。install 方法调用时,会将 app 实例作为参数传入:

javascript
// plugins/myPlugin.js
export default {
  install(app, options) {
    // 配置此应用
  }
}

或者作为函数:

javascript
// plugins/myPlugin.js
export default function (app, options) {
  // 配置此应用
}

基本示例

让我们创建一个简单的插件,添加一个全局方法:

javascript
// plugins/globalMethod.js
export default {
  install(app, options) {
    app.config.globalProperties.$translate = (key) => {
      // 根据 key 获取翻译文本
      return key.split('.').reduce((o, i) => {
        if (o) return o[i]
      }, options)
    }
  }
}

使用插件:

javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import globalMethodPlugin from './plugins/globalMethod'

const app = createApp(App)

app.use(globalMethodPlugin, {
  greetings: {
    hello: 'Bonjour!'
  }
})

app.mount('#app')

在组件中使用:

vue
<template>
  <h1>{{ $translate('greetings.hello') }}</h1>
</template>

提供/注入

插件还可以使用 provide 来为插件用户提供功能或 attribute:

javascript
// plugins/i18nPlugin.js
export default {
  install(app, options) {
    app.provide('i18n', options)
  }
}

在组件中使用:

vue
<script setup>
import { inject } from 'vue'

const i18n = inject('i18n')
</script>

添加全局 Property

javascript
// plugins/globalProperty.js
export default {
  install(app, options) {
    app.config.globalProperties.$http = {
      get(url) {
        return fetch(url).then(res => res.json())
      },
      post(url, data) {
        return fetch(url, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(data)
        }).then(res => res.json())
      }
    }
  }
}

添加全局指令

javascript
// plugins/directivePlugin.js
export default {
  install(app) {
    app.directive('focus', {
      mounted(el) {
        el.focus()
      }
    })
    
    app.directive('color', {
      beforeMount(el, binding) {
        el.style.color = binding.value
      },
      updated(el, binding) {
        el.style.color = binding.value
      }
    })
  }
}

使用指令:

vue
<template>
  <input v-focus />
  <p v-color="'red'">This text is red</p>
</template>

添加全局组件

javascript
// plugins/componentPlugin.js
import MyGlobalComponent from './components/MyGlobalComponent.vue'

export default {
  install(app) {
    app.component('MyGlobalComponent', MyGlobalComponent)
  }
}

复杂插件示例

让我们创建一个更复杂的插件,实现一个简单的状态管理:

javascript
// plugins/store.js
import { reactive, readonly } from 'vue'

export default {
  install(app, options) {
    const state = reactive(options.state || {})
    const mutations = options.mutations || {}
    const actions = options.actions || {}
    
    const store = {
      state: readonly(state),
      
      commit(type, payload) {
        const mutation = mutations[type]
        if (mutation) {
          mutation(state, payload)
        } else {
          console.error(`Mutation ${type} not found`)
        }
      },
      
      dispatch(type, payload) {
        const action = actions[type]
        if (action) {
          return action({ state, commit: this.commit }, payload)
        } else {
          console.error(`Action ${type} not found`)
        }
      }
    }
    
    // 提供 store
    app.provide('store', store)
    
    // 添加全局 property
    app.config.globalProperties.$store = store
  }
}

使用这个状态管理插件:

javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import storePlugin from './plugins/store'

const app = createApp(App)

app.use(storePlugin, {
  state: {
    count: 0
  },
  mutations: {
    increment(state) {
      state.count++
    },
    decrement(state) {
      state.count--
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    }
  }
})

app.mount('#app')

在组件中使用:

vue
<template>
  <div>
    <p>Count: {{ store.state.count }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="incrementAsync">+ (async)</button>
  </div>
</template>

<script setup>
import { inject } from 'vue'

const store = inject('store')

function increment() {
  store.commit('increment')
}

function decrement() {
  store.commit('decrement')
}

function incrementAsync() {
  store.dispatch('incrementAsync')
}
</script>

插件的 TypeScript 支持

为插件添加 TypeScript 类型支持:

typescript
// types/vue.d.ts
declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $translate: (key: string) => string
    $http: {
      get(url: string): Promise<any>
      post(url: string, data: any): Promise<any>
    }
  }
}

export {}

插件的类型定义:

typescript
// plugins/myPlugin.ts
import { App } from 'vue'

interface PluginOptions {
  apiUrl?: string
}

export default {
  install(app: App, options: PluginOptions = {}) {
    // 插件逻辑
  }
}

插件最佳实践

1. 命名约定

  • 插件名应该清晰地表达其功能
  • 避免与现有插件冲突
  • 使用一致的命名风格

2. 配置选项

提供合理的默认配置,同时允许用户自定义:

javascript
export default {
  install(app, options = {}) {
    const config = {
      // 默认配置
      prefix: '$',
      debug: false,
      ...options // 用户配置覆盖默认配置
    }
    
    // 使用 config
  }
}

3. 错误处理

javascript
export default {
  install(app, options) {
    if (!options.apiKey) {
      throw new Error('API key is required')
    }
    
    // 插件逻辑
  }
}

4. 开发模式检查

javascript
export default {
  install(app, options) {
    if (process.env.NODE_ENV === 'development') {
      console.log('Plugin installed with options:', options)
    }
    
    // 插件逻辑
  }
}

5. 避免全局污染

谨慎添加全局 property,考虑使用 provide/inject 模式:

javascript
// 好的做法
export default {
  install(app, options) {
    app.provide('myPlugin', {
      // 插件 API
    })
  }
}

// 避免过多的全局 property
export default {
  install(app, options) {
    app.config.globalProperties.$plugin1 = {}
    app.config.globalProperties.$plugin2 = {}
    app.config.globalProperties.$plugin3 = {}
    // ...
  }
}

发布插件

1. 包结构

my-vue-plugin/
├── src/
│   └── index.js
├── dist/
│   ├── index.js
│   └── index.esm.js
├── package.json
├── README.md
└── LICENSE

2. package.json 配置

json
{
  "name": "my-vue-plugin",
  "version": "1.0.0",
  "description": "A Vue 3 plugin",
  "main": "dist/index.js",
  "module": "dist/index.esm.js",
  "files": ["dist"],
  "keywords": ["vue", "plugin", "vue3"],
  "peerDependencies": {
    "vue": "^3.0.0"
  }
}

3. 构建配置

使用 Rollup 或 Vite 构建插件:

javascript
// vite.config.js
import { defineConfig } from 'vite'

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.js',
      name: 'MyVuePlugin',
      fileName: (format) => `index.${format}.js`
    },
    rollupOptions: {
      external: ['vue'],
      output: {
        globals: {
          vue: 'Vue'
        }
      }
    }
  }
})

测试插件

javascript
// tests/plugin.test.js
import { createApp } from 'vue'
import { mount } from '@vue/test-utils'
import MyPlugin from '../src/index.js'

describe('MyPlugin', () => {
  test('should install correctly', () => {
    const app = createApp({})
    app.use(MyPlugin)
    
    expect(app.config.globalProperties.$myMethod).toBeDefined()
  })
  
  test('should work in components', () => {
    const wrapper = mount({
      template: '<div>{{ $myMethod() }}</div>'
    }, {
      global: {
        plugins: [MyPlugin]
      }
    })
    
    expect(wrapper.text()).toBe('expected result')
  })
})

下一步

vue study guide - 专业的 Vue.js 学习平台