2343 字
12 分钟
前端动手做项目完整教程(待办清单、天气查询、简易商城)

前端动手做项目完整教程(待办清单、天气查询、简易商城)#

学编程最容易犯的错:学了很多语法,却做不出东西。这一篇我们用前面学的所有知识,从零做三个完整项目。每个项目都给出全部代码,复制就能跑。动手敲一遍,比看十遍教程都有用。


项目一:待办清单 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}&current_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>

这个项目的精华:

  1. 数据结构:商品数组 + 购物车对象(id 对应数量),真实项目也是这个思路
  2. 渲染函数:改数据 → 调 renderCart() → 页面更新
  3. 事件委托:给父元素绑一个监听,子元素的按钮都能触发,代码简洁
  4. reduce 求和:虽然这里用了循环,你也可以试试改成 reduce 写法

升级练习: 把购物车数据也存进 localStorage(刷新不丢)、加”结算”按钮弹出总价。


做项目的三个心法#

  1. 先抄后改:拿到示例代码,先原样敲一遍跑起来,再改一个地方看效果。理解每一行是干嘛的
  2. 一次只加一个功能:先让它能跑,再加功能,坏了也知道是哪一步改的
  3. 完成比完美重要:第一个项目丑没关系,跑起来就是胜利。做出 3 个,你就不再是”零基础”了

总结#

这一篇我们从零完成了三个项目:

  • 待办清单:数组数据 + 渲染 + localStorage 持久化
  • 天气查询:fetch 两次请求串联 + 加载/错误状态
  • 简易商城:商品列表 + 购物车 + 总价计算 + 事件委托

这三个项目覆盖了前端开发的核心套路:数据怎么存、怎么渲染、用户操作怎么处理、数据怎么持久化。把它们吃透,你已经具备独立开发简单网页应用的能力了。最后一篇,我们学 TypeScript——给 JavaScript 加上”类型检查”,让你写的代码更可靠。

前端动手做项目完整教程(待办清单、天气查询、简易商城)
https://021028.xyz/posts/default/72/
作者
021028
发布于
2026-08-11
许可协议
CC BY-NC-SA 4.0