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

什么是 React 高阶组件

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

<!-- more -->react

使用 React 高阶组件的好处

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

本身动手实现一个高阶组件

前端项目里,带连接指向的面包屑导航十分经常使用,但因为面包屑导航须要手动维护一个全部目录路径与目录名映射的数组,而这里全部的数据咱们都能从 react-router 的路由表中取得,所以咱们能够从这里入手,实现一个面包屑导航的高阶组件。数组

首先咱们看看咱们的路由表提供的数据以及目标面包屑组件所须要的数据: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 提供的数据扁平化成面包屑导航须要的格式,咱们可使用一个函数来实现它。ui

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

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

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;
};

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

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 来包裹 connectreturn value,即

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

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

相关文章
相关标签/搜索