插件开发 #
概述 #
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