博客示例demo已经上传gitee
Module Federation 可以实现微前端的效果,只是它是模块级的。即一个应用可以引入另外一个应用的模块。例如在a域名下启动的应用里,可以直接引用b域名应用里的资源。也就是说a应用可以异步获取b应用的组件进行使用。

这样就可以将应用分为更小的应用块,头部导航栏、侧边栏、业务逻辑组件都可以分到不同的应用块里开发,同时应用块间间可共享,互相依赖,可实时获取其它应用块构建好的bundle资源。

在入门前,先对一些概念达成共识:

在Module Federation中,每个应用都是一个独立的构建(webpack工程),称为容器。引用远程模块的应用叫作host,被引用模块所在的应用称为remote。

用到的插件:html-webpack-plugin、vue-loader、vue-template-compiler(需要和vue版本一致)、vue-style-loader、css-loader、webpack、webpack-cli、webpack-dev-server
启动webpack服务可使用:npx webpack serve --open

基础使用

一、配置remote应用,模块联邦插件会根据配置打包出对应的资源。

如下配置会构建向外暴露的三个组件对应的bundle,并暴露给其它应用。如果remote应用自身的入口文件里引用了这三个组件,会按本地依赖关系进行打包,不会与模块联邦共享。也就是说这三个组件会被打包两次,一次是根据模块联邦的配置,另一次是根据自身依赖关系。

remote应用只要在注册ModuleFederationPlugin插件时,定义三个参数

  • name(向外暴露容器名称)
  • filename(连接本容器的入口文件,包含暴露出去组件的及它们的依赖信息)
  • exposes:向外暴露的组件
// remote应用的webpack.config.js配置
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
// webpack5中配置模块联邦的插件
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        clean: true,
        // publicPath: 'http://localhost:3001/' 
    },
    devServer: {
        port: '3001'
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new VueLoaderPlugin(), // 处理vue文件
        new ModuleFederationPlugin({
            name: 'remote_app', // 向外暴露容器名称
            filename: 'remote_index.js', // 连接本容器的入口文件,包含暴露出去组件的及它们的依赖信息
            exposes: {
                // 这里暴露了三个vue组件
                // 它们依赖vue、lodash、本地其它组件
                // 格式:{remote组件访问路径} :{本地文件路径}
                './MyTable': './src/com/myTable.vue',
                './MyTopBar': './src/com/top-bar.vue',
                './MyText': './src/com/text.vue'
            },
        })
    ],
    experiments: { 
        // 模块中可在一级作用域直接使用await import('xxx')
        topLevelAwait: true 
    },
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            }
        ]
    },
    
}

二、host应用配置远程组件地址

host端同样需要注册ModuleFederationPlugin插件,只要定义remotes参数来接入远程应用即可。
也可像remote应用那样加上name、filename、exposes参数来向外暴露组件

// host应用的webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { VueLoaderPlugin } = require('vue-loader');
// webpack5中配置模块联邦的插件,也可以解构导入
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        clean: true
    },
    devServer: {
        port: 3000
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: './index.html'
        }),
        new VueLoaderPlugin(),
        new ModuleFederationPlugin({
            remotes: {
                // {远程app本地别名}: {远程app配置的name}@远程app根地址/{远程app配置的filename}}
                // remote_index.js里包含的组件会用到的所有第三方依赖
                // ,这些依赖可能拆分多个文件,具体看远程容器的webpack配置
                'remote-app': 'remote_app@http://localhost:3001/remote_index.js',
            },
        })
    ],
    module: {
        rules: [
            {
                test: /\.vue$/,
                loader: 'vue-loader'
            },
            {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            }

        ]
    }
}

三、在host里使用远程组件,像异步组件一样使用

远程组件的导入路径由两部分组成:

import('remote-app/MyTopBar')
  1. 一是host应用中,new模块联邦插件时,定义的remotes参数里,对应的远程应用地址的key,如remote-app
  2. 二是remote应用中,new模块联邦插件时,定义的exposes参数里,对应的模块key,如./MyTable
<!-- 
// host:app.vue
// 远程组件对应的bundle需要在运行时被加载
// 所以要通过动态引入,地址格式:
// import('{远程应用本地别名}/{远程应用exposes的key}')
-->
<template>
    <div>
        <reomteTable></reomteTable>
        <hostHellow></hostHellow>
    </div>
</template>

<script>
// 引入host应用本地的两个组件
import hostLoading from './com/loading.vue';
import hostError from './com/Error.vue';

export default {
    components: {
    	// 引入本地异步组件
    	hostHellow: () => import('./com/hellow.vue'),
    	// 引入远程组件,和引入异步组件(动态导入的模块)语法类似
    	// 路径里remote-app是host里配置的,MyTopBar是remote里配的
        remoteTopBar: () => import('remote-app/MyTopBar'), 
        reomteTable: () => ({
            // 远程组件和本地异步组件使用上无差别
            component: import('remote-app/MyTable'),
            // 异步组件加载时使用的组件
            loading: hostLoading,
            // 展示加载时组件的延时时间。默认值是 200 (毫秒)
            delay: 200,
            // 加载失败时使用的组件
            error: hostError,
            // 如果提供了超时时间且组件加载也超时了,
            // 则使用加载失败时使用的组件。默认值是:`Infinity`
            timeout: 3000
        }),
        
    }
}
</script>

到这里已经可以使用远程组件了,但还可以继续优化:

抽离公共依赖

远程组件依赖了vue和lodash,这两个依赖会被打包进remote应用的remote_index.js里,当host引入远程组件,会加载加载remote应用生成的remote_index.js文件,如果host本地也依赖了vue、lodash,它们会被加载两次

remote如果通过缓存组等配置把vue、lodash打包到了独立bundle,remote_index.js就不会把它们打包进来,而是remote_index.js在host被加载后,向remote请求这些依赖的bundle

在host、remote应用里,同时配置插件的shared字段,可避免依赖重复加载。这里配置singleton为true,把vue等依赖打包到独立的bundle里,被host、remote两个应用加载:

一、remote的模块联邦插件配置

只要在ModuleFederationPlugin插件多定义shared参数即可

// webpack.config.js
new ModuleFederationPlugin({
            name: 'remote_app', 
            filename: 'remote_index.js', 
            exposes: {
                './MyTable': './src/com/myTable.vue',
                './MyTopBar': './src/com/top-bar.vue',
                './MyText': './src/com/text.vue'
            },
            // 暴露出去的组件依赖了vue、lodash,将vue、lodash独立打包,
            // 与引用这些组件的host容器共享这些依赖,避免多次加载
            shared: {
                vue: {
                    singleton: true,
                },
                lodash: {
                    singleton: true,
                }
            }
        })

二、host的模块联邦插件配置

同样添加shared参数即可

// webpack.config.js
new ModuleFederationPlugin({
            remotes: {
                'remote-app': 'remote_app@http://localhost:3001/remote_index.js',
            },
            shared: {
                // 远程和本地主机都要设置一样的
                vue: {
                    singleton: true, 
                },
                lodash: {
                    singleton: true,
                }
            }
        })

三、vue等依赖改为动态引入

无论host还是remote应用,vue等依赖只要配置了shared的 singleton: true后,这些依赖会被独立打包。

它的弊端在于:

会直接影响本地代码对这些依赖的引入方式,本地代码是无法再静态导入这些依赖的,只能动态导入,如下代码会报错:

// 入口文件 index.js
// 如果这样引入vue会报错:Shared module is not available for eager consumption
// 这里可以是remote的或者host的,因为两个都配置了singleton: true
import Vue form 'vue'
new Vue({ 
    // ...
})

解决:

把应用的入口文件代码抽离到bootstrap.js里,入口文件再动态引入bootstrap.js。

这样,整个应用都变成了异步导入,原本需要动态引入的依赖可以直接静态引入

  1. 新建bootstrap.js(可叫其它名字)

    // 静态导入vue并初始化等
    import Vue from 'vue';
    import App from './app.vue';
    // Vue.config.productionTip = false
    new Vue({
      render: h => h(App),
    }).$mount('#app')
    
  2. 入口文件动态引入bootstrap.js模块

    // index.js
    import('./bootstrap.js')
    console.log('index 入口文件');
    
  3. 这里vue等依赖可正常加载了,不再报开头的错,同时bootstrap.js及其导入的依赖里,都可以直接使用静态导入的方式,加载异步组件(动态导入)

    // 以bootstrap.js引入的app.vue为例
    // 例如:remoteTopBar = ()=>import('remote-app/MyTopBar') 
    // 可改成 import remoteTopBar from 'remote-app/MyTopBar'
    <template>
        <div>
            <remoteTopBar></remoteTopBar>
            <hostHellow></hostHellow>
        </div>
    </template>
    
    <script>
    import remoteTopBar from 'remote-app/MyTopBar';
    // 动态引入依旧可以使用
    // const remoteTopBar = () => import('remote-app/MyTopBar');
    
    export default {
        components: {
            remoteTopBar, 
            // 本地动态组件
            hostHellow: () => import('./com/hellow.vue')
        }
    }
    </script>
    
Logo

前往低代码交流专区

更多推荐