Хотя новости о Webpack 5 появились уже давно, официальная версия еще не выпущена. В журнале изменений Webpack 5, помимо обычной оптимизации производительности и ускорения компиляции, есть более ожидаемая функция, котораяModule Federation
.Module Federation
Его можно принудительно перевести в "федерация модулей", но звучит очень странно Я тоже закинул этот вопрос в группу фронтенда, и не ожидал, что ответы у всех будут разные. Поэтому в данной статье напрямую используетсяModule Federation
Теперь, кажется, удобнее не переводить.
Что такое федерация модулей?
Module Federation
В основном он используется для решения проблемы совместного использования кода между несколькими приложениями, что позволяет более элегантно реализовать совместное использование кода между приложениями. Предположим, теперь у нас есть два проекта A и B, внутри проекта A есть компонент карусели, а у проекта B есть компонент списка новостей.
Теперь есть требование пересадить список новостей проекта B в проект A, и необходимо обеспечить согласованность стилей списков новостей с обеих сторон в процессе последующей итерации. На данный момент у вас есть два варианта:
- Используйте CV Dafa, чтобы скопировать весь код проекта B в проект A;
- Отделите компонент новостей, опубликуйте его во внутреннем 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;
На данный момент проект А успешно подключился к новостному компоненту проекта Б. Посмотрим еще раз на сетевой запрос проекта А, проект А настроенapp2: "app2@http://localhost:3002/remoteEntry.js"
После того, как удаленный проект B будет запрошен первымremoteEntry.js
файл как запись. Когда мы импортируем компонент новостей проекта B, мы получим компонент новостей проекта B.src_NewsList_js.js
документ.
Конфигурация: общая
В дополнение к упомянутой выше конфигурации, связанной с введением и демонстрацией модуля, также имеется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;
Внедряйте несколько зависимостей одновременно
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;
логика загрузки
Здесь есть момент, на который стоит обратить особое внимание, а именно файл входа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
Возможно ли это? После испытаний это действительно невозможно.
Основная причина заключается в том, что файлы js, предоставляемые удаленным компьютером, должны быть загружены в первую очередь, еслиbootstrap.js
не является асинхронной логикой, вimport NewsList
, это будет зависеть от app2remote.js
, если прямо вmain.js
выполнить, app2remote.js
Он вообще не загружается, так что будут проблемы.
Это также видно из сетевой панели,remote.js
предшествуетbootstrap.js
загружен, поэтому нашbootstrap.js
Должна быть асинхронная логика.
Логика загрузки проекта А выглядит следующим образом:
загрузить main.js
main.js
В основном он содержит некоторую логику времени выполнения веб-пакета, а также удаленные запросы и запросы начальной загрузки.
загрузить remote.js
main.js
Элемент B будет загружен первымremote.js
, файл показываетexposes
Сконфигурированные внутренние компоненты предназначены для внешнего использования.
Загрузите bootstrap.js
main.js
Загрузите свою собственную основную логикуbootstrap.js
,bootstrap.js
Будет использоваться компонент списка новостей app2.
внутреннее использование__webpack_require__.e
чтобы загрузить новостной компонент,__webpack_require__.e
существуетmain.js
определено.
/* 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
уже определено.
src_NewsList_js.js
загруженоremote.js
положить начало.
Суммировать
предоставлено Webpack 5Module Federation
Он по-прежнему очень мощный, особенно для совместного использования кода в нескольких проектах, что обеспечивает большое удобство, но у него есть фатальный недостаток, требующий, чтобы все ваши проекты были основаны на Webpack, и он был обновлен до Webpack 5. В сравнении сModule Federation
, я по-прежнему предпочитаю решение, предоставляемое vite, которое использует встроенные в браузер возможности модуляризации для совместного использования кода.
Полный код доступен на моемgithub, если вы хотите узнать больше оModule Federation
случаи, вы можете посетитьОфициальный склад.