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/index.ts b/packages/bridge-react/src/index.ts index 868e1e57..1d3a39e6 100644 --- a/packages/bridge-react/src/index.ts +++ b/packages/bridge-react/src/index.ts @@ -1,192 +1,31 @@ -// 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 -} - -/** - * 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} - } +// 直接导入 classComponent 中的实现 +import { + createBridgeComponent, + createRemoteAppComponent, +} from './classComponent' +import { + AsyncBridgeProvider, + BridgeProvider, + BridgeProviderReturn, + ComponentProvider, + ReactOptions as ReactComponentOptions, + RemoteComponentOptions, +} from './types' + +// 重新导出类型 +export { + BridgeProviderReturn, + BridgeProvider, + AsyncBridgeProvider, + ComponentProvider, + ReactComponentOptions, + RemoteComponentOptions, } -/** - * 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 RemoteAppComponent 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) { - 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}) - } - } - - return RemoteAppComponent +// 直接导出 classComponent 中的实现 +export { + createBridgeComponent, + createRemoteAppComponent, } 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/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/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/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 6f581932..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 @@ -9,71 +8,62 @@ 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) + 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) { - Object.keys(props).forEach(key => { - existingInstance.$set(existingInstance.$props, key, props[key]) - }) + try { + existingInstance.$options.render = (h: any) => h(Component, {props: props || {}}) + existingInstance.$options.propsData = props || {} + existingInstance.$forceUpdate() + } catch (error) { + console.warn('[EMP-WARN] Failed to update props:', error) + } } } else { - // Create new Vue instance + const vueContainer = document.createElement('div') + vueContainer.className = 'vue2-container' + dom.appendChild(vueContainer) - // 使用自定义插件(如果提供) - 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 || {}), + propsData: props || {}, render: (h: any) => h(Component, {props: props || {}}), - el: dom, - }) as Vue2Instance - if (options.enableDebug) debug('render: Vue instance created successfully') + el: vueContainer, + beforeDestroy() { + if (vueContainer && vueContainer.parentNode) { + while (vueContainer.firstChild) { + vueContainer.removeChild(vueContainer.firstChild) + } + try { + dom.removeChild(vueContainer) + } catch (e) { + console.warn('[EMP-WARN] Failed to remove Vue container:', e) + } + } + }, + }) + if (options.plugin) { + options.plugin(Vue) + } 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) @@ -82,81 +72,48 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } 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) + 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 { - // 先销毁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操作冲突 + const vmToDestroy = instance + instanceMap.delete(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)') + if (dom) { + try { + if (typeof dom.replaceChildren === 'function') { + dom.replaceChildren() + } + } catch (replaceError) { + console.warn('[EMP-WARN] destroy - replaceChildren failed:', replaceError) + } + + try { + while (dom.firstChild) { + dom.removeChild(dom.firstChild) + } + } catch (removeError) { + console.warn('[EMP-WARN] destroy - removeChild failed:', removeError) + } } } catch (domError) { - // 捕获可能的DOM操作错误,避免影响React卸载流程 - console.warn('[bridge-vue2] Error during component destroy:', domError) - if (options.enableDebug) debug('destroy: error cleaning up DOM', domError) + console.warn('[EMP-WARN] Error clearing DOM before destroy:', domError) } - 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) - } - } - - 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) - 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) + vmToDestroy.$destroy() + } catch (destroyError) { + console.error('[EMP-ERROR] Error during Vue instance destroy:', destroyError) } - } else { - if (options.enableDebug) debug('unmountComponent: DOM cleanup skipped (disableAutoCleanup=true)') + } catch (error) { + console.error('[EMP-ERROR] Failed to unmount Vue component', error) } } @@ -164,46 +121,25 @@ export function createBridgeComponent(Component: any, options: Vue2Options): Bri } } -// 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 { +): any { if (!component) { throw new Error('createRemoteAppComponent: component parameter cannot be empty') } return { - name: 'RemoteAppComponent', - props: {}, + name: 'Vue2RemoteAppComponent', + props: { + name: String, + [Symbol.toPrimitive]: Function, + }, data() { return { - provider: null as BridgeProviderReturn | null, - providerInfo: null as BridgeProvider | null, + provider: null, + providerInfo: null, isMounted: false, } }, @@ -233,42 +169,42 @@ export function createRemoteAppComponent( if (!this.providerInfo || !this.$el) return try { - if (!this.provider) { + if (!this.provider && this.providerInfo) { this.provider = this.providerInfo() } - this.provider.render(this.$el, this.$props || {}) + + if (!this.provider) { + console.warn('[EMP-WARN] Provider not available yet') + return + } + + const props = this.$props || this.$options.propsData || {} + + 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() { - // 只检查provider是否存在,不再依赖DOM节点关系 - if (this.provider) { + if (this.provider && this.$el) { 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) + try { + while (this.$el.firstChild) { + this.$el.removeChild(this.$el.firstChild) } + } catch (clearError) { + console.error('[EMP-ERROR] unmountComponent - Error during DOM clearing:', clearError) } - // 无论如何都清理provider引用 + + this.provider.destroy(this.$el) 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) } } }, @@ -279,34 +215,20 @@ export function createRemoteAppComponent( }, updated() { if (this.provider && this.$el) { - this.provider.render(this.$el, this.$props || {}) + const props = this.$props || this.$options.propsData || {} + this.provider.render(this.$el, 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.') - }, } } 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/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/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/emp.config.ts b/projects/adapter-app/emp.config.ts index fdedeaa3..4eda636a 100644 --- a/projects/adapter-app/emp.config.ts +++ b/projects/adapter-app/emp.config.ts @@ -11,10 +11,14 @@ 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`, 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/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 ecc4064a..5e4d73af 100644 --- a/projects/adapter-app/src/App.tsx +++ b/projects/adapter-app/src/App.tsx @@ -2,17 +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

@@ -21,11 +25,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 62% rename from projects/adapter-app/src/adapter/Vue2.ts rename to projects/adapter-app/src/adapter/Vue2.tsx index be7c9c86..84736364 100644 --- a/projects/adapter-app/src/adapter/Vue2.ts +++ b/projects/adapter-app/src/adapter/Vue2.tsx @@ -1,8 +1,11 @@ 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' + +console.log('v2App', v2App) // import plugin from 'v2h/plugin' // import store from 'v2h/store' @@ -10,8 +13,6 @@ import v2App from 'v2h/Hello' const {EMP_ADAPTER_VUE_v2} = window as any const {Vue} = EMP_ADAPTER_VUE_v2 -// 创建Vue2桥接组件,添加额外配置以增强稳定性 -const BridgeComponent = createBridgeComponent(v2App, {Vue, enableDebug: true}) - -// 创建React远程组件,添加额外配置以增强稳定性 +//创建Vue2桥接组件,添加额外配置以增强稳定性 +const BridgeComponent = createBridgeComponent(v2App, {Vue}) export const RemoteVue2App = createRemoteAppComponent(BridgeComponent, {React}) 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/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-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
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-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 f8c8dc39..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: [ @@ -10,13 +11,21 @@ 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', './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: { @@ -56,8 +65,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/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/index.js similarity index 51% rename from projects/adapter-vue2-host/src/main.js rename to projects/adapter-vue2-host/src/index.js index a747eeac..f94eccc4 100644 --- a/projects/adapter-vue2-host/src/main.js +++ b/projects/adapter-vue2-host/src/index.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, }, }