掌握插件基础后,本节通过实际案例学习如何编写功能完整的插件。
工具类插件提供常用的工具方法:
const utilsPlugin = {
install(Vue, options = {}) {
const utils = {
debounce(fn, delay = 300) {
let timer = null
return function(...args) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, args)
}, delay)
}
},
throttle(fn, delay = 300) {
let lastTime = 0
return function(...args) {
const now = Date.now()
if (now - lastTime >= delay) {
lastTime = now
fn.apply(this, args)
}
}
},
deepClone(obj) {
if (obj === null || typeof obj !== 'object') return obj
const clone = Array.isArray(obj) ? [] : {}
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = this.deepClone(obj[key])
}
}
return clone
},
formatDate(date, format = 'YYYY-MM-DD') {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
const hours = String(d.getHours()).padStart(2, '0')
const minutes = String(d.getMinutes()).padStart(2, '0')
const seconds = String(d.getSeconds()).padStart(2, '0')
return format
.replace('YYYY', year)
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
},
formatNumber(num, decimals = 2) {
return Number(num).toFixed(decimals)
},
formatCurrency(value, symbol = '¥') {
return symbol + Number(value).toLocaleString('zh-CN', {
minimumFractionDigits: 2,
maximumFractionDigits: 2
})
}
}
Vue.prototype.$utils = utils
Vue.utils = utils
}
}
export default utilsPlugin
封装 HTTP 请求功能:
import axios from 'axios'
const httpPlugin = {
install(Vue, options = {}) {
const instance = axios.create({
baseURL: options.baseURL || '',
timeout: options.timeout || 10000,
headers: options.headers || {}
})
instance.interceptors.request.use(
config => {
if (options.onRequest) {
options.onRequest(config)
}
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => {
return Promise.reject(error)
}
)
instance.interceptors.response.use(
response => {
if (options.onResponse) {
return options.onResponse(response)
}
return response.data
},
error => {
if (options.onError) {
return options.onError(error)
}
const message = error.response?.data?.message || '请求失败'
console.error(message)
return Promise.reject(error)
}
)
const http = {
get(url, params, config = {}) {
return instance.get(url, { params, ...config })
},
post(url, data, config = {}) {
return instance.post(url, data, config)
},
put(url, data, config = {}) {
return instance.put(url, data, config)
},
delete(url, config = {}) {
return instance.delete(url, config)
}
}
Vue.prototype.$http = http
Vue.http = http
}
}
export default httpPlugin
创建消息提示功能:
const toastPlugin = {
install(Vue, options = {}) {
const defaultOptions = {
duration: 3000,
position: 'top',
type: 'info'
}
const ToastConstructor = Vue.extend({
template: `
<transition name="toast-fade">
<div v-if="visible" :class="['toast', 'toast-' + type, 'toast-' + position]">
<span class="toast-icon">{{ icon }}</span>
<span class="toast-message">{{ message }}</span>
</div>
</transition>
`,
data() {
return {
visible: false,
message: '',
type: 'info',
position: 'top'
}
},
computed: {
icon() {
const icons = {
success: '✓',
error: '✗',
warning: '⚠',
info: 'ℹ'
}
return icons[this.type]
}
}
})
let instance = null
const toast = {
show(message, type = 'info', duration = defaultOptions.duration) {
if (!instance) {
instance = new ToastConstructor()
document.body.appendChild(instance.$mount().$el)
}
instance.message = message
instance.type = type
instance.position = defaultOptions.position
instance.visible = true
setTimeout(() => {
instance.visible = false
}, duration)
},
success(message, duration) {
this.show(message, 'success', duration)
},
error(message, duration) {
this.show(message, 'error', duration)
},
warning(message, duration) {
this.show(message, 'warning', duration)
},
info(message, duration) {
this.show(message, 'info', duration)
}
}
Vue.prototype.$toast = toast
Vue.toast = toast
}
}
export default toastPlugin
const confirmPlugin = {
install(Vue, options = {}) {
const ConfirmConstructor = Vue.extend({
template: `
<transition name="confirm-fade">
<div v-if="visible" class="confirm-overlay" @click.self="cancel">
<div class="confirm-box">
<div class="confirm-header">
<h3>{{ title }}</h3>
</div>
<div class="confirm-body">
<p>{{ message }}</p>
</div>
<div class="confirm-footer">
<button class="btn-cancel" @click="cancel">{{ cancelText }}</button>
<button class="btn-confirm" @click="confirm">{{ confirmText }}</button>
</div>
</div>
</div>
</transition>
`,
data() {
return {
visible: false,
title: '提示',
message: '',
confirmText: '确定',
cancelText: '取消',
resolve: null,
reject: null
}
},
methods: {
confirm() {
this.visible = false
if (this.resolve) {
this.resolve(true)
}
},
cancel() {
this.visible = false
if (this.resolve) {
this.resolve(false)
}
}
}
})
let instance = null
const confirm = {
show(message, title = '提示', options = {}) {
return new Promise((resolve) => {
if (!instance) {
instance = new ConfirmConstructor()
document.body.appendChild(instance.$mount().$el)
}
instance.message = message
instance.title = title
instance.confirmText = options.confirmText || '确定'
instance.cancelText = options.cancelText || '取消'
instance.resolve = resolve
instance.visible = true
})
}
}
Vue.prototype.$confirm = confirm
Vue.confirm = confirm
}
}
export default confirmPlugin
const loadingPlugin = {
install(Vue, options = {}) {
const LoadingConstructor = Vue.extend({
template: `
<transition name="loading-fade">
<div v-if="visible" class="loading-overlay">
<div class="loading-spinner">
<div class="spinner"></div>
<p v-if="text" class="loading-text">{{ text }}</p>
</div>
</div>
</transition>
`,
data() {
return {
visible: false,
text: ''
}
}
})
let instance = null
let count = 0
const loading = {
show(text = '') {
count++
if (!instance) {
instance = new LoadingConstructor()
document.body.appendChild(instance.$mount().$el)
}
instance.text = text
instance.visible = true
},
hide() {
count--
if (count <= 0) {
count = 0
if (instance) {
instance.visible = false
}
}
}
}
Vue.prototype.$loading = loading
Vue.loading = loading
}
}
export default loadingPlugin
const directivePlugin = {
install(Vue, options = {}) {
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__
}
})
Vue.directive('focus', {
inserted(el, binding) {
if (binding.value !== false) {
el.focus()
}
}
})
Vue.directive('lazy', {
inserted(el, binding) {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
el.src = binding.value
observer.unobserve(el)
}
})
})
observer.observe(el)
el.__observer__ = observer
},
unbind(el) {
if (el.__observer__) {
el.__observer__.disconnect()
}
}
})
Vue.directive('permission', {
inserted(el, binding) {
const { value } = binding
const permissions = options.permissions || []
if (value && !permissions.includes(value)) {
el.parentNode?.removeChild(el)
}
}
})
Vue.directive('debounce', {
inserted(el, binding) {
let timer = null
el.addEventListener('input', () => {
clearTimeout(timer)
timer = setTimeout(() => {
el.dispatchEvent(new Event('change'))
}, binding.value || 300)
})
}
})
}
}
export default directivePlugin
import Button from './components/Button.vue'
import Input from './components/Input.vue'
import Modal from './components/Modal.vue'
import Table from './components/Table.vue'
const components = {
Button,
Input,
Modal,
Table
}
const uiPlugin = {
install(Vue, options = {}) {
const prefix = options.prefix || 'V'
Object.keys(components).forEach(name => {
const componentName = prefix + name
Vue.component(componentName, components[name])
})
Vue.prototype.$UI_CONFIG = {
size: options.size || 'medium',
theme: options.theme || 'light'
}
}
}
export default uiPlugin
插件编写要点: