阐述 JavaScript Monorepo 架构下,如何利用 Webpack Module Federation 或其他工具实现 JavaScript 模块的共享、版本兼容和按需加载。

各位朋友,大家好! 欢迎来到今天的“Monorepo 模块共享与 Webpack Module Federation 漫谈”讲座。 今天,咱们不搞那些玄乎其玄的概念,也不拽那些听不懂的术语,就用大白话聊聊 Monorepo 架构下,如何像搭积木一样,灵活地共享、管理 JavaScript 模块,以及如何让它们在不同应用之间和谐共处。

一、Monorepo 架构:代码的“大杂烩”与“集约化”

想象一下,你是一家大型软件公司的架构师,手底下管理着十几个甚至几十个项目。传统的做法是,每个项目一个独立的仓库,各自为战。时间一长,你会发现:

  • 代码重复: 同一个组件、同一个工具函数,在不同项目里复制粘贴,维护起来简直是噩梦。
  • 依赖地狱: 每个项目都有自己的依赖版本,升级一个依赖,可能要改动十几个项目的代码,想想都头大。
  • 协作困难: 修改一个底层模块,需要同步更新所有依赖它的项目,沟通成本高到爆炸。

这时候,Monorepo 就闪亮登场了。简单来说,Monorepo 就是把所有项目代码都放在同一个仓库里。 这听起来有点像把所有鸡蛋放在同一个篮子里,但实际上,它带来的好处远大于风险。

  • 代码复用: 所有项目共享代码,减少重复,提高开发效率。
  • 依赖统一: 统一管理依赖版本,避免依赖冲突,方便升级。
  • 协作便捷: 修改一个模块,可以一次性更新所有相关项目,减少沟通成本。
  • 原子性变更: 一次提交可以修改多个相关的 Package,保证功能的完整性。

当然,Monorepo 也有一些挑战,比如构建速度慢、代码可见性控制等。 但是,通过合理的工具和策略,这些挑战都可以迎刃而解。

二、Webpack Module Federation:模块共享的“魔法棒”

Module Federation 是 Webpack 5 引入的一个强大特性,它可以让你像搭积木一样,在不同的 Webpack 构建之间共享模块。 简单来说,就是让一个应用可以“消费”另一个应用的模块,而无需重新构建。

1. 基本概念

  • Host: 宿主应用,也就是“消费”其他应用模块的应用。
  • Remote: 远程应用,也就是“提供”模块的应用。
  • Shared Modules: 被 Host 和 Remote 共享的模块,例如 reactreact-dom等。

2. 配置详解

咱们先来看一个简单的例子。假设我们有两个应用:app1app2app1 是 Host,它要“消费” app2 提供的模块。

  • app1 (Host) 的 webpack.config.js
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1', // 必须唯一
      remotes: {
        app2: 'app2@http://localhost:3002/remoteEntry.js', // app2 的名字和 remoteEntry.js 的地址
      },
      shared: {
        react: { singleton: true, eager: true, requiredVersion: "18.0.0" },
        "react-dom": { singleton: true, eager: true, requiredVersion: "18.0.0" },
      },
    }),
  ],
};
  • name: 这个是必须的, 模块联邦每一个应用都需要设置,保证唯一性, 可以理解为模块的名字。

  • remotes: 远程模块配置,告诉Host去哪里寻找远程模块。app2 是远程模块的名字,app2@http://localhost:3002/remoteEntry.js 是远程模块的地址。

  • shared: 共享依赖配置,告诉 Webpack 哪些模块需要在 Host 和 Remote 之间共享。 singleton: true 表示只使用一个实例, eager: true 表示立即加载,requiredVersion表示需要的版本。

  • app2 (Remote) 的 webpack.config.js

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');

module.exports = {
  // ... 其他配置
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2', // 必须唯一
      filename: 'remoteEntry.js', // 暴露的文件名
      exposes: {
        './Button': './src/Button', // 暴露的模块
      },
      shared: {
        react: { singleton: true, eager: true, requiredVersion: "18.0.0" },
        "react-dom": { singleton: true, eager: true, requiredVersion: "18.0.0" },
      },
    }),
  ],
};
  • name: 这个是必须的, 模块联邦每一个应用都需要设置,保证唯一性, 可以理解为模块的名字。
  • filename: 暴露的文件名,一般是 remoteEntry.js
  • exposes: 暴露的模块配置,告诉 Webpack 哪些模块需要暴露给其他应用使用。 './Button' 是模块的名字,'./src/Button' 是模块的路径。
  • shared: 共享依赖配置,告诉 Webpack 哪些模块需要在 Host 和 Remote 之间共享。 singleton: true 表示只使用一个实例, eager: true 表示立即加载,requiredVersion表示需要的版本。

3. 使用方法

app1 中,你可以这样使用 app2 提供的 Button 组件:

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

import Button from 'app2/Button'; // 注意这里的路径

function App() {
  return (
    <div>
      <h1>App1</h1>
      <Button text="Click me from App2!" />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

注意,这里的 import Button from 'app2/Button'; 路径就是 app2webpack.config.jsexposes 字段配置的。

4. 动态加载

有时候,我们并不需要一开始就加载所有模块,而是希望按需加载。 Module Federation 也支持动态加载。

import React, { useState, useEffect } from 'react';

function App() {
  const [RemoteButton, setRemoteButton] = useState(null);

  useEffect(() => {
    const loadRemoteButton = async () => {
      try {
        const Button = await import('app2/Button');
        setRemoteButton(() => Button.default); // 假设 Button 是默认导出
      } catch (error) {
        console.error('Failed to load remote button:', error);
      }
    };

    loadRemoteButton();
  }, []);

  return (
    <div>
      <h1>App1</h1>
      {RemoteButton ? <RemoteButton text="Click me from App2 (Dynamically Loaded)!" /> : <p>Loading...</p>}
    </div>
  );
}

5. 版本兼容

Module Federation 通过 shared 字段来管理共享依赖的版本。 如果 Host 和 Remote 使用的依赖版本不一致,Webpack 会自动选择一个兼容的版本。

  • 精确匹配: 最好让 Host 和 Remote 使用相同的依赖版本。
  • 版本范围: 可以使用版本范围,例如 react: { requiredVersion: "^17.0.0" },表示允许使用 17.x.x 版本的 React。

三、Monorepo 中的 Module Federation:最佳实践

在 Monorepo 架构下,使用 Module Federation 可以更方便地共享和管理模块。

1. 统一依赖管理

使用 Monorepo 工具(如 Lerna、Yarn Workspaces、Nx 等)统一管理所有项目的依赖版本。 这样可以避免依赖冲突,提高代码复用率。

2. 模块划分

合理划分模块,将通用组件、工具函数等放在独立的模块中,方便共享。

3. 代码可见性控制

使用 Monorepo 工具提供的代码可见性控制功能,限制模块的访问权限。 例如,只允许特定的项目访问某个模块。

4. 构建优化

使用 Webpack 的缓存、代码分割等功能,优化构建速度。

5. 自动化部署

使用 CI/CD 工具自动化部署,确保每次代码提交都能及时更新。

四、其他模块共享方案

除了 Webpack Module Federation,还有一些其他的模块共享方案。

方案 优点 缺点 适用场景
Webpack Module Federation 动态加载、版本兼容、配置灵活 配置复杂、学习成本高 微前端架构、大型单页应用
Bit 组件共享、版本控制、文档生成 依赖 Bit 平台 组件库开发、团队协作
npm/yarn link 方便本地开发调试 不适合生产环境 本地开发调试
Git Submodules 可以将其他仓库的代码嵌入到当前仓库中 管理复杂、容易出错 不推荐使用
共享 npm 包 将模块发布到 npm 仓库,然后安装到其他项目中 需要发布到 npm 仓库、版本管理复杂 小型项目、公共库

五、Monorepo 工具的选择

在 Monorepo 架构中,选择合适的工具至关重要。 常见的 Monorepo 工具包括:

  • Lerna: 老牌 Monorepo 工具,功能强大,但配置相对复杂。
  • Yarn Workspaces: Yarn 内置的 Monorepo 功能,配置简单,易于上手。
  • Nx: 功能最强大的 Monorepo 工具,支持多种框架和语言,提供丰富的插件和工具。

选择哪个工具,取决于你的项目规模、技术栈和团队经验。

六、实战演练:一个简单的 Monorepo 示例

为了让大家更好地理解 Monorepo 架构和 Module Federation,咱们来做一个简单的示例。

  1. 初始化 Monorepo
mkdir my-monorepo
cd my-monorepo
yarn init -y
yarn add -D lerna
  1. 创建 packages 目录
mkdir packages
  1. 创建 app1 和 app2 目录
mkdir packages/app1
mkdir packages/app2
cd packages/app1
yarn init -y
cd ../app2
yarn init -y
cd ../..
  1. 配置 lerna.json
{
  "packages": [
    "packages/*"
  ],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true
}
  1. 安装依赖

在根目录下执行 yarn install

  1. 配置 Webpack

packages/app1packages/app2 目录下分别创建 webpack.config.js 文件,并按照上面的配置进行修改。

  1. 编写代码

packages/app2/src 目录下创建一个 Button.js 文件,并导出一个 React 组件。

// packages/app2/src/Button.js
import React from 'react';

function Button({ text }) {
  return <button>{text}</button>;
}

export default Button;

packages/app1/src 目录下创建一个 App.js 文件,并引入 app2 提供的 Button 组件。

// packages/app1/src/App.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import Button from 'app2/Button'; // 注意这里的路径

function App() {
  return (
    <div>
      <h1>App1</h1>
      <Button text="Click me from App2!" />
    </div>
  );
}

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
  1. 启动应用

使用 Webpack Dev Server 分别启动 app1app2

cd packages/app1
yarn webpack serve
cd ../app2
yarn webpack serve
  1. 验证

打开 app1 的页面,就可以看到 app2 提供的 Button 组件了。

七、总结

Monorepo 架构和 Webpack Module Federation 是现代 Web 开发的利器。 它们可以帮助我们更好地管理代码、提高开发效率、构建可扩展的应用程序。 当然,它们也有一些挑战,需要我们不断学习和实践。 希望今天的讲座能给大家带来一些启发。

感谢大家的聆听! 咱们下期再见!

发表回复

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