利用React高阶组件实现一个面包屑导航的示例


Posted in Javascript onAugust 23, 2020

什么是 React 高阶组件

React 高阶组件就是以高阶函数的方式包裹需要修饰的 React 组件,并返回处理完成后的 React 组件。React 高阶组件在 React 生态中使用的非常频繁,比如react-router 中的 withRouter 以及 react-redux 中 connect 等许多 API 都是以这样的方式来实现的。

使用 React 高阶组件的好处

在工作中,我们经常会有很多功能相似,组件代码重复的页面需求,通常我们可以通过完全复制一遍代码的方式实现功能,但是这样页面的维护可维护性就会变得极差,需要对每一个页面里的相同组件去做更改。因此,我们可以将其中共同的部分,比如接受相同的查询操作结果、组件外同一的标签包裹等抽离出来,做一个单独的函数,并传入不同的业务组件作为子组件参数,而这个函数不会修改子组件,只是通过组合的方式将子组件包装在容器组件中,是一个无副作用的纯函数,从而我们能够在不改变这些组件逻辑的情况下将这部分代码解耦,提升代码可维护性。

自己动手实现一个高阶组件

前端项目里,带链接指向的面包屑导航十分常用,但由于面包屑导航需要手动维护一个所有目录路径与目录名映射的数组,而这里所有的数据我们都能从 react-router 的路由表中取得,因此我们可以从这里入手,实现一个面包屑导航的高阶组件。

首先我们看看我们的路由表提供的数据以及目标面包屑组件所需要的数据:

// 这里展示的是 react-router4 的route示例
let routes = [
 {
  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 并都附上链接
const BreadcrumbsComponent = ({ breadcrumbs }) => (
 <div>
  {breadcrumbs.map((breadcrumb, index) => (
   <span key={breadcrumb.props.path}>
    <link to={breadcrumb.props.path}>{breadcrumb}</link>
    {index < breadcrumbs.length - 1 && <i> / </i>}
   </span>
  ))}
 </div>
);

这里我们可以看到,面包屑组件需要提供的数据一共有三种,一种是当前页面的路径,一种是面包屑所带的文字,一种是该面包屑的导航链接指向。

其中第一种我们可以通过 react-router 提供的 withRouter 高阶组件包裹,可使子组件获取到当前页面的 location 属性,从而获取页面路径。

后两种需要我们对 routes 进行操作,首先将 routes 提供的数据扁平化成面包屑导航需要的格式,我们可以使用一个函数来实现它。

/**
 * 以递归的方式展平react router数组
 */
const flattenRoutes = arr =>
 arr.reduce(function(prev, item) {
  prev.push(item);
  return prev.concat(
   Array.isArray(item.items) ? flattenRoutes(item.items) : item
  );
 }, []);

之后将展平的目录路径映射与当前页面路径一同放入处理函数,生成面包屑导航结构。

export const getBreadcrumbs = ({ flattenRoutes, location }) => {
 // 初始化匹配数组match
 let matches = [];

 location.pathname
  // 取得路径名,然后将路径分割成每一路由部分.
  .split('?')[0]
  .split('/')
  // 对每一部分执行一次调用`getBreadcrumb()`的reduce.
  .reduce((prev, curSection) => {
   // 将最后一个路由部分与当前部分合并,比如当路径为 `/x/xx/xxx` 时,pathSection分别检查 `/x` `/x/xx` `/x/xx/xxx` 的匹配,并分别生成面包屑
   const pathSection = `${prev}/${curSection}`;
   const breadcrumb = getBreadcrumb({
    flattenRoutes,
    curSection,
    pathSection,
   });

   // 将面包屑导入到matches数组中
   matches.push(breadcrumb);

   // 传递给下一次reduce的路径部分
   return pathSection;
  });
 return matches;
};

然后对于每一个面包屑路径部分,生成目录名称并附上指向对应路由位置的链接属性。

const getBreadcrumb = ({ flattenRoutes, curSection, pathSection }) => {
 const matchRoute = flattenRoutes.find(ele => {
  const { breadcrumb, path } = ele;
  if (!breadcrumb || !path) {
   throw new Error(
    'Router中的每一个route必须包含 `path` 以及 `breadcrumb` 属性'
   );
  }
  // 查找是否有匹配
  // exact 为 react router4 的属性,用于精确匹配路由
  return matchPath(pathSection, { path, exact: true });
 });

 // 返回breadcrumb的值,没有就返回原匹配子路径名
 if (matchRoute) {
  return render({
   content: matchRoute.breadcrumb || curSection,
   path: matchRoute.path,
  });
 }

 // 对于routes表中不存在的路径
 // 根目录默认名称为首页.
 return render({
  content: pathSection === '/' ? '首页' : curSection,
  path: pathSection,
 });
};

之后由 render 函数生成最后的单个面包屑导航样式。单个面包屑组件需要为 render 函数提供该面包屑指向的路径 path, 以及该面包屑内容映射content 这两个 props。

/**
 *
 */
const render = ({ content, path }) => {
 const componentProps = { path };
 if (typeof content === 'function') {
  return <content {...componentProps} />;
 }
 return <span {...componentProps}>{content}</span>;
};

有了这些功能函数,我们就能实现一个能为包裹组件传入当前所在路径以及路由属性的 React 高阶组件了。传入一个组件,返回一个新的相同的组件结构,这样便不会对组件外的任何功能与操作造成破坏。

const BreadcrumbsHoc = (
 location = window.location,
 routes = []
) => Component => {
 const BreadComponent = (
  <Component
   breadcrumbs={getBreadcrumbs({
    flattenRoutes: flattenRoutes(routes),
    location,
   })}
  />
 );
 return BreadComponent;
};
export default BreadcrumbsHoc;

调用这个高阶组件的方法也非常简单,只需要传入当前所在路径以及整个 react router 生成的 routes 属性即可。
至于如何取得当前所在路径,我们可以利用 react router 提供的 withRouter 函数,如何使用请自行查阅相关文档。
值得一提的是,withRouter 本身就是一个高阶组件,能为包裹组件提供包括 location 属性在内的若干路由属性。所以这个 API 也能作为学习高阶组件一个很好的参考。

withRouter(({ location }) =>
 BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
);

Q&A

如果react router 生成的 routes 不是由自己手动维护的,甚至都没有存在本地,而是通过请求拉取到的,存储在 redux 里,通过 react-redux 提供的 connect 高阶函数包裹时,路由发生变化时并不会导致该面包屑组件更新。使用方法如下:

function mapStateToProps(state) {
 return {
  routes: state.routes,
 };
}

connect(mapStateToProps)(
 withRouter(({ location }) =>
  BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
 )
);

这其实是 connect 函数的一个bug。因为 react-redux 的 connect 高阶组件会为传入的参数组件实现 shouldComponentUpdate 这个钩子函数,导致只有 prop 发生变化时才触发更新相关的生命周期函数(含 render),而很显然,我们的 location 对象并没有作为 prop 传入该参数组件。

官方推荐的做法是使用 withRouter 来包裹 connect 的 return value,即

withRouter(
 connect(mapStateToProps)(({ location, routes }) =>
  BreadcrumbsHoc(location, routes)(BreadcrumbsComponent)
 )
);

其实我们从这里也可以看出,高阶组件同高阶函数一样,不会对组件的类型造成任何更改,因此高阶组件就如同链式调用一样,可以任意多层包裹来给组件传入不同的属性,在正常情况下也可以随意调换位置,在使用上非常的灵活。这种可插拔特性使得高阶组件非常受 React 生态的青睐,很多开源库里都能看到这种特性的影子,有空也可以都拿出来分析一下。

到此这篇关于利用React高阶组件实现一个面包屑导航的示例的文章就介绍到这了,更多相关React 面包屑导航内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
JavaScript写的一个DIV 弹出网页对话框
Aug 14 Javascript
jquery validate使用攻略 第四步
Jul 01 Javascript
Jquery右下角抖动、浮动 实例代码(兼容ie6、FF)
Aug 15 Javascript
Jquery $.getJSON 在IE下的缓存问题解决方法
Oct 10 Javascript
jQuery中cookie插件用法实例分析
Dec 04 Javascript
JavaScript学习小结之被嫌弃的eval函数和with语句实例详解
Aug 01 Javascript
vue、react等单页面项目应该这样子部署到服务器
Jan 03 Javascript
Angular @HostBinding()和@HostListener()用法
Mar 05 Javascript
vue实现弹框遮罩点击其他区域弹框关闭及v-if与v-show的区别介绍
Sep 29 Javascript
JavaScript函数Call、Apply原理实例解析
Feb 17 Javascript
解决vue请求接口第一次成功,第二次失败问题
Sep 08 Javascript
Javascript实现贪吃蛇小游戏(含详细注释)
Oct 23 Javascript
vue中watch和computed的区别与使用方法
Aug 23 #Javascript
vue动态设置页面title的方法实例
Aug 23 #Javascript
Vue管理系统前端之组件拆分封装详解
Aug 23 #Javascript
Vue中keep-alive组件的深入理解
Aug 23 #Javascript
google广告之另类js调用实现代码
Aug 22 #Javascript
JS typeof fn === 'function' &amp;&amp; fn()详解
Aug 22 #Javascript
js+canvas实现图片格式webp/png/jpeg在线转换
Aug 22 #Javascript
You might like
php执行sql语句的写法
2009/03/10 PHP
php数组中删除元素的实现代码
2012/06/22 PHP
深入eAccelerator与memcached的区别详解
2013/06/06 PHP
php支付宝手机网页支付类实例
2015/03/04 PHP
ThinkPHP3.2.3实现分页的方法详解
2016/06/03 PHP
ThinkPHP框架实现导出excel数据的方法示例【基于PHPExcel】
2018/05/12 PHP
laravel 验证错误信息到 blade模板的方法
2019/09/29 PHP
jquery实现搜索框常见效果的方法
2015/01/22 Javascript
JavaScript中的继承之类继承
2016/05/01 Javascript
Bootstrap Validator 表单验证
2016/07/25 Javascript
[原创]JavaScript语法高亮插件highlight.js用法详解【附highlight.js本站下载】
2016/11/01 Javascript
基于jQuery实现咖啡订单管理简单应用
2017/02/10 Javascript
JavaScript运动框架 解决速度正负取整问题(一)
2017/05/17 Javascript
快速掌握jquery分页插件jqPaginator的使用方法
2017/08/09 jQuery
JavaScript基础进阶之数组方法总结(推荐)
2017/09/04 Javascript
原生JS实现日历组件的示例代码
2017/09/22 Javascript
vue学习笔记之slot插槽用法实例分析
2020/02/29 Javascript
Python2.5/2.6实用教程 入门基础篇
2009/11/29 Python
python处理文本文件并生成指定格式的文件
2014/07/31 Python
python删除指定类型(或非指定)的文件实例详解
2015/07/06 Python
python查看微信好友是否删除自己
2016/12/19 Python
python中print()函数的“,”与java中System.out.print()函数中的“+”功能详解
2017/11/24 Python
python生成器,可迭代对象,迭代器区别和联系
2018/02/04 Python
Django自定义用户认证示例详解
2018/03/14 Python
在python环境下运用kafka对数据进行实时传输的方法
2018/12/27 Python
调试Django时打印SQL语句的日志代码实例
2019/09/12 Python
实现Python与STM32通信方式
2019/12/18 Python
Pytorch中的自动求梯度机制和Variable类实例
2020/02/29 Python
英国最专业的健身器材供应商之一:Best Gym Equipment
2017/12/22 全球购物
美国最值得信赖的宠物药房:Allivet
2019/03/23 全球购物
会计应聘求职信范文
2013/12/17 职场文书
毕业生的自我评价分享
2013/12/18 职场文书
离婚协议书该怎么写
2014/10/04 职场文书
2015年宣传部部长竞选演讲稿
2014/11/28 职场文书
OpenCV-Python实现人脸美白算法的实例
2021/06/11 Python
Windows Server 2012 R2 磁盘分区教程
2022/04/29 Servers