webpack5 Module Federation "微前端"vue版demo使用示例及实现细节

2,094 阅读3分钟

webpack5 Module Federation "微前端"vue版demo使用示例及实现细节

最近有个需求,考虑用到“微前端”。了解了一番,此文作个整理记录

Module Federation:是webpack5新出的一种“微前端”的概念,此文介绍一下具体的实际操作 vue版

demo内关系介绍:

关系:app1对外暴露“微服务”,app2使用app1的“微服务”

Module-Federation.png

上代码

目录结构

.
├── README.md
├── app1  // app1对外暴露“微服务”
│   ├── package.json
│   ├── public
│   │   └── index.html
│   ├── src
│   │   ├── App.vue
│   │   ├── components
│   │   │   └── Header.vue
│   │   └── main.js
│   └── webpack.config.js
└── app2   // app2使用app1的“微服务”
    ├── package.json
    ├── public
    │   └── index.html
    ├── src
    │   ├── App.vue
    │   └── main.js
    └── webpack.config.js

app1和app2相同部分代码(环境)

  • package.json相同
  • src/main.js相同
  • public/index.html相同
// package.json
{
  "scripts": {
    "start": "webpack serve",
    "build": "webpack"
  },
  "devDependencies": {
    "vue-loader": "^15.9.3",
    "vue-template-compiler": "^2.6.12",
    "@babel/core": "^7.14.3",
    "babel-loader": "^8.2.2",
    "html-webpack-plugin": "^5.3.1",
    "webpack": "^5.38.1",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2"
  },
  "dependencies": {
    "vue": "^2.6.12"
  }
}

// src/main.js
import Vue from "vue";
import App from "./App.vue";

new Vue({
    el: '#app',
    render: h => h(App)
})

// public/index.html
<html>
<head></head>
<body>
<div id="app"></div>
</body>
</html>

app1 的组件代码

  • App.vue
  • components/Header.vue
// App.vue
<template>
  <div id="app" style="border: 1px solid cornflowerblue">
    <h2>我是app.vue</h2>
    <Header name="app1"/>
  </div>
</template>
<script>
import Header from './components/Header.vue'
export default {
  components: {
    Header
  }
}
</script>

// components/Header.vue
<template>
    <div style="border: 1px solid olivedrab">
        <h4>我是header.vue, 这是传参:{{name}}</h4>
    </div>
</template>
<script>
export default {
    props: {
        name: {
            type: String,
            default: ''
        }
    }
}
</script>

app2 的组件代码

  • App.vue 内 使用了app1的“微服务”,支持props传参
// App.vue
<template>
  <div id="app">
    <Header name="app222222"/> // 此处 app2使用app1的“微服务”,并支持props传参
    <br>
    <qwe></qwe> // 此处 app2使用app1的“微服务”
  </div>
</template>
<script>

export default {
  components: {
    Header: () => import('app1/Header'), // 此处 app2使用app1的“微服务”
    qwe: () => import('app1/appIndex') // 此处 app2使用app1的“微服务”
  }
}
</script>

实现关键:webpack.config.js

app1的webpack.config.js (对外暴露“微服务”)

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
    target: 'web',
    entry: './src/main.js',
    mode: "development",
    devServer: {
      port: 3000,
      hot: true,
      open: true,
      contentBase: path.join(__dirname, "dist"),
    },
    module: {
        rules: [
            {
                test: /.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
          {
            test: /.vue$/,
            loader: 'vue-loader'
          }
        ]
    },
    plugins: [
        // 请确保引入这个插件!
        new VueLoaderPlugin(),
        new HTMLWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        }),
        new ModuleFederationPlugin({
            // 提供给其他服务加载的文件
            filename: "remoteEntry.js",
            // 唯一ID,用于标记当前服务
            name: "app1",
            library: { type: "var", name: "app1" },
            // 需要暴露的模块,使用时通过 `${name}/${expose}` 引入
            exposes: {
              './Header': "./src/components/Header.vue", // app1对外暴露“微服务”Header(组件)
              './appIndex': "./src/App.vue", // app1对外暴露“微服务”appIndex(组件)
            }
          })
      ]
}

app2的webpack.config.js (接受app1的“微服务”(组件))

const path = require('path');
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const HTMLWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require("webpack").container;

module.exports = {
    target: 'web',
    entry: './src/main.js',
    mode: "development",
    devServer: {
      port: 3001,
      hot: true,
      open: true,
      contentBase: path.join(__dirname, "dist"),
    },
    module: {
        rules: [
            {
                test: /.js$/,
                loader: 'babel-loader',
                exclude: /node_modules/
            },
          {
            test: /.vue$/,
            loader: 'vue-loader'
          }
        ]
    },
    plugins: [
        // 请确保引入这个插件!
        new VueLoaderPlugin(),
        new HTMLWebpackPlugin({
            template: path.resolve(__dirname, './public/index.html')
        }),
        new ModuleFederationPlugin({
          name: "app2",
          remotes: {
            app1: "app1@http://localhost:3000/remoteEntry.js", // (接受app1的“微服务”),remoteEntry可以理解为中间代理人)
          }
        })
      ]
}

效果展示

app1要先npm run start起来

然后app2 才能 npm run start起来

Module-Federation-display.png

可以看到app2内,可以使用app1的对外暴露的“微服务”(组件)

实现细节

(这里面对比上面的代码,多加了moment.js库依赖) 微服务入口.png

此图中: 端口3001是当前服务,使用了端口3000的微服务

细节解析:

  1. 3001服务引入微服务入口 remoteEntry.js,通过remoteEntry.js去获取对应的依赖
  2. 对应的依赖有:组件和第三方库
    • 每一个组件都会是一个独立的文件,然后第三方依赖也会单独拆出来,防止重复打包
    • 上图中
      • 倒数的两个资源都是用了moment方法的组件,也被3000对外暴露微服务了。此处作为组件引入3001服务内
      • 倒数第三个是 第三方依赖 目前主要是moment.js (此处应当按需加载优化,不应该整个moment.js都加载进来了)

另外还做了一个测试:3000的微服务做了变更后,3001服务不用变动,强制刷新就能生效

  • 因为,这个是通过域名,获取到对应的资源。相当于直接拉服务器的静态资源

码字不易,点赞鼓励