主题
放大镜区域-效果实现01与放大镜区域-效果实现02(Day20 2/3)
本页对应 Day20_PC项目,共 6 段视频,约 1 小时 54 分。
学习目标
- 能够实现放大镜蒙层与大图的显示/隐藏联动,并说明鼠标移入、移出时对应事件绑定与样式切换的检验结果。
- 能够使用鼠标视口坐标减去元素视口坐标计算蒙层位置,避免
offsetX/offsetY因目标元素变化导致位置跳动,并通过连续移动验证蒙层稳定跟随。 - 能够完成蒙层居中修正与边界限制,并验证蒙层始终限制在小图区域内且不超出左右、上下边界。
- 能够按大小图放大比例同步控制大图滚动位置,并验证蒙层区域内容与大图显示内容一致。
- 能够根据数据动态创建缩略图、完成左右按钮切换与边界判断,并验证缩略图可平滑移动且到边界后不再越界偏移。
- 能够为缩略图左右切换事件添加节流,验证高频点击时过渡未完成前不会重复触发,且正常间隔点击仍可连续切换。
知识点
放大镜的显示与隐藏
为什么需要:放大镜区域默认不应出现,只有鼠标进入小图区域才显示蒙层和大图,离开时隐藏,交互才符合直觉。
怎么想:先在小图区域监听进入和离开事件,进入时把蒙层、大图设为 display:block,离开时设为 none。
怎么做:
js
smallImageBox.onmouseenter = function () {
maskBox.style.display = 'block';
largeImageBox.style.display = 'block';
};
smallImageBox.onmouseleave = function () {
maskBox.style.display = 'none';
largeImageBox.style.display = 'none';
};易错点:事件名不要写错,转写稿中曾把 mousemove 写错导致完全不触发;隐藏和显示要成对处理,否则蒙层会残留。
蒙层跟随鼠标:坐标不能用 offsetX
为什么需要:蒙层要跟着鼠标在小图区域内移动,必须先拿到鼠标相对小图的位置。
怎么想:最初用 event.offsetX / offsetY,结果蒙层来回跳。原因是 offsetX 相对“目标元素”计算,而目标元素可能是小图,也可能是刚显示出来的蒙层,取值基准不断变化。
怎么做:改用视口坐标相减:鼠标在视口中的位置 clientX/clientY 减去小图在视口中的位置 getBoundingClientRect()。
js
smallImageBox.onmousemove = function (event) {
const rect = smallImageBox.getBoundingClientRect();
let left = event.clientX - rect.left;
let top = event.clientY - rect.top;
};易错点:不能混搭“视口坐标”和“页面坐标”。转写稿强调,网上不少放大镜案例把 clientX 和 offsetLeft 混用,页面一滚动就会错位。
蒙层居中与边界限制
为什么需要:鼠标位置若直接赋给蒙层左上角,蒙层会偏在鼠标右下方;同时蒙层不能跑出小图区域。
怎么想:让蒙层中心与鼠标重合,所以 left/top 要减去蒙层自身宽高的一半;再用小图尺寸限制范围。
怎么做:
js
left -= maskBox.offsetWidth / 2;
top -= maskBox.offsetHeight / 2;
left = Math.max(0, Math.min(left, smallImageBox.clientWidth - maskBox.offsetWidth));
top = Math.max(0, Math.min(top, smallImageBox.clientHeight - maskBox.offsetHeight));
maskBox.style.left = left + 'px';
maskBox.style.top = top + 'px';易错点:边界最大值要用小图的可视宽高减去蒙层宽高;不要忘记单位 px。
大图联动:按比例移动
为什么需要:蒙层显示小图局部时,大图也要显示对应局部。
怎么想:小图 400×400,大图 800×800,放大 2 倍,所以蒙层移动多少,大图内容就按 2 倍反向滚动。
怎么做:用大图包裹元素的 scrollLeft/scrollTop:
js
largeImageBox.scrollLeft = left * 2;
largeImageBox.scrollTop = top * 2;易错点:scrollLeft 改的是包裹元素,不是图片本身;比例要与图片实际尺寸一致,不能凭感觉写。
缩略图布局与动态创建
为什么需要:缩略图不能写死,要根据数据生成,后续才能联动切换大图。
怎么想:先搭结构:左右按钮、中间显示区、内部长条包裹层;长条包裹层定位,父级 overflow:hidden,再根据数据创建 img。
怎么做:
js
goodData.imageSrc.forEach(item => {
const img = new Image();
img.src = item.s;
thumbWrapper.appendChild(img);
});易错点:appendChild 报“不存在”时,先检查获取元素是否成功;转写稿中就是类名写错导致 thumbWrapper 为 null。
左右移动、边界与事件节流
为什么需要:点击左右按钮要一次移动两张图,且不能越界;用户快速连点时,过渡未完成会继续叠加位置,导致偏移。
怎么想:单张图占位 = 图片宽度 + 右外边距;一次移动两张,所以乘 2。最小值不能直接用中间容器宽度算,因为容器 372px 放不下 5 张图占位 380px,应按“单图占位×5”模拟计算。节流则记录上次触发时间,400ms 内直接 return。
怎么做:
js
const step = thumbWrapper.children[0].offsetWidth
+ parseInt(getStyle(thumbWrapper.children[0], 'margin-right'));
nextBtn.onclick = function (event) {
if (event.timeStamp - nextBtn.time <= 400) return;
nextBtn.time = event.timeStamp;
let left = thumbWrapper.offsetLeft - step * 2;
const minLeft = -(thumbWrapper.offsetWidth - step * 5);
if (left < minLeft) left = minLeft;
thumbWrapper.style.left = left + 'px';
};易错点:节流记录时间后要更新 nextBtn.time,否则判断永远不生效;边界判断要转清正负号,向左移动时 left 会越来越小、越来越负。
先确定放大比例:小图动多少,大图动多少
为什么需要:蒙层已经能跟着鼠标在小图区域里移动了,但如果大图不动,放大镜就只剩一个半透明框,右侧看不到对应区域的放大内容。要让“蒙层框住哪里,大图就显示哪里”,必须先确定小图与大图的尺寸比例。
怎么想:转写中给出的图片尺寸是小图 400×400,大图 800×800,所以大图是小图的 2 倍。也就是说,小图这边移动 100 像素,大图内容要移动 200 像素。这个比例不是随便写的,而是由图片实际尺寸决定的;本例比例就是 2。
怎么做:先把这个比例记清楚,后面把蒙层位置映射到大图滚动位置时统一乘 2。不要一会儿乘 2、一会儿乘 1,否则蒙层和大图会对不上。
易错点:比例要看“大图尺寸 ÷ 小图尺寸”,不是看蒙层尺寸。蒙层大小和小图大小也有关系,但本节真正用于大图联动的是大小图比例。若换图后尺寸变化,比例也应重新确认,不能无脑照抄 2。
为什么改大图包裹元素的 scrollLeft / scrollTop
为什么需要:大图比它的显示容器大,容器只露出其中一块区域。要让大图“在框里移动”,本质是改变容器内部内容的滚动位置,让不同区域露出来。
怎么想:转写里提到三种思路:给大图定位、把大图换成背景图改 background-position、或者改包裹元素的 scrollLeft 和 scrollTop。本节采用第三种:大图是包裹元素的内容,改包裹元素的滚动位置,就能让大图在显示框中移动。
怎么做:拿到大图包裹元素,例如 largeImageBox,在蒙层位置更新后设置:
js
// left、top 是蒙层相对小图区域的位置
largeImageBox.style.scrollLeft = left * 2;
largeImageBox.style.scrollTop = top * 2;易错点:scrollLeft、scrollTop 是数值属性,不要拼 "px";它们改的是滚动内容的位置,不是 CSS 的 left/top。另外,大图包裹元素必须能形成“只显示一块”的效果,通常需要 overflow: hidden,否则滚动或溢出表现会不对。
把蒙层位置映射成大图滚动位置
为什么需要:蒙层位置表示“用户正在看小图的哪一块”,大图滚动位置表示“大图应该露出哪一块”。两者必须一一对应,放大镜才自然。
怎么想:蒙层的左上角到了小图的某个位置,大图里同一内容的位置是它的 2 倍。因此不需要再重新算鼠标坐标,直接把已经算好并限制过边界的蒙层 left/top 拿来乘比例即可。转写中也是先算蒙层位置,再让大图跟着这个位置走。
怎么做:在鼠标移动事件里,更新完蒙层位置后紧接着更新大图:
js
maskBox.style.left = left + "px";
maskBox.style.top = top + "px";
largeImageBox.style.scrollLeft = left * 2;
largeImageBox.style.scrollTop = top * 2;易错点:不要把鼠标的 clientX/clientY 直接乘 2 塞给大图;也不要把视口坐标、页面坐标、元素坐标混搭。本节用的是蒙层相对小图区域的位置,这个位置已经处理过“鼠标在蒙层中间”和边界限制,直接乘比例最稳。
验证与调试:看蒙层和大图是否同步
为什么需要:只写代码不看效果,很容易出现“蒙层动了,大图方向反了”或“比例不对”的问题。
怎么想:验证标准是:蒙层框住小图的哪个区域,大图容器里就应显示同一区域的放大版。转写中用“小图上的小人和大图上的小人”来帮助推导:蒙层显示头部,大图也应显示头部所在区域。
怎么做:刷新页面后缓慢移动鼠标,观察蒙层左上角与大图内容的对应关系;必要时在控制台输出 left、top、scrollLeft、scrollTop,确认它们是否按 2 倍关系变化。
易错点:如果大图方向相反,先检查是不是把 scrollLeft 当成了 CSS left;如果大图完全不动,检查大图包裹元素是否获取正确、是否真的设置了滚动容器;如果一动就跳,回到上一节检查蒙层位置是否还在混用 offsetX/offsetY 与目标元素。最后记住:本节的关键就是“蒙层位置算好后,大图滚动位置 = 蒙层位置 × 放大比例”。
先明确本节目标:先把缩略图区域“搭稳”
本节笔记未附图片,下面只整理转写中与“商品预览缩略图-布局”直接相关的结构、样式和调试思路。
为什么需要:缩略图不是简单把几张图片排一行。后面要实现左右按钮滑动、点击缩略图切换大图,所以布局阶段必须先把“可视窗口”和“可移动长条”分开。
怎么想:整个缩略图区域放在 product-preview 下,内部拆成三块:左按钮、中间图片显示区、右按钮。中间再套一层长条容器,长条里放所有缩略图。
怎么做:先写 HTML 结构,再写 LESS。左右按钮用 button,中间用 thumb-content 作为固定可视区,里面放 thumb-wrapper 承载图片。
易错点:不要一上来就把图片直接塞进固定宽度容器里;否则后续要整体移动时没有“长条”可动。
html
<div class="product-preview">
<div class="thumb">
<button class="thumb-prev"><</button>
<div class="thumb-content">
<div class="thumb-wrapper">
<img src="./images/s1.png" alt="商品缩略图1">
<img src="./images/s2.png" alt="商品缩略图2">
<img src="./images/s3.png" alt="商品缩略图3">
<!-- 转写中用 s1、s2、s3 重复补足数量 -->
</div>
</div>
<button class="thumb-next">></button>
</div>
</div>三块横向排列:用 flex 做两端对齐
为什么需要:左右按钮要贴两边,中间缩略图区在中间,最自然的是横向弹性布局。
怎么想:外层 .thumb 设置 display:flex,再用 justify-content:space-between 把三块分开。
怎么做:.thumb 加上边距 5px,宽度不用重复设置,因为父级 product-preview 已经是 400。
易错点:转写中一开始图片特别大,把按钮挤得很难看;这不是 flex 错,而是图片还没有限制尺寸。布局和图片尺寸要分步处理。
less
.thumb {
margin-top: 5px;
display: flex;
justify-content: space-between;
}左右按钮:宽高、背景和鼠标样式
为什么需要:按钮是滑动入口,必须有稳定尺寸,不能靠文字撑开。
怎么想:转写参数里按钮宽 10px、高 54px,背景 #EBEBEB,还有边框要求;文字默认会居中,再补一个“小手”提示可点击。
怎么做:统一写 .thumb button,左按钮再加右外边距 4px。
易错点:按钮文字是 <、>,不是普通小于号;类名要统一,转写里出现过 THUMB、thumb 混写导致选择器取不到元素的问题。
less
.thumb button {
width: 10px;
height: 54px;
border: 1px solid;
background: #EBEBEB;
cursor: pointer;
}
.thumb-prev {
margin-right: 4px;
}中间可视区与长条容器:为后续滑动做准备
为什么需要:缩略图总宽度大于可视宽度,后面要整体左右移动,所以必须有“窗口”和“内容长条”。
怎么想:.thumb-content 是窗口,尺寸按参数 372 × 56;.thumb-wrapper 是长条,用绝对定位放在窗口内,初始 left:0; top:0,由内部图片撑开宽度。
怎么做:窗口设 position:relative 和 overflow:hidden;长条设 position:absolute,并 display:flex 防止图片换行、避免行内空格影响排列。
易错点:忘记 overflow:hidden,长条会露到外面;忘记父级相对定位,绝对定位会参照别的祖先;长条不设 flex,图片可能竖排或受空格影响。
less
.thumb-content {
width: 372px;
height: 56px;
position: relative;
overflow: hidden;
}
.thumb-wrapper {
position: absolute;
left: 0;
top: 0;
display: flex;
}单个缩略图尺寸:50×50、边框、内边距和右外边距
为什么需要:图片项尺寸决定一次移动多少,也决定最后边界是否准确。
怎么想:转写参数:每张图宽高 50px,边框 1px solid #ccc,内边距 2px,右外边距 20px。默认盒模型下,单张图实际占宽是 50 + 2*2 + 1*2 = 56,再加右外边距 20,一个图项占位 76。
怎么做:给 .thumb-wrapper img 写尺寸、边框、内边距和右外边距。
易错点:不要只算 50;边框、内边距、外边距都占位置。转写后面算出单图占位 76,五张就是 380,而 thumb-content 参数只有 372,所以后续做边界时不能直接拿 372 当五张图的总宽,否则最后会偏。本节先按布局参数写,但心里要记住这个隐患。
less
.thumb-wrapper img {
width: 50px;
height: 50px;
border: 1px solid #ccc;
padding: 2px;
margin-right: 20px;
}本节布局完成后的检查点
为什么需要:布局写完不是“看起来差不多”就行,要能支撑后续效果。
怎么想:检查三件事:三块是否横向两端对齐;中间窗口是否只露出固定区域;长条是否由图片横向撑开且能被整体改 left。
怎么做:浏览器里检查 .thumb-wrapper 的实际宽度、单图 offsetWidth、右外边距,以及窗口是否 overflow:hidden。
易错点:转写中曾出现类名写错、选择器取不到元素、图片太大、按钮难看等问题。遇到“元素不存在”一类报错,先检查获取元素的类名和选择器,而不是先怀疑后面的定位逻辑。
课堂画面






代码示例
常见错误
蒙层跟随鼠标时来回抖动
现象:鼠标移动时蒙层位置反复跳变。
原因:使用offsetX/offsetY,目标元素在smallImageBox与蒙层之间切换。
修法:统一用“鼠标视口坐标 − 元素视口坐标”计算鼠标在smallImageBox内的位置。页面出现滚动条后放大镜位置偏移
现象:无滚动条时正常,滚动后坐标不准。
原因:鼠标坐标与元素坐标混搭了视口坐标和页面坐标。
修法:统一坐标体系;推荐鼠标用clientX/clientY,元素用getBoundingClientRect()。蒙层左上角贴着鼠标,观感不对
现象:鼠标不在蒙层中心。
原因:计算出的left/top直接赋给蒙层,未减去蒙层自身宽高的一半。
修法:left/top分别减去蒙层offsetWidth/2、offsetHeight/2,再做边界夹取。蒙层能移出小图区域,鼠标离开后效果异常
现象:蒙层跑出边界,甚至“离不开”小图区域。
原因:只设置位置,未限制left/top的最小值和最大值。
修法:left夹在0到smallImageBox.clientWidth - maskBox.offsetWidth;top同理。大图与蒙层显示区域不同步
现象:蒙层里的内容和大图区域对不上。
原因:大图移动量未按放大比例计算,或scrollLeft/scrollTop正负方向处理错误。
修法:按大小图比例设置大图包裹元素的scrollLeft/scrollTop,并验证正负方向。缩略图点到最后一张时左侧对不齐
现象:临界位置整体偏位。
原因:用容器实际宽度372计算边界,但 5 张图实际占位为76 × 5 = 380。
修法:边界按“单图占位 × 可视数量”计算,而不是直接用容器宽度。快速连点左右按钮后位置错乱
现象:连续点击后缩略图偏移,不再按整张图移动。
原因:过渡动画未结束就继续基于当前位置累加。
修法:对点击回调做节流,记录上次触发时间,间隔不足时直接return,触发后更新时间。动态创建缩略图时报
appendChild为 null 或不显示
现象:控制台报错,缩略图未插入。
原因:选择器类名/ID 拼写错误,获取到的包裹元素为null。
修法:先输出包裹元素核对选择器,再执行创建与插入。
练习
- 复现“蒙层抖动”问题,并改用视口坐标体系修复,要求页面出现滚动条后仍准确。
- 为蒙层位置补齐上下左右边界限制,验证鼠标从四个方向移出小图区域时的表现。
- 实现大图与蒙层联动,要求蒙层移动量与大小图放大比例一致,并检查正负方向。
- 修正缩略图临界位置偏移问题,要求按“单图占位 × 可视数量”计算最小
left。 - 为左右按钮添加节流,要求快速连点时不发生半途累加偏移。
原视频对照
| # | 视频 | 时长 |
|---|---|---|
| 1 | 放大镜区域-效果实现01 | 32:21 |
| 2 | 放大镜区域-效果实现02 | 10:26 |
| 3 | 商品预览缩略图-布局 | 11:25 |
| 4 | 商品预览缩略图-效果实现01 | 11:25 |
| 5 | 商品预览缩略图-效果实现02 | 34:47 |
| 6 | 商品预览缩略图-效果实现03-事件节流 | 14:07 |
小结
- 放大镜类效果首先要保证坐标体系统一,否则滚动、嵌套元素都会引发偏移。
- 蒙层位置必须同时处理“居中修正”和“边界夹取”,不能只做跟随。
- 大图移动依赖蒙层位置与放大比例,方向需通过实际效果验证。
- 缩略图移动量应包含图片宽度与右外边距,边界应按实际占位计算。
- 容器宽度不足时,不要直接用容器宽度做临界判断,应使用虚拟占位尺寸。
- 带过渡动画的交互必须考虑用户快速连点,节流是必要保护。
- 动态 DOM 操作前先确认选择器与目标元素,避免把错误定位到样式或逻辑层。
- 边界计算涉及负值时,先明确“当前值增大还是减小”,再决定加减与比较方向。