当前位置: 首页 > Web前端 > HTML5

组件复用那些事——React实现按需加载轮子

时间:2023-04-05 11:07:06 HTML5

组件化是当今前端开发领域一个非常重要的概念。著名的前端库,如React、Vue等,都极力推荐这个概念。确实,组件可重用性(reusability)和模块化(modularization)的优势对于复杂的场景需求有着先天优势。组件就像乐高积木和积木,一点点拼接就构成了我们的应用。同时,延迟加载/按需加载的概念至关重要。为页面性能优化和用户体验提升提供了新的思路。我们请求的资源越少,我们解析的脚本越少,必要时执行的事情越少,我们取得的结果就越好。本文将从懒加载时机、组件复用方式、代码示例三个方面进行分析。阅读愉快!按需加载场景设计与分析一个典型的页面如下图所示:它包含以下块:页眉页眉;图片显示区;地图显示区;页脚。对应代码示例:constPage=()=>{

};用户访问时,如果不滚动页面,只能看到头部区域。但在很多场景下,我们会加载所有的JavaScript脚本、CSS资源等资源,然后渲染出完整的页面。这显然是不必要的,会消耗更多带宽,并延迟页面加载时间。为此,在前端的历史上进行了很多懒加载的探索,也应运而生了很多大公司的开源作品:比如Yahoo的YUILoader,Facebook的Haste,Bootloader和Primer等。今天,这些实现延迟加载脚本的代码,学习起来还是很有意义的。这里就不展开了。如下图所示,在正常逻辑条件下,在代码覆盖率层面,我们可以看到有1.1MB/1.5MB(76%)的代码没有被应用。另外,并不是所有的资源都需要延迟加载。我们在设计层面需要考虑以下几点:不要按需加载首屏内容。这很容易理解。首屏时间非常重要,用户越早看到越好。那么如何定义首屏内容呢?这需要结合用户终端和站点布局来考虑;预延迟加载。我们应该避免向用户呈现空白内容,因此预延迟加载和预执行脚本可以显着改善用户体验。例如下图,当图片以100px出现在屏幕上时,会提前请求并渲染图片;延迟加载对SEO的影响。这里涉及的内容比较多,需要开发者了解搜索引擎爬虫机制。以Googlebot为例,它支持IntersectionObserver,但它只对视口中的内容起作用。这里就不细说了,有兴趣的读者可以结合谷歌站长工具:FetchasGoogle,通过测试页和测试页源码进行实验。React组件复用技术提到组件复用,大多数开发者应该对高阶组件不陌生。这些组件接受其他组件,增强它们的功能,最后返回一个组件供消费。React-redux的connect是一个典型的柯里化应用,代码示例:constMyComponent=props=>(
{props.id}-{props.name}
);//...constConnectedComponent=connect(mapStateToProps,mapDispatchToProps)(MyComponent);同样,FunctionasChildComponent或RenderCallback技术也很常用。许多React库如react-media和unstated被广泛使用。以react-media为例:constMyComponent=()=>({matches=>matches?(

文档宽度小于600px。

):(

文档宽度至少为600px。

)});Media组件将调用其子组件进行渲染。核心逻辑是:classMediaextendsReact.Component{...render(){React.Children.only(children)}}这样子组件就不需要感知媒体查询逻辑,进而完成复用。另外还有很多组件复用的技巧,比如renderprops等,这里就不一一分析了。有兴趣的读者可以在我的新书中找到相关内容。让我们手动实现一个按需加载轮。首先,需要设计一个Observer组件,它会检测目标块是否在视口中可见。为了简化不必要的逻辑,我们使用IntersectionObserverAPI,它异步观察目标元素的视觉状态。它的兼容性可以参考这里。类Observer扩展组件{constructor(){super();this.state={isVisible:false};这个.io=null;这个.container=null;}componentDidMount(){this.io=newIntersectionObserver([entry]=>{this.setState({isVisible:entry.isIntersecting});},{});this.io.observe(this.container);}componentWillUnmount(){if(this.io){this.io.disconnect();}}render(){return(//这个也可以用findDOMNode来实现,但是不推荐{this.container=div;}}>{Array.isArray(this.props.儿童)?this.props.children.map(child=>child(this.state.isVisible)):this.props.children(this.state.isVisible)}
);}}同上,这个组件有isVisible状态,表示目标元素是否可见。this.io代表当前的IntersectionObserver实例;this.container代表当前被观察的元素,它通过ref完成对目标元素的获取。在componentDidMount方法中,我们切换this.setState.isVisible状态;在componentWillUnmount方法中,我们执行垃圾收集。很明显,这种复用方式就是上面提到的FunctionasChildComponent。注意,对于上面的基本实现,我们完全可以自定义个性化设置。IntersectionObserver支持边距或阈值选项。我们可以在构造函数中实现配置项初始化,并在componentWillReceiveProps生命周期函数中进行更新。这样,对于前面的页面内容,我们就可以对Gallery组件和Map组件进行懒加载了:constPage=()=>{
{isVisible=>}{isVisible=>}
}我们传递isVisible状态。对应的消费者组件可以根据isVisible有选择地渲染。具体实现:classMapextendsComponent{constructor(){super();this.state={初始化:false};这个.map=null;}initializeMap(){this.setState({initialized:true});//加载第三方谷歌地图loadScript("https://maps.google.com/maps/api/js?key=",()=>{constlatlng=newgoogle.maps.LatLng(38.34,-0.48);constmyOptions={zoom:15,center:latlng};constmap=newgoogle.maps.Map(this.map,myOptions);});}componentDidMount(){如果(this.props.isVisible){这个.initializeMap();}}componentWillReceiveProps(nextProps){if(!this.state.initialized&&nextProps.isVisible){this.initializeMap();}}render(){return({this.map=div;}}/>);}}只有当Map组件对应的容器出现在视口中时,我们才会加载第三方资源。同样,对于Gallery组件:classGalleryextendsComponent{constructor(){super();this.state={hasBeenVisible:false};}componentDidMount(){if(this.props.isVisible){this.setState({hasBeenVisible:true});}}componentWillReceiveProps(nextProps){if(!this.state.hasBeenVisible&&nextProps.isVisible){this.setState({hasBeenVisible:true});}}render(){return(

部分图片

图片1{this.state.hasBeenVisible?():()}图片2{this.state.hasBeenVisible?():()}
);}}也可以使用无状态组件/功能组件来实现:constGallery=({isVisible})=>(

一些图片

图片1{isVisible?():()}图片2{isVisible?():()}
);这样无疑更加简洁,但是当元素移出视口时,相应的图片不会继续显示,而是会重新显示占位符。如果我们需要懒加载的内容在页面生命周期内只记录一次,可以设置hasBeenVisible参数:constPage=()=>{...{(isVisible,hasBeenVisible)=>//Gallery现在可以是无状态的}...}或者直接实现ObserverOnce组件:classObserverOnceextendsComponent{constructor(){super();this.state={hasBeenVisible:false};这个.io=null;这个.container=null;}componentDidMount(){this.io=newIntersectionObserver(entries=>{entries.forEach(entry=>{if(entry.isIntersecting){this.setState({hasBeenVisible:true});this.io.disconnect();}});},{});this.io.observe(this.container);}componentWillUnmount(){if(this.io){this.io.disconnect();}}render(){return({this.container=div;}}>{Array.isArray(this.props.children)?这个.props.children.map(child=>child(this.state.hasBeenVisible)):this.props.children(this.state.hasBeenVisible)}
);}}更多场景上面我们使用了Observer组件来加载包括GoogleMap第三方内容和图片在内的资源。我们也可以实现“仅当组件出现在视口中时才显示元素动画”的需求。继ReactAlicante网站之后,我们实现了类似的点播动画需求。详情见codepen地址。IntersectionObserverpolyfilling前面提到了IntersectionObserverAPI的兼容性,自然绕不开polyfill这个话题。处理兼容性的一种选择是“渐进式增强”,即在支持的场景下只实现按需加载,否则isVisible状态始终设置为true:this.state={isVisible:!(window.IntersectionObserver)};这个.io=null;这个.container=null;}componentDidMount(){if(window.IntersectionObserver){this.io=newIntersectionObserver(entries=>{...}}}}这显然不能达到按需的目的,我推荐w3c的IntersectionObserverpolyfill:classObserverextendsComponent{...componentDidMount(){(window.IntersectionObserver?Promise.resolve():import('intersection-observer')).then(()=>{this.io=newwindow.IntersectionObserver(entries=>{条目。forEach(entry=>{this.setState({isVisible:entry.isIntersecting});});},{});this.io.observe(this.container);});}...}当浏览器不支持IntersectionObserver,我们动态导入polyfill,需要支持动态导入,这个另说问题,这里不展开最终测试,在不支持的Safari浏览器下,我们可以看到Network时间线如下:总结本文介绍了组件复用和按需加载(懒加载)相关的实现内容。更多相关知识,可以关注作者的新书。同时,本文截取自JoséM.Pérez的《ImprovethePerformanceofyourSitewithLazy-LoadingandCode-Splitting》,有一些改动。广告时间:如果你对前端开发感兴趣,尤其是React技术栈:我的新书,或许有你想看的。关注作者LucasHC,新书出版时有赠书。快乐编码!PS:作者Github仓库和知乎问答链接,欢迎各种形式的交流。