Webpack5 Module Federation 简单探索

2,553 阅读6分钟

Module Federation是什么?

Module Federation中文释义为模块联合,是在webpack5提出的一个概念。它为我们提供了一种新的构建模式:程序子模块之间可以单独开发、构建、部署,互不影响。这种模式一般称作微前端,但它提供的能力不止于此。

Module Federation有哪些特点?

  1. 模块拆分:程序子模块拥有完全独立的开发流程,互不影响;
  2. 模块资源共享:通过配置,可以实现各模块之前的依赖包共享,避免重复加载;
  3. 全局状态共享:对于react、vue等框架,使用Module Federation可以很容易在各模块之间共用全局状态,无需额外处理。

项目中如何使用Module Federation?

使用Module Federation前,先明确一下本地模块与远程模块的概念:

  1. 本地模块是普通模块,它们是当前应用的一部分;远程模块不存在于当前应用,是在运行时从其他地方引用的模块。
  2. 本地模块与远程模块是相对的,一个模块既可以是本地模块也可以是远程模块(可以引用别的模块也可以被其他模块引用)。

ModuleFederationPlugin插件

在原有项目引入webpack5自带ModuleFederationPlugin的插件,修改配置项即可,无需做其他操作。

  1. ModuleFederationPlugin插件引用
const {ModuleFederationPlugin} = require("webpack").container;
  1. ModuleFederationPlugin插件配置项
module.exports = { 
    ...
    plugins: [
        new ModuleFederationPlugin({
        
            // 作为远程模块时必须提供的配置项
            name: 'remote', //当前应用的别名,被其他模块引用时作为模块名称
            filename: 'remoteEntry.js', //作为远程模块,被其他模块引用的入口文件名。
            exposes: { //提供给其他模块使用的组件,key为在其他模块引入时的相对路径
                "./About": "./src/views/About.js", 
            },
            // 作为远程模块时可选配置项
            library: { // 定义如何将输出内容暴露给应用。
                type: 'var', //暴露变量的方式,常用 var、window,默认为 var。
                name: 'remote' //暴露给外部应用的变量名
            },
            
            // 作为本地模块时必须提供的配置项
            remotes: { //加载其他远程模块的入口文件
                remote: "remote@http://localhost:8001/remoteEntry.js",
            },
            
            // 作为本地模块和远程模块共用的可选配置项
            shared: {
                'vue': {
                    import: 'false', //false|string,
                    singleton: false, 
                    // 是否开启单例模式。
                    // 默认不开启,当前模块的依赖版本与其他模块共享的依赖版本不一致时,分别加载各自的依赖;
                    //开启后,加载的依赖的版本为共享版本中较高的。(本地模块不开启,远程模块开启,只加载本地模块,远程模块即使版本更高,也不加载。)
                    
                    version: '3.2.6', //指定共享依赖的版本
                    requiredVersion: '3.2.6', //指定当前模块需要的版本,默认值为当前应用的依赖版本
                    strictVersion: 'false', //是否需要严格的版本控制。如果开启,单例模式下,strictVersion与实际应用的依赖的版本不一致时,会抛出异常。
                    packageName: 'string', //用于从描述文件中确定所需版本的包名称。仅当无法从请求中自动确定包名称时才需要这样做。
                    sharedKey: 'string', //共享依赖的别名, 默认值 shared 配置项的 key 值.
                    shareScope: 'default' //当前共享依赖的作用域名称,默认为 default
                    eager: false, //共享依赖在打包过程中是否被分离为单独文件,默认分离打包。如果为true,共享依赖会打包到入口文件,不会分离出来,失去了共享的意义。
                }
            },
            
        })

    ]
}

一个简单的例子

  1. 远程应用模块设置
plugins: [
    new ModuleFederationPlugin({
        name: "remote",
        filename: "remoteEntry.js",
        exposes: { //提供了Button和About两个组件
            "./Button": "./src/components/Button.js",
            "./About": "./src/views/About.js",
        },
    }),
],
  1. 本地应用模块设置
plugins: [
    new ModuleFederationPlugin({
        remotes: {
            remote: "remote@http://localhost:8002/remoteEntry.js", //生产环境替换成真实的URL地址
        },
    }),
],
  1. 在本地应用引入远程模块的组件
const About = () => import("remote/About");
const routes = [
    ...
    {
        path: '/about',
        name: 'About',
        component: About
    },
]

以上就是使用ModuleFederationPlugin的全部操作,就是这么简单。

ModuleFederationPlugin的工作原理

1. 远程模块组件的加载

使用上面的配置,启动本地环境,页面加载了远程模块的About组件(8000端口是本地模块服务,8002端口是远程模块服务)。

我们可以发现,远程模块一共有3个js文件被加载了,他们分别是:

  • remoteEntry.js:远程模块的入口文件
  • node_modules_vue_dist_vue_runtime_esm-bundler_js.js:远程模块的依赖文件
  • src_views_About_vue.js:远程模块的About组件文件

image.png

如果你留意本地模块的配置,可以发现:本地模块只是引用了远程模块的入口文件remoteEntry.js。那么,远程模块的依赖文件和远程模块的组件文件是何如加载的呢?

首先,我们看看作为远程模块时,会打包出那些文件:

  1. 入口文件remoteEntry.js
  2. 依赖文件
  3. 若干组件文件(每个组件都会单独被打包)

入口文件remoteEntry.js在本地项目运行时就会同步加载,它记录了每个远程模块组件的name与资源地址(依赖和组件本身都记录)的映射关系,当在本地模块使用到某个远程组件时,就会异步加载所需要的资源。所以,依赖文件和组件文件,都是通过入口文件异步加载的。

image.png

2. 依赖共享的实现

依赖共享配置

在ModuleFederationPlugin配置项里添加需要共享的依赖信息

本地模块配置

new ModuleFederationPlugin({
    remotes: {
        remote: "remote@http://localhost:8002/remoteEntry.js",
    },
    shared: {
        vue: {
            version: '3.2.5',
            singleton: true
        },
    },
}),

远程模块配置

plugins: [
    new ModuleFederationPlugin({
        name: "remote",
        filename: "remoteEntry.js",
        exposes: { //提供了Button和About两个组件
            "./Button": "./src/components/Button.js",
            "./About": "./src/views/About.js",
        },
        shared: {
            vue: {
                version: '3.2.0',
                singleton: true
            },
        },
    }),
],

重启服务,可以看到,远程模块的依赖文件没有被加载了。而是使用本地模块的依赖文件(本地的依赖文件版本更高;如果是远程模块的依赖版本更高,则加载的是远程模块依赖,本地模块依赖不加载)。

image.png

依赖共享原理

当模块都配置了共享信息,每个依赖都会单独打包成一个文件。

  • 远程模块的入口文件会记录共享的依赖的版本信息以及依赖加载规则。
  • 本地模块加载远程模块的入口文件后,会根据共享规则,选择加载哪个版本的依赖文件。 被共享的依赖文件通过这种异步加载的方式,避免了多个模块之间重复加载各自依赖的情况。

( ...未完待续 )

参考文章

webpack 学习系列(一):module federation

深入探索Webpack5之Module Federation的“奇淫技巧”

webpack.js.org/concepts/mo…

webpack.js.org/plugins/mod…

延伸了解

YY团队基于Module Federation微前端框架EMP