插件开发 #

概述 #

Alpine.js 提供了丰富的扩展 API,允许开发者创建自定义插件来扩展框架功能。本章将介绍如何开发 Alpine.js 插件。

Alpine.plugin #

基本用法 #

javascript
Alpine.plugin((Alpine) => {
    Alpine.magic('foo', () => 'bar')
    Alpine.directive('foo', (el, { value }) => { })
})

注册插件 #

javascript
import MyPlugin from './my-plugin'

Alpine.plugin(MyPlugin)

Alpine.magic #

创建魔术属性(以 $ 开头的属性):

基本用法 #

javascript
Alpine.magic('now', () => {
    return Date.now()
})
html
<div x-data>
    <span x-text="$now"></span>
</div>

访问组件上下文 #

javascript
Alpine.magic('log', (el, { Alpine }) => {
    return (message) => {
        console.log(`[${el.tagName}]:`, message)
    }
})
html
<div x-data>
    <button @click="$log('clicked!')">点击</button>
</div>

实用示例 #

工具函数 #

javascript
Alpine.magic('utils', () => ({
    formatCurrency(value) {
        return new Intl.NumberFormat('zh-CN', {
            style: 'currency',
            currency: 'CNY'
        }).format(value)
    },
    
    formatDate(date) {
        return new Intl.DateTimeFormat('zh-CN').format(new Date(date))
    },
    
    debounce(fn, delay) {
        let timer
        return (...args) => {
            clearTimeout(timer)
            timer = setTimeout(() => fn(...args), delay)
        }
    }
}))
html
<div x-data="{ price: 1234.56 }">
    <span x-text="$utils.formatCurrency(price)"></span>
</div>

本地存储 #

javascript
Alpine.magic('localStorage', () => ({
    get(key, defaultValue = null) {
        const value = localStorage.getItem(key)
        return value ? JSON.parse(value) : defaultValue
    },
    
    set(key, value) {
        localStorage.setItem(key, JSON.stringify(value))
    },
    
    remove(key) {
        localStorage.removeItem(key)
    }
}))
html
<div x-data x-init="theme = $localStorage.get('theme', 'light')">
    <select x-model="theme" @change="$localStorage.set('theme', theme)">
        <option value="light">浅色</option>
        <option value="dark">深色</option>
    </select>
</div>

复制到剪贴板 #

javascript
Alpine.magic('clipboard', () => ({
    async copy(text) {
        await navigator.clipboard.writeText(text)
        return true
    },
    
    async read() {
        return await navigator.clipboard.readText()
    }
}))
html
<div x-data>
    <button @click="$clipboard.copy('Hello World')">复制</button>
</div>

Alpine.directive #

创建自定义指令:

基本用法 #

javascript
Alpine.directive('uppercase', (el, { expression }, { effect, evaluateLater }) => {
    const getValue = evaluateLater(expression)
    
    effect(() => {
        getValue(value => {
            el.textContent = value.toUpperCase()
        })
    })
})
html
<div x-data="{ name: 'john' }">
    <span x-uppercase="name"></span>
</div>

指令参数 #

javascript
Alpine.directive('click-outside', (el, { expression }, { evaluateLater, cleanup }) => {
    const handler = evaluateLater(expression)
    
    const onClick = (e) => {
        if (!el.contains(e.target)) {
            handler()
        }
    }
    
    document.addEventListener('click', onClick)
    
    cleanup(() => {
        document.removeEventListener('click', onClick)
    })
})
html
<div x-data="{ open: false }">
    <button @click="open = true">打开</button>
    <div x-show="open" x-click-outside="open = false">
        内容
    </div>
</div>

指令修饰符 #

javascript
Alpine.directive('tooltip', (el, { expression, modifiers }, { evaluateLater, effect }) => {
    const getContent = evaluateLater(expression)
    const position = modifiers.includes('top') ? 'top' : 'bottom'
    
    let tooltip = null
    
    el.addEventListener('mouseenter', () => {
        getContent(content => {
            tooltip = document.createElement('div')
            tooltip.className = `tooltip tooltip-${position}`
            tooltip.textContent = content
            document.body.appendChild(tooltip)
            
            const rect = el.getBoundingClientRect()
            tooltip.style.left = rect.left + 'px'
            tooltip.style.top = position === 'top' 
                ? (rect.top - tooltip.offsetHeight - 8) + 'px'
                : (rect.bottom + 8) + 'px'
        })
    })
    
    el.addEventListener('mouseleave', () => {
        if (tooltip) {
            tooltip.remove()
            tooltip = null
        }
    })
})
html
<div x-data>
    <button x-tooltip.top="'这是提示'">悬停查看</button>
</div>

指令值 #

javascript
Alpine.directive('intersect', (el, { value, expression }, { evaluateLater }) => {
    const handler = evaluateLater(expression)
    const options = {
        threshold: value ? parseFloat(value) : 0.5
    }
    
    const observer = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                handler()
            }
        })
    }, options)
    
    observer.observe(el)
})
html
<div x-data>
    <div x-intersect:0.3="console.log('进入视口')">
        滚动到此处触发
    </div>
</div>

Alpine.bind #

创建可复用的绑定对象:

javascript
Alpine.bind('submitBtn', () => ({
    type: 'submit',
    class: 'btn btn-primary',
    ':disabled': 'loading',
    '@click': 'submit()',
    x-text: "loading ? '提交中...' : '提交'"
}))
html
<div x-data="{ loading: false, submit() { } }">
    <button x-bind="submitBtn"></button>
</div>

完整插件示例 #

持久化插件 #

javascript
function PersistPlugin(Alpine) {
    Alpine.magic('persist', (el, { Alpine }) => {
        return (key, defaultValue = null) => {
            const stored = localStorage.getItem(key)
            return stored ? JSON.parse(stored) : defaultValue
        }
    })
    
    Alpine.directive('persist', (el, { expression, value }, { effect, evaluateLater }) => {
        const key = expression || el.getAttribute('x-persist-key') || 'persist'
        const getValue = value ? evaluateLater(value) : null
        
        effect(() => {
            if (getValue) {
                getValue(val => {
                    localStorage.setItem(key, JSON.stringify(val))
                })
            }
        })
    })
}

Alpine.plugin(PersistPlugin)

表单验证插件 #

javascript
function ValidationPlugin(Alpine) {
    Alpine.magic('validate', () => {
        return (value, rules) => {
            const errors = []
            
            if (rules.required && !value) {
                errors.push('此字段必填')
            }
            if (rules.email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {
                errors.push('请输入有效的邮箱地址')
            }
            if (rules.minLength && value.length < rules.minLength) {
                errors.push(`最少 ${rules.minLength} 个字符`)
            }
            if (rules.maxLength && value.length > rules.maxLength) {
                errors.push(`最多 ${rules.maxLength} 个字符`)
            }
            if (rules.pattern && !rules.pattern.test(value)) {
                errors.push(rules.message || '格式不正确')
            }
            
            return errors
        }
    })
    
    Alpine.directive('validate', (el, { expression, modifiers }, { evaluateLater, effect, cleanup }) => {
        const getRules = evaluateLater(expression)
        const showError = modifiers.includes('show')
        
        let errorEl = null
        
        effect(() => {
            getRules(rules => {
                const value = el.value
                const errors = Alpine.magic('validate')(value, rules)
                
                if (showError) {
                    if (errorEl) errorEl.remove()
                    
                    if (errors.length > 0) {
                        errorEl = document.createElement('span')
                        errorEl.className = 'error-message'
                        errorEl.textContent = errors[0]
                        el.parentNode.appendChild(errorEl)
                        el.classList.add('error')
                    } else {
                        el.classList.remove('error')
                    }
                }
            })
        })
    })
}

Alpine.plugin(ValidationPlugin)

Focus 插件 #

javascript
function FocusPlugin(Alpine) {
    Alpine.directive('focus', (el, { value, modifiers }, { effect, cleanup }) => {
        const delay = value ? parseInt(value) : 0
        const select = modifiers.includes('select')
        
        effect(() => {
            setTimeout(() => {
                el.focus()
                if (select) el.select()
            }, delay)
        })
    })
    
    Alpine.magic('focus', () => ({
        first(selector) {
            const el = document.querySelector(selector)
            el?.focus()
        },
        all(selector) {
            document.querySelectorAll(selector).forEach(el => el.focus())
        }
    }))
}

Alpine.plugin(FocusPlugin)

插件发布 #

NPM 包结构 #

text
my-alpine-plugin/
├── src/
│   └── index.js
├── dist/
│   ├── index.js
│   └── index.min.js
├── package.json
└── README.md

package.json #

json
{
    "name": "alpinejs-my-plugin",
    "version": "1.0.0",
    "main": "dist/index.js",
    "module": "dist/index.esm.js",
    "peerDependencies": {
        "alpinejs": "^3.0.0"
    }
}

入口文件 #

javascript
export default function MyPlugin(Alpine) {
    Alpine.magic('myMagic', () => { })
    Alpine.directive('myDirective', () => { })
}

if (window.Alpine) {
    window.Alpine.plugin(MyPlugin)
}

最佳实践 #

1. 命名规范 #

javascript
Alpine.magic('myFeature', () => { })    // $myFeature
Alpine.directive('my-feature', () => { }) // x-my-feature

2. 清理资源 #

javascript
Alpine.directive('myDirective', (el, {}, { cleanup }) => {
    const handler = () => { }
    document.addEventListener('click', handler)
    
    cleanup(() => {
        document.removeEventListener('click', handler)
    })
})

3. 提供配置选项 #

javascript
function MyPlugin(Alpine, options = {}) {
    const prefix = options.prefix || 'my'
    
    Alpine.directive(`${prefix}-directive`, () => { })
}

小结 #

插件开发要点:

  • 使用 Alpine.plugin 注册插件
  • 使用 Alpine.magic 创建魔术属性
  • 使用 Alpine.directive 创建自定义指令
  • 使用 Alpine.bind 创建可复用绑定
  • 记得清理资源

下一章,我们将学习全局状态管理。

最后更新:2026-03-28