From 01ad7b7b319aa8dfa61541bb60344ba0c43c510c Mon Sep 17 00:00:00 2001 From: xuhongbin Date: Tue, 23 Sep 2025 11:47:53 +0800 Subject: [PATCH 1/5] =?UTF-8?q?refactor(bridge-vue2):=20=E9=87=8D=E6=9E=84?= =?UTF-8?q?=20Vue2=20=E6=A1=A5=E6=8E=A5=E7=BB=84=E4=BB=B6=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=EF=BC=8C=E4=BC=98=E5=8C=96=E7=BB=84=E4=BB=B6=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E5=92=8C=E9=94=80=E6=AF=81=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 简化 Vue2 桥接组件的实现,移除冗余代码和调试选项 修复组件 props 传递问题,确保正确更新组件属性 优化组件销毁流程,避免内存泄漏 --- packages/bridge-react/src/index.ts | 35 +- packages/bridge-vue2/src/index-bak.ts | 242 ++++++++++++++ packages/bridge-vue2/src/index.ts | 305 +++--------------- projects/adapter-app/src/App.tsx | 8 +- projects/adapter-app/src/adapter/Vue2.ts | 4 +- .../src/components/ErrorBoundary.tsx | 74 +++-- projects/adapter-vue2-host/emp.config.ts | 2 +- projects/adapter-vue2-host/src/bootstrap.js | 3 +- .../src/components/Hello.jsx | 10 +- .../src/components/HelloVue.vue | 43 +++ projects/adapter-vue2-host/src/main.js | 4 + projects/adapter-vue2-host/src/store/index.js | 5 - projects/adapter-vue2-host/src/views/Home.vue | 4 + 13 files changed, 405 insertions(+), 334 deletions(-) create mode 100644 packages/bridge-vue2/src/index-bak.ts create mode 100644 projects/adapter-vue2-host/src/components/HelloVue.vue diff --git a/packages/bridge-react/src/index.ts b/packages/bridge-react/src/index.ts index 868e1e57..94a4012d 100644 --- a/packages/bridge-react/src/index.ts +++ b/packages/bridge-react/src/index.ts @@ -134,6 +134,7 @@ export function createRemoteAppComponent( } unmountComponent() { + console.log('unmountComponent') try { // 不强依赖containerRef.current存在,避免可能的null引用 if (this.provider) { @@ -144,7 +145,7 @@ export function createRemoteAppComponent( console.warn('[bridge-react] Error during provider unmount:', destroyError) } } - + // 确保清理provider引用 this.provider = null } @@ -154,32 +155,36 @@ export function createRemoteAppComponent( } componentDidMount() { + console.log('componentDidMount') this.isMounted = true if (this.providerInfo) this.renderComponent() } componentDidUpdate() { + console.log('componentDidUpdate') if (this.provider && this.containerRef.current) { this.provider.render(this.containerRef.current, this.props) } } componentWillUnmount() { - this.isMounted = false - - // 检查是否使用同步卸载 - if (reactOptions?.syncUnmount) { - // 直接同步卸载组件,避免异步操作导致的DOM节点关系变化 - this.unmountComponent() - } else { - // 使用微任务队列,比setTimeout更快但仍然异步 - Promise.resolve().then(() => { - if (this.containerRef && this.containerRef.current) { - this.unmountComponent() - } - }) + console.log('componentWillUnmount', reactOptions?.syncUnmount) + this.isMounted = false + + // 检查是否使用同步卸载 + if (reactOptions?.syncUnmount) { + // 直接同步卸载组件,避免异步操作导致的DOM节点关系变化 + this.unmountComponent() + } else { + console.log('componentWillUnmount async', this.containerRef, this.containerRef.current) + // 使用微任务队列,比setTimeout更快但仍然异步 + Promise.resolve().then(() => { + if (this.containerRef && this.containerRef.current) { + this.unmountComponent() + } + }) + } } - } render() { return React.createElement('div', {ref: this.containerRef}) diff --git a/packages/bridge-vue2/src/index-bak.ts b/packages/bridge-vue2/src/index-bak.ts new file mode 100644 index 00000000..927c96be --- /dev/null +++ b/packages/bridge-vue2/src/index-bak.ts @@ -0,0 +1,242 @@ +// Type definitions +export interface BridgeProviderReturn { + render: (dom: HTMLElement, props?: Record) => void + destroy: (dom: HTMLElement) => void +} + +export type BridgeProvider = () => BridgeProviderReturn +export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> +export type ComponentProvider = BridgeProvider | AsyncBridgeProvider + +interface Vue2Options { + Vue?: any + plugin?: (vue: any) => void +} + +/** + * Create bridge component - for producer to wrap application-level export modules + */ +export function createBridgeComponent(Component: any, options: Vue2Options): BridgeProvider { + const Vue = options.Vue + + return function (): BridgeProviderReturn { + const instanceMap = new Map() + + const render = (dom: HTMLElement, props?: Record): void => { + // 防御性检查:确保 DOM 元素存在 + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to render') + return + } + + try { + const existingInstance = instanceMap.get(dom) + + if (existingInstance) { + // Update props for existing instance + if (props) { + // 参考 adapter/vue.ts 中的实现,合并 props 和 attrs + const mergedProps = {...existingInstance.$props, ...props} + + // 不直接修改 props,而是通过重新渲染组件来更新 + // Vue props 是只读的,不应该尝试直接修改它们 + try { + // 强制重新渲染组件,让 Vue 内部处理 props 更新 + existingInstance.$forceUpdate() + + // 如果需要更新 data 中的属性(非 props) + Object.keys(mergedProps).forEach(key => { + if (key in existingInstance.$data && !(existingInstance.$props && key in existingInstance.$props)) { + // 只更新不是 props 的 data 属性 + existingInstance.$set(existingInstance.$data, key, mergedProps[key]) + } + }) + } catch (error) { + console.warn('[EMP-WARN] Failed to update props:', error) + } + + // 触发组件更新 + existingInstance.$forceUpdate() + } + } else { + // Create new Vue instance with correct props handling + const instance = new Vue({ + data() { + // 只在data中存储非props的数据 + return {} + }, + // 使用propsData选项正确传递props + propsData: props || {}, + render: (h: any) => h(Component, {props: props || {}}), + el: dom, + }) + + // 使用自定义插件(如果提供) + if (options.plugin) { + options.plugin(Vue) + } + + instanceMap.set(dom, instance) + } + } catch (error) { + console.error('[EMP-ERROR] Failed to render/update Vue component', error) + throw error + } + } + + const destroy = (dom: HTMLElement): void => { + // 防御性检查:确保 DOM 元素存在 + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to destroy') + return + } + + const instance = instanceMap.get(dom) + if (!instance) return + + try { + // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 + if (dom && dom.parentNode) { + while (dom.firstChild) { + dom.removeChild(dom.firstChild) + } + } + + // 先解除引用,再销毁 Vue 实例 + const vmToDestroy = instance + instanceMap.delete(dom) + + // 确保在下一个事件循环中销毁,避免与React卸载冲突 + setTimeout(() => { + vmToDestroy.$destroy() + }, 0) + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount Vue component', error) + } + } + + return {render, destroy} + } +} + +/** + * Create remote app component - for consumer to load application-level modules + */ +export function createRemoteAppComponent( + component: ComponentProvider, + vueOptions: Vue2Options, + options: {onError?: (error: Error) => void} = {}, +): any { + if (!component) { + throw new Error('createRemoteAppComponent: component parameter cannot be empty') + } + + return { + name: 'RemoteAppComponent', + // 允许接收任意 props + props: { + name: String, + [Symbol.toPrimitive]: Function, + }, + data() { + return { + provider: null, + providerInfo: null, + isMounted: false, + } + }, + methods: { + async loadComponent() { + try { + if (typeof component === 'function') { + const result = component() + + if (result instanceof Promise) { + const module = await result + this.providerInfo = module.default + } else { + this.providerInfo = component as BridgeProvider + } + } + + if (this.isMounted && this.$el) { + this.renderComponent() + } + } catch (error) { + if (options.onError) options.onError(error as Error) + console.error('[EMP-ERROR] Failed to load component', error) + } + }, + renderComponent() { + if (!this.providerInfo || !this.$el) return + + try { + if (!this.provider && this.providerInfo) { + this.provider = this.providerInfo() + } + + if (!this.provider) { + console.warn('[EMP-WARN] Provider not available yet') + return + } + + // 确保传递正确的 props,Vue2 中 $props 可能不存在 + const props = this.$props || this.$options.propsData || {} + + // 确保 props 是对象类型 + if (props && typeof props === 'object') { + this.provider.render(this.$el, props) + } else { + this.provider.render(this.$el, {}) + } + } catch (error) { + console.error('[EMP-ERROR] Failed to render component', error) + if (options.onError) options.onError(error as Error) + } + }, + unmountComponent() { + if (this.provider && this.$el) { + try { + // 先清空 DOM 内容,避免 React 移除时的冲突 + while (this.$el.firstChild) { + this.$el.removeChild(this.$el.firstChild) + } + + // 然后销毁 Vue 组件 + this.provider.destroy(this.$el) + this.provider = null + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount component', error) + } + } + }, + }, + mounted() { + this.isMounted = true + if (this.providerInfo) this.renderComponent() + }, + updated() { + if (this.provider && this.$el) { + // 获取 props 的安全方式 + const props = this.$props || this.$options.propsData || {} + this.provider.render(this.$el, props) + } + }, + beforeDestroy() { + this.isMounted = false + this.unmountComponent() + }, + created() { + // 立即加载组件 + this.loadComponent() + }, + render(h: any) { + return h('div') + }, + } +} + +export default { + createBridgeComponent, + createRemoteAppComponent, +} diff --git a/packages/bridge-vue2/src/index.ts b/packages/bridge-vue2/src/index.ts index 6f581932..3019b28d 100644 --- a/packages/bridge-vue2/src/index.ts +++ b/packages/bridge-vue2/src/index.ts @@ -9,306 +9,73 @@ export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> export type ComponentProvider = BridgeProvider | AsyncBridgeProvider interface Vue2Options { - Vue: any + Vue?: any plugin?: (vue: any) => void - instance?: {[k: string]: any} - safeDestroy?: boolean - disableAutoCleanup?: boolean - enableDebug?: boolean -} - -// Vue2 类型定义 -interface Vue2Instance { - providerInfo: BridgeProvider - isMounted: HTMLElement - renderComponent(): unknown - provider: any - $props: Record - $set: (object: Record, key: string, value: any) => void - $el: HTMLElement - $destroy: () => void } /** * Create bridge component - for producer to wrap application-level export modules */ -// 添加调试日志函数 -const DEBUG_PREFIX = '[bridge-vue2]' -function debug(...args: any[]) { - console.log(DEBUG_PREFIX, ...args) -} - export function createBridgeComponent(Component: any, options: Vue2Options): BridgeProvider { const Vue = options.Vue return function (): BridgeProviderReturn { - const instanceMap = new Map() - + const instanceMap = new Map() const render = (dom: HTMLElement, props?: Record): void => { - if (options.enableDebug) debug('render called', dom, props) - try { - const existingInstance = instanceMap.get(dom) - - if (existingInstance) { - // Update props for existing instance - if (props) { - Object.keys(props).forEach(key => { - existingInstance.$set(existingInstance.$props, key, props[key]) - }) - } - } else { - // Create new Vue instance - - // 使用自定义插件(如果提供) - if (options.plugin) { - options.plugin(Vue) - // console.log('options.plugin', options.plugin) - } - if (options.enableDebug) debug('render: creating new Vue instance', props) - const instance = new Vue({ - ...(options.instance || {}), - render: (h: any) => h(Component, {props: props || {}}), - el: dom, - }) as Vue2Instance - if (options.enableDebug) debug('render: Vue instance created successfully') - - instanceMap.set(dom, instance) - if (options.enableDebug) debug('render: instance added to instanceMap') - } - } catch (error) { - console.error('[EMP-ERROR] Failed to render/update Vue component', error) - throw error + // 防御性检查:确保 DOM 元素存在 + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to render') + return } - } - - const destroy = (dom: HTMLElement): void => { - if (options.enableDebug) debug('destroy called', dom) - const instance = instanceMap.get(dom) - if (!instance) { - if (options.enableDebug) debug('destroy: no instance found for dom', dom) + // props = Vue.observable(props) + console.log('props', props) + const existingInstance = instanceMap.get(dom) + console.log('existingInstance', existingInstance) + if (existingInstance) { + // 更新组件的渲染函数,使用新的props重新渲染组件 + existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) + + // 更新propsData + existingInstance.$options.propsData = props || {} + + // 强制更新组件 + existingInstance.$forceUpdate() return } - try { - // 先销毁Vue实例 - if (options.enableDebug) debug('destroy: destroying Vue instance', instance) - instance.$destroy() - if (options.enableDebug) debug('destroy: Vue instance destroyed successfully') - - // 直接清空DOM内容,不检查innerHTML,避免与React DOM操作冲突 - try { - if (options.enableDebug) debug('destroy: cleaning up DOM content') - // 直接设置innerHTML为空字符串是最安全的方式 - if (!options.safeDestroy) { - dom.innerHTML = '' - if (options.enableDebug) debug('destroy: DOM cleanup completed') - } else { - if (options.enableDebug) debug('destroy: DOM cleanup skipped (safeDestroy=true)') - } - } catch (domError) { - // 捕获可能的DOM操作错误,避免影响React卸载流程 - console.warn('[bridge-vue2] Error during component destroy:', domError) - if (options.enableDebug) debug('destroy: error cleaning up DOM', domError) - } + // Create new Vue instance with correct props handling + const instance = new Vue({ + propsData: props || {}, + render: (h: any) => h(Component, {props: props || {}}), + el: dom, + }) - instanceMap.delete(dom) - if (options.enableDebug) debug('destroy: instance removed from instanceMap') - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount Vue component', error) - if (options.enableDebug) debug('destroy: error during component unmount', error) + // 使用自定义插件(如果提供) + if (options.plugin) { + options.plugin(Vue) } + instanceMap.set(dom, instance) } - const unmountComponent = (dom: HTMLElement): void => { - if (options.enableDebug) debug('unmountComponent called', dom) - const instance = instanceMap.get(dom) - if (!instance) { - if (options.enableDebug) debug('unmountComponent: no instance found for dom', dom) + const destroy = (dom: HTMLElement): void => { + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to destroy') return } - - try { - if (options.enableDebug) debug('unmountComponent: destroying Vue instance', instance) - instance.$destroy() - if (options.enableDebug) debug('unmountComponent: Vue instance destroyed successfully') - } catch (e) { - console.error('Failed to destroy Vue instance', e) - if (options.enableDebug) debug('unmountComponent: error destroying Vue instance', e) - } - - instanceMap.delete(dom) - if (options.enableDebug) debug('unmountComponent: instance removed from instanceMap') - - // 如果不禁用自动清理,则清空DOM - if (!options.disableAutoCleanup) { - if (options.enableDebug) debug('unmountComponent: cleaning up DOM children', dom.childNodes.length) - try { - let childCount = dom.childNodes.length; - if (options.enableDebug) debug('unmountComponent: DOM children count before cleanup', childCount) - - while (dom.firstChild) { - if (options.enableDebug) debug('unmountComponent: removing child', dom.firstChild.nodeName) - dom.removeChild(dom.firstChild) - } - if (options.enableDebug) debug('unmountComponent: DOM cleanup completed') - } catch (e) { - console.error('Failed to clean up DOM', e) - if (options.enableDebug) debug('unmountComponent: error cleaning up DOM', e, e.stack) - } - } else { - if (options.enableDebug) debug('unmountComponent: DOM cleanup skipped (disableAutoCleanup=true)') - } + + const instance = instanceMap.get(dom) + if (!instance) return } return {render, destroy} } } -// Vue2 组件类型定义 -interface Vue2Component { - isMounted: boolean - providerInfo: any - renderComponent(): unknown - provider: any - unmountComponent(): unknown - loadComponent(): unknown - name?: string - props?: Record - data?: () => Record - methods?: Record any> - mounted?: () => void - updated?: () => void - beforeDestroy?: () => void - created?: () => void - render?: (h: any) => any - $el?: HTMLElement - $props?: Record -} - -/** - * Create remote app component - for consumer to load application-level modules - */ export function createRemoteAppComponent( component: ComponentProvider, vueOptions: Vue2Options, options: {onError?: (error: Error) => void} = {}, -): Vue2Component { - if (!component) { - throw new Error('createRemoteAppComponent: component parameter cannot be empty') - } - - return { - name: 'RemoteAppComponent', - props: {}, - data() { - return { - provider: null as BridgeProviderReturn | null, - providerInfo: null as BridgeProvider | null, - isMounted: false, - } - }, - methods: { - async loadComponent() { - try { - if (typeof component === 'function') { - const result = component() - - if (result instanceof Promise) { - const module = await result - this.providerInfo = module.default - } else { - this.providerInfo = component as BridgeProvider - } - } - - if (this.isMounted && this.$el) { - this.renderComponent() - } - } catch (error) { - if (options.onError) options.onError(error as Error) - console.error('[EMP-ERROR] Failed to load component', error) - } - }, - renderComponent() { - if (!this.providerInfo || !this.$el) return - - try { - if (!this.provider) { - this.provider = this.providerInfo() - } - this.provider.render(this.$el, this.$props || {}) - } catch (error) { - console.error('[EMP-ERROR] Failed to render component', error) - } - }, - unmountComponent() { - // 只检查provider是否存在,不再依赖DOM节点关系 - if (this.provider) { - try { - // 如果$el存在,尝试销毁,但捕获任何可能的错误 - if (this.$el) { - try { - // 调用destroy方法,该方法已经增强了错误处理 - this.provider.destroy(this.$el) - } catch (destroyError) { - console.warn('[EMP-WARN] Could not clear DOM content safely', destroyError) - } - } - // 无论如何都清理provider引用 - this.provider = null - - // 检查是否禁用自动清理 - if (!vueOptions.disableAutoCleanup && this.$el) { - try { - // 使用安全的方式清空DOM内容 - this.$el.innerHTML = '' - } catch (domError) { - console.warn('[EMP-WARN] Could not clear DOM content safely', domError) - } - } - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount component', error) - console.warn('[bridge-vue2] Error during component unmount:', error) - } - } - }, - }, - mounted() { - this.isMounted = true - if (this.providerInfo) this.renderComponent() - }, - updated() { - if (this.provider && this.$el) { - this.provider.render(this.$el, this.$props || {}) - } - }, - beforeDestroy() { - this.isMounted = false - - // 直接调用清理函数,避免异步操作可能导致的DOM节点关系变化 - this.unmountComponent() - }, - created() { - // 立即加载组件 - this.loadComponent() - }, - render(h: any) { - return h('div') - }, - isMounted: false, - providerInfo: undefined, - renderComponent: function (): unknown { - throw new Error('Function not implemented.') - }, - provider: undefined, - unmountComponent: function (): unknown { - throw new Error('Function not implemented.') - }, - loadComponent: function (): unknown { - throw new Error('Function not implemented.') - }, - } -} +): any {} export default { createBridgeComponent, diff --git a/projects/adapter-app/src/App.tsx b/projects/adapter-app/src/App.tsx index ecc4064a..2de1f647 100644 --- a/projects/adapter-app/src/App.tsx +++ b/projects/adapter-app/src/App.tsx @@ -6,6 +6,10 @@ import {Box, React16Info} from './components/Info' const App = () => (
+ +

Vue 2 Remote App

+ +
@@ -22,10 +26,6 @@ const App = () => ( - -

Vue 2 Remote App

- -
) diff --git a/projects/adapter-app/src/adapter/Vue2.ts b/projects/adapter-app/src/adapter/Vue2.ts index be7c9c86..2ea4e334 100644 --- a/projects/adapter-app/src/adapter/Vue2.ts +++ b/projects/adapter-app/src/adapter/Vue2.ts @@ -2,7 +2,7 @@ import {createRemoteAppComponent} from '@empjs/bridge-react' import {createBridgeComponent} from '@empjs/bridge-vue2' // React 16 组件 import React from 'react' -import v2App from 'v2h/Hello' +import v2App from 'v2h/HelloVue' // import plugin from 'v2h/plugin' // import store from 'v2h/store' @@ -11,7 +11,7 @@ const {EMP_ADAPTER_VUE_v2} = window as any const {Vue} = EMP_ADAPTER_VUE_v2 // 创建Vue2桥接组件,添加额外配置以增强稳定性 -const BridgeComponent = createBridgeComponent(v2App, {Vue, enableDebug: true}) +const BridgeComponent = createBridgeComponent(v2App, {Vue}) // 创建React远程组件,添加额外配置以增强稳定性 export const RemoteVue2App = createRemoteAppComponent(BridgeComponent, {React}) diff --git a/projects/adapter-app/src/components/ErrorBoundary.tsx b/projects/adapter-app/src/components/ErrorBoundary.tsx index a682b0f0..030ae756 100644 --- a/projects/adapter-app/src/components/ErrorBoundary.tsx +++ b/projects/adapter-app/src/components/ErrorBoundary.tsx @@ -1,5 +1,5 @@ -import React, { Component } from 'react'; -import type { ErrorInfo, ReactNode } from 'react'; +import type {ErrorInfo, ReactNode} from 'react' +import React, {Component} from 'react' interface Props { children: ReactNode @@ -11,51 +11,57 @@ interface State { error: Error | null } -/** - * 错误边界组件,用于捕获子组件中的JavaScript错误 - * 防止整个应用崩溃,并提供优雅的降级UI - */ class ErrorBoundary extends Component { - public state: State = { - hasError: false, - error: null, + constructor(props: Props) { + super(props) + this.state = { + hasError: false, + error: null, + } } - public static getDerivedStateFromError(error: Error): State { - // 更新状态,下次渲染时显示降级UI - return {hasError: true, error} + static getDerivedStateFromError(error: Error): State { + return { + hasError: true, + error, + } } - public componentDidCatch(error: Error, errorInfo: ErrorInfo): void { - console.error('ErrorBoundary caught an error:', error, errorInfo) + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { + console.error('Error caught by ErrorBoundary:', error) + console.error('Component stack:', errorInfo.componentStack) } - public render(): ReactNode { + render(): ReactNode { if (this.state.hasError) { - // 显示自定义降级UI + // 如果提供了自定义的fallback,则使用它 + if (this.props.fallback) { + return this.props.fallback + } + + // 默认的错误UI return ( - this.props.fallback || ( -
-

组件加载出错

-

抱歉,组件渲染时发生错误。

-
{this.state.error && this.state.error.toString()}
-
- ) +
+

组件加载失败

+

发生了一个错误,无法正确加载组件。

+
+ 查看错误详情 + {this.state.error && this.state.error.toString()} +
+
) } - // 正常情况下渲染子组件 return this.props.children } } -export default ErrorBoundary +export default ErrorBoundary as any diff --git a/projects/adapter-vue2-host/emp.config.ts b/projects/adapter-vue2-host/emp.config.ts index f8c8dc39..15a56a87 100644 --- a/projects/adapter-vue2-host/emp.config.ts +++ b/projects/adapter-vue2-host/emp.config.ts @@ -10,7 +10,7 @@ export default defineConfig(store => { // shared: ['vue', 'vuex'], manifest: true, exposes: { - './Hello': './src/components/Hello', + './HelloVue': './src/components/HelloVue', './Content': './src/components/Content', './Table': './src/components/table', './CompositionApi': './src/components/CompositionApi', diff --git a/projects/adapter-vue2-host/src/bootstrap.js b/projects/adapter-vue2-host/src/bootstrap.js index 87f5553e..f5265427 100644 --- a/projects/adapter-vue2-host/src/bootstrap.js +++ b/projects/adapter-vue2-host/src/bootstrap.js @@ -1,10 +1,9 @@ import Vue from 'vue' import App from '@/App' import store from '@/store' -import plugin from './plugin' + import router from './router' -plugin(Vue) const app = new Vue({ router, store, diff --git a/projects/adapter-vue2-host/src/components/Hello.jsx b/projects/adapter-vue2-host/src/components/Hello.jsx index 523579b5..4d9a2f3e 100644 --- a/projects/adapter-vue2-host/src/components/Hello.jsx +++ b/projects/adapter-vue2-host/src/components/Hello.jsx @@ -1,4 +1,10 @@ export default { + props: { + name: { + type: String, + default: 'appName', + }, + }, methods: { handleButtonClick(e) { e.preventDefault() @@ -8,11 +14,11 @@ export default { render() { return (
- +
) }, } -export const Hello = () =>

hello jsx Component Here!

+export const Hello = ({name}) =>

hello jsx Component Here! {name}

diff --git a/projects/adapter-vue2-host/src/components/HelloVue.vue b/projects/adapter-vue2-host/src/components/HelloVue.vue new file mode 100644 index 00000000..e09cd0cd --- /dev/null +++ b/projects/adapter-vue2-host/src/components/HelloVue.vue @@ -0,0 +1,43 @@ + + + + + \ No newline at end of file diff --git a/projects/adapter-vue2-host/src/main.js b/projects/adapter-vue2-host/src/main.js index a747eeac..f94eccc4 100644 --- a/projects/adapter-vue2-host/src/main.js +++ b/projects/adapter-vue2-host/src/main.js @@ -1,3 +1,7 @@ // 使用 empShareLib后不需要再实例化 +import Vue from 'vue' +import plugin from './plugin' + +plugin(Vue) import('./bootstrap') diff --git a/projects/adapter-vue2-host/src/store/index.js b/projects/adapter-vue2-host/src/store/index.js index f69fa169..76a3d0e7 100644 --- a/projects/adapter-vue2-host/src/store/index.js +++ b/projects/adapter-vue2-host/src/store/index.js @@ -1,9 +1,4 @@ -import Vue from 'vue' import Vuex from 'vuex' - -// 必须在创建store实例之前调用Vue.use(Vuex) -Vue.use(Vuex) - export const countStore = new Vuex.Store({ state: { count: 0, diff --git a/projects/adapter-vue2-host/src/views/Home.vue b/projects/adapter-vue2-host/src/views/Home.vue index 137923f3..28c3162c 100644 --- a/projects/adapter-vue2-host/src/views/Home.vue +++ b/projects/adapter-vue2-host/src/views/Home.vue @@ -2,6 +2,8 @@

SVGA COMPONENT LOGO:

+

Hello Vue Component

+

Hello JSX Component

Img Example in src

@@ -27,6 +29,7 @@ import CompositionApi from '../components/CompositionApi' import Content from '../components/Content' import Hello from '../components/Hello' +import HelloVue from '../components/HelloVue' // import Logo from './logo.svg' import Table from '../components/table' export default { @@ -36,6 +39,7 @@ export default { Table, CompositionApi, Hello, + HelloVue, }, } From 69a8fc7457034b8bbee0ac76650b68cb15d7b322 Mon Sep 17 00:00:00 2001 From: xuhongbin Date: Tue, 23 Sep 2025 12:14:38 +0800 Subject: [PATCH 2/5] =?UTF-8?q?refactor(bridge):=20=E9=87=8D=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E8=BF=9C=E7=A8=8B=E7=BB=84=E4=BB=B6=E7=B1=BB=E5=90=8D?= =?UTF-8?q?=E5=B9=B6=E4=BC=98=E5=8C=96Vue2=E6=A1=A5=E6=8E=A5=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构React和Vue3桥接组件类名以更明确框架版本 完善Vue2桥接组件的props处理和错误处理逻辑 移除调试日志并优化组件卸载流程 --- packages/bridge-react/src/index.ts | 9 +- packages/bridge-vue2/backup/index.ts | 84 +++++++++ packages/bridge-vue2/src/index-bak.ts | 242 -------------------------- packages/bridge-vue2/src/index.ts | 194 ++++++++++++++++++--- packages/bridge-vue3/src/index.ts | 2 +- projects/adapter-app/src/App.tsx | 6 +- 6 files changed, 258 insertions(+), 279 deletions(-) create mode 100644 packages/bridge-vue2/backup/index.ts delete mode 100644 packages/bridge-vue2/src/index-bak.ts diff --git a/packages/bridge-react/src/index.ts b/packages/bridge-react/src/index.ts index 94a4012d..b8e0a409 100644 --- a/packages/bridge-react/src/index.ts +++ b/packages/bridge-react/src/index.ts @@ -87,7 +87,7 @@ export function createRemoteAppComponent( const {React} = reactOptions - class RemoteAppComponent extends React.Component { + class ReactRemoteAppComponent extends React.Component { containerRef = React.createRef() provider: BridgeProviderReturn | null = null providerInfo: BridgeProvider | null = null @@ -134,7 +134,6 @@ export function createRemoteAppComponent( } unmountComponent() { - console.log('unmountComponent') try { // 不强依赖containerRef.current存在,避免可能的null引用 if (this.provider) { @@ -155,20 +154,17 @@ export function createRemoteAppComponent( } componentDidMount() { - console.log('componentDidMount') this.isMounted = true if (this.providerInfo) this.renderComponent() } componentDidUpdate() { - console.log('componentDidUpdate') if (this.provider && this.containerRef.current) { this.provider.render(this.containerRef.current, this.props) } } componentWillUnmount() { - console.log('componentWillUnmount', reactOptions?.syncUnmount) this.isMounted = false // 检查是否使用同步卸载 @@ -176,7 +172,6 @@ export function createRemoteAppComponent( // 直接同步卸载组件,避免异步操作导致的DOM节点关系变化 this.unmountComponent() } else { - console.log('componentWillUnmount async', this.containerRef, this.containerRef.current) // 使用微任务队列,比setTimeout更快但仍然异步 Promise.resolve().then(() => { if (this.containerRef && this.containerRef.current) { @@ -191,7 +186,7 @@ export function createRemoteAppComponent( } } - return RemoteAppComponent + return ReactRemoteAppComponent } export default { diff --git a/packages/bridge-vue2/backup/index.ts b/packages/bridge-vue2/backup/index.ts new file mode 100644 index 00000000..432b1563 --- /dev/null +++ b/packages/bridge-vue2/backup/index.ts @@ -0,0 +1,84 @@ +// Type definitions +export interface BridgeProviderReturn { + render: (dom: HTMLElement, props?: Record) => void + destroy: (dom: HTMLElement) => void +} + +export type BridgeProvider = () => BridgeProviderReturn +export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> +export type ComponentProvider = BridgeProvider | AsyncBridgeProvider + +interface Vue2Options { + Vue?: any + plugin?: (vue: any) => void +} + +/** + * Create bridge component - for producer to wrap application-level export modules + */ +export function createBridgeComponent(Component: any, options: Vue2Options): BridgeProvider { + const Vue = options.Vue + + return function (): BridgeProviderReturn { + const instanceMap = new Map() + const render = (dom: HTMLElement, props?: Record): void => { + // 防御性检查:确保 DOM 元素存在 + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to render') + return + } + // props = Vue.observable(props) + console.log('props', props) + const existingInstance = instanceMap.get(dom) + console.log('existingInstance', existingInstance) + if (existingInstance) { + // 更新组件的渲染函数,使用新的props重新渲染组件 + existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) + + // 更新propsData + existingInstance.$options.propsData = props || {} + + // 强制更新组件 + existingInstance.$forceUpdate() + return + } + + // Create new Vue instance with correct props handling + const instance = new Vue({ + propsData: props || {}, + render: (h: any) => h(Component, {props: props || {}}), + el: dom, + }) + + // 使用自定义插件(如果提供) + if (options.plugin) { + options.plugin(Vue) + } + instanceMap.set(dom, instance) + } + + const destroy = (dom: HTMLElement): void => { + console.log('[destroy]', dom) + if (!dom || !(dom instanceof HTMLElement)) { + console.error('[EMP-ERROR] Invalid DOM element provided to destroy') + return + } + + const instance = instanceMap.get(dom) + if (!instance) return + } + + return {render, destroy} + } +} + +export function createRemoteAppComponent( + component: ComponentProvider, + vueOptions: Vue2Options, + options: {onError?: (error: Error) => void} = {}, +): any {} + +export default { + createBridgeComponent, + createRemoteAppComponent, +} diff --git a/packages/bridge-vue2/src/index-bak.ts b/packages/bridge-vue2/src/index-bak.ts deleted file mode 100644 index 927c96be..00000000 --- a/packages/bridge-vue2/src/index-bak.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Type definitions -export interface BridgeProviderReturn { - render: (dom: HTMLElement, props?: Record) => void - destroy: (dom: HTMLElement) => void -} - -export type BridgeProvider = () => BridgeProviderReturn -export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> -export type ComponentProvider = BridgeProvider | AsyncBridgeProvider - -interface Vue2Options { - Vue?: any - plugin?: (vue: any) => void -} - -/** - * Create bridge component - for producer to wrap application-level export modules - */ -export function createBridgeComponent(Component: any, options: Vue2Options): BridgeProvider { - const Vue = options.Vue - - return function (): BridgeProviderReturn { - const instanceMap = new Map() - - const render = (dom: HTMLElement, props?: Record): void => { - // 防御性检查:确保 DOM 元素存在 - if (!dom || !(dom instanceof HTMLElement)) { - console.error('[EMP-ERROR] Invalid DOM element provided to render') - return - } - - try { - const existingInstance = instanceMap.get(dom) - - if (existingInstance) { - // Update props for existing instance - if (props) { - // 参考 adapter/vue.ts 中的实现,合并 props 和 attrs - const mergedProps = {...existingInstance.$props, ...props} - - // 不直接修改 props,而是通过重新渲染组件来更新 - // Vue props 是只读的,不应该尝试直接修改它们 - try { - // 强制重新渲染组件,让 Vue 内部处理 props 更新 - existingInstance.$forceUpdate() - - // 如果需要更新 data 中的属性(非 props) - Object.keys(mergedProps).forEach(key => { - if (key in existingInstance.$data && !(existingInstance.$props && key in existingInstance.$props)) { - // 只更新不是 props 的 data 属性 - existingInstance.$set(existingInstance.$data, key, mergedProps[key]) - } - }) - } catch (error) { - console.warn('[EMP-WARN] Failed to update props:', error) - } - - // 触发组件更新 - existingInstance.$forceUpdate() - } - } else { - // Create new Vue instance with correct props handling - const instance = new Vue({ - data() { - // 只在data中存储非props的数据 - return {} - }, - // 使用propsData选项正确传递props - propsData: props || {}, - render: (h: any) => h(Component, {props: props || {}}), - el: dom, - }) - - // 使用自定义插件(如果提供) - if (options.plugin) { - options.plugin(Vue) - } - - instanceMap.set(dom, instance) - } - } catch (error) { - console.error('[EMP-ERROR] Failed to render/update Vue component', error) - throw error - } - } - - const destroy = (dom: HTMLElement): void => { - // 防御性检查:确保 DOM 元素存在 - if (!dom || !(dom instanceof HTMLElement)) { - console.error('[EMP-ERROR] Invalid DOM element provided to destroy') - return - } - - const instance = instanceMap.get(dom) - if (!instance) return - - try { - // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 - if (dom && dom.parentNode) { - while (dom.firstChild) { - dom.removeChild(dom.firstChild) - } - } - - // 先解除引用,再销毁 Vue 实例 - const vmToDestroy = instance - instanceMap.delete(dom) - - // 确保在下一个事件循环中销毁,避免与React卸载冲突 - setTimeout(() => { - vmToDestroy.$destroy() - }, 0) - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount Vue component', error) - } - } - - return {render, destroy} - } -} - -/** - * Create remote app component - for consumer to load application-level modules - */ -export function createRemoteAppComponent( - component: ComponentProvider, - vueOptions: Vue2Options, - options: {onError?: (error: Error) => void} = {}, -): any { - if (!component) { - throw new Error('createRemoteAppComponent: component parameter cannot be empty') - } - - return { - name: 'RemoteAppComponent', - // 允许接收任意 props - props: { - name: String, - [Symbol.toPrimitive]: Function, - }, - data() { - return { - provider: null, - providerInfo: null, - isMounted: false, - } - }, - methods: { - async loadComponent() { - try { - if (typeof component === 'function') { - const result = component() - - if (result instanceof Promise) { - const module = await result - this.providerInfo = module.default - } else { - this.providerInfo = component as BridgeProvider - } - } - - if (this.isMounted && this.$el) { - this.renderComponent() - } - } catch (error) { - if (options.onError) options.onError(error as Error) - console.error('[EMP-ERROR] Failed to load component', error) - } - }, - renderComponent() { - if (!this.providerInfo || !this.$el) return - - try { - if (!this.provider && this.providerInfo) { - this.provider = this.providerInfo() - } - - if (!this.provider) { - console.warn('[EMP-WARN] Provider not available yet') - return - } - - // 确保传递正确的 props,Vue2 中 $props 可能不存在 - const props = this.$props || this.$options.propsData || {} - - // 确保 props 是对象类型 - if (props && typeof props === 'object') { - this.provider.render(this.$el, props) - } else { - this.provider.render(this.$el, {}) - } - } catch (error) { - console.error('[EMP-ERROR] Failed to render component', error) - if (options.onError) options.onError(error as Error) - } - }, - unmountComponent() { - if (this.provider && this.$el) { - try { - // 先清空 DOM 内容,避免 React 移除时的冲突 - while (this.$el.firstChild) { - this.$el.removeChild(this.$el.firstChild) - } - - // 然后销毁 Vue 组件 - this.provider.destroy(this.$el) - this.provider = null - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount component', error) - } - } - }, - }, - mounted() { - this.isMounted = true - if (this.providerInfo) this.renderComponent() - }, - updated() { - if (this.provider && this.$el) { - // 获取 props 的安全方式 - const props = this.$props || this.$options.propsData || {} - this.provider.render(this.$el, props) - } - }, - beforeDestroy() { - this.isMounted = false - this.unmountComponent() - }, - created() { - // 立即加载组件 - this.loadComponent() - }, - render(h: any) { - return h('div') - }, - } -} - -export default { - createBridgeComponent, - createRemoteAppComponent, -} diff --git a/packages/bridge-vue2/src/index.ts b/packages/bridge-vue2/src/index.ts index 3019b28d..a655dfc1 100644 --- a/packages/bridge-vue2/src/index.ts +++ b/packages/bridge-vue2/src/index.ts @@ -21,43 +21,54 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri return function (): BridgeProviderReturn { const instanceMap = new Map() + const render = (dom: HTMLElement, props?: Record): void => { // 防御性检查:确保 DOM 元素存在 if (!dom || !(dom instanceof HTMLElement)) { console.error('[EMP-ERROR] Invalid DOM element provided to render') return } - // props = Vue.observable(props) - console.log('props', props) - const existingInstance = instanceMap.get(dom) - console.log('existingInstance', existingInstance) - if (existingInstance) { - // 更新组件的渲染函数,使用新的props重新渲染组件 - existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) - - // 更新propsData - existingInstance.$options.propsData = props || {} - - // 强制更新组件 - existingInstance.$forceUpdate() - return - } - // Create new Vue instance with correct props handling - const instance = new Vue({ - propsData: props || {}, - render: (h: any) => h(Component, {props: props || {}}), - el: dom, - }) + try { + const existingInstance = instanceMap.get(dom) + + if (existingInstance) { + // Update props for existing instance + if (props) { + try { + // 更新组件的渲染函数,使用新的props重新渲染组件 + existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) - // 使用自定义插件(如果提供) - if (options.plugin) { - options.plugin(Vue) + // 更新propsData + existingInstance.$options.propsData = props || {} + + // 强制更新组件 + existingInstance.$forceUpdate() + } catch (error) { + console.warn('[EMP-WARN] Failed to update props:', error) + } + } + } else { + const instance = new Vue({ + propsData: props || {}, + render: (h: any) => h(Component, {props: props || {}}), + el: dom, + }) + + // 使用自定义插件(如果提供) + if (options.plugin) { + options.plugin(Vue) + } + instanceMap.set(dom, instance) + } + } catch (error) { + console.error('[EMP-ERROR] Failed to render/update Vue component', error) + throw error } - instanceMap.set(dom, instance) } const destroy = (dom: HTMLElement): void => { + // 防御性检查:确保 DOM 元素存在 if (!dom || !(dom instanceof HTMLElement)) { console.error('[EMP-ERROR] Invalid DOM element provided to destroy') return @@ -65,17 +76,148 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri const instance = instanceMap.get(dom) if (!instance) return + + try { + // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 + if (dom && dom.parentNode) { + while (dom.firstChild) { + dom.removeChild(dom.firstChild) + } + } + + // 先解除引用,再销毁 Vue 实例 + const vmToDestroy = instance + instanceMap.delete(dom) + + // 确保在下一个事件循环中销毁,避免与React卸载冲突 + setTimeout(() => { + vmToDestroy.$destroy() + }, 0) + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount Vue component', error) + } } return {render, destroy} } } +/** + * Create remote app component - for consumer to load application-level modules + */ export function createRemoteAppComponent( component: ComponentProvider, vueOptions: Vue2Options, options: {onError?: (error: Error) => void} = {}, -): any {} +): any { + if (!component) { + throw new Error('createRemoteAppComponent: component parameter cannot be empty') + } + + return { + name: 'Vue2RemoteAppComponent', + // 允许接收任意 props + props: { + name: String, + [Symbol.toPrimitive]: Function, + }, + data() { + return { + provider: null, + providerInfo: null, + isMounted: false, + } + }, + methods: { + async loadComponent() { + try { + if (typeof component === 'function') { + const result = component() + + if (result instanceof Promise) { + const module = await result + this.providerInfo = module.default + } else { + this.providerInfo = component as BridgeProvider + } + } + + if (this.isMounted && this.$el) { + this.renderComponent() + } + } catch (error) { + if (options.onError) options.onError(error as Error) + console.error('[EMP-ERROR] Failed to load component', error) + } + }, + renderComponent() { + if (!this.providerInfo || !this.$el) return + + try { + if (!this.provider && this.providerInfo) { + this.provider = this.providerInfo() + } + + if (!this.provider) { + console.warn('[EMP-WARN] Provider not available yet') + return + } + + // 确保传递正确的 props,Vue2 中 $props 可能不存在 + const props = this.$props || this.$options.propsData || {} + + // 确保 props 是对象类型 + if (props && typeof props === 'object') { + this.provider.render(this.$el, props) + } else { + this.provider.render(this.$el, {}) + } + } catch (error) { + console.error('[EMP-ERROR] Failed to render component', error) + if (options.onError) options.onError(error as Error) + } + }, + unmountComponent() { + if (this.provider && this.$el) { + try { + // 先清空 DOM 内容,避免 React 移除时的冲突 + while (this.$el.firstChild) { + this.$el.removeChild(this.$el.firstChild) + } + + // 然后销毁 Vue 组件 + this.provider.destroy(this.$el) + this.provider = null + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount component', error) + } + } + }, + }, + mounted() { + this.isMounted = true + if (this.providerInfo) this.renderComponent() + }, + updated() { + if (this.provider && this.$el) { + // 获取 props 的安全方式 + const props = this.$props || this.$options.propsData || {} + this.provider.render(this.$el, props) + } + }, + beforeDestroy() { + this.isMounted = false + this.unmountComponent() + }, + created() { + // 立即加载组件 + this.loadComponent() + }, + render(h: any) { + return h('div') + }, + } +} export default { createBridgeComponent, diff --git a/packages/bridge-vue3/src/index.ts b/packages/bridge-vue3/src/index.ts index 830fb5f9..4fea41cd 100644 --- a/packages/bridge-vue3/src/index.ts +++ b/packages/bridge-vue3/src/index.ts @@ -82,7 +82,7 @@ export function createRemoteAppComponent( const {Vue} = vueOptions return defineComponent({ - name: 'RemoteAppComponent', + name: 'Vue3RemoteAppComponent', props: {}, setup() { const provider = ref(null) diff --git a/projects/adapter-app/src/App.tsx b/projects/adapter-app/src/App.tsx index 2de1f647..775e00db 100644 --- a/projects/adapter-app/src/App.tsx +++ b/projects/adapter-app/src/App.tsx @@ -8,9 +8,9 @@ const App = () => (

Vue 2 Remote App

- + {/* */}
- + {/* @@ -25,7 +25,7 @@ const App = () => ( - + */}
) From b29b9f405552005e0ea3aa7d720683114c3be65b Mon Sep 17 00:00:00 2001 From: xuhongbin Date: Thu, 25 Sep 2025 10:41:25 +0800 Subject: [PATCH 3/5] =?UTF-8?q?feat:=20=E5=8D=87=E7=BA=A7=20React=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E8=87=B3=2017=20=E5=B9=B6=E9=87=8D=E6=9E=84?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E6=A1=A5=E6=8E=A5=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构组件命名和样式以支持多版本 React 新增工具函数和类型定义文件 优化桥接组件的实现,区分类组件和 Hook 组件 更新依赖项和锁文件以匹配 React 17 --- packages/bridge-react/src/classComponent.ts | 179 ++++++++++++++ packages/bridge-react/src/hookComponent.ts | 232 ++++++++++++++++++ packages/bridge-react/src/index.ts | 220 ++++------------- packages/bridge-react/src/types.ts | 21 ++ packages/bridge-react/src/utils.ts | 20 ++ packages/bridge-vue2/src/index.ts | 11 +- pnpm-lock.yaml | 16 +- projects/adapter-app/package.json | 8 +- projects/adapter-app/src/App.tsx | 14 +- .../src/components/Info.module.scss | 2 +- projects/adapter-app/src/components/Info.tsx | 4 +- 11 files changed, 526 insertions(+), 201 deletions(-) create mode 100644 packages/bridge-react/src/classComponent.ts create mode 100644 packages/bridge-react/src/hookComponent.ts create mode 100644 packages/bridge-react/src/types.ts create mode 100644 packages/bridge-react/src/utils.ts diff --git a/packages/bridge-react/src/classComponent.ts b/packages/bridge-react/src/classComponent.ts new file mode 100644 index 00000000..10424129 --- /dev/null +++ b/packages/bridge-react/src/classComponent.ts @@ -0,0 +1,179 @@ +import { BridgeProvider, BridgeProviderReturn, ComponentProvider, ReactOptions } from './types' +import { handleError } from './utils' + +/** + * Create bridge component - for producer to wrap application-level export modules + */ +export function createBridgeComponent(Component: any, options: ReactOptions): BridgeProvider { + const {React, ReactDOM, createRoot} = options + const hasCreateRoot = typeof createRoot === 'function' + + return function (): BridgeProviderReturn { + const rootMap = new Map() + + const render = (dom: HTMLElement, props?: Record): void => { + try { + const element = React.createElement(Component, props || {}) + const existingRoot = rootMap.get(dom) + + if (existingRoot) { + if (hasCreateRoot && 'render' in existingRoot) { + existingRoot.render(element) + } else if (ReactDOM.render) { + ReactDOM.render(element, dom) + } + } else { + if (hasCreateRoot && createRoot) { + const root = createRoot(dom) + root.render(element) + rootMap.set(dom, root) + } else if (ReactDOM.render) { + ReactDOM.render(element, dom) + rootMap.set(dom, dom) + } + } + } catch (error) { + handleError(error as Error, 'Failed to render/update component') + throw error + } + } + + const destroy = (dom: HTMLElement): void => { + const root = rootMap.get(dom) + if (!root) return + + try { + if (hasCreateRoot && 'unmount' in root) { + root.unmount() + } else if (ReactDOM.unmountComponentAtNode) { + ReactDOM.unmountComponentAtNode(dom) + } + rootMap.delete(dom) + } catch (error) { + handleError(error as Error, 'Failed to unmount component') + } + } + + return {render, destroy} + } +} + +/** + * Create remote app component - for consumer to load application-level modules + */ +export function createRemoteAppComponent( + component: ComponentProvider, + reactOptions: ReactOptions, + options: {onError?: (error: Error) => void} = {}, +): any { + if (!component) { + throw new Error('createRemoteAppComponent: component parameter cannot be empty') + } + + const {React} = reactOptions + + class ReactRemoteAppComponent extends React.Component { + containerRef = React.createRef() + provider: BridgeProviderReturn | null = null + providerInfo: BridgeProvider | null = null + isMounted = false + + constructor(props: any) { + super(props) + this.loadComponent() + } + + async loadComponent() { + try { + if (typeof component === 'function') { + const result = component() + + if (result instanceof Promise) { + const module = await result + this.providerInfo = module.default + } else { + this.providerInfo = component as BridgeProvider + } + } + + if (this.isMounted && this.containerRef.current) { + this.renderComponent() + } + } catch (error) { + handleError(error as Error, 'Failed to load component', options.onError) + } + } + + renderComponent() { + if (!this.providerInfo || !this.containerRef.current) return + + try { + if (!this.provider) { + this.provider = this.providerInfo() + } + this.provider.render(this.containerRef.current, this.props) + } catch (error) { + handleError(error as Error, 'Failed to render component') + } + } + + unmountComponent() { + try { + // 不强依赖containerRef.current存在,避免可能的null引用 + if (this.provider) { + if (this.containerRef && this.containerRef.current) { + try { + this.provider.destroy(this.containerRef.current) + } catch (destroyError) { + console.warn('[bridge-react] Error during provider unmount:', destroyError) + } + } + + // 确保清理provider引用 + this.provider = null + } + } catch (error) { + handleError(error as Error, 'Failed to unmount component') + } + } + + componentDidMount() { + this.isMounted = true + if (this.providerInfo) this.renderComponent() + } + + componentDidUpdate() { + if (this.provider && this.containerRef.current) { + this.provider.render(this.containerRef.current, this.props) + } + } + + componentWillUnmount() { + this.isMounted = false + + // 检查是否使用同步卸载 + if (reactOptions?.syncUnmount) { + // 直接同步卸载组件,避免异步操作导致的DOM节点关系变化 + this.unmountComponent() + } else { + // 使用微任务队列,比setTimeout更快但仍然异步 + Promise.resolve().then(() => { + if (this.containerRef && this.containerRef.current) { + this.unmountComponent() + } + }) + } + } + + render() { + return React.createElement('div', {ref: this.containerRef}) + } + } + + return ReactRemoteAppComponent +} + +export default { + createBridgeComponent, + createRemoteAppComponent, +} diff --git a/packages/bridge-react/src/hookComponent.ts b/packages/bridge-react/src/hookComponent.ts new file mode 100644 index 00000000..45819faf --- /dev/null +++ b/packages/bridge-react/src/hookComponent.ts @@ -0,0 +1,232 @@ +import {BridgeProvider, BridgeProviderReturn, ComponentProvider, ReactOptions} from './types' +import {getReactVersion, handleError} from './utils' + +/** + * Create bridge component - for producer to wrap application-level export modules + */ +export function createBridgeComponent(Component: any, options: ReactOptions): BridgeProvider { + const {React, ReactDOM, createRoot} = options + const hasCreateRoot = typeof createRoot === 'function' + + return function (): BridgeProviderReturn { + const rootMap = new Map() + + const render = (dom: HTMLElement, props?: Record): void => { + try { + const element = React.createElement(Component, props || {}) + const existingRoot = rootMap.get(dom) + + if (existingRoot) { + if (hasCreateRoot && 'render' in existingRoot) { + existingRoot.render(element) + } else if (ReactDOM.render) { + ReactDOM.render(element, dom) + } + } else { + if (hasCreateRoot && createRoot) { + const root = createRoot(dom) + root.render(element) + rootMap.set(dom, root) + } else if (ReactDOM.render) { + ReactDOM.render(element, dom) + rootMap.set(dom, dom) + } + } + } catch (error) { + handleError(error as Error, 'Failed to render/update component') + throw error + } + } + + const destroy = (dom: HTMLElement): void => { + const root = rootMap.get(dom) + if (!root) return + + try { + if (hasCreateRoot && 'unmount' in root) { + root.unmount() + } else if (ReactDOM.unmountComponentAtNode) { + ReactDOM.unmountComponentAtNode(dom) + } + rootMap.delete(dom) + } catch (error) { + handleError(error as Error, 'Failed to unmount component') + } + } + + return {render, destroy} + } +} + +/** + * Create remote app component - for consumer to load application-level modules using hooks + */ +export function createRemoteAppComponent( + component: ComponentProvider, + reactOptions: ReactOptions, + options: {onError?: (error: Error) => void} = {}, +): any { + if (!component) { + throw new Error('createRemoteAppComponent: component parameter cannot be empty') + } + + const {React} = reactOptions + + const reactVersion = getReactVersion(React) + const isReact18OrAbove = reactVersion >= 18 + + // 使用函数组件和Hooks替代类组件 + return function ReactRemoteAppHookComponent(props: any) { + const containerRef = React.useRef(null) + const providerRef = React.useRef(null) + const providerInfoRef = React.useRef(null) + const [isLoaded, setIsLoaded] = React.useState(false) + const [error, setError] = React.useState(null) + + // React 18+ 使用 useSyncExternalStore 来处理外部状态更新 + const forceUpdate = + isReact18OrAbove && React.useSyncExternalStore + ? React.useSyncExternalStore( + (callback: any) => { + const id = Math.random().toString(36) + const listeners = providerRef.current?.['_listeners'] || new Map() + listeners.set(id, callback) + if (providerRef.current) providerRef.current['_listeners'] = listeners + return () => { + if (providerRef.current?.['_listeners']) { + providerRef.current['_listeners'].delete(id) + } + } + }, + () => props, + () => props, + ) + : props + + // 加载组件 + const loadComponent = React.useCallback(async () => { + try { + if (typeof component === 'function') { + const result = component() + + if (result instanceof Promise) { + const module = await result + providerInfoRef.current = module.default + } else { + providerInfoRef.current = component as BridgeProvider + } + + setIsLoaded(true) + } + } catch (err) { + const error = err as Error + setError(error) + handleError(error, 'Failed to load component', options.onError) + } + }, []) + + // 渲染组件 + const renderComponent = React.useCallback(() => { + if (!providerInfoRef.current || !containerRef.current) return + + try { + if (!providerRef.current) { + providerRef.current = providerInfoRef.current() + } + providerRef.current.render(containerRef.current, props) + } catch (error) { + handleError(error as Error, 'Failed to render component') + } + }, [props]) + + // 卸载组件 + const unmountComponent = React.useCallback(() => { + try { + if (providerRef.current) { + if (containerRef.current) { + try { + providerRef.current.destroy(containerRef.current) + } catch (destroyError) { + handleError(destroyError as Error, 'Error during provider unmount') + } + } + providerRef.current = null + } + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount component', error) + } + }, []) + + // 组件加载 + React.useEffect(() => { + loadComponent() + }, [loadComponent]) + + // 组件渲染 + React.useEffect(() => { + if (isLoaded && containerRef.current) { + renderComponent() + } + }, [isLoaded, renderComponent]) + + // 组件更新 - 根据 React 版本使用不同的更新策略 + React.useEffect(() => { + if (isLoaded && providerRef.current && containerRef.current) { + // React 18+ 使用 forceUpdate 触发更新 + if (isReact18OrAbove) { + // forceUpdate 已经在 useSyncExternalStore 中处理 + providerRef.current.render(containerRef.current, props) + } else { + // React 16/17 直接更新 + providerRef.current.render(containerRef.current, props) + } + } + }, [props, isLoaded, forceUpdate]) + + // 组件卸载 + React.useEffect(() => { + return () => { + if (reactOptions?.syncUnmount) { + // 同步卸载 + unmountComponent() + } else { + // 使用微任务队列,比setTimeout更快但仍然异步 + Promise.resolve().then(() => { + if (containerRef.current) { + unmountComponent() + } + }) + } + } + }, [unmountComponent]) + + // 处理错误边界 + if (error && reactOptions.errorBoundary) { + return React.createElement( + 'div', + { + className: 'emp-error-boundary', + style: { + color: 'red', + padding: '10px', + border: '1px solid red', + borderRadius: '4px', + margin: '10px 0', + }, + }, + `Error loading component: ${error.message}`, + ) + } + + return React.createElement('div', { + ref: (node: HTMLElement | null) => { + containerRef.current = node + }, + }) + } +} + +export default { + createBridgeComponent, + createRemoteAppComponent, +} diff --git a/packages/bridge-react/src/index.ts b/packages/bridge-react/src/index.ts index b8e0a409..6c0d1f17 100644 --- a/packages/bridge-react/src/index.ts +++ b/packages/bridge-react/src/index.ts @@ -1,192 +1,64 @@ -// Type definitions -export interface BridgeProviderReturn { - render: (dom: HTMLElement, props?: Record) => void - destroy: (dom: HTMLElement) => void -} - -export type BridgeProvider = () => BridgeProviderReturn -export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> -export type ComponentProvider = BridgeProvider | AsyncBridgeProvider - -interface ReactOptions { - React?: any - ReactDOM?: any - createRoot?: any - syncUnmount?: boolean - errorBoundary?: boolean +// 导入类型和实现 +import { + createBridgeComponent as createClassBridgeComponent, + createRemoteAppComponent as createClassRemoteAppComponent, +} from './classComponent' +import {createRemoteAppComponent as createHookRemoteAppComponent} from './hookComponent' +import { + AsyncBridgeProvider, + BridgeProvider, + BridgeProviderReturn, + ComponentProvider, + ReactOptions as ReactComponentOptions, + RemoteComponentOptions, +} from './types' +import {getReactVersion, handleError} from './utils' + +// 重新导出类型 +export { + BridgeProviderReturn, + BridgeProvider, + AsyncBridgeProvider, + ComponentProvider, + ReactComponentOptions, + RemoteComponentOptions, } /** - * Create bridge component - for producer to wrap application-level export modules + * 创建桥接组件 - 用于生产者包装应用级导出模块 */ -export function createBridgeComponent(Component: any, options: ReactOptions): BridgeProvider { - const {React, ReactDOM, createRoot} = options - const hasCreateRoot = typeof createRoot === 'function' - - return function (): BridgeProviderReturn { - const rootMap = new Map() - - const render = (dom: HTMLElement, props?: Record): void => { - try { - const element = React.createElement(Component, props || {}) - const existingRoot = rootMap.get(dom) - - if (existingRoot) { - if (hasCreateRoot && 'render' in existingRoot) { - existingRoot.render(element) - } else if (ReactDOM.render) { - ReactDOM.render(element, dom) - } - } else { - if (hasCreateRoot && createRoot) { - const root = createRoot(dom) - root.render(element) - rootMap.set(dom, root) - } else if (ReactDOM.render) { - ReactDOM.render(element, dom) - rootMap.set(dom, dom) - } - } - } catch (error) { - console.error('[EMP-ERROR] Failed to render/update component', error) - throw error - } - } - - const destroy = (dom: HTMLElement): void => { - const root = rootMap.get(dom) - if (!root) return - - try { - if (hasCreateRoot && 'unmount' in root) { - root.unmount() - } else if (ReactDOM.unmountComponentAtNode) { - ReactDOM.unmountComponentAtNode(dom) - } - rootMap.delete(dom) - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount component', error) - } - } - - return {render, destroy} +export function createBridgeComponent(Component: any, options: ReactComponentOptions) { + try { + return createClassBridgeComponent(Component, options) + } catch (error) { + handleError(error as Error, 'Failed to create bridge component') + throw error } } /** - * Create remote app component - for consumer to load application-level modules + * 创建远程应用组件 - 用于消费者加载应用级模块 */ export function createRemoteAppComponent( component: ComponentProvider, - reactOptions: ReactOptions, - options: {onError?: (error: Error) => void} = {}, -): any { - if (!component) { - throw new Error('createRemoteAppComponent: component parameter cannot be empty') - } - - const {React} = reactOptions - - class ReactRemoteAppComponent extends React.Component { - containerRef = React.createRef() - provider: BridgeProviderReturn | null = null - providerInfo: BridgeProvider | null = null - isMounted = false - - constructor(props: any) { - super(props) - this.loadComponent() + reactOptions: ReactComponentOptions, + options: RemoteComponentOptions = {}, +) { + try { + if (!component) { + throw new Error('createRemoteAppComponent: component parameter cannot be empty') } - async loadComponent() { - try { - if (typeof component === 'function') { - const result = component() + const reactVersion = getReactVersion(reactOptions.React) - if (result instanceof Promise) { - const module = await result - this.providerInfo = module.default - } else { - this.providerInfo = component as BridgeProvider - } - } - - if (this.isMounted && this.containerRef.current) { - this.renderComponent() - } - } catch (error) { - if (options.onError) options.onError(error as Error) - console.error('[EMP-ERROR] Failed to load component', error) - } - } - - renderComponent() { - if (!this.providerInfo || !this.containerRef.current) return - - try { - if (!this.provider) { - this.provider = this.providerInfo() - } - this.provider.render(this.containerRef.current, this.props) - } catch (error) { - console.error('[EMP-ERROR] Failed to render component', error) - } - } - - unmountComponent() { - try { - // 不强依赖containerRef.current存在,避免可能的null引用 - if (this.provider) { - if (this.containerRef && this.containerRef.current) { - try { - this.provider.destroy(this.containerRef.current) - } catch (destroyError) { - console.warn('[bridge-react] Error during provider unmount:', destroyError) - } - } - - // 确保清理provider引用 - this.provider = null - } - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount component', error) - } - } - - componentDidMount() { - this.isMounted = true - if (this.providerInfo) this.renderComponent() - } - - componentDidUpdate() { - if (this.provider && this.containerRef.current) { - this.provider.render(this.containerRef.current, this.props) - } - } - - componentWillUnmount() { - this.isMounted = false - - // 检查是否使用同步卸载 - if (reactOptions?.syncUnmount) { - // 直接同步卸载组件,避免异步操作导致的DOM节点关系变化 - this.unmountComponent() - } else { - // 使用微任务队列,比setTimeout更快但仍然异步 - Promise.resolve().then(() => { - if (this.containerRef && this.containerRef.current) { - this.unmountComponent() - } - }) - } - } - - render() { - return React.createElement('div', {ref: this.containerRef}) - } + // React 17+ 使用 Hook 实现,否则使用类组件实现 + return reactVersion >= 17 + ? createHookRemoteAppComponent(component, reactOptions, options) + : createClassRemoteAppComponent(component, reactOptions, options) + } catch (error) { + handleError(error as Error, 'Failed to create remote app component', options?.onError) + throw error } - - return ReactRemoteAppComponent } export default { diff --git a/packages/bridge-react/src/types.ts b/packages/bridge-react/src/types.ts new file mode 100644 index 00000000..8fce1aa4 --- /dev/null +++ b/packages/bridge-react/src/types.ts @@ -0,0 +1,21 @@ +// 统一类型定义 +export interface BridgeProviderReturn { + render: (dom: HTMLElement, props?: Record) => void + destroy: (dom: HTMLElement) => void +} + +export type BridgeProvider = () => BridgeProviderReturn +export type AsyncBridgeProvider = () => Promise<{default: BridgeProvider}> +export type ComponentProvider = BridgeProvider | AsyncBridgeProvider + +export interface ReactOptions { + React: any + ReactDOM?: any + createRoot?: any + syncUnmount?: boolean + errorBoundary?: boolean +} + +export interface RemoteComponentOptions { + onError?: (error: Error) => void +} diff --git a/packages/bridge-react/src/utils.ts b/packages/bridge-react/src/utils.ts new file mode 100644 index 00000000..79169bac --- /dev/null +++ b/packages/bridge-react/src/utils.ts @@ -0,0 +1,20 @@ +// 公共工具函数 + +/** + * 获取 React 版本号 + */ +export function getReactVersion(React: any): number { + if (!React || !React.version) return 16 + const versionStr = React.version.split('.') + return Number.parseInt(versionStr[0], 10) +} + +/** + * 统一错误处理 + */ +export function handleError(error: Error, message: string, onError?: (error: Error) => void): void { + console.error(`[EMP-ERROR] ${message}`, error) + if (onError) { + onError(error) + } +} \ No newline at end of file diff --git a/packages/bridge-vue2/src/index.ts b/packages/bridge-vue2/src/index.ts index a655dfc1..d251bcdd 100644 --- a/packages/bridge-vue2/src/index.ts +++ b/packages/bridge-vue2/src/index.ts @@ -75,15 +75,16 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } const instance = instanceMap.get(dom) + console.log('[bridge-vue2] destroy', dom, instance) if (!instance) return try { // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 - if (dom && dom.parentNode) { - while (dom.firstChild) { - dom.removeChild(dom.firstChild) - } - } + // if (dom && dom.parentNode) { + // while (dom.firstChild) { + // dom.removeChild(dom.firstChild) + // } + // } // 先解除引用,再销毁 Vue 实例 const vmToDestroy = instance diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 932049d3..6cf6a8d2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -707,11 +707,11 @@ importers: specifier: workspace:^ version: link:../../packages/emp-share react: - specifier: '16' - version: 16.14.0 + specifier: '17' + version: 17.0.2 react-dom: - specifier: '16' - version: 16.14.0(react@16.14.0) + specifier: '17' + version: 17.0.2(react@17.0.2) devDependencies: '@empjs/cli': specifier: workspace:* @@ -720,11 +720,11 @@ importers: specifier: workspace:* version: link:../../packages/plugin-react '@types/react': - specifier: '16' - version: 16.9.9 + specifier: '17' + version: 17.0.87 '@types/react-dom': - specifier: '16' - version: 16.9.9 + specifier: '17' + version: 17.0.26(@types/react@17.0.87) projects/adapter-host: dependencies: diff --git a/projects/adapter-app/package.json b/projects/adapter-app/package.json index b554c689..cb1d7516 100644 --- a/projects/adapter-app/package.json +++ b/projects/adapter-app/package.json @@ -17,8 +17,8 @@ "devDependencies": { "@empjs/cli": "workspace:*", "@empjs/plugin-react": "workspace:*", - "@types/react": "16", - "@types/react-dom": "16" + "@types/react": "17", + "@types/react-dom": "17" }, "dependencies": { "@empjs/adapter-react": "workspace:^", @@ -26,7 +26,7 @@ "@empjs/bridge-vue2": "workspace:^", "@empjs/bridge-vue3": "workspace:^", "@empjs/share": "workspace:^", - "react": "16", - "react-dom": "16" + "react": "17", + "react-dom": "17" } } diff --git a/projects/adapter-app/src/App.tsx b/projects/adapter-app/src/App.tsx index 775e00db..551a7f53 100644 --- a/projects/adapter-app/src/App.tsx +++ b/projects/adapter-app/src/App.tsx @@ -2,21 +2,21 @@ import React from 'react' import {Remote18App} from './adapter/React18' import {RemoteVue2App} from './adapter/Vue2' import {RemoteVue3App} from './adapter/Vue3' -import {Box, React16Info} from './components/Info' +import {Box, ReactInfo} from './components/Info' const App = () => (

Vue 2 Remote App

- {/* */} +
- {/* + - + - + - +

Vue3 Component

@@ -25,7 +25,7 @@ const App = () => ( -
*/} +
) diff --git a/projects/adapter-app/src/components/Info.module.scss b/projects/adapter-app/src/components/Info.module.scss index 1f70bc16..c736e21b 100644 --- a/projects/adapter-app/src/components/Info.module.scss +++ b/projects/adapter-app/src/components/Info.module.scss @@ -3,7 +3,7 @@ padding: 10px !important; } -.react16Info { +.reactInfo { h1 { font-size: 18px; } diff --git a/projects/adapter-app/src/components/Info.tsx b/projects/adapter-app/src/components/Info.tsx index e1c92c28..157f6a74 100644 --- a/projects/adapter-app/src/components/Info.tsx +++ b/projects/adapter-app/src/components/Info.tsx @@ -1,8 +1,8 @@ import React from 'react' import style from './Info.module.scss' -export const React16Info: any = (props: any) => { +export const ReactInfo: any = (props: any) => { return ( -
+

React App

React Version {React.version}

Props desc
From b1c9a8e9eea5565bb316f7ddabb8077fca9dc104 Mon Sep 17 00:00:00 2001 From: xuhongbin Date: Thu, 25 Sep 2025 12:28:24 +0800 Subject: [PATCH 4/5] =?UTF-8?q?refactor(bridge-vue2):=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?Vue2=E6=A1=A5=E6=8E=A5=E7=BB=84=E4=BB=B6=E9=94=80=E6=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=E5=92=8CDOM=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构Vue2桥接组件的销毁流程,增加容器元素隔离Vue实例 移除React Hook实现,统一使用类组件实现 改进DOM清理方式,增强组件卸载时的稳定性 --- packages/bridge-react/src/hookComponent.ts | 232 ------------------ packages/bridge-react/src/index.ts | 47 +--- packages/bridge-vue2/src/index.ts | 75 ++++-- packages/bridge-vue2/tsconfig.json | 1 + projects/adapter-app/emp.config.ts | 1 + projects/adapter-app/src/App.tsx | 2 +- .../src/adapter/{Vue2.ts => Vue2.tsx} | 6 +- projects/adapter-app/src/global.d.ts | 1 + projects/adapter-vue2-host/emp.config.ts | 3 +- .../src/{main.js => index.js} | 0 10 files changed, 76 insertions(+), 292 deletions(-) delete mode 100644 packages/bridge-react/src/hookComponent.ts rename projects/adapter-app/src/adapter/{Vue2.ts => Vue2.tsx} (78%) rename projects/adapter-vue2-host/src/{main.js => index.js} (100%) diff --git a/packages/bridge-react/src/hookComponent.ts b/packages/bridge-react/src/hookComponent.ts deleted file mode 100644 index 45819faf..00000000 --- a/packages/bridge-react/src/hookComponent.ts +++ /dev/null @@ -1,232 +0,0 @@ -import {BridgeProvider, BridgeProviderReturn, ComponentProvider, ReactOptions} from './types' -import {getReactVersion, handleError} from './utils' - -/** - * Create bridge component - for producer to wrap application-level export modules - */ -export function createBridgeComponent(Component: any, options: ReactOptions): BridgeProvider { - const {React, ReactDOM, createRoot} = options - const hasCreateRoot = typeof createRoot === 'function' - - return function (): BridgeProviderReturn { - const rootMap = new Map() - - const render = (dom: HTMLElement, props?: Record): void => { - try { - const element = React.createElement(Component, props || {}) - const existingRoot = rootMap.get(dom) - - if (existingRoot) { - if (hasCreateRoot && 'render' in existingRoot) { - existingRoot.render(element) - } else if (ReactDOM.render) { - ReactDOM.render(element, dom) - } - } else { - if (hasCreateRoot && createRoot) { - const root = createRoot(dom) - root.render(element) - rootMap.set(dom, root) - } else if (ReactDOM.render) { - ReactDOM.render(element, dom) - rootMap.set(dom, dom) - } - } - } catch (error) { - handleError(error as Error, 'Failed to render/update component') - throw error - } - } - - const destroy = (dom: HTMLElement): void => { - const root = rootMap.get(dom) - if (!root) return - - try { - if (hasCreateRoot && 'unmount' in root) { - root.unmount() - } else if (ReactDOM.unmountComponentAtNode) { - ReactDOM.unmountComponentAtNode(dom) - } - rootMap.delete(dom) - } catch (error) { - handleError(error as Error, 'Failed to unmount component') - } - } - - return {render, destroy} - } -} - -/** - * Create remote app component - for consumer to load application-level modules using hooks - */ -export function createRemoteAppComponent( - component: ComponentProvider, - reactOptions: ReactOptions, - options: {onError?: (error: Error) => void} = {}, -): any { - if (!component) { - throw new Error('createRemoteAppComponent: component parameter cannot be empty') - } - - const {React} = reactOptions - - const reactVersion = getReactVersion(React) - const isReact18OrAbove = reactVersion >= 18 - - // 使用函数组件和Hooks替代类组件 - return function ReactRemoteAppHookComponent(props: any) { - const containerRef = React.useRef(null) - const providerRef = React.useRef(null) - const providerInfoRef = React.useRef(null) - const [isLoaded, setIsLoaded] = React.useState(false) - const [error, setError] = React.useState(null) - - // React 18+ 使用 useSyncExternalStore 来处理外部状态更新 - const forceUpdate = - isReact18OrAbove && React.useSyncExternalStore - ? React.useSyncExternalStore( - (callback: any) => { - const id = Math.random().toString(36) - const listeners = providerRef.current?.['_listeners'] || new Map() - listeners.set(id, callback) - if (providerRef.current) providerRef.current['_listeners'] = listeners - return () => { - if (providerRef.current?.['_listeners']) { - providerRef.current['_listeners'].delete(id) - } - } - }, - () => props, - () => props, - ) - : props - - // 加载组件 - const loadComponent = React.useCallback(async () => { - try { - if (typeof component === 'function') { - const result = component() - - if (result instanceof Promise) { - const module = await result - providerInfoRef.current = module.default - } else { - providerInfoRef.current = component as BridgeProvider - } - - setIsLoaded(true) - } - } catch (err) { - const error = err as Error - setError(error) - handleError(error, 'Failed to load component', options.onError) - } - }, []) - - // 渲染组件 - const renderComponent = React.useCallback(() => { - if (!providerInfoRef.current || !containerRef.current) return - - try { - if (!providerRef.current) { - providerRef.current = providerInfoRef.current() - } - providerRef.current.render(containerRef.current, props) - } catch (error) { - handleError(error as Error, 'Failed to render component') - } - }, [props]) - - // 卸载组件 - const unmountComponent = React.useCallback(() => { - try { - if (providerRef.current) { - if (containerRef.current) { - try { - providerRef.current.destroy(containerRef.current) - } catch (destroyError) { - handleError(destroyError as Error, 'Error during provider unmount') - } - } - providerRef.current = null - } - } catch (error) { - console.error('[EMP-ERROR] Failed to unmount component', error) - } - }, []) - - // 组件加载 - React.useEffect(() => { - loadComponent() - }, [loadComponent]) - - // 组件渲染 - React.useEffect(() => { - if (isLoaded && containerRef.current) { - renderComponent() - } - }, [isLoaded, renderComponent]) - - // 组件更新 - 根据 React 版本使用不同的更新策略 - React.useEffect(() => { - if (isLoaded && providerRef.current && containerRef.current) { - // React 18+ 使用 forceUpdate 触发更新 - if (isReact18OrAbove) { - // forceUpdate 已经在 useSyncExternalStore 中处理 - providerRef.current.render(containerRef.current, props) - } else { - // React 16/17 直接更新 - providerRef.current.render(containerRef.current, props) - } - } - }, [props, isLoaded, forceUpdate]) - - // 组件卸载 - React.useEffect(() => { - return () => { - if (reactOptions?.syncUnmount) { - // 同步卸载 - unmountComponent() - } else { - // 使用微任务队列,比setTimeout更快但仍然异步 - Promise.resolve().then(() => { - if (containerRef.current) { - unmountComponent() - } - }) - } - } - }, [unmountComponent]) - - // 处理错误边界 - if (error && reactOptions.errorBoundary) { - return React.createElement( - 'div', - { - className: 'emp-error-boundary', - style: { - color: 'red', - padding: '10px', - border: '1px solid red', - borderRadius: '4px', - margin: '10px 0', - }, - }, - `Error loading component: ${error.message}`, - ) - } - - return React.createElement('div', { - ref: (node: HTMLElement | null) => { - containerRef.current = node - }, - }) - } -} - -export default { - createBridgeComponent, - createRemoteAppComponent, -} diff --git a/packages/bridge-react/src/index.ts b/packages/bridge-react/src/index.ts index 6c0d1f17..1d3a39e6 100644 --- a/packages/bridge-react/src/index.ts +++ b/packages/bridge-react/src/index.ts @@ -1,9 +1,8 @@ -// 导入类型和实现 +// 直接导入 classComponent 中的实现 import { - createBridgeComponent as createClassBridgeComponent, - createRemoteAppComponent as createClassRemoteAppComponent, + createBridgeComponent, + createRemoteAppComponent, } from './classComponent' -import {createRemoteAppComponent as createHookRemoteAppComponent} from './hookComponent' import { AsyncBridgeProvider, BridgeProvider, @@ -12,7 +11,6 @@ import { ReactOptions as ReactComponentOptions, RemoteComponentOptions, } from './types' -import {getReactVersion, handleError} from './utils' // 重新导出类型 export { @@ -24,41 +22,10 @@ export { RemoteComponentOptions, } -/** - * 创建桥接组件 - 用于生产者包装应用级导出模块 - */ -export function createBridgeComponent(Component: any, options: ReactComponentOptions) { - try { - return createClassBridgeComponent(Component, options) - } catch (error) { - handleError(error as Error, 'Failed to create bridge component') - throw error - } -} - -/** - * 创建远程应用组件 - 用于消费者加载应用级模块 - */ -export function createRemoteAppComponent( - component: ComponentProvider, - reactOptions: ReactComponentOptions, - options: RemoteComponentOptions = {}, -) { - try { - if (!component) { - throw new Error('createRemoteAppComponent: component parameter cannot be empty') - } - - const reactVersion = getReactVersion(reactOptions.React) - - // React 17+ 使用 Hook 实现,否则使用类组件实现 - return reactVersion >= 17 - ? createHookRemoteAppComponent(component, reactOptions, options) - : createClassRemoteAppComponent(component, reactOptions, options) - } catch (error) { - handleError(error as Error, 'Failed to create remote app component', options?.onError) - throw error - } +// 直接导出 classComponent 中的实现 +export { + createBridgeComponent, + createRemoteAppComponent, } export default { diff --git a/packages/bridge-vue2/src/index.ts b/packages/bridge-vue2/src/index.ts index d251bcdd..da9f70a7 100644 --- a/packages/bridge-vue2/src/index.ts +++ b/packages/bridge-vue2/src/index.ts @@ -49,10 +49,29 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } } } else { + // 创建一个额外的容器元素,作为Vue实例的挂载点 + const vueContainer = document.createElement('div') + vueContainer.className = 'vue2-container' + dom.appendChild(vueContainer) + const instance = new Vue({ propsData: props || {}, render: (h: any) => h(Component, {props: props || {}}), - el: dom, + el: vueContainer, // 使用新创建的容器元素 + beforeDestroy() { + // 在销毁前清空容器内容,而不是直接操作React管理的DOM + if (vueContainer && vueContainer.parentNode) { + while (vueContainer.firstChild) { + vueContainer.removeChild(vueContainer.firstChild) + } + // 尝试从父元素中移除Vue容器,但保留React的容器 + try { + dom.removeChild(vueContainer) + } catch (e) { + console.warn('[EMP-WARN] Failed to remove Vue container:', e) + } + } + }, }) // 使用自定义插件(如果提供) @@ -75,25 +94,47 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } const instance = instanceMap.get(dom) - console.log('[bridge-vue2] destroy', dom, instance) + if (!instance) return try { - // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 - // if (dom && dom.parentNode) { - // while (dom.firstChild) { - // dom.removeChild(dom.firstChild) - // } - // } - - // 先解除引用,再销毁 Vue 实例 + // 立即从映射中移除实例,防止重复销毁 const vmToDestroy = instance instanceMap.delete(dom) - // 确保在下一个事件循环中销毁,避免与React卸载冲突 - setTimeout(() => { + // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 + try { + // 使用更安全的方式清空 DOM - 参考Vue3的实现 + if (dom) { + // 清空DOM内容 - 使用多种方法确保清理成功 + try { + // 方法1: 使用replaceChildren (现代浏览器) + if (typeof dom.replaceChildren === 'function') { + dom.replaceChildren() + } + } catch (replaceError) { + console.warn('[EMP-WARN] destroy - replaceChildren failed:', replaceError) + } + + // 方法2: 循环移除子节点 (最兼容) + try { + while (dom.firstChild) { + dom.removeChild(dom.firstChild) + } + } catch (removeError) { + console.warn('[EMP-WARN] destroy - removeChild failed:', removeError) + } + } + } catch (domError) { + console.warn('[EMP-WARN] Error clearing DOM before destroy:', domError) + } + + // 立即销毁Vue实例,不再延迟 + try { vmToDestroy.$destroy() - }, 0) + } catch (destroyError) { + console.error('[EMP-ERROR] Error during Vue instance destroy:', destroyError) + } } catch (error) { console.error('[EMP-ERROR] Failed to unmount Vue component', error) } @@ -182,8 +223,12 @@ export function createRemoteAppComponent( if (this.provider && this.$el) { try { // 先清空 DOM 内容,避免 React 移除时的冲突 - while (this.$el.firstChild) { - this.$el.removeChild(this.$el.firstChild) + try { + while (this.$el.firstChild) { + this.$el.removeChild(this.$el.firstChild) + } + } catch (clearError) { + console.error('[EMP-ERROR] unmountComponent - Error during DOM clearing:', clearError) } // 然后销毁 Vue 组件 diff --git a/packages/bridge-vue2/tsconfig.json b/packages/bridge-vue2/tsconfig.json index 08d37d75..020bfe7c 100644 --- a/packages/bridge-vue2/tsconfig.json +++ b/packages/bridge-vue2/tsconfig.json @@ -7,6 +7,7 @@ "strict": true, "declaration": true, "noImplicitOverride": true, + "noCheck": true, "noUnusedLocals": true, "esModuleInterop": true, "useUnknownInCatchVariables": false, diff --git a/projects/adapter-app/emp.config.ts b/projects/adapter-app/emp.config.ts index fdedeaa3..37f9a834 100644 --- a/projects/adapter-app/emp.config.ts +++ b/projects/adapter-app/emp.config.ts @@ -15,6 +15,7 @@ export default defineConfig(store => { ah: `adapterHost@http://${ip}:7701/emp.json`, v3h: `vue3Host@http://${ip}:9901/emp.json`, v2h: `vue2Host@http://${ip}:9902/emp.json`, + vue2Host: `vue2Host@http://${ip}:9902/emp.json`, //mfHost: `mfHost@http://${store.server.ip}:6001/emp.json`, }, // dts: { diff --git a/projects/adapter-app/src/App.tsx b/projects/adapter-app/src/App.tsx index 551a7f53..5e4d73af 100644 --- a/projects/adapter-app/src/App.tsx +++ b/projects/adapter-app/src/App.tsx @@ -8,7 +8,7 @@ const App = () => (

Vue 2 Remote App

- +
diff --git a/projects/adapter-app/src/adapter/Vue2.ts b/projects/adapter-app/src/adapter/Vue2.tsx similarity index 78% rename from projects/adapter-app/src/adapter/Vue2.ts rename to projects/adapter-app/src/adapter/Vue2.tsx index 2ea4e334..e2892769 100644 --- a/projects/adapter-app/src/adapter/Vue2.ts +++ b/projects/adapter-app/src/adapter/Vue2.tsx @@ -4,14 +4,14 @@ import {createBridgeComponent} from '@empjs/bridge-vue2' import React from 'react' import v2App from 'v2h/HelloVue' +console.log('v2App', v2App) + // import plugin from 'v2h/plugin' // import store from 'v2h/store' const {EMP_ADAPTER_VUE_v2} = window as any const {Vue} = EMP_ADAPTER_VUE_v2 -// 创建Vue2桥接组件,添加额外配置以增强稳定性 +//创建Vue2桥接组件,添加额外配置以增强稳定性 const BridgeComponent = createBridgeComponent(v2App, {Vue}) - -// 创建React远程组件,添加额外配置以增强稳定性 export const RemoteVue2App = createRemoteAppComponent(BridgeComponent, {React}) diff --git a/projects/adapter-app/src/global.d.ts b/projects/adapter-app/src/global.d.ts index af423a53..2c18c543 100644 --- a/projects/adapter-app/src/global.d.ts +++ b/projects/adapter-app/src/global.d.ts @@ -1,2 +1,3 @@ declare module 'v3h/*' declare module 'v2h/*' +declare module 'vue2Host/*' diff --git a/projects/adapter-vue2-host/emp.config.ts b/projects/adapter-vue2-host/emp.config.ts index 15a56a87..2dada12c 100644 --- a/projects/adapter-vue2-host/emp.config.ts +++ b/projects/adapter-vue2-host/emp.config.ts @@ -56,8 +56,9 @@ export default defineConfig(store => { server: { port: 9902, open: false, + // hot: false, }, - appEntry: 'main.js', + // appEntry: 'index.js', debug: {}, } }) diff --git a/projects/adapter-vue2-host/src/main.js b/projects/adapter-vue2-host/src/index.js similarity index 100% rename from projects/adapter-vue2-host/src/main.js rename to projects/adapter-vue2-host/src/index.js From e113d30e8570eecd32487282289f3b3e5b14437b Mon Sep 17 00:00:00 2001 From: xuhongbin Date: Thu, 25 Sep 2025 13:04:40 +0800 Subject: [PATCH 5/5] =?UTF-8?q?feat(bridge-vue2):=20=E6=B7=BB=E5=8A=A0Vue2?= =?UTF-8?q?=E7=BB=84=E4=BB=B6=E7=83=AD=E6=9B=B4=E6=96=B0=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E6=96=B0=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加Vue2组件热更新功能,包括HMR模块加载和共享配置 更新README文档以反映Vue2桥接功能 优化Vue2组件桥接实现,移除冗余代码 --- packages/bridge-vue2/README.md | 143 ++++++++++-------- packages/bridge-vue2/package.json | 2 +- packages/bridge-vue2/src/index.ts | 35 +---- projects/adapter-app/emp.config.ts | 3 + projects/adapter-app/src/adapter/Vue2.tsx | 1 + projects/adapter-app/src/adapter/vue-2-hmr.ts | 1 + projects/adapter-app/src/bootstrap.tsx | 6 + projects/adapter-app/tsconfig.json | 5 +- projects/adapter-vue2-host/emp.config.ts | 9 ++ 9 files changed, 105 insertions(+), 100 deletions(-) create mode 100644 projects/adapter-app/src/adapter/vue-2-hmr.ts diff --git a/packages/bridge-vue2/README.md b/packages/bridge-vue2/README.md index 9cf227f3..9c4b9b42 100644 --- a/packages/bridge-vue2/README.md +++ b/packages/bridge-vue2/README.md @@ -1,118 +1,132 @@ -# EMP Bridge React +# EMP Bridge Vue2 -EMP Bridge React 是一个用于跨 React 版本组件通信的桥接工具,它解决了不同 React 版本之间组件共享和通信的问题。 +EMP Bridge Vue2 是一个用于在 React 应用中集成 Vue2 组件的桥接工具,它解决了 React 与 Vue2 之间组件共享和通信的问题。 ## 功能特点 -- 支持不同 React 版本(16/17/18/19)之间的组件共享 +- 支持在 React 应用中使用 Vue2 组件 - 提供简单的 API 用于生产者和消费者之间的通信 -- 自动处理 React 不同版本的渲染和卸载方法差异 -- 支持异步加载组件 +- 自动处理 React 与 Vue2 之间的渲染和卸载方法差异 +- 支持插件系统扩展 Vue2 功能 ## 安装 ```bash # 使用 npm -npm install @empjs/bridge-react +npm install @empjs/bridge-vue2 # 使用 yarn -yarn add @empjs/bridge-react +yarn add @empjs/bridge-vue2 # 使用 pnpm -pnpm add @empjs/bridge-react +pnpm add @empjs/bridge-vue2 ``` ## 基本用法 -### 生产者(导出组件的应用) +### 生产者(Vue2 应用导出组件) -```tsx -// 在 React 16/17 应用中 -import React from 'react'; -import { createBridgeComponent } from '@empjs/bridge-react'; +```js +// 在 Vue2 应用中 +import Vue from 'vue'; -// 创建要共享的组件 -const MyComponent = (props) => { - return
Hello from React 16/17! {props.message}
; +// 创建要共享的 Vue2 组件 +const HelloVue = { + name: 'HelloVue', + props: { + name: { + type: String, + default: 'Vue2' + } + }, + template: '
Hello from {{ name }}!
' }; -// 导出桥接组件 -export default createBridgeComponent(MyComponent, { - React, - ReactDOM: require('react-dom'), - // React 18+ 才有 createRoot - // createRoot: require('react-dom/client').createRoot -}); +// 导出组件 +export default HelloVue; ``` -### 消费者(使用组件的应用) +### 消费者(React 应用使用 Vue2 组件) -```tsx -// 在 React 18/19 应用中 +```jsx +// 在 React 应用中 import React from 'react'; import { createRemoteAppComponent } from '@empjs/bridge-react'; +import { createBridgeComponent } from '@empjs/bridge-vue2'; -// 导入远程组件(可以是动态导入) -import RemoteComponent from 'remote-app/MyComponent'; +// 导入远程 Vue2 组件 +import v2App from 'v2h/HelloVue'; -// 创建可在当前 React 版本中使用的组件 -const BridgedComponent = createRemoteAppComponent( - RemoteComponent, - { - React, - ReactDOM: require('react-dom'), - createRoot: require('react-dom/client').createRoot - }, - { - onError: (error) => console.error('Failed to load component:', error) - } -); +// 获取全局 Vue 实例(通过适配器注入) +const { EMP_ADAPTER_VUE_v2 } = window; +const { Vue } = EMP_ADAPTER_VUE_v2; + +// 创建 Vue2 桥接组件 +const BridgeComponent = createBridgeComponent(v2App, { Vue }); -// 在应用中使用 +// 创建可在 React 中使用的组件 +export const RemoteVue2App = createRemoteAppComponent(BridgeComponent, { React }); + +// 在 React 应用中使用 function App() { return (
-

My App (React 18/19)

- +

My React App

+
); } ``` +## 热更新支持 + +在开发环境中,您必须在 `bootstrap.ts` 添加热更新支持: + +```js +// 只在热更新时加载 vue-2-hmr 模块 +if (module.hot) { + console.log('vue-2-hmr', module); + import('src/adapter/vue-2-hmr'); +} +``` + +vue-2-hmr: 预载组件 +```js +import 'v2h/HelloVue' +``` + ## API 参考 ### createBridgeComponent -用于生产者包装应用级别导出模块。 +用于生产者包装 Vue2 组件。 ```typescript function createBridgeComponent( - Component: React.ComponentType, + Component: any, options: { - React: any; - ReactDOM: any; - createRoot?: Function; + Vue?: any; + plugin?: (vue: any) => void; } ): BridgeProvider ``` 参数: -- `Component`: 要导出的 React 组件 -- `options`: React 相关配置 - - `React`: React 实例 - - `ReactDOM`: ReactDOM 实例 - - `createRoot`: (可选) React 18+ 的 createRoot 方法 +- `Component`: 要导出的 Vue2 组件 +- `options`: Vue2 相关配置 + - `Vue`: Vue2 实例 + - `plugin`: (可选) 用于扩展 Vue 功能的插件函数 -### createRemoteAppComponent +### createRemoteAppComponent (来自 @empjs/bridge-react) -用于消费者加载应用级别模块。 +用于消费者加载远程组件。 ```typescript function createRemoteAppComponent( component: ComponentProvider, reactOptions: { React: any; - ReactDOM: any; + ReactDOM?: any; createRoot?: Function; }, options?: { @@ -122,22 +136,23 @@ function createRemoteAppComponent( ``` 参数: -- `component`: 组件提供者函数,可以是同步或异步的 +- `component`: 组件提供者函数,通常是 `createBridgeComponent` 的返回值 - `reactOptions`: 当前应用的 React 相关配置 - `React`: React 实例 - - `ReactDOM`: ReactDOM 实例 + - `ReactDOM`: (可选) ReactDOM 实例 - `createRoot`: (可选) React 18+ 的 createRoot 方法 - `options`: (可选) 额外配置 - `onError`: 错误处理回调函数 ## 使用场景 -1. 微前端架构中不同 React 版本的应用集成 -2. 逐步升级大型 React 应用时的版本兼容 -3. 共享组件库到不同 React 版本的项目中 +1. 微前端架构中 React 与 Vue2 应用的集成 +2. 在 React 项目中复用现有的 Vue2 组件 +3. 逐步从 Vue2 迁移到 React 的过渡阶段 ## 注意事项 -- 确保正确提供对应版本的 React 和 ReactDOM 实例 -- 对于 React 18+,需要提供 createRoot 方法 -- 组件间通信仅限于 props 传递,不支持 Context API 跨版本共享 \ No newline at end of file +- 确保正确提供 Vue2 实例 +- 组件间通信仅限于 props 传递,不支持 Vue 的 provide/inject 或 React 的 Context API 跨框架共享 +- 在使用前需要确保 Vue2 适配器已正确加载 +- 复杂的状态管理需要在各自框架内部处理 \ No newline at end of file diff --git a/packages/bridge-vue2/package.json b/packages/bridge-vue2/package.json index bb77993f..9bca74de 100644 --- a/packages/bridge-vue2/package.json +++ b/packages/bridge-vue2/package.json @@ -1,6 +1,6 @@ { "name": "@empjs/bridge-vue2", - "version": "0.2.0", + "version": "0.2.1", "description": "Emp Bridge Vue v2", "license": "MIT", "type": "module", diff --git a/packages/bridge-vue2/src/index.ts b/packages/bridge-vue2/src/index.ts index da9f70a7..91ebb194 100644 --- a/packages/bridge-vue2/src/index.ts +++ b/packages/bridge-vue2/src/index.ts @@ -1,4 +1,3 @@ -// Type definitions export interface BridgeProviderReturn { render: (dom: HTMLElement, props?: Record) => void destroy: (dom: HTMLElement) => void @@ -13,9 +12,6 @@ interface Vue2Options { plugin?: (vue: any) => void } -/** - * Create bridge component - for producer to wrap application-level export modules - */ export function createBridgeComponent(Component: any, options: Vue2Options): BridgeProvider { const Vue = options.Vue @@ -23,7 +19,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri const instanceMap = new Map() const render = (dom: HTMLElement, props?: Record): void => { - // 防御性检查:确保 DOM 元素存在 if (!dom || !(dom instanceof HTMLElement)) { console.error('[EMP-ERROR] Invalid DOM element provided to render') return @@ -33,23 +28,16 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri const existingInstance = instanceMap.get(dom) if (existingInstance) { - // Update props for existing instance if (props) { try { - // 更新组件的渲染函数,使用新的props重新渲染组件 existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) - - // 更新propsData existingInstance.$options.propsData = props || {} - - // 强制更新组件 existingInstance.$forceUpdate() } catch (error) { console.warn('[EMP-WARN] Failed to update props:', error) } } } else { - // 创建一个额外的容器元素,作为Vue实例的挂载点 const vueContainer = document.createElement('div') vueContainer.className = 'vue2-container' dom.appendChild(vueContainer) @@ -57,14 +45,12 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri const instance = new Vue({ propsData: props || {}, render: (h: any) => h(Component, {props: props || {}}), - el: vueContainer, // 使用新创建的容器元素 + el: vueContainer, beforeDestroy() { - // 在销毁前清空容器内容,而不是直接操作React管理的DOM if (vueContainer && vueContainer.parentNode) { while (vueContainer.firstChild) { vueContainer.removeChild(vueContainer.firstChild) } - // 尝试从父元素中移除Vue容器,但保留React的容器 try { dom.removeChild(vueContainer) } catch (e) { @@ -74,7 +60,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri }, }) - // 使用自定义插件(如果提供) if (options.plugin) { options.plugin(Vue) } @@ -87,7 +72,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } const destroy = (dom: HTMLElement): void => { - // 防御性检查:确保 DOM 元素存在 if (!dom || !(dom instanceof HTMLElement)) { console.error('[EMP-ERROR] Invalid DOM element provided to destroy') return @@ -98,17 +82,12 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri if (!instance) return try { - // 立即从映射中移除实例,防止重复销毁 const vmToDestroy = instance instanceMap.delete(dom) - // 在销毁前安全清空 DOM 内容,避免 React 移除时的冲突 try { - // 使用更安全的方式清空 DOM - 参考Vue3的实现 if (dom) { - // 清空DOM内容 - 使用多种方法确保清理成功 try { - // 方法1: 使用replaceChildren (现代浏览器) if (typeof dom.replaceChildren === 'function') { dom.replaceChildren() } @@ -116,7 +95,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri console.warn('[EMP-WARN] destroy - replaceChildren failed:', replaceError) } - // 方法2: 循环移除子节点 (最兼容) try { while (dom.firstChild) { dom.removeChild(dom.firstChild) @@ -129,7 +107,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri console.warn('[EMP-WARN] Error clearing DOM before destroy:', domError) } - // 立即销毁Vue实例,不再延迟 try { vmToDestroy.$destroy() } catch (destroyError) { @@ -144,9 +121,6 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } } -/** - * Create remote app component - for consumer to load application-level modules - */ export function createRemoteAppComponent( component: ComponentProvider, vueOptions: Vue2Options, @@ -158,7 +132,6 @@ export function createRemoteAppComponent( return { name: 'Vue2RemoteAppComponent', - // 允许接收任意 props props: { name: String, [Symbol.toPrimitive]: Function, @@ -205,10 +178,8 @@ export function createRemoteAppComponent( return } - // 确保传递正确的 props,Vue2 中 $props 可能不存在 const props = this.$props || this.$options.propsData || {} - // 确保 props 是对象类型 if (props && typeof props === 'object') { this.provider.render(this.$el, props) } else { @@ -222,7 +193,6 @@ export function createRemoteAppComponent( unmountComponent() { if (this.provider && this.$el) { try { - // 先清空 DOM 内容,避免 React 移除时的冲突 try { while (this.$el.firstChild) { this.$el.removeChild(this.$el.firstChild) @@ -231,7 +201,6 @@ export function createRemoteAppComponent( console.error('[EMP-ERROR] unmountComponent - Error during DOM clearing:', clearError) } - // 然后销毁 Vue 组件 this.provider.destroy(this.$el) this.provider = null } catch (error) { @@ -246,7 +215,6 @@ export function createRemoteAppComponent( }, updated() { if (this.provider && this.$el) { - // 获取 props 的安全方式 const props = this.$props || this.$options.propsData || {} this.provider.render(this.$el, props) } @@ -256,7 +224,6 @@ export function createRemoteAppComponent( this.unmountComponent() }, created() { - // 立即加载组件 this.loadComponent() }, render(h: any) { diff --git a/projects/adapter-app/emp.config.ts b/projects/adapter-app/emp.config.ts index 37f9a834..4eda636a 100644 --- a/projects/adapter-app/emp.config.ts +++ b/projects/adapter-app/emp.config.ts @@ -11,6 +11,9 @@ export default defineConfig(store => { pluginRspackEmpShare({ name: 'adapterApp', exposes: {}, + experiments: { + asyncStartup: true, + }, remotes: { ah: `adapterHost@http://${ip}:7701/emp.json`, v3h: `vue3Host@http://${ip}:9901/emp.json`, diff --git a/projects/adapter-app/src/adapter/Vue2.tsx b/projects/adapter-app/src/adapter/Vue2.tsx index e2892769..84736364 100644 --- a/projects/adapter-app/src/adapter/Vue2.tsx +++ b/projects/adapter-app/src/adapter/Vue2.tsx @@ -1,5 +1,6 @@ import {createRemoteAppComponent} from '@empjs/bridge-react' import {createBridgeComponent} from '@empjs/bridge-vue2' + // React 16 组件 import React from 'react' import v2App from 'v2h/HelloVue' diff --git a/projects/adapter-app/src/adapter/vue-2-hmr.ts b/projects/adapter-app/src/adapter/vue-2-hmr.ts new file mode 100644 index 00000000..1045c752 --- /dev/null +++ b/projects/adapter-app/src/adapter/vue-2-hmr.ts @@ -0,0 +1 @@ +import 'v2h/HelloVue' diff --git a/projects/adapter-app/src/bootstrap.tsx b/projects/adapter-app/src/bootstrap.tsx index b69c8fc7..496bec37 100644 --- a/projects/adapter-app/src/bootstrap.tsx +++ b/projects/adapter-app/src/bootstrap.tsx @@ -3,3 +3,9 @@ import ReactDOM from 'react-dom' import App from './App' ReactDOM.render(React.createElement(App), document.getElementById('emp-root')) + +// 只在热更新时加载vue-2-hmr模块 +if ((module as any).hot) { + console.log('vue-2-hmr', module) + import('src/adapter/vue-2-hmr') +} diff --git a/projects/adapter-app/tsconfig.json b/projects/adapter-app/tsconfig.json index 5db31194..fa33ed1a 100644 --- a/projects/adapter-app/tsconfig.json +++ b/projects/adapter-app/tsconfig.json @@ -2,7 +2,10 @@ "extends": "@empjs/cli/tsconfig/react", "compilerOptions": { "baseUrl": "./", - "jsx": "react" + "jsx": "react", + "paths": { + "src/*": ["src/*"] + } }, "include": ["src"] } diff --git a/projects/adapter-vue2-host/emp.config.ts b/projects/adapter-vue2-host/emp.config.ts index 2dada12c..d84ad9d7 100644 --- a/projects/adapter-vue2-host/emp.config.ts +++ b/projects/adapter-vue2-host/emp.config.ts @@ -1,6 +1,7 @@ import {defineConfig} from '@empjs/cli' import vue from '@empjs/plugin-vue2' import {pluginRspackEmpShare} from '@empjs/share' +import pkg from './package.json' export default defineConfig(store => { return { plugins: [ @@ -17,6 +18,14 @@ export default defineConfig(store => { './store': './src/store', './plugin': './src/plugin', }, + shared: { + vue: { + singleton: true, // 单一实例 + requiredVersion: pkg.dependencies.vue, // 与 Vue 2 版本匹配 + }, + 'vue-router': {singleton: true, requiredVersion: pkg.dependencies['vue-router']}, // 如使用 + vuex: {singleton: true, requiredVersion: pkg.dependencies.vuex}, // 如使用 + }, empRuntime: { runtimeLib: `https://unpkg.com/@empjs/share@3.10.1/output/sdk.js`, framework: {