组件基础 #

什么是组件? #

组件是可复用的、独立的 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