本文是《彻底搞懂 Module Federation》系列的第5篇(完结篇),介绍 Runtime API 的使用和项目实操经验。
📚 系列文章
- 📖 第1篇:概述与实战示例
- 📖 第2篇:原理分析 - Webpack 异步加载流程
- 📖 第3篇:原理分析 - MF 模块加载(上)
- 📖 第4篇:原理分析 - MF 模块加载(下)
- ✅ 第5篇(本文):原理分析 - Runtime API + 项目实操
3.3 Module Federation Runtime API 加载流程
3.3.1 概述
官方提供的Runtime API进行模块注册和加载,很大程度上打破了构建工具的限制,在一些老版本的项目上,也可以直接使用共享模块。从构建插件转向自定义的API提供加载,同时提供了更多的自定义配置和生命钩子函数。
深入理解API模型加载,可以发现实际上面模拟了前面构建时加载里面的动态创建script标签和通过全局的S变量进行共享库传递等核心功能。
3.3.2 初始化请求链路
-
Request initiator chain
-
跟前面module federation构建时加载流程有一点区别,remoteEntry.js文件的请求发起者是前置的基础库,也就是@module-federation/runtime-core。这是因为修改成Runtime API形式,使用官方库提供的loadRemote方法动态加载组件,而构建版本通过插件,在构建时已经将发起请求的逻辑注入到了前置的main.js文件中了。
这里主要关注host,也就是消费者(Layout)应用,在使用官方提供的API动态加载时的流程,所以在host应用中去掉webpack配置文件中的ModuleFederationPlugin 插件。
//const { ModuleFederationPlugin } = require('webpack').container;
...
plugins: [
new MiniCssExtractPlugin({
filename: '[name].css',
}),
// new ModuleFederationPlugin({
// name: 'layout',
// filename: 'remoteEntry.js',
// remotes: {
// home: 'home@http://localhost:3002/remoteEntry.js',
// },
// exposes: {},
// shared: {
// vue: {
// singleton: true,
// },
// },
// }),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './index.html'),
chunks: ['main'],
}),
new VueLoaderPlugin(),
],
在入口main.js里面修改引入远程的组件的方法(红色部分为改动)
用了init和loadRemote两个Runtime API
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
import { init, loadRemote } from '@module-federation/runtime';
init({
name: 'layout',
remotes: [
{
name: 'home',
entry: 'http://localhost:3002/remoteEntry.js',
},
],
shared: {
vue: {
singleton: true,
},
},
});
const Content = defineAsyncComponent(async () => await loadRemote('home/Content'));
const Button = defineAsyncComponent(async () => await loadRemote('home/Button'));
const app = createApp(Layout);
app.component('content-element', Content);
app.component('button-element', Button);
app.mount('#app');
3.3.3 源码与构建后代码对照
大部分代码跟构建时类似,只有在正式初始化和加载远程组件时,需要调用前置加载的module-federation/runtime-core基础库
3.3.3.1 依赖库
"use strict";
(self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || []).push([["vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2__swc_h-8958a1"],{
"../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js":
/*!****************************************************************************************************************************!*\
!*** ../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js ***!
****************************************************************************************************************************/
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ Module: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.Module),
/* harmony export */ ModuleFederation: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.ModuleFederation),
/* harmony export */ createInstance: () => (/* binding */ createInstance),
/* harmony export */ getInstance: () => (/* binding */ getInstance),
/* harmony export */ getRemoteEntry: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.getRemoteEntry),
/* harmony export */ getRemoteInfo: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.getRemoteInfo),
/* harmony export */ init: () => (/* binding */ init),
/* harmony export */ loadRemote: () => (/* binding */ loadRemote),
/* harmony export */ loadScript: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.loadScript),
/* harmony export */ loadScriptNode: () => (/* reexport safe */ _module_federation_runtime_core__WEBPACK_IMPORTED_MODULE_0__.loadScriptNode),
/* harmony export */ loadShare: () => (/* binding */ loadShare),
/* harmony export */ loadShareSync: () => (/* binding */ loadShareSync),
/* harmony export */ preloadRemote: () => (/* binding */ preloadRemote),
...
})
3.3.3.2 main.js文件
3.3.3.2.1 源码
import { createApp, defineAsyncComponent } from 'vue';
import Layout from './Layout.vue';
import { init, loadRemote } from '@module-federation/runtime';
init({
name: 'layout',
remotes: [
{
name: 'home',
entry: 'http://localhost:3002/remoteEntry.js',
},
],
shared: {
vue: {
singleton: true,
},
},
});
const Content = defineAsyncComponent(async () => await loadRemote('home/Content'));
const Button = defineAsyncComponent(async () => await loadRemote('home/Button'));
const app = createApp(Layout);
app.component('content-element', Content);
app.component('button-element', Button);
app.mount('#app');
3.3.3.2.2 构建后代码
"use strict";
(self["webpackChunkvue3_demo_layout"] = self["webpackChunkvue3_demo_layout"] || []).push([["src_main_js"], {
/***/
"./src/main.js": /*!*********************!*\
!*** ./src/main.js ***!
*********************/
/***/
( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */
/* harmony import */
var _Layout_vue__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./Layout.vue */
"./src/Layout.vue");
/* harmony import */
var _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @module-federation/runtime */
"../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js");
(0,
_module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.init)({
name: 'layout',
remotes: [{
name: 'home',
entry: 'http://localhost:3002/remoteEntry.js',
}, ],
shared: {
vue: {
singleton: true,
},
},
});
const Content = (0,
vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
_module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Content'));
const Button = (0,
vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
_module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Button'));
const app = (0,
vue__WEBPACK_IMPORTED_MODULE_0__.createApp)(_Layout_vue__WEBPACK_IMPORTED_MODULE_1__["default"]);
app.component('content-element', Content);
app.component('button-element', Button);
app.mount('#app');
/***/
}
)
}]);
3.3.4 过程分析
分析这里的init和loadRemote API就不用从入口文件加载开始,按照顺序分析,重点关注方法本身就行,也就是@module-federation/runtime里面的代码
还是看一下原始的main.js里面,init和loadRemote方法构建后源码;
"./src/main.js": /*!*********************!*\
/***/
( (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
/* harmony import */
/* harmony import */
var _module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @module-federation/runtime */
"../../node_modules/.pnpm/@module-federation+runtime@0.20.0/node_modules/@module-federation/runtime/dist/index.esm.js");
(0,
_module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.init)({
name: 'layout',
remotes: [{
name: 'home',
entry: 'http://localhost:3002/remoteEntry.js',
}, ],
shared: {
vue: {
singleton: true,
},
},
});
const Content = (0,
vue__WEBPACK_IMPORTED_MODULE_0__.defineAsyncComponent)(async () => await (0,
_module_federation_runtime__WEBPACK_IMPORTED_MODULE_2__.loadRemote)('home/Content'));
/***/
}
)
提前获取@module-federation/runtime module,这个module在前置的公共chunk里面,也就是demo里面对应的http://localhost:3001/vendors-node_modules_pnpm_mini-css-extract-plugin_2_9_2_webpack_5_96_1__swc_core_1_9_2__swc_h-8958a1.js 文件。
异步模块加载的流程这里就不细讲了,聚焦到@module-federation/runtime仓库。
3.3.4.1 init方法
3.3.4.1.1 传入参数
查看源码位置:core/packages/runtime/src/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federation/c
export function createInstance(options: UserOptions) {
// Retrieve debug constructor
const ModuleFederationConstructor =
getGlobalFederationConstructor() || ModuleFederation;
const instance = new ModuleFederationConstructor(options);
setGlobalFederationInstance(instance);
return instance;
}
let FederationInstance: ModuleFederation | null = null;
/**
* @deprecated Use createInstance or getInstance instead
*/
export function init(options: UserOptions): ModuleFederation {
// Retrieve the same instance with the same name
const instance = getGlobalFederationInstance(options.name, options.version);
if (!instance) {
FederationInstance = createInstance(options);
return FederationInstance;
} else {
// Merge options
instance.initOptions(options);
if (!FederationInstance) {
FederationInstance = instance;
}
return instance;
}
}
官方文档里面推荐使用createInstance方法取代init.
init方法只做 一次单实例的前置校验,通过getGlobalFederationInstance方法,判断相同name命名的实例是否已经注册过。没有注册则用createInstance 传入配置项初始化一个实例
3.3.4.1.2 runtime-core中执行构造函数
实例对应的构造函数ModuleFederation,在另一个依赖里面@module-federation/runtime-core 实现,
对应源码位置 github.com/module-fede…
constructor(userOptions: UserOptions) {
const plugins = USE_SNAPSHOT
? [snapshotPlugin(), generatePreloadAssetsPlugin()]
: [];
// TODO: Validate the details of the options
// Initialize options with default values
// 合并用户选项和默认选项
const defaultOptions: Options = {
id: getBuilderId(),
name: userOptions.name,
plugins,
remotes: [],
shared: {},
inBrowser: isBrowserEnv(),
};
this.name = userOptions.name;
this.options = defaultOptions;
// 2. 初始化各个处理器
this.snapshotHandler = new SnapshotHandler(this);
this.sharedHandler = new SharedHandler(this);
this.remoteHandler = new RemoteHandler(this);
this.shareScopeMap = this.sharedHandler.shareScopeMap;
// 3. 注册传入的插件
this.registerPlugins([
...defaultOptions.plugins,
...(userOptions.plugins || []),
]);
this.options = this.formatOptions(defaultOptions, userOptions);
}
前面参数传递到这里的constructor并执行。
创建完成后setGlobalFederationInstance(instance); 存储到全局变量,方便后面的loadRemote使用
core/packages/runtime-core/src/global.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federa
export function setGlobalFederationInstance(
FederationInstance: ModuleFederation,
): void {
CurrentGlobal.__FEDERATION__.__INSTANCES__.push(FederationInstance);
}
3.3.4.2 loadRemote方法
3.3.4.2.1 外层调用查看
函数初始化在外层,具体代码:core/packages/runtime/src/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federation/c
export function loadRemote<T>(
...args: Parameters<ModuleFederation['loadRemote']>
): Promise<T | null> {
assert(FederationInstance, getShortErrorMsg(RUNTIME_009, runtimeDescMap));
const loadRemote: typeof FederationInstance.loadRemote<T> =
FederationInstance.loadRemote;
// eslint-disable-next-line prefer-spread
return loadRemote.apply(FederationInstance, args);
}
继续转到runtime-core的实例构造函数里面查看真正的loadRemote方法。
core/packages/runtime-core/src/core.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-federati
async loadRemote<T>(
id: string,
options?: { loadFactory?: boolean; from: CallFrom },
): Promise<T | null> {
return this.remoteHandler.loadRemote(id, options);
}
this.remoteHandler 已经在前面实例化时,执行了初始化,再转到remote文件里面
async loadRemote<T>(
id: string,
options?: { loadFactory?: boolean; from: CallFrom },
): Promise<T | null> {
const { host } = this;
try {
const { loadFactory = true } = options || {
loadFactory: true,
};
// 1. 获取 Module 实例和相关配置信息
const { module, moduleOptions, remoteMatchInfo } =
await this.getRemoteModuleAndOptions({
id,
});
..
// 2. 执行module.get
const moduleOrFactory = (await module.get(
idRes,
expose,
options,
remoteSnapshot,
)) as T;
// 3. 调用对应的生命周期钩子
const moduleWrapper = await this.hooks.lifecycle.onLoad.emit({
id: idRes,
pkgNameOrAlias,
expose,
exposeModule: loadFactory ? moduleOrFactory : undefined,
exposeModuleFactory: loadFactory ? undefined : moduleOrFactory,
remote,
options: moduleOptions,
moduleInstance: module,
origin: host,
});
this.setIdToRemoteMap(id, remoteMatchInfo);
// 4. 返回远程组件的具体实现
return moduleOrFactory;
} catch (error) {
return failOver as T;
}
}
remoteHandle.loadRemote管理整个远程应用加载的执行顺序,并最终返回共享组件的具体内容。
3.3.4.2.2 New Module
通过getRemoteModuleAndOptions获取Module实例和相关配置信息,getRemoteModuleAndOptions里面除了初始化Module实例 new Module(moduleOptions);前面还有各种钩子函数的执行。
3.3.4.2.3 Module.get
核心逻辑都在Module.get函数里面,后面主要分析这个get方法
Module实例可以简单理解成管理和加载远程模块remoteEntry.js的构造函数,
具体看Module 构造函数后面的get方法
core/packages/runtime-core/src/module/index.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-
async get(
id: string,
expose: string,
options?: { loadFactory?: boolean },
remoteSnapshot?: ModuleInfo,
) {
const { loadFactory = true } = options || { loadFactory: true };
// 获取remoteEntry.js
const remoteEntryExports = await this.getEntry();
....
await remoteEntryExports.init(
initContainerOptions.shareScope,
initContainerOptions.initScope,
initContainerOptions.remoteEntryInitOptions,
);
await this.host.hooks.lifecycle.initContainer.emit({
...initContainerOptions,
id,
remoteSnapshot,
remoteEntryExports,
});
}
this.lib = remoteEntryExports;
this.inited = true;
let moduleFactory;
moduleFactory = await this.host.loaderHook.lifecycle.getModuleFactory.emit({
remoteEntryExports,
expose,
moduleInfo: this.remoteInfo,
});
// get exposeGetter
if (!moduleFactory) {
moduleFactory = await remoteEntryExports.get(expose);
}
assert(
moduleFactory,
`${getFMId(this.remoteInfo)} remote don't export ${expose}.`,
);
// keep symbol for module name always one format
const symbolName = processModuleAlias(this.remoteInfo.name, expose);
const wrapModuleFactory = this.wraperFactory(moduleFactory, symbolName);
if (!loadFactory) {
return wrapModuleFactory;
}
const exposeContent = await wrapModuleFactory();
return exposeContent;
}
module.get里面也包括了对应的生命周期钩子函数,主要关注几个核心的流程,注意这里的钩子函数主要方便用户进行自定义的远程模块加载,而我们主要讨论的是runtime-core提供的默认加载顺序,所以暂时忽略这些钩子的影响
- this.getEntry(): 异步加载remoteEntry.js,在浏览器环境下通过动态创建script标签的方式异步加载远程应用,并执行
- remoteEntryExports.init:加载并执行完成后,拿到remoteEntry.js对外暴露出来的init方法并执行,这一步就是进行共享依赖传递融合,在远程应用中提前传入宿主的共享依赖库,并检查远程应用是否有统一的共享库,为后面的共享组件加载做准备
- remoteEntryExports.get(expose): 执行远程应用对我暴露的get方法,在远程应用的执行上下文中拿到共享组件
3.3.4.2.3.1 通过getEntry,异步加载remoteEntry.js
async getEntry(): Promise<RemoteEntryExports> {
...
remoteEntryExports = await getRemoteEntry({
origin: this.host,
remoteInfo: this.remoteInfo,
remoteEntryExports: this.remoteEntryExports,
});
...
this.remoteEntryExports = remoteEntryExports as RemoteEntryExports;
return this.remoteEntryExports;
}
core/packages/runtime-core/src/utils/load.ts at cfae7c06bd0f19aea0757fb2bcb7088ac29457cb · module-fe
export async function getRemoteEntry(params: {
origin: ModuleFederation;
remoteInfo: RemoteInfo;
remoteEntryExports?: RemoteEntryExports | undefined;
getEntryUrl?: (url: string) => string;
_inErrorHandling?: boolean; // Add flag to prevent recursion
}): Promise<RemoteEntryExports | false | void> {
const uniqueKey = getRemoteEntryUniqueKey(remoteInfo);
if (!globalLoading[uniqueKey]) {
const loadEntryHook = origin.remoteHandler.hooks.lifecycle.loadEntry;
const loaderHook = origin.loaderHook;
globalLoading[uniqueKey] = loadEntryHook
.emit({
loaderHook,
remoteInfo,
remoteEntryExports,
})
.then((res) => {
if (res) {
return res;
}
// Use ENV_TARGET if defined, otherwise fallback to isBrowserEnv, must keep this
const isWebEnvironment =
typeof ENV_TARGET !== 'undefined'
? ENV_TARGET === 'web'
: isBrowserEnv();
return isWebEnvironment
? loadEntryDom({
remoteInfo,
remoteEntryExports,
loaderHook,
getEntryUrl,
})
: loadEntryNode({ remoteInfo, loaderHook });
})
.catch(async (err) => {
throw err;
});
}
return globalLoading[uniqueKey];
}
这里有个钩子loadEntryHook.emit,就是支持用户自定义加载remoteEntry.js函数,如果没有配置,则继续判断是否在浏览器环境,是则通过loadEntryDom进行动态创建script标签。
async function loadEntryDom({
remoteInfo,
remoteEntryExports,
loaderHook,
getEntryUrl,
}: {
remoteInfo: RemoteInfo;
remoteEntryExports?: RemoteEntryExports;
loaderHook: ModuleFederation['loaderHook'];
getEntryUrl?: (url: string) => string;
}) {
const { entry, entryGlobalName: globalName, name, type } = remoteInfo;
switch (type) {
case 'esm':
case 'module':
return loadEsmEntry({ entry, remoteEntryExports });
case 'system':
return loadSystemJsEntry({ entry, remoteEntryExports });
default:
return loadEntryScript({
entry,
globalName,
name,
loaderHook,
getEntryUrl,
});
}
}
loadEntryDom里面区别了不同的加载方法,默认走到loadEntryScript, 加载并执行remoteEntry.js
async function loadEntryScript({
name,
globalName,
entry,
loaderHook,
getEntryUrl,
}: {
name: string;
globalName: string;
entry: string;
loaderHook: ModuleFederation['loaderHook'];
getEntryUrl?: (url: string) => string;
}): Promise<RemoteEntryExports> {
// if getEntryUrl is passed, use the getEntryUrl to get the entry url
const url = getEntryUrl ? getEntryUrl(entry) : entry;
return loadScript(url, {
attrs: {},
createScriptHook: (url, attrs) => {
const res = loaderHook.lifecycle.createScript.emit({ url, attrs });
},
})
.then(() => {
return handleRemoteEntryLoaded(name, globalName, entry);
})
}
3.3.4.2.3.2 执行remoteEntryExports.init,进行共享依赖传递融合
拿到remoteEntryExports内容后,继续Module.get后面的逻辑,后面开始处理远程应用的共享依赖shareScope了,其中shareScrope是我们初始化配置,同时也支持钩子函数设置,最后将处理的shareScope传入remoteEntryExports.init,开始共享依赖库跨应用传递
await remoteEntryExports.init(
initContainerOptions.shareScope,
initContainerOptions.initScope,
initContainerOptions.remoteEntryInitOptions,
);
这里对应了前面构建时加载的[module.init], 有点类型
var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
回到remoteEntry.js文件,再看一次里面的init和get方法
var moduleMap = {
"./Content": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Content_vue-_94460")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Content */ "./src/components/Content.vue")))));
},
"./Button": () => {
return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_js")]).then(() => (() => ((__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.js")))));
}
};
var get = (module, getScope) => {
__webpack_require__.R = getScope;
getScope = (
__webpack_require__.o(moduleMap, module)
? moduleMap[module]()
: Promise.resolve().then(() => {
throw new Error('Module "' + module + '" does not exist in container.');
})
);
__webpack_require__.R = undefined;
return getScope;
};
var init = (shareScope, initScope) => {
if (!__webpack_require__.S) return;
var name = "default"
var oldScope = __webpack_require__.S[name];
if(oldScope && oldScope !== shareScope) throw new Error("Container initialization failed as it has already been initialized with a different share scope");
__webpack_require__.S[name] = shareScope;
return __webpack_require__.I(name, initScope);
};
这里执行的init函数
__webpack_require__.S是 Webpack 运行时内部用来存储所有共享作用域 (Share Scopes) 的地方,表示当前运行环境不支持共享功能,
var oldScope = __webpack_require__.S[name];表示远程应用上进行共享配置项目,并进行缓存处理,这里进行oldScope && oldScope !== shareScope判断,主要兼容remoteEntry.js被加载多次的场景,比如远程应用在当前页面被多个宿主(host)引入并初始化,host-a 和host-b在一个页面被加载,他们都依赖同一个remote,远程应用的运行时只能有统一的远程应用的share scope。
如何进行共享依赖呢?其实获取保存宿主传递过来的shareScope,__webpack_require__.S[name] = shareScope,这里通常是vue、react这些共享库。
这里共享依赖init函数就执行完成后,继续Module.get函数后面的流程
3.3.4.2.3.3 执行远程应用remoteEntryExports暴露出来的get方法,加载共享组件Content,并返回给宿主应用
moduleFactory = await remoteEntryExports.get(expose);
关键的一步就是执行remoteEntryExports.get('home/Content')
继续到moduleMap['./Content'](),找到content远程组件真实的地址,并通过__webpack_require__.e进行异步获取src_components_Content_vue-_94460
3.3.4.2.4 Return moduleFactory
到这里远程组件才真正获取完成。moduleFactory返回给宿主。
3.3.5 整体组件加载执行过程
4. 项目实操
4.1 如何动态引入远程应用的remoteEntry.js如何进行控制?
- 通过构建时注入变量进行控制,达到版本迭代的效果
- 直接固定一个加时间戳的cdn资源,进行实时更新
4.2 远程应用加载失败如何处理?
- 在runtime API里面,通过了自定义的钩子进行远程应用加载及加载失败的回调上报
- 远程组件尽量包裹在React/Vue 的Suspense组件里面。
4.3 js/css作用域控制?
- 人为约定控制,BEM、css scope、css in js、Shadow DOM
4.4 其他一些类似的模块组件级加载方案
const app = await Garfish.loadApp('vue-app', {
cache: true,
basename,
domGetter: '#container',
// 子应用的入口资源地址,支持 HTML 和 JS
entry: 'http://localhost:8092',
});
setApp(app);
this.microApp = loadMicroApp({
name: 'app1',
entry: '//localhost:1234',
container: this.containerRef.current,
props: { brand: 'qiankun' },
});
🎉 系列完结
恭喜你完成了《彻底搞懂 Module Federation》系列的学习!