1. 项目缘起与价值:为什么你需要这50个练手项目?
干了这么多年前端,带过不少新人,也面试过很多求职者,我发现一个特别普遍的现象:很多朋友把HTML、CSS、JavaScript的语法背得滚瓜烂熟,各种API、概念说起来头头是道,但一到动手环节,面对一个空白的编辑器,就不知道从何下手了。要么是写出来的页面布局七扭八歪,要么是实现一个简单的交互效果要查半天资料,代码写得磕磕绊绊。这其实就是典型的“眼高手低”,理论和实践严重脱节。
这个“夯实基础 CSS/HTML/JS 50个练手项目”合集,就是专门为解决这个问题而生的。它不是一个简单的代码仓库,而是一套精心设计的、循序渐进的实战训练体系。它的核心价值在于,通过“做项目”这个最直接的方式,把你学到的零散知识点,像拼图一样组合成解决实际问题的能力。你不再需要去纠结“学了这个能干嘛”,因为每一个项目都会给你一个明确的目标,让你在实现目标的过程中,自然而然地运用和巩固知识。
无论是刚入门的前端小白,想通过实践加深对基础三剑客的理解;还是有一定基础但感觉遇到瓶颈、动手能力不强的同学,希望提升编码熟练度和工程思维;甚至是准备面试,需要积累项目经验来丰富简历和应对实操题,这个项目合集都能提供一条清晰的提升路径。接下来,我将为你详细拆解这套训练体系的设计思路、核心内容以及如何高效利用它。
2. 项目体系设计与难度阶梯
这套50个项目不是随意堆砌的,而是遵循着“由浅入深、由点到面”的原则精心编排的。整体上,我会将其分为四个大的阶段,每个阶段聚焦不同的核心能力和技术栈深度,确保你能平滑过渡,逐步建立起前端开发的完整知识框架。
2.1 第一阶段:静态呈现与基础布局(项目1-15)
这个阶段的目标是让你彻底征服CSS,建立起对网页视觉呈现的绝对控制力。HTML是骨架,CSS就是血肉和衣服。很多新手觉得CSS“玄学”,主要是因为对盒模型、文档流、选择器优先级等基础概念理解不深,以及缺乏足够的“手感”。
核心训练点:
- HTML语义化:告别满屏的
<div>,学习使用<header>,<nav>,<main>,<article>,<footer>等标签构建有意义的页面结构。这不仅对SEO友好,也让代码更易读和维护。 - CSS盒模型精通:
width/height,padding,border,margin之间的计算关系必须了然于胸。我会设计项目让你手动计算尺寸,深刻理解box-sizing: border-box这个“神器”为何能极大简化布局。 - 经典布局方案实战:
- 浮动布局:虽然现代布局中已不常用,但理解浮动和清除浮动是前端必修课,对于处理图文环绕等场景仍有价值。
- 定位布局:
position: relative/absolute/fixed/sticky的用法和区别,以及它们如何脱离或影响文档流。 - Flexbox弹性布局:这是本阶段的重点。通过多个项目练习
flex-direction,justify-content,align-items,flex-grow/shrink/basis等属性,实现各种水平垂直居中、等分布局、自适应排列。 - Grid网格布局:更强大的二维布局系统。练习定义网格轨道、放置网格项目,实现复杂的杂志式排版。
- 响应式设计入门:使用媒体查询 (
@media),让同一个网页在手机、平板、电脑上都能有良好的阅读体验。从“移动优先”的角度思考布局。
代表性项目示例:
- 项目1:个人名片:纯HTML+CSS,练习文本样式、基础盒模型和居中。
- 项目5:响应式导航栏:练习Flexbox,并在小屏幕下切换为汉堡菜单。
- 项目10:圣杯布局/双飞翼布局:用浮动或Flexbox实现经典的三栏布局,理解布局演进史。
- 项目15:个人博客文章页:综合运用语义化HTML、Grid进行整体布局,并加入响应式。
实操心得:这个阶段切忌急于引入JavaScript。把全部精力放在CSS上,反复调整
padding、margin一个像素,观察变化。推荐使用浏览器的开发者工具,实时编辑CSS并查看效果,这是学习CSS最快的方式。
2.2 第二阶段:交互逻辑与DOM操作(项目16-35)
当你能熟练地“画”出静态页面后,就要让它“动”起来,能与用户交互了。这个阶段的核心是JavaScript,特别是DOM API。你将学习如何用JS这把手术刀,精准地操作网页的每一个部分。
核心训练点:
- DOM查询与操作:熟练使用
document.getElementById/querySelector/querySelectorAll获取元素。然后对它们进行增 (createElement,appendChild)、删 (removeChild)、改 (textContent,innerHTML,setAttribute)、查 (classList,style)。 - 事件处理机制:理解事件流(捕获、目标、冒泡),熟练使用
addEventListener为元素绑定click,mouseover,keydown,input,submit等事件。学会使用event.preventDefault()和event.stopPropagation()。 - 基础算法与逻辑:将编程基础语法(循环、条件、函数)应用于实际场景。例如,实现一个TODO List(增删改查),就需要用到数组操作、循环渲染和事件委托。
- 表单验证:学习在客户端用JS进行基础验证(非空、邮箱格式、密码强度),并给用户即时的反馈。
- 基础动画:通过
setInterval/setTimeout或requestAnimationFrame配合修改CSS属性(如transform,opacity),实现简单的动画效果。
代表性项目示例:
- 项目18:计数器:练习按钮点击事件、数据更新和DOM同步显示。
- 项目22:模态框:练习绝对定位、显示隐藏(
display,visibility)、遮罩层和事件绑定。 - 项目28:图片轮播图:综合项目,涉及定时器、事件监听、CSS过渡效果和状态管理。
- 项目33:简易绘画板:练习
mousedown,mousemove,mouseup事件,以及Canvas API的基础使用。
注意事项:这个阶段很容易写出“面条式代码”(所有逻辑堆在一起)。要有意识地将不同功能的代码用函数封装起来,这是培养良好编码习惯的开始。另外,事件委托是一个必须掌握的高效技巧,它允许你在父元素上监听子元素的事件,对于动态添加的元素尤其有用。
2.3 第三阶段:数据驱动与异步编程(项目36-45)
前端不仅仅是处理界面,更重要的是处理数据。这个阶段会引入更接近真实开发的模式,让你的项目从“玩具”升级为“工具”。
核心训练点:
- 数据状态管理:思考如何用变量或对象来集中管理应用的状态(如TODO列表的所有项)。状态改变后,如何高效地更新视图?这会自然引向更高级的框架思想。
- 异步JavaScript:这是现代前端的分水岭。彻底理解回调函数、Promise、
async/await。学会处理异步操作,比如定时任务、读取本地文件(FileReader)、或者与后端API通信。 - Fetch API与数据获取:学习使用
fetch()或axios(可后续引入)从公开的免费API(如天气API、随机用户API)获取真实数据,并渲染到页面上。这是前后端分离开发的核心。 - 本地存储:使用
localStorage或sessionStorage在浏览器端持久化数据,实现关闭页面后数据不丢失,例如保存TODO列表或用户设置。
代表性项目示例:
- 项目38:天气预报应用:调用第三方天气API,获取数据后动态更新页面。练习异步请求、错误处理和JSON数据解析。
- 项目41:GitHub用户搜索器:调用GitHub API,根据输入的用户名搜索并展示用户信息。练习表单提交、异步搜索和结果渲染。
- 项目44:Markdown实时预览器:监听文本输入框的变化,将Markdown文本实时转换为HTML并预览。练习事件监听、字符串处理和DOM更新。
- 项目45:带持久化的笔记应用:综合运用CRUD操作、本地存储和事件处理,构建一个功能相对完整的应用。
2.4 第四阶段:综合应用与性能初探(项目46-50)
最后这个阶段的项目规模会稍大,复杂度更高,旨在训练你将前三个阶段的所有技能融会贯通,并开始关注一些工程化和性能方面的基础问题。
核心训练点:
- 模块化思维:尝试将JavaScript代码按功能拆分成不同的模块(可以使用ES6 Modules),例如将数据操作、视图渲染、工具函数分开。
- 组件化概念:虽然不使用框架,但可以模仿组件化的思想,构建可复用的UI部分,比如一个自定义的按钮、卡片组件。
- 性能优化意识:学习简单的优化技巧,如图片懒加载、防抖与节流(用于搜索框或滚动事件)、减少重绘与回流。
- 工具链初体验:可能会引入最简单的构建工具(如Parcel)来体验模块打包,或者使用ESLint来规范代码风格。
代表性项目示例:
- 项目48:简易看板应用:实现类似Trello的拖拽卡片功能,涉及复杂的状态管理、事件监听和DOM操作。
- 项目50:单页应用路由模拟:利用
hashchange事件或History API,在一个页面内模拟多页面切换的效果,理解前端路由的基本原理。
3. 核心细节解析与实操要点
有了整体的路线图,我们深入看看在具体实现这些项目时,有哪些共通的、必须掌握的核心细节和容易踩坑的地方。
3.1 HTML:语义化是优雅的起点
很多新手写HTML只关心“能不能显示”,不关心“表达的是什么”。语义化标签不仅能提升可访问性(屏幕阅读器可以更好地解读),也让代码结构一目了然。
错误示范:
<div id="header"> <div class="nav">...</div> </div> <div class="main-content"> <div class="article">...</div> </div> <div id="footer">...</div>正确示范:
<header> <nav>...</nav> </header> <main> <article>...</article> </main> <footer>...</footer>实操要点:
<main>在一个页面中应该只出现一次,代表主要内容区域。<article>代表一个独立、完整的内容块,如博客文章、新闻、论坛帖子,它应该有自己的标题(<h1>-<h6>)。<section>代表文档中的一个 thematic grouping(主题分组),通常会有自己的标题。它和<article>的区别在于,<article>更强调独立性,即使脱离上下文也成立。- 不要为了语义化而过度使用
<section>,如果只是为了样式包裹,一个简单的<div>更合适。
3.2 CSS:布局、选择器与响应式的艺术
3.2.1 布局选择:Flexbox vs. Grid
- 一维布局用Flexbox:当你需要沿一条轴线(水平或垂直)排列元素时,Flexbox是首选。比如导航栏、工具栏、卡片列表(等宽或不等宽)。
- 二维布局用Grid:当你需要同时控制行和列,进行更复杂的整体页面布局时,Grid更强大。比如整个页面的整体架构,或者需要精确控制元素在网格中位置的场景。
- 混合使用:在实际项目中,常常是Grid负责宏观布局(定义页面区域),Flexbox负责微观布局(排列区域内的内容)。例如,用Grid定义
header, sidebar, main, footer的位置,在header内部用Flexbox排列logo和导航菜单。
3.2.2 CSS选择器优先级与维护性优先级计算公式:!important> 行内样式 > ID选择器 > 类/伪类/属性选择器 > 元素/伪元素选择器。
- 尽量避免使用
!important和 ID 选择器:它们优先级太高,难以覆盖,是样式难以维护的罪魁祸首。 - 推荐使用类选择器:通过有意义的类名(如
.btn--primary,.card__title)来定义样式,可读性和可复用性都更好。可以考虑学习BEM命名方法论。 - 善用后代选择器和子选择器:但要避免过深的嵌套(如
.a .b .c .d),这会导致优先级过高且性能下降。
3.2.3 响应式断点设置不要盲目跟从设备尺寸设置断点(如@media (max-width: 768px))。应该根据内容本身来决定何时需要调整布局。
/* 基于内容断点 */ .container { width: 100%; padding: 1rem; } /* 当容器宽度足够时,改为固定宽度并居中 */ @media (min-width: 600px) { .container { width: 80%; max-width: 1200px; margin: 0 auto; } } /* 导航栏在更窄的屏幕上切换为垂直排列 */ @media (max-width: 500px) { .navbar { flex-direction: column; } }3.3 JavaScript:清晰的数据管理与事件处理
3.3.1 状态管理雏形即使不用框架,也要有状态管理的意识。以一个计数器为例:
// 不好的做法:状态分散在DOM中 function badIncrement() { const countEl = document.getElementById('count'); let currentCount = parseInt(countEl.textContent); // 每次都从DOM读取 currentCount++; countEl.textContent = currentCount; } // 好的做法:单一数据源 let state = { count: 0 }; // 集中管理状态 function goodIncrement() { state.count++; // 只修改状态 renderCount(); // 然后根据状态更新视图 } function renderCount() { document.getElementById('count').textContent = state.count; }这种“状态改变 -> 更新视图”的模式,是React/Vue等框架的核心思想。提前练习,能让你在学框架时豁然开朗。
3.3.2 事件委托:处理动态元素的利器当你需要给很多相似子元素(比如列表项)绑定事件,或者子元素是动态添加/删除时,事件委托是唯一优雅的解决方案。
// 假设有一个todo列表 const todoList = document.getElementById('todo-list'); // 传统方式(有问题):动态新增的li不会有事件 // document.querySelectorAll('#todo-list li').forEach(item => { // item.addEventListener('click', () => {...}); // }); // 事件委托:将事件绑定在父元素上 todoList.addEventListener('click', function(event) { // 检查被点击的元素是不是我们关心的li if (event.target.tagName === 'LI') { console.log('你点击了:', event.target.textContent); } // 或者更精确,如果li内部有复杂结构 const deleteButton = event.target.closest('.delete-btn'); if (deleteButton) { const listItem = deleteButton.closest('li'); listItem.remove(); // 删除对应的待办项 } });4. 实操过程与核心环节实现
让我们以一个具体的项目——项目28:图片轮播图为例,来走一遍完整的实现流程。这是一个综合性的项目,涵盖了HTML结构、CSS样式、布局、动画以及JavaScript的事件处理、状态管理和定时器。
4.1 第一步:HTML结构与CSS样式
首先,我们搭建静态的骨架和皮肤。
<!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> <div class="carousel-container"> <!-- 轮播图片区域 --> <div class="carousel-track"> <!-- 图片会在JS中动态插入 --> </div> <!-- 导航按钮 --> <button class="carousel-btn prev-btn"><</button> <button class="carousel-btn next-btn">></button> <!-- 指示器小圆点 --> <div class="indicators"> <!-- 小圆点会在JS中动态生成 --> </div> </div> <script src="script.js"></script> </body> </html>/* style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; justify-content: center; align-items: center; min-height: 100vh; background-color: #f0f0f0; font-family: sans-serif; } .carousel-container { position: relative; width: 800px; height: 450px; overflow: hidden; /* 隐藏超出容器的图片 */ border-radius: 10px; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2); } .carousel-track { display: flex; height: 100%; transition: transform 0.5s ease-in-out; /* 添加滑动过渡效果 */ } .carousel-slide { min-width: 100%; height: 100%; } .carousel-slide img { width: 100%; height: 100%; object-fit: cover; /* 让图片覆盖整个区域,保持比例 */ } .carousel-btn { position: absolute; top: 50%; transform: translateY(-50%); background-color: rgba(0, 0, 0, 0.5); color: white; border: none; width: 50px; height: 50px; border-radius: 50%; font-size: 1.5rem; cursor: pointer; z-index: 10; transition: background-color 0.3s; } .carousel-btn:hover { background-color: rgba(0, 0, 0, 0.8); } .prev-btn { left: 20px; } .next-btn { right: 20px; } .indicators { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; } .indicator { width: 12px; height: 12px; border-radius: 50%; background-color: rgba(255, 255, 255, 0.5); cursor: pointer; transition: background-color 0.3s; } .indicator.active { background-color: white; }关键点解析:
- 容器与轨道:
.carousel-container设置了overflow: hidden,只显示一张图片。.carousel-track是一个Flex容器,所有图片(.carousel-slide)水平排列,每张min-width: 100%确保占满容器宽度。 - 过渡动画:在
.carousel-track上设置transition: transform 0.5s ease-in-out。我们通过JavaScript改变它的transform: translateX()值来实现平滑滑动。 - 绝对定位:左右按钮和指示器都使用
position: absolute定位在轮播图容器之上。
4.2 第二步:JavaScript逻辑实现
接下来是让轮播图“活”起来的核心逻辑。
// script.js document.addEventListener('DOMContentLoaded', function() { // 1. 初始化数据与状态 const images = [ 'https://picsum.photos/800/450?image=1', 'https://picsum.photos/800/450?image=2', 'https://picsum.photos/800/450?image=3', 'https://picsum.photos/800/450?image=4', 'https://picsum.photos/800/450?image=5', ]; let currentIndex = 0; // 当前显示的图片索引 let autoPlayInterval; // 自动轮播的定时器ID // 获取DOM元素 const track = document.querySelector('.carousel-track'); const prevBtn = document.querySelector('.prev-btn'); const nextBtn = document.querySelector('.next-btn'); const indicatorsContainer = document.querySelector('.indicators'); // 2. 初始化轮播图结构 function initCarousel() { // 清空轨道和指示器 track.innerHTML = ''; indicatorsContainer.innerHTML = ''; // 创建图片元素和指示器 images.forEach((imgSrc, index) => { // 创建图片slide const slide = document.createElement('div'); slide.className = 'carousel-slide'; const img = document.createElement('img'); img.src = imgSrc; img.alt = `轮播图 ${index + 1}`; slide.appendChild(img); track.appendChild(slide); // 创建指示器 const indicator = document.createElement('div'); indicator.className = 'indicator'; if (index === currentIndex) { indicator.classList.add('active'); } indicator.dataset.index = index; // 自定义属性存储索引 indicator.addEventListener('click', () => goToSlide(index)); indicatorsContainer.appendChild(indicator); }); updateCarousel(); // 初始定位 } // 3. 更新轮播图位置和激活状态 function updateCarousel() { // 移动轨道 const slideWidth = track.clientWidth; // 获取一张图片的宽度 track.style.transform = `translateX(-${currentIndex * slideWidth}px)`; // 更新指示器激活状态 document.querySelectorAll('.indicator').forEach((indicator, index) => { indicator.classList.toggle('active', index === currentIndex); }); } // 4. 跳转到指定幻灯片 function goToSlide(index) { // 处理边界:循环轮播 // 如果 index >= 图片总数,则回到第一张 (0) // 如果 index < 0,则跳到最后一张 if (index >= images.length) { currentIndex = 0; } else if (index < 0) { currentIndex = images.length - 1; } else { currentIndex = index; } updateCarousel(); } // 5. 上一张/下一张 function goToPrev() { goToSlide(currentIndex - 1); } function goToNext() { goToSlide(currentIndex + 1); } // 6. 自动轮播 function startAutoPlay(interval = 3000) { stopAutoPlay(); // 先清除可能存在的旧定时器 autoPlayInterval = setInterval(goToNext, interval); } function stopAutoPlay() { if (autoPlayInterval) { clearInterval(autoPlayInterval); autoPlayInterval = null; } } // 7. 事件监听 prevBtn.addEventListener('click', goToPrev); nextBtn.addEventListener('click', goToNext); // 鼠标悬停时暂停自动轮播,离开时恢复 const container = document.querySelector('.carousel-container'); container.addEventListener('mouseenter', stopAutoPlay); container.addEventListener('mouseleave', () => startAutoPlay(3000)); // 8. 初始化并开始 initCarousel(); startAutoPlay(3000); // 3秒自动切换 // 9. 响应窗口大小变化(重要!) window.addEventListener('resize', updateCarousel); });核心逻辑拆解:
- 状态驱动:
currentIndex是核心状态,它决定了当前显示哪张图片。任何操作(点击按钮、指示器、自动播放)最终都是修改这个状态,然后调用updateCarousel()去更新视图。这是典型的数据驱动UI思想。 - 动态构建DOM:
initCarousel函数根据图片数据数组,动态创建所有的图片元素和指示器,并绑定事件。这使得更换轮播图片只需修改images数组即可。 - 平滑过渡:
updateCarousel函数计算translateX的偏移量(-当前索引 * 单张图片宽度),并通过CSStransition实现动画。 - 自动轮播与交互:使用
setInterval实现自动播放,并通过mouseenter/mouseleave事件在用户交互时暂停播放,提升体验。 - 响应式处理:监听
resize事件,在窗口大小改变后重新计算translateX的值,因为图片容器的宽度可能发生了变化。
5. 常见问题与排查技巧实录
在实际动手做这50个项目时,你几乎一定会遇到下面这些问题。我把它们和解决思路整理出来,希望能帮你节省大量调试时间。
5.1 CSS布局“玄学”问题
问题1:元素莫名有个边距,或者对不齐。
- 排查:首先检查浏览器的默认样式。使用
* { margin: 0; padding: 0; box-sizing: border-box; }进行全局重置是一个好习惯。然后,用浏览器开发者工具的“元素检查”功能,仔细查看目标元素的margin,padding,border和实际计算出的width/height。 - 技巧:给可疑元素临时加上醒目的背景色或边框(如
border: 1px solid red !important;),能让你一眼看清它的实际占据空间。
问题2:flex或grid布局不按预期排列。
- 排查:
- Flexbox:确认容器的
flex-direction和flex-wrap是否正确。子项的flex-grow,flex-shrink,flex-basis属性会影响其最终尺寸。 - Grid:确认定义的
grid-template-columns/rows是否合理。使用开发者工具中的网格覆盖显示功能(在Elements面板,点击Grid容器旁的grid标签),可以可视化网格线。
- Flexbox:确认容器的
- 技巧:对于复杂布局,先在纸上或注释里画出草图,明确行列关系,再写CSS。
问题3:position: absolute的元素“跑飞了”。
- 排查:绝对定位的元素是相对于最近的非
static定位的祖先元素定位的。检查其父级、祖父级元素是否有position: relative/absolute/fixed。如果没有,它会一直找到<html>。 - 技巧:如果你想让它相对于某个父容器定位,记得给该容器加上
position: relative(通常不改变其原有布局方式)。
5.2 JavaScript交互逻辑问题
问题1:事件监听器没反应。
- 排查:
- 元素是否存在:确保
document.querySelector成功获取到了元素,在绑定事件前用console.log打印一下。 - 绑定时机:如果你的脚本在
<head>中,而元素在<body>里,脚本执行时元素还不存在。解决方案:将<script>标签放在<body>末尾,或者用DOMContentLoaded事件包裹代码。 - 事件类型:确认你绑定的事件类型是否正确(是
click还是mouseover?)。 - 事件委托:如果是给动态创建的元素绑定事件,必须使用事件委托。
- 元素是否存在:确保
问题2:console.log显示的值和预期不符,尤其是异步操作后。
- 排查:这是经典的异步问题。JavaScript是单线程非阻塞的。例如:
let data; fetch('some-api.com').then(res => data = res); // 异步 console.log(data); // 这里打印的是 undefined,因为fetch还没完成 - 技巧:所有依赖于异步结果的操作,都必须放在回调函数、
.then()或async/await的上下文中。fetch('some-api.com') .then(res => res.json()) .then(jsonData => { console.log(jsonData); // 在这里使用数据 updateUI(jsonData); });
问题3:变量污染或命名冲突。
- 排查:在全局作用域声明大量变量,很容易冲突。比如两个不同的函数都用了同名变量
i做循环索引。 - 技巧:
- 使用函数作用域:尽量将代码封装在函数或模块中,用
let/const声明变量。 - 使用IIFE:对于简单的脚本,可以用立即执行函数表达式包裹,创建独立作用域。
(function() { // 你的代码 const privateVar = 'secret'; })(); // console.log(privateVar); // 报错,访问不到
- 使用函数作用域:尽量将代码封装在函数或模块中,用
5.3 性能与体验优化初探
当项目变得复杂时,就需要开始关注性能。
问题1:页面滚动或频繁触发事件时卡顿。
- 可能原因:在
scroll,resize,input等高频触发的事件回调中,执行了复杂的DOM操作或计算。 - 解决方案:防抖与节流。
- 防抖:事件触发后,等待一段时间内不再触发,才执行函数。适用于搜索框输入联想。
function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } const handleSearch = debounce(() => { console.log('搜索...') }, 300); searchInput.addEventListener('input', handleSearch);- 节流:保证在一定时间内,函数只执行一次。适用于滚动加载。
function throttle(func, limit) { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } }; }
问题2:图片加载慢,影响页面渲染。
- 解决方案:
- 图片优化:使用工具压缩图片(如TinyPNG),选择合适格式(WebP格式更小)。
- 懒加载:只加载可视区域内的图片。可以使用
loading="lazy"属性(现代浏览器支持),或使用Intersection Observer API自己实现。 - 预加载关键资源:对于首屏至关重要的图片,可以使用
<link rel="preload" as="image" href="hero.jpg">。
做这50个项目,最重要的不是“做完”,而是“做透”。每一个项目完成后,问问自己:我能不能不用看任何参考,从头再实现一遍?我能不能给它添加一个新功能?我能不能用另一种技术方案(比如用Grid重写Flexbox的布局)再实现一次?当你能够举一反三,自由组合这些基础知识去解决新问题时,你的前端基础才算是真正夯实了。这份项目清单会持续更新,但核心的学习方法是不变的:动手,思考,再动手。