|
1、背景考虑这样一种情况,产品同学希望达到以下功能:在我们的网页中有一个固定区域,这个区域会用于渲染从后端拉取的含有等资源的富文本字符串。他需要在内容不超过一个最大高度的时候完全显示所有内容,超过最大内容后仅展示最大高度范围内的内容,超出部分隐藏,并通过一个按钮“展示更多”来给用户展示更多的选择。在这看似简单的需求当中,其实涉及到了一个难点,那就是怎样动态的监听到内容区域的高度变化?因为在这里面会含有资源,他们在渲染的时候会发起网络请求,等待加载完成后触发浏览器重排,该区域的高度被撑开。因此,内容区域的高度是动态变化,且变化的时间点是未知的,那么怎样知道我们的内容区高度发生了变化呢?为此我做了以下几种尝试:MutationObserverIntersectionObserverResizeObserver监听所有资源的onload事件iframe2、MutationObserverMutationObserver接口提供了监视对DOM树所做更改的能力。它被设计为旧的MutationEvents功能的替代品,该功能是DOM3Events规范的一部分。observe(target,options)这个方法会根据传入的options配置,观察DOM树中的单个Node或者所有的子孙节点的变化。他一共有七个属性,这里就不一一介绍了,可以通过MutationObserverInit来获取相应的介绍.那么我们要怎么使用这个API来监听目标区域的高度变化呢?首先我们要创建对该区域的dom根结点引用://useRef创建引用constcontentRef=useRef();//绑定ref;然后我们需要创建一个MutationObserver实例:const[height,setHeight]=useState(-1);const[observer,setObserver]=useState(null!);useEffect(()=>{ constobserver=newMutationObserver((mutationList)=>{ if(height!==contentRef.current?.clientHeight){ console.log("高度变化了!"); setHeight(contentRef.current.clientHeight); } }); setObserver(observer);},[]);当我们的ref或者observer发生变化的时候,对ref节点进行观察:useEffect(()=>{ if(!observer||!contentRef.current)return; observer.observe(contentRef.current,{ childList:true,//子节点的变动(新增、删除或者更改) attributes:true,//属性的变动 characterData:true,//节点内容或节点文本的变动 subtree:true,//是否将观察器应用于该节点的所有后代节点 });},[contentRef.current,observer]);完整代码:constDetails=()=>{ //useRef创建引用 constcontentRef=useRef(); const[height,setHeight]=useState(-1); const[observer,setObserver]=useState(null!); useEffect(()=>{ constobserver=newMutationObserver((mutationList)=>{ if(height!==contentRef.current?.clientHeight){ console.log('高度变化了!'); setHeight(contentRef.current.clientHeight); } }); setObserver(observer); },[]); useEffect(()=>{ if(!observer||!contentRef.current)return observer.observe(contentRef,{ childList:true,//子节点的变动(新增、删除或者更改) attributes:true,//属性的变动 characterData:true,//节点内容或节点文本的变动 subtree:true//是否将观察器应用于该节点的所有后代节点 }); },[contentRef.current,observer]); //绑定ref return}经过上面的一番操作之后,发现根本达不到效果,因为我们的css属性根本没有发生变化(我们是通过maxHeight来约束容器的高度的),但是资源加载完毕之后,浏览器重排根本没有产生css属性的变化,它的高度是自动计算的因此这个方案无济于事!但是它确实可以监听到认为修改容器的高度产生的变化,比如:contentRef.current.style.height='1000px',这个api是可以监听到这一操作的,但是并不符合我们的场景此外,它的浏览器兼容性也还行:3、IntersectionObserver经过激情编码,最后发现 MutationObserver 根本达不到我们想要的效果之后,其实我的心态已经产生了一些变化,不过不要紧!我们可以换一种思路,既然我们无法通过监听容器的高度变化来展示相应的“展开更多”操作,那么我们可不可以将这个“展开更多”固定到一个位置上,然后超出部分隐藏,当我们的内容自动撑开,达到指定高度后,我们这个“展开更多”的操作的按钮就显示出来了,听上去不错,能达到要求!废话不多说,开撸!因为这里只涉及到相应的css样式的书写,就不做展示了。经过处理之后,确实在容器高度小于指定高度的时候,“展示更多”按钮不会展示,超过最大值之后,会将该按钮展示出来,但是也遇到了一个问题,操作按钮是有高度的,如果我们的内容高度介于最大高度-按钮高度到容器的最大高度之间,按钮会产生显示一部分,同时又隐藏一部分的效果,这可不是我们想要的!如图所示:显然这种效果是不符合要求的,我们的“展示更多”按钮,只有两种状态,要么全部展示,要么不展示,没有这种部分展示的效果因此我查阅了相关资料,了解到了IntersectionObserver这个API,它可以监听一个元素是否进入用户视野,它的相关使用方法可以参考这篇文章:IntersectionObserverAPI使用教程它使用起来和MutationObserver几乎一样,只是名字不一样而已它监听的值里面有一个比较重要的属性:intersectionRatio借助这个API,我的设计思路是这样的:当用户滚动网页的时候(或者不滚动,此时目标区域已经出现在屏幕中),可以得到intersectionRatio的值,通过判断这个值是否等于1来决定要不要展示“展示更多”按钮但经过我的编码实现后,发现滚动事件发生的时候,intersectionRatio的变化是不可靠的,有时候完全可见了,但是它并不等于1。经过多轮实验,结果依然如此。但是它确实可以用来判断一个元素是否进入用户视野由于使用上结果的不可靠,我放弃这个方案(可能是我使用方式上出了问题)它的各浏览器兼容性如下:4、ResizeObserver顾名思义,这个API就是专门监听DOM尺寸变化的,只不过它还处于试验阶段,各浏览器的兼容性很差,所以基本不考虑具体使用方法可以参考这篇文章:检测DOM尺寸变化JSAPIResizeObserver简介它现阶段各浏览器的兼容性情况:5、监听所有资源的onload事件既然上述方法都不行,那么我绞尽脑汁,又想出了另外一种方法:监听所有带有src属性的DOM元素的onload事件,通过他的回调来判断当前容器的高度情况这种实现方式,在思路上是完全符合目的的,具体做法参考如下:const[height,setHeight]=useState(-1);const[showMore,setShowMore]=useState(false);//contentRef的定义见MutationObserver一节useEffect(()=>{ constsources=contentRef.current.querySelectorAll("[src]"); sources.onload=()=>{ constheight=contentRef?.current?.clientHeight??0; constshow=height>=parseInt(MAX_HEIGHT,10); setHeight(height); setShowMore(show); };},[]);通过这种方式可以实现对富文本中的进行加载后,对容器高度进行相应的判断。但是这种方式,存在不确定性,即无法判断是否找齐了所有高度由内容撑开的资源。6、Iframe这是终极方案,也是在此背景中所采用的方案。既然window可以监听到resize事件,那么我们就可以利用iframe来达到同样的效果,具体做法就是在容器里面嵌套一个隐藏的高度为100%的iframe,通过监听他的resize事件,来判断当前容器的高度。话不多说,具体实现方式如下:constDetail:FC=()=>{ constref=useRef(null); constifr=useRef(null); const[height,setHeight]=useState(-1); const[showMore,setShowMore]=useState(false); const[maxHeight,setMaxHeight]=useState(MAX_HEIGHT); constintroduceInfo=useAppSelect( (state)=>state.courseInfo?.data?.introduce_info??{} ); constdetails=introduceInfo.details??""; constisFolded=maxHeight===MAX_HEIGHT; constonresize=useCallback(()=>{ constheight=ref?.current?.clientHeight??0; constshow=height>=parseInt(MAX_HEIGHT,10); setHeight(height); setShowMore(show); if(ifr.current&show){ ifr.current.remove(); } },[]); useEffect(()=>{ if(!ref.current||!ifr.current?.contentWindow)return; ifr.current.contentWindow.onresize=onresize; onresize(); },[details]); if(!details)returnnull; return( {/*这个iframe是用来动态监听content高度的变化的*/} {isFolded&showMore&( { setMaxHeight(isFolded?"none":MAX_HEIGHT); }} > 查看全部 )} );};这种方式实际上就是对ResizeObserver的一种hack,经过多次实践,符合功能要求。7、总结解决问题要尽可能的考虑多种情况,对比多种方案,采取最为可靠的一种方案。不断学习,多查询资料,你所遇到的问题基本上前人都已经踩过坑了。监听DOM元素的高度变化,可以采用内嵌iframe的方式来解决。
|
|