Appearance
插件开发
插件是自包含的代码,通常向 Vue 添加全局级功能。插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property
- 添加全局资源:指令/过滤器/过渡等
- 通过全局混入来添加一些组件选项
- 添加 app 实例方法,通过把它们添加到
app.config.globalProperties上实现 - 一个库,提供自己的 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
└── LICENSE2. 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')
})
})