基于 React 的组件分离式挂载不同于接口或者是状态,组件的组织方式是 UI 结构的体现,是具有较强的结构层次限制的,
温馨提示:本文仅以作者熟悉的 React 体系来分享关于 “组件分离式挂载” 的内容。但我相信解决问题的思路是可以推广的,欢迎任何讨论。
故事的开始
我们从一个特别熟悉的场景开始,下面是常见的一个应用结构,由 Header 、Sider 、Content 三部分组成:
很多的组件库也是如此为我们提供 Layout 组件。
应用的结构布局
假设我们有两个路由 /about
、 /setting
分别对应着 AboutPage 、SettingPage 两个页面,常见的方式我们会如下实现:
没有任何问题,只不过我们会发现:在 AboutPage 和 SettingPage 中,对于布局,有着重复的实现 。
如果整体布局发生变化,那么就需要更改很多的页面。React Router v6 中就为我们提供了一个很好用 API 来解决这个问题。
在这个方案中我们使用了 AppLayout
组件统一定义了贯穿应用的顶层布局(Header、Silder、Content),并且在变化的部分 content 那里预留了一个 Outlet
组件,在实际渲染中,会根据路由,替换为实际的渲染组件。例如:在 /about
路由下渲染 AboutContent
组件,在 /setting
路由下渲染 SettingContent
组件 。
关于 Outlet 的更多信息,可以参考:
为什么要提这个,因为我认为 Nested Routes 和我今天想分享的 “组件分离式挂载” 主题很类似,都是通过一个预留的渲染入口,然后根据某些条件(例如:路由)来渲染相应的组件。
好了,我们继续往下看吧!
组件的分离式挂载
通过前面的例子,我们已经意识到:对于当前应用来说,Header、Sider 是通用的组件,而 Content 则会根据路由的不同来渲染不同的业务组件。
当然这是最理想的情况,很多时候,例如 Header 会因为业务的不同,展示出一些不同的变化:
面对这样的情况,我们通常会尝试一下这些解决方式:
在 Header 组件中承载业务
这里大家采用最多的方案便是:在 Header
组件中承载业务,就像下面这样。
这样的方案呢其实上是出现了 “业务侵入”,违反了软件设计中的 "开闭原则" 。
随着业务变化迭代,越来越多的业务入口加入到原本相对 “稳定” Header
组件中,甚至很多时候,还会加入部分的业务逻辑,这也是前端开发中 “熵增” 的元凶之一。
扩展 Header 组件
前面既然提到了 “开闭原则” ,那就不去修改 Header
组件,我让它拥有扩展的能力。具体来讲,就是通过 props
或者 children
来让 Header
组件来实现扩展:
这样一来,我只要在每个业务页面中使用这个 Header
组件即可,也不需要修改它了。
可是你发现了没有,这样一来,我们就没有办法使用之前提到的 Nested Routes 方案了。
因为我们需要在不同的路由下,如 /about
、 /setting
..., 调用 Header 组件传递不同业务入口,AboutButton、SettingButton ...
而且,更 “糟心” 的一点是,如果存在 <AboutContent>
和 <AboutButton>
之前的交互逻辑和状态,你都需提取到第三方状态库中或者提升至 <AboutPage>
组件中,这便是受限于组件层级结构的状态传递,因为组件的扩展方式(props 或者 children)只能在调用时传递,例如:
那有没有好的办法呢?下面就来介绍 “组件的分离式挂载”。
分离式挂载
先来看一下 “组件分离式挂载” 方案是什么样子的:
首先,在 Header 中使用
<MountConsumer>
声明这里有一个预留的挂载位置,它以 name 为标识,用以挂载视图。
在业务中,我们使用 <MountProvider>
组件来包裹 AboutButton、SettingButton 来,并以 name 标识。它们并不会在该位置渲染,而是会被 Header 中的 <MountConsumer>
组件所渲染。
MountProvider 用来提供要被渲染的组件, MountConsumer 则用来在其位置渲染那些的组件,有点类似于 React.Context 那样,只不过它们提供和消费的数据是视图组件而已。
使用 MountProvider 和 MountConumer 之后,不再需要通过 props 或者是 children ,也可以为 Header 组件增加扩展能力,从而使得我们并不需要显式调用 Header 组件来为其注入不同的扩展。
组件分离式挂载的意义
不同于接口或者是状态,组件的组织方式是 UI结构 的体现,是具有较强的结构层次限制的,而接口或者状态往往是组织在其所属的领域(模块)中。但同时,组件很多时候也具有明显的业务属性。
就拿上面的例子来说,从 UI结构上看 AboutButton、SettingButton 所属于 Header ,从业务上看它们所属于 AboutContent、SettingContent 里的内容,我们通常会从 UI 的视角上组织组件结构,这当然没有问题,并且这应该是应用中最标准的思考方式,否则也太反人类了。
可是就像今天的例子一样,在局部,往往就会出现组件在 UI 结构 和 业务领域 的所属权上产生了不一致,如果还按照 UI 结构的视角组织,就会出现 “在 Header 组件中承载业务” 和 “扩展 Header 组件” 中那样的情况。
而如果按照业务所属的方式实现,我们可以将 AboutButton 在 AboutContent 中声明,它既可以平滑地访问业务逻辑和状态,又通过 MountProvider 和 MountConsumer 可以轻易地挂载到目标组件(Header)中,而没有任何入侵。
灵感来源
设计灵感来自于 DevExtreme Reactive
中的 Template
和 TemplatePlaceholder
的使用方式。
另外,换一种思路:我们经常用到的 Modal 组件,以及 ReactDOM.createPortal 的案例:既然我们可以跨组件往 DOM 上挂,为什么不能夸组件往别的目标组件上挂载呢。
使用
可以通过 npm 包:www.npmjs.com/package/@s7…
# If you use npm:
npm install @s7n/react-mounter
# If you use yarn:
yarn add @s7n/react-mounter
转载自:https://juejin.cn/post/7216526756096901180