Module Federation你的浪漫我来懂

发表于 3年以前  | 总阅读数:268 次

前言

我们在实际开发中,经历过许多次的模块共享的场景。最常见的场景例如我们将代码封装后根据版本和环境的不同发布到公共平台或者私有平台,供不同项目进行使用,npm 工程化就是其中最日常的实践。

【通关目标: 在页面中插入轮播图模块】

NPM 方式-share lib

将轮播图代码打包发布到 NPM 包。主项目中通过 package.json 依赖加载到本地进行编译打包。 【biu~通关成功,当前一星】

当投入生产时,多个项目对于被引入的轮播图代码都没有进行共享,他们是各自独立的。如果二次封装的某个模块进行更改并且要求全部同步……亦或者后期迭代或者定制化需求——

小孙 : “啊!!那岂不是要手动去一一修改?” 代码爸爸慈祥一笑,看着二傻子痛苦加班。

Module Federation-share subApp

初初查看到一些资料的时候,脑海中浮现一些远古项目:项目中通过 iframe 引入别的网页作为一个块进行展示,当然这也是微前端的一种实现,但是受 postMessage 通信机制、刷新后退、安全问题、SEO、Cookie、速度慢等影响,目前还有许多难以解决的点,一旦复杂度提升,后期迭代就可能需要更多的成本去维护项目或者妥协功能。

Module Federation 的出现使得多部门协作开发变得更便捷。多个单独的构建形成一个应用程序。这些单独的构建彼此之间不应该有依赖关系,因此可以单独开发和部署它们,它们可以随时被应用,再各自成为新的应用块。官网对于这块概念拆解成Low-level conceptsHigh-level concepts

让我们来结合配置项来更详细了解作者的整个设计过程。

Low-level concepts - 代码中的分子与原子

PS: 这里的引用不是语法中的引用哦

这里小孙想引用一下化学中的分子与原子的关系。这张图要侧重说明的是本地模块远程模块

  • 在图中每一份 Host 对于它本身来说, 都被理解成本地模块,它是当前应用构建的一部分,对于它本身项目来说,它是分子,是一个容器。但是对于引用它整个项目的 Host 爸爸来说,它整个远程模块是原子。
  • 每一份 Romote远程模块是原子,它是运行时从容器加载的模块,是异步行为。当使用远程模块时,这些异步操作将放置在远程模块和入口点之间的下一个块加载操作中。如果没有块加载操作,就不可能使用远程模块。

A container is created through a container entry, which exposes asynchronous access to the specific modules. The exposed access is separated into two steps:

  1. loading the module (asynchronous) 加载模块(异步)
  2. evaluating the module (synchronous) 执行模块(同步)

加载模块将在 chunk 加载期间完成。执行模块将在与其他(本地和远程)的模块交错执行期间完成。

我们来找个例子配合看一下整个设计过程:

一个例子介绍 MF 的常规用法

例子把 App3 的组件引入到 App2 的组件,然后 App1 再引入 App2 的这个二次封装的组件。这个例子还是非常接近目前常见的开发场景。

/* 业务代码如下 */

// App1 webpack-config
new ModuleFederationPlugin({
    // ...other config
    name: "app1",
    remotes: {
      app2: `app2@${getRemoteEntryUrl(3002)}`,
    },
})

// App2 webpack-config
new ModuleFederationPlugin({
    // ...other config
    name: "app2",
    filename: "remoteEntry.js",
    exposes: {
      "./ButtonContainer": "./src/ButtonContainer",
    },
    remotes: {
      app3: `app3@${getRemoteEntryUrl(3003)}`,
    },
})

// App3 webpack-config
new ModuleFederationPlugin({
  // ...other config
  name: "app3",
  filename: "remoteEntry.js",
  exposes: {
    "./Button": "./src/Button",
  },
}),

加载模块相关代码解析

// from App1 的 remoteEntry.js 中__webpack_modules__某个对象的 value
"webpack/container/entry/app3":((__unused_webpack_module, exports, __webpack_require__) => {
  var moduleMap = {
    "./Button": () => {
      return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_react_react-_0085"), __webpack_require__.e("src_Button_js")]).then(() => (() => ((
        __webpack_require__( /*! ./src/Button */
          "./src/Button.js")))));
    }
  };
  // get 方法作用:获取 Scope
  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;
  };
  // init 方法作用:初始化作用域对象 并把依赖存储到 shareScope 中
  var init = (shareScope, initScope) => {
    if (!__webpack_require__.S) return;
    var oldScope = __webpack_require__.S["default"];
    var name = "default"
    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);
  };
  // This exports getters to disallow modifications
  __webpack_require__.d(exports, {
    get: () => (get),
    init: () => (init)
  });
})


// App main.js 后面有详细代码 这边简单介绍一下就是一个 JSONP 的下载
__webpack_require__.I = (name, initScope) => {})

/* 在消费模块执行的操作 from consumes   */
// 确认好 loaded 以后调用原子的 Scope
var get = (entry) => {
  entry.loaded = 1;
  return entry.get()
};
// 消费模块再执行原子的异步初始化行为 在这个模块还会处理后面提到的一个疑问 公共模块的版本问题
//__webpack_require__.S[scopeName] 取出 scopeName 对应的 scope
var init = (fn) => (function(scopeName, a, b, c) {
  var promise = __webpack_require__.I(scopeName);
  if (promise && promise.then) return promise.then(fn.bind(fn, scopeName, __webpack_require__.S[scopeName], a, b, c));
  return fn(scopeName, __webpack_require__.S[scopeName], a, b, c);
});
  • 执行 init 初始化以后,收集的依赖存储到 shareScope 中,并初始化作用域。
  • 中间会涉及到 版本号处理,关联关系,公共模块等处理,拼数据挂载到__webpack_require__上使用。
  • 调用时通过通过 JSONP 的远程加载模块(异步行为),相关代码如下:
// from App1 main.js
/***/ "webpack/container/reference/app2":
/*!*******************************************************!*\
  !*** external "app2@//localhost:3002/remoteEntry.js" ***!
  \*******************************************************/
/***/ ((module, __unused_webpack_exports, __webpack_require__) => {

"use strict";
var __webpack_error__ = new Error();
module.exports = new Promise((resolve, reject) => {
 if(typeof app2 !== "undefined") return resolve();
 __webpack_require__.l("//localhost:3002/remoteEntry.js", (event) => {
  //...这个方法根据 JSONP 加载远程脚本 
 }, "app2");
}).then(() => (app2));

// __webpack_require__.l 定义如下
// 对 IE 和 ES module 单独处理 如果是 ES module,取 module[default]的值
// 这边特别定义 inProgress 去监控多个 url 的回调状态,这段设计挺有意思的
__webpack_require__.l = (url, done, key, chunkId) => {
    if(inProgress[url]) { inProgress[url].push(done); return; }
    var script, needAttach;
    if(key !== undefined) {
     var scripts = document.getElementsByTagName("script");
     for(var i = 0; i < scripts.length; i++) {
      var s = scripts[i];
      if(s.getAttribute("src") == url || s.getAttribute("data-webpack") == dataWebpackPrefix + key) { script = s; break; }
     }
    }
    if(!script) {
     needAttach = true;
     script = document.createElement('script');

     script.charset = 'utf-8';
     script.timeout = 120;
     if (__webpack_require__.nc) {
      script.setAttribute("nonce", __webpack_require__.nc);
     }
     script.setAttribute("data-webpack", dataWebpackPrefix + key);
     script.src = url;
    }
    inProgress[url] = [done];
    var onScriptComplete = (prev, event) => {
     // avoid mem leaks in IE.
     script.onerror = script.onload = null;
     clearTimeout(timeout);
     var doneFns = inProgress[url];
     delete inProgress[url];
     script.parentNode && script.parentNode.removeChild(script);
     doneFns && doneFns.forEach((fn) => (fn(event)));
     if(prev) return prev(event);
    }
    ;
    var timeout = setTimeout(onScriptComplete.bind(null, undefined, { type: 'timeout', target: script }), 120000);
    script.onerror = onScriptComplete.bind(null, script.onerror);
    script.onload = onScriptComplete.bind(null, script.onload);
    needAttach && document.head.appendChild(script);
   };
  })();

前面说了单个具象化的加载模块和执行模块的代码,现在说说分子与原子之间的代码关系,如何知晓并加载原子代码:

// 每个分子 main.js 中 例如 App1 只引入了 App2 的
var __webpack_modules__ = ({
 "webpack/container/reference/app2":": ()
  ....
})

// 如果是有原子代码的 查看 remotes loading 模块
// 执行后找到//localhost:3002/remoteEntry.js 的文件 再异步执行里面的原子代码
__webpack_require__.l("//localhost:3002/remoteEntry.js", (event) => {
  if (typeof app2 !== "undefined") return resolve();
  var errorType = event && (event.type === 'load' ? 'missing' :
                            event.type);
  var realSrc = event && event.target && event.target.src;
  __webpack_error__.message = 'Loading script failed.\n(' +
    errorType + ': ' + realSrc + ')';
  __webpack_error__.name = 'ScriptExternalLoadError';
  __webpack_error__.type = errorType;
  __webpack_error__.request = realSrc;
  reject(__webpack_error__);
}, "app2");

// 然后加载相关 chuck 的时候根据枚举进行 get 调用 
var chunkMapping = {"app2/ButtonContainer": ["webpack/container/remote/app2/ButtonContainer"]};
var idToExternalAndNameMapping = {
  "webpack/container/remote/app2/ButtonContainer": ["default","./ButtonContainer","webpack/container/reference/app2"]
};

__webpack_require__.f.remotes = (chunkId, promises) => {
  if(__webpack_require__.o(chunkMapping, chunkId)) {
    chunkMapping[chunkId].forEach((id) => {
      var getScope = __webpack_require__.R;
      if(!getScope) getScope = [];
      // 获取渲染时候的 moduleName
      var data = idToExternalAndNameMapping[id];
      if(getScope.indexOf(data) >= 0) return;
      getScope.push(data);
      if(data.p) return promises.push(data.p);
      var onError = (error) => {
        // 处理错误然后给一个标志数据表示错误 太长不看
        data.p = 0;
      };
      var handleFunction = (fn, arg1, arg2, d, next, first) => {
        // 异步执行方法 太长不看
      }
      var onExternal = (external, _, first) => (external ? handleFunction(__webpack_require__.I, data[0], 0, external, onInitialized, first) : onError());
      // 核心代码 本质是调用 get 方法
      var onInitialized = (_, external, first) => (handleFunction(external.get, data[1], getScope, 0, onFactory, first));
      var onFactory = (factory) => {
        data.p = 1;
        __webpack_modules__[id] = (module) => {
          module.exports = factory();
        }
      };
      handleFunction(__webpack_require__, data[2], 0, 0, onExternal, 1);
    });
  }
}

// 调用 get 以后 下载下面这个文件再做具象化的处理
// 在打包后的代码中 import 相关的原子模块 异步加载
(self["webpackChunk_nested_app2"] = self["webpackChunk_nested_app2"] || []).push([["src_bootstrap_js"], {
 "./src/App.js": "...",
  "./src/ButtonContainer.js": "...",
  "./src/bootstrap.js":"..."
}])

High-level concepts - 双向共享和推断

前面说了容器的概念,再深入拓展一个过去常有的场景: 暂不考虑抽离公共逻辑的基础上,组件 A 和组件 B 都互相需要移植一部分功能,你刷刷刷复制对应代码过去,后期每次迭代都需要同时更新组件 A 和组件 B 中的对应内容,那如果这个纬度是两个项目呢?

疑问一:例子中的 import("./bootstrap")作为入口是为什么

看看打包后做了什么:

(self["webpackChunk_nested_app2"] = self["webpackChunk_nested_app2"] || []).push([["src_bootstrap_js"], {
 "./src/App.js": "...",
  "./src/ButtonContainer.js": "...",
  "./src/bootstrap.js":"..."
}])

//每个模块大概处理几件事
((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    // exports module
    __webpack_require__.r(__webpack_exports__)
    // 处理不同 module 类型之间的差异 如果是 ES module 取这个 default 的值
    __webpack_require__.d(__webpack_exports__, {
      /* harmony export */
      "default": () => (__WEBPACK_DEFAULT_EXPORT__)
      /* harmony export */
    });
    //...具体组件逻辑 或者 import 原子部分的代码~触发后续的回调钩子去初始化 scoped
   // 最后挂载
   react_dom__WEBPACK_IMPORTED_MODULE_2___default().render( /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_1___default().createElement(
   _App__WEBPACK_IMPORTED_MODULE_0__.default, null), document.getElementById("root"));  
 }),

It's still a valid a approach to wrap your entry point with import("./bootstrap"). When doing so, make sure to inline the entry chunk into the HTML for best performance (no double round trip).

This is now the recommended approach. The old "fix" no longer works as remotes could provide shared modules to the app, which requires an async step before using shared modules. Maybe we provide some flag for the entry option in future to do this automatically.

文章里写到在开启 MF 中共享模块时,入口采用异步边界可以有效规避掉双重更新造成的性能加载问题。官方文档对此还提供了这种做法的缺陷案例【以下来自官网】:

1.通过 ModuleFederationPlugin 将依赖的 eager 属性设置为 true:

new ModuleFederationPlugin({
    // ...other config
    shared: {
        eager: true,
    }
});  
// webpack beta.16 升级到 webpack beta.17 可能类似报错 Uncaught Error: Module "./Button" does not exist in container.

2.更改 exposes:Uncaught TypeError: fn is not a function

new ModuleFederationPlugin({
  exposes: {
-   'Button': './src/Button'
+   './Button':'./src/Button'
  }
});

// 此处错误可能是丢失了远程容器,请确保在使用前添加它。
// 如果已为试图使用远程服务器的容器加载了容器,但仍然看到此错误,则需将主机容器的远程容器文件也添加到 HTML 中。

疑问二:包的版本选择

在我们目前应用到的许多场景中,就对私有库的自定义组件做过本地的二次封装,由于代码是单向更新的,在移植项目的过程中就存在许多难以规避的问题,Module Federation 通过设置singleton: true 开启公共模块可以一定程度解决这个问题。但是如果两方项目所需的版本号不一致是按照什么依据呢?

// 前提情况 App1 是 host App2 是 remote App1 中引用 App2 的组件
// App1 package.json:
"dependencies": {
  "mf-test-ssy": "^1.0.0"
}
// App2 package.json:
"dependencies": {
  "mf-test-ssy": "^2.0.0"
}

// webpack-config-common 部分:
new ModuleFederationPlugin({
  // ...other config
  shared: { 
    react: { singleton: true }, 
    "react-dom": { singleton: true },
    "mf-test-ssy":{ singleton: true }, 
  },
  }),

这里小孙简单写了个 demo 尝试模拟这个问题,以basic-host-remote 案例为基础,自己发布了两个不同版本的 npm 包,分别引入 v1.0.0 和 v2.0.0 查看一下结果。

可以看到 host 展示的 Npm 版本虽然低于 remote 中 Npm 的版本,但是展示的还是 remote 中较高的版本的代码。

然后互换 App1 和 App2 的 npm 版本:

// App1 package.json:
"dependencies": {
  "mf-test-ssy": "^2.0.0"
}
// App2 package.json:
"dependencies": {
  "mf-test-ssy": "^1.0.0"
}

可以看到此时 App2 还是以低版本展示为主,App1 还是以本地的引用版本为主,开启共享的差异性并不大。 共享中的模块请求(from 官网中文站):

  • 只在使用时提供
  • 会匹配构建中所有使用的相等模块请求
  • 将提供所有匹配模块
  • 将从图中这个位置的 package.json 提取 requiredVersion
  • 当你有嵌套的 node_modules 时,可以提供和使用多个不同的版本

如何解决? => 自动推断的设置

packageName 选项允许通过设置包名来查找所需的版本。默认情况下,它会自动推断模块请求,当想禁用自动推断时,请将 requiredVersion 设置为 false。

疑问三:共享模块是什么程度的共享

借此猜测某些库是不是也只会一次实例化,实验继续 UP!! Npm 中的构造函数逻辑更改如下:初始化成功的例子在 window 下挂载上数据,并且每次初始化后打印值递增。两份代码都更新成 V5.0.0,我们看一下效果:

看似没有问题对不对,小孙复检的时候猛然惊醒,这是两个项目,window 各自为政,这个例子这样设计本身就是大错特错。但是没关系,虽然小孙不靠谱,但是 webpack 靠谱呀。

  • main.js 是应用主文件每次都先加载这个。
  • remoteEntry.js 在 App1 中先加载,是因为 App1 中依赖于 App2 的一些依赖配置,所以 App1 中的 remoteEntry.js 加载优先级非常高,加载以后它可以知道自己需要远程加载什么资源。
  • 可以看到 App2 加载了 mf-test-ssy, App1 并没有加载 mf-test-ssy,但是直接加载了 App2 的 remoteEntry,说明 remoteEntry.js 是作为 remote 时被引的文件。
  • 构造函数应该实质上只是初始化了一次,我们从这个结论出发,看一下 webpack 相关的代码配置,再逐步细化到源码。

从加载文件到源码

这里先贴会影响打包的业务代码:

/*
** App1 app.js
*/
import React from "react";
import Test from "mf-test-ssy"
// 这里用了 App2 的 button 组件代码
const RemoteButton = React.lazy(() => import("app2/Button"));
const App = () => (
  <div>
    <React.Suspense fallback="Loading Button">
      <RemoteButton />
    </React.Suspense>
  </div>
);
export default App;
/*
** App2 Html-Script 注意这里是编译后动态生成的。
*/
<script defer src="remoteEntry.js"></script>

把 remoteEntry 打包后的代码,把 相关部分截取出来:

/* webpack/runtime/sharing */
//前面暂且忽略一些定义以及判空
//存放 scope
__webpack_require__.S = {};
var initPromises = {};
var initTokens = {};
//初始化 scope,最后把数据拼成一个大对象
__webpack_require__.I = (name, initScope) => {
  if(!initScope) initScope = [];
  // handling circular init calls
  var initToken = initTokens[name];
  if(!initToken) initToken = initTokens[name] = {};
  if(initScope.indexOf(initToken) >= 0) return;
  initScope.push(initToken);
  // only runs once
  if(initPromises[name]) return initPromises[name];
  // creates a new share scope if needed
  if(!__webpack_require__.o(__webpack_require__.S, name)) __webpack_require__.S[name] = {};
  // runs all init snippets from all modules reachable
  var scope = __webpack_require__.S[name];
  var warn = (msg) => (typeof console !== "undefined" && console.warn && console.warn(msg));
  var uniqueName = "@basic-host-remote/app2";
  //注册共享模块
  var register = (name, version, factory, eager) => {
    var versions = scope[name] = scope[name] || {};
    var activeVersion = versions[version];
    if(!activeVersion || (!activeVersion.loaded && (!eager != !activeVersion.eager ? eager : uniqueName > activeVersion.from))) versions[version] = { get: factory, from: uniqueName, eager: !!eager };
  };
  //初始化远程外部模块
  var initExternal = (id) => {
    var handleError = (err) => (warn("Initialization of sharing external failed: " + err));
    try {
      var module = __webpack_require__(id);
      if(!module) return;
      var initFn = (module) => (module && module.init && module.init(__webpack_require__.S[name], initScope))
      if(module.then) return promises.push(module.then(initFn, handleError));
      var initResult = initFn(module);
      if(initResult && initResult.then) return promises.push(initResult.catch(handleError));
    } catch(err) { handleError(err); }
  }
  var promises = [];
  //根据 chunkId 的名称注册共享模块
  switch(name) {
    case "default": {
      register("mf-test-ssy", "6.0.0", () => (__webpack_require__.e("node_modules_mf-test-ssy_index_js").then(() => (() => (__webpack_require__(/*! ./node_modules/mf-test-ssy/index.js */ "./node_modules/mf-test-ssy/index.js"))))));
      register("react-dom", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react-dom_index_js"), __webpack_require__.e("webpack_sharing_consume_default_react_react-_76b1")]).then(() => (() => (__webpack_require__(/*! ./node_modules/react-dom/index.js */ "./node_modules/react-dom/index.js"))))));
      register("react", "16.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_react_index_js"), __webpack_require__.e("node_modules_object-assign_index_js-node_modules_prop-types_checkPropTypes_js")]).then(() => (() => (__webpack_require__(/*! ./node_modules/react/index.js */ "./node_modules/react/index.js"))))));
    }
      break;
  }
  if(!promises.length) return initPromises[name] = 1;
  return initPromises[name] = Promise.all(promises).then(() => (initPromises[name] = 1));
};
})();

这段代码所做的就是根据配置项将模块生成内部对应的 modules,定义了一个 scope 去存储所有的 module,然后注册了共享模块等操作。全部挂载在__webpack_require__上,这样处理以方便后续 require 的方式引入进来。对应最最最核心的源码:

// 四大天王镇宅 
sharing: {
   // 处理分子原子关系的依赖
  get ConsumeSharedPlugin() {
   return require("./sharing/ConsumeSharedPlugin");
  },
    // 处理 provide 依赖
  get ProvideSharedPlugin() {
   return require("./sharing/ProvideSharedPlugin");
  },
    // 我是入口 让我来调用 并且我实现了共享
  get SharePlugin() {
   return require("./sharing/SharePlugin");
  },
  get scope() {
   return require("./container/options").scope;
  }
},
// from /webpack-master/lib/sharing/SharePlugin.js
class SharePlugin {
 /**
  * @param {SharePluginOptions} options options
  */
 constructor(options) {
  /** @type {[string, SharedConfig][]} */
    // 处理 options 格式 模块二次封装 
  const sharedOptions = parseOptions(...太长不看);
  /** @type {Record<string, ConsumesConfig>[]} */
    // 定义 Host 消费 remote 的信息 后面会根据这个关联去加载前面说的原子的初始化以及 scoped
  const consumes = sharedOptions.map(([key, options]) => ({
   [key]: {
    import: options.import,
    shareKey: options.shareKey || key,
    shareScope: options.shareScope,
    requiredVersion: options.requiredVersion,
    strictVersion: options.strictVersion,
    singleton: options.singleton,
    packageName: options.packageName,
    eager: options.eager
   }
  }));
  /** @type {Record<string, ProvidesConfig>[]} */
    // 核心代码 处理
  const provides = sharedOptions
   .filter(([, options]) => options.import !== false)
   .map(([key, options]) => ({
    [options.import || key]: {
     shareKey: options.shareKey || key,
     shareScope: options.shareScope,
     version: options.version,
     eager: options.eager
    }
   }));
  this._shareScope = options.shareScope;
  this._consumes = consumes;
  this._provides = provides;
 }

 /**
  * Apply the plugin
  * @param {Compiler} compiler the compiler instance
  * @returns {void}
  */
 apply(compiler) {
    // 处理分子原子关系的依赖
  new ConsumeSharedPlugin({
   shareScope: this._shareScope,
   consumes: this._consumes
  }).apply(compiler);
    // 处理 provider 依赖
  new ProvideSharedPlugin({
   shareScope: this._shareScope,
   provides: this._provides
  }).apply(compiler);
 }
}
module.exports = SharePlugin;

总结

每一个分子跟原子的爱恨纠葛终有一个文件去划分好主次,虽然异步加载分离打包,但是爱永不失联。 每一个公共分享的时刻,runtime 在各自心中,就像共同孕育同一个孩子,生了一次不会生第两次。 但是—— 共享模块中 remote 版本大,按照较大的算,如果 remote 版本小,按照我本地说了算。

其他:MF 生态

ExternalTemplateRemotesPlugin

有需求在构建中使用上下文处理处理动态 Url 的,且需要解决缓存失效问题的,可以看一下这个插件。

from https://github.com/module-federation/module-federation-examples/issues/566

  • Dynamic URL, have the ability to define the URL at runtime instead of hard code at build time.
  • Cache invalidation.
// from webpack.config
plugins: [
    new ModuleFederationPlugin({
        //...config
        remotes: {
          'my-remote-1': 'my-remote-1@[window.remote-1-domain]/remoteEntry.js?[getRandomString()]',
        },
    }),
    new ExternalTemplateRemotesPlugin(), //no parameter,
]

参考资料

  • https://webpack.js.org/concepts/module-federation/#building-blocks
  • https://github.com/sokra/slides/blob/master/content/ModuleFederationWebpack5.md
  • https://www.youtube.com/watch?v=x22F4hSdZJM
  • https://github.com/module-federation/module-federation-examples
  • https://segmentfault.com/a/1190000039031505
  • http://img.iamparadox.club/img/mf2.jpg
  • https://developer.aliyun.com/article/755252

本文由哈喽比特于3年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/HxeV_sgLFRfmKVryT6_n7w

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237231次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8065次阅读
 目录