From c11d382c1cb5daafeff2daf9e9fc66f1e08b0acc Mon Sep 17 00:00:00 2001 From: Sungyu Kang Date: Sat, 7 Dec 2024 16:10:26 +0900 Subject: [PATCH] fix: multiple linkBridge (#86) * fix: multiple linkBridge * fix: by id * fix: space * fix: indent --- packages/react-native/src/createWebView.tsx | 12 +++-- .../react-native/src/integrations/bridge.ts | 46 +++++++++++++++---- packages/web/src/global.d.ts | 5 +- packages/web/src/internal/bridgeInstance.ts | 16 ++++--- packages/web/src/linkBridge.ts | 22 +++++---- 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/packages/react-native/src/createWebView.tsx b/packages/react-native/src/createWebView.tsx index 115c1e9..d003f2d 100644 --- a/packages/react-native/src/createWebView.tsx +++ b/packages/react-native/src/createWebView.tsx @@ -7,7 +7,8 @@ import type { Primitive, } from "@webview-bridge/types"; import { createEvents } from "@webview-bridge/utils"; -import React, { +import type React from "react"; +import { forwardRef, useEffect, useImperativeHandle, @@ -19,12 +20,12 @@ import type { WebViewMessageEvent, WebViewProps } from "react-native-webview"; import WebView from "react-native-webview"; import { - handleBridge, INJECT_BRIDGE_METHODS, INJECT_BRIDGE_STATE, SAFE_NATIVE_EMITTER_EMIT, + handleBridge, } from "./integrations/bridge"; -import { handleLog, INJECT_DEBUG, LogType } from "./integrations/console"; +import { INJECT_DEBUG, type LogType, handleLog } from "./integrations/console"; import { handleRegisterWebMethod } from "./integrations/handleRegisterWebMethod"; import type { BridgeWebView } from "./types/webview"; @@ -190,6 +191,7 @@ export const createWebView = < }; }, []); + // biome-ignore lint/correctness/useExhaustiveDependencies: const initData = useMemo(() => { const bridgeMethods = Object.entries(bridge.getState() ?? {}) .filter(([_, bridge]) => typeof bridge === "function") @@ -203,6 +205,7 @@ export const createWebView = < }, []); useEffect(() => { + // lazy initialize the bridgeId webviewRef.current?.injectJavaScript( SAFE_NATIVE_EMITTER_EMIT("hydrate", initData), ); @@ -216,7 +219,7 @@ export const createWebView = < if (!webviewRef.current) { return; } - const { type, body } = JSON.parse(event.nativeEvent.data); + const { type, body, bridgeId } = JSON.parse(event.nativeEvent.data); switch (type) { case "log": { @@ -235,6 +238,7 @@ export const createWebView = < }; handleBridge({ + bridgeId, bridge, method, args, diff --git a/packages/react-native/src/integrations/bridge.ts b/packages/react-native/src/integrations/bridge.ts index 1251bd9..ecb31f8 100644 --- a/packages/react-native/src/integrations/bridge.ts +++ b/packages/react-native/src/integrations/bridge.ts @@ -5,7 +5,7 @@ import type { Primitive, } from "@webview-bridge/types"; import { equals, removeUndefinedKeys } from "@webview-bridge/utils"; -import WebView from "react-native-webview"; +import type WebView from "react-native-webview"; export type StoreCallback = ({ get, @@ -69,6 +69,7 @@ type HandleBridgeArgs = { args?: ArgType[]; webview: WebView; eventId: string; + bridgeId: string; }; export const handleBridge = async ({ @@ -77,12 +78,15 @@ export const handleBridge = async ({ args, webview, eventId, + bridgeId, }: HandleBridgeArgs) => { const _bridge = bridge.getState(); const _method = _bridge[method]; const handleThrow = () => { - webview.injectJavaScript(SAFE_NATIVE_EMITTER_THROW(`${method}-${eventId}`)); + webview.injectJavaScript( + SAFE_NATIVE_EMITTER_THROW_BY_BRIDGE_ID(bridgeId, `${method}-${eventId}`), + ); }; if (!(method in _bridge)) { handleThrow(); @@ -96,7 +100,11 @@ export const handleBridge = async ({ const response = await _method?.(...(args ?? [])); webview.injectJavaScript( - SAFE_NATIVE_EMITTER_EMIT(`${method}-${eventId}`, response), + SAFE_NATIVE_EMITTER_EMIT_BY_BRIDGE_ID( + bridgeId, + `${method}-${eventId}`, + response, + ), ); } catch (error) { handleThrow(); @@ -117,8 +125,27 @@ export const INJECT_BRIDGE_STATE = ( export const SAFE_NATIVE_EMITTER_EMIT = (eventName: string, data: unknown) => { const dataString = JSON.stringify(data); return ` -if (window.nativeEmitter) { - window.nativeEmitter.emit('${eventName}', ${dataString}); +if (window.nativeEmitter && Object.keys(window.nativeEmitter).length > 0) { + for (const [_, emitter] of Object.entries(window.nativeEmitter)) { + emitter.emit('${eventName}', ${dataString}); + } +} else { + window.nativeBatchedEvents = window.nativeBatchedEvents || []; + window.nativeBatchedEvents.push(['${eventName}', ${dataString}]); +} +true; +`; +}; + +export const SAFE_NATIVE_EMITTER_EMIT_BY_BRIDGE_ID = ( + bridgeId: string, + eventName: string, + data: unknown, +) => { + const dataString = JSON.stringify(data); + return ` +if (window.nativeEmitter && window.nativeEmitter['${bridgeId}']) { + window.nativeEmitter['${bridgeId}'].emit('${eventName}', ${dataString}); } else { window.nativeBatchedEvents = window.nativeBatchedEvents || []; window.nativeBatchedEvents.push(['${eventName}', ${dataString}]); @@ -127,9 +154,12 @@ true; `; }; -export const SAFE_NATIVE_EMITTER_THROW = (eventName: string) => ` -if (window.nativeEmitter) { - window.nativeEmitter.emit('${eventName}', {}, true); +export const SAFE_NATIVE_EMITTER_THROW_BY_BRIDGE_ID = ( + bridgeId: string, + eventName: string, +) => ` +if (window.nativeEmitter['${bridgeId}']) { + window.nativeEmitter['${bridgeId}'].emit('${eventName}', {}, true); } else { window.nativeBatchedEvents = window.nativeBatchedEvents || []; window.nativeBatchedEvents.push(['${eventName}', {}, true]); diff --git a/packages/web/src/global.d.ts b/packages/web/src/global.d.ts index c073aa0..2d581ee 100644 --- a/packages/web/src/global.d.ts +++ b/packages/web/src/global.d.ts @@ -1,14 +1,15 @@ import type { DefaultEmitter } from "@webview-bridge/utils"; -import { Primitive } from "."; +import type { Primitive } from "./types"; +// biome-ignore lint/complexity/noUselessEmptyExport: export {}; declare global { interface Window { __bridgeMethods__?: string[]; __bridgeInitialState__?: Record; - nativeEmitter?: DefaultEmitter; + nativeEmitter?: Record; nativeBatchedEvents?: [string, ...any][]; webEmitter?: DefaultEmitter; ReactNativeWebView: { diff --git a/packages/web/src/internal/bridgeInstance.ts b/packages/web/src/internal/bridgeInstance.ts index 5c61343..a813bf8 100644 --- a/packages/web/src/internal/bridgeInstance.ts +++ b/packages/web/src/internal/bridgeInstance.ts @@ -8,15 +8,15 @@ import type { PrimitiveObject, } from "@webview-bridge/types"; import { + type DefaultEmitter, createRandomId, createResolver, - DefaultEmitter, timeout, } from "@webview-bridge/utils"; import { NativeMethodError } from "../error"; -import { LinkBridgeOptions } from "../linkBridge"; -import { LinkBridge } from "../types"; +import type { LinkBridgeOptions } from "../linkBridge"; +import type { LinkBridge } from "../types"; import { createPromiseProxy } from "./createPromiseProxy"; import { linkBridgeStore } from "./linkBridgeStore"; import { mockStore } from "./mockStore"; @@ -26,10 +26,11 @@ export class BridgeInstance< V extends ParserSchema = ParserSchema, > { constructor( + private _bridgeId: string, private _options: LinkBridgeOptions, private _emitter: DefaultEmitter, - private _bridgeMethods: string[] = [], + private _bridgeMethods: string[], public _nativeInitialState: PrimitiveObject, ) { this._hydrate(_bridgeMethods, _nativeInitialState); @@ -75,9 +76,11 @@ export class BridgeInstance< ? { type, body, + bridgeId: this._bridgeId, } : { type, + bridgeId: this._bridgeId, }, ), ); @@ -203,10 +206,9 @@ export class BridgeInstance< writable: false, }); - return { - ...acc, + return Object.assign(acc, { [methodName]: nativeMethod, - }; + }); }, initialBridge as LinkBridge, Omit, V>, ); diff --git a/packages/web/src/linkBridge.ts b/packages/web/src/linkBridge.ts index 9e4141a..2fccb44 100644 --- a/packages/web/src/linkBridge.ts +++ b/packages/web/src/linkBridge.ts @@ -6,12 +6,12 @@ import type { ParserSchema, PrimitiveObject, } from "@webview-bridge/types"; -import { createEvents } from "@webview-bridge/utils"; +import { createEvents, createRandomId } from "@webview-bridge/utils"; import { MethodNotFoundError } from "./error"; import { BridgeInstance } from "./internal/bridgeInstance"; import { mockStore } from "./internal/mockStore"; -import { LinkBridge } from "./types"; +import type { LinkBridge } from "./types"; export interface LinkBridgeOptions< T extends BridgeStore, @@ -92,15 +92,19 @@ export const linkBridge = < console.warn("[WebViewBridge] Not in a WebView environment"); } + const bridgeId = createRandomId(); const emitter = createEvents(); - if (!window.nativeEmitter) { - window.nativeEmitter = emitter; - } + + window.nativeEmitter = { + ...(window.nativeEmitter || {}), + [bridgeId]: emitter, + }; const bridgeMethods = window.__bridgeMethods__ ?? []; const nativeInitialState = window.__bridgeInitialState__ ?? {}; const instance = new BridgeInstance( + bridgeId, options, emitter, bridgeMethods, @@ -134,11 +138,11 @@ export const linkBridge = < onFallback?.(methodName, args); return Promise.reject(new MethodNotFoundError(methodName)); }; - } else { - console.warn( - `[WebViewBridge] ${methodName} is not defined, using fallback.`, - ); } + + console.warn( + `[WebViewBridge] ${methodName} is not defined, using fallback.`, + ); return () => Promise.resolve(); }, }) as LinkBridge>, Omit, V>;