开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第 2 天,点击查看活动详情
背景
最近在和其他部门的同事合作处理一个toC的项目,他们负责前端组件库的开发,我处理一些特定业务的组件,然后将业务组件插入组件库中,进行渲染。两边的开发是完全解耦的( 各搞各的 ),就是说,组件库对我来说完全是黑盒子,当我往组件库中插入我的渲染节点时,我的一个设置了定位的元素 消失 了...
- 计划渲染的效果
- 实际渲染的效果
定位元素被隐藏了
问题原因
在我调试样式的过程中,我发现,父级元素设置了 overflow: 'hidden',测试代码如下
function App() {
return (
<div className='wrapper'>
<div style={{ overflow: 'hidden' }}>
<TestComponent />
</div>
</div>
);
}
const TestComponent = () => {
return (
<div style={{ border: '1px solid red', position: 'relative' }}>
<div
style={{
position: 'absolute',
height: '22px',
width: '140px',
left: 0,
top: '-26px',
border: '1px solid green'
}}
>
定位元素absolute
</div>
<div style={{ height: '22px', width: '150px' }}>组件上面有定位元素</div>
</div>
)
}
也就是说,组件库中的元素设置了 overflow: 'hidden'。这会导致我插入的元素,如果设置了 position: 'absolute' 都会被隐藏
解决方案
方案一:去除父级 overflow: hidden
既然是父级样式影响的,那去掉不就可以了?如果所有代码都是你写的,你可以发现一处改一处。但是对于一个 多人协作 的项目,这明显不科学(都说了是各搞各的,别人不可能为你改代码)。组件库的代码已经写好了,我们能做的就是 不入侵 别人的代码。所以 此方案不可取
方案二:外层设置position: 'absolute'
既然不能让别人改代码,那就只能从我们自己的代码入手。你可以在你的组件外层,再包裹一层节点,并设置position: 'absolute',可以这样写:
function App() {
return (
<div className='wrapper'>
<div style={{ overflow: 'hidden' }}>
{/* 加一层节点,并设置 position: 'absolute' */}
<div style={{ position: 'absolute' }}>
<TestComponent />
</div>
</div>
</div>
);
}
定位元素确实是 显示 了,但是整个业务组件的样式却乱了,测试代码中,应该是要居中展示,现在却靠右了。所以 此方案也不可取
方案三:createPortal + getBoundingClientRect
由于是父元素设置 overflow 引起的问题,且不能去改父元素的样式。那我们能不能给定位元素 重新找一个父元素,比如把 body 作为父元素,再通过top、left值,重新把定位元素定位到目标元素上面。理论上是可行的,我们需要解决两个问题:1. 通过某种方法,把定位元素放到 body 下;2. 确定定位元素的位置:
- 给定位元素重新找个父元素(body)
ReactDOM.createPortal: 提供了一种将子节点渲染到存在于父组件以外 DOM 节点的方案
我们可以通过 react 提供的 ReactDOM.createPortal,将我们的定位元素传送到 body 去做节点渲染
// 这里默认挂载在 document.body 元素下,你可以自定义挂载在哪
const Portal = ({ children }) =>
typeof document === 'object' ? ReactDOM.createPortal(children, document.body) : null;
const TestComponent = () => {
return (
<div style={{ border: '1px solid red', position: 'relative' }}>
<Portal>
<div style={{ position: 'absolute', ... }}>
定位元素absolute
</div>
</Portal>
<div style={{ height: '22px', width: '150px' }}>组件上面有定位元素</div>
</div>
)
}
可以看到,我们的定位元素已经被传送到 body 下面,并且正常渲染了,那接下的事情就是找位置了(设置 top 和 left)
- 找定位的位置
getBoundingClientRect: 用于获得页面中某个元素的左,上,右和下分别相对浏览器视窗的位置。是DOM元素到浏览器可视范围的距离(不包含文档卷起的部分)
我们首先要找到目标元素的位置,以此来确定我们的定位元素要在哪里显示,这里我们可以通过 getBoundingClientRect 来做
const Portal = ({ children }) =>
typeof document === 'object' ? ReactDOM.createPortal(children, document.body) : null;
const TestComponent = () => {
const divRef = useRef()
const [rect, setRect] = useState({ top: 0, left: 0 })
useEffect(() => {
if (divRef.current) {
// 获取目标元素的位置
const { left, top } = divRef.current.getBoundingClientRect()
// 设置定位元素的位置
setRect({ top: `${top - 26}px`, left: `${left}px` })
}
}, [divRef])
return (
<div style={{ border: '1px solid red', position: 'relative' }}>
<Portal>
<div
style={{
position: 'absolute',
height: '22px',
width: '140px',
left: rect.left,
top: rect.top,
border: '1px solid green'
}}
>
定位元素absolute
</div>
</Portal>
<div ref={divRef} style={{ height: '22px', width: '150px' }}>组件上面有定位元素</div>
</div>
)
}
这样就实现了我们想要的效果
结束语
其实这样的方案并不是特例,业内很多组件库在实现 Dialog 等全局组件的时候,都会采用类似的方案,另外一些组件库,为了规避代码入侵,也会这样做。需要注意的是,当目标元素发生了位置的变动,需要重新计算一次 getBoundingClientRect,我们可以监听 resize、 scroll 事件
如果是
absolute定位,可以不用考虑 scroll
此方案还要考虑父元素是否用了
transform。transform会导致父元素偏离原来位置,使得getBoundingClientRect拿不到正确的值
/************ 监听 dom 滚动 ****************/
// 记得要取消监听
document.getElementById('xxx').addEventListener('scroll', this.handleScroll)
/************ 监听 dom 宽高变化 ****************/
// 新建obsever对象
const resizeObserver = new ResizeObserver(entries => {
for (let entry of entries) {
console.log(entry.target.offsetWidth)
}
});
// 开始监听,传入dom对象
resizeObserver.observe(targetNode);