如何优雅的跨框架跨组件通信

无论什么框架,都会纠结用什么方式进行跨组件通信;尤其是现在比较热门的微前端,可能layout是用react的nextjs渲染的,content则用vue编写;其中涉及到用户信息、操作分发等成为了一个问题,这里将先分析在单框架下的常见解决方案,然后给出一种优雅的解决方案。

状态管理工具

比如react常用的redux、mobx、zustand等全局状态管理工具,angular可能就是rxjs,vue可能就是vuex啥的。其特点是可以和UI框架结合的很好,但是至少存在以下几种问题:

额外的学习或时间成本

这个看上去有点牵强,都用了UI框架还不用状态管理工具?其实不然,作为react开发人员应该都挺讨厌redux这个库的;模版代码太多,我就想搞一个全局状态怎么要写这么多代码?mobx好像没这个问题,但是响应式又是啥,这和react的单向数据流是不是冲突了?那用recoil吧,天,这么多api,用了atom还得用selector,异步请求还不能用selector...

什么样的数据才放进去

选择了某个状态管理工具后也不是万事大吉,接下来又开始思考什么样的数据才存到全局store里面去呢?用户数据?这个可以存;搜索词?这个好像不用存,那如果加上过滤项呢?好像又有必要存了...更加特别还有一种场景:过滤项本身就是一个页面,选择完成之后返回,这部分过滤数据应该存在哪呢?

能否和其他框架结合

正如文章开头所说,假如是一个多框架页面,我在vue子项目中如何拿到从react项目中获得的登录数据呢?当然有些库可以做到,比如redux。

模拟cache

这个算是比较常见的做法,比如我要保存编辑的数据但是不想让用户通过sessionStorage啥的看到,那就用在框架外面用一个变量存储起来,像这样:

js 复制代码
// xxx.jsx
export const draftCache='';
const Edit=()=>{
    const [value,setValue]=useState(draftCache);
    const onChange=(newValue)=>{
        setValue(newValue);
        draftCache=newValue;
    }
    return <Input defaultValue={draftCache} value={value} onChange={onChange} />
}

然后发现这种方法还挺好用的,编辑草稿写一个cache、上次阅读进度写一个cache、首页搜索栏写一个cache...然后满项目调用。

那当然用Storage啊

上面那种方法可能很多人都不理解为什么不用如localStorage、sessionStorage等去保存。确实如果是一些相对局部需要共享的数据,用sessionStorage或者localStorage去保存比较好,但还是有一些问题:

安全问题

即使是局部共享的数据也不是都能存进Storage,一旦将数据存进Storage就代表这部分数据公开了,当然你可以说在存储的时候进行加密,这又牵涉到效率问题了;更别提可能有有心人禁用Storage,那就又需要额外处理代码的健壮性了。

效率问题

Storage最大的问题是只能存储string,当然你可能没注意:如果你保存的不是string类型的数据时会自动帮你转换一下类型。如果你还想保证数据安全给数据加密,一来二去仅仅保存和读取一个数据就得花不少时间。

IndexDB或webSQL

最大的就是兼容问题了,除此之外仅仅存个局部数据有些浪费了。

参考原生

上面的方案有些是和框架绑定的,有些是借助web api。其实原生跨组件通信方法每个人都知道,就是这种:

js 复制代码
window.addEventListener('click',()=>{
        //code
    })

尤其DOM还能自定义事件,参考MDN

当然为了尽可能的减少对api的依赖,实现跨平台我们可以手动实现一个typescript版本的事件分发。

这里我们用vite快速创建一个react项目,删除多余的文件后,编写类型:

ts 复制代码
//src/utils/event/types.ts
/** 事件回调模版 */
export type EventListener<T> = (data: T) => void;

/** 所有事件回调 */
export type ListenerMap<E extends PropertyKey> = {
  [K in E]: EventListener<unknown>;
};

/** 事件对应回调 */
export type EventCallback<
  E extends PropertyKey,
  M extends ListenerMap<E>
> = NonNullable<M[E]>;

/** 事件回调reducer */
export type EventReducers<E extends PropertyKey, M extends ListenerMap<E>> = {
  [K in E]: EventCallback<K, M>[];
};
/** 事件回调参数 */
export type EventParams<
  E extends string,
  M extends ListenerMap<E>
> = Parameters<EventCallback<E, M>>[number];

然后简单写一个订阅发布模式

ts 复制代码
//src/utils/event/index.ts
import {
  ListenerMap,
  EventCallback,
  EventParams,
  EventReducers,
} from "./types";

class PubSub<T extends string, M extends ListenerMap<T>> {
  private reducers: EventReducers<T, M> = {};
  /** reducers守卫,保证某事件存在回调数组 */
  private checkReducers<E extends T>(event: E): void {
    if (!Reflect.has(this.reducers, event)) {
      this.setEventReducer(event, []);
    }
  }
  /** 获取某事件的回调数组 */
  private getEventReducer<E extends T>(event: E) {
    this.checkReducers(event);
    return Reflect.get(this.reducers, event) as EventReducers<T, M>[E];
  }
  /** 设置某事件的回调数组 */
  private setEventReducer<E extends T>(event: E, list: EventReducers<T, M>[E]) {
    Reflect.set(this.reducers, event, list);
  }
  /** 订阅事件 */
  subscribe<E extends T>(
    event: E,
    listener: EventCallback<E, M>
  ): { unsubscribe: () => void } {
    const callbacks = this.getEventReducer(event);
    callbacks.push(listener);
    this.setEventReducer(event, callbacks);
    return {
      unsubscribe: () => this.unsubscribe(event, listener),
    };
  }

  /** 取消订阅 */
  unsubscribe<E extends T>(event: E, listener: EventCallback<E, M>): void {
    const callbacks = this.getEventReducer(event);
    this.setEventReducer(
      event,
      callbacks.filter((callback) => callback !== listener)
    );
  }

  /** 发布事件 */
  publish<E extends T>(event: E, data: EventParams<E, M>): void {
    const callbacks = this.getEventReducer(event);
    callbacks.map((callback) => callback(data));
  }
}

同时为了测试我们编写两个自定义事件:

ts 复制代码
//src/utils/event/index.ts
export enum Events {
  "EDIT" = "edit",
  "LOGIN" = "login",
}
export interface EventMaps {
  [Events.EDIT]: (value: string) => void;
  [Events.LOGIN]: (user: Record<"username" | "uuid", string>) => void;
}
const events = new PubSub<Events, EventMaps>();

export default events;

然后在input.tsx文件内,写一个react组件订阅edit事件:

tsx 复制代码
//react
function App() {
  const [value, setValue] = useState("");
  useEffect(() => {
    events.subscribe(Events.EDIT, (val) => {
      setValue(val);
    });
  }, []);
  return (
    <div>
      <span>react-{">"}</span>
      {value}
    </div>
  );
}

我们可以简单看一下类型提示是否生效:

嗯,可以根据事件名自动切换回调参数类型。

然后在同一个文件下写一个Vanilla js框架的模块,模拟跨框架通信:

tsx 复制代码
//Vanilla JS
(function createEdit() {
  const container = document.getElementById("VanillaJS"),
    inputDom = document.createElement("input");
  inputDom.type = "text";
  inputDom.addEventListener("input", (e) => {
    const target = e.target as HTMLInputElement;
    events.publish(Events.EDIT, target.value);
  });
  container.replaceChildren(inputDom);
})();

因为我们是通过HTML id获取的,在index.html文件内,我们这样写:

html 复制代码
<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Vite + React + TS</title>
</head>

<body>
  <div id="root"></div>
  <!-- 新增 -->
  <div id="VanillaJS"></div>
  <script type="module" src="/src/main.tsx"></script>
</body>

</html>

然后启动项目,查看效果:

成功跨框架跨组件通信。最后,为了方便react项目使用,将上述封装成自定义hooks:

ts 复制代码
export function useCustomEvent<E extends Events>(
  eventName: E,
  defaultValue?: EventParams<E, EventMaps>
) {
  const [value, setValue] = useState(defaultValue);
  useEffect(() => {
    const event = events.subscribe(eventName, (val) => {
      setValue(val);
    });
    return event.unsubscribe;
  }, [eventName]);
  return value;
}

类型提示依旧生效。

总结

其实懂得人看完就知道上面的步骤都是在模拟rxjs,关于rxjs可以看深红老师的这篇文章。如果条件允许可以直接用rxjs,毕竟大部分情况人家都帮你处理了。但是作为一名现代前端工程师,写代码不应该只有自己看得懂,在引入rxjs之前,应该考虑会遇到的所有问题,比如其他成员是否能学会rxjs、引入后打包体积等问题。关于这个其实有一项标准可以衡量:如果你写的功能别人不需要看你的源码就知道如何使用,那就是好的实现。以上面的代码为例,虽然写了一大串的类型推导,但是使用者完全不需要了解内部原理,只需要调用一个hooks就能完成跨组件的通信。

相关推荐
拾光拾趣录1 分钟前
CSS 深入解析:提升网页样式技巧与常见问题解决方案
前端·css
莫空00002 分钟前
深入理解JavaScript属性描述符:从数据属性到存取器属性
前端·面试
guojl3 分钟前
深度剖析Kafka读写机制
前端
FogLetter3 分钟前
图片懒加载:让网页飞起来的魔法技巧 ✨
前端·javascript·css
Mxuan4 分钟前
vscode webview 插件开发(精装篇)
前端
Mxuan5 分钟前
vscode webview 插件开发(交付篇)
前端
Mxuan6 分钟前
vscode 插件与 electron 应用跳转网页进行登录的实践
前端
拾光拾趣录6 分钟前
JavaScript 加载对浏览器渲染的影响
前端·javascript·浏览器
Codebee6 分钟前
OneCode图表配置速查手册
大数据·前端·数据可视化
然我7 分钟前
React 开发通关指南:用 HTML 的思维写 JS🚀🚀
前端·react.js·html