前端动手做项目完整教程(待办清单、天气查询、简易商城)
学编程最容易犯的错:学了很多语法,却做不出东西。这一篇我们用前面学的所有知识,从零做三个完整项目。每个项目都给出全部代码,复制就能跑。动手敲一遍,比看十遍教程都有用。
项目一:待办清单 App(Todo List)
功能: 添加任务、勾选完成、删除任务、数据刷新后不丢失。
技术点: DOM 操作、事件监听、数组操作、localStorage 本地存储。
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>待办清单</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, "PingFang SC", sans-serif; background: #f0f2f5; display: flex; justify-content: center; padding-top: 60px; } .app { width: 360px; background: white; border-radius: 16px; padding: 24px; box-shadow: 0 8px 24px rgba(0,0,0,0.1); } h1 { font-size: 24px; margin-bottom: 16px; } .input-row { display: flex; gap: 8px; margin-bottom: 16px; } input { flex: 1; padding: 10px; border: 1px solid #ddd; border-radius: 8px; font-size: 14px; } button { padding: 10px 16px; border: none; border-radius: 8px; background: #4a90d9; color: white; font-size: 14px; cursor: pointer; } button:hover { background: #357abd; } ul { list-style: none; } li { display: flex; align-items: center; gap: 8px; padding: 10px 0; border-bottom: 1px solid #eee; } li input[type="checkbox"] { width: 18px; height: 18px; } .text { flex: 1; font-size: 15px; } .done .text { text-decoration: line-through; color: #aaa; } .del { background: #e74c3c; padding: 6px 10px; font-size: 13px; } .del:hover { background: #c0392b; } </style></head><body> <div class="app"> <h1>📝 待办清单</h1> <div class="input-row"> <input id="todoInput" placeholder="输入新任务,按回车添加"> <button id="addBtn">添加</button> </div> <ul id="list"></ul> </div>
<script> // 1. 数据:从 localStorage 读,没有就给空数组 // localStorage 可以把数据存到浏览器里,刷新不丢 let todos = JSON.parse(localStorage.getItem("todos") || "[]");
const input = document.getElementById("todoInput"); const list = document.getElementById("list");
// 2. 保存:每次数据变化都存到 localStorage function save() { localStorage.setItem("todos", JSON.stringify(todos)); }
// 3. 渲染:把 todos 数组画到页面上 function render() { list.innerHTML = ""; // 先清空,再重新画
todos.forEach((todo, index) => { let li = document.createElement("li"); if (todo.done) li.className = "done";
// 勾选框 let checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.checked = todo.done; checkbox.addEventListener("change", () => { todos[index].done = checkbox.checked; // 切换完成状态 save(); render(); });
// 文字 let span = document.createElement("span"); span.className = "text"; span.textContent = todo.text;
// 删除按钮 let delBtn = document.createElement("button"); delBtn.className = "del"; delBtn.textContent = "删除"; delBtn.addEventListener("click", () => { todos.splice(index, 1); // 从数组删除这一项 save(); render(); });
li.appendChild(checkbox); li.appendChild(span); li.appendChild(delBtn); list.appendChild(li); }); }
// 4. 添加任务 function addTodo() { let text = input.value.trim(); if (!text) return; // 空内容不添加 todos.push({ text: text, done: false }); input.value = ""; save(); render(); }
// 5. 绑定事件:点击按钮、按回车都触发添加 document.getElementById("addBtn").addEventListener("click", addTodo); input.addEventListener("keydown", (e) => { if (e.key === "Enter") addTodo(); });
// 6. 首次渲染 render(); </script></body></html>核心流程回顾: 数据(todos 数组)→ 渲染(render 画页面)→ 交互(改数组)→ 保存(localStorage)→ 重新渲染。改数据 → 自动重画,这就是框架里”数据驱动”思想的原生版。
升级练习: 试着加上”剩余未完成数量”、“一键清空已完成”。
项目二:天气查询页(真实 API)
功能: 输入城市名 → 查真实天气 → 显示温度风速,带加载状态和错误处理。
技术点: fetch、async/await、两次请求串联、错误处理、模板字符串渲染。
(代码和上一篇 fetch 教程里的天气页一样,这里做一个升级版:支持城市下拉快捷键、显示多个信息点。)
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>天气查询</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, "PingFang SC", sans-serif; background: linear-gradient(135deg, #667eea, #764ba2); min-height: 100vh; display: flex; justify-content: center; align-items: center; } .card { width: 380px; background: white; border-radius: 20px; padding: 32px; box-shadow: 0 16px 40px rgba(0,0,0,0.2); text-align: center; } h1 { font-size: 26px; margin-bottom: 20px; } .input-row { display: flex; gap: 8px; margin-bottom: 24px; } input { flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 10px; font-size: 15px; } button { padding: 12px 20px; border: none; border-radius: 10px; background: #667eea; color: white; font-size: 15px; cursor: pointer; } button:hover { background: #5566d8; } .city { font-size: 24px; font-weight: 700; margin-bottom: 8px; } .temp { font-size: 56px; font-weight: 700; color: #667eea; margin: 12px 0; } .detail { display: flex; justify-content: space-around; color: #888; font-size: 14px; } .loading { color: #999; } .error { color: #e74c3c; } </style></head><body> <div class="card"> <h1>🌤 天气查询</h1> <div class="input-row"> <input id="city" placeholder="城市英文名,如 beijing、shanghai"> <button id="search">查询</button> </div> <div id="result"> <p class="loading">输入城市开始查询</p> </div> </div>
<script> const input = document.getElementById("city"); const btn = document.getElementById("search"); const result = document.getElementById("result");
btn.addEventListener("click", query); input.addEventListener("keydown", (e) => { if (e.key === "Enter") query(); });
async function query() { let city = input.value.trim(); if (!city) return;
result.innerHTML = '<p class="loading">查询中...</p>';
try { // 第一步:城市名 → 经纬度 let geoRes = await fetch( `https://geocoding-api.open-meteo.com/v1/search?name=${city}&count=1` ); let geoData = await geoRes.json(); let place = geoData.results && geoData.results[0];
if (!place) { throw new Error("找不到这个城市,试试英文名"); }
// 第二步:经纬度 → 天气 let wRes = await fetch( `https://api.open-meteo.com/v1/forecast?latitude=${place.latitude}&longitude=${place.longitude}¤t_weather=true` ); let weather = await wRes.json();
// 第三步:渲染结果 let cur = weather.current_weather; result.innerHTML = ` <div class="city">📍 ${place.name}${place.country ? "," + place.country : ""}</div> <div class="temp">${cur.temperature}°C</div> <div class="detail"> <span>💨 风速 ${cur.windspeed} km/h</span> <span>🧭 风向 ${cur.winddirection}°</span> </div> <p style="color:#aaa;font-size:12px;margin-top:12px"> 更新时间:${new Date(cur.time).toLocaleString()} </p> `; } catch (error) { result.innerHTML = `<p class="error">查询失败:${error.message}</p>`; } } </script></body></html>为什么这个项目重要? 它完整覆盖了真实前端开发的日常:请求数据、等待、成功渲染、失败提示、更新 UI。把这两步请求(先查坐标再查天气)理解透,以后接任何后端 API 都是一个套路。
项目三:简易商城(商品列表 + 购物车)
功能: 展示商品列表、加入购物车、数量增减、计算总价。
技术点: 数组渲染、事件委托、对象数据、reduce 求和。
<!DOCTYPE html><html lang="zh-CN"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>简易商城</title> <style> * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: system-ui, "PingFang SC", sans-serif; background: #f0f2f5; padding: 40px 20px; } .wrap { max-width: 720px; margin: 0 auto; } h1 { margin-bottom: 24px; }
.goods-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } .card { background: white; border-radius: 14px; padding: 16px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); display: flex; flex-direction: column; gap: 8px; } .card .emoji { font-size: 40px; text-align: center; } .card .name { font-weight: 700; text-align: center; } .card .price { color: #e74c3c; font-weight: 700; text-align: center; } .card button { padding: 8px; border: none; border-radius: 8px; background: #4a90d9; color: white; cursor: pointer; } .card button:hover { background: #357abd; }
.cart { margin-top: 24px; background: white; border-radius: 14px; padding: 20px; box-shadow: 0 4px 12px rgba(0,0,0,0.06); } .cart h2 { font-size: 18px; margin-bottom: 12px; } .cart-item { display: flex; align-items: center; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; } .qty-btn { width: 26px; height: 26px; border: 1px solid #ddd; border-radius: 6px; background: white; cursor: pointer; } .total { margin-top: 12px; font-size: 18px; font-weight: 700; text-align: right; } .total span { color: #e74c3c; } </style></head><body> <div class="wrap"> <h1>🛒 简易商城</h1>
<!-- 商品列表 --> <div class="goods-grid" id="goods"></div>
<!-- 购物车 --> <div class="cart"> <h2>购物车</h2> <div id="cartList"></div> <div class="total">合计:<span id="total">¥0</span></div> </div> </div>
<script> // 1. 商品数据 const goods = [ { id: 1, name: "苹果", emoji: "🍎", price: 5 }, { id: 2, name: "香蕉", emoji: "🍌", price: 3 }, { id: 3, name: "橘子", emoji: "🍊", price: 4 }, { id: 4, name: "西瓜", emoji: "🍉", price: 10 }, { id: 5, name: "葡萄", emoji: "🍇", price: 8 }, { id: 6, name: "草莓", emoji: "🍓", price: 15 } ];
// 2. 购物车数据:{ 商品id: 数量 } const cart = {};
// 3. 渲染商品列表 function renderGoods() { let goodsEl = document.getElementById("goods"); goodsEl.innerHTML = "";
for (let item of goods) { let card = document.createElement("div"); card.className = "card"; card.innerHTML = ` <div class="emoji">${item.emoji}</div> <div class="name">${item.name}</div> <div class="price">¥${item.price}</div> `;
let btn = document.createElement("button"); btn.textContent = "加入购物车"; btn.addEventListener("click", () => { cart[item.id] = (cart[item.id] || 0) + 1; // 数量加 1 renderCart(); });
card.appendChild(btn); goodsEl.appendChild(card); } }
// 4. 渲染购物车 function renderCart() { let cartEl = document.getElementById("cartList"); cartEl.innerHTML = ""; let total = 0;
for (let id in cart) { let qty = cart[id]; // 数量 if (qty === 0) continue; let item = goods.find(g => g.id === Number(id)); // 找到商品信息 let subtotal = item.price * qty; // 小计 total += subtotal;
let row = document.createElement("div"); row.className = "cart-item"; row.innerHTML = ` <span>${item.emoji} ${item.name}</span> <span> <button class="qty-btn" data-id="${item.id}" data-op="minus">-</button> <span style="margin:0 8px">${qty}</span> <button class="qty-btn" data-id="${item.id}" data-op="plus">+</button> </span> <span>¥${subtotal}</span> `; cartEl.appendChild(row); }
// 空购物车提示 if (cartEl.innerHTML === "") { cartEl.innerHTML = '<p style="color:#aaa">购物车是空的</p>'; }
document.getElementById("total").textContent = "¥" + total; }
// 5. 事件委托:购物车里所有 +/- 按钮,统一由 cartList 监听 document.getElementById("cartList").addEventListener("click", (e) => { let btn = e.target.closest(".qty-btn"); if (!btn) return;
let id = Number(btn.dataset.id); let op = btn.dataset.op;
if (op === "plus") { cart[id] = (cart[id] || 0) + 1; } else { cart[id] = (cart[id] || 0) - 1; if (cart[id] <= 0) delete cart[id]; // 数量归零就移除 } renderCart(); });
// 6. 初始化 renderGoods(); renderCart(); </script></body></html>这个项目的精华:
- 数据结构:商品数组 + 购物车对象(id 对应数量),真实项目也是这个思路
- 渲染函数:改数据 → 调 renderCart() → 页面更新
- 事件委托:给父元素绑一个监听,子元素的按钮都能触发,代码简洁
- reduce 求和:虽然这里用了循环,你也可以试试改成
reduce写法
升级练习: 把购物车数据也存进 localStorage(刷新不丢)、加”结算”按钮弹出总价。
做项目的三个心法
- 先抄后改:拿到示例代码,先原样敲一遍跑起来,再改一个地方看效果。理解每一行是干嘛的
- 一次只加一个功能:先让它能跑,再加功能,坏了也知道是哪一步改的
- 完成比完美重要:第一个项目丑没关系,跑起来就是胜利。做出 3 个,你就不再是”零基础”了
总结
这一篇我们从零完成了三个项目:
- 待办清单:数组数据 + 渲染 + localStorage 持久化
- 天气查询:fetch 两次请求串联 + 加载/错误状态
- 简易商城:商品列表 + 购物车 + 总价计算 + 事件委托
这三个项目覆盖了前端开发的核心套路:数据怎么存、怎么渲染、用户操作怎么处理、数据怎么持久化。把它们吃透,你已经具备独立开发简单网页应用的能力了。最后一篇,我们学 TypeScript——给 JavaScript 加上”类型检查”,让你写的代码更可靠。