资讯专栏INFORMATION COLUMN

漫谈 React 组件库开发(一):多层嵌套弹层组件

warmcheng / 1374人阅读

摘要:引言组件中有很多弹出式组件,常见的如,以及等。这样一种层次结构在实践中大大降低了各类弹层组件的实现和维护成本。但是的组件实现了一个大多数组件库都没有实现的功能弹层的嵌套处理。

引言

UI 组件中有很多弹出式组件,常见的如 DialogTooltip 以及 Select 等。这些组件都有一个特点,它们的弹出层通常不是渲染在当前的 DOM 树中,而是直接插入在 body (或者其它类似的地方)上的。这么做的主要目的是方便控制这些弹出层的 z-index ,确保它们能够处于合适的层级上,不至于被遮挡。

我们都知道 React App 的顶层某个地方肯定有这么一行代码:ReactDOM.render(, mountNode),这个 API 调用的作用是在 mountNode 的位置创建一棵 React 的渲染树,React 会接管 mountNode 开始的这棵 DOM 树。

在 React 的这种管理模式下,会发现使用弹层似乎不太方便,因为组件树是逐层往下生长的,但React 的 API 中并没有直接提供跳出这棵组件树的方法[注1]

所以,为了实现弹层组件,我们需要先实现一个 Portal 组件(玩游戏的都知道,这是传送门的意思),这个组件只做一件事:将组件树中某些节点移出当前的DOM 树,并且渲染到指定的 DOM 节点中。

Portal 组件

Portal 组件的要做的事情很简单,render 函数因为不需要在当前位置输出任何东西,所以直接返回 null 就可以了,剩下的就是在组件的生命周期中去手动管理要渲染到指定位置的那些组件。

// 简化的 Portal 实现
class Portal extends Component {
  static propTypes = {
    children: PropTypes.node.isRequired,
    container: PropTypes.object.isRequired
  };

  render() {
    return null;
  }

  componentDidMount() {
    const { children, container } = this.props;
    mountChildrenAtNode(children, container);
  }

  componentWillUnmount() {
    const { container } = this.props;
    unmountChildrenAtNode(container);
  }
}

剩下唯一的问题是 mountChildrenAtNode 这个函数怎么实现?仔细的同学应该已经发现了,这个函数和 ReactDOM.render 非常像,仔细一想,其实它们做的事情就是一样的。所以我们直接用 ReactDOM.render 去替换 mountChildrenAtNode 就可以了。

那么真的这么简单吗?

是,但也不是。

说是,是因为逻辑上这代码并没有什么问题,而且大部分场景下是确实可以完美工作。

说不是,是因为剩下的小部分场景下这段代码确实存在很严重的问题。

那么问题是什么呢?

别急,我们先聊点别的。

相信大部分 React 开发者都用过 redux(至少听过吧),react-redux 这个 binding 库提供了连接 React 和 redux 的一个桥梁。react-redux 的实现依赖 React 很有用的一个功能Context,简单来说 context 就是提供了一个方便的跨越层级往下传递数据的方式。

ReactDOM.render 的问题正是在于这个 context 的功能,它无法连接两棵 React 组件树的 context

ReactDOM.render 的函数原型中并没有当前组件树的信息,而 context 是跟组件树有关的。

ReactDOM.render(
  element,
  container,
  [callback]
)

解决这个问题的方法也很简单,这里也不卖关子了,React 提供了另一个非公开 API:ReactDOM.unstable_renderSubtreeIntoContainer。这个 API 多了一个参数,这个参数就是用来指定新的 React 组件树根节点的父组件的,有了这个参数,两棵本来互不相干的 React 组件树就被联系起来了,同时它们的 context 也连接了起来。

ReactDOM.unstable_renderSubtreeIntoContainer(
  parentComponent,
  element,
  container,
  [callback]
)

想更好的了解 Context 的同学可以自己 Google,这不是本文重点,这里不做展开了。

Portal 组件的可扩展性

不同的 UI 组件对弹层可能会有不同的功能需求,举个例子, Dialog 组件需要在弹出的时候禁止页面滚动,同时有些场景下需要支持点击背景部分关闭,或者按 ESC 键关闭。

这些很细节的功能点往往会出现需要不同组合的使用场景,例如只需要禁止滚动,或者同时需要禁止滚动和 ESC 键关闭。

一个很自然的想法是在 Portal 组件上加几个可配置的 props 来控制这些功能。这么做有个问题,不管用户需不需要,代码都在那里。

更好的方式是通过高阶组件(HOC)的方式让使用者自己去组合这些功能,这样子没有用到的功能并不会出现在最终的代码中。

说了这么多关于 Portal 组件的实现细节,有兴趣的同学可以去看看有赞的组件库 Zent 里面的 Portal 是如何实现的,大体上就是按上面说的那些方案做的。

弹层组件

有了 Portal 组件之后,基本上所有弹层组件都可以基于 Portal 去实现。例如 Dialog 无非就是在 Portal 组件的基础上加了一些 CSS 样式。复杂一点的组件例如 Select,需要实现一些触发逻辑来控制弹层的打开和关闭,比如 click 打开或者 hover 打开。我们接下来要讨论的弹层组件正是特指类似 Select 中的这些弹层。

在 Zent 里面有一个叫 Popover 的组件来处理这些复杂的弹层场景,Popover 封装了常用的触发逻辑,例如 click, hover, focus,同时 Popover 的触发机制是可扩展的,使用者可以实现自己的触发逻辑。

Popover 组件提供的另外一个重要功能是弹层的定位能力,也就是相对于 Trigger 的一个定位功能。除了内置的十几种定位算法,使用者可以实现自己的定位算法来实现特殊场景下的需求。

有了 Popover 组件提供的触发逻辑以及弹层定位这两个功能之后,类似 Tooltip , Select 这样的组件在实现时就完全不需要关心弹层的事了,只需要实现弹层内的组件逻辑就行了。

这里已经能够看出一个层次化的弹层组件设计了:Portal 负责脱离组件树,PopoverPortal 的基础上提供了更丰富的功能逻辑,其它组件又在 Popover 的基础上去做封装。这样一种层次结构在实践中大大降低了各类弹层组件的实现和维护成本。

在组件库的设计中,这种对能力的抽象封装是很重要的,在提高开发效率的同时也保证了各个组件行为的一致性。

干货:弹层组件的嵌套处理

上面介绍的弹层组件实现细节上并没有特别之处,成熟的组件库基本都是用类似方式实现的。但是 Zent 的 Popover 组件实现了一个大多数 React 组件库都没有实现的功能:弹层的嵌套处理。

如果你还没有明白这里的弹层嵌套是什么意思,没关系,给你举个例子就明白了。

如下图,点击按钮之后会弹出一个气泡,这个气泡中又有一个时间选择器,所谓的弹层嵌套指的就是这种弹层之中又嵌了弹层的场景。正常的操作逻辑是鼠标点击位置1的时候气泡和时间选择器同时关闭,但是点击位置2的时候应该只有时间选择器关闭。

上面提到的点击两个不同位置的不同行为其实就是弹层嵌套最主要的问题:上级的弹层组件应该知道哪个区域是属于下级弹层组件的。

由于弹层组件的特殊性,它们在 DOM 树中的位置跟它们实际的层次以及包含关系是没有必然联系的,上图中的两个弹层是body 下面的两个兄弟节点,但从弹层的角度看它们是有层次关系的,并不是并列的。

通常来说,弹层的层次结构也是一个树状结构,那么处理嵌套问题最直接的想法就是每个弹层组件都各自维护一个子弹层的列表。当需要判断点击是否在弹层外面时,不光要考虑当前弹层对应的 DOM 节点,还要考虑它的下级弹层对应的 DOM 节点。

这种方式处理的话需要手动维护这棵弹层的层级关系树,包括树中节点的插入/删除,这些操作都不是很难。这个方法最大的问题在于,在 React 的体系内一个弹层组件很难跟不是它直接孩子(direct child)的子弹层交互。

Zent 的 Popover 组件并没有直接去维护这棵层级关系树,而是利用了 React 中 context 的层级关系来避免自己去维护这棵树。使用 context 的另一个附带好处是,和非直接孩子的交互也不再是问题,因为 context 本身就是可以跨层级传递信息的。Popover 的层级管理结构示意图如下:

 *                context                       context
 *                ------>                       ------>
 * Popover Root               Popover child                    Popover grand-child     ......
 *                <------                       <------
 *             isOutsideQuery                isOutsideQuery

就是这么一个很简单的设计解决了 Zent 中弹层组件的层级嵌套问题,想了解实现细节的同学可以看 Popover 的源码。

总结

弹层组件是 UI 组件库中很重要的部分,一个逐层抽象的结构可以极大简化这些组件的开发和维护成本。

合理利用 React 的 context 功能可以很方便地解决一些像嵌套弹层一样看似很麻烦的问题。

如果觉得有所收获,请给 Zent 点个 star 吧。

*注1: React Fiber 中提供了一个新的 API:ReactDOM. unstable_createPortal ,这个 API 可以将一个组件渲染到指定的 DOM 节点内。

本文由 李晨 首发于 有赞技术博客。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/88434.html

相关文章

  • 2017-09-08 前端日报

    摘要:前端日报精选低成本将你的网站切换为漫谈组件库开发一多层嵌套弹层组件可作的备胎深入理解进阶系列如何设计中文刷题系列前端笔试面试题知乎专栏个拯救前端开发者的工具库和资源众成翻译前端技术大会震撼登陆,明星团队讲师倾城而出前端组件库我们做 2017-09-08 前端日报 精选 低成本将你的网站切换为 HTTPS漫谈 React 组件库开发(一):多层嵌套弹层组件Preact: 可作React的...

    Sanchi 评论0 收藏0
  • 2017-08-26 前端日报

    摘要:前端日报精选译中一些超级好用的内置方法漫谈组件库开发一多层嵌套弹层组件高阶组件浅析的工厂函数打包优化之速度篇中文教程用纯实现跳跳球动画众成翻译个帮助你学习的快速且久经考验的技巧众成翻译自定义属性使用进行动态更改众成翻译真假值知多 2017-08-26 前端日报 精选 【译】ES6中一些超!级!好!用!的内置方法漫谈 React 组件库开发(一):多层嵌套弹层组件React 高阶组件浅析...

    lykops 评论0 收藏0
  • React开发中提升幸福度的些小技巧

    摘要:又一篇来自日常开发的汇总各位客官请对号入席,店小二逐一上菜。解决方案有很多种,例如把字符串数组等重组对象数组,每个元素设置一个唯一等。另外有个方式推荐使用生成唯一的数组,和数据数组一起使用,省去提交数据时再重组数组。 又一篇来自日常开发的汇总:各位客官请对号入席,店小二逐一上菜。 第一道菜:回锅肉 react数组循环,基本都会设置一个唯一的key,表格的对象数组循环一般没什么问题,数据...

    smartlion 评论0 收藏0
  • 读zent源码之Dialog组件实现

    摘要:但是,最后一步,事件怎么绑定呢这块没有深入研究了,不过我想,应该这样去实现也是没有问题的。的具体做法是,把方法放到了一个叫做的组件上去实现这个功能,然后再把内容放进这个组件。其他的逻辑比如显示隐藏之类,全部都放到组件自身上去实现。 1、Dialog组件提供什么功能,解决什么问题? zent的Dialog组件,使用姿势是这样的(代码摘自zent官方文档:https://www.youza...

    陈江龙 评论0 收藏0
  • react进阶漫谈

    摘要:父组件向子组件之间非常常见,通过机制传递即可。我们应该听说过高阶函数,这种函数接受函数作为输入,或者是输出一个函数,比如以及等函数。在传递数据的时候,我们可以用进一步提高性能。 本文主要谈自己在react学习的过程中总结出来的一些经验和资源,内容逻辑参考了深入react技术栈一书以及网上的诸多资源,但也并非完全照抄,代码基本都是自己实践,主要为平时个人学习做一个总结和参考。 本文的关键...

    neuSnail 评论0 收藏0

发表评论

0条评论

warmcheng

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<