组件基础 #
什么是组件? #
组件是可复用的、独立的 UI 单元。在 Alpine.js 中,每个带有 x-data 的元素就是一个组件。使用 Alpine.data() 可以注册可复用的组件定义。
组件定义方式 #
内联定义 #
html
<div x-data="{ count: 0 }">
<button @click="count++">点击: <span x-text="count"></span></button>
</div>
函数定义 #
html
<div x-data="counter()">
<button @click="count++">点击: <span x-text="count"></span></button>
</div>
<script>
function counter() {
return {
count: 0
}
}
</script>
Alpine.data 注册 #
javascript
Alpine.data('counter', () => ({
count: 0
}))
html
<div x-data="counter">
<button @click="count++">点击: <span x-text="count"></span></button>
</div>
Alpine.data 详解 #
基本用法 #
javascript
Alpine.data('组件名', () => ({
// 数据
property: 'value',
// 计算属性
get computed() {
return this.property + '!'
},
// 方法
method() {
this.property = 'new value'
},
// 生命周期
init() {
console.log('组件初始化')
}
}))
带参数的组件 #
javascript
Alpine.data('counter', (initial = 0, step = 1) => ({
count: initial,
step: step,
increment() {
this.count += this.step
},
decrement() {
this.count -= this.step
},
reset() {
this.count = initial
}
}))
html
<div x-data="counter(10, 5)">
<span x-text="count"></span>
<button @click="increment()">+<span x-text="step"></span></button>
<button @click="decrement()">-<span x-text="step"></span></button>
<button @click="reset()">重置</button>
</div>
使用配置对象 #
javascript
Alpine.data('dataTable', (config = {}) => ({
data: [],
loading: false,
page: config.page || 1,
perPage: config.perPage || 10,
sortField: config.sortField || 'id',
sortOrder: config.sortOrder || 'asc',
async fetchData() {
this.loading = true
const res = await fetch(config.apiUrl)
this.data = await res.json()
this.loading = false
},
init() {
this.fetchData()
}
}))
html
<div x-data="dataTable({
apiUrl: '/api/users',
perPage: 20,
sortField: 'name'
})">
</div>
组件示例 #
下拉菜单组件 #
javascript
Alpine.data('dropdown', () => ({
open: false,
toggle() {
this.open = !this.open
},
open() {
this.open = true
},
close() {
this.open = false
}
}))
html
<div x-data="dropdown" class="dropdown">
<button @click="toggle()">
菜单
<span x-show="!open">▼</span>
<span x-show="open">▲</span>
</button>
<div x-show="open" @click.away="close()" class="dropdown-menu">
<a href="#">选项 1</a>
<a href="#">选项 2</a>
<a href="#">选项 3</a>
</div>
</div>
模态框组件 #
javascript
Alpine.data('modal', () => ({
show: false,
open() {
this.show = true
document.body.style.overflow = 'hidden'
},
close() {
this.show = false
document.body.style.overflow = ''
},
toggle() {
this.show ? this.close() : this.open()
}
}))
html
<div x-data="modal">
<button @click="open()">打开模态框</button>
<div
x-show="show"
x-transition
@keydown.escape.window="close()"
class="modal-overlay"
@click.self="close()"
>
<div class="modal-content" @click.stop>
<h2>模态框标题</h2>
<p>模态框内容</p>
<button @click="close()">关闭</button>
</div>
</div>
</div>
标签页组件 #
javascript
Alpine.data('tabs', (defaultTab = null) => ({
tabs: [],
activeTab: defaultTab,
init() {
if (!this.activeTab && this.tabs.length > 0) {
this.activeTab = this.tabs[0].id
}
},
setActive(id) {
this.activeTab = id
},
isActive(id) {
return this.activeTab === id
}
}))
html
<div x-data="tabs('profile')">
<div class="tabs">
<button
@click="setActive('home')"
:class="{ active: isActive('home') }"
>
首页
</button>
<button
@click="setActive('profile')"
:class="{ active: isActive('profile') }"
>
个人资料
</button>
<button
@click="setActive('settings')"
:class="{ active: isActive('settings') }"
>
设置
</button>
</div>
<div class="content">
<div x-show="isActive('home')">首页内容</div>
<div x-show="isActive('profile')">个人资料内容</div>
<div x-show="isActive('settings')">设置内容</div>
</div>
</div>
手风琴组件 #
javascript
Alpine.data('accordion', (allowMultiple = false) => ({
items: [],
toggle(index) {
if (allowMultiple) {
this.items[index].open = !this.items[index].open
} else {
this.items.forEach((item, i) => {
item.open = i === index ? !item.open : false
})
}
},
open(index) {
if (allowMultiple) {
this.items[index].open = true
} else {
this.items.forEach((item, i) => {
item.open = i === index
})
}
},
close(index) {
this.items[index].open = false
},
closeAll() {
this.items.forEach(item => item.open = false)
}
}))
表单验证组件 #
javascript
Alpine.data('formValidator', (rules = {}) => ({
data: {},
errors: {},
touched: {},
rules: rules,
validate(field) {
const value = this.data[field]
const fieldRules = this.rules[field]
if (!fieldRules) return true
for (const rule of fieldRules) {
const error = this.validateRule(value, rule)
if (error) {
this.errors[field] = error
return false
}
}
delete this.errors[field]
return true
},
validateRule(value, rule) {
if (rule.required && !value) {
return rule.message || '此字段必填'
}
if (rule.minLength && value.length < rule.minLength) {
return rule.message || `最少 ${rule.minLength} 个字符`
}
if (rule.maxLength && value.length > rule.maxLength) {
return rule.message || `最多 ${rule.maxLength} 个字符`
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || '格式不正确'
}
if (rule.validator && !rule.validator(value)) {
return rule.message || '验证失败'
}
return null
},
validateAll() {
let valid = true
for (const field of Object.keys(this.rules)) {
if (!this.validate(field)) {
valid = false
}
}
return valid
},
touch(field) {
this.touched[field] = true
},
isTouched(field) {
return this.touched[field]
},
hasError(field) {
return this.errors[field]
},
getError(field) {
return this.errors[field]
}
}))
组件组合 #
继承扩展 #
javascript
Alpine.data('baseInput', () => ({
value: '',
disabled: false,
focused: false,
focus() {
this.focused = true
this.$refs.input?.focus()
},
blur() {
this.focused = false
},
clear() {
this.value = ''
}
}))
Alpine.data('textInput', () => ({
...Alpine.raw(Alpine.data('baseInput')()),
type: 'text',
placeholder: '',
get hasValue() {
return this.value.length > 0
}
}))
Mixin 模式 #
javascript
const loadingMixin = {
loading: false,
startLoading() {
this.loading = true
},
stopLoading() {
this.loading = false
}
}
const notificationMixin = {
notification: null,
showNotification(message, type = 'info') {
this.notification = { message, type }
setTimeout(() => this.notification = null, 3000)
}
}
Alpine.data('userList', () => ({
...loadingMixin,
...notificationMixin,
users: [],
async loadUsers() {
this.startLoading()
try {
const res = await fetch('/api/users')
this.users = await res.json()
} catch (e) {
this.showNotification('加载失败', 'error')
} finally {
this.stopLoading()
}
}
}))
组件注册时机 #
alpine:init 事件 #
javascript
document.addEventListener('alpine:init', () => {
Alpine.data('myComponent', () => ({
// ...
}))
})
模块化注册 #
javascript
import { registerDropdown } from './components/dropdown'
import { registerModal } from './components/modal'
import { registerTabs } from './components/tabs'
document.addEventListener('alpine:init', () => {
registerDropdown()
registerModal()
registerTabs()
})
javascript
export function registerDropdown() {
Alpine.data('dropdown', () => ({
// ...
}))
}
最佳实践 #
1. 单一职责 #
每个组件只负责一个功能:
javascript
Alpine.data('counter', () => ({ /* 只负责计数 */ }))
Alpine.data('timer', () => ({ /* 只负责计时 */ }))
2. 清晰的接口 #
javascript
Alpine.data('toggle', (initial = false) => ({
value: initial,
on() { this.value = true },
off() { this.value = false },
toggle() { this.value = !this.value }
}))
3. 合理的默认值 #
javascript
Alpine.data('pagination', (options = {}) => ({
page: options.page || 1,
perPage: options.perPage || 10,
total: options.total || 0
}))
4. 文档化参数 #
javascript
Alpine.data('dataTable', (config) => ({
apiUrl: config.apiUrl,
perPage: config.perPage ?? 10,
sortable: config.sortable ?? true,
filterable: config.filterable ?? true
}))
小结 #
组件基础要点:
- 使用
Alpine.data注册可复用组件 - 支持参数配置
- 可以组合和扩展
- 在
alpine:init事件中注册 - 遵循单一职责原则
下一章,我们将学习组件通信。
最后更新:2026-03-28