第一个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()">
@click 是 x-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))
}
扩展练习 #
尝试添加以下功能:
- 编辑功能:双击待办事项可以编辑内容
- 优先级:为待办事项添加优先级标记
- 截止日期:添加截止日期字段
- 分类标签:支持按标签分类
小结 #
通过这个项目,我们学习了:
x-data定义组件数据x-init初始化组件x-model双向数据绑定x-on/@事件处理x-show条件显示x-for列表渲染x-bind/:动态属性x-text文本绑定
下一章,我们将深入理解 Alpine.js 的核心概念。
最后更新:2026-03-28