如果是使用 React 推荐做法来实现数据共享,那么我们就需要在保证各个业务组件依旧可以挂载在页面不同的 DOM 节点的前提下,将所有业务组件都放在同一颗 React Tree 下,因为只有所有业务组件都在同一颗 React Tree 下时才能让 React 的事件冒泡、状态共享、React 的生命周期按照预期进行工作。所以我们首先需要将多入口打包的方式改成单入口打包,至少针对单页面是这样的。多入口打包的方式改成单入口打包非常简单,直接改 webpack 的配置就 ok 了。然后接着解决如何保证在同一颗 React Tree 的前提下将不同的业务组件挂载在不同的 DOM 节点。
再简单说明一下我们现在需要解决的问题。我们都知道将一个 React APP 应用挂载在某个 DOM 节点就是直接 ReactDOM.render(<App />, targetElement) 就好了,但是业务组件各自都有各自不同的挂载 DOM 节点,如果业务组件都各自执行 ReactDOM.render 的话,那就不能保证所有业务组件都在同一颗 React Tree 下,也就不能让 React 的事件冒泡、状态共享、React 的生命周期按照预期进行工作了。
所以接下来我们要解决的问题就是:如何保证让不同的业务组件可以挂载在不同的 DOM 节点的前提下,他们依旧是在同一颗 React Tree 下的呢?
最终我们发现 ReactDOM.createPortal 可以将组件放在 HTML 的任意 DOM 中,被 Portal 的组件行为和普通的 React 子节点行为一致,因为它仍然在 React Tree 中, 且与 DOM Tree 中的位置无关,也就是说像 context 、事件冒泡以及 React 的生命周期这样的 Feature 依旧可以使用。
传送门可以将组件放在 HTML 的任意 DOM 中,被 Portal 的组件行为和普通的 React、Vue 子节点行为一致,因为它仍然在 React、Vue Tree 中, 且与 DOM Tree 中的位置无关,也就是说像 context 、事件冒泡以及 React、Vue 的生命周期这样的 Feature 依旧可以使用。
事件冒泡正常工作 —— 通过将事件传播到 React 树的祖先节点,事件冒泡将按预期工作,而与 DOM 中的 Portal 节点位置无关。
前言
码过的每一个需求、踩过的每一个坑、修过的每一个
bug
、学过的每一个知识以及看过的每一篇文章都不会成为无用功,它们都将为自己的技术城堡添砖加瓦。今天我们将从实现不同的React、Vue App
之间的状态共享这个需求着手,学习React
、Vue
中那些我们很少用到,但是一旦遇到这些特殊的需求就非它莫属的特性 🤹🏻需求 & 问题
需求现状
我在字节的日常业务开发中,我需要将不同的业务组件挂载在一个不属于我们接管的平台页面中,由于每个业务组件都有各自不同的挂载位置和时机,并且都可以看做一个单独的
React
应用,所以我们用Webpack
进行多入口打包,打出多个React
应用,然后在这个页面通过引入sdk
的方式挂载业务组件。问题
多入口打包这样的做法会导致业务组件内部状态可以共享,但是各个业务组件之间的状态无法很好的共享。并且每个组件内部可能需要相同的数据,所以会导致相同的网络请求会在同一个页面发送多次的情况。
所以我们面临问题以及最终目的就是解决多个
React
应用之间的状态共享:DOM
节点的业务组件间共享(访问 + 更新)解决方案
一、将状态挂载在全局 window 对象、
EventEmitter
触发更新使用类继承
EventEmitter
通过在类中申明公共变量来进行存储和共享数据,使用事件订阅发送的方式来实现数据共享以及更新。使用单例模式同步在window
中,以实现多个组件使用同一个发布订阅实例,来同步和共享数据。EventEmitter
我们直接使用 eventemitter3 库提供的on
监听事件以及emit
触发事件。以下是TS Demo
代码这样一个非常原始的状态共享方式就完成啦,接下来我们就看看在
React
中是如何使用的吧优点
这样的解决方案比较原始,但是的确可以解决我们的面临的问题:
缺点
但是缺点也非常的明显:
window
对象,不优雅、不安全React
推荐做法二、单入口打包 + 传送门
React 推荐做法
在方案一中我们说了,使用事件触发的方式同步数据不是
React
推荐做法,那数据共享的推荐做法是什么呢?React
的推荐做法是 提升状态 到各个组件最近的父级节点,借助React
官方文档useContext
demo 来简单理解:真正要解决的问题
如果是使用
React
推荐做法来实现数据共享,那么我们就需要在保证各个业务组件依旧可以挂载在页面不同的DOM
节点的前提下,将所有业务组件都放在同一颗React Tree
下,因为只有所有业务组件都在同一颗React Tree
下时才能让React
的事件冒泡、状态共享、React
的生命周期按照预期进行工作。所以我们首先需要将多入口打包的方式改成单入口打包,至少针对单页面是这样的。多入口打包的方式改成单入口打包非常简单,直接改 webpack 的配置就 ok 了。然后接着解决如何保证在同一颗React Tree
的前提下将不同的业务组件挂载在不同的 DOM 节点。再简单说明一下我们现在需要解决的问题。我们都知道将一个
React APP
应用挂载在某个DOM
节点就是直接ReactDOM.render(<App />, targetElement)
就好了,但是业务组件各自都有各自不同的挂载DOM
节点,如果业务组件都各自执行ReactDOM.render
的话,那就不能保证所有业务组件都在同一颗React Tree
下,也就不能让React
的事件冒泡、状态共享、React
的生命周期按照预期进行工作了。所以接下来我们要解决的问题就是:如何保证让不同的业务组件可以挂载在不同的
DOM
节点的前提下,他们依旧是在同一颗React Tree
下的呢?开始解决问题
在
ReactDOM.render
主应用后可以让子组件挂载在页面上的不同位置 🤔,这让我想到了 Ant-Design 中Modal
,在需要用户处理事务,又不希望跳转页面以致打断工作流程时,可以使用Modal
在当前页面正中打开一个浮层,承载相应的操作。Modal
其中有一个getContainer
属性,说的是Modal
默认的挂载位置是document.body
,可以指定Modal
挂载的HTML
节点,当值为false
时挂载在当前DOM
。那不就意味着我们在
React
应用写的Modal
组件,它本来的挂载位置是跟随主应用的,但是Ant-Design
把它默认提到了document.body
中,这不就是我们要找的解决方法吗?我们来看看Ant-Design
源码是通过什么来实现的呢?我们先找到
Ant-Design
的Modal
组件的弹窗,发现弹窗是通过rc-dialog
包实现的。那么我们接着找
rc-dialog
的实现,然后我们发现rc-dialog
在挂载时候使用了Portal
组件包了一层。那我们接着找
rc-util
包看看他的Portal
组件是如何实现的。最终我们发现
ReactDOM.createPortal
可以将组件放在HTML
的任意DOM
中,被Portal
的组件行为和普通的React
子节点行为一致,因为它仍然在React Tree
中, 且与DOM Tree
中的位置无关,也就是说像context
、事件冒泡以及React
的生命周期这样的Feature
依旧可以使用。我们对
ReactDOM.createPoral
进行简单封装就可以随处使用啦传送门
接下来我们就复习一下
React、Vue
中Portal
(传送门)的知识以及使用场景传送门可以将组件放在
HTML
的任意DOM
中,被Portal
的组件行为和普通的React、Vue
子节点行为一致,因为它仍然在React、Vue Tree
中, 且与DOM Tree
中的位置无关,也就是说像context
、事件冒泡以及React、Vue
的生命周期这样的Feature
依旧可以使用。React
树的祖先节点,事件冒泡将按预期工作,而与DOM
中的Portal
节点位置无关。Portal
仅影响HTML DOM
结构且不影响React、Vue
组件树。Portal
时,需要定义一个 HTML DOM 元素作为Portal
组件的挂载点。当我们需要在正常
DOM
层次结构之外呈现子组件而又不通过React
组件树层次结构破坏事件传播等的默认行为时,React、Vue Portal
就会显得非常有用:Shawdow DOM
内挂载React、Vue
组件Vue 3.0
新增了Teleport
的概念,在Vue 2
中是不支持这个特性的。Vue2
没有传送门的概念,是不是就不支持了呢?我们可以使用这个3K
Star
的开源项目 portal-vue总结
之前:我们是向宿主平台某个页面提供多个业务组件,按照多入口打包方式打包成多个 chunk 给宿主使用。
问题:多入口的方式对于数据共享非常不友好,能解决但是不优雅,也就是文中的方案一。
解决:所以我们想要用相对正规的数据共享方式解决,Redux、Mobx、unstate、React Context 等。但是正规的方式都是在一个 React App 工作的,由于多入口打包打成了多个 React 应用,所以我们先针对单页面改用单入口打包,保证多个业务组件都在同一个 React App 上。与此同时,针对各个业务组件要挂载在不同 DOM 的需求,我们再用 Portal 对业务组件包裹一层,保证他们都在同一颗 React Tree。
🌈 今天的文章分享就到这里啦,如果喜欢这篇文章的话请点赞、Star、关注我吧 🎯
参考
传送门:React Portal
Vue teleport
React createportal
https://reactjs.org/docs/react-dom.html#createportal)