了解使用webpack Module Federation 就从这个Demo 开始

497 阅读4分钟

在上一篇《从一个需求开始慢慢了解 webpack 5 的Module Federation(模块联邦)》 我们讲解了需求的演变过程

也通过webpack官网基本的了解了一些信息,最终决定尝试使用 模块联邦 这个方案进行技术预演,看看可行性;

那我们先简单搭建一个Demo 来 1:1 还原开发环境的场景;

系统甲(模块A),系统乙(引用系统甲的模块A);

我们在网上溜达溜达发现了这个项目:module-federation-examples

赶紧点进去看看,这不就是我们真正需要的吗??? 里面各种example 应有尽有啊

那就赶紧clone 下来选择我们想要的example

我们最终使用 module-federation-examples/vue-cli 这个,因为他无限接近于我们现场的开发环境的场景;

该Demo 包含了 三个 目录 consumer , core 以及 other;

我们简单的安装完后跑起来看看样子:

首先 consumer 是这个样子的:

consumer.png

core 是这个样子的:

core.png

other 是这个样子的:

other.png

我们看到 consumer 中包含了 core 和 other 的内容

再看看那个 core 中的红色 border的按钮是多么的显眼;

修改了 core 中的按钮为红色边框,刷新consumer 立马就开到了 红色的边框按钮;

接下来 我们看一下 这个三个项目的具体配置;

首先看看 consumer 中的 vue.config.js


const ModuleFederationPlugin =
  require("webpack").container.ModuleFederationPlugin;

module.exports = {
  publicPath: "http://localhost:9999/",
  configureWebpack: {
    plugins: [
      new ModuleFederationPlugin({
        name: "consumer",
        filename: "remoteEntry.js",
        remotes: {
          core: "core@http://localhost:9000/remoteEntry.js",
          other : 'other@http://localhost:9001/remoteEntry.js'
        },
        shared: require("./package.json").dependencies,
      }),
    ],
  },
};

我们发现 他仅仅配置了 ModuleFederationPlugin;

其中 包含了 name, filename, remotes, shared 这几个选项;

配置相当简洁 猜想 其中的remotes 配置 就是表示了从远端加载 core , 和 other 的模块组件组件以使用; 那我们看看 代码中是如何引入的呢:

export default {
  name: "App",
  components: {
    Button: () => import("core/Button"),
    Section: () => import("core/Section"),
    MainComponent: () => import("other/MainComponent"),
  },
};

我们发现在 components 选项中 import 了core/Button. 和 Section 以及other/MainComponent

那我们看看 core 中的配置呢:

new ModuleFederationPlugin({
    name: "core",
    filename: "remoteEntry.js",
    exposes: {
      "./Button": "./src/components/Button",
      "./Section": "./src/components/Section",
    },
    shared: require("./package.json").dependencies,
}),

发现 在配置 exposes 中到导出了 Button,Section 这才使 consumer 中能够配置 remotes ,也才能在组件中 通过 import 进行引入,在template 中使用;

通过查看 consumer 应用的控制面板我们发现 加载了 几个组件文件,而且文件名称尤为的明显,就是定义的那几个组件的名称组合;

image.png

这是开发环境下我们看到的的样子,我们继续build 一下该应用看一下 构建后的产物;

这里我们就先build , core 这个;

build.png

我们发现 除了常规熟悉的文件外 还多了一个 remoteEntry.js 文件;

这个文件 就是 我们在配置选项中配置的 filename ,也是在 remotes 声明中的 入口文件;

打开 remoteEntry.js 格式化后 我们可以看到如下图

image.png

首先定义了 core 这个是在配置中的 name 选择; 接着的定义的 r 对象有两个 键 "./Button" ,"./Section" 就是我们在 expoese 中配置的需要导出的组件模块;都return 了 一个 promise 的函数,对应了应用方 使用 import 方法的导入,如下:

components: {
  Button: () => import("core/Button"),
  Section: () => import("core/Section"),
  MainComponent: () => import("other/MainComponent"),
},

都是我们眼熟的一些信息;

既然是 var 申明的 core , 那我们去控制台 看看 code 是不是在 window 上呢;

image.png

确实存在 core , 而且挂载在 window 上的

再简单看下 get 方法呢


var core;
/******/ (function() { // webpackBootstrap
/******/ 	"use strict";
/******/ 	var __webpack_modules__ = ({

/***/ "webpack/container/entry/core":
/*!***********************!*\
  !*** container entry ***!
  \***********************/
/***/ (function(__unused_webpack_module, exports, __webpack_require__) {

var moduleMap = {
	"./Button": function() {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Button_vue")]).then(function() { return function() { return (__webpack_require__(/*! ./src/components/Button */ "./src/components/Button.vue")); }; });
	},
	"./Section": function() {
		return Promise.all([__webpack_require__.e("webpack_sharing_consume_default_vue_vue"), __webpack_require__.e("src_components_Section_vue")]).then(function() { return function() { return (__webpack_require__(/*! ./src/components/Section */ "./src/components/Section.vue")); }; });
	}
};
var get = function(module, getScope) {
	__webpack_require__.R = getScope;
	getScope = (
		__webpack_require__.o(moduleMap, module)
			? moduleMap[module]()
			: Promise.resolve().then(function() {
				throw new Error('Module "' + module + '" does not exist in container.');
			})
	);
	__webpack_require__.R = undefined;
	return getScope;
};
var init = function(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: function() { return get; },
	init: function() { return init; }
});

/***/ })

/******/ 	});

截取了部分代码:

moduleMap ,get, init 等都写的比较简洁 。

我们知道了大概的工作流程后,对后面的深入了解其原理就显得不那么陌生了。

还有没有其他方案?

接下来 就会吧 按该方案(模块联邦)用于生产环境进行验证 同时也将去使用 UMD方式共享模块 进行实践 最终选择一个适合当前环境以及团队的合理方案;

最后的最后

下面请上我们今天的主角:有请小趴菜

小趴菜.jpeg