利用 React 高阶组件实现一个面包屑导航
什么是 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)
);
4. 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生态的青睐,很多开源库里都能看到这种特性的影子,有空也可以都拿出来分析一下。
本文由博客群发一文多发等运营工具平台 OpenWrite 发布
利用 React 高阶组件实现一个面包屑导航的更多相关文章
- React高阶组件学习笔记
高阶函数的基本概念: 函数可以作为参数被传递,函数可以作为函数值输出. 高阶组件基本概念: 高阶组件就说接受一个组件作为参数,并返回一个新组件的函数. 为什么需要高阶组件 多个组件都需要某个相同的功能 ...
- 聊聊React高阶组件(Higher-Order Components)
使用 react已经有不短的时间了,最近看到关于 react高阶组件的一篇文章,看了之后顿时眼前一亮,对于我这种还在新手村晃荡.一切朝着打怪升级看齐的小喽啰来说,像这种难度不是太高同时门槛也不是那么低 ...
- 当初要是看了这篇,React高阶组件早会了
当初要是看了这篇,React高阶组件早会了. 概况: 什么是高阶组件? 高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式. 具体地说 ...
- react:高阶组件wrappedComponent
什么是高阶组件? 高阶部件是一种用于复用组件逻辑的高级技术,它并不是 React API的一部分,而是从React 演化而来的一种模式. 具体地说,高阶组件就是一个接收一个组件并返回另外一个新组件的函 ...
- react高阶组件的一些运用
今天学习了react高阶组件,刚接触react学习起来还是比较困难,和大家分享一下今天学习的知识吧,另外缺少的地方欢迎补充哈哈 高阶组件(Higher Order Components,简称:HOC) ...
- react 高阶组件的 理解和应用
高阶组件是什么东西 简单的理解是:一个包装了另一个基础组件的组件.(相对高阶组件来说,我习惯把被包装的组件称为基础组件) 注意:这里说的是包装,可以理解成包裹和组装: 具体的是高阶组件的两种形式吧: ...
- react高阶组件的理解
[高阶组件和函数式编程] function hello() { console.log('hello jason'); } function WrapperHello(fn) { return fun ...
- 函数式编程与React高阶组件
相信不少看过一些框架或者是类库的人都有印象,一个函数叫什么creator或者是什么什么createToFuntion,总是接收一个函数,来返回另一个函数.这是一个高阶函数,它可以接收函数可以当参数,也 ...
- React高阶组件总结
在多个不同的组件中需要用到相同的功能,这个解决方法,通常有Mixin和高阶组件. Mixin方法例如: //给所有组件添加一个name属性 var defaultMixin = { getDefaul ...
随机推荐
- Java实现 LeetCode 28 实现strStr()
28. 实现 strStr() 实现 strStr() 函数. 给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 ...
- Java实现 洛谷 P1009 阶乘之和
import java.util.Scanner; public class 阶乘之和 { public static void main(String[] args) { Scanner sc = ...
- java实现第七届蓝桥杯机器人塔
机器人塔 X星球的机器人表演拉拉队有两种服装,A和B. 他们这次表演的是搭机器人塔. 类似: A B B A B A A A B B B B B A B A B A B B A 队内的组塔规则是: A ...
- 小师妹学JavaIO之:文件系统和WatchService
目录 简介 监控的痛点 WatchService和文件系统 WatchSerice的使用和实现本质 总结 简介 小师妹这次遇到了监控文件变化的问题,F师兄给小师妹介绍了JDK7 nio中引入的Watc ...
- Action的三种实现方式,struts.xml配置的详细解释及其简单执行过程(二)
勿以恶小而为之,勿以善小而不为--------------------------刘备 劝诸君,多行善事积福报,莫作恶 上一章简单介绍了Struts2的'两个蝴蝶飞,你好' (一),如果没有看过,请观 ...
- 对学长TD课程通(.apk)的评价
界面简介: 界面分为三个部分,第一部分是对课程表的添加,第二部分是对空闲教室的查询,第三部分是实时时间的展示和对软件的评价,实用性很强,仅限于本校学生,界面观赏性较差,从界面上对人的吸引力不够, 实用 ...
- 宇宙第一IDE是谁?
更多精彩文章,尽在码农翻身 微服务把我坑了 如何降低程序员的工资? 程序员,你得选准跑路的时间! 两年,我学会了所有的编程语言! 一直CRUD,一直996,我烦透了,我要转型 字节码万岁! 上帝托梦给 ...
- 关于Java优质代码的那些事
以前别人告诉我,代码都是粘贴复制,然后写多了,就有了自己的思想,然后1,2年过去了,我的代码质量并没有什么提高,问了一些博客园里的前辈,前辈们都是语重心长的说:"少年,多看书呀!" ...
- 记录一次vue 访问空白的排错
访问vue项目页面空白 场景 内网访问访问url很快就可以打开页面,外网访问一片浏览器端一片空白 排查思路 [x] 由于不熟悉vue 先看了nginx的配置,以为是nginx的配置导致的 [x] 百度 ...
- C#数据结构与算法系列(四):链表——单链表(Single-LinkedList)
1.介绍: 链表是有序的列表,但是它在内存的存储如下: 链表是以节点的方式来存储,链式存储 每一个节点包含data域,next域:指向下一个节点 链表的各个节点不一定是连续存储 链表分带头节点的链表 ...