简介:一份仿苹果官网的超文本标记语言(HTML)基础教程,面向刚接触前端开发的初学者,目标是带领读者复刻类似苹果官方网站的页面布局与交互体验。教程重点讲解超文本标记语言中的语义化标签,包括页头、导航、内容区块、页脚等结构要素,同时结合JavaScript与jQuery完成下拉菜单、轮播图、淡入淡出等常见动效,让静态页面逐步变成可操作的动态网页。资源包共449个文件,压缩后约8.54MB,包含12个超文本页面、14个层叠样式表、18个JavaScript脚本,以及250张PNG图片、147张JPG图片、8张GIF图片,基本覆盖页面排版、视觉还原与动效演示所需素材。已有1874人浏览学习。学习时,读者可以边看代码边调整效果,既能掌握苹果风格页面的布局拆解与组合方法,又能理解多页面共享样式、页面对象操作与事件绑定等实用技能,为后续独立开发交互型网站打下坚实基础。
1. 仿苹果官网这套 HTML 首页:html、js、jQuery 三样东西怎么落到一个页面里
写 HTML 的人迟早想对着苹果官网抄一遍:导航栏滚动后从透明变半透明,Hero 大字在暗色背景下翻转,产品卡悬浮时轻微抬升。看着不难,真动手才发现一堆细节——监听放哪、样式怎么切、参数改哪个文件。这套仿 Apple 官网首页的源码包正好覆盖 html、js、jQ 三条线:一份语义化 HTML 骨架,一套做状态切换的 CSS 类名,一份绑滚动事件的 jQuery 脚本。它解决的是“练习页怎么写才不散”的问题,适合刚学完基础语法、想找完整项目练手的新手,也适合想复用导航栏交互逻辑的老手。它不是用来炫技的,是用来拆开改的。
2. 页面结构拆解:导航栏固定定位、Hero 暗色切换与产品磁贴布局
2.1 导航栏用 position: fixed 而不是 sticky:脱离文档流的层级稳定性
苹果官网首页的导航栏是页面上最醒目的交互点:初始透明,向下滚动超过一定距离后变成带模糊效果的半透明深色条。实现这个效果,定位方式得先定死。我一般直接用position: fixed,而不是sticky。原因在于sticky的偏移受父容器边界约束,一旦导航栏被包在某个section里,滚到该 section 底部就会被“顶走”,状态切换会出现跳跃。fixed完全脱离文档流,滚动时始终钉在视口顶部,层级用z-index单独控制,省去一堆边界判断。
页面骨架的常见写法是这样,header和main是平级结构,这样导航栏不会受任何兄弟元素影响:
<header class="nav" id="appleNav"> <div class="nav-inner"> <a href="#" class="nav-logo">商店</a> <a href="#" class="nav-link">Mac</a> <a href="#" class="nav-link">iPad</a> <a href="#" class="nav-link">iPhone</a> <a href="#" class="nav-link">Watch</a> <a href="#" class="nav-link">技术规格</a> </div> </header> <main> <section class="hero">...</section> <section class="products">...</section> </main>header在main外层,意味着后续所有区块的scrollTop偏移量都是相对文档整体计算的,不需要针对不同父容器做补偿。CSS 侧需要固定定位和层级两个参数:
.nav { position: fixed; top: 0; left: 0; right: 0; z-index: 1000; background-color: rgba(22, 22, 23, 0.8); backdrop-filter: saturate(180%) blur(20px); transition: background-color 0.3s ease; } .nav.is-transparent { background-color: transparent; backdrop-filter: none; }这里z-index: 1000是常用值,确保 Hero 里的文字、产品卡里的悬浮层都压在导航栏下面。backdrop-filter做毛玻璃模糊,真机上要注意前缀兼容,iOS 低版本需要-webkit-backdrop-filter。状态切换由后面 JS 控制is-transparent类名实现,初始默认不透明,滚动到顶部时再移除——这种做法比默认透明、滚下去再上色要直观,因为页面加载瞬间就能看到导航栏,避免首屏白条。
2.2 Hero 区用 CSS 变量控制文字亮暗:不写死颜色值
Hero 区是整页的品牌视觉中心,背景是深色大图,文字是白色大标题。苹果官网那个“大字翻转”效果,其实可以拆成两层:一层是标题的入场动画,另一层是滚动时文字亮暗切换。这套源码把后者做成了 CSS 变量驱动的形式,而不是直接切换颜色值。原因是 Hero 区颜色需要在多个地方复用,直接写color: #fff会让后续改主题时到处找。
Hero 区标记了一个数据属性>.hero { --hero-text: #f5f5f7; --hero-muted: rgba(245, 245, 247, 0.8); --nav-bg: rgba(22, 22, 23, 0); min-height: 100vh; display: flex; align-items: center; justify-content: center; background: center / cover no-repeat url("images/hero-bg.jpg"); color: var(--hero-text); } .hero .subtitle { color: var(--hero-muted); font-size: 1.25rem; letter-spacing: 0.02em; }
--hero-text和--hero-muted这两个变量只在.hero作用域内生效,页面其他区域的文字不会误伤。JS 切换状态时,只需要在.hero上追加一个类名,把变量整体覆盖:
.hero.is-dim .subtitle { --hero-muted: rgba(245, 245, 247, 0.4); }这样做的收益是:修改颜色只动变量定义,不用去查color、border-color、text-shadow分散的命中点。做暗色主题适配时,这套变量结构能直接复用。
2.3 产品磁贴与页脚的 flex 栅格:不定宽卡片布局
产品区是一组并排的卡片,每张卡片含一张产品图、标题、描述和一个链接。源码里没有用 Bootstrap 的col-md-4栅格,而是用flex加固定百分比宽度,理由是这个页面只面向桌面端演示,不需要覆盖多种断点。
.products { display: flex; flex-wrap: wrap; gap: 16px; padding: 40px 20px; max-width: 1200px; margin: 0 auto; } .product-card { flex: 1 1 30%; min-width: 260px; border-radius: 12px; overflow: hidden; transition: transform 0.3s ease, box-shadow 0.3s ease; } .product-card:hover { transform: translateY(-4px); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); }flex: 1 1 30%有三个值:flex-grow: 1让卡片拉伸填满剩余空间,flex-shrink: 1允许压缩,flex-basis: 30%是初始宽度。配合min-width: 260px防止窄屏下卡片被压成细条。卡片的悬浮抬升用了transform而不是top,因为transform走 GPU 合成,不会触发布局重排,悬浮动画更稳。页脚的做法相同,只是把 flex 换成更简单的间距控制,内部链接用column排列,边界只需要一个上边框线。整页布局到这里已经成立,接下来是 JS 部分如何把这些状态串起来。
3. 滚动交互实现:从 scroll 事件到 jQuery 状态切换
3.1 原生 JS 与 jQuery 的两种监听写法:addEventListener 与 on
页面要响应的核心事件只有一个:scroll。源码里同时给出原生和 jQuery 两种写法,方便对照学习。原生写法是:
window.addEventListener("scroll", function () { const nav = document.getElementById("appleNav"); const hero = document.querySelector(".hero"); if (window.scrollY > 80) { nav.classList.remove("is-transparent"); } else { nav.classList.add("is-transparent"); } });jQuery 写法用.on("scroll"):
$(window).on("scroll", function () { var nav = $("#appleNav"); var hero = $(".hero"); if ($(window).scrollTop() > 80) { nav.removeClass("is-transparent"); } else { nav.addClass("is-transparent"); } });两者有几点差异要清楚。原生addEventListener的第二个参数是普通函数,this指向window;jQuery 的.on("scroll")绑在$(window)上后,回调里this是原生 DOM,但$()包一层才能用 jQuery 方法。scrollY和scrollTop()是两种获取滚动距离的方式,前者只读,后者是 jQuery 封装,IE 时代的老代码里常见。这套源码以 jQuery 为主,因为链式调用的代码量少,对刚接触 JS 的读者更友好。
事件绑定还有一个细节:不要在 DOM 就绪之前执行。原生写法要放在DOMContentLoaded回调里,jQuery 写法放进$(function() { ... })即可:
$(function () { // 所有事件绑定与初始化都写在这里 });$(function(){})是$(document).ready()的简写,确保事件绑定发生在 DOM 树构建完成后。如果脚本放在<head>里且没有加defer,这个简写就是必须的,否则$("#appleNav")会拿到空对象,事件永远不触发。
3.2 throttle 节流:滚动回调不能每帧都执行
scroll 事件在浏览器里触发频率极高,滚动一下可能触发几十次回调。如果回调里还做了类名切换、读取位置、操作 DOM,性能立刻被打穿。常见的做法是加一个节流函数,控制回调在指定时间窗口内最多执行一次。
function throttle(fn, delay) { var last = 0; return function () { var now = Date.now(); if (now - last >= delay) { last = now; fn.apply(this, arguments); } }; } var handleScroll = throttle(function () { console.log("scroll 位置:", $(window).scrollTop()); }, 100); $(window).on("scroll", handleScroll);throttle的参数说明:fn是真正要执行的逻辑,delay是毫秒数。last记录上次执行时间,now - last >= delay成立才执行。这里fn.apply(this, arguments)很关键,它把事件对象和正确的this传回原函数,否则某些依赖事件的代码会拿不到参数。间隔选 100 到 120 毫秒比较合适,小于 80 会很敏感,大于 200 会感觉响应迟钝。节流函数在$(function(){})外层定义,每次滚动只创建一次,不会因为滚动重复定义。
3.3 状态切换的时机:scrollTop 阈值与 offsetTop 缓存
导航栏从透明变不透明的临界位置,苹果官网实际是“滚出 Hero 区域底部”之后才变。如果只用一个固定值 80 像素,遇到不同高度的 Hero 就得手动改。源码里用一个变量存 Hero 的底部位置,滚动时拿scrollTop和它比较:
var heroBottom = $(".hero").offset().top + $(".hero").outerHeight(); function updateNavOnScroll() { var current = $(window).scrollTop(); if (current > heroBottom - 50) { $("#appleNav").removeClass("is-transparent"); } else { $("#appleNav").addClass("is-transparent"); } }heroBottom - 50的 50 是一个缓冲值,意思是滚到距离 Hero 底部还有 50 像素时就切换,视觉上更顺滑,不会在临界点反复横跳。offset().top返回元素相对文档顶部的位置,注意它和position().top的区别:后者是相对偏移父元素的位置。outerHeight()默认含margin,如果 Hero 有margin-top会导致计算偏大,需要传true参数调整。这个计算在初始化时执行一次,缓存成变量,不要在滚动回调里重复读取,因为这些属性每次读取都会触发浏览器重排。
状态切换本身用类名,不用内联样式,这是为了让 CSS 过渡动画参与进来。类名切换时transition: background-color 0.3s ease会生效,颜色变化是渐变的,不是生硬的跳变。
4. 源码包跑法与调参:从 index.html 到 main.js 的改动位置
4.1 目录结构与文件职责
这套资源解压后是一个典型的静态站点结构,不用构建工具,浏览器直接打开就能跑。目录结构如下:
| 文件 | 职责 | 主要改动内容 |
|---|---|---|
index.html | 页面骨架,所有区块的 DOM 结构 | 导航链接文字、Hero 标题、产品卡内容 |
css/style.css | 全站样式,包括变量、布局、状态类 | 颜色变量、间距、导航栏透明度 |
js/main.js | jQuery 初始化、事件绑定、节流逻辑 | 滚动阈值、缓冲值、节流间隔 |
images/ | Hero 背景图、产品图占位 | 替换图片文件,保持同名即可 |
运行方式很简单:把整个目录放到任意 HTTP 服务下,或者直接双击index.html。但直接双击打开时有两点要注意:一是部分浏览器对本地文件模式下 jQuery 的加载没有限制,但图片懒加载可能受 file 协议影响,后面会讲;二是backdrop-filter在本地文件模式下的表现和线上一致,不存在协议差异。
4.2 核心参数在哪个文件改:阈值、颜色、间距
改动频率最高的几个参数集中在js/main.js的开头一段,源码里已经做了配置化处理,避免散落在代码中间。常见的写法是:
var CONFIG = { navToggleOffset: 50, // 距 Hero 底部多少像素后切换导航栏状态 throttleDelay: 100, // 节流间隔,单位毫秒 anchorSpeed: 400 // 锚点平滑滚动速度 };navToggleOffset对应前文的缓冲值 50,想晚一点变就调大,想早一点变就调小。throttleDelay是前面节流的 delay,真机上觉得卡就调大到 150。anchorSpeed是锚点点击时平滑滚动动画的时长,单位毫秒,400 适合短滚距,页面很长就调到 600。颜色和间距在css/style.css的:root变量段改,不需要进 JS:
:root { --page-bg: #ffffff; --nav-bg-solid: rgba(22, 22, 23, 0.8); --gap-card: 16px; }这里有个意想不到的小坑::root里定义的变量和.hero里的变量作用域不同。:root是全站可见,.hero里的只对 Hero 内部生效。如果改了:root的--nav-bg-solid,但.hero里没有引用它,导航栏颜色不变,容易让人误以为改错了位置。排查时先确认变量定义在哪个选择器里。
4.3 图片懒加载与><img class="lazy">function lazyLoadImages() { $("img.lazy").each(function () { var $img = $(this); if ($img.offset().top < $(window).scrollTop() + $(window).height()) { $img.attr("src", $img.data("src")); $img.removeClass("lazy"); } }); }
$(window).height()是视口高度,offset().top是图片相对文档顶部的位置,两者之和表示当前视口底部边界。图片位置小于这个值就说明已经进入可视区。这个函数每次滚动都会遍历所有img.lazy,图片多时建议在加载完成后把节点从遍历集合中移除,源码里用removeClass("lazy")做到了这一点,因为选择器匹配的集合会自动变化。
要注意data()和attr("data-src")的区别。jQuery 的data("src")会尝试把字符串解析为数字、布尔值等类型,而attr("data-src")永远返回原始字符串。图片地址一般没有类型歧义,但养成用attr()的习惯更稳,尤其是地址以数字结尾时。
5. 常见问题避坑:滚动失效、闪白、真机发飘的四条踩坑记录
5.1 导航栏切到深色时页面闪白
现象:向下滚动时,导航栏从透明变成深色的一瞬间,Hero 顶部会闪一下白底。
原因:backdrop-filter: blur(20px)在部分浏览器里对透明背景区域会先渲染一个白色衬底,再叠加模糊。背景从完全透明切换到半透明时,模糊效果被重新计算,产生一次闪白。
解决:把默认背景从transparent改成rgba(22, 22, 23, 0.01),只留 0.01 的透明度,视觉上仍然是透明,但能触发 GPU 合成层,避免闪白。做法是改 CSS 变量:
.nav.is-transparent { background-color: rgba(22, 22, 23, 0.01); backdrop-filter: none; }这样切换时是透明度渐变的连续值,不会经历“无背景 → 有背景”的突变。
5.2 锚点跳转后导航栏状态错乱
现象:点击页脚里的链接跳转到页面中部,导航栏停在透明状态,向上滚回顶部后依然透明,必须刷新才恢复。
原因:锚点跳转是瞬时的,浏览器直接修改scrollTop,这期间 scroll 事件只触发一次。如果跳转过程没有经过 Hero 底部位置,updateNavOnScroll就没有执行到切换分支,导航栏停留在旧状态。
解决:点击锚点后主动执行一次状态刷新。在锚点点击事件回调里调用一次updateNavOnScroll(),或者把跳转改成平滑滚动,让中间位置也能触发事件:
$(".footer a[href^='#']").on("click", function (e) { e.preventDefault(); var target = $($(this).attr("href")); if (target.length) { $("html, body").animate({ scrollTop: target.offset().top - 100 }, 400); } setTimeout(updateNavOnScroll, 450); });setTimeout(updateNavOnScroll, 450)里的 450 毫秒是留给 400 毫秒动画结束后的额外缓冲。直接用锚点默认行为时,浏览器通知 scroll 事件的时机不确定,手动补一次调用是兜底方案。
5.3 移动端橡皮筋回弹导致 scrollTop 为负
现象:iPhone 上在页面顶部继续向下拉,页面出现空白的橡皮筋效果,此时导航栏状态被切换成透明,松手后状态不恢复。
原因:移动端 WebView 的滚动实现允许scrollTop变成负值或超过最大值,而 jQuery 的scrollTop()返回负值时,状态判断current > heroBottom不成立,走了透明分支。
解决:在判断前把scrollTop钳制到合法范围:
var current = Math.max(0, $(window).scrollTop());Math.max(0, value)把负值统一改成 0,这样橡皮筋回弹期间不会误切状态。还有一个伴生问题是滚动结束事件在移动端会延迟触发,可以在touchend后 100 毫秒再调用一次更新函数,确保状态收敛。
5.4 jQuery 与其他库的 $ 符号冲突
现象:页面里同时引用了另一个也使用$的库,结果 jQuery 代码报$ is not a function。
原因:后加载的库覆盖了$全局引用。jQuery 默认把$和jQuery都暴露到全局,后加载者覆盖后,之前绑定的代码在运行时就会出错。
解决:在脚本开头把 jQuery 重新赋值给本地变量:
(function ($) { $(function () { // 原有代码全部放在这里 }); })(jQuery);这是一个 IIFE,参数$接收jQuery对象,内部所有$引用都指向 jQuery,不受外部干扰。注意jQuery这个名字必须在使用前已经加载,否则jQuery本身也是 undefined。还有一种做法是$.noConflict()释放$,但 IIFE 的作用域隔离更干净,不会影响页面里其他引用$的脚本。
6. 进阶:用 IntersectionObserver 替换 scroll 监听,彻底去掉滚动计数
滚动监听这套逻辑跑通之后,你会发现一个限制:节流只管频率,管不了触发方式。图片懒加载、导航栏状态更新都依赖scrollTop计算,每次滚动都要读取一堆位置值,这在桌面端问题不大,但在低端安卓机的 WebView 里仍然会有可感知的掉帧。进阶做法是用 IntersectionObserver 替换掉大部分 scroll 监听,浏览器底层自己判断元素是否进入视口,回调是异步且按帧批量触发的,开发者的代码不用关心滚动位置。
保持原有 HTML 结构不变,只改 JS 的初始化部分:
var nav = $("#appleNav")[0]; var hero = $(".hero")[0]; var observer = new IntersectionObserver(function (entries) { entries.forEach(function (entry) { if (entry.isIntersecting) { nav.classList.add("is-transparent"); } else { nav.classList.remove("is-transparent"); } }); }, { rootMargin: "0px 0px -50px 0px", threshold: 0.1 }); observer.observe(hero);IntersectionObserver的构造函数接收两个参数:回调函数和配置项。回调里的entries是观察对象的状态数组,isIntersecting为真表示元素正在视口内。rootMargin扩展视口的判定边界,0px 0px -50px 0px表示视口底部收缩 50 像素,相当于之前的heroBottom - 50缓冲值。threshold: 0.1表示 Hero 10% 的区域可见时触发回调。
这个方案的改动点是:导航栏状态的判定从“滚动位置和 Hero 底部比较”变成“Hero 是否还在视口内”,省掉了scrollTop读取和节流函数,代码量少一半。原来的throttle和updateNavOnScroll都不再需要。懒加载也可以换成同样的观察器,图片进入视口时读取dataset.src赋值,语言更接近“声明式”。
我实际替换之后发现一个边界:rootMargin的负值在不同设备上的表现略有差异,手头测试机里有的在 Hero 露出 20% 时才触发,有的 10% 就触发。解决方法是把threshold调成[0, 0.1, 0.2]数组,观察多个比例交点,把切换点细化为分级。从那以后我每次接手页面优化,都会先翻一遍 JS 里绑了几个 scroll 监听、每个监听内部读了几次位置属性,能用观察器解决的绝不硬扛滚动计数。这套资源的价值不在代码量,在于它把“改哪里、为什么这改、改完怎么验”整个路径走通了,照着这个思路练一次,比单纯抄一百行 jQuery 有用得多。希望帮到你。
本文还有配套的精品资源,点击获取