问题
多入口打包这样的做法会导致业务组件内部状态可以共享,但是各个业务组件之间的状态无法很好的共享。并且每个组件内部可能需要相同的数据,所以会导致相同的网络请求会在同一个页面发送多次的情况。
所以我们面临问题以及最终目的就是解决多个 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
}
// 触发事件
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.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’}>
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
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后
前端校招精编面试解析大全点击这里即可获取完整版pdf查看
数前端工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!**
因此收集整理了一份《2024年Web前端开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-dUSR2m6p-1711765496352)]
[外链图片转存中…(img-39FJo3YA-1711765496353)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上前端开发知识点,真正体系化!
[外链图片转存中…(img-oBIjWyQo-1711765496354)]
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(资料价值较高,非无偿)
最后
前端校招精编面试解析大全点击这里即可获取完整版pdf查看
-