看到这个标题,很多读者可能会好奇:文章是打算晒三只小动物的日常吗?其实不是。这是我在整理前端练习素材时冒出来的一个灵感——与其拿千篇一律的“Todo List”或“登录表单”练手,不如做一个充满童趣的宠物展示小网站,让三只性格迥异的“小家伙”作为主角,串起 HTML、CSS、JavaScript 的核心知识点。
这篇实战教程会从一个纯静态页面开始,逐步实现宠物卡片展示、互动按钮(投喂、抚摸、玩耍)、心情与精力状态管理、本地存储、夜间模式切换等常见功能。整个过程不依赖任何框架,不需要安装 Node.js,不涉及后端服务,只要有一台电脑、一个浏览器和任意一个文本编辑器就能完成。读完并动手实践后,你将掌握:
- HTML 语义化标签的合理组织方式;
- CSS Flex / Grid 布局、动画和主题切换的写法;
- JavaScript 对象数组、事件监听、DOM 渲染和 localStorage 持久化;
- 一个完整小项目从设计到运行的思路闭环。
如果你刚学完前端基础,正愁没有合适的项目练手,或者想快速做一个可爱风格的展示页面送给朋友,这篇文章可以直接照着抄。
1. 项目背景与功能规划
1.1 这个项目解决什么问题
很多初学前端的人会遇到一个尴尬阶段:语法都看懂了,但一打开编辑器不知道写什么。做计算器太枯燥,做后台管理模板又涉及太多框架知识。而“宠物展示页”这类项目刚好处于“简单但不简陋”的区间——它足够轻量,能让初学者把注意力集中在三件核心事情上:
- 用语义化 HTML 搭建页面骨架;
- 用 CSS 控制视觉表现和布局;
- 用 JavaScript 让页面“动”起来。
同时,“三只小宠物”的设定天然具备叙事性,写代码的过程更像是给三只虚拟角色做成长记录页。看着页面逐渐成型、按钮产生响应、状态被保存,这种成就感是单纯的语法练习无法带来的。
1.2 功能模块拆解
在动手写代码之前,先明确页面需要具备哪些能力。我不建议一上来就写,先花两分钟做拆解,后面代码组织会清晰很多。
| 模块 | 功能说明 | 涉及技术点 |
|---|---|---|
| 宠物信息展示 | 展示三只宠物的头像、名字、年龄、性格标签和简介 | HTML 结构、CSS 卡片布局 |
| 状态面板 | 每只宠物拥有心情值、精力值两个数值属性 | JavaScript 对象、动态渲染 |
| 互动按钮 | 投喂、抚摸、玩耍三种操作,影响数值变化 | 事件监听、函数封装 |
| 数据持久化 | 刷新页面后状态不被重置 | localStorage |
| 主题切换 | 白天/夜间模式一键切换 | CSS 变量、class 切换 |
| 响应式布局 | 在手机和电脑上都能正常浏览 | Flex / Grid 媒体查询 |
1.3 三只“小家伙”的人设
为了让项目有辨识度,我给三只宠物设定了简单的人设。你在复制代码时可以随意改名、换性格标签。
- 橘子:一只喜欢晒太阳的小橘猫,性格慵懒,最爱小鱼干,心情值掉得最慢;
- 布丁:一只精力旺盛的小柯基,热爱奔跑,玩耍时精力消耗最快;
- 棉花糖:一只毛茸茸的小白兔,胆子小,被抚摸时心情值涨得最快。
这三个人设不仅让页面有温度,还会影响后续 JavaScript 参数设计。比如每只宠物的数值增减幅度可以不同,这正好引出“配置数据驱动渲染”的思想。
2. 环境准备与项目结构
2.1 开发环境说明
本项目介绍的是纯前端静态页面,对运行环境的要求非常低。你可以选择任意一种方式创建项目:
- 使用 VS Code 等编辑器新建文件夹,手动创建
index.html、style.css、script.js三个文件; - 直接复制下面的完整代码,在本地保存后双击
index.html用浏览器打开; - 如果你更习惯在线编辑,也可以使用 CodePen、JSFiddle 等在线工具直接粘贴运行。
版本方面不需要过度纠结。示例代码使用的是 ES6 语法(const、let、模板字符串、箭头函数),只要浏览器版本不是特别老旧即可。建议使用 Chrome、Edge 或 Firefox 最新版测试。
2.2 项目目录结构
推荐创建如下目录:
cute-three-pets/ ├── index.html ├── style.css └── script.js其中:
index.html负责页面结构,是项目的入口;style.css负责全部样式,包括布局、颜色、动画和响应式规则;script.js负责宠物数据、互动逻辑、存储读写和主题切换。
把结构、样式、行为分离是前端开发的基本规范。即使以后项目变大,这种分层也会让你的代码更容易维护。
3. 核心设计思路拆解
3.1 用 JavaScript 对象描述宠物数据
整个项目的核心是“宠物数据”。如果直接把三只宠物写死在 HTML 里,那么互动逻辑和渲染逻辑都会变得很僵硬。更合理的做法是:先用 JavaScript 对象数组保存宠物信息,然后动态生成卡片。
const pets = [ { id: 'cat', name: '橘子', avatar: '🐱', type: '小橘猫', age: '2岁', desc: '喜欢晒太阳,最爱小鱼干。', tags: ['慵懒', '黏人'], mood: 80, energy: 70, moodChange: { feed: 5, stroke: 8, play: -10 }, energyChange: { feed: 6, stroke: 2, play: -15 } }, { id: 'dog', name: '布丁', avatar: '🐶', type: '小柯基', age: '1岁半', desc: '精力旺盛,每天都要出门跑三圈。', tags: ['活泼', '贪玩'], mood: 70, energy: 90, moodChange: { feed: 4, stroke: 5, play: 12 }, energyChange: { feed: 5, stroke: 1, play: -20 } }, { id: 'rabbit', name: '棉花糖', avatar: '🐰', type: '小白兔', age: '8个月', desc: '胆子很小,但熟悉之后特别粘人。', tags: ['温柔', '胆小'], mood: 60, energy: 55, moodChange: { feed: 5, stroke: 12, play: -6 }, energyChange: { feed: 4, stroke: 3, play: -8 } } ];这段代码中:
mood和energy是每只宠物的当前状态值,范围建议控制在 0 到 100;moodChange和energyChange是三种互动操作对数值的影响幅度;feed表示投喂,stroke表示抚摸,play表示玩耍。
把参数设计成这样,后续新增一只宠物只需要往数组里追加一个对象,不需要修改渲染逻辑,这其实就是“数据驱动视图”的雏形。
3.2 渲染函数:让数据和 UI 保持同步
修改数据之后必须让页面重新更新,这个操作我们称之为“渲染”。常规做法是写一个renderCards函数,循环遍历pets数组,生成对应的卡片 HTML 字符串,然后插入到页面容器中。
function renderCards() { const container = document.getElementById('pet-list'); container.innerHTML = pets.map(pet => { return ` <article class="pet-card">document.getElementById('pet-list').addEventListener('click', function (e) { const btn = e.target.closest('.btn'); if (!btn) return; const card = btn.closest('.pet-card'); const petId = card.dataset.id; const action = btn.dataset.action; handleAction(petId, action); });e.target.closest('.btn')会向上查找最近的.btn元素。如果点击位置不在按钮上,closest返回null,这时直接return即可。这种写法在列表项很多、需要频繁增删元素的场景中非常实用。
3.4 数值变化与边界处理
handleAction函数负责更新宠物数据。这里要注意一个问题:心情值和精力值不能无限上涨或下跌,应该被限制在 0 到 100 之间。
function handleAction(petId, action) { const pet = pets.find(p => p.id === petId); if (!pet) return; pet.mood += pet.moodChange[action]; pet.energy += pet.energyChange[action]; pet.mood = Math.max(0, Math.min(100, pet.mood)); pet.energy = Math.max(0, Math.min(100, pet.energy)); saveData(); renderCards(); }Math.max(0, Math.min(100, value))是一种非常常见的数值裁剪写法:先限制上限 100,再限制下限 0。如果你不这样做,投喂多次后数值可能变成 120,精力也有可能变成负数。
saveData()负责把宠物数组写入 localStorage。这里需要先把 JavaScript 对象转换成 JSON 字符串,因为 localStorage 只能存储字符串。
function saveData() { localStorage.setItem('cute-three-pets', JSON.stringify(pets)); } function loadData() { const saved = localStorage.getItem('cute-three-pets'); if (saved) { const parsed = JSON.parse(saved); if (Array.isArray(parsed) && parsed.length) { parsed.forEach((item, index) => { pets[index].mood = item.mood; pets[index].energy = item.energy; }); } } }loadData中做了一层防御性判断,因为 localStorage 里的内容可能被用户手动修改或删除,不校验就直接使用,可能让页面崩溃。这个习惯在真实项目中很重要。
4. 完整实战案例:三只宠物的互动小站
4.1 创建项目结构
第一步,新建一个文件夹,命名为cute-three-pets。在文件夹内创建三个文件:
index.htmlstyle.cssscript.js
如果你使用的是 VS Code,可以直接在编辑器里创建这些文件,然后安装一个名为Live Server的插件,右键index.html选择“Open with Live Server”,就能在浏览器中实时预览效果。如果没有插件,直接双击index.html也能运行,只是修改代码后需要手动刷新浏览器。
4.2 编写 HTML 骨架
index.html是所有内容的载体。页面整体分为三块区域:顶部导航、宠物卡片主舞台、底部页脚。我把首页展示区与相册趣味区都放在了主容器内,让页面内容更丰富。
<!-- 文件路径:index.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>可爱的三小只~</title> <link rel="stylesheet" href="style.css"> </head> <body> <header class="site-header"> <h1>🐾 可爱的三小只~</h1> <p>这是一座由 HTML、CSS 和 JavaScript 搭建的宠物小站</p> <button id="theme-toggle" class="theme-btn">🌙 夜间模式</button> </header> <main class="container"> <section class="intro"> <h2>欢迎来到萌宠之家</h2> <p>点击每只宠物卡片上的按钮,投喂、抚摸或陪它玩耍,看看它们的情绪和精力会发生什么变化。所有状态都会保存在你的浏览器中,刷新页面也不会消失。</p> </section> <section id="pet-list" class="pet-list"> <!-- 宠物卡片由 JavaScript 动态生成 --> </section> <section class="gallery"> <h2>生活时光</h2> <div class="gallery-grid"> <div class="gallery-item">🌞 午后晒太阳</div> <div class="gallery-item">🏃 草地狂奔</div> <div class="gallery-item">🥕 偷吃胡萝卜</div> </div> </section> </main> <footer class="site-footer"> <p>Made with ❤️ by HTML / CSS / JavaScript</p> </footer> <script src="script.js"></script> </body> </html>这段 HTML 中有几个值得注意的细节:
<meta name="viewport" ...>是移动端适配的基础配置,没有它,手机浏览器会自动缩放页面;<script>标签放在<body>末尾,是为了保证脚本执行时,页面中的 DOM 元素已经全部加载完成;<section id="pet-list" class="pet-list">是一个空容器,里面的卡片内容将由 JavaScript 动态插入。
4.3 编写 CSS 样式
CSS 是整个页面“可爱感”的来源。我会通过 CSS 变量定义配色,方便后续切换夜间模式。核心样式包括:
- 卡片布局使用 Grid,在宽屏下三列排列,窄屏下自动降为单列;
- 状态条采用两层嵌套结构,外层是灰色背景,内层宽度由 JavaScript 控制;
- 按钮使用圆角与 hover 效果;
- 夜间模式通过给
<body>添加night类实现。
/* 文件路径:style.css */ :root { --bg-color: #fff8f0; --text-color: #4a3f35; --card-bg: #ffffff; --card-shadow: rgba(255, 180, 130, 0.25); --primary: #ff9a76; --primary-light: #ffd4c2; --bar-bg: #f0e6dd; --tag-bg: #fff0e8; } body.night { --bg-color: #2b2b36; --text-color: #ece5dd; --card-bg: #3a3a48; --card-shadow: rgba(0, 0, 0, 0.3); --bar-bg: #55556a; --tag-bg: #494958; } * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif; background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.4s ease, color 0.4s ease; line-height: 1.6; } .site-header { text-align: center; padding: 40px 20px 20px; } .site-header h1 { font-size: 32px; } .theme-btn { margin-top: 16px; padding: 8px 18px; border: none; border-radius: 20px; background-color: var(--primary); color: #fff; font-size: 14px; cursor: pointer; transition: opacity 0.3s; } .theme-btn:hover { opacity: 0.9; } .container { max-width: 1080px; margin: 0 auto; padding: 20px; } .intro { text-align: center; margin-bottom: 32px; } .intro h2 { font-size: 24px; margin-bottom: 8px; } .intro p { max-width: 600px; margin: 0 auto; color: var(--text-color); opacity: 0.8; } .pet-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 24px; margin-bottom: 40px; } .pet-card { background-color: var(--card-bg); border-radius: 20px; padding: 24px; box-shadow: 0 10px 24px var(--card-shadow); text-align: center; transition: transform 0.3s ease, box-shadow 0.3s ease; } .pet-card:hover { transform: translateY(-4px); } .pet-avatar { font-size: 72px; line-height: 1; margin-bottom: 12px; } .pet-type { display: block; margin: 4px 0 10px; font-size: 14px; opacity: 0.7; } .pet-tags { margin-bottom: 12px; } .tag { display: inline-block; background-color: var(--tag-bg); color: var(--primary); border-radius: 12px; padding: 4px 12px; font-size: 12px; margin: 0 4px 6px; } .pet-desc { font-size: 14px; opacity: 0.85; margin-bottom: 16px; min-height: 40px; } .status-bar { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; font-size: 13px; } .bar { flex: 1; height: 10px; background-color: var(--bar-bg); border-radius: 6px; overflow: hidden; } .bar-inner { height: 100%; border-radius: 6px; transition: width 0.4s ease; } .bar-inner.mood { background: linear-gradient(90deg, #ffb199, #ff8259); } .bar-inner.energy { background: linear-gradient(90deg, #fddb92, #f5a64c); } .actions { display: flex; justify-content: center; gap: 10px; margin-top: 16px; } .btn { padding: 8px 14px; border: none; border-radius: 16px; background-color: var(--primary-light); color: var(--text-color); font-size: 13px; cursor: pointer; transition: background-color 0.3s, transform 0.2s; } .btn:hover { background-color: var(--primary); color: #fff; transform: scale(1.05); } .gallery { margin: 40px 0; } .gallery h2 { text-align: center; margin-bottom: 20px; } .gallery-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; } .gallery-item { background-color: var(--card-bg); border-radius: 16px; padding: 28px 12px; text-align: center; font-size: 18px; box-shadow: 0 6px 16px var(--card-shadow); } .site-footer { text-align: center; padding: 30px 20px; opacity: 0.7; font-size: 14px; } @media (max-width: 768px) { .pet-list { grid-template-columns: 1fr; } .gallery-grid { grid-template-columns: 1fr; } }这段 CSS 中值得学习的地方是 CSS 变量(Custom Properties)的使用。:root中定义的变量在全局可用,body.night中重新定义同名变量后,所有引用var(--bg-color)、var(--text-color)的地方都会自动使用新值。这种机制比“写死颜色再逐个覆盖”要干净得多。
4.4 编写 JavaScript 逻辑
script.js承担三类职责:维护数据、处理交互、保存状态。完整代码如下:
// 文件路径:script.js const pets = [ { id: 'cat', name: '橘子', avatar: '🐱', type: '小橘猫', age: '2岁', desc: '喜欢晒太阳,最爱小鱼干。', tags: ['慵懒', '黏人'], mood: 80, energy: 70, moodChange: { feed: 5, stroke: 8, play: -10 }, energyChange: { feed: 6, stroke: 2, play: -15 } }, { id: 'dog', name: '布丁', avatar: '🐶', type: '小柯基', age: '1岁半', desc: '精力旺盛,每天都要出门跑三圈。', tags: ['活泼', '贪玩'], mood: 70, energy: 90, moodChange: { feed: 4, stroke: 5, play: 12 }, energyChange: { feed: 5, stroke: 1, play: -20 } }, { id: 'rabbit', name: '棉花糖', avatar: '🐰', type: '小白兔', age: '8个月', desc: '胆子很小,但熟悉之后特别粘人。', tags: ['温柔', '胆小'], mood: 60, energy: 55, moodChange: { feed: 5, stroke: 12, play: -6 }, energyChange: { feed: 4, stroke: 3, play: -8 } } ]; function saveData() { localStorage.setItem('cute-three-pets', JSON.stringify(pets)); } function loadData() { const saved = localStorage.getItem('cute-three-pets'); if (saved) { try { const parsed = JSON.parse(saved); if (Array.isArray(parsed) && parsed.length === pets.length) { parsed.forEach((item, index) => { pets[index].mood = item.mood; pets[index].energy = item.energy; }); } } catch (error) { console.warn('本地数据解析失败,使用默认数据', error); } } } function renderCards() { const container = document.getElementById('pet-list'); container.innerHTML = pets.map(pet => { return ` <article class="pet-card">const MAX_VALUE = 100; const MIN_VALUE = 0;把最大最小值抽成常量,比在代码里直接写100和0更容易维护。当业务规则变化时,只需要修改常量定义。
6.3 交互反馈要即时
按钮点击后,数值的变动不应该只在数字文本上体现,还要通过颜色、动画、提示文案等方式强化反馈。比如:
- 心情值增加时,状态条颜色由浅变深;
- 数值变化时,数字增加一个小幅缩放的动画;
- 投喂操作后,在卡片角落弹出“小鱼干 +1”的小提示。
这些细节会让页面更有生命力,但实现成本并不高。
6.4 善用浏览器存储,但要注意容量和权限
localStorage 的容量一般是 5MB 左右,适合保存小体积的配置项和状态数据,不适合存储图片、大文本。同时,localStorage 只能存储字符串,对象的读写必须经过JSON.stringify和JSON.parse。另外,用户如果开启隐私模式或禁用站点数据,localStorage 可能不可用,所以需要提前用try...catch包住读取操作。
6.5 CSS 命名规范
示例中使用了类似.pet-card、.status-bar、.bar-inner的命名方式。如果你接手更大的项目,可以尝试 BEM 命名法(Block Element Modifier),例如.pet-card__avatar、.pet-card__action。命名规范的主要价值是让样式结构更清晰,方便团队协作和维护。
6.6 兼容性与前缀
现代浏览器对 CSS 变量、Flex、Grid 的支持度已经很高,但如果你需要兼容老旧浏览器,可能需要考虑:
- 为
display: grid提供 flex 回退布局; - 为渐变、
border-radius等属性添加-webkit-前缀; - 使用 autoprefixer 等构建工具自动处理。
不过在本项目中,面向现代浏览器即可,不需要过度处理兼容性。
7. 扩展方向与总结
到这里,一个完整的“可爱的三小只~”互动页面已经可以做出来了。它包含了一个前端页面从“设计数据结构”到“渲染视图”,再到“处理交互”和“持久化存储”的完整链条。通过这个项目,你会更深刻地理解下面几个核心概念:
- 数据驱动渲染:页面内容是数据的映射;
- 事件委托:动态元素的统一事件处理思路;
- 状态管理:数值变化后如何同步视图并持久化;
- 主题切换:CSS 变量在换肤场景中的实际应用。
如果你希望继续深入,这里还有几个明确的扩展方向。
第一个方向是接入后端。宠物数据不再硬编码在pets数组中,而是通过fetch('/api/pets')从后端接口获取。这一步会让你接触到异步编程、接口设计、错误处理等内容,对巩固 JavaScript 基础很有帮助。
第二个方向是引入前端框架。把这个静态页面迁移到 Vue 或 React 中,你会发现之前手动renderCards、手动维护innerHTML的工作,框架都帮你完成了。对比起来,你会更容易理解“响应式”和“组件化”的意义。
第三个方向是给项目增加更多玩法。例如:
- 为每只宠物增加“喂食记录”和“成长值”,连续喂养 7 天解锁新头像;
- 增加宠物对话气泡,不同心情状态输出不同文案;
- 使用 Canvas 或第三方动画库,让宠物做出跑步、飞翔等动作。
技术学习的路径从来不是靠背语法,而是靠一个又一个可运行、可展示、可分享的小作品往前走。“可爱的三小只~”就是一个很好的起点。如果你在实际搭建过程中遇到了其他问题,欢迎对照上面的排查表逐项检查,也可以重新阅读对应章节,理解每一步为什么这样设计。代码和笔记都放在手边,动手多敲几遍,收获会更大。