首发于前端杂谈
webpack5模块联邦(Module Federation)

webpack5模块联邦(Module Federation)

一、引言

自webpack5发布以来,推出了很多全新的特性,其中最主要的就如下三点:

①、可持续性缓存--通过cache配置可实现首次构建后一直保存缓存。

②、真正意义上的tree-shaking--让你的打包体积更小,去掉无用的代码。

③、模块联邦(module federation)--本文需要探讨实战的特性;


二、模块联邦(Module Federation)

我们知道webpack可以通过dll实现对同一个项目的公共组件模块,做成代码共享common chunk,但是如果是要实现跨项目针对不同的应用就变得非常的困难不易实现。几乎没办法做到不同应用之间进行插拔式的热更新。那怎么样去实现这种跨应用间的共用模块运用呢?于是乎webpack5内置了一个模块联邦的功能特性,这个功能可以让跨应用间做到模块共享真正的插拔式的便捷使用。比如a应用如果想使用b应用中list的组件,通过模块联邦可以直接在a中进行import('b/list')非常的方便。

三、共享模块处理的方式

1、npm方式公共模块共享

平时大家开发想必对npm也会十分的熟悉,像我们如果想把某一块功能某一组件做成共用的,可以让其他应用进行使用的,一般就把他做成一个npm包然后发布上去,其他需要用到的应用进行下载安装就能用了。效果如下图:

虽然说这种方式在一定的程度上是实现了代码的共用,跨项目之间的安装即使用,但是npm的方式存在一个巨大的问题就是当npm包更新后,怎样让各个子应用也实现更新呢?只能是使用到的相关项目进行一次重新构建然后发布上线,这样才能在正式线上用到最新的代码。这也就是走本地编译带来的缺陷。

2、UMD 方式共享模块

UMD方式可以说的是真正的runtime方式使用公共模块,就像我们平时使用jQuery、lodsah一样,通过直接引用他们的cdn,然后直接在项目中进行使用。而且当他们发布更新时候我们也可以做到更新。如下图:

虽然说这种方式是可以实现多个子应用共享共用模块,但是依旧存在两个问题:1、各包库之间容易存在冲突;2、无法在本地编译时候达到最大的优化效果。

3、微前端

微前端也是目前前端领域相当火的一个趋势,在处理大型项目应用、历史项目重构兼容等场景时候相当有用也是未来的一个发展趋向,它主要解决了多项目并存、代码隔离、与语言框架无关。如下图所示:



微前端基座下面可以有n个子应用,每个子应用都是可以单独构建的,他们之间是相互独立的,都可以引入对应的公共模块common,微前端有两种构建方式1、每个子应用单独进行构建,这样无法抽离出公共的代码,每个子应用都会有一份common的代码;2、可以进行整体的打包但是这样如果子应用规模庞大那构建速度是很慢的,没法扩展微前端的子应用规模。

4、模块联邦

终于到了本文的主角了webpack5新特性模块联邦,webpack5内置的这个功能比较完美的解决上面几种共享模式下的问题,既可以做到打包发布模块供给后,消费者能够实时保持同步,也可以进行代码构建时候的优化,他可以在一个应用中直接导出使用另外一个应用的模块,如下图:

如图B应用作为模块消费方(host),直接import应用A中的模块,A应用作为模块提供方(remote)

四、模块联邦的使用

模块联邦是webpack的内置模块,使用起来也是相当的简单,做好相关配置就可以了,首先要保障项目webpack是5.0及以上。然后在对应的项目的webpack.config.js进行配置,ModuleFederationPlugin有几个重要的参数:

1、name: 当前应用的名称,需要唯一性;
2、exposes: 需要导出的模块,用于提供给外部其他项目进行使用;
3、remotes: 需要依赖的远程模块,用于引入外部其他模块;
4、filename: 入口文件名称,用于对外提供模块时候的入口文件名;
5、shared: 配置共享的组件,一般是对第三方库做共享使用;

配置如下:

new ModuleFederationPlugin({
            name: "main_app",
            filename: "remoteEntry.js",
            exposes: {
                "./search": "./src/search/search.vue"
            },
            remotes: {
                lib_remote: "lib_remote@http://localhost:8085/remoteEntry.js",
            },
             shared: {
                 vue: {
                     eager: true,
                     singleton: true,
                 }
             }
        })

案例实操:

项目关系介绍,我们先创建2个项目分别:main_app、lib_app;我们让lib_app作为模块提供方(remote),而mian_app就作为模块的消费方(host)。


①、创建lib_app文件夹,cd到文件目录下面npm init;然后安装相关依赖,创建webpack.config.js文件,配置文件如下;

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const resolve = (p) => path.resolve(__dirname, p)
const {VueLoaderPlugin}  = require('vue-loader/lib/index')
// 引入模块联邦
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/main.js'
    },
    cache: {
        type: "memory" // filessystem memory
    },
    resolve: {
        extensions: [".vue", ".js", "json"],
        alias: {
            vue$: "vue/dist/vue.esm.js",
            "@": resolve("src"),
            crypto: false,
            stream: false,
            assert: false,
            http: false
        }
        },
    output: {
        path: resolve('./dist'),
        filename: "[name].js",
        chunkFilename: "[name].js"
    },
    devServer: {
        port: 8085,
        // 配置允许跨域
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Method": "GET,POST,PUT,OPTIONS"
        }
    },
    devtool: false,
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "babel-loader",
                        options: {
                            presets: [
                                '@babel/preset-env'
                            ]
                        }
                    }
                ]
            },
            {
                test: /\.vue$/,
                use: [
                    'vue-loader'
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
        new VueLoaderPlugin(),
        new ModuleFederationPlugin({
            name: 'lib_remote',
            filename: 'remoteEntry.js',
            exposes: {
                './list': './src/component/list.vue'
            },
            shared: ['vue']
        })
    ]
}

在lib_app项目中我们导出了一个list.vue组件模块,以便提供给消费方使用,而list..vue组件和普通组件无异,如下在lib_app项目中创建一个list.vue组件;

<template>
  <div class="ul">
    <div class="item" v-for="(item, idx) in list" :key="idx">{{ item }}</div>
  </div>
</template><script>
export default {
  name: "list",
  data() {
    return {
      list: [1,2,3,5]
    }
  }
}
</script><style scoped>
</style>

创建完成后可以跑一下npm run build;看一下构建后的文件目录都生成了什么文件,如下图;

1、remoteEntry.js使我们lib_app导出模块的入口js;

2、src_component_list_vue.js被导出的模块文件;

3、vendors-node_modules_vue_dist_vue_esm_js共享的文件,配置了shared就会有这个文件


②、按照上面的方式创建一个main_app项目,也创建一个webpack.config.js。配置如下;

const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')
const resolve = (p) => path.resolve(__dirname, p)
const {VueLoaderPlugin}  = require('vue-loader/lib/index')
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin')
module.exports = {
    mode: 'development',
    entry: {
        main: './src/main.js'
    },
    cache: {
        type: "memory" // filessystem memory
    },
    output: {
        path: resolve('./dist'),
        filename: "[name].js",
        chunkFilename: "[name].js"
    },
    resolve: {
        extensions: [".vue", ".js", "json"],
        alias: {
            vue$: "vue/dist/vue.esm.js",
            "@": resolve("src"),
            crypto: false,
            stream: false,
            assert: false,
            http: false
        }
    },
    devServer: {
        port: 8086,
        headers: {
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Method": "GET,POST,PUT,OPTIONS"
        }
    },
    devtool: false,
    module: {
        rules: [
            {
                test: /\.js$/,
                exclude: /node_modules/,
                use: [
                    {
                        loader: "babel-loader",
                        options: {
                            presets: [
                                '@babel/preset-env'
                            ]
                        }
                    }
                ]
            },
            {
                test: /\.vue$/,
                use: [
                    'vue-loader'
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './public/index.html'
        }),
        new VueLoaderPlugin(),
        new ModuleFederationPlugin({
            name: "main_app",
            filename: "remoteEntry.js",
            remotes: {
                lib_remote: "lib_remote@http://localhost:8085/remoteEntry.js",
              },
             shared: {
                 vue: {
                     eager: true,
                     singleton: true,
                 }
             }
        })
    ]
}

从配置上可以看出main_app作为消费方,需要在remote里面引入lib_app中的模块lib_remote: "lib_remote@http://localhost:8085/remoteEntry.js",“lib_remote”这个需要和lib_app中配置的name一致,这个是唯一的标识;


③、模块的使用,在main_app中怎么去使用lib_app中导出的list模块呢?只需要动态异步将组件引入即可,import('lib_remote/list')即可使用,代码如下:

<template>
  <div id="app">
    <div class="box">首页</div>
    <search></search>
    <list :list="lists"></list>
  </div>
</template><script>
import search from "@/search/search";
export default {
  name: "App",
  data() {
    return {
      lists: [4,5,6,7,8,9]
    }
  },
  components: {
    search,
    list: () => import('lib_remote/list'),
  }
}
</script><style scoped>
</style>

通过在app.vue中进行异步的引入,就可以使用到lib_app中的组件了;页面效果如下:

至此就实现了简单的模块联邦的实战应用,webpack5中模块联邦并没有限定一个项目只能作为消费方(host)或者提供方(remote);他们是可以共存的,也就是说一个项目既可以是模块消费方也可以是模块的提供方,做到更加的灵活,配置如下,比如我在mian_app中需要导出一个search模块,只要加上相关导出配置即可;

// main_app中的webpack.config.js

new ModuleFederationPlugin({
            name: "main_app",
            filename: "remoteEntry.js",
            exposes: {
                "./search": "./src/search/search.vue"
            },
            remotes: {
                lib_remote: "lib_remote@http://localhost:8085/remoteEntry.js"
            },
            shared: {
                vue: {
                    eager: true,
                    singleton: true,
                }
            }
        })

然后在lib_app中进行配置引入;

// lib_app项目中webpack.config.js

new ModuleFederationPlugin({
            name: 'lib_remote',
            filename: 'remoteEntry.js',
            exposes: {
                './list': './src/component/list.vue'
            },
            remotes: {
                main_app: "main_app@http://localhost:8086/remoteEntry.js"
            },
            shared: ['vue']
        })

五、总结

通过上面的学习我们知道模块联邦其实可以当作是webpack5将需要导出来的组件打包成一个运行时的文件,然后在其他项目可以进行运行时的动态加载,加载的过程是异步的,执行的时候是同步的。根据这个特性我们可以实现一个中心化组件模块中心,然后对外进行模块的分发,如下图:

获取更多技术文章搜索关注公众号:非著名bug认证师

编辑于 2022-03-22 09:47