JavaScript内核与高级编程之:`JavaScript`的`Micro Frontends`:`Webpack`的`Module Federation`实现。

各位观众老爷,晚上好!今天咱们聊点儿时髦的,关于前端架构的——微前端。尤其是,用Webpack的Module Federation这个“大杀器”来实现微前端。这玩意儿,说白了,就是把一个巨无霸前端应用拆成一堆小而美的应用,然后像搭积木一样拼起来。

开场白:为什么要微前端?

想象一下,你有一个巨大的单体前端应用,代码库像一座喜马拉雅山,每次改动都像攀登珠穆朗玛峰,发布一次都要战战兢兢,生怕雪崩。新团队加入,光是熟悉代码就要耗费几个月。这,就是单体应用的痛。

微前端,就是为了解决这个问题。它可以:

  • 解耦业务: 不同团队负责不同的业务模块,互不干扰,开发效率更高。
  • 技术栈自由: 每个微应用可以使用不同的技术栈,老项目可以逐步迁移,新项目可以拥抱新技术。
  • 独立部署: 每个微应用可以独立部署,快速迭代,减少发布风险。
  • 提升可维护性: 小而美的代码库,更容易维护和测试。

主角登场:Webpack Module Federation

Module Federation,是Webpack 5推出的一个重量级特性。它允许不同的Webpack构建的应用,共享彼此的代码,就像共享DLL一样。但是,它比DLL更灵活,更动态。

Module Federation的核心概念:

  • Host (容器应用): 负责加载和管理微应用。
  • Remote (微应用): 对外暴露可共享的模块。
  • Shared Modules (共享模块): 被Host和Remote共享的依赖。

实战演练:搭建一个简单的微前端应用

咱们来撸起袖子,搭一个简单的微前端应用。这个应用包含一个Host应用,和两个Remote应用(app1和app2)。

1. 项目初始化

首先,创建项目目录:

mkdir micro-frontend
cd micro-frontend
mkdir host app1 app2

然后,在每个目录下,初始化一个Webpack项目:

cd host
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save

cd ../app1
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save

cd ../app2
npm init -y
npm install webpack webpack-cli webpack-dev-server html-webpack-plugin --save-dev
npm install react react-dom --save

cd ..

2. Host应用 (host/webpack.config.js)

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 8080,
  },
  output: {
    publicPath: 'http://localhost:8080/',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'host',
      remotes: {
        app1: 'app1@http://localhost:8081/remoteEntry.js',
        app2: 'app2@http://localhost:8082/remoteEntry.js',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.8.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.8.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

代码解释:

  • name: 当前应用的名称,必须唯一。
  • remotes: 定义需要加载的微应用。app1@http://localhost:8081/remoteEntry.js 表示从 http://localhost:8081/remoteEntry.js 加载名为 app1 的微应用。remoteEntry.js 是微应用暴露的入口文件。
  • shared: 定义共享的依赖。singleton: true 确保只加载一个版本的React和ReactDOM。requiredVersion: '>=16.8.0' 指定需要的版本范围。

3. Host应用 (host/src/index.js)

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

import App from './App';

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

4. Host应用 (host/src/App.js)

import React, { Suspense } from 'react';

const App1 = React.lazy(() => import('app1/App'));
const App2 = React.lazy(() => import('app2/App'));

const App = () => (
  <div>
    <h1>Host Application</h1>
    <Suspense fallback={<div>Loading App1...</div>}>
      <App1 />
    </Suspense>
    <Suspense fallback={<div>Loading App2...</div>}>
      <App2 />
    </Suspense>
  </div>
);

export default App;

代码解释:

  • React.lazy: 用于懒加载微应用。只有当组件被渲染时,才会加载对应的代码。
  • Suspense: 用于显示加载状态。当微应用正在加载时,会显示 fallback 中的内容。

5. Host应用 (host/public/index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Host Application</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

6. Remote应用 (app1/webpack.config.js)

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  entry: './src/index.js',
  mode: 'development',
  devServer: {
    port: 8081,
  },
  output: {
    publicPath: 'http://localhost:8081/',
  },
  module: {
    rules: [
      {
        test: /.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-react', '@babel/preset-env'],
          },
        },
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      exposes: {
        './App': './src/App',
      },
      shared: {
        react: { singleton: true, requiredVersion: '>=16.8.0' },
        'react-dom': { singleton: true, requiredVersion: '>=16.8.0' },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};

代码解释:

  • name: 当前应用的名称,必须唯一。
  • exposes: 定义需要暴露的模块。'./App': './src/App' 表示将 src/App.js 暴露为 App 模块。
  • shared: 定义共享的依赖。

7. Remote应用 (app1/src/index.js)

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

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

8. Remote应用 (app1/src/App.js)

import React from 'react';

const App = () => (
  <div>
    <h2>App1</h2>
    <p>This is App1 content.</p>
  </div>
);

export default App;

9. Remote应用 (app1/public/index.html)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>App1</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

10. Remote应用 (app2/webpack.config.js), (app2/src/index.js), (app2/src/App.js), (app2/public/index.html)

App2 的配置和代码与 App1 类似,只需将端口号改为 8082,应用名称改为 app2,内容稍作修改即可。这里不再赘述,节省篇幅。

11. 添加 Babel 配置 (host, app1, app2 目录下都添加 .babelrc)

{
  "presets": ["@babel/preset-env", "@babel/preset-react"]
}

12. package.json 添加启动命令 (host, app1, app2 目录下都添加)

  "scripts": {
    "start": "webpack serve"
  },

13. 运行项目

分别在 host, app1, app2 目录下运行 npm start

然后,在浏览器中打开 http://localhost:8080,你将会看到 Host 应用,其中包含了 App1 和 App2 的内容。

代码总结:

文件名 描述
host/webpack.config.js Host 应用的 Webpack 配置,定义了 remotesshared
host/src/App.js Host 应用的 React 组件,使用 React.lazySuspense 加载微应用。
app1/webpack.config.js App1 应用的 Webpack 配置,定义了 exposesshared
app1/src/App.js App1 应用的 React 组件,对外暴露。
.babelrc (host, app1, app2 目录下) Babel 配置文件,定义了预设。

Module Federation 的高级用法

上面的例子只是一个入门级的演示,Module Federation 的功能远不止于此。

  • 版本控制: 可以指定共享依赖的版本范围,避免版本冲突。
  • 动态加载: 可以根据需要动态加载微应用,提高性能。
  • 代码分割: 可以将微应用的代码分割成多个 chunk,按需加载。
  • 自定义加载器: 可以自定义加载器,加载不同类型的模块。

踩坑指南:Module Federation 的常见问题

  • 版本冲突: 共享依赖的版本不一致,导致运行时错误。解决办法是明确指定版本范围,并使用 singleton: true 确保只加载一个版本。
  • 循环依赖: 微应用之间存在循环依赖,导致加载失败。解决办法是重新设计模块依赖关系,避免循环依赖。
  • 类型定义: 如果微应用使用了 TypeScript,需要共享类型定义,否则会出现类型错误。解决办法是使用 declaration: true 生成类型定义文件,并共享这些文件。
  • CORS 问题: 如果微应用部署在不同的域名下,可能会遇到 CORS 问题。解决办法是在服务器端配置 CORS 策略。
  • 性能问题: 加载过多的微应用会影响性能。解决办法是优化代码,减少依赖,并使用懒加载和代码分割。

微前端的架构模式

除了 Module Federation,还有其他的微前端架构模式,例如:

  • Iframe: 最简单的微前端实现方式,但隔离性太强,通信成本高。
  • Web Components: 可以使用 Web Components 将微应用封装成自定义元素,然后在 Host 应用中使用。
  • Single-SPA: 一个专门用于构建微前端应用的框架,提供了路由、生命周期管理等功能。
  • Qiankun: 阿里开源的微前端框架,基于 Single-SPA,提供了更完善的功能和更易用的 API。

各种方案的对比:

方案 优点 缺点 适用场景
Iframe 隔离性强,技术栈无关。 通信成本高,SEO 不友好,用户体验差。 老项目改造,技术栈差异大,对隔离性要求高的场景。
Web Components 可以将微应用封装成自定义元素,易于集成。 需要浏览器支持 Web Components,学习成本高。 新项目,技术栈统一,对组件化要求高的场景。
Single-SPA 提供了路由、生命周期管理等功能,功能完善。 学习成本高,需要对 Single-SPA 的原理有深入了解。 中大型项目,需要统一的微前端框架。
Qiankun 基于 Single-SPA,提供了更完善的功能和更易用的 API,上手快。 依赖 Single-SPA,需要了解 Single-SPA 的原理。 中大型项目,需要快速搭建微前端应用。
Module Federation 代码共享,性能好,易于集成。 需要使用 Webpack 5,对 Webpack 的配置有一定要求,共享模块需要仔细管理。 新项目,技术栈统一,对性能要求高的场景。

总结:微前端的未来

微前端是一种非常有前景的前端架构模式,它可以解决单体应用的痛点,提高开发效率和可维护性。虽然微前端还面临一些挑战,例如版本管理、状态管理、通信等,但随着技术的发展,这些问题将会得到解决。

Module Federation 作为一种新兴的微前端实现方式,具有代码共享、性能好、易于集成等优点,越来越受到开发者的青睐。相信在不久的将来,Module Federation 将会在微前端领域发挥更大的作用。

好了,今天的讲座就到这里,希望对大家有所帮助。感谢各位的观看!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注