主题
2-11综合案例-顶部导航案例
本页对应 第二章WebAPIs,共 6 段视频,约 57 分钟。
学习目标
- 能使用页面滚动事件、
offsetTop与classList实现吸附顶部导航栏的显示与隐藏,并验证滚动到指定位置时导航栏滑入、离开时隐藏。 - 能解释并实现“仿 Bilibili 滑动效果”:通过事件委托获取被点击链接的
offsetLeft,使用transform: translateX()移动滑块,并添加过渡动画。 - 能使用排他思想完成电梯导航点击高亮效果,并通过判断是否已存在
active元素,结合逻辑与中断避免首次移除报错。 - 能实现点击电梯导航小模块后页面滑动到对应大模块位置,并使用
scrollTop = offsetTop - offsetHeight避免固定导航栏遮挡标题。 - 能实现页面滚动时电梯导航小模块自动高亮:遍历大模块判断滚动位置,先清除旧高亮,再为对应小模块添加
active。 - 能调试电梯导航综合案例中的常见问题,包括首次无高亮元素报错、固定导航遮挡导致判断不准确、滚动回顶部后高亮未取消。
知识点
DOM 树与节点类型
为什么需要:HTML 写出来是嵌套结构,浏览器要能按层级找到、修改、增加、删除页面内容,就必须把文档解析成一棵可操作的对象树。DOM 把 HTML 文档以树状结构表现出来,称为 DOM 树或节点树。节点是树中的单个点,文档本身、元素、文本、注释都属于节点。重点掌握元素节点:所有标签如 body、div,其中 html 是根节点;属性节点如 href;文本节点如标签中的文字。

怎么想:先建立“树”的空间感:父节点在上,子节点在下,兄弟节点同级。查找、增加、删除都围绕“节点对象”进行。怎么做:用 document.querySelector() 先定位元素,再通过节点关系属性扩展。易错点:节点不等于标签字符串,查找到的结果是对象;文本节点、注释节点也会存在于节点树中,但日常布局操作多以元素节点为主。
查找父节点
为什么需要:有时拿到的是子元素,却要控制它外面的父盒子,例如点击关闭按钮隐藏整个弹层。怎么想:子元素对象上有 parentNode,可直接取父节点。怎么做:
JavaScript
元素.parentNodehtml
<body>
<div class="pop">
<a href="javascript:;" class="close"></a>
</div>
<script>
// 点击关闭按钮可以关闭父盒子
const closeBtn = document.querySelector('.close')
// 利用孩子选取父节点,返回的也是一个元素对象
console.log(closeBtn.parentNode)
closeBtn.addEventListener('click', function () {
this.parentNode.style.display = 'none'
})
</script>
</body>易错点:parentNode 可能取到文档等非元素节点;若只想取元素父节点,要理解节点类型。转写稿中“西服”应纠正为“吸附”,“打号栏”应纠正为“导航栏”,这类案例本质仍是先查找元素节点,再控制其类名或样式。
查找子节点与兄弟节点
为什么需要:列表、导航、卡片组中经常要批量取子元素,或从当前项找前后兄弟。怎么想:父元素用 children 取元素子节点集合;当前元素用 previousElementSibling、nextElementSibling 取前后元素兄弟。怎么做:
javascript
父元素.childrenhtml
<body>
`<ul>`
<li>我是第1个孩子</li>
<li>我是第2个孩子</li>
<li>我是第3个孩子</li>
<li>我是第4个孩子</li>
</ul>
<script>
// 1. 查询子节点
const ul = document.querySelector('ul')
console.log(ul.children)
// 2. 查询兄弟节点
const li2 = document.querySelector('ul li:nth-child(2)')
console.log(li2.previousElementSibling) // 上一个兄弟
console.log(li2.nextElementSibling) // 下一个兄弟
console.log(ul.children[0]) // 第一个孩子
console.log(ul.children[2]) // 第三个孩子
</script>
</body>易错点:children 是集合,不是数组,不能直接 forEach 时要注意兼容或转换;previousSibling 可能拿到文本节点,找元素兄弟优先用 previousElementSibling。转写稿里的电梯导航用“小模块控制大模块”,推导时也是先按索引找到对应元素节点,再操作类名;其中“受/收/瘦”应结合语境理解为控制显示类名,不能与删除节点混淆。
增加节点
为什么需要:发布信息、追加列表项时,需要在页面中动态增加元素。怎么想:先创建新节点,再把它放入指定父元素内部;放最后用 append,放最前用 prepend。怎么做:
javascript
element.append()
element.prepend()html
<body>
<ul>
<li>我是小li</li>
</ul>
<script>
// 1. 创建节点
const li = document.createElement('li')
li.innerHTML = '我是放到后面的'
console.log(li)
// 2. 追加给父元素
const ul = document.querySelector('ul')
// 2.1 append 放到ul 的最后面 类似css的 after伪元素
ul.append(li)
// 2.2 prepend放到 ul 的最前面 类似css的 before伪元素
const firstli = document.createElement('li')
firstli.innerHTML = '我是放到前面的'
ul.prepend(firstli)
</script>
</body>易错点:append/prepend 插入的是节点对象,不是 HTML 字符串;同一个节点重复插入会移动位置。转写稿中调试“初次没有 active 类直接移除会报错”时,用 old && old.classList.remove('active') 利用逻辑与中断避免误删,这提示我们:操作节点前先确认查找结果是否存在。
删除节点
为什么需要:元素不再需要时,要从 DOM 树中移除。怎么想:删除与隐藏不同,隐藏只是看不见,节点还在;删除是真正从 DOM 树去掉。怎么做:
JavaScript
element.remove()html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>删除节点</title>
</head>
<body>
<div class="remove">我要删除</div>
<div class="none">我要隐藏</div>
<script>
// 1. 删除节点, remove 会从dom树中删除这个元素
const remove = document.querySelector('.remove')
remove.remove()
// 2. display:none 隐藏元素,页面看不见,但是dom树中还存在这个标签
const none = document.querySelector('.none')
none.style.display = 'none'
</script>
</body>
</html>易错点:element.remove() 删除节点,classList.remove() 只移除类名,二者不要混用;删除前要确保元素已查到。转写稿的调试过程说明:先 querySelector 判断有没有目标,再执行移除类名或样式控制,能减少“空对象报错”。
为什么移动端要单独认识 touch 事件
为什么需要:移动端没有鼠标,用户主要用手指或触控笔操作屏幕。桌面端常用的 click 虽然也能用,但它和“手指按下、滑动、抬起”不是一回事。移动端要做出跟手的效果,就要直接监听触屏事件。
怎么想:先把“触摸”拆成三个动作:按下、移动、抬起。对应就是 touchstart、touchmove、touchend。Android 和 iOS 都支持 touch 事件。
怎么做:先不要急着写复杂逻辑,只绑定事件并打印,确认事件真的触发。
易错点:不要用鼠标事件的经验硬套 touch;touchmove 不是只触发一次,而是滑动过程中持续触发。
三个基础触屏事件
笔记里的常见触屏事件表如下:

touch 对象代表一个触摸点。触摸点可能是一根手指,也可能是一根触摸笔。触屏事件可响应用户手指(或触控笔)对屏幕或者触控板操作。
为什么需要:只有先认清事件顺序,后面做滑动、拖拽、移动端轮播才有基础。
怎么想:touchstart 是“开始摸”,touchmove 是“一直摸”,touchend 是“摸完了”。先验证这三个阶段是否按顺序出现。
怎么做:保留笔记中的最小案例:
html
<body>
<div class="box"></div>
<script>
// 触摸事件
const box = document.querySelector('.box')
// 1. 手指触屏开始事件 touchstart
box.addEventListener('touchstart', function () {
console.log('我开始摸了')
})
// 2. 手指触屏滑动事件 touchmove
box.addEventListener('touchmove', function () {
console.log('我一直摸')
})
// 3. 手指触屏结束事件 touchend
box.addEventListener('touchend', function () {
console.log('我摸完了')
})
</script>
</body>易错点:如果 .box 没有宽高或被别的元素盖住,手指可能根本摸不到它。先给 .box 一个可见面积,再测试事件。
touch 对象与触摸点
为什么需要:移动端可能多指操作,也可能用触控笔。事件对象里需要知道“当前是哪个触摸点”。
怎么想:touch 对象代表一个触摸点,一根手指或一根触控笔就是一个触摸点。多指时会有多个触摸点。初学阶段先建立“事件对象里能拿到触摸点信息”的意识,常见入口是 event.touches。
怎么做:可以在回调里先打印事件对象,观察触摸点集合是否存在,再决定后续取坐标。
易错点:不要把“触摸点数量”和“事件触发次数”混为一谈;也不要在还没验证事件触发时,就直接写复杂坐标计算。
最小案例的调试过程
为什么需要:touch 事件在桌面浏览器里直接用鼠标点,通常不会触发;很多同学以为代码写错,其实是测试方式不对。
怎么想:先确认环境,再确认元素,再确认事件。调试顺序应是:打开移动端模拟或真机 → 找到 .box → 手指按下、滑动、抬起 → 看控制台输出。
怎么做:在 Chrome 开发者工具中切换到移动设备模拟,或使用真机调试。按下时看是否输出“我开始摸了”;滑动时看是否连续输出“我一直摸”;抬起时看是否输出“我摸完了”。
易错点:touchmove 会高频触发,控制台刷屏是正常现象;不要因为输出很多就以为代码坏了。真正做业务时,也不要在 touchmove 里做大量 DOM 操作,否则容易卡顿。
从转写稿借来的调试习惯与同音字校正
需要说明:转写稿主要记录的是后面的滚动、吸附导航和电梯导航案例,并不是 touch 事件本身。因此这里只借用它的调试习惯,不把滚动案例硬套到 touch 上。
转写稿里反复出现“先打印一下看看结果”“先看控制台有没有报错”“先验证再改逻辑”。这个习惯同样适用于 touch:先打印三个事件有没有触发,再写滑动逻辑。转写稿中的同音字也要按上下文校正,例如“西服顶部导航栏”应理解为“吸附顶部导航栏”,“打号栏”应理解为“导航栏”,“受/收”要结合代码理解为显示类名,“N”指被卷去的头部/滚动距离。做 touch 调试时也一样:先根据控制台和实际现象校正判断,不要凭感觉猜。
易错点清单
- 桌面鼠标直接点通常不触发 touch,要用移动端模拟或真机。
.box没宽高、被遮挡,会导致摸不到。touchmove是连续触发,不是一次触发。- 先打印验证,再写业务;不要一上来就阻止默认滚动。
- touch 对象代表触摸点,可能来自手指,也可能来自触控笔;多指时要意识到有多个触摸点。
插件:先会用,再理解
插件就是别人已经写好、经过一定验证的代码。我们把它引入项目,按文档完成配置,就能较快实现轮播、手势、弹层等效果。
为什么需要?因为实际开发不可能所有功能都从零手写,插件能节省时间,也能借鉴成熟实现。
怎么想?不要一上来就改插件源码,先判断“它解决什么问题、我的需求是否匹配”。
怎么做?先看官网和示例,再复制最小可用代码跑通。
易错点:不要只看博客片段,版本、引入顺序、初始化时机不对,都会导致插件不生效。
学习插件的四步法
笔记给出的思路很实用:
- 看官网,了解这个插件可以完成什么需求:https://www.swiper.com.cn/
- 查看基本使用流程:https://www.swiper.com.cn/usage/index.html
- 写个小 demo,对照在线演示找到符合自己需求的例子:https://www.swiper.com.cn/demo/index.html
- 再应用到开发中。
为什么需要这四步?因为插件的“配置项很多”,但项目往往只用其中一小部分。先跑通最小 demo,能快速排除路径、结构、初始化顺序等问题。易错点是把官网示例整段复制进项目却不改选择器和结构,最后误以为插件有 bug。
AlloyFinger:轻量级手势插件
AlloyFinger 是腾讯 AlloyTeam 开源的超轻量级 Web 手势插件,可以为元素注册各种手势事件。
GitHub 地址:https://github.com/AlloyTeam/AlloyFinger
使用步骤:
- 下载 js 库:http://alloyteam.github.io/AlloyFinger/alloy_finger.js
- 将 AlloyFinger 库引入当前文件:
<script src="alloy_finger.js"></script>
或使用在线地址:<script src="https://unpkg.com/[email protected]/alloy_finger.js"></script> - 配置:
javascript
new AlloyFinger(element, { // element 是给哪个元素做滑动事件
swipe: function (e) {
// 滑动的时候要做的事情 e.direction 可以判断上下左右滑动 Left Right 等
}
})为什么需要?移动端手势判断自己写会涉及 touchstart、touchmove、touchend 和方向计算,插件把这些封装好了。
怎么想?先确定给哪个元素绑定,再在回调里根据 e.direction 做业务。
易错点:element 必须先获取到;脚本要先引入再 new;在线地址要能访问;不要把 swipe 写成普通点击事件。
用吸顶导航理解“位置判断”
转写稿里的“西服顶部导航栏”应理解为“吸附/吸顶导航栏”。核心思路是:监听页面滚动,比较页面被卷去的头部距离和头部盒子的 offsetTop。调试时先打印头部盒子的 offsetTop,转写稿中得到约 53,于是判断 scrollTop >= 53 时给顶部导航盒子添加显示类,否则移除。
为什么需要?因为吸顶效果本质是“滚动到某个位置后显示/隐藏”。
怎么想?先找参照盒子,再比较 document.documentElement.scrollTop 与 盒子.offsetTop。
怎么做?滚动事件里做判断,满足条件添加类,不满足移除类。
易错点:过渡效果放在显示类上更自然;移除类后直接隐藏,不会出现“慢慢滑回去”的别扭效果。
用仿 B 站滑动条理解“点击后移动”
转写稿中的红色滑块必须是一个单独盒子,不能做成下划线。点击哪个链接,就取该链接的 offsetLeft,再设置滑块 transform: translateX(值)。调试时打印各链接的 offsetLeft,得到 0、60、120 等值,说明距离可直接用于移动。
为什么需要?因为滑块要跟着当前选中项移动。
怎么想?点击事件委托给父元素,判断 e.target.tagName === 'A',再取当前 A 的 offsetLeft。
怎么做?line.style.transform = 'translateX(' + e.target.offsetLeft + 'px)',并给滑块加 transition。
易错点:px 不能省;事件委托要判断点击的是不是 A;滑块要单独获取。
用电梯导航理解“排他 + 逻辑与中断”
电梯导航有两块业务:点击小模块高亮并滚动到大模块;页面滚动大模块时反向控制小模块高亮。点击高亮用排他:先移除旧 active,再给当前项添加。转写稿调试时出现“不能设置属性,因为它是空的”,定位到 remove('active'):页面初始没有任何 .small.active,直接移除会报错。解决方法是先查询,再利用逻辑与中断:
javascript
const old = document.querySelector('.small.active');
old && old.classList.remove('active');页面滚动控制高亮时,遍历所有大模块,判断 scrollTop >= big.offsetTop - sticky.offsetHeight,先移除旧高亮,再给对应小模块添加。为什么要减 sticky.offsetHeight?因为固定导航会盖住标题。易错点:移除旧高亮的代码要写在判断前;小模块和大模块要一一对应;固定导航高度最好用 offsetHeight 动态获取,而不是手写固定值。
课件图示








代码示例
示例一:DOM 节点查找 / 增加 / 删除
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DOM 节点操作</title>
<style>
.pop { position: relative; width: 200px; height: 100px; background: #eee; margin-bottom: 20px; }
.close { display: block; width: 20px; height: 20px; background: red; }
</style>
</head>
<body>
<div class="pop">
<a href="javascript:;" class="close"></a>
</div>
<ul>
<li>我是第1个孩子</li>
<li>我是第2个孩子</li>
<li>我是第3个孩子</li>
<li>我是第4个孩子</li>
</ul>
<div class="remove">我要删除</div>
<div class="none">我要隐藏</div>
<script>
// 1. 父节点:点击关闭按钮关闭父盒子
const closeBtn = document.querySelector('.close')
console.log(closeBtn.parentNode) // 返回的是一个元素对象
closeBtn.addEventListener('click', function () {
this.parentNode.style.display = 'none' // this 指向 closeBtn
})
// 2. 子节点 / 兄弟节点
const ul = document.querySelector('ul')
console.log(ul.children) // 只拿到元素节点,不含文本、注释
const li2 = document.querySelector('ul li:nth-child(2)')
console.log(li2.previousElementSibling) // 上一个兄弟
console.log(li2.nextElementSibling) // 下一个兄弟
console.log(ul.children[0]) // 第一个孩子
console.log(ul.children[2]) // 第三个孩子
// 3. 增加节点:先创建,再追加
const li = document.createElement('li')
li.innerHTML = '我是放到后面的'
ul.append(li) // 放到父元素最后面,类似 ::after
const firstli = document.createElement('li')
firstli.innerHTML = '我是放到前面的'
ul.prepend(firstli) // 放到父元素最前面,类似 ::before
// 4. 删除节点
const remove = document.querySelector('.remove')
remove.remove() // 从 DOM 树中真正删除
const none = document.querySelector('.none')
none.style.display = 'none' // 隐藏,但 DOM 树中仍然存在
</script>
</body>
</html>关键行与易错点
元素.parentNode返回的是父元素对象,可以直接改它的style;this.parentNode中的this指向事件绑定的元素本身。父元素.children只返回元素节点集合,纯文本、注释节点不会算进去;而childNodes会把文本节点也算上,容易踩坑。- 兄弟节点要用
previousElementSibling/nextElementSibling,不要用previousSibling/nextSibling,后者会把换行产生的文本节点也算进去。 append()/prepend()是新的插入方法,一次可传多个节点,也可以直接传字符串;老写法是appendChild()(只能放最后,且不能传字符串)。remove()是真正从 DOM 树移除,display:none只是看不见,元素依然在 DOM 树里 —— 两者不是一回事。
示例二:仿 bilibili 滑动效果(事件委托 + offsetLeft + translateX)
js
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>仿 bilibili 滑动效果</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.tab-list {
position: relative;
display: flex;
width: 320px;
height: 44px;
margin: 100px auto;
background: #fff;
border-bottom: 1px solid #eee;
}
.tab-list a {
flex: 1;
line-height: 44px;
text-align: center;
font-size: 14px;
color: #333;
text-decoration: none;
}
/* 小滑块必须是一个单独的盒子,不能做成下划线 */
.line {
position: absolute;
left: 0;
bottom: 0;
width: 80px;
height: 3px;
background: #fb7299;
transition: all .3s; /* 谁做动画就给谁加过渡 */
}
</style>
</head>
<body>
<div class="tab-list">
<a href="javascript:;">首页</a>
<a href="javascript:;">动画</a>
<a href="javascript:;">番剧</a>
<a href="javascript:;">音乐</a>
<span class="line"></span>
</div>
<script>
// 1. 获取父元素,准备事件委托
const list = document.querySelector('.tab-list')
const line = document.querySelector('.line')
// 2. 给父元素注册点击事件
list.addEventListener('click', function (e) {
// 只有点到了 A 才处理
if (e.target.tagName === 'A') {
// 拿到当前点击链接距离左侧的距离
const left = e.target.offsetLeft
// 让滑块水平移动过去
line.style.transform = `translateX(${left}px)`
}
})
</script>
</body>
</html>关键行与易错点
- 事件委托:把点击事件绑在父元素
.tab-list上,通过e.target判断真正点的是谁,比循环给每个a注册更省事。 e.target.tagName返回的是大写标签名,必须写成'A',写成'a'永远不成立。- 核心数据是
e.target.offsetLeft:第一个是 0,第二个是 60,第三个是 120…… 点谁就拿谁距离左侧的距离,喂给滑块即可。 translateX()后面的单位px不能省,translateX(60)是无效的。- 过渡
transition一定要写在.line自己身上,并且transform只在原来的位置上继续移动,不会影响其他元素布局。 - 别用下划线(
border-bottom)冒充滑块 —— 那样没法单独移动,必须是独立盒子。
示例三:电梯导航(点击高亮 + 点击滑动 + 滚动高亮)
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>电梯导航</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { height: 3200px; }
/* 顶部固定导航栏,带边框 */
.sticky {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 80px;
line-height: 80px;
text-align: center;
background: #fff;
border-bottom: 1px solid #ccc;
z-index: 99;
}
/* 大模块 */
.big {
height: 600px;
margin-bottom: 20px;
padding-top: 100px;
text-align: center;
background: #f5f5f5;
font-size: 30px;
}
/* 电梯导航(小模块) */
.small {
position: fixed;
right: 20px;
top: 200px;
width: 80px;
}
.small a {
display: block;
height: 40px;
line-height: 40px;
margin-bottom: 5px;
text-align: center;
font-size: 12px;
color: #333;
text-decoration: none;
background: #fff;
border: 1px solid #ccc;
}
.small a.active {
background: #fb7299;
color: #fff;
}
</style>
</head>
<body>
<div class="sticky">我是固定导航栏</div>
<div class="big">新鲜好物</div>
<div class="big">人气推荐</div>
<div class="big">热门品牌</div>
<div class="big">最新专题</div>
<div class="small">
<a href="javascript:;" class="small-item">新鲜好物</a>
<a href="javascript:;" class="small-item">人气推荐</a>
<a href="javascript:;" class="small-item">热门品牌</a>
<a href="javascript:;" class="small-item">最新专题</a>
<a href="javascript:;">返回顶部</a>
</div>
<script>
// 获取元素:小模块 与 大模块 一一对应
const smalls = document.querySelectorAll('.small-item')
const bigs = document.querySelectorAll('.big')
const sticky = document.querySelector('.sticky')
// ===== 业务一:点击小模块 =====
for (let i = 0; i < smalls.length; i++) {
smalls[i].addEventListener('click', function () {
// 1. 排他:先移除已有的 active(可能不存在,用逻辑与中断避免报错)
const old = document.querySelector('.small .active')
old && old.classList.remove('active')
// 2. 点谁谁高亮
this.classList.add('active')
// 3. 页面滑动到对应的大模块(减去固定导航栏的高度)
document.documentElement.scrollTop =
bigs[i].offsetTop - sticky.offsetHeight
})
}
// ===== 业务二:页面滚动 =====
window.addEventListener('scroll', function () {
const n = document.documentElement.scrollTop
// 先移除所有小模块的高亮
const old = document.querySelector('.small .active')
old && old.classList.remove('active')
// 再判断滚动到了哪个大模块
for (let i = 0; i < bigs.length; i++) {
if (n >= bigs[i].offsetTop - sticky.offsetHeight) {
smalls[i].classList.add('active')
}
}
})
</script>
</body>
</html>关键行与易错点
- 页面滚动事件必须写在
window的scroll里,滚动的距离取document.documentElement.scrollTop,不要写错位置。 old && old.classList.remove('active')是本节最关键的技巧:页面刚打开时没有任何小模块带active,querySelector返回null,直接remove会报错。利用逻辑与中断 —— 前面为假(null)后面就不执行,自然不会报错。- 点击业务用传统 for 循环注册事件,不要用事件委托,否则会和后面的滚动业务互相影响。
- 点击滑动时一定要减去固定导航栏的高度,否则标题会被顶部导航栏盖住。高度用
sticky.offsetHeight动态获取(要带边框所以用offsetHeight,不是clientHeight),这样导航栏高度变了也不用改代码。 - 滚动高亮里同样要减
sticky.offsetHeight,否则滚动到刚好贴合时高亮会“慢一拍”。 - 滚动处理中“先移除全部高亮,再判断添加”这两句的顺序不能颠倒;如果写反了,刚加上的高亮会被下面的代码又清掉。
- 因为
offsetTop是从上到下递增的,越往下滚会同时满足多个条件,实际项目里可再加else { break }(或倒序遍历取第一个满足的),保证同一时刻只亮一个小模块。
常见错误
现象: 电梯导航首次点击时控制台报错,提示无法移除
active。
原因: 页面初始没有小模块带active,直接remove('active')会作用在null上。
修法: 先查询当前active元素,再用old && old.classList.remove('active')或if判空后再移除。现象: 点击电梯导航小模块后,页面滚动位置被固定顶部导航遮住标题。
原因:scrollTop直接等于大模块offsetTop,没有扣除固定导航高度。
修法: 滚动目标改为big.offsetTop - sticky.offsetHeight,高度用 DOM 读取而非手写固定值。现象: 页面滚动切换高亮时,已经到某个大模块但小模块高亮慢半拍或不准确。
原因: 滚动距离判断没有扣除吸顶导航高度。
修法: 判断条件使用window.pageYOffset >= big.offsetTop - sticky.offsetHeight。现象: 电梯导航点击高亮正常,但后续页面滚动高亮业务互相干扰。
原因: 点击小模块使用事件委托,把不相关点击也纳入统一处理,影响后续业务边界。
修法: 该案例点击高亮改用for循环给每个小模块单独注册点击事件,并配合排他思想。现象: 吸顶导航滑出有动画,滑回隐藏时也慢慢滑走,效果不自然。
原因:transition加在基础盒子上,显示和隐藏都触发过渡。
修法: 把过渡写在状态类(如.sticky)上,只有添加状态类时才过渡;移除状态类后直接隐藏。现象: 仿 B 站滑块点击后不动或移动位置错误。
原因: 没有取当前点击A的offsetLeft,或translateX未拼单位、未加过渡。
修法: 事件委托中判断e.target.tagName === 'A',用line.style.transform = `translateX(\${e.target.offsetLeft}px)`,过渡加在滑块上。
练习
- 电梯导航首次点击修复: 基于综合案例,保留“点击谁谁高亮”,要求页面初始无任何
active时点击不报错,并说明你用的判空方式。 - 吸顶导航状态切换: 做一个顶部导航,滚动到头部模块时显示并吸顶,回到上方时隐藏;验收点:显示有过渡、隐藏无多余过渡。
- 仿 B 站滑块: 用事件委托实现点击不同
A链接,底部滑块移动到对应链接下方;验收点:只响应A,位移取offsetLeft,有过渡。 - 移动端手势插件: 引入 AlloyFinger,对某个盒子监听
swipe,在页面输出Left/Right/Up/Down;验收点:引入顺序正确、元素获取正确、方向可读。 - 页面滚动高亮: 在电梯导航中实现滚动到大模块时对应小模块高亮;验收点:扣除固定导航高度、回顶部后取消高亮、切换时不残留多个
active。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 吸附顶部导航栏案例 | 09:40 |
| 2 | 仿bilibili滑动效果 | 11:13 |
| 3 | 综合案例-案例分析 | 06:11 |
| 4 | 综合案例-点击高亮效果 | 12:44 |
| 5 | 综合案例-点击页面滑动到对应位置 | 08:15 |
| 6 | 综合案例-页面滑动电梯导航高亮显示 | 09:31 |
小结
- 节点关系查找要优先使用元素节点相关 API,返回对象后再操作样式或类名。
- 新增节点先创建,再按业务位置选择
append或prepend放入父元素。 - 删除节点与隐藏节点语义不同,业务上要明确是移出 DOM 树还是仅视觉隐藏。
- 移动端交互优先考虑
touchstart/touchmove/touchend或成熟手势插件,不要只依赖 PC 端点击思维。 - 吸顶导航的核心是滚动距离与目标盒子位置比较,过渡效果应绑定在状态类上。
- 滑块跟随效果要围绕“目标元素
offsetLeft+translateX+ 过渡”组织,事件委托必须限定触发元素。 - 电梯导航是双向联动:点击小模块控制页面滚动,页面滚动反控小模块高亮;实现时要统一索引、处理判空、扣除固定导航高度,并在回顶时清除高亮。
- 插件使用按“看官网、走流程、跑最小 demo、再接入业务”的顺序推进,避免直接堆进综合案例排错。