本章将深入探讨Vue.js的高级特性,包括自定义指令、插件开发、组合式API深入应用、性能优化和测试策略。
7.1 自定义指令
指令基础
自定义指令用于封装对DOM的直接操作,提供可复用的DOM操作逻辑。
// directives/focus.js
export const vFocus = {
// 元素被插入到DOM中时调用
mounted(el) {
el.focus()
},
// 组件更新时调用
updated(el) {
el.focus()
}
}
// 简化写法(只需要mounted和updated时)
export const vFocusSimple = (el) => {
el.focus()
}
// directives/clickOutside.js
export const vClickOutside = {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
// 检查点击是否在元素外部
if (!(el === event.target || el.contains(event.target))) {
// 调用绑定的方法
binding.value(event)
}
}
// 添加事件监听器
document.addEventListener('click', el._clickOutsideHandler)
},
unmounted(el) {
// 清理事件监听器
document.removeEventListener('click', el._clickOutsideHandler)
delete el._clickOutsideHandler
}
}
// directives/loading.js
export const vLoading = {
mounted(el, binding) {
el._loadingInstance = null
updateLoading(el, binding)
},
updated(el, binding) {
updateLoading(el, binding)
},
unmounted(el) {
if (el._loadingInstance) {
el._loadingInstance.remove()
}
}
}
function updateLoading(el, binding) {
if (binding.value) {
showLoading(el, binding.arg)
} else {
hideLoading(el)
}
}
function showLoading(el, text = '加载中...') {
if (el._loadingInstance) return
// 创建loading元素
const loadingEl = document.createElement('div')
loadingEl.className = 'v-loading-overlay'
loadingEl.innerHTML = `
<div class="v-loading-spinner">
<div class="v-loading-dot"></div>
<div class="v-loading-dot"></div>
<div class="v-loading-dot"></div>
<div class="v-loading-text">${text}</div>
</div>
`
// 设置样式
const originalPosition = getComputedStyle(el).position
if (originalPosition === 'static') {
el.style.position = 'relative'
}
el.appendChild(loadingEl)
el._loadingInstance = loadingEl
}
function hideLoading(el) {
if (el._loadingInstance) {
el._loadingInstance.remove()
el._loadingInstance = null
}
}
/* directives/loading.css */
.v-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.v-loading-spinner {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.v-loading-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #3498db;
display: inline-block;
margin: 0 2px;
animation: v-loading-bounce 1.4s ease-in-out infinite both;
}
.v-loading-dot:nth-child(1) { animation-delay: -0.32s; }
.v-loading-dot:nth-child(2) { animation-delay: -0.16s; }
@keyframes v-loading-bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1);
}
}
.v-loading-text {
color: #666;
font-size: 14px;
margin-top: 8px;
}
高级指令示例
// directives/lazyLoad.js
export const vLazyLoad = {
mounted(el, binding) {
const options = {
root: null,
rootMargin: '0px',
threshold: 0.1,
...binding.modifiers
}
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
const src = binding.value
// 创建新的图片对象来预加载
const newImg = new Image()
newImg.onload = () => {
img.src = src
img.classList.remove('lazy-loading')
img.classList.add('lazy-loaded')
}
newImg.onerror = () => {
img.classList.remove('lazy-loading')
img.classList.add('lazy-error')
}
newImg.src = src
observer.unobserve(img)
}
})
}, options)
el.classList.add('lazy-loading')
observer.observe(el)
el._lazyLoadObserver = observer
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
// 重新观察
if (el._lazyLoadObserver) {
el._lazyLoadObserver.unobserve(el)
el._lazyLoadObserver.observe(el)
}
}
},
unmounted(el) {
if (el._lazyLoadObserver) {
el._lazyLoadObserver.unobserve(el)
}
}
}
// directives/permission.js
import { useUserStore } from '@/stores/user'
export const vPermission = {
mounted(el, binding) {
checkPermission(el, binding)
},
updated(el, binding) {
checkPermission(el, binding)
}
}
function checkPermission(el, binding) {
const userStore = useUserStore()
const { value: permission, modifiers } = binding
let hasPermission = false
if (Array.isArray(permission)) {
// 数组权限,支持 AND/OR 逻辑
if (modifiers.or) {
// OR 逻辑:有任一权限即可
hasPermission = permission.some(p => userStore.hasPermission(p))
} else {
// AND 逻辑:需要所有权限
hasPermission = permission.every(p => userStore.hasPermission(p))
}
} else {
// 单个权限
hasPermission = userStore.hasPermission(permission)
}
if (!hasPermission) {
if (modifiers.hide) {
// 隐藏元素
el.style.display = 'none'
} else if (modifiers.disable) {
// 禁用元素
el.disabled = true
el.classList.add('permission-disabled')
} else {
// 默认移除元素
el.remove()
}
} else {
// 恢复元素状态
el.style.display = ''
el.disabled = false
el.classList.remove('permission-disabled')
}
}
注册和使用指令
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 导入指令
import { vFocus } from './directives/focus'
import { vClickOutside } from './directives/clickOutside'
import { vLoading } from './directives/loading'
import { vLazyLoad } from './directives/lazyLoad'
import { vPermission } from './directives/permission'
// 导入指令样式
import './directives/loading.css'
const app = createApp(App)
// 注册全局指令
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.directive('loading', vLoading)
app.directive('lazy-load', vLazyLoad)
app.directive('permission', vPermission)
app.mount('#app')
<!-- 使用指令的组件示例 -->
<template>
<div class="directive-demo">
<!-- 自动聚焦 -->
<input v-focus placeholder="自动聚焦的输入框" class="demo-input">
<!-- 点击外部关闭 -->
<div class="dropdown" :class="{ active: showDropdown }">
<button @click="showDropdown = !showDropdown" class="dropdown-toggle">
下拉菜单
</button>
<div
v-show="showDropdown"
v-click-outside="closeDropdown"
class="dropdown-menu"
>
<div class="dropdown-item">选项 1</div>
<div class="dropdown-item">选项 2</div>
<div class="dropdown-item">选项 3</div>
</div>
</div>
<!-- 加载状态 -->
<div class="loading-demo">
<button @click="toggleLoading" class="demo-button">
{{ isLoading ? '停止加载' : '开始加载' }}
</button>
<div
v-loading="isLoading"
class="loading-container"
>
<p>这里是内容区域</p>
<p>当加载时会显示loading遮罩</p>
</div>
</div>
<!-- 懒加载图片 -->
<div class="lazy-load-demo">
<h3>懒加载图片</h3>
<div class="image-grid">
<img
v-for="(url, index) in imageUrls"
:key="index"
v-lazy-load="url"
:alt="`图片 ${index + 1}`"
class="lazy-image"
>
</div>
</div>
<!-- 权限控制 -->
<div class="permission-demo">
<h3>权限控制</h3>
<button v-permission="'admin'" class="demo-button">
仅管理员可见
</button>
<button v-permission.hide="'edit'" class="demo-button">
无编辑权限时隐藏
</button>
<button v-permission.disable="['read', 'write']" class="demo-button">
需要读写权限
</button>
<button v-permission.or="['admin', 'moderator']" class="demo-button">
管理员或版主可见
</button>
</div>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
name: 'DirectiveDemo',
setup() {
const showDropdown = ref(false)
const isLoading = ref(false)
const imageUrls = ref([
'https://picsum.photos/300/200?random=1',
'https://picsum.photos/300/200?random=2',
'https://picsum.photos/300/200?random=3',
'https://picsum.photos/300/200?random=4',
'https://picsum.photos/300/200?random=5',
'https://picsum.photos/300/200?random=6'
])
function closeDropdown() {
showDropdown.value = false
}
function toggleLoading() {
isLoading.value = !isLoading.value
}
return {
showDropdown,
isLoading,
imageUrls,
closeDropdown,
toggleLoading
}
}
}
</script>
<style scoped>
.directive-demo {
max-width: 800px;
margin: 0 auto;
padding: 2rem;
}
.demo-input {
width: 100%;
padding: 0.75rem;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 2rem;
}
.dropdown {
position: relative;
display: inline-block;
margin-bottom: 2rem;
}
.dropdown-toggle {
padding: 0.75rem 1.5rem;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
min-width: 150px;
z-index: 1000;
}
.dropdown-item {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #f1f1f1;
}
.dropdown-item:hover {
background: #f8f9fa;
}
.dropdown-item:last-child {
border-bottom: none;
}
.loading-demo {
margin-bottom: 2rem;
}
.loading-container {
background: #f8f9fa;
padding: 2rem;
border-radius: 4px;
margin-top: 1rem;
min-height: 150px;
}
.demo-button {
padding: 0.75rem 1.5rem;
background: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
margin-right: 1rem;
margin-bottom: 1rem;
}
.demo-button:hover {
background: #2980b9;
}
.demo-button:disabled {
background: #bdc3c7;
cursor: not-allowed;
}
.permission-disabled {
opacity: 0.5;
cursor: not-allowed;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-top: 1rem;
}
.lazy-image {
width: 100%;
height: 200px;
object-fit: cover;
border-radius: 4px;
transition: opacity 0.3s;
}
.lazy-image.lazy-loading {
opacity: 0.3;
background: #f0f0f0;
}
.lazy-image.lazy-loaded {
opacity: 1;
}
.lazy-image.lazy-error {
opacity: 0.5;
background: #ffebee;
}
.permission-demo {
margin-top: 2rem;
}
.permission-demo h3 {
margin-bottom: 1rem;
color: #2c3e50;
}
</style>
7.2 插件开发
基础插件结构
// plugins/toast.js
import { createApp } from 'vue'
import ToastComponent from './ToastComponent.vue'
class ToastManager {
constructor() {
this.toasts = []
this.container = null
this.init()
}
init() {
// 创建容器
this.container = document.createElement('div')
this.container.className = 'toast-container'
document.body.appendChild(this.container)
}
show(options) {
const toast = {
id: Date.now() + Math.random(),
message: '',
type: 'info',
duration: 3000,
closable: true,
...options
}
this.toasts.push(toast)
this.render()
// 自动关闭
if (toast.duration > 0) {
setTimeout(() => {
this.close(toast.id)
}, toast.duration)
}
return toast.id
}
close(id) {
const index = this.toasts.findIndex(toast => toast.id === id)
if (index > -1) {
this.toasts.splice(index, 1)
this.render()
}
}
clear() {
this.toasts = []
this.render()
}
render() {
// 清空容器
this.container.innerHTML = ''
// 渲染每个toast
this.toasts.forEach(toast => {
const toastEl = document.createElement('div')
const app = createApp(ToastComponent, {
...toast,
onClose: () => this.close(toast.id)
})
app.mount(toastEl)
this.container.appendChild(toastEl.firstElementChild)
})
}
// 便捷方法
success(message, options = {}) {
return this.show({ message, type: 'success', ...options })
}
error(message, options = {}) {
return this.show({ message, type: 'error', ...options })
}
warning(message, options = {}) {
return this.show({ message, type: 'warning', ...options })
}
info(message, options = {}) {
return this.show({ message, type: 'info', ...options })
}
}
// 创建插件
export default {
install(app, options = {}) {
const toastManager = new ToastManager()
// 全局属性
app.config.globalProperties.$toast = toastManager
// 提供注入
app.provide('toast', toastManager)
}
}
// 导出管理器类供直接使用
export { ToastManager }
<!-- plugins/ToastComponent.vue -->
<template>
<transition name="toast" appear>
<div
class="toast"
:class="[
`toast-${type}`,
{ 'toast-closable': closable }
]"
>
<div class="toast-icon">
<component :is="iconComponent" />
</div>
<div class="toast-content">
<div class="toast-message">{{ message }}</div>
</div>
<button
v-if="closable"
@click="$emit('close')"
class="toast-close"
>
×
</button>
</div>
</transition>
</template>
<script>
import { computed } from 'vue'
// 图标组件
const SuccessIcon = {
template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z"/></svg>'
}
const ErrorIcon = {
template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>'
}
const WarningIcon = {
template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/></svg>'
}
const InfoIcon = {
template: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/></svg>'
}
export default {
name: 'ToastComponent',
props: {
message: {
type: String,
required: true
},
type: {
type: String,
default: 'info',
validator: (value) => ['success', 'error', 'warning', 'info'].includes(value)
},
closable: {
type: Boolean,
default: true
}
},
emits: ['close'],
setup(props) {
const iconComponent = computed(() => {
const icons = {
success: SuccessIcon,
error: ErrorIcon,
warning: WarningIcon,
info: InfoIcon
}
return icons[props.type]
})
return {
iconComponent
}
}
}
</script>
<style scoped>
.toast {
display: flex;
align-items: center;
padding: 1rem 1.5rem;
margin-bottom: 0.5rem;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
min-width: 300px;
max-width: 500px;
position: relative;
}
.toast-success {
background: #d4edda;
color: #155724;
border-left: 4px solid #28a745;
}
.toast-error {
background: #f8d7da;
color: #721c24;
border-left: 4px solid #dc3545;
}
.toast-warning {
background: #fff3cd;
color: #856404;
border-left: 4px solid #ffc107;
}
.toast-info {
background: #d1ecf1;
color: #0c5460;
border-left: 4px solid #17a2b8;
}
.toast-icon {
width: 20px;
height: 20px;
margin-right: 0.75rem;
flex-shrink: 0;
}
.toast-content {
flex: 1;
}
.toast-message {
font-size: 0.9rem;
line-height: 1.4;
}
.toast-close {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
padding: 0;
margin-left: 1rem;
opacity: 0.7;
transition: opacity 0.2s;
}
.toast-close:hover {
opacity: 1;
}
/* 动画 */
.toast-enter-active {
transition: all 0.3s ease;
}
.toast-leave-active {
transition: all 0.3s ease;
}
.toast-enter-from {
opacity: 0;
transform: translateX(100%);
}
.toast-leave-to {
opacity: 0;
transform: translateX(100%);
}
</style>
/* plugins/toast.css */
.toast-container {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
pointer-events: none;
}
.toast-container .toast {
pointer-events: auto;
}
表单验证插件
// plugins/validator.js
class Validator {
constructor() {
this.rules = new Map()
this.messages = new Map()
this.setupDefaultRules()
}
setupDefaultRules() {
// 必填
this.addRule('required', (value) => {
if (Array.isArray(value)) return value.length > 0
if (typeof value === 'string') return value.trim().length > 0
return value != null && value !== ''
}, '此字段为必填项')
// 邮箱
this.addRule('email', (value) => {
if (!value) return true
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value)
}, '请输入有效的邮箱地址')
// 最小长度
this.addRule('minLength', (value, min) => {
if (!value) return true
return value.length >= min
}, (min) => `最少需要${min}个字符`)
// 最大长度
this.addRule('maxLength', (value, max) => {
if (!value) return true
return value.length <= max
}, (max) => `最多允许${max}个字符`)
// 数字范围
this.addRule('range', (value, min, max) => {
if (!value) return true
const num = Number(value)
return !isNaN(num) && num >= min && num <= max
}, (min, max) => `请输入${min}到${max}之间的数字`)
// 正则表达式
this.addRule('pattern', (value, regex) => {
if (!value) return true
return regex.test(value)
}, '格式不正确')
// 确认密码
this.addRule('confirmed', (value, confirmValue) => {
return value === confirmValue
}, '两次输入的密码不一致')
}
addRule(name, validator, message) {
this.rules.set(name, validator)
this.messages.set(name, message)
}
validate(value, rules) {
const errors = []
for (const rule of rules) {
let ruleName, ruleArgs = []
if (typeof rule === 'string') {
ruleName = rule
} else if (typeof rule === 'object') {
ruleName = rule.name
ruleArgs = rule.args || []
}
const validator = this.rules.get(ruleName)
if (!validator) {
console.warn(`Unknown validation rule: ${ruleName}`)
continue
}
const isValid = validator(value, ...ruleArgs)
if (!isValid) {
const message = this.messages.get(ruleName)
const errorMessage = typeof message === 'function'
? message(...ruleArgs)
: message
errors.push(errorMessage)
}
}
return {
valid: errors.length === 0,
errors
}
}
validateForm(formData, schema) {
const result = {
valid: true,
errors: {},
firstError: null
}
for (const [field, rules] of Object.entries(schema)) {
const value = formData[field]
const validation = this.validate(value, rules)
if (!validation.valid) {
result.valid = false
result.errors[field] = validation.errors
if (!result.firstError) {
result.firstError = {
field,
message: validation.errors[0]
}
}
}
}
return result
}
}
// Vue插件
export default {
install(app, options = {}) {
const validator = new Validator()
// 添加自定义规则
if (options.rules) {
for (const [name, config] of Object.entries(options.rules)) {
validator.addRule(name, config.validator, config.message)
}
}
app.config.globalProperties.$validator = validator
app.provide('validator', validator)
}
}
export { Validator }
使用插件
// main.js
import { createApp } from 'vue'
import App from './App.vue'
// 导入插件
import ToastPlugin from './plugins/toast'
import ValidatorPlugin from './plugins/validator'
// 导入样式
import './plugins/toast.css'
const app = createApp(App)
// 使用插件
app.use(ToastPlugin)
app.use(ValidatorPlugin, {
rules: {
phone: {
validator: (value) => {
if (!value) return true
return /^1[3-9]\d{9}$/.test(value)
},
message: '请输入有效的手机号码'
}
}
})
app.mount('#app')
<!-- 使用插件的组件 -->
<template>
<div class="plugin-demo">
<h2>插件使用示例</h2>
<!-- Toast示例 -->
<div class="toast-demo">
<h3>Toast通知</h3>
<div class="button-group">
<button @click="showSuccess" class="btn btn-success">成功</button>
<button @click="showError" class="btn btn-danger">错误</button>
<button @click="showWarning" class="btn btn-warning">警告</button>
<button @click="showInfo" class="btn btn-info">信息</button>
</div>
</div>
<!-- 表单验证示例 -->
<div class="form-demo">
<h3>表单验证</h3>
<form @submit.prevent="handleSubmit" class="demo-form">
<div class="form-group">
<label>用户名 *</label>
<input
v-model="form.username"
type="text"
class="form-control"
:class="{ 'is-invalid': errors.username }"
>
<div v-if="errors.username" class="invalid-feedback">
{{ errors.username[0] }}
</div>
</div>
<div class="form-group">
<label>邮箱 *</label>
<input
v-model="form.email"
type="email"
class="form-control"
:class="{ 'is-invalid': errors.email }"
>
<div v-if="errors.email" class="invalid-feedback">
{{ errors.email[0] }}
</div>
</div>
<div class="form-group">
<label>手机号</label>
<input
v-model="form.phone"
type="tel"
class="form-control"
:class="{ 'is-invalid': errors.phone }"
>
<div v-if="errors.phone" class="invalid-feedback">
{{ errors.phone[0] }}
</div>
</div>
<div class="form-group">
<label>密码 *</label>
<input
v-model="form.password"
type="password"
class="form-control"
:class="{ 'is-invalid': errors.password }"
>
<div v-if="errors.password" class="invalid-feedback">
{{ errors.password[0] }}
</div>
</div>
<div class="form-group">
<label>确认密码 *</label>
<input
v-model="form.confirmPassword"
type="password"
class="form-control"
:class="{ 'is-invalid': errors.confirmPassword }"
>
<div v-if="errors.confirmPassword" class="invalid-feedback">
{{ errors.confirmPassword[0] }}
</div>
</div>
<div class="form-group">
<label>年龄</label>
<input
v-model="form.age"
type="number"
class="form-control"
:class="{ 'is-invalid': errors.age }"
>
<div v-if="errors.age" class="invalid-feedback">
{{ errors.age[0] }}
</div>
</div>
<button type="submit" class="btn btn-primary">提交</button>
<button type="button" @click="resetForm" class="btn btn-secondary">重置</button>
</form>
</div>
</div>
</template>
<script>
import { ref, inject } from 'vue'
export default {
name: 'PluginDemo',
setup() {
const toast = inject('toast')
const validator = inject('validator')
const form = ref({
username: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
age: ''
})
const errors = ref({})
// 表单验证规则
const validationSchema = {
username: ['required', { name: 'minLength', args: [3] }],
email: ['required', 'email'],
phone: ['phone'],
password: ['required', { name: 'minLength', args: [6] }],
confirmPassword: [
'required',
{ name: 'confirmed', args: [form.value.password] }
],
age: [{ name: 'range', args: [1, 120] }]
}
function showSuccess() {
toast.success('操作成功!')
}
function showError() {
toast.error('操作失败,请重试')
}
function showWarning() {
toast.warning('请注意检查输入内容')
}
function showInfo() {
toast.info('这是一条信息提示', { duration: 5000 })
}
function handleSubmit() {
// 更新确认密码验证规则
validationSchema.confirmPassword = [
'required',
{ name: 'confirmed', args: [form.value.password] }
]
const result = validator.validateForm(form.value, validationSchema)
if (result.valid) {
errors.value = {}
toast.success('表单验证通过!')
console.log('提交表单:', form.value)
} else {
errors.value = result.errors
toast.error(`验证失败: ${result.firstError.message}`)
}
}
function resetForm() {
form.value = {
username: '',
email: '',
phone: '',
password: '',
confirmPassword: '',
age: ''
}
errors.value = {}
toast.info('表单已重置')
}
return {
form,
errors,
showSuccess,
showError,
showWarning,
showInfo,
handleSubmit,
resetForm
}
}
}
</script>
<style scoped>
.plugin-demo {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
}
.toast-demo {
margin-bottom: 3rem;
}
.button-group {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.btn {
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
transition: all 0.3s;
}
.btn-success {
background: #28a745;
color: white;
}
.btn-success:hover {
background: #218838;
}
.btn-danger {
background: #dc3545;
color: white;
}
.btn-danger:hover {
background: #c82333;
}
.btn-warning {
background: #ffc107;
color: #212529;
}
.btn-warning:hover {
background: #e0a800;
}
.btn-info {
background: #17a2b8;
color: white;
}
.btn-info:hover {
background: #138496;
}
.btn-primary {
background: #007bff;
color: white;
}
.btn-primary:hover {
background: #0056b3;
}
.btn-secondary {
background: #6c757d;
color: white;
margin-left: 1rem;
}
.btn-secondary:hover {
background: #545b62;
}
.demo-form {
background: #f8f9fa;
padding: 2rem;
border-radius: 8px;
}
.form-group {
margin-bottom: 1.5rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
color: #333;
}
.form-control {
width: 100%;
padding: 0.75rem;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.form-control:focus {
outline: none;
border-color: #80bdff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.form-control.is-invalid {
border-color: #dc3545;
}
.form-control.is-invalid:focus {
border-color: #dc3545;
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
}
.invalid-feedback {
display: block;
width: 100%;
margin-top: 0.25rem;
font-size: 0.875rem;
color: #dc3545;
}
h2, h3 {
color: #2c3e50;
margin-bottom: 1rem;
}
</style>
本章小结
本章我们学习了Vue.js的高级特性:
- 自定义指令:封装DOM操作逻辑,提供可复用的功能
- 插件开发:创建可复用的功能模块,扩展Vue应用能力
- 高级组件模式:深入理解组件设计和架构
- 性能优化:提升应用性能的各种技巧
下一章预告
下一章我们将学习Vue.js的测试策略,包括: - 单元测试和组件测试 - 集成测试和端到端测试 - 测试工具和最佳实践 - 持续集成和部署
练习题
基础练习
自定义指令练习:
- 创建一个图片懒加载指令
- 实现一个权限控制指令
- 开发一个表单验证指令
插件开发练习:
- 创建一个通知插件
- 开发一个表单验证插件
- 实现一个国际化插件
进阶练习
高级组件练习:
- 创建一个可配置的数据表格组件
- 实现一个拖拽排序组件
- 开发一个富文本编辑器组件
性能优化练习:
- 分析应用性能瓶颈
- 实现虚拟滚动列表
- 优化大型表单的渲染性能
提示:掌握这些高级特性能让你开发出更加专业和高效的Vue.js应用。