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
}
// 触发事件
const ButtonDemo = ()=>{
return <button onClick={()=>{emitter.emit(ACTION.ADD_COUNT)}}>add count
}
优点
这样的解决方案比较原始,但是的确可以解决我们的面临的问题:
-
解决多入口打包应用无法使用统一数据源问题,统一维护管理多应用数据状态
-
单一数据源
缺点
但是缺点也非常的明显:
-
数据暴露在全局
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 => (
<Button {…props} />
);
const App = props => {
const [state] = useContext(ThemeContext);
const bg = state.theme === “light” ? “#ffffff” : “#000000”;
return (
className=“App”
style={{
background: bg
}}
);
};
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}
);
};
const rootElement = document.getElementById(“root”);
ReactDOM.render(
,
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 = (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.createPort 《大厂前端面试题解析+Web核心总结学习笔记+企业项目实战源码+最新高清讲解视频》无偿开源 徽信搜索公众号【编程进阶路】 al(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’}>
Modal
传送门
接下来我们就复习一下
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!)
I’m a teleported modal!
(My parent is “body”)
<button @click=“modalOpen = false”>
Close
`,
data() {
-