Совместное использование кода между приложениями Webpack5 — федерация модулей

React.js Webpack
Совместное использование кода между приложениями Webpack5 — федерация модулей

Хотя новости о Webpack 5 появились уже давно, официальная версия еще не выпущена. В журнале изменений Webpack 5, помимо обычной оптимизации производительности и ускорения компиляции, есть более ожидаемая функция, котораяModule Federation.Module FederationЕго можно принудительно перевести в "федерация модулей", но звучит очень странно Я тоже закинул этот вопрос в группу фронтенда, и не ожидал, что ответы у всех будут разные. Поэтому в данной статье напрямую используетсяModule FederationТеперь, кажется, удобнее не переводить.

Что такое федерация модулей?

Module FederationВ основном он используется для решения проблемы совместного использования кода между несколькими приложениями, что позволяет более элегантно реализовать совместное использование кода между приложениями. Предположим, теперь у нас есть два проекта A и B, внутри проекта A есть компонент карусели, а у проекта B есть компонент списка новостей.

项目 A

项目 B

Теперь есть требование пересадить список новостей проекта B в проект A, и необходимо обеспечить согласованность стилей списков новостей с обеих сторон в процессе последующей итерации. На данный момент у вас есть два варианта:

  1. Используйте CV Dafa, чтобы скопировать весь код проекта B в проект A;
  2. Отделите компонент новостей, опубликуйте его во внутреннем npm и загрузите компонент через npm;

CV Dafa определенно быстрее, чем независимые компоненты, в конце концов, нет необходимости отделять код компонента от проекта B, а затем публиковать npm. Но недостатком CV Dafa является то, что код не может быть синхронизирован во времени.Если другой ваш коллега изменит компонент новостей проекта B после того, как вы скопируете код, компоненты новостей проекта A и проекта B будут несовместимы.

В настоящее время, если ваши два проекта используют Webpack 5, это должно быть очень приятно, потому что вы можете использовать новостной компонент проекта B непосредственно в проекте A с помощью всего нескольких строк конфигурации без каких-либо затрат. Кроме того, вы можете использовать компонент Карусель проекта А в проекте Б. То есть поModule FederationРеализованный обмен кодом является двусторонним, и похоже, что он действительно хочет, чтобы люди кричали: «Я не могу учиться!».

Практика модульной федерации

Давайте посмотрим на код проекта A/B.

Структура каталогов проекта А выглядит следующим образом:

├── public
│   └── index.html
├── src
│   ├── index.js
│   ├── bootstrap.js
│   ├── App.js
│   └── Slides.js
├── package.json
└── webpack.config.js

Структура каталогов проекта B выглядит следующим образом:

├── public
│   └── index.html
├── src
│   ├── index.js
│   ├── bootstrap.js
│   ├── App.js
│   └── NewsList.js
├── package.json
└── webpack.config.js

Разница между проектами A и B в основном заключается в компонентах, импортированных в App.js, index.js и bootstrap.js у них одинаковые.

// index.js
import("./bootstrap");

// bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));

App.js проекта A:

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

const App = () => (
  <div>
    <h2 style={{ textAlign: 'center' }}>App1, Local Slides</h2>
    <Slides />
  </div>
);

export default App;

App.js проекта B:

import React from "react";
import NewsList from './NewsList';
const RemoteSlides = React.lazy(() => import("app1/Slides"));

const App = () => (
  <div>
    <h2 style={{ textAlign: 'center' }}>App 2, Local NewsList</h2>
    <NewsList />
  </div>
);

export default App;

Теперь смотрим доступModule FederationПредыдущая конфигурация веб-пакета:

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

module.exports = {
  mode: "development",
  // 入口文件
  entry: "./src/index",
  // 开发服务配置
  devServer: {
    // 项目 A 端口为 3001,项目 B 端口为 3002
    port: 3001,
    contentBase: path.join(__dirname, "dist"),
  },
  output: {
    // 项目 A 端口为 3001,项目 B 端口为 3002
    publicPath: "http://localhost:3001/",
  },
  module: {
    // 使用 babel-loader 转义
    rules: [
      {
        test: /\.jsx?$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          presets: ["@babel/preset-react"],
        },
      },
    ],
  },
  plugins: [
    // 处理 html
    new HtmlWebpackPlugin({
      template: "./public/index.html",
    }),
  ],
};

Конфигурация: экспозиции/пульты

Теперь мы изменим конфигурацию веб-пакета, чтобы ввестиModule Federation, пусть проект A импортирует новостной компонент проекта B.

// 项目 B 的 webpack 配置
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      // 提供给其他服务加载的文件
      filename: "remoteEntry.js",
      // 唯一ID,用于标记当前服务
      name: "app2",
      // 需要暴露的模块,使用时通过 `${name}/${expose}` 引入
      exposes: {
        "./NewsList": "./src/NewsList",
      }
    })
  ]
};

// 项目 A 的 webpack 配置
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      // 引用 app2 的服务
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",
      }
    })
  ]
};

мы фокусируемся наexposes/remotes:

  • при условииexposesпараметр, указывающий, что текущее приложение являетсяRemote,exposesмодули внутри могут использоваться другимиHostцитировать, ссылаясь наimport(${name}/${expose}).
  • при условииremotesпараметр, указывающий, что текущее приложение являетсяHost, который можно назватьremoteсерединаexposeмодуль.

Затем измените App.js проекта A:

import React from "react";
import Slides from './Slides';
// 引入项目 B 的新闻组件
const RemoteNewsList = React.lazy(() => import("app2/NewsList"));

const App = () => (
  <div>
    <h2 style={{ textAlign: 'center' }}>App1, Local Slides, Remote NewsList</h2>
    <Slides />
    <React.Suspense fallback="Loading Slides">
      <RemoteNewsList />
    </React.Suspense>
  </div>
);

export default App;

项目 A

На данный момент проект А успешно подключился к новостному компоненту проекта Б. Посмотрим еще раз на сетевой запрос проекта А, проект А настроенapp2: "app2@http://localhost:3002/remoteEntry.js"После того, как удаленный проект B будет запрошен первымremoteEntry.jsфайл как запись. Когда мы импортируем компонент новостей проекта B, мы получим компонент новостей проекта B.src_NewsList_js.jsдокумент.

network

Конфигурация: общая

В дополнение к упомянутой выше конфигурации, связанной с введением и демонстрацией модуля, также имеетсяsharedКонфигурация в основном используется, чтобы избежать множественных публичных зависимостей в проекте.

Например, наш текущий проект A представилreact/react-dom, а компонент списка новостей, предоставляемый проектом B, также зависит отreact/react-dom. Если вы не исправите это, проект A загрузит дваreactбиблиотека. Это напоминает мне о том, когда я впервые пришел в индустрию, один из проектов компании был способом сращивания шаблонов PHP.Разные отделы внедряли jQuery в свои шаблоны, в результате чего в проект были введены три разные версии jQuery, что особенно повлияло страница производительность.

Поэтому, когда мы используем федерацию модулей, мы должны помнить о настройке общих зависимостей дляsharedсередина. Кроме того, обязательно настройте оба проекта одновременно.shared, иначе будет сообщено об ошибке.

Затем мы открываем в браузере проект А. В сетевой панели Chrome мы видим, что проект А напрямую использует проект Б.react/react-dom.

共享依赖

двусторонний обмен

Как упоминалось ранее, совместное использование Module Federation может быть двунаправленным. Ниже мы также настраиваем проект A какRemote, предоставьте карусельный компонент элемента A элементу B для использования.

// 项目 B 的 webpack 配置
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app2",
      filename: "remoteEntry.js",
      // 暴露新闻列表组件
      exposes: {
        "./NewsList": "./src/NewsList",
      },
      // 引用 app1 的服务
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true }
      }
    })
  ]
};

// 项目 A 的 webpack 配置
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app1",
      filename: "remoteEntry.js",
      // 暴露轮播图组件
      exposes: {
        "./Slides": "./src/Slides",
      },
      // 引用 app2 的服务
      remotes: {
        app2: "app2@http://localhost:3002/remoteEntry.js",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true }
      },
    })
  ]
};

Используйте компонент карусели в проекте B:

// App.js
import React from "react";
import NewsList from './NewsList';
+const RemoteSlides = React.lazy(() => import("app1/Slides"));

const App = () => (
  <div>
-   <h2 style={{ textAlign: 'center' }}>App 2, Local NewsList</h2>
+   <h2 style={{ textAlign: 'center' }}>App 2, Remote Slides, Local NewsList</h2>
+   <React.Suspense fallback="Loading Slides">
+     <RemoteSlides />
+   </React.Suspense>
    <NewsList />
  </div>
);

export default App;

项目 B

Внедряйте несколько зависимостей одновременно

Module Federation также поддерживает несколько удаленных проектов одновременно. Мы можем создать новый проект C и одновременно ввести карусельный компонент проекта A и компонент списка новостей проекта B.

// 项目 C 的 webpack 配置
// 其他配置与之前的项目基本一致,除了需要将端口修改为 3003
const { ModuleFederationPlugin } = require("webpack").container;
module.exports = {
  plugins: [
    new ModuleFederationPlugin({
      name: "app3",
      // 同时依赖项目 A、B
      remotes: {
        app1: "app1@http://localhost:3001/remoteEntry.js",
        app2: "app2@http://localhost:3002/remoteEntry.js",
      },
      shared: {
        react: { singleton: true },
        "react-dom": { singleton: true }
      }
    })
  ]
};

Компоненты доступа:

import React from "react";
const RemoteSlides = React.lazy(() => import("app1/Slides"));
const RemoteNewsList = React.lazy(() => import("app2/NewsList"));

const App = () => (
  <div>
    <h2 style={{ textAlign: 'center' }}>App 3, Remote Slides, Remote Remote</h2>
    <React.Suspense fallback="Loading Slides">
      <RemoteSlides />
      <RemoteNewsList />
    </React.Suspense>
  </div>
);

export default App;

项目 C

логика загрузки

Здесь есть момент, на который стоит обратить особое внимание, а именно файл входаindex.jsЛогики самой по себе нет, но логика помещена вbootstrap.jsсередина,index.jsдля динамической загрузкиbootstrap.js.

// index.js
import("./bootstrap");

// bootstrap.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(<App />, document.getElementById("root"));

Если вы удалитеbootstrap.js, поместите логику прямо вindex.jsВозможно ли это? После испытаний это действительно невозможно.

去除 bootstrap.js

Основная причина заключается в том, что файлы js, предоставляемые удаленным компьютером, должны быть загружены в первую очередь, еслиbootstrap.jsне является асинхронной логикой, вimport NewsList, это будет зависеть от app2remote.js, если прямо вmain.jsвыполнить, app2remote.jsОн вообще не загружается, так что будут проблемы.

依赖查找

依赖查找

Это также видно из сетевой панели,remote.jsпредшествуетbootstrap.jsзагружен, поэтому нашbootstrap.jsДолжна быть асинхронная логика.

network

Логика загрузки проекта А выглядит следующим образом:

загрузить main.js

main.jsВ основном он содержит некоторую логику времени выполнения веб-пакета, а также удаленные запросы и запросы начальной загрузки.

main.js-1

main.js-2

загрузить remote.js

main.jsЭлемент B будет загружен первымremote.js, файл показываетexposesСконфигурированные внутренние компоненты предназначены для внешнего использования.

remote.js

Загрузите bootstrap.js

main.jsЗагрузите свою собственную основную логикуbootstrap.js,bootstrap.jsБудет использоваться компонент списка новостей app2.

bootstrap.js

внутреннее использование__webpack_require__.eчтобы загрузить новостной компонент,__webpack_require__.eсуществуетmain.jsопределено.

RemoteNewsList

/* webpack/runtime/ensure chunk */
(() => {
  __webpack_require__.f = {};
  __webpack_require__.e = (chunkId) => {
    // __webpack_require__.e 会通过传入的 chunkId 在 __webpack_require__.f 中查找
    return Promise.all(Object.keys(__webpack_require__.f).reduce((promises, key) => {
      __webpack_require__.f[key](chunkId, promises);
      return promises;
    }, []));
  };
})();

__webpack_require__.fЕсть три части:

__webpack_require__.f.remotes = (chunkId, promises) => {}  // webpack/runtime/remotes
__webpack_require__.f.consumes = (chunkId, promises) => {} // webpack/runtime/consumes
__webpack_require__.f.j = (chunkId, promises) => {}        // webpack/runtime/jsonp

Мы пока просто посмотрим на логику пультов, так как наш новостной компонент загружается как удаленный.

	/* webpack/runtime/remotes loading */
	(() => {
		var installedModules = {};
		var chunkMapping = {
			"webpack_container_remote_app2_NewsList": [
				"webpack/container/remote/app2/NewsList"
			]
		};
		var idToExternalAndNameMapping = {
			"webpack/container/remote/app2/NewsList": [
				"default",
				"./NewsList",
				"webpack/container/reference/app2"
			]
		};

		__webpack_require__.f.remotes = (chunkId, promises) => {
			// chunkId: webpack_container_remote_app2_NewsList
			chunkMapping[chunkId].forEach((id) => {
				// id: webpack/container/remote/app2/NewsList
				var data = idToExternalAndNameMapping[id];
        // require("webpack/container/reference/app2")["./NewsList"]
				var promise = __webpack_require__(data[2])[data[1]];
        return promise;
			});
		}
	})();

Как видите, окончательным методом вызова станетrequire("webpack/container/reference/app2")["./NewsList"], и этот модуль загружается перед app2remote.jsуже определено.

app2 remote

src_NewsList_js.jsзагруженоremote.jsположить начало.

image-20200914114816682

Суммировать

предоставлено Webpack 5Module FederationОн по-прежнему очень мощный, особенно для совместного использования кода в нескольких проектах, что обеспечивает большое удобство, но у него есть фатальный недостаток, требующий, чтобы все ваши проекты были основаны на Webpack, и он был обновлен до Webpack 5. В сравнении сModule Federation, я по-прежнему предпочитаю решение, предоставляемое vite, которое использует встроенные в браузер возможности модуляризации для совместного использования кода.

Полный код доступен на моемgithub, если вы хотите узнать больше оModule Federationслучаи, вы можете посетитьОфициальный склад.