彻底搞懂 Module Federation(下):Runtime API 与项目实操

0 阅读11分钟

本文是《彻底搞懂 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 初始化请求链路

跟前面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 依赖库

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

 
 "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文件里面

github.com

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提供的默认加载顺序,所以暂时忽略这些钩子的影响

  1. this.getEntry(): 异步加载remoteEntry.js,在浏览器环境下通过动态创建script标签的方式异步加载远程应用,并执行
  2. remoteEntryExports.init:加载并执行完成后,拿到remoteEntry.js对外暴露出来的init方法并执行,这一步就是进行共享依赖传递融合,在远程应用中提前传入宿主的共享依赖库,并检查远程应用是否有统一的共享库,为后面的共享组件加载做准备
  3. 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 整体组件加载执行过程

image-9.png

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》系列的学习!