Loading

[翻译]微前端:关于WebPack 5的Module Federation

[翻译]Micro-frontends: Module Federation with WebPack 5(微前端:关于WebPack 5的Module Federation)

原文地址: https://dev.to/brandonvilla21/micro-frontends-module-federation-with-webpack-5-426

Part 1. 什么是 Module Federation

Module Federation 是一种 Javascript 架构,它允许某一 Javascript 应用程序从另一应用程序(不同的 Webpack 构建)内动态地加载代码

Webpack 常规使用方式

开发者通常使用 Webpack 生成用于生产或开发的包,它帮助开发者生成名为 dist 的文件夹以及其中的 main.js 文件,这些都是你 src 文件夹中所有 Javascript 代码打包输出后的结果

Webpack 产出的 main.js 文件会随着 src 文件夹内逐步增长的代码量而变得繁重。考虑到在生产环境及客户端需要下载该文件,因此逐渐繁重的文件势必会拖慢界面的载入时间

如何在既考量文件捆绑包大小的前提下,又为项目引入新特性就成为我们关心的议题

是否有针对上述问题的解决方法呢?

可以将 main.js 文件分解为体量更小的小文件块,以避免在第一次渲染时需要加载你的所有代码。这称为代码拆分

存在不同的方式来实现上述方案,例如其中一种是在 Webpack 的配置中定义多个入口点,但它也存在一些缺陷,有时在块与块之间存在重复的模块,它们都将被这些块所包含,因此块之间的重复模块会增加最终合并的 chunks 的大小

另一种流行且更被接收的方式是使用符合 ES 提议的 import() 语法进行动态导入

使用方式如下所示

function test() {
  import('./some-file-inside-my-project.js')
    .then(module => module.loadItemsInPage())
    .catch(error => alert('There was an error'))
}

我们可以通过 import() 语法将元素懒加载到页面上,这种方式会创建一个新的 chunk 并将按需引入

如果我告诉你还有另一种方法能够将 main.js 文件分解成不同的 chunk,而且能够分解成不同的项目呢?

这就是 Module Federation/模块聚合的由来

借助于 Module Federation 你可以将远程的 Webpack 构建引入到你的应用程序当中。原先你可以导入这些 chunks 但它们必须来自同一个项目,现在你可以引入不同来源不同项目下的 chunks(通过Webpack 构建)

Module Federation 的运作

为了解释它是怎么一回事,我们将通过一些使用了 ModuleFederationPluginWebpack 配置的代码示例和一些 React.js 代码来阐述

故,我们将使用当前处于 beta 版本的 Webpack 5。以下是 package.json 文件:

// package.json (fragment)

...

  "scripts": {
   "start": "webpack-dev-server --open",
   "build": "webpack --mode production"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/core": "7.10.3",
    "@babel/preset-react": "7.10.1",
    "babel-loader": "8.1.0",
    "html-webpack-plugin": "^4.3.0",
    "webpack": "5.0.0-beta.24",
    "webpack-cli": "3.3.11",
    "webpack-dev-server": "^3.11.0"
  },
  "dependencies": {
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  }

...

上述包含创建 React 应用程序基本配置的所有 Webpack 模块

以下是 webpack.config.js 文件

// webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3000,
  },
    output: {
    publicPath: "http://localhost:3000/",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

上述为常规的 Webpack 配置

让我们往项目内增加一个 React 组件:

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return (
    <h1>Hello from React component</h1>
  )
}

ReactDOM.render(<App />, document.getElementById('root'));

此时,如果运行该项目,你将看到一个页面并附上一行标题 “Hello from React component” 。就目前为止,没有什么特别的地方。

该项目代码到这一步详见:https://github.com/brandonvilla21/module-federation/tree/initial-project

Part 2. 创建第二个项目

现在,我们创建第二个项目使用相同的 package.json 文件,但在 webpack 配置上存在下述部分不同:

const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

// Import Plugin
const { ModuleFederationPlugin } = require('webpack').container;

module.exports = {
  entry: './src/index',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    // Change port to 3001
    port: 3001,
  },
    output: {
    publicPath: "http://localhost:3001/",
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        options: {
          presets: ['@babel/preset-react'],
        },
      },
    ],
  },
  plugins: [
    // Use Plugin
    new ModuleFederationPlugin({
      name: 'app2',
      library: { type: 'var', name: 'app2' },
      filename: 'remoteEntry.js',
      exposes: {
        // expose each component you want 
        './Counter': './src/components/Counter',
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

我们在配置开头就导入 ModuleFederationPlugin

const { ModuleFederationPlugin } = require('webpack').container;

我们需要改变项目端口因为两个项目将在同时运行

port: 3001,

以下是 Plugin 插件配置:

new ModuleFederationPlugin({
  name: 'app2', // 我们需要给它个名字作为识别标识
  library: { type: 'var', name: 'app2' },
  filename: 'remoteEntry.js', // 远程文件名
  exposes: {
    './Counter': './src/components/Counter', // 公开你想要的每个组件 
  },
  shared: ['react', 'react-dom'], // 如果消费者应用程序已经加载了这些库,则不会加载它们两次
}),

上述为配置的主要部分,目的是与第一个项目共享第二个项目的依赖项。

在第一个应用程序内使用第二个应用程序之前,让我们创建 Counter 组件:

// src/components/Counter.js

import React from 'react'

function Counter(props) {
  return (
     <>
       <p>Count: {props.count}</p>
       <button onClick={props.onIncrement}>Increment</button>
       <button onClick={props.onDecrement}>Decrement</button>
     </>
  )
}

export default Counter

这是一个非常常见的例子,关键的重点在于展示我们如何使用该组件并从第一个应用程序内传递某些参数过来

如果此时你尝试运行第二个应用程序并添加基础的 index.js 就如同我们在第一个应用程序下所做的,你可能会收到如下的消息提示:

Uncaught Error: Shared module is not available for eager consumption

就像这个报错所说的,未准备好执行该程序。为了提供加载应用程序的异步方式,我们可以执行以下操作:

创建一个 bootstrap.js 文件并将所有代码从 index.js 迁移至该文件

// src/bootstrap.js

import React from 'react';
import ReactDOM from 'react-dom';

function App() {
  return <h1>Hello from second app</h1>;
}

ReactDOM.render(<App />, document.getElementById('root'));

然后像这样在 index.js 中导入它:(注意我们在这里使用 import() 语法)

// src/index.js

import('./bootstrap')

现在如果你运行第二个项目,你将能够看到页面并显示 ”Hello from second app“

给第一个项目导入 Counter 组件

首先我们需要更新 webpack.config.js 使其能够接受来自第二个应用程序的 Counter 组件

// webpack.config.js (fragment)

...
plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      library: { type: 'var', name: 'app1' },
      remotes: {
        app2: 'app2', // 添加远端 (第二个项目)
      },
      shared: ['react', 'react-dom'],
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
...

Webpack 配置与其他配置之间的区别在于 exposeremote 两个配置项。在第一个应用程序中,我们公开出我们想要从第一个应用程序获取的组件,因此在第二个应用程序里,我们指定了远程应用程序的名称

同样我们也需要从外部引入 remoteEntry.js

<!-- public/index.html (fragment)-->

...
<body>
  <div id="root"></div>
  <script src="http://localhost:3001/remoteEntry.js"></script>
</body>
...

从远程项目导入 React 组件

现在在第一个项目内使用来自第二个项目的 Counter 组件:

// src/bootstrap.js

import React, { useState } from 'react';
import ReactDOM from 'react-dom';

const Counter = React.lazy(() => import('app2/Counter'));

function App() {
  const [count, setCount] = useState(0);
  return (
    <>
      <h1>Hello from React component</h1>
      <React.Suspense fallback='Loading Counter...'>
        <Counter
          count={count}
          onIncrement={() => setCount(count + 1)}
          onDecrement={() => setCount(count - 1)}
        />
      </React.Suspense>
    </>
  );
}

ReactDOM.render(<App />, document.getElementById('root'));

我们需要懒加载 Counter 组件,然后可以使用 React Suspense 加载带有 fallback 回调的组件

你现在应该能够从第一个项目中加载 Counter 组件了

总结

将远程 Webpack 构建加载到你的应用程序中,为一种新的前端架构创造了可能性:

微前端

由于我们可以将独立的 JavaScript 包放入到独立的项目中去,因此我们可以在每个应用程序中使用单独的构建过程。

你将感觉像在开发单一模块一样去开发一个独立的应用程序。这允许大团队分解成更小、更高效的团队,这些团队将从前端团队垂直扩展到后端团队。

通过这种方式,我们将能够保证团队开发相对独立,不依赖他人来交付新功能。

就像下图所示的这样:

在运行时整合系统

目前,有多种方法可以在构建时实现系统整合(npm/yarn 包、GitHub 包、Bit.dev),但这可能引出某些项目问题。每当你需要更新系统中的某些组件时,你都必须重新构建应用程序并再次部署它,以便在生产中使用最新版本的系统。

如果在运行时拉取系统,您将能够将最新版本的系统添加到您的应用程序中,而无需经历整个应用程序的构建和重新部署过程,因为您将在运行时从不同来源获取组件 。

这两个只是联合模块整合系统的一些可能性。

完整示例的存储库

github.com/brandonvilla21/module-federation

Reference

  1. 原文地址
posted @ 2021-12-24 11:22  诀别、泪  阅读(738)  评论(0编辑  收藏  举报