分析问题
- 现代浏览器中标准化了 CSS
zoom
属性(如 Chrome 128 以上版本更新),不再与先前非标准化时的处理规范一致,getBoundingClientRect
,getClientRects
获取到的ClientRect
实例不再计算所有父元素中的缩放层级,导致获取到的属性不准确。
- 因 ElementUI, AntDesign 等悬浮组件均使用
ClientRect
计算定位,且组件继承的父元素 CSSzoom
属性存在值不为1
的变化,则导致悬浮组件的定位样式出现偏移。
解决方案
- 解决核心问题:使获取到的
ClientRect
实例计算所有父元素中的缩放层级。 - 获取元素的实际缩放层级,需计算所有继承的
zoom
比例,不能仅计算body
的zoom
比例,以免其父元素之一存在zoom != 1
时导致比例畸变。 - 以下为实测可用的通用解决方案(兼容 Chrome, Firefox, Edge 等主流浏览器):
/** [Hotfix] 修复 ClientRect 受 zoom 影响导致属性不精确的问题 */ if (CSS.supports('zoom', '1')) { const isUseFix = function () { let el = document.createElement('div') el.style.cssText = 'position: fixed; width: 100px; top: 0; left: 0; z-index: -1;' document.body.appendChild(el) let a = el.getBoundingClientRect().width el.style.zoom = 2 let b = el.getBoundingClientRect().width document.body.removeChild(el) return a !== b } if (isUseFix()) { const useDOMRect = typeof DOMRect === 'function' const useDOMRectList = typeof DOMRectList === 'function' const _getBoundingClientRect = Element.prototype.getBoundingClientRect const _getClientRects = Element.prototype.getClientRects const getRectItem = function (i) { return this[i] } const getRealZoom = function (el) { let zoom = 1 while (el) zoom /= +getComputedStyle(el).zoom || 1, el = el.parentElement return zoom } const newRealRect = function ({ width, height, left, top, x = left, y = top }, zoom) { return useDOMRect ? new DOMRect(x * zoom, y * zoom, width * zoom, height * zoom) : { x: x * zoom, y: y * zoom, width: width * zoom, height: height * zoom, top: y * zoom, left: x * zoom, right: x * zoom + width * zoom, bottom: y * zoom + height * zoom } } Element.prototype.getBoundingClientRect = function () { let rect = _getBoundingClientRect.call(this), zoom = getRealZoom(this) return zoom != 1 ? newRealRect(rect, zoom) : rect } Element.prototype.getClientRects = function () { let list = _getClientRects.call(this), zoom = getRealZoom(this) if (zoom != 1) { list = [...list].map(rect => newRealRect(rect, zoom)) Object.defineProperty(list, 'item', { value: getRectItem }) useDOMRectList && Object.setPrototypeOf(list, DOMRectList.prototype) } return list } } }
- 使用 API 方式调用:
/** * 获取实际的 ClientRect 属性 * @param {HTMLElement} el DOM元素 * @return {DOMRect|Object} */ function getRealClientRect(el) { const rect = el.getBoundingClientRect() const isUseFix = CSS.supports('zoom', '1') && (function () { let checkEl = document.createElement('div') checkEl.style.cssText = 'position: fixed; width: 100px; top: 0; left: 0; z-index: -1;' document.body.appendChild(checkEl) let a = checkEl.getBoundingClientRect().width checkEl.style.zoom = 2 let b = checkEl.getBoundingClientRect().width document.body.removeChild(checkEl) return a !== b })() if (isUseFix) { let target = el, zoom = 1 while (target) zoom /= +getComputedStyle(target).zoom || 1, target = target.parentElement if (zoom != 1) { let { width, height, left, top, x = left, y = top } = rect return typeof DOMRect === 'function' ? new DOMRect(x * zoom, y * zoom, width * zoom, height * zoom) : { x: x * zoom, y: y * zoom, width: width * zoom, height: height * zoom, top: y * zoom, left: x * zoom, right: x * zoom + width * zoom, bottom: y * zoom + height * zoom } } } return rect }