微前端从零到剖析qiankun源码 — Module Federation的玩法!

一.为什么会出现Module Federation

我们用一个图片说明一下关键点

微前端从零到剖析qiankun源码 -- Module Federation的玩法!

  • 每个应用块由不同的组开发TeamA TeamB
  • 应用或应用块共享其他其他应用块或者库React
  • Module Federation的动机是为了不同开发小组间共同开发一个或者多个应用
  • 应用将被划分为更小的应用块,一个应用块,可以是比如头部导航或者侧边栏的前端组件,也可以是数据获取逻辑的逻辑组件

什么是模块联邦?

我们再用一张图说明一下容器主机(host)的关系

微前端从零到剖析qiankun源码 -- Module Federation的玩法!

  • 使用模块联邦,每个部分将是一个单独的构建, 这些构建被编译为容器
  • 容器可以被应用程序或其他容器引用
  • 在这种关系中,容器是远程的,容器的使用者是主机
  • 远程可以将模块公开给主机
  • 主机可以使用此类模块,它们被称为远程模块
  • 通过使用单独的构建,我们可以获得整个系统的良好构建性能
  • 一个被引用的容器被称为remote, 引用者被称为hostremote暴露模块给hosthost则可以使用这些暴露的模块,这些模块被称为remote模块

我们来实际跑起来

我们还有一张图来表示我们的项目之间的关系

微前端从零到剖析qiankun源码 -- Module Federation的玩法!

我们做一个双向依赖的项目,remote本地list组件,依赖hostfrom组件,host本地from组件,依赖remotelist组件。他们共同依赖reactreact-dom

配置参数

字段 类型 含义
name string 必传值,即输出的模块名,被远程引用时路径为name/{name}/{expose}
library object 声明全局变量的方式,name为umd的name
filename string 构建输出的文件名
remotes object 远程引用的应用名及其别名的映射,使用时以key值作为name
exposes object 被远程引用时可暴露的资源路径及其别名
shared object 与其他应用之间可以共享的第三方依赖,使你的代码中不用重复加载同一份依赖

安装

npm i webpack webpack-cli webpack-dev-server babel-loader @babel/core  @babel/preset-env html-webpack-plugin react react-dom -D

host项目的配置

webpack.config.js
let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    publicPath: "http://localhost:3001/",
    clean: true,
    path: path.join(__dirname, 'dist')
  },
  devServer: {
    port: 3001,
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ["@babel/preset-react"]
          },
        },
        exclude: /node_modules/,
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new ModuleFederationPlugin({
      filename: "hostEntry.js",
      name: "host",
      exposes: {
        "./From": "./src/From",
      },
      remotes: {
        remote: "remote@http://localhost:3000/remoteEntry.js"
      }
    })
  ]
}

我们主要讲一下new ModuleFederationPlugin()的操作,主要是暴露一个模块./From指向我们本地的From组件,同时加载一个远程的模块remote@http://localhost:3000/remoteEntry.js

App.jsx

import React from "react";
import From from './From';
const RemotList = React.lazy(() => import("remote/List"));

const App = () => (
  <div>
    <h2>host项目</h2>
    <From />

    <React.Suspense fallback="LoadingList">
      <RemotList />
    </React.Suspense>
  </div>
);

export default App;

import语法webpack是会自动转移成自己的webpack__require__去加载远程模块的。

remote项目的配置

let path = require("path");
let webpack = require("webpack");
let HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    publicPath: "http://localhost:3000/",
    clean: true,
    path: path.join(__dirname, 'dist')
  },
  devServer: {
    port: 3000,
    static: {
      directory: path.join(__dirname, 'dist'),
    },
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ["@babel/preset-react"]
          },
        },
        exclude: /node_modules/,
      },
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html'
    }),
    new ModuleFederationPlugin({
      filename: "remoteEntry.js",
      name: "remote",
      exposes: {
        "./List": "./src/List",
      },
      remotes: {
        remote: "host@http://localhost:3001/hostEntry.js"
      }
    })
  ]
}

Remot项目App主入口

import React from "react";
import List from './List';

const HostFrom = React.lazy(() => import("remote/From"));

const App = () => (
  <div>
    <h2>Remot项目</h2>
    <List />

    <React.Suspense fallback="LoadingList">
      <HostFrom />
    </React.Suspense>
  </div>
)

export default App;

页面展示

微前端从零到剖析qiankun源码 -- Module Federation的玩法!

Module Federation 在微前端的用法

之前我们在为什么需要微前端中讲过目前国内微前端方案大概分为:

  • 基座模式:通过搭建基座、配置中心来管理子应用。如基于SIngle Spa的偏通用的qiankun方案,也有基于本身团队业务量身定制的方案。
  • 自组织模式: 通过约定进行互调,但会遇到处理第三方依赖等问题。
  • 去中心模式: 脱离基座模式,每个应用之间都可以彼此分享资源。如基于Webpack 5 Module Federation实现的EMP微前端方案,可以实现多个应用彼此共享资源分享。

那么基于Webpack 5 Module Federation的微前端的解决方案,旧的项目的迁移成本就比较高了。对于一个大型的工程来说,需要进行按照项目目录或者功能点的拆分的时候,可能基座模式模式成本更小、更易上手。而去中心模式的优势在于,过多的项目同时有大量的公用性组件或者逻辑时,Module Federation便能很好的彼此共享资源。

EMP 微前端方案

不啰嗦,我们直接上成果图

微前端从零到剖析qiankun源码 -- Module Federation的玩法!

使用react-base去加载了reace-projecthello组件,这里大家可以直接看EMP微前端方案,但是我看文档不是很全,vue的项目对接方案是缺失的。
下面是配置emp的关键点:

我们对比react-basereact-project项目的配置,看到react-baseemp.config.js配置文件中声明分享了一个Hello组件:

exposes: {
  // 别名:组件的路径
  './components/Hello': 'src/components/Hello',
},

react-projectemp.config.js配置文件中声明需要使用react-base项目的Hello组件:

remotes: {
  // 远程项目别名:远程引入的项目名
  '@emp/react-base': 'empReactBase',
},

并且react-project在项目代码中,引入使用了react-base项目的Hello组件:

import HelloDEMO from '@emp/react-base/components/Demo'
const App = () => (
  <>
    // ...
     <HelloDEMO />
    // ...
  </>
)

那么,我们跟着指令分别跑起react-basereact-project项目就可以看到上面的成果图了:

cd react-base && yarn && yarn dev
cd react-project && yarn && yarn dev

总结一下

  • Module Federation对于无限嵌套支持较好
  • Module Federation对于老项目不太友好,需要升级对应的webpack
  • single-spa一样,不支持js沙箱,需要自己实现
  • 第一次需要将引用的依赖前置,会导致加载时间变长的问题
  • 对于大型拆分应用不太友好,对于多应用共享很友好。

写在最后:下一篇我们将讲single-spa的前置知识《微前端从零到剖析qiankun源码 -- single-spa玩法!》

原文链接:https://juejin.cn/post/7212901252014243896 作者:顾昂_

(0)
上一篇 2023年3月21日 下午7:28
下一篇 2023年3月21日 下午7:38

相关推荐

发表回复

登录后才能评论