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

无论什么框架,都会纠结用什么方式进行跨组件通信;尤其是现在比较热门的微前端,可能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就能完成跨组件的通信。

相关推荐
Cshaosun8 分钟前
js版本之ES6特性简述【Proxy、Reflect、Iterator、Generator】(五)
开发语言·javascript·es6
web1828548251214 分钟前
ctfshow-web 151-170-文件上传
前端·状态模式
轻口味19 分钟前
【每日学点鸿蒙知识】Web请求支持Http、PDF展示、APP上架应用搜索问题、APP备案不通过问题、滚动列表问题
前端·http·harmonyos
一棵开花的树,枝芽无限靠近你28 分钟前
【PPTist】表格功能
前端·笔记·学习·编辑器·ppt·pptist
马船长1 小时前
RCE-PLUS (学习记录)
java·linux·前端
轻口味1 小时前
【每日学点鸿蒙知识】webview性能优化、taskpool、热更新、Navigation问题、调试时每次都卸载重装问题
javascript·list·harmonyos
学前端的小朱1 小时前
修改输出资源的名称和路径、自动清空上次打包资源
前端·webpack·打包工具
涔溪1 小时前
如何在Express.js中定义多个HTTP方法?
javascript·http·express
嘤嘤怪呆呆狗2 小时前
【开发问题记录】执行 git cz 报require() of ES Module…… 错误
前端·javascript·vue.js·git·vue
夜斗(dou)2 小时前
谷歌开发者工具 - 网络篇
前端·网络·chrome devtools