前端完整示例
前端完整示例
HTML、CSS、JS 各学了一篇, 这篇拿一个真项目把三样串起来: 一个待办事项 (Todo) 应用, 支持添加、勾选完成、删除, 刷新页面数据不丢。麻雀虽小, 前端开发的完整套路 (结构、样式、逻辑、存储) 全在里面。写完这个项目, 我才算真的理解三件套是怎么配合的。
一共三个文件, 放同一个文件夹里, 双击 index.html 就能跑, 不需要装任何东西 (这也是纯三件套时代的美好, 工具链篇会讲为什么后来还是需要装一堆东西):
代码块收起展开
todo-app/
├── index.html 结构
├── style.css 样式
└── script.js 逻辑1. HTML: 搭结构
代码块收起展开
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Todo 待办事项</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="container">
<h1>我的待办</h1>
<!-- 输入区: form 包起来是为了回车也能提交 -->
<form id="todoForm">
<input
type="text"
id="todoInput"
placeholder="添加新的待办事项..."
required
>
<button type="submit">添加</button>
</form>
<!-- 列表区: 现在是空的, 内容全靠 JS 动态生成 -->
<ul id="todoList"></ul>
<!-- 统计区: 数字也是 JS 填的 -->
<div class="stats">
<span>总计: <strong id="totalCount">0</strong></span>
<span>已完成: <strong id="doneCount">0</strong></span>
</div>
</div>
<script src="script.js"></script>
</body>
</html>注意这个 HTML 的特点: 它只有”容器”, 没有内容。<ul> 是空的, 待办列表长什么样完全由 JS 根据数据决定。这和 HTML 篇里写死内容的静态页面已经是两种思路了。required 是浏览器自带的校验, 空内容按提交按钮会被拦下。
2. CSS: 上样式
代码块收起展开
/* 全局重置: 清掉浏览器默认边距, 统一盒模型 (CSS 篇讲过 box-sizing 的坑) */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 整页: 渐变背景, 用 Flex 把卡片钉在正中间 */
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
/* 白色卡片容器 */
.container {
background: white;
border-radius: 12px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
padding: 30px;
width: 100%;
max-width: 500px;
}
h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
/* 输入行: 又是 Flex, 输入框 flex:1 吃掉剩余宽度 */
#todoForm {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
#todoInput {
flex: 1;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: border-color 0.3s;
}
#todoInput:focus {
outline: none;
border-color: #667eea;
}
button[type="submit"] {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
}
button[type="submit"]:hover {
background: #5568d3;
}
#todoList {
list-style: none; /* 去掉 li 前面的小圆点 */
margin-bottom: 20px;
}
/* 每一条待办: Flex 横排, 复选框 + 文本 + 删除按钮 */
.todo-item {
display: flex;
align-items: center;
gap: 12px;
padding: 12px;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 10px;
transition: transform 0.2s;
}
.todo-item:hover {
transform: translateX(4px);
}
/* 完成状态的样式: JS 只负责加/删 completed 这个 class, 样式效果归 CSS 管 */
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-checkbox {
width: 20px;
height: 20px;
cursor: pointer;
}
.todo-text {
flex: 1;
color: #333;
font-size: 16px;
}
.delete-btn {
padding: 6px 12px;
background: #ff4757;
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.3s;
}
.delete-btn:hover {
background: #ee3744;
}
.stats {
display: flex;
justify-content: space-around;
padding-top: 20px;
border-top: 2px solid #e0e0e0;
color: #666;
font-size: 14px;
}
.stats strong {
color: #667eea;
font-size: 18px;
}
.empty-state {
text-align: center;
color: #999;
padding: 40px 20px;
font-size: 16px;
}
/* 窄屏适配: 手机上输入框和按钮改成上下排 */
@media (max-width: 600px) {
.container {
padding: 20px;
}
h1 {
font-size: 24px;
}
#todoForm {
flex-direction: column;
}
button[type="submit"] {
width: 100%;
}
}CSS 里最值得咂摸的一处是 .todo-item.completed .todo-text: JS 从头到尾没碰过删除线和灰色, 它只负责给元素加减 completed 这个 class, 视觉效果全写在 CSS 里。逻辑和样式各管各的, 这是三件套配合的标准分工。
3. JavaScript: 写逻辑
代码块收起展开
// 先把要操作的 DOM 元素都抓到手
const todoForm = document.getElementById('todoForm');
const todoInput = document.getElementById('todoInput');
const todoList = document.getElementById('todoList');
const totalCount = document.getElementById('totalCount');
const doneCount = document.getElementById('doneCount');
// 核心数据: 一个数组, 整个应用唯一的"真相"
// 启动时先从 localStorage 恢复, 没存过就用空数组 (|| 的兜底用法)
let todos = JSON.parse(localStorage.getItem('todos')) || [];
// 一进来先把已有数据画到页面上
renderTodos();
// 表单提交 = 添加待办 (preventDefault 拦住浏览器默认的刷新式提交, JS 篇讲过)
todoForm.addEventListener('submit', (e) => {
e.preventDefault();
addTodo();
});
function addTodo() {
const text = todoInput.value.trim();
if (text === '') return;
const todo = {
id: Date.now(), // 用时间戳当唯一 id, 小项目够用
text: text,
completed: false
};
todos.push(todo); // 改数据
saveTodos(); // 存起来
renderTodos(); // 重新画页面
todoInput.value = ''; // 清空输入框
}
// 勾选/取消勾选: map 出一个新数组, 只改中招的那一项 (展开运算符复制其余字段)
function toggleTodo(id) {
todos = todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed }
: todo
);
saveTodos();
renderTodos();
}
// 删除: filter 留下 id 不相等的
function deleteTodo(id) {
todos = todos.filter(todo => todo.id !== id);
saveTodos();
renderTodos();
}
// 渲染: 把 todos 数组原样画成 DOM, 每次都整个重画
function renderTodos() {
todoList.innerHTML = '';
if (todos.length === 0) {
todoList.innerHTML = '<li class="empty-state">还没有待办事项, 添加一个吧!</li>';
updateStats();
return;
}
todos.forEach(todo => {
const li = document.createElement('li');
li.className = `todo-item ${todo.completed ? 'completed' : ''}`;
li.innerHTML = `
<input
type="checkbox"
class="todo-checkbox"
${todo.completed ? 'checked' : ''}
>
<span class="todo-text">${todo.text}</span>
<button class="delete-btn">删除</button>
`;
// 给刚造出来的节点绑事件
li.querySelector('.todo-checkbox')
.addEventListener('change', () => toggleTodo(todo.id));
li.querySelector('.delete-btn')
.addEventListener('click', () => deleteTodo(todo.id));
todoList.appendChild(li);
});
updateStats();
}
function updateStats() {
totalCount.textContent = todos.length;
doneCount.textContent = todos.filter(todo => todo.completed).length;
}
// 持久化: 对象数组序列化成 JSON 字符串存进 localStorage
function saveTodos() {
localStorage.setItem('todos', JSON.stringify(todos));
}4. 这份代码最重要的思想: 数据驱动
注意所有操作函数 (add/toggle/delete) 的形状完全一样:
代码块收起展开
改 todos 数组 → saveTodos() 存盘 → renderTodos() 重画页面上显示什么, 从来由 todos 这个数组说了算, 我从不直接去 DOM 上”改某一行的文字”。想知道应用当前什么状态? 看数组就行。这个”数据是唯一真相, 页面只是数据的投影”的思路, 就是数据驱动视图, 是理解 Vue 的钥匙。
同时也该看到它的笨拙: 每次哪怕只勾选一项, renderTodos 都把整个列表推倒重画, 还要重新绑一遍事件。
列表一大, 又慢又费劲; 而且”改完数据记得调 renderTodos”全靠自觉, 忘一处页面就和数据脱节。
这些体力活, Vue 会全部接管: 我只改数据, 它负责最小代价地更新页面。带着这个痛点去学 Vue, 会有”啊原来是治这个病的”的感觉。
顺带一提 innerHTML 拼模板字符串这个写法: 方便, 但如果 todo.text 里混进 <script> 这类内容会被当 HTML 解析 (JS 篇提过注入风险)。这里是本地小玩具无所谓, 真项目的用户输入不能这么直接拼。
5. 动手扩展
按顺序做, 每个都用”改数据 → 重渲染”的套路:
- 加一个”清空已完成”按钮 (filter 一把梭)。
- 加”全部 / 未完成 / 已完成”三档筛选 (渲染前先过滤数组)。
- 双击文本进入编辑 (给 todo 对象加个 editing 字段试试)。
- 加优先级字段并按优先级排序显示。
做到第二个就会发现筛选状态本身也是数据, 也得存变量。状态越攒越多, 就越能体会框架的必要性。
6. 出问题怎么查
老三样: console.log(todos) 看数据对不对; Elements 面板看 DOM 生成了没; Application 面板看 localStorage 里存的字符串。系统排查方法在调试篇专门写了, 这里不展开。
7. 下一步
到这里, 纯三件套的路已经走完了。接下来的路线: 调试篇把 F12 用熟, 然后进工具链篇理解 npm 和模块化 (从”双击就能跑”进化到工程化开发), 最后开 Vue 主线, 亲眼看这份 Todo 用 Vue 重写后能省多少代码。