Совместное использование компонентов React с Webpack 5

Введение 

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

Неудивительно, что вы не понимаете материал и по-прежнему сталкиваетесь с трудностями в этом вопросе! 

Поэтому предлагаю поэкспериментировать с плагином Webpack 5 Module Federation. Подробный разбор и отличные скриншоты гарантируются! 

Примечание. Специально для данной статьи я написал 2 небольших приложения React. Нижепредставленные фрагменты кода взяты как раз из них. Весь код доступен на GitLab: Gerard’s Bicycle Shop (Веломагазин Джерарда) и Gerard’s Blog (Блог Джерарда).

Цель

С помощью React мы создали небольшой веб-магазинчик и блог, которые применяют Webpack 5 в качестве сборщика модулей. 

В магазине есть компонент React, отображающий новое предложение. Наша цель  —  добиться того, чтобы это же предложение отображалось в блоге. Для этого необходимо обеспечить совместное использование компонента. 

Сначала рассмотрим оба приложения. 

Приложение Gerard’s Bicycle Shop

Наш небольшой стильный веб-магазин 

В самом деле есть чем восхищаться! Помимо всего прочего, не особо примечательного, магазин содержит один отличный компонент  —  LatestOffer. Как видно, он выводит изображение и разноцветный текст. Все, что находится внутри оранжевой границы, включая и ее саму, является компонентом LatestOffer. Он отображен в строке 8 в BicycleShop.jsx

import LatestOffer from "../LatestOffer/LatestOffer";

const BicycleShop = () => {
return (
<>
<MainMenu />
<Container style={{ padding: "60px 0 20px 0" }}>
<LatestOffer />
</Container>
...
</>
);
};

Сам компонент определен в отдельном файле LatestOffer.jsx:

import bicycleImage from "./bicycle.jpg";
import styles from "./LatestOffer.module.scss";

const LatestOffer = () => {
return (
<div className={styles.latestOffer}>
<img src={bicycleImage} alt="Bicycle" />
<div className={styles.banner}>
<div>
<div>Latest offer!</div>
<div>The Jellybean 5000™</div>
</div>
<div>
<div>$ 1,999.00</div>
<div>$ 1,599.00</div>
</div>
</div>
<div className={styles.shadow} />
</div>
);
};

Обратите внимание на то, как мы использовали изображение и стилизацию в компоненте. Выбор пал на модули CSS, но это не обязательный вариант. 

Вот так у нас появился небольшой магазинчик с компонентом LatestOffer

Переходим ко второму приложению. 

Приложение Gerard’s Blog

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

Мы знаем о существовании компонента LatestOffer. Однако он определен в репозитории Shop, а не в Blog.

Как же совместно использовать этот компонент между двумя приложениями? Как передать его из магазина в блог? Ответ  —  задействовать мощные функциональности Webpack 5 Module Federation. Хотя и придется попотеть, разбираясь в механизме его работы. 

Начнем углубляться в тему и будем действовать пошагово.

Сборка кода с Webpack 

Оба приложения имеют схожие настройки для сборки кода. Они задействуют Webpack и по большей части обладают идентичной конфигурацией. 

Оба приложения имеют одну и ту же конфигурацию module-rules в файле webpack.config.js

module.exports = {
mode: "development",
...
module: {
rules: [
{
test: /\.s?css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
...
},
},
"sass-loader",
],
},
{
test: /\.jsx?$/,
use: ["babel-loader"],
exclude: /node_modules/,
},
{
test: /\.(png|svg|jpg|jpeg|gif)$/i,
type: "asset/resource",
},
],
},
...
};

Это гарантирует корректную загрузку и сборку таблиц стилей и файлов JavaScript/JSX. Но поскольку конфигурация Webpack не является главным объектом изучения статьи, то об этих правилах поговорим в другой раз. 

Лучше разберемся, как расширить конфигурацию Webpack 5, чтобы предоставить компоненты на одном участке (приложение Shop) и получить их на другом (приложение Blog). 

Module Federation: предоставление компонентов 

Наша цель  —  поделиться компонентом LatestOffer из приложения Shop. Поэтому сначала мы преобразуем файл этого приложения webpack.config.js, добавив экземпляр ModuleFederationPlugin. В строках 11–17 рождается магия: 

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

module.exports = {
mode: "development",
...
module: {
...
},
plugins: [
...
new ModuleFederationPlugin({
name: "SHOP",
filename: "remoteEntry.js",
exposes: {
"./LatestOffer": "./src/components/LatestOffer/LatestOffer",
},
}),
],
};

Передаем в конструктор объект с указанными параметрами.

  • name (строка 12). Определяет имя текущего проекта. Как вы помните, мы редактируем конфигурационный файл Webpack проекта Shop. Имя и верхний регистр выбраны произвольно. 
  • filename (строка 13). Поясняется в следующем абзаце. 
  • exposes (строки 14–16). Здесь определяем предоставляемые компоненты и имена, под которыми они будут доступны. В данном примере мы предлагаем LatestOffer, присваивая ему имя ./LatestOffer и указывая путь к файлу определения компонента в виде строки ./src/components/LatestOffer/LatestOffer. Обратите внимание на обязательное наличие символов ./ перед именем. Имя можно выбрать любое, но мы решили его продублировать. При необходимости этим способом можно предоставить несколько компонентов.

Теперь поговорим о filename  —  remoteEntry.js.

Другие приложения, которые намерены задействовать предоставленный компонент LatestOffer, должны получить его определение, а именно код компонента. Иначе говоря, они должны суметь загрузить этот код. 

Допустим, приложение Shop работает по адресу localhost:8080 (как и в нашем примере). 

Настраивая для параметра filename значение remoteEntry.js (оно произвольное), мы создаем маршрут localhost:8080/remoteEntry.js. Как вы уже догадались, по нему среди шаблонного кода мы находим код компонента LatestOffer:

Мы предъявили компонент и теперь поделимся им из приложения Shop

Посмотрим, как получить его на другом участке и отобразить компонент LatestOffer в приложении Blog

Module Federation: удаленные компоненты 

На следующем этапе рассмотрим конфигурационный файл Webpack приложения Blog. Как и в случае с конфигурацией Shop, добавляем экземпляр ModuleFederationPlugin с параметрами: 

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

module.exports = {
mode: "development",
...
module: {
...
},
plugins: [
...
new ModuleFederationPlugin({
name: "BLOG",
remotes: {
SHOP: "SHOP@http://localhost:8080/remoteEntry.js",
},
}),
],
};

Передаем в конструктор следующие параметры: 

  • name (строка 12). Определяет имя текущего проекта. 
  • remotes (строка 13–15). По сути мы говорим приложению: “Другое приложение под названием SHOP предоставляет компоненты! Давай загрузим их в проект!”. Ключ объекта SHOP  —  это способ, позволяющий локально ссылаться на внешние компоненты (обсудим это позже). Значение представлено строкой, которая состоит из двух частей, соединенных знаком @. Первая часть  —  имя удаленного приложения, а вторая  —  файл, содержащий код загружаемых компонентов (понемногу становится понятнее). 

Теперь переходим к этапу отображения компонента LatestOffer в Blog.

Отображение удаленных компонентов 

Рассмотрим компонент Blog, который отображает компонент LatestOffer:

import React, { lazy, Suspense } from "react";

const LatestOffer = lazy(() => import("SHOP/LatestOffer"));

const Blog = () => {
return (
<div className={styles.blog}>
<h1 className={styles.header}>Gerard's Blog</h1>
...
<Suspense fallback={<span>Loading...</span>}>
<LatestOffer />
</Suspense>
...
</div>
);
};

В строке 3 получаем ссылку на компонент LatestOffer.

Мы применяем так называемый динамический импорт, и функция React lazy обеспечивает возможность его выполнения. Эта тема повышенного уровня сложности, и с более подробной информацией по ней можно ознакомиться в документации React

Загрузив (удаленный!) компонент, отображаем его в строках 10–12. Обратите внимание, что мы обертываем его в компонент высшего порядка (HOC) suspense с резервным значением fallback. Это значение будет отображаться во время загрузки удаленного компонента (если он еще не загружен). Как только загрузка выполнена, компонент LatestOffer отображается в Blog!

Заключение 

На мой взгляд, Webpack 5 Module Federation был изначально слабо раскручен, о чем можно только сожалеть. Материал статьи подтверждает мощные возможности этого плагина. А ведь мы рассмотрели лишь малую часть его потенциала. 

Ряд факторов сыграл не в пользу Module Federation.

Неудачное название 

Большинство разработчиков, услышав термин “Module Federation”, не сразу вспомнят, о чем идет речь. Другое дело, если бы плагин Webpack назывался “ShareComponentsPlugin” (“Плагин совместного использования компонентов”). 

Пугающая терминология 

Зачастую в руководствах по Module Federation фигурирует понятие микрофронтендов. Уверен, вы потеряли половину своей аудитории, оперируя этим сложным и весомым термином. Не лучше ли сказать: “Хотите делиться компонентами между приложениями? Сейчас покажу как”.

В процессе разработчики сами увидят, что они могут не только поделиться простым компонентом, но и целым приложением. В этом и таится маленький секрет, поскольку само приложение React является просто еще одним компонентом. 

Несовершенная документация 

Даже Zack Jackson, активно участвующий в разработке и обслуживании Webpack и Module Federation, отмечает необходимость доработки документации. 

Несмотря на эти недочеты, самое главное преимущество Module Federation заключается в его невероятной эффективности. Иначе говоря, мощь без границ! 

Берите его в работу! Возможно, скоро вы с коллегами займетесь обслуживанием следующего приложения Web 4.0 на основе микрофронтендов.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Gerard van der Put: Sharing React Components With Webpack 5

Предыдущая статьяКак легко и надежно реализовать модульные тесты на Python
Следующая статья16 полезных расширений для SwiftUI