Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(vue3-bridge): resolved remote component prop handling and root container attribute passing #3562

Merged
merged 6 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .changeset/tough-pets-learn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
'@module-federation/bridge-vue3': patch
---

Fixed several issues:

1. Resolved inconsistencies in naming between `name` and `moduleName` to align with the type defined in `packages/bridge/bridge-shared/src/type.ts`. This also fixed an issue where `name` was being passed to the remote component, and if it was `<router-view>`, it caused rendering issues.

2. Issue: When passing props from a Vue 3 host application to a Vue 3 remote application created with `createRemoteComponent`, the props were being applied as attributes on the root container instead of being passed to the remote component.
Fix: Set `inheritAttrs: false` in `remoteApp.tsx` and explicitly pass all attributes to the remote component using `useAttrs()`.

3. Added a `rootAttrs` parameter to `createRemoteComponent` to allow passing attributes to the root container where the remote application is mounted. This enables setting classes, identifiers, and other attributes for the container element.
21 changes: 14 additions & 7 deletions apps/website-new/docs/en/practice/bridge/vue-bridge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import { PackageManagerTabs } from '@theme';
function createRemoteComponent<T, E extends keyof T>(
options: {
// Function to load remote application, e.g., loadRemote('remote1/export-app') or import('remote1/export-app')
loader: () => Promise<T>,
loader: () => Promise<T>;
// Default is 'default', used to specify module export
export?: E,
export?: E;
// Parameters that will be passed to defineAsyncComponent
asyncComponentOptions?: Omit<AsyncComponentOptions, 'loader'>;
// Attributes that will be bound to the root container where the remote Vue application will be mounted
rootAttrs?: Record<string, unknown>;
}
): (props: {
basename?: string;
Expand Down Expand Up @@ -115,14 +117,14 @@ export default defineConfig({
// ./src/router.ts
import * as bridge from '@module-federation/bridge-vue3';

const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app') });
const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app'), rootAttrs: {class: 'root-element-class'} });

const router = createRouter({
history: createWebHistory(),
routes: [
// Define your routes here
{ path: '/', component: Home },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2 },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2, props: { foo: 'bar' } },
// Other routes
],
});
Expand All @@ -137,11 +139,13 @@ export default router;
function createRemoteComponent<T, E extends keyof T>(
options: {
// Function to load remote application, e.g., loadRemote('remote1/export-app') or import('remote1/export-app')
loader: () => Promise<T>,
loader: () => Promise<T>;
// Default is 'default', used to specify module export
export?: E;
// Parameters that will be passed to defineAsyncComponent
asyncComponentOptions?: Omit<AsyncComponentOptions, 'loader'>;
// Attributes that will be bound to the root container where the remote Vue application will be mounted
rootAttrs?: Record<string, unknown>;
}
): (props: {
basename?: string;
Expand All @@ -164,6 +168,9 @@ const Remote1App = createRemoteComponent({ loader: () => loadRemote('remote1/exp
* `asyncComponentOptions`
* type: `Omit<AsyncComponentOptions, 'loader'>`
* Purpose: Parameters that will be passed to defineAsyncComponent, except for the loader parameter
* `rootAttrs`
* type: `Record<string, unknown>`
* Purpose: Attributes that will be bound to the root container where the remote Vue application will be mounted
```tsx
// remote
export const provider = createBridgeComponent({
Expand All @@ -184,14 +191,14 @@ const Remote1App = createRemoteComponent({
```tsx
import * as bridge from '@module-federation/bridge-vue3';

const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app') });
const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app'), rootAttrs: {class: 'root-element-class'} });

const router = createRouter({
history: createWebHistory(),
routes: [
// Define your routes here
{ path: '/', component: Home },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2 },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2, props: { foo: 'bar' } },
// Other routes
],
});
Expand Down
17 changes: 12 additions & 5 deletions apps/website-new/docs/zh/practice/bridge/vue-bridge.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ import { PackageManagerTabs } from '@theme';
function createRemoteComponent<T, E extends keyof T>(
options: {
// Function to load remote application, e.g., loadRemote('remote1/export-app') or import('remote1/export-app')
loader: () => Promise<T>,
loader: () => Promise<T>;
// Default is 'default', used to specify module export
export?: E,
export?: E;
// Parameters that will be passed to defineAsyncComponent
asyncComponentOptions?: Omit<AsyncComponentOptions, 'loader'>;
// Attributes that will be bound to the root container where the remote Vue application will be mounted
rootAttrs?: Record<string, unknown>;
}
): (props: {
basename?: string;
Expand Down Expand Up @@ -141,11 +143,13 @@ export default router;
function createRemoteComponent<T, E extends keyof T>(
options: {
// Function to load remote application, e.g., loadRemote('remote1/export-app') or import('remote1/export-app')
loader: () => Promise<T>,
loader: () => Promise<T>;
// Default is 'default', used to specify module export
export?: E;
// Parameters that will be passed to defineAsyncComponent
asyncComponentOptions?: Omit<AsyncComponentOptions, 'loader'>;
// Attributes that will be bound to the root container where the remote Vue application will be mounted
rootAttrs?: Record<string, unknown>;
}
): (props: {
basename?: string;
Expand All @@ -168,6 +172,9 @@ const Remote1App = createRemoteComponent({ loader: () => loadRemote('remote1/exp
* `asyncComponentOptions`
* type: `Omit<AsyncComponentOptions, 'loader'>`
* Purpose: Parameters that will be passed to defineAsyncComponent, except for the loader parameter
* `rootAttrs`
* type: `Record<string, unknown>`
* Purpose: Attributes that will be bound to the root container where the remote Vue application will be mounted
```tsx
// remote
export const provider = createBridgeComponent({
Expand All @@ -188,14 +195,14 @@ const Remote1App = createRemoteComponent({
```tsx
import * as bridge from '@module-federation/bridge-vue3';

const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app') });
const Remote2 = bridge.createRemoteComponent({ loader: () => loadRemote('remote1/export-app'), rootAttrs: {class: 'root-element-class'} });

const router = createRouter({
history: createWebHistory(),
routes: [
// Define your routes here
{ path: '/', component: Home },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2 },
{ path: '/remote1/:pathMatch(.*)*', component: Remote2, props: { foo: 'bar' } },
// Other routes
],
});
Expand Down
4 changes: 3 additions & 1 deletion packages/bridge/vue3-bridge/src/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export function createRemoteComponent(info: {
loader: () => Promise<any>;
export?: string;
asyncComponentOptions?: Omit<AsyncComponentOptions, 'loader'>;
rootAttrs?: Record<string, unknown>;
}) {
return defineAsyncComponent({
__APP_VERSION__,
Expand Down Expand Up @@ -39,7 +40,7 @@ export function createRemoteComponent(info: {

LoggerInstance.debug(
`createRemoteComponent LazyComponent loadRemote info >>>`,
{ name: moduleName, module, exportName, basename, route },
{ moduleName, module, exportName, basename, route },
);

if (exportName in module && typeof exportFn === 'function') {
Expand All @@ -49,6 +50,7 @@ export function createRemoteComponent(info: {
moduleName,
providerInfo: exportFn,
basename,
rootAttrs: info.rootAttrs,
});
},
};
Expand Down
12 changes: 6 additions & 6 deletions packages/bridge/vue3-bridge/src/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function createBridgeComponent(bridgeInfo: ProviderFnParams) {
LoggerInstance.debug(`createBridgeComponent render Info`, info);
const { moduleName, dom, basename, memoryRoute, ...propsInfo } = info;
const app = Vue.createApp(bridgeInfo.rootComponent, propsInfo);
rootMap.set(info.dom, app);
rootMap.set(dom, app);

const beforeBridgeRenderRes =
await instance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(info);
Expand Down Expand Up @@ -61,19 +61,19 @@ export function createBridgeComponent(bridgeInfo: ProviderFnParams) {
routes: bridgeOptions.router.getRoutes(),
});

LoggerInstance.log(`createBridgeComponent render router info>>>`, {
name: info.moduleName,
LoggerInstance.debug(`createBridgeComponent render router info>>>`, {
moduleName,
router,
});
// memory route Initializes the route
if (info.memoryRoute) {
await router.push(info.memoryRoute.entryPath);
if (memoryRoute) {
await router.push(memoryRoute.entryPath);
}

app.use(router);
}

app.mount(info.dom);
app.mount(dom);
instance?.bridgeHook?.lifecycle?.afterBridgeRender?.emit(info);
},
destroy(info: { dom: HTMLElement }) {
Expand Down
23 changes: 17 additions & 6 deletions packages/bridge/vue3-bridge/src/remoteApp.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { ref, onMounted, onBeforeUnmount, watch, defineComponent } from 'vue';
import {
ref,
onMounted,
onBeforeUnmount,
watch,
defineComponent,
useAttrs,
} from 'vue';
import { dispatchPopstateEnv } from '@module-federation/bridge-shared';
import { useRoute } from 'vue-router';
import { LoggerInstance } from './utils';
Expand All @@ -11,20 +18,24 @@ export default defineComponent({
basename: String,
memoryRoute: Object,
providerInfo: Function,
rootAttrs: Object,
},
inheritAttrs: false,
setup(props) {
const rootRef = ref(null);
const providerInfoRef = ref(null);
const pathname = ref('');
const route = useRoute();
const hostInstance = getInstance();
const componentAttrs = useAttrs();

const renderComponent = () => {
const renderComponent = async () => {
const providerReturn = props.providerInfo?.();
providerInfoRef.value = providerReturn;

let renderProps = {
name: props.moduleName,
...componentAttrs,
moduleName: props.moduleName,
dom: rootRef.value,
basename: props.basename,
memoryRoute: props.memoryRoute,
Expand All @@ -35,9 +46,9 @@ export default defineComponent({
);

const beforeBridgeRenderRes =
hostInstance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(
(await hostInstance?.bridgeHook?.lifecycle?.beforeBridgeRender?.emit(
renderProps,
) || {};
)) || {};

renderProps = { ...renderProps, ...beforeBridgeRenderRes.extraProps };
providerReturn.render(renderProps);
Expand Down Expand Up @@ -92,6 +103,6 @@ export default defineComponent({
});
});

return () => <div ref={rootRef}></div>;
return () => <div {...(props.rootAttrs || {})} ref={rootRef}></div>;
},
});