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

什么是 React 高阶组件

React 高阶组件就是以高阶函数的方式包裹需要修饰的 React 组件,并返回处理完成后的 React 组件。React 高阶组件在 React 生态中使用的非常频繁,比如react-router 中的 withRouter 以及 react-reduxconnect 等许多 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)
);

4. Q&A

  1. 如果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 来包裹 connectreturn value,即

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

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

(0)

相关推荐

  • 一切前端概念,都是纸老虎

    (给前端大学加星标,提升前端技能.) 作者:姜小抖 https://zhuanlan.zhihu.com/p/53599723 这篇文章试着聊明白这一堆看起来挺复杂的东西.在聊之前,大家要始终记得一句 ...

  • React Ref 其实是这样的

    ref 的由来 在典型的 React 数据流中,props 是父组件与子组件交互的唯一方式.要修改一个子组件,你需要使用新的 props 来重新渲染它.但是,在某些情况下,你需要在典型数据流之外强制修 ...

  • React Hooks

    Hooks介绍 之前没有用hooks写react的时候,用class类组件,state在构造函数定义,然后是钩子函数. 在这里,发现用的是函数组件,useState来定义state,useEffect ...

  • 微前端(Micro Frontend ) 落地实施的一些具体例子

    前文微前端概述(Micro Frontends) 以及相比单体应用,微前端能带来什么好处 简单介绍了微前端的概念,本文来看一个具体的应用例子. 原文地址 想象一个网站,客户可以在其中订购外卖食品.从表 ...

  • 一文讲解关于Vue和React区别

    这篇文章主要讲述了了Vue和React它们的不同之处的一些思考,不仅局限于它们本身,也会包括比如Vuex/Redux等经常搭配使用的工具.因为涉及到的内容很多,请认真对待接下来的内容哦! 监听数据变化 ...

  • ReactHook快速上车

    React16.8开始内置了10个Hook,核心是2个: 状态管理:useState 副作用管理:useEffect 有状态的函数 useState 有状态组件写法: class Example ex ...

  • 猎者营:面包屑导航是什么意思?

    在我们的网站seo优化的过程中,其实很多都是对网站细节上的优化和建设,比如说面包屑导航,这是网站优化当中的一个重中之重,只不过现在很多的网站主题开发者都会在主题中设置好这个,所以没什么人谈起这个话题, ...

  • 南山谷主:面包屑导航是什么意思?对SEO有什么作用?

    面包屑导航是什么意思 面包屑导航(BreadcrumbNavigation)这个概念来自童话故事"汉赛尔和格莱特",当汉赛尔和格莱特穿过森林时,不小心迷路了,但是他们发现在沿途走过 ...

  • 什么是面包屑导航,它对SEO的作用有哪些?

    众所周知,面包屑导航设计思路来源于一则童话故事,在不小心迷失方向后,主人公利用沿途撒下的面包屑,顺利的找到回家的路,这就是面包屑导航的原型. 它几乎是所有优秀网站的重要组成部分,不仅可以让访客清晰的知 ...

  • 仅需一个公式,即可深度解析“频闪拍摄”高阶技巧(实战干货篇)

    我想大家都看过一些舞蹈表演中所使用的闪光特效.通过闪光灯的快速闪烁,流畅的动作被分解成一系列的运动定格效果,就像老式电影的胶片重叠一样.现在,我们也能用相机的静态图像做到这一点.在一帧内创建一系列运动 ...

  • “快马”中骏商管,何以成为又一个上市的高阶玩家?

    微信排版/kasiwa sama 封面图&头图来自/中骏商管 ▼  ▼  ▼  ▼  ▼   ▼  ▼  ▼  ▼ 商管公司拆分上市潮,继续汹涌. 6月6日晚,中骏集团发布了建议分拆中骏商管于 ...

  • 如何保证面包、蛋糕等高水分产品有一个理想的货架期?

        为什么会引起蛋糕.面包等高水分产品发霉?     怎样才能保证面包.蛋糕等高水分产品达到理想的货架期?     根据HACCP原理,我们知道面包.蛋糕等高水分的关键控制点为:焙烤.冷却.包装, ...

  • 跑步高阶:如何利用先天耐力和先天爆发力优势,进行马拉松训练

    发掘力量最深处,点击关注 Advanced 训练的方法和先天优势密切相关,特别是当跑步遇到瓶颈的时候. 同样的速度训练,有的人斗志昂扬,有的人谈之变色:同样是耐力训练,有的人游刃有余,有的人痛不欲生. ...

  • 跑步高阶:利用人体生物节律来优化你的训练项目

    发掘力量最深处,点击关注 On Running 人体生物节律周期,了解你的情绪.智力.体力的起起伏伏曲线,决定了你可以利用的内部动力和外部动力. 今天我们聊一个有点儿跨界的话题,估计你从任何书上也看不 ...

  • 高阶谋略:如何利用矛盾?孔子高徒的两句话极精辟,值得反复回味

    哲人讲:"矛盾是普遍存在的."同样面对各种矛盾冲突,不同的人会选择不同的方式.愚蠢的人会选择视而不见,普通人会选择躲闪回避,而谋略家则与大众的思维截然不同,他们会把矛盾视之为宝,以 ...