第一个Alpine.js应用 #

项目概述 #

我们将创建一个功能完整的待办事项(Todo List)应用,包含以下功能:

  • 添加待办事项
  • 标记完成/未完成
  • 删除待办事项
  • 过滤显示(全部/未完成/已完成)
  • 本地存储持久化

完整代码 #

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Alpine.js 待办事项</title>
    <script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
    <script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/persist@3.x.x/dist/cdn.min.js"></script>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 40px 20px;
        }
        .container {
            max-width: 500px;
            margin: 0 auto;
            background: white;
            border-radius: 16px;
            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            overflow: hidden;
        }
        .header {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 30px;
            text-align: center;
        }
        .header h1 {
            font-size: 24px;
            margin-bottom: 8px;
        }
        .header p {
            opacity: 0.8;
            font-size: 14px;
        }
        .input-section {
            padding: 20px;
            border-bottom: 1px solid #eee;
        }
        .input-wrapper {
            display: flex;
            gap: 10px;
        }
        .input-wrapper input {
            flex: 1;
            padding: 12px 16px;
            border: 2px solid #eee;
            border-radius: 8px;
            font-size: 16px;
            outline: none;
            transition: border-color 0.3s;
        }
        .input-wrapper input:focus {
            border-color: #667eea;
        }
        .input-wrapper button {
            padding: 12px 24px;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            border: none;
            border-radius: 8px;
            font-size: 16px;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .input-wrapper button:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
        }
        .filters {
            display: flex;
            padding: 15px 20px;
            gap: 10px;
            border-bottom: 1px solid #eee;
        }
        .filter-btn {
            flex: 1;
            padding: 8px;
            border: none;
            background: #f5f5f5;
            border-radius: 6px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .filter-btn.active {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
        }
        .todo-list {
            list-style: none;
            max-height: 400px;
            overflow-y: auto;
        }
        .todo-item {
            display: flex;
            align-items: center;
            padding: 15px 20px;
            border-bottom: 1px solid #f0f0f0;
            transition: background 0.3s;
        }
        .todo-item:hover {
            background: #f9f9f9;
        }
        .todo-item.completed .todo-text {
            text-decoration: line-through;
            color: #aaa;
        }
        .todo-checkbox {
            width: 22px;
            height: 22px;
            margin-right: 15px;
            cursor: pointer;
            accent-color: #667eea;
        }
        .todo-text {
            flex: 1;
            font-size: 16px;
        }
        .delete-btn {
            padding: 6px 12px;
            background: #ff4757;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            opacity: 0;
            transition: opacity 0.3s;
        }
        .todo-item:hover .delete-btn {
            opacity: 1;
        }
        .empty-state {
            text-align: center;
            padding: 60px 20px;
            color: #aaa;
        }
        .empty-state svg {
            width: 80px;
            height: 80px;
            margin-bottom: 20px;
            opacity: 0.5;
        }
        .footer {
            padding: 15px 20px;
            background: #f9f9f9;
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 14px;
            color: #666;
        }
        .clear-btn {
            padding: 6px 12px;
            background: transparent;
            border: 1px solid #ddd;
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.3s;
        }
        .clear-btn:hover {
            background: #ff4757;
            color: white;
            border-color: #ff4757;
        }
    </style>
</head>
<body>
    <div class="container" x-data="todoApp()" x-init="init()">
        <div class="header">
            <h1>待办事项</h1>
            <p>使用 Alpine.js 构建</p>
        </div>
        
        <div class="input-section">
            <div class="input-wrapper">
                <input 
                    type="text" 
                    x-model="newTodo" 
                    @keyup.enter="addTodo()"
                    placeholder="添加新的待办事项..."
                >
                <button @click="addTodo()">添加</button>
            </div>
        </div>
        
        <div class="filters">
            <button 
                class="filter-btn" 
                :class="{ active: filter === 'all' }"
                @click="filter = 'all'"
            >
                全部
            </button>
            <button 
                class="filter-btn" 
                :class="{ active: filter === 'active' }"
                @click="filter = 'active'"
            >
                未完成
            </button>
            <button 
                class="filter-btn" 
                :class="{ active: filter === 'completed' }"
                @click="filter = 'completed'"
            >
                已完成
            </button>
        </div>
        
        <ul class="todo-list">
            <template x-for="todo in filteredTodos" :key="todo.id">
                <li class="todo-item" :class="{ completed: todo.completed }">
                    <input 
                        type="checkbox" 
                        class="todo-checkbox"
                        x-model="todo.completed"
                        @change="saveTodos()"
                    >
                    <span class="todo-text" x-text="todo.text"></span>
                    <button class="delete-btn" @click="deleteTodo(todo.id)">删除</button>
                </li>
            </template>
        </ul>
        
        <div class="empty-state" x-show="filteredTodos.length === 0">
            <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                <path d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
            </svg>
            <p x-show="filter === 'all'">暂无待办事项</p>
            <p x-show="filter === 'active'">没有未完成的事项</p>
            <p x-show="filter === 'completed'">没有已完成的事项</p>
        </div>
        
        <div class="footer" x-show="todos.length > 0">
            <span>
                <span x-text="remainingCount"></span> 项未完成
            </span>
            <button class="clear-btn" @click="clearCompleted()" x-show="completedCount > 0">
                清除已完成
            </button>
        </div>
    </div>

    <script>
        function todoApp() {
            return {
                newTodo: '',
                filter: 'all',
                todos: [],
                
                init() {
                    const saved = localStorage.getItem('alpine-todos')
                    if (saved) {
                        this.todos = JSON.parse(saved)
                    }
                },
                
                get filteredTodos() {
                    if (this.filter === 'active') {
                        return this.todos.filter(t => !t.completed)
                    }
                    if (this.filter === 'completed') {
                        return this.todos.filter(t => t.completed)
                    }
                    return this.todos
                },
                
                get remainingCount() {
                    return this.todos.filter(t => !t.completed).length
                },
                
                get completedCount() {
                    return this.todos.filter(t => t.completed).length
                },
                
                addTodo() {
                    const text = this.newTodo.trim()
                    if (text) {
                        this.todos.push({
                            id: Date.now(),
                            text: text,
                            completed: false
                        })
                        this.newTodo = ''
                        this.saveTodos()
                    }
                },
                
                deleteTodo(id) {
                    this.todos = this.todos.filter(t => t.id !== id)
                    this.saveTodos()
                },
                
                clearCompleted() {
                    this.todos = this.todos.filter(t => !t.completed)
                    this.saveTodos()
                },
                
                saveTodos() {
                    localStorage.setItem('alpine-todos', JSON.stringify(this.todos))
                }
            }
        }
    </script>
</body>
</html>

代码解析 #

1. 数据定义 (x-data) #

html
<div x-data="todoApp()">

x-data 定义组件的数据和方法:

javascript
function todoApp() {
    return {
        newTodo: '',           // 新待办事项输入
        filter: 'all',         // 当前过滤条件
        todos: [],             // 待办事项列表
        
        // 计算属性
        get filteredTodos() { /* ... */ },
        get remainingCount() { /* ... */ },
        get completedCount() { /* ... */ },
        
        // 方法
        init() { /* ... */ },
        addTodo() { /* ... */ },
        deleteTodo() { /* ... */ },
        clearCompleted() { /* ... */ },
        saveTodos() { /* ... */ }
    }
}

2. 初始化 (x-init) #

html
<div x-data="todoApp()" x-init="init()">

x-init 在组件初始化时执行,用于从 localStorage 加载数据:

javascript
init() {
    const saved = localStorage.getItem('alpine-todos')
    if (saved) {
        this.todos = JSON.parse(saved)
    }
}

3. 双向绑定 (x-model) #

html
<input type="text" x-model="newTodo" placeholder="添加新的待办事项...">

x-model 实现表单元素与数据的双向绑定。

4. 事件处理 (x-on / @) #

html
<button @click="addTodo()">添加</button>
<input @keyup.enter="addTodo()">

@clickx-on:click 的简写,@keyup.enter 是修饰符用法。

5. 条件渲染 (x-show) #

html
<div x-show="filteredTodos.length === 0">
<div x-show="filter === 'all'">

x-show 根据条件显示/隐藏元素。

6. 列表渲染 (x-for) #

html
<template x-for="todo in filteredTodos" :key="todo.id">
    <li>...</li>
</template>

x-for 循环渲染列表,需要使用 <template> 标签。

7. 动态属性 (x-bind / :) #

html
<li :class="{ completed: todo.completed }">

:x-bind: 的简写,动态绑定属性值。

8. 文本绑定 (x-text) #

html
<span x-text="todo.text"></span>

x-text 设置元素的文本内容。

功能详解 #

添加待办事项 #

javascript
addTodo() {
    const text = this.newTodo.trim()
    if (text) {
        this.todos.push({
            id: Date.now(),      // 唯一ID
            text: text,          // 待办内容
            completed: false     // 完成状态
        })
        this.newTodo = ''        // 清空输入
        this.saveTodos()         // 保存到本地
    }
}

过滤功能 #

javascript
get filteredTodos() {
    if (this.filter === 'active') {
        return this.todos.filter(t => !t.completed)
    }
    if (this.filter === 'completed') {
        return this.todos.filter(t => t.completed)
    }
    return this.todos
}

本地存储 #

javascript
saveTodos() {
    localStorage.setItem('alpine-todos', JSON.stringify(this.todos))
}

扩展练习 #

尝试添加以下功能:

  1. 编辑功能:双击待办事项可以编辑内容
  2. 优先级:为待办事项添加优先级标记
  3. 截止日期:添加截止日期字段
  4. 分类标签:支持按标签分类

小结 #

通过这个项目,我们学习了:

  • x-data 定义组件数据
  • x-init 初始化组件
  • x-model 双向数据绑定
  • x-on / @ 事件处理
  • x-show 条件显示
  • x-for 列表渲染
  • x-bind / : 动态属性
  • x-text 文本绑定

下一章,我们将深入理解 Alpine.js 的核心概念。

最后更新:2026-03-28