前言
人生是个积攒的过程,你总会有摔倒,即便跌倒了,你也要懂得抓一把沙子在手里。 —— 丁磊
码过的每一个需要、踩过的每一个坑、修过的每一个 bug
、学过的每一个常识以及看过的每一篇文章都不会成为无用功,它们都将为本人的技术城堡添砖加瓦。明天咱们将从实现不同的 React、Vue App
之间的状态共享这个需要着手,学习 React
、Vue
中那些咱们很少用到,然而一旦遇到这些非凡的需要就非它莫属的个性 ????????
需要 & 问题
需要现状
我在字节的日常业务开发中,我须要将不同的业务组件挂载在一个不属于咱们接管的平台页面中,因为每个业务组件都有各自不同的挂载地位和机会,并且都能够看做一个独自的 React
利用,所以咱们用 Webpack
进行多入口打包,打出多个 React
利用,而后在这个页面通过引入 sdk
的形式挂载业务组件。
问题
多入口打包这样的做法会导致业务组件外部状态能够共享,然而各个业务组件之间的状态无奈很好的共享。并且每个组件外部可能须要雷同的数据,所以会导致雷同的网络申请会在同一个页面发送屡次的状况。
所以咱们面临问题以及最终目标就是解决多个 React
利用之间的状态共享:
- 某个状态须要在多个挂载在页面不同
DOM
节点的业务组件间共享(拜访 + 更新) - 某组件内交互须要触发其余组件的状态更新
解决方案
一、将状态挂载在全局 window 对象、EventEmitter
触发更新
应用类继承 EventEmitter
通过在类中申明公共变量来进行存储和共享数据,应用事件订阅发送的形式来实现数据共享以及更新。应用单例模式同步在 window
中,以实现多个组件应用同一个公布订阅实例,来同步和共享数据。EventEmitter
咱们间接应用 eventemitter3 库提供的 on
监听事件以及emit
触发事件。以下是 TS Demo
代码
import EventEmitter from 'eventemitter3'
// 定义触发的事件常量
export const ACTION = {
ADD_COUNT: 'add-count',
} as const
// 申明 Store 接口
export interface IStore {
count: {
value:number,
addCount:() => void
}
}
// 通过继承 EventEmitter 的 class 中申明 store 来存储数据
export class MyEmitter extends EventEmitter {
public store: IStore = {
count:{
value:1,
addCount:()=>{this.count.value++}
}
}
}
// 将类实例挂载在 Window 中,并保障不同组件中应用的是同一个实例
export const getMyEmitter: () => MyEmitter = () => {
if (window.myEmitter) {
return window.myEmitter
}
window.myEmitter = new Emitter()
const currentEmitter = window.myEmitter
const store = currentEmitter.store
ee.on(ACTION.ADD_COUNT, store.count.addCount, store.count)
return window.myEmitter
}
这样一个十分原始的状态共享形式就实现啦,接下来咱们就看看在 React
中是如何应用的吧
import React,{ useState, useEffect} from 'react'
import {getMyEmitter, ACTION} from './getMyEmitter'
// 应用
const emitter = getMyEmitter()
const CountDemo = ()=>{
return <div>{emitter.store.count.value}</div>
}
// 触发事件
const ButtonDemo = ()=>{
return <button onClick={()=>{emitter.emit(ACTION.ADD_COUNT)}}>add count</button>
}
长处
这样的解决方案比拟原始,然而确实能够解决咱们的面临的问题:
- 解决多入口打包利用无奈应用对立数据源问题,对立保护治理多利用数据状态
- 繁多数据源
毛病
然而毛病也十分的显著:
- 数据裸露在全局
window
对象,不优雅、不平安 - 应用事件触发的形式来同步数据如同不是
React
举荐做法 - 一旦须要注册的事件变多,将难以治理事件和状态
二、单入口打包 + 传送门
React 举荐做法
在计划一中咱们说了,应用事件触发的形式同步数据不是 React
举荐做法,那数据共享的举荐做法是什么呢?React
的举荐做法是 晋升状态 到各个组件最近的父级节点,借助 React
官网文档 useContext
demo 来简略了解:
// 须要共享的数据
import ReactDOM from "react-dom";
import React, { createContext, useContext, useReducer } from "react";
import "./styles.css";
const ThemeContext = createContext();
const DEFAULT_STATE = {
theme: "light"
};
const reducer = (state, actions) => {
switch (actions.type) {
case "theme":
return { ...state, theme: actions.payload };
default:
return DEFAULT_STATE;
}
};
const ThemeProvider = ({ children }) => {
return (
<ThemeContext.Provider value={useReducer(reducer, DEFAULT_STATE)}>
{children}
</ThemeContext.Provider>
);
};
const ListItem = props => (
<li>
<Button {...props} />
</li>
);
const App = props => {
const [state] = useContext(ThemeContext);
const bg = state.theme === "light" ? "#ffffff" : "#000000";
return (
<div
className="App"
style={{
background: bg
}}
>
<ul>
<ListItem value="light" />
<ListItem value="dark" />
</ul>
</div>
);
};
const Button = ({ value }) => {
const [state, dispatcher] = useContext(ThemeContext);
const bgColor = state.theme === "light" ? "#333333" : "#eeeeee";
const textColor = state.theme === "light" ? "#ffffff" : "#000000";
return (
<button
style={{
backgroundColor: bgColor,
color: textColor
}}
onClick={() => {
dispatcher({ type: "theme", payload: value });
}}
>
{value}
</button>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(
<ThemeProvider>
<App />
</ThemeProvider>,
rootElement
);
真正要解决的问题
如果是应用 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
组件是如何实现的。
唉,我一说 “ 啪 ” 就 Github 撸了起来,很快啊!而后上来就是,一个 Ant-Design
Modal
,吭,一个rc-dialog
,一个re-util
,我全副找到了,找到了啊!找到当前,天然是,传统 React API 以点到为止。ReactDOM 放在了鼻子上,我没看文档。我笑一下,筹备关掉 Github,因为这工夫,按传统 Github 的点到为止,最终我曾经找到了答案 ——ReactDOM.CreatePortal
。
最终咱们发现 ReactDOM.createPortal
能够将组件放在 HTML
的任意 DOM
中,被 Portal
的组件行为和一般的 React
子节点行为统一,因为它依然在 React Tree
中, 且与 DOM Tree
中的地位无关,也就是说像 context
、事件冒泡以及 React
的生命周期这样的 Feature
仍旧能够应用。
咱们对 ReactDOM.createPoral
进行简略封装就能够随处应用啦
interface IWrapPortalProps {
elementId: string // 创立带 id 的 createPortal container
effect: (container: HTMLElement, targetDom: Element) => void // 获取挂载地位,将 container 插入指标节点
targetDom?: Element
}
/**
*
* 通过 createPortal 实现在不同的 DOM 上挂载仍旧在同一颗 React tree 上
* @param {*} IWrapPortalProps
* @returns
*/
export const WrapPortal: React.FC<IWrapPortalProps> = (props) => {
const [container] = useState(document.createElement('div'))
useEffect(() => {
container.id = props.elementId
if (!props.targetDom) {
return
}
props.effect(container, props.targetDom, props.elementId)
return () => {
container.remove()
}
}, [container, props])
return ReactDOM.createPortal(props.children, container)
}
// 应用
const effect = (container: HTMLElement, targetDom: Element) => {
targetDom!.insertAdjacentElement('afterbegin', container)
}
const targetDom = document.body
<WrapPortal effect={effect} targetDom={targetDom} elementId={'modal-root'}>
<button>Modal</button>
</WrapPortal>
传送门
接下来咱们就温习一下 React、Vue
中 Portal
(传送门)的常识以及应用场景
传送门能够将组件放在 HTML
的任意 DOM
中,被 Portal
的组件行为和一般的 React、Vue
子节点行为统一,因为它依然在 React、Vue Tree
中, 且与 DOM Tree
中的地位无关,也就是说像 context
、事件冒泡以及 React、Vue
的生命周期这样的 Feature
仍旧能够应用。
- 事件冒泡失常工作 —— 通过将事件流传到
React
树的先人节点,事件冒泡将按预期工作,而与DOM
中的Portal
节点地位无关。 - React、Vue 能够管制 Portal 节点及其生命周期 —— 通过 Portal 渲染子元素时,React、Vue 依然能够管制其生命周期。
- Portal 仅影响 DOM 构造 ——
Portal
仅影响HTML DOM
构造且不影响React、Vue
组件树。 - 预约义 HTML 挂载点 —— 应用
Portal
时,须要定义一个 HTML DOM 元素作为Portal
组件的挂载点。
当咱们须要在失常 DOM
层次结构之外出现子组件而又不通过 React
组件树层次结构毁坏事件流传等的默认行为时,React、Vue Portal
就会显得十分有用:
- 模态对话框
- 工具提醒
- 悬浮卡片
- 加载提醒组件
- 在
Shawdow DOM
内挂载React、Vue
组件
Vue 3.0
新增了 Teleport
的概念,在 Vue 2
中是不反对这个个性的。
const app = Vue.createApp({});
app.component('modal-button', {
template: `
<button @click="modalOpen = true">
Open full screen modal! (With teleport!)
</button>
<teleport to="body">
<div v-if="modalOpen" class="modal">
<div>
I'm a teleported modal!
(My parent is "body")
<button @click="modalOpen = false">
Close
</button>
</div>
</div>
</teleport>
`,
data() {
return {
modalOpen: false
}
}
})
app.mount('#app')
Vue2
没有传送门的概念,是不是就不反对了呢?咱们能够应用这个 3K
Star
的开源我的项目 portal-vue
<template>
<div>
<button @click="disabled = !disabled">Toggle "Disable"</button>
<Design-Container>
<Design-Panel color="green" text="Source">
<p>
The content below this paragraph is
rendered in the right/bottom (red) container by PortalVue
if the portal is enabled. Otherwise, it's shown here in place.
</p>
<Portal to="right-disable" :disabled="disabled">
<p class="red">This is content from the left/top container (green).</p>
</Portal>
</Design-Panel>
<Design-Panel color="red" text="Target" left>
<PortalTarget name="right-disable"></PortalTarget>
</Design-Panel>
</Design-Container>
</div>
</template>
<script>
export default {
data: () => ({
disabled: false,
}),
}
</script>
???? 明天的文章分享就到这里啦,如果喜爱这篇文章的话请点赞、Star、关注我吧 ????
参考
- 传送门:React Portal
- Vue teleport
- React createportal
- https://reactjs.org/docs/reac…
发表回复