模块联邦

应用:组件跨项目实时共享,页面运行时按需加载另外一个应用的chunk,开箱即用,直接执行

区别于npm编译引入,类似<script src="http://cdn/jquery.js" />,只不过共享的模块只会有业务上的代码,不会有底层的框架和公用库代码

项目如何配置

app1应用:webpack.config.js

plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      filename: "remoteEntry.js", //共享模块的入口控制文件,共享的代码不一定会在此文件中
      library: { type: 'var', name: 'app1Global' }, // 共享模块的全局引用
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js", // 远程其他共享模块的加载地址
      },
      exposes: {
        "./Content": "./src/components/Content", // 共享组件定义
        "./Button": "./src/components/Button",
      },
      shared: ['vue'] // 全局模块剥离
    }),
  ]

app2应用:webpack.config.js

plugins: [
    new ModuleFederationPlugin({
      name: "app2",
      filename: "remoteEntry.js", //共享模块的入口控制文件,共享的代码不一定会在此文件中
      library: { type: 'var', name: 'app2Global' }, // 共享模块的全局引用
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js", // 远程其他共享模块的加载地址
      },
      exposes: {
        "./Form": "./src/components/Form", // 共享组件定义,
      },
      shared: ['vue'] // 全局模块剥离
    }),
  ]

如果exposes 为空对象,则webpack不会打出remoteEntry.js包,逻辑上讲没有共享的组件自然也不会打包共享模块

项目中如何使用共享模块

这里以app2应用使用app1里面的组件举例:

// layout.vue
<template>
  <div class="layout-app">
    <content-element />
      <button-element />
  </div>
</template>
<script>
    export default {};
</script> 
// main.js
import { createApp, defineAsyncComponent } from "vue";
import Layout from "./Layout.vue";

const Content = defineAsyncComponent(() => import("app1/Content"));
const Button = defineAsyncComponent(() => import("app1/Button"));

const app = createApp(Layout);

app.component("content-element", Content);
app.component("button-element", Button);

app.mount("#app");

import("app1/Content") 会被解析大致为:
loadScript("http://localhost:3001/remoteEntry.js").then(() => require('Content'))

核心逻辑 & 源码解析

1: 首先从共享模块打出的包开始:

比如Button.vue组件,和正常的打出的包结构是一致的

(self["webpackChunk_vue3_demo_home"] = self["webpackChunk_vue3_demo_home"] || []).push([[488,347],{
    // __unused_webpack_module:模块对象,__webpack_exports__: 导出对象, __webpack_require__:模块加载器
 347: ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
    
    "use strict";
    __webpack_require__.r(__webpack_exports__);

    // 定义导出对象中的default属性即默认导出值 : export default button
    __webpack_require__.d(__webpack_exports__, {
        "default": () => __WEBPACK_DEFAULT_EXPORT__
     });
    
    // 252 是vue文件,返回的对象里面有 h渲染器
    var vue__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(252);
    
    // button 组件业务代码
    const button = {
      name: "btn-component",
      render() {
        return (0,vue__WEBPACK_IMPORTED_MODULE_0__.h)(
          "button",
          {
            id: "btn-primary",
          },
          "Hello World!!!"
        );
      },
    };
    
    const __WEBPACK_DEFAULT_EXPORT__ = (button);
    
 })
    
}]);

(self["webpackChunk_vue3_demo_home"] = self["webpackChunk_vue3_demo_home"] || []).push 的作用是执行后,会将该模块代码执行,并将返回的模块值,注册到全局的模块池中缓存下来

2:从上面可以看到共享模块的包跟平时的包是一样的结构,这里webpack为了让其他应用方便访问,提供了一个统一的访问入口remoteEntry.js ,remoteEntry.js负责维护管理加载一系列共享模块,本身不涉及具体的业务代码

var app1;app1=(() => { // webpackBootstrap

    // 该模块定义了各种共享模块的导出方法
    var __webpack_modules__ = {270: function(module,export,require){
        Button: () => import(Button.vue)
        Form: () => import(Form.vue)
        Title: () => import(Title.vue)
         export.get = (module)  {return maps[module]()}
    }}
/********************************* webpack的基础方法 ***************************************/
    ***********
      ********
/********************************* 加载270这个模块,这个模块里面属性是各种export的对象,这些对象就是共享模块对象 ***************************************/
    return __webpack_require__(270); // { get(module){} }

})()

那么app2应用是如何根据remoteEntry这个模块加载指定的组件的呢,比如
import(app1/button)

app2应用会载入270这个模块姑且叫remote,然后引入button组件:remote.get(button)会从webpack_modules里面拿到加载组件的方法,执行webpack_require.e,并返回promise,
具体源码如下:

var __webpack_modules__ = ({
270: ((__unused_webpack_module, exports, __webpack_require__) => {
    // 具体业务组件以及对应组件的加载策略,252代表vue公共库,347代表button.vue组件
    var moduleMap = {
        "./Content": () => {
            return Promise.all([__webpack_require__.e(252), __webpack_require__.e(561)]).then(() => () => (__webpack_require__(561)));
        },
        "./Button": () => {
            return Promise.all([__webpack_require__.e(252), __webpack_require__.e(347)]).then(() => () => (__webpack_require__(347)));
        }
    };
    
    // 外部应用调用get方法加载具体业务组件
    var get = (module, getScope) => {
        .........
        getScope = moduleMap[module]()
        .........
        
        return getScope;
    };

    // 处理全局依赖共享的问题
    var init = (shareScope, initScope) => {
        // 处理共享模块的全局依赖比如Vue模块的逻辑
    };

    // This exports getters to disallow modifications
    __webpack_require__.d(exports, {
        get: () => get,
        init: () => init
    });

})

3: app2应用 是如何载入app1应用的remoteEntry.js的呢

// main.js 
import("app1/Button")

会编译成如下代码:

__webpack_require__.e(/* import() */ 190).then(__webpack_require__.t.bind(__webpack_require__, 190, 23))

webpack_require.e作为加载远程chunk的方法,可与webpack_require加载本地脚本进行对比

__webpack_require__.f = {};
    // This file contains only the entry chunk.
    // The chunk loading function for additional chunks
    __webpack_require__.e = (chunkId) => {
        return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
            __webpack_require__.f[key](chunkId, promises);
            return promises;
        }, []));
};

这里遍历执行webpack_require.f里面的方法并收集返回的promise,当所有的promise都完成时意味着这个远程的模块已经加载到内存里面了,注册进webpack_modules这个全局模块缓冲池里面了

webpack_require.f 有3个属性:
{
remotes:(chunkId, promises) => {},
jsonp: (chunkId, promises) => {
installedChunks[chunkId] = 0;
};
}
jsonp这个方法的作用如下:
1: 存储已加载或者正在加载的chunk
2: 根据chunkId 查找到的值为undefined 代表:chunk not loaded, 为null 代表 chunk preloaded/prefetched
3: 根据chunkId 查找到的值为Promise = chunk loading, 0 = chunk loaded

remotes这个方法的作用是:
1:载入远程模块
2:获取远程模块里面指定的业务组件模块

在main.js 编译后的chunk文件最开始的地方对webpack_modules进行如下初始化

var __webpack_modules__ = ({
293: ((module, __unused_webpack_exports, __webpack_require__) => {
    
        var error = new Error();
        module.exports = new Promise((resolve, reject) => {
            if(typeof home !== "undefined") return resolve();
            __webpack_require__.l("http://localhost:3002/remoteEntry.js", (event) => {
                if(typeof home !== "undefined") return resolve();
                var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                var realSrc = event && event.target && event.target.src;
                error.message = 'Loading script failed.\n(' + errorType + ': ' + realSrc + ')';
                error.name = 'ScriptExternalLoadError';
                error.type = errorType;
                error.request = realSrc;
                reject(error);
            }, "home");
        }).then(() => home);
    
    })
    
});

<strong>里面293这个模块在初始化的时候定义,当webpack_require(293)执行这个模块时,会去载入app1的远程remoteEntry.js 并返回一个promise</strong>

回到最开始引入远程组件的地方:webpack_require.e(/* import() */ 190)

根据上面所说里面会执行webpack_require.f.remotes(190, promises<type: []>), 这个方法主要做如下事情:
1:找到远程组件对应的远程加载配置,这个配置里面包含如下信息:
{
远程模块的全局作用域标识,通过这个标识找到模块对应的全局依赖列表,这个作用是解决全局依赖的问题
远程模块id(远程应用的入口控制模块,包含多个组件),
需要加载的组件名称,
加载远程模块脚本的promise(控制只加载一次)
}

2:执行加载远程脚本,并将promise存到promises里面

3:当promise完成后,执行webpack_require.I,给远程模块注入全局依赖,这个过程比较复杂,是一个递归的过程,详细会在下面讲到

4:webpack_require.I 会返回一个promise,当全局依赖注入完成后,远程模块external会调用get方法
external.get(远程业务组件名称) 返回组件模块对象


未完待续。。。
具体代码解释如下:

/********************************* webpack的基础方法 ***************************************/
// The module cache
    var __webpack_module_cache__ = {};
    // The require function
    __webpack_require__(){...}
    // expose the modules object (__webpack_modules__)
    __webpack_require__.m = __webpack_modules__;
    // define getter functions for harmony exports
    __webpack_require__.d() {...}
    // 用于加载远程共享模块,__webpack_require__.f对象里面定义了一组处理和加载共享模块的方法,
    // 这些方法执行返回promise,等所有promise都完成后,如下方法返回共享模块处理加载完成的最终的promise
    __webpack_require__.e() {...}
    // 作用是根据chunkId,返回对应的chunk地址
    __webpack_require__.u() {return "" + chunkId + ".js"}
    // 和上面作用类似,返回对应 chunk.css
    __webpack_require__.miniCssF() {...}
    // 全局变量
    __webpack_require__.g
    // hasOwnProperty
    __webpack_require__.o(){...}
    // load script
    __webpack_require__.l(){...}
    // define __esModule on exports
    __webpack_require__.r(){...}
    // 加载共享模块以及导出共享模块里面的指定导出对象
    __webpack_require__.f.remotes(){...}
    // 全局依赖处理,并同步到共享模块里面,使得远程模块能共享全局的Vue之类的
    __webpack_require__.S = {};
    __webpack_require__.I(){...}
    // 本地chunk的publicPath 设置
    __webpack_require__.p
    // object to store loaded and loading chunks
    // undefined = chunk not loaded, null = chunk preloaded/prefetched
    // Promise = chunk loading, 0 = chunk loaded
    __webpack_require__.f.j
    // 注册并执行异步加载的本地chunk, 与import('./abc.vue') 有关
    webpackJsonpCallback(){...}



参考相关代码:
https://zhuanlan.zhihu.com/p/115403616
https://juejin.cn/post/6844904187147321352
https://segmentfault.com/a/1190000022373938
https://cloud.tencent.com/developer/article/1703407
https://blog.csdn.net/sendudu/article/details/110232830

https://github.com/efoxTeam/emp/wiki/%E3%80%8Amodule-Federation%E5%8E%9F%E7%90%86%E5%AD%A6%E4%B9%A0%E3%80%8B

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,755评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,369评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,799评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,910评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,096评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,159评论 3 411
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,917评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,360评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,673评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,814评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,509评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,156评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,123评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,641评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,728评论 2 351

推荐阅读更多精彩内容