2026前端避坑指南:弹窗遮罩层背景乱滚?3招彻底锁死滚动条

2026前端避坑指南:弹窗遮罩层背景乱滚?3招彻底锁死滚动条
在这里插入图片描述


2026前端避坑指南:弹窗遮罩层背景乱滚?3招彻底锁死滚动条

2026前端避坑指南:弹窗遮罩层背景乱滚?3招彻底锁死滚动条

别整那些虚的,先说说为啥你写的弹窗一出来,背景还能丝滑滚动,用户体验直接负分。咱今天不聊大道理,就聊聊这个让无数前端兄弟头秃的"背景滚动"玄学问题。你以为加个overflow: hidden就完事了?太天真了,iOS和安卓能把你教做人。

这破玩意儿到底啥原理

先给大伙盘盘道。说白了就是DOM层级和事件冒泡那点破事——遮罩层虽然盖住了,但滚动事件没被拦截,浏览器觉得你还能接着滑。特别是移动端,那个惯性滚动简直离谱,手指都抬起来了页面还在跑,跟吃了炫迈似的根本停不下来。

这里有个核心概念得整明白:滚动穿透(Scroll Penetration)。当你在一个固定定位的遮罩层上滑动时,如果事件没被妥善处理,这个滑动事件就会"穿透"到下面的body层,导致背景页面跟着动。iOS的WebKit内核和安卓的Blink内核对这个行为的处理还不一样,所以你在Chrome DevTools里测得好好的,一到真机上直接翻车。

更恶心的是,iOS还有个"橡皮筋效果"(Rubber Band Effect),就是页面滑到顶或底时那个回弹动画。这个效果会让overflow: hidden在某些情况下形同虚设,因为浏览器觉得"用户想滚动,我得给点反馈",于是继续处理滚动事件。

方案一:暴力美学,直接给body动刀子

这是最原始但也最通用的办法。思路简单粗暴:弹窗打开时,把body的overflow设为hidden,顺便记录一下当前的scrollTop;关掉弹窗时再还原回去。听着简单,做起来全是坑。

// 先定义几个变量存状态,别用全局变量污染windowlet scrollLocker ={scrollTop:0,originalStyle:'',isLocked:false};/** * 锁定背景滚动 - 暴力版 * 原理:直接给body加overflow:hidden,让它滚不动 */functionlockBodyScroll(){if(scrollLocker.isLocked)return;// 记录当前滚动位置,这一步很关键,不然后面恢复不了 scrollLocker.scrollTop = window.pageYOffset || document.documentElement.scrollTop;// 保存原始样式,方便后面还原,别直接覆盖,万一body本来就有样式呢const body = document.body; scrollLocker.originalStyle = body.getAttribute('style')||'';// 这里是个骚操作:先给body设个fixed定位,再调top值,不然页面会跳回顶部// 注意要减去滚动距离,这样视觉上页面才不会抖动 body.style.cssText =` position: fixed; top: -${scrollLocker.scrollTop}px; left: 0; right: 0; overflow: hidden; width: 100%; height: 100%; `; scrollLocker.isLocked =true; console.log('🔒 背景已锁定,滚动位置:', scrollLocker.scrollTop);}/** * 解锁背景滚动 - 暴力版 * 坑点:iOS上如果直接还原,会有个诡异的跳动,得用requestAnimationFrame */functionunlockBodyScroll(){if(!scrollLocker.isLocked)return;const body = document.body;// 先清空内联样式,让body回到文档流 body.style.cssText = scrollLocker.originalStyle;// 关键步骤:把页面滚回原来的位置// 这里要用window.scrollTo,别用body.scrollTop,iOS不认 window.scrollTo({top: scrollLocker.scrollTop,behavior:'auto'// 必须用auto,smooth会有延迟,用户能看到滚动动画,很诡异}); scrollLocker.isLocked =false; console.log('🔓 背景已解锁,恢复位置:', scrollLocker.scrollTop);}// 实际使用示例,配合弹窗组件classModalManager{constructor(){this.modal =null;this.isOpen =false;}open(modalElement){if(this.isOpen)return;this.modal = modalElement;// 显示弹窗,这里假设你用了一个CSS类来控制显示隐藏 modalElement.classList.add('active');// 重点:先显示弹窗再锁背景,顺序别反了,不然页面会闪lockBodyScroll();this.isOpen =true;// 加个ESC键关闭,提升体验this.handleEsc=(e)=>{if(e.key ==='Escape')this.close();}; document.addEventListener('keydown',this.handleEsc);}close(){if(!this.isOpen)return;// 先解锁背景,再隐藏弹窗,这样过渡更自然unlockBodyScroll();this.modal.classList.remove('active');this.isOpen =false; document.removeEventListener('keydown',this.handleEsc);}}// 初始化使用const modalManager =newModalManager();// HTML结构示例/* <div> <div> <h2>我是弹窗</h2> <p>背景应该滚不动了</p> <button onclick="modalManager.close()">关闭</button> </div> </div> <button onclick="modalManager.open(document.getElementById('myModal'))"> 打开弹窗 </button> */// CSS配合/* .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 9999; display: none; justify-content: center; align-items: center; } .modal-overlay.active { display: flex; } .modal-content { background: white; padding: 20px; border-radius: 8px; max-height: 80vh; // 内容太多时内部可滚动 overflow-y: auto; -webkit-overflow-scrolling: touch; // iOS惯性滚动 } */

这方案的优缺点咱得唠唠:

优点是真的简单粗暴,兼容性拉满,IE11都能跑(如果你还在伺候IE的话)。而且逻辑清晰,看一眼就懂,维护成本低。

但缺点也很明显——代码像补丁一样难看,而且有个致命bug:iOS Safari上,如果页面有底部固定导航栏(比如常见的tab bar),锁滚动后底部可能会出现白边或者布局错乱。这是因为Safari的底部工具栏高度计算有问题,fixed定位的元素会被工具栏挡住一部分。

还有个坑是软键盘。移动端input获取焦点弹出软键盘时,fixed定位的body会被键盘顶起来,关闭键盘后有时候恢复不了,页面会卡在中间位置。这个坑我踩过无数次,后面会讲怎么修。

方案二:position固定法,投机取巧但有用

这招的核心是把body变成fixed定位,宽度设成100%,这样它就动不了了。但前面说过,这有个致命伤:页面会瞬间跳回顶部,用户刚才看到哪全忘了。除非你配合JS算出偏移量,强行把用户拽回原来的位置,不然就是耍流氓。

/** * 滚动锁定管理器 - Fixed定位版 * 比暴力版更精细,处理了iOS的各种奇葩情况 */classScrollLockManager{constructor(){this.scrollY =0;this.bodyStyleCache =null;this.htmlStyleCache =null;this.isLocked =false;}/** * 锁定滚动 * 注意:这里同时操作body和html,因为有些浏览器滚动条在html上 */lock(){if(this.isLocked)return;// 记录当前滚动位置,用window.scrollY更现代this.scrollY = window.scrollY;const body = document.body;const html = document.documentElement;// 缓存原始样式,注意要深拷贝,不然引用类型会出问题this.bodyStyleCache = body.getAttribute('style');this.htmlStyleCache = html.getAttribute('style');// 计算滚动条宽度,避免页面抖动(从有滚动条到没滚动条,页面会横向扩展)const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth;// 给html也加overflow:hidden,防止某些浏览器在html层滚动 html.style.overflow ='hidden'; html.style.height ='100%';// body的骚操作:fixed定位 + 负margin-top模拟滚动位置// 这样视觉上页面不会跳动,因为body还在原来的位置 body.style.position ='fixed'; body.style.top =`-${this.scrollY}px`; body.style.left ='0'; body.style.right ='0'; body.style.width ='100%'; body.style.height ='100%'; body.style.overflow ='hidden'; body.style.paddingRight =`${scrollBarWidth}px`;// 补偿滚动条宽度,防止页面抖动this.isLocked =true;// 触发自定义事件,方便其他组件监听 window.dispatchEvent(newCustomEvent('scroll:locked',{detail:{scrollY:this.scrollY }}));}/** * 解锁滚动 * 坑点:iOS上必须用setTimeout或者rAF,不然会有视觉跳动 */unlock(){if(!this.isLocked)return;const body = document.body;const html = document.documentElement;// 先恢复样式,但别急着滚动,让浏览器先渲染一帧 body.style.cssText =this.bodyStyleCache ||''; html.style.cssText =this.htmlStyleCache ||'';// 关键:用requestAnimationFrame确保在下一帧渲染前恢复滚动位置// 这样能避免iOS上的"闪屏"问题requestAnimationFrame(()=>{ window.scrollTo(0,this.scrollY);// 再触发个事件 window.dispatchEvent(newCustomEvent('scroll:unlocked',{detail:{scrollY:this.scrollY }}));});this.isLocked =false;}/** * 获取当前锁定状态 */getState(){return{isLocked:this.isLocked,scrollY:this.scrollY };}}// 单例模式,全局就一个实例const scrollLock =newScrollLockManager();// Vue3组合式函数示例,现代前端项目常用import{ onMounted, onUnmounted }from'vue';exportfunctionuseScrollLock(isLocked){// 监听传入的响应式变量watch(isLocked,(locked)=>{if(locked){ scrollLock.lock();}else{ scrollLock.unlock();}});// 组件卸载时自动清理,防止内存泄漏onUnmounted(()=>{if(scrollLock.getState().isLocked){ scrollLock.unlock();}});}// React Hook版本import{ useEffect }from'react';exportfunctionuseBodyScrollLock(isLocked){useEffect(()=>{if(isLocked){ scrollLock.lock();}else{ scrollLock.unlock();}// 清理函数return()=>{if(scrollLock.getState().isLocked){ scrollLock.unlock();}};},[isLocked]);}// 实际组件使用示例(React)functionMyModal({ isOpen, onClose, children }){useBodyScrollLock(isOpen);if(!isOpen)returnnull;return(<div className="modal-backdrop" onClick={onClose}><div className="modal-content" onClick={e=> e.stopPropagation()}>{children}<button onClick={onClose}>关闭</button></div></div>);}

这方案的细节得好好说道说道:

首先,paddingRight补偿滚动条宽度这个操作很重要。PC端页面有滚动条时,滚动条大概占17px宽度(不同系统不一样)。如果你直接overflow: hidden去掉滚动条,页面内容会横向扩展17px,用户能感觉到"抖了一下"。加上paddingRight后,内容区域宽度不变,视觉上无缝切换。

其次,iOS上的requestAnimationFrame trick是血泪教训。如果你直接scrollTo然后恢复样式,有时候页面会闪一下,或者滚动位置不对。用rAF延迟一帧,让浏览器先完成样式计算和布局,再执行滚动,就能避免这个问题。

但这招也有硬伤:如果弹窗里也有滚动内容(比如长列表),这时候body被fixed了,弹窗内的滚动可能会受到影响。特别是iOS的overflow-scrolling: touch,有时候会失效或者卡顿。这时候得给弹窗内容区单独设置-webkit-overflow-scrolling: touch,并且确保它有固定高度。

方案三:touchmove事件拦截,真·高手玩法

前面两招都是"堵"——直接不让body滚动。这第三招是"疏"——精准拦截遮罩层上的滑动事件,只拦背景,不拦弹窗内容。这招最优雅,逻辑清晰,但在某些奇葩安卓机上可能会失效,得做降级处理。

/** * 事件拦截版滚动锁定 * 原理:在遮罩层监听touchmove,阻止默认行为,让事件不冒泡到body * 适用于移动端,PC端还得配合overflow:hidden */classTouchScrollLock{constructor(){this.overlay =null;this.startY =0;this.isLocked =false;this.passiveSupported =false;// 检测浏览器是否支持passive事件}/** * 检测passive事件支持 * 现代浏览器必须用passive: false才能preventDefault,不然控制台会报错 */checkPassiveSupport(){try{const opts = Object.defineProperty({},'passive',{get:()=>{this.passiveSupported =true;returntrue;}}); window.addEventListener('test',null, opts); window.removeEventListener('test',null, opts);}catch(e){this.passiveSupported =false;}}/** * 初始化,绑定事件 * @param {HTMLElement} overlayElement - 遮罩层DOM元素 */init(overlayElement){this.checkPassiveSupport();this.overlay = overlayElement;// 绑定触摸事件,注意第三个参数const eventOptions =this.passiveSupported ?{passive:false}:false;// 遮罩层上的touchmove直接拦截,不让它滚动this.overlay.addEventListener('touchmove',this.handleOverlayTouchMove, eventOptions);// 点击遮罩层关闭弹窗(如果需要的话)this.overlay.addEventListener('click',(e)=>{if(e.target ===this.overlay){this.closeModal &&this.closeModal();}});// 处理弹窗内容的滚动,这里要复杂一点const content =this.overlay.querySelector('.modal-scroll-content');if(content){this.setupContentScroll(content, eventOptions);}}/** * 处理遮罩层的触摸移动 * 直接阻止默认行为,背景就不会滚了 */handleOverlayTouchMove=(e)=>{// 如果触摸的是遮罩层本身(不是内容区),直接拦截if(e.target ===this.overlay){ e.preventDefault();return;}// 如果触摸的是内容区,但内容区已经滚到顶或底了,也要拦截// 不然会触发"滚动穿透"const content = e.target.closest('.modal-scroll-content');if(content){const isAtTop = content.scrollTop <=0;const isAtBottom = content.scrollTop + content.clientHeight >= content.scrollHeight;// 判断滑动方向const currentY = e.touches[0].clientY;const isScrollingUp = currentY >this.startY;const isScrollingDown = currentY <this.startY;// 到顶了还往上滑,或者到底了还往下滑,都拦截if((isAtTop && isScrollingUp)||(isAtBottom && isScrollingDown)){ e.preventDefault();}}}/** * 设置内容区的滚动处理 * 这里要区分"内容滚动"和"背景滚动" */setupContentScroll(contentElement, eventOptions){// 记录触摸起始位置 contentElement.addEventListener('touchstart',(e)=>{this.startY = e.touches[0].clientY;}, eventOptions);// 内容区的touchmove默认不拦截,让它正常滚动// 但要在handleOverlayTouchMove里做边界判断}/** * 锁定滚动(结合CSS) * 移动端主要靠事件拦截,PC端还是靠overflow:hidden */lock(){if(this.isLocked)return;// 移动端方案if('ontouchstart'in window){ document.body.style.overflow ='hidden';// 给body加个类,方便写CSS针对性处理 document.body.classList.add('modal-open');}else{// PC端用传统方案 document.body.style.overflow ='hidden';const scrollBarWidth = window.innerWidth - document.documentElement.clientWidth; document.body.style.paddingRight =`${scrollBarWidth}px`;}this.isLocked =true;}unlock(){if(!this.isLocked)return; document.body.style.overflow =''; document.body.style.paddingRight =''; document.body.classList.remove('modal-open');this.isLocked =false;}destroy(){this.unlock();if(this.overlay){this.overlay.removeEventListener('touchmove',this.handleOverlayTouchMove);}}}// 高级用法:结合现代框架的组件classSmartModal{constructor(options){this.options ={overlaySelector:'.modal-overlay',contentSelector:'.modal-content',onClose:()=>{},...options };this.overlay = document.querySelector(this.options.overlaySelector);this.touchLock =newTouchScrollLock();this.bodyLock =newScrollLockManager();// 复用前面的类this.init();}init(){if(!this.overlay){ console.error('找不到遮罩层元素,检查selector');return;}// 移动端用touch方案,PC端用body lock方案if(this.isMobile()){this.touchLock.init(this.overlay);this.touchLock.lock();}else{this.bodyLock.lock();}// 绑定关闭事件const closeBtn =this.overlay.querySelector('.modal-close');if(closeBtn){ closeBtn.addEventListener('click',()=>this.close());}}isMobile(){return/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent );}close(){if(this.isMobile()){this.touchLock.destroy();}else{this.bodyLock.unlock();}this.options.onClose();this.overlay.classList.remove('active');}}// CSS配合,处理iOS的橡皮筋效果/* .modal-open { overflow: hidden; position: fixed; width: 100%; height: 100%; } .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); z-index: 1000; display: flex; align-items: center; justify-content: center; /* iOS橡皮筋效果处理 */ overscroll-behavior: none;-webkit-overscroll-behavior: none;}.modal-content {background: white; border-radius: 12px; max-height: 80vh;width:90%; max-width: 500px; overflow-y: auto;/* 关键:iOS惯性滚动 + 防止橡皮筋 */-webkit-overflow-scrolling: touch; overscroll-behavior: contain;}/* 防止安卓上的点击穿透 */.modal-overlay.active { pointer-events: auto;}.modal-overlay { pointer-events: none;}*/

这招的精髓在于overscroll-behavior这个CSS属性。 这是比较新的标准,专门用来控制滚动边界行为。overscroll-behavior: contain告诉浏览器:这个元素的滚动不要传播给父元素。配合事件拦截,能完美解决iOS的橡皮筋问题。

但要注意浏览器兼容性,iOS 13.0+和Chrome 63+才支持。老设备还得靠JS兜底。

这些方案到底咋选

咱得客观聊聊优缺点,别盲目抄代码。

暴力改body属性兼容性最好,老古董浏览器也能跑,但代码像补丁一样难看。而且iOS上那个著名的"回弹"bug,位置还原不对直接起飞。适合项目兼容性要求高、不想折腾的场景。

固定定位法视觉冲击小,但计算量大,稍微算错一点像素就穿帮。而且软键盘弹出时fixed布局会变形,这个坑很难填。适合PC端为主、移动端为辅的项目。

事件拦截法最优雅,逻辑清晰,精准打击。但在某些奇葩安卓机上可能会失效(比如某些国产浏览器的"智能省流模式"会干预事件处理),得做降级处理。适合移动端为主、追求极致体验的项目。

实际开发里,我通常的做法是特性检测 + 分层处理

/** * 终极解决方案:根据设备特性自动选择最优方案 */classUltimateScrollLock{constructor(){this.strategy =null;this.init();}init(){// 检测iOSconst isIOS =/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream;// 检测安卓const isAndroid =/Android/.test(navigator.userAgent);// 检测是否支持overscroll-behaviorconst supportsOverscroll =CSS.supports('overscroll-behavior','contain');// 检测是否支持passive事件let supportsPassive =false;try{const opts = Object.defineProperty({},'passive',{get:()=>{ supportsPassive =true;returntrue;}}); window.addEventListener('test',null, opts); window.removeEventListener('test',null, opts);}catch(e){}// 策略选择if(isIOS && supportsOverscroll){// iOS新版用CSS方案,性能最好this.strategy =newIOSStrategy();}elseif(isAndroid && supportsPassive){// 安卓用事件拦截this.strategy =newAndroidStrategy();}else{// 兜底用body lockthis.strategy =newFallbackStrategy();} console.log(`🎯 采用策略: ${this.strategy.name}`);}lock(){returnthis.strategy.lock();}unlock(){returnthis.strategy.unlock();}}// 具体策略实现...classIOSStrategy{ name ='iOS-CSS-Optimized';lock(){ document.body.style.overscrollBehavior ='none'; document.body.style.overflow ='hidden'; document.body.style.position ='fixed'; document.body.style.width ='100%';this.scrollY = window.scrollY; document.body.style.top =`-${this.scrollY}px`;}unlock(){ document.body.style.overscrollBehavior =''; document.body.style.overflow =''; document.body.style.position =''; document.body.style.width =''; document.body.style.top =''; window.scrollTo(0,this.scrollY);}}// 使用const locker =newUltimateScrollLock();// locker.lock();// locker.unlock();

实际场景里的坑

实际开发里这场景简直不要太多。登录注册弹窗、优惠券领取、还有那种烦人的活动公告。特别是电商大促,用户正看着商品详情,突然弹个窗,背景要是跟着滚,商品都找不到了。这时候技术细节决定转化率,别让一个滚动条毁了你的KPI。

说几个我踩过的血坑:

坑1:键盘弹出灾难

移动端input获取焦点,软键盘弹出来,fixed定位的body会被顶上去。关闭键盘后,页面卡在中间,上面的内容看不到了。

// 解决方案:监听resize事件,键盘收起时强制重置functionhandleKeyboard(){const originalHeight = window.innerHeight; window.addEventListener('resize',()=>{const currentHeight = window.innerHeight;// 高度变化超过150px,认为是键盘弹出/收起if(Math.abs(originalHeight - currentHeight)>150){if(currentHeight < originalHeight){// 键盘弹出,调整弹窗位置 document.querySelector('.modal-content').style.transform ='translateY(-20%)';}else{// 键盘收起,恢复位置并检查滚动 document.querySelector('.modal-content').style.transform ='';// 关键:iOS上需要延迟重置滚动setTimeout(()=>{ window.scrollTo(0,0); document.body.style.top ='0';},100);}}});}

坑2:第三方库样式污染

有时候你明明锁了滚动,但背景还能动。查了半天发现是某个UI库(我就不点名了)偷偷给body加了!important的样式,把你的覆盖掉了。

// 防御性编程:用MutationObserver监视body样式变化const observer =newMutationObserver((mutations)=>{ mutations.forEach((mutation)=>{if(mutation.target === document.body && mutation.attributeName ==='style'){// 检查是否被恶意修改const currentOverflow = document.body.style.overflow;if(currentOverflow !=='hidden'&& modalIsOpen){ console.warn('⚠️ body样式被外部修改,正在恢复...'); document.body.style.overflow ='hidden';}}});}); observer.observe(document.body,{attributes:true});

坑3:z-index地狱

弹窗打开了,但点击遮罩层没反应,背景还在响应点击。检查发现是某个父元素创建了新的层叠上下文(stacking context),导致z-index失效。

/* 创建层叠上下文的常见属性,排查时注意 */.parent{opacity: 0.9;/* 会创建层叠上下文! */transform:translateX(0);/* 也会! */filter:blur(1px);/* 也会! */will-change: transform;/* 也会! */isolation: isolate;/* 也会! */}

排查思路得野路子一点

遇到问题的排查思路得野路子一点。别上来就查文档,先真机调试。拿个iPhone 15 Pro Max和一堆安卓千元机轮着测。看看是不是某些CSS属性触发了硬件加速导致失效。或者是不是z-index层级没盖住,点击事件穿透到了背景层。

几个私藏的调试技巧:

// 1. 可视化滚动位置,方便调试functionshowScrollDebug(){const div = document.createElement('div'); div.style.cssText =` position: fixed; top: 10px; right: 10px; background: red; color: white; padding: 5px 10px; z-index: 99999; font-family: monospace; `; document.body.appendChild(div);setInterval(()=>{ div.textContent =`scrollY: ${window.scrollY} | body.top: ${document.body.style.top}`;},100);}// 2. 检测所有层叠上下文functioncheckStackingContexts(){const allElements = document.querySelectorAll('*');const contexts =[]; allElements.forEach(el=>{const style = window.getComputedStyle(el);const isContext = style.position !=='static'&&(style.zIndex !=='auto'|| style.opacity !=='1'|| style.transform !=='none'|| style.filter !=='none');if(isContext){ contexts.push({element: el.tagName +(el.id ?'#'+ el.id :'')+(el.className ?'.'+ el.className.split(' ').join('.'):''),zIndex: style.zIndex,position: style.position });}}); console.table(contexts);}// 3. 事件监听调试,看看到底谁在触发滚动functiondebugScrollEvents(){let lastScrollTime = Date.now(); window.addEventListener('scroll',(e)=>{const now = Date.now(); console.log(`滚动事件!间隔: ${now - lastScrollTime}ms`, e.target); lastScrollTime = now;},true);// 捕获阶段监听,能看到所有滚动事件// 拦截touchmove看是否被阻止 document.addEventListener('touchmove',(e)=>{ console.log('touchmove触发于:', e.target,'默认行为:', e.defaultPrevented);},{passive:true});}

几个私藏的防抖小技巧

记录滚动位置时别直接用scrollTop,做个防抖处理,不然高频触发卡成PPT。关闭弹窗恢复滚动时,加个微小的延时,让浏览器喘口气再渲染。

// 防抖函数,基础但重要functiondebounce(func, wait, immediate){let timeout;returnfunction(...args){const context =this;constlater=()=>{ timeout =null;if(!immediate)func.apply(context, args);};const callNow = immediate &&!timeout;clearTimeout(timeout); timeout =setTimeout(later, wait);if(callNow)func.apply(context, args);};}// 节流函数,滚动监听必备functionthrottle(func, limit){let inThrottle;returnfunction(...args){if(!inThrottle){func.apply(this, args); inThrottle =true;setTimeout(()=> inThrottle =false, limit);}};}// 使用示例:记录滚动位置时防抖const saveScrollPosition =debounce(()=>{ sessionStorage.setItem('scrollPosition', window.scrollY);},150); window.addEventListener('scroll', saveScrollPosition);// 恢复时加点延迟,让DOM先稳定functionrestoreScroll(){const saved = sessionStorage.getItem('scrollPosition');if(saved){setTimeout(()=>{ window.scrollTo({top:parseInt(saved),behavior:'auto'}); sessionStorage.removeItem('scrollPosition');},50);// 50ms的喘息时间}}

如果是Vue或React项目,记得在组件卸载时清理全局事件监听,不然内存泄漏让你哭都找不到调。

// React useEffect清理示例useEffect(()=>{consthandleScroll=()=>{/* ... */}; window.addEventListener('scroll', handleScroll);// 清理函数,组件卸载时执行return()=>{ window.removeEventListener('scroll', handleScroll);// 如果锁定了滚动,强制解锁,防止死锁if(document.body.style.overflow ==='hidden'){ document.body.style.overflow ='';}};},[]);// Vue3 onUnmounted示例import{ onUnmounted, onMounted }from'vue';onMounted(()=>{constlistener=(e)=>{/* ... */}; document.addEventListener('touchmove', listener,{passive:false});onUnmounted(()=>{ document.removeEventListener('touchmove', listener);});});

还有啊,千万别忘记测试键盘弹出的情况,软键盘一出,fixed布局直接变形,画面太美不敢看。测试时要覆盖这些场景:

  • iOS Safari(最难搞的主)
  • iOS微信内置浏览器(比Safari还难搞)
  • 安卓Chrome(相对乖一点)
  • 安卓微信内置浏览器(X5内核,玄学问题多)
  • 各种折叠屏(屏幕比例变化时会有惊喜)

最后唠两句

这功能看着小,其实是检验前端功底的试金石。别总觉得这是UI的事,交互细节才是魔鬼。一个滚动条处理不好,用户体验直接崩盘,转化率掉几个点,产品经理能追杀你到天涯海角。

下次产品经理再提这种需求,直接把这篇甩给他,告诉他咱们为了这点体验掉了多少头发。从overflow: hiddenpassive: false,从iOS橡皮筋到安卓点击穿透,从fixed定位抖动到软键盘灾难,这里面的水深得一批。

行了,大概就这么些干货,代码都给你写好了,注释也塞满了,拿去直接用或者根据项目改改都行。赶紧去修你那乱滚的页面吧,不然测试妹子又要找你麻烦了。要是还有啥奇葩场景搞不定,记得回来交流,这坑咱一起填。

(完)

欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
推荐:DTcode7的博客首页。
一个做过前端开发的产品经理,经历过睿智产品的折磨导致脱发之后,励志要翻身农奴把歌唱,一边打入敌人内部一边持续提升自己,为我们广大开发同胞谋福祉,坚决抵制睿智产品折磨我们码农兄弟!

专栏系列(点击解锁)学习路线(点击解锁)知识定位
《微信小程序相关博客》持续更新中~结合微信官方原生框架、uniapp等小程序框架,记录请求、封装、tabbar、UI组件的学习记录和使用技巧等
《AIGC相关博客》持续更新中~AIGC、AI生产力工具的介绍,例如stable diffusion这种的AI绘画工具安装、使用、技巧等总结
《HTML网站开发相关》《前端基础入门三大核心之html相关博客》前端基础入门三大核心之html板块的内容,入坑前端或者辅助学习的必看知识
《前端基础入门三大核心之JS相关博客》前端JS是JavaScript语言在网页开发中的应用,负责实现交互效果和动态内容。它与HTML和CSS并称前端三剑客,共同构建用户界面。
通过操作DOM元素、响应事件、发起网络请求等,JS使页面能够响应用户行为,实现数据动态展示和页面流畅跳转,是现代Web开发的核心
《前端基础入门三大核心之CSS相关博客》介绍前端开发中遇到的CSS疑问和各种奇妙的CSS语法,同时收集精美的CSS效果代码,用来丰富你的web网页
《canvas绘图相关博客》Canvas是HTML5中用于绘制图形的元素,通过JavaScript及其提供的绘图API,开发者可以在网页上绘制出各种复杂的图形、动画和图像效果。Canvas提供了高度的灵活性和控制力,使得前端绘图技术更加丰富和多样化
《Vue实战相关博客》持续更新中~详细总结了常用UI库elementUI的使用技巧以及Vue的学习之旅
《python相关博客》持续更新中~Python,简洁易学的编程语言,强大到足以应对各种应用场景,是编程新手的理想选择,也是专业人士的得力工具
《sql数据库相关博客》持续更新中~SQL数据库:高效管理数据的利器,学会SQL,轻松驾驭结构化数据,解锁数据分析与挖掘的无限可能
《算法系列相关博客》持续更新中~算法与数据结构学习总结,通过JS来编写处理复杂有趣的算法问题,提升你的技术思维
《IT信息技术相关博客》持续更新中~作为信息化人员所需要掌握的底层技术,涉及软件开发、网络建设、系统维护等领域的知识
《信息化人员基础技能知识相关博客》无论你是开发、产品、实施、经理,只要是从事信息化相关行业的人员,都应该掌握这些信息化的基础知识,可以不精通但是一定要了解,避免日常工作中贻笑大方
《信息化技能面试宝典相关博客》涉及信息化相关工作基础知识和面试技巧,提升自我能力与面试通过率,扩展知识面
《前端开发习惯与小技巧相关博客》持续更新中~罗列常用的开发工具使用技巧,如 Vscode快捷键操作、Git、CMD、游览器控制台等
《photoshop相关博客》持续更新中~基础的PS学习记录,含括PPI与DPI、物理像素dp、逻辑像素dip、矢量图和位图以及帧动画等的学习总结
日常开发&办公&生产【实用工具】分享相关博客》持续更新中~分享介绍各种开发中、工作中、个人生产以及学习上的工具,丰富阅历,给大家提供处理事情的更多角度,学习了解更多的便利工具,如Fiddler抓包、办公快捷键、虚拟机VMware等工具

吾辈才疏学浅,摹写之作,恐有瑕疵。望诸君海涵赐教。望轻喷,嘤嘤嘤

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。愿斯文对汝有所裨益,纵其简陋未及渊博,亦足以略尽绵薄之力。倘若尚存阙漏,敬请不吝斧正,俾便精进!
在这里插入图片描述

Read more

前端环境配置(nvm、nodejs、npm)

前端环境配置(nvm、nodejs、npm)

一、安装nvm 1. 下载vnm url: https://nvm.uihtm.com/doc/download-nvm.html 2. 解压文件后双击exe文件进行安装 3. 选择nvm的安装地址,我是安装在D:\App\nvm 4. 选择nodejs的安装地址,我是安装在C:\Program Files\nodejs 5. 点击next 一直点击 完成安装; 6. 找到nvm的settings.txt文件打开后: 给该文件添加这两行命令: node_mirror: https://npmmirror.com/mirrors/node/ npm_mirror: https://npmmirror.com/mirrors/npm/ 二、环境变量配置 1.

【DeepSeek R1部署至RK3588】RKLLM转换→板端部署→局域网web浏览

【DeepSeek R1部署至RK3588】RKLLM转换→板端部署→局域网web浏览

本文为DeepSeek R1 7B 以qwen为底座的LLM在瑞芯微RK3588 SoC上的完整部署流程,记录从开发板驱动适配烧录开始,到最终的开发板终端访问模型和局域网web访问模型的完整流程,有不足之处希望大家共同讨论。 文章目录 * 一、项目背景介绍 * 二、所需工具介绍 * 1.硬件工具 * 1.X86 PC虚拟机Ubuntu20.04 * 2. 准备NPU驱动为0.9.8的RK3588开发板 * 2.软件工具 * 三、获取.safetensors模型权重 * 四、safetensors转RKLLM * 1.转换环境搭建 * 2.模型转换 * 五、RKLLM模型板端部署及推理 * 六、集成开源gradio工具实现web访问 一、项目背景介绍 先来介绍下项目背景吧,目前有一个空闲的firefly出厂的搭载瑞芯微RK3588 SoC的arm64开发板,样式如图所示: 博主之前主要进行CV领域的模型的RK开发板部署,对于LLM和VLM的接触并不算多,但现在大模型是趋势所向,并且瑞芯微及时的完成了针对各开源

想做多语言项目?试试Hunyuan-MT-7B-WEBUI快速部署方案

想做多语言项目?试试Hunyuan-MT-7B-WEBUI快速部署方案 你有没有遇到过这样的情况:手头有个跨境项目,要同时处理日语产品说明、西班牙语用户反馈、维吾尔语政策文件,甚至还有藏文古籍数字化需求——可翻来翻去,不是翻译质量差强人意,就是部署起来像在解一道高数题?在线工具不敢传敏感数据,本地跑模型又卡在CUDA版本、依赖冲突、显存爆炸上……最后只能靠人工硬啃,进度一拖再拖。 Hunyuan-MT-7B-WEBUI 就是为这种真实困境而生的。它不讲大道理,不堆参数,不做“实验室里的冠军”,而是把腾讯混元团队打磨出的最强开源翻译模型,连同网页界面、一键脚本、预装环境,全打包进一个镜像里。你不需要懂Transformer结构,不用查PyTorch兼容表,甚至不用打开终端敲命令——点一下,等两分钟,就能在浏览器里开始翻译38种语言。 这不是又一个“需要调参、需要写代码、需要配环境”的AI工具。这是你今天下午就能用上的多语言工作台。 1. 为什么这款翻译镜像值得你立刻试试? 1.1 它真能覆盖你没想过的语言 很多翻译模型标榜“支持多语言”,但实际打开列表一看:英、法、

前端实现Word文档在线编辑与导出:基于mammoth.js与Blob对象的完整解决方案

如何在浏览器中直接编辑Word文档并导出?本文将深入探索一种基于mammoth.js和Blob对象的完整技术方案。 在当今的Web应用开发中,实现文档的在线编辑与导出已成为常见需求。无论是企业内部系统、教育平台还是项目管理工具,都迫切需要让用户能够在浏览器中直接编辑Word文档,而无需安装桌面软件。本文将详细介绍如何利用mammoth.js和Blob对象实现这一功能,并对比其他可行方案。 一、为什么选择mammoth.js与Blob方案? 在Web前端实现Word文档处理,主要有三种主流方案:浏览器原生Blob导出、mammoth.js专业转换和基于模板的docxtemplater方案。它们各有优劣,适用于不同场景。 mammoth.js的核心优势在于它能将.docx文档转换为语义化的HTML,而非简单复制视觉样式。这意味着它生成的HTML结构清晰、易于维护和样式定制。配合Blob对象,我们可以轻松将编辑后的内容重新导出为Word文档。 与直接使用Microsoft Office Online或Google Docs嵌入相比,mammoth.js方案不依赖外部服务,能更好地