什么是React高阶组件?React高阶组件以高阶函数的形式包装一个需要装饰的React组件,处理后返回React组件。React高级组件在React生态系统中使用非常频繁。比如react-router中的withRouter、react-redux中的connect等很多API都是这样实现的。使用React高阶组件的好处在工作中,我们经常会遇到很多功能相似、组件代码重复的页面需求。通常我们完全复制代码就可以实现功能,但是这种页面维护的可维护性会变得极差,需要对每个页面中相同的组件进行改动。因此,我们可以把公共的部分,比如接受相同的查询操作结果,相同的标签封装在组件外等,单独做一个函数,将不同的业务组件作为子组件的参数传入,而这个函数是不会修改子组件,但是通过组合的方式将子组件包裹在容器组件中。它是一个没有副作用的纯函数,让我们可以在不改变这些组件逻辑的情况下,对这部分代码进行解耦,改进代码。可维护性。自己实现一个高阶组件前端项目,带链接的面包屑导航很常见,但是由于面包屑导航需要手动维护一个所有目录路径和目录名之间的映射数组,我们可以从这里获取所有数据react-router的路由表,所以我们可以从这里开始实现面包屑导航的高级组件。首先看一下我们的路由表提供的数据和目标面包屑组件需要的数据://这里是react-router4的路由示例letroutes=[{breadcrumb:'一级目录',path:'/a',component:require('../a/index.js').default,items:[{breadcrumb:'二级目录',path:'/a/b',component:require('../a/b/index.js').default,items:[{breadcrumb:'三级目录1',path:'/a/b/c1',component:require('../a/b/c1/index.js').default,exact:true,},{breadcrumb:'三级目录2',path:'/a/b/c2',component:require('../a/b/c2/index.js').default,exact:true,},}]}]//理想的面包屑组件//显示格式为a/b/c1并附上链接constBreadcrumbsComponent=({breadcrumbs})=>(
{breadcrumbs.map((breadcrumb,index)=>({breadcrumb}{索引<面包屑。长度-1&&/}))}
);这里我们可以看到面包屑组件需要提供三种数据,一种是当前页面Path,一种是面包屑携带的文字,一种是面包屑指向第一个的导航链接.我们可以使用react-router提供的withRouter高级组件包,让子组件通过获取当前页面属性的location来获取页面路径。后两者需要我们在路由上进行操作。首先,将路线提供的数据扁平化为面包屑导航所需的格式。我们可以使用一个函数来实现它。/***递归地展平react路由器数组*/constflattenRoutes=arr=>arr.reduce(function(prev,item){prev.push(item);returnprev.concat(Array.isArray(item.items)?flattenRoutes(item.items):项目);},[]);然后将扁平化后的目录路径映射和当前页面路径放入处理函数中,生成面包屑导航结构。exportconstgetBreadcrumbs=({flattenRoutes,location})=>{//初始化匹配数组matchletmatches=[];location.pathname//获取路径名,然后将路径拆分为各个路由部分。.split('?')[0].split('/')//对每个部分执行对`getBreadcrumb()`的reduce调用。.reduce((prev,curSection)=>{//将最后一个路由部分与当前部分合并,例如,当路径为`/x/xx/xxx`时,pathSection会检查`/x`的匹配`/x/xx``/x/xx/xxx`分别生成面包屑constpathSection=`${prev}/${curSection}`;constbreadcrumb=getBreadcrumb({flattenRoutes,curSection,pathSection,});//Import将面包屑放入匹配数组matches.push(breadcrumb);//传递给下一个reduce部分的路径returnpathSection;});返回匹配项;};然后为每个面包屑路径部分,生成目录名称并附加指向相应路由位置的链接属性。constgetBreadcrumb=({flattenRoutes,curSection,pathSection})=>{constmatchRoute=flattenRoutes.find(ele=>{const{breadcrumb,path}=ele;if(!breadcrumb||!path){thrownewError('Router中的每个路由都必须包含`path`和`breadcrumb`属性');}//查找是否匹配//exact是reactrouter4的属性,用于精确匹配路由returnmatchPath(pathSection,{path,exact:true});});//返回面包屑值,如果没有,则返回原来匹配的子路径名}//对于路由表中不存在的路径//根目录的默认名称是主页。returnrender({content:pathSection==='/'?'Homepage':curSection,path:pathSection,});};render函数生成最终的单个面包屑样式。单个面包屑组件需要提供面包屑指向的path路径的render函数,以及面包屑内容映射内容的两个props。/****/constrender=({content,path})=>{constcomponentProps={path};if(typeofcontent==='function'){return
;}返回
{content};};有了这些函数式的功能,我们就可以实现一个可以为被包装组件传入当前路径和路由属性的React高层组件。传入一个组件,返回一个相同组件的新结构,以免对组件外部的任何功能和操作造成破坏。constBreadcrumbsHoc=(location=window.location,routes=[])=>Component=>{constBreadcrumbs=(
);returnBreadComponent;};exportdefaultBreadcrumbsHoc;调用这个高层组件的方法也很简单,传入当前路径和整个reactrouter生成的routes属性即可。至于如何获取当前路径,我们可以使用reactrouter提供的withRouter函数。使用方法请参考相关文档。值得一提的是,withRouter本身是一个高级组件,可以为被包裹的组件提供包括location属性在内的多种路由属性。所以这个API也可以作为学习高级组件的一个很好的参考。withRouter(({location})=>BreadcrumbsHoc(location,routes)(BreadcrumbsComponent));4.Q&A如果react-router生成的路由不是自己手动维护的,本地根本不存在,而是requests拉取,存储在redux中,用react-redux提供的connect高阶函数包装时,当路由改变时,面包屑组件不会更新。使用方法如下:;这实际上是connect函数中的一个错误。因为react-redux的connect高层组件实现了传入参数组件的shouldComponentUpdate钩子函数,所以更新相关的生命周期函数(包括render)只有在prop发生变化时才会被触发。显然,我们的location对象并没有将参数组件作为prop传入。官方推荐的方法是用withRouter包裹connect的返回值,即withRouter(connect(mapStateToProps)(({location,routes})=>BreadcrumbsHoc(location,routes)(BreadcrumbsComponent)));其实从这里我们也可以看出,高阶组件和高阶函数一样,不会对组件的类型造成任何改变,所以高阶组件就像链式调用一样,可以被包裹在任意数量的层中以将不同的属性传递给组件。一般情况下也可以随意改变位置,使用起来非常灵活。这种可插拔的特性使得高级组件在React生态系统中非常受欢迎。在很多开源库中都能看到这个特性的影子,有空可以拿出来分析一下。