Micro Frontend چیست؟
micro frontend یک الگوی معماری (architecture pattern) میباشد؛ جایی که یک front-end app، به چند app کوچکتر تقسیم میشود و هر کدام از آنها به صورت مستقل توسعه داده و تست میشوند. مفهومی شبیه به مایکروسرویسها است؛ اما برای سورس کدهای یکپارچهی سمت کلاینت.
چرا؟
خیلی سخت است که بخواهیم روی سورس کدهای یکپارچه سمت کلاینت تست نویسی، بهروز رسانی و هم چنین نگهداری کنیم. این در حالی است که توانایی تیم را به منظور مستقل کار کردن بر روی بخشهای مختلفی از app، محدود میکند. شکستن یک app یکپارچه به micro frontendهای کوچکتر و قابل مدیریت، این امکان را فراهم میسازد که چندین تیم، به صورت مستقل کار کنند و از فریم ورکهای ترجیحی خود استفاده کنند.
چگونه؟
اساسا 3 راه برای ادغام کردن ماژولهای micro frontend با container app وجود دارد.
1-Server integration
هر micro frontend در یک وب سرور احتمالا جداگانه هاست شدهاست که مسئول رندر کردن و خدمت دادن markupهای مربوطه میباشد. به محض دریافت درخواستی از سمت مرورگر، container app در خواست را برای markup، با برقراری تماس به سرور، برای micro frontend مربوطه انجام میدهد.
این یک روش ایده آل نمیباشد؛ بهطوریکه چندین تماس به سرور برای رندر کردن محتوا در صفحه برقرار میشود و همچنین مستلزم پیاده سازی استراتژی cache، به منظور کاهش تاخیر است.
2-Compile time integration
container app دسترسی به کدهای micro frontendها را در طول توسعه و زمان کامپایل دارد. یکی از راههای انجام این روش این است که micro frontendها را به عنوان بستههای npm منتشر کنیم و سپس از آنها به عنوان یک وابستگی در container app استفاده کنیم.
در حالیکه راه اندازی و پیاده سازی، در این حالت ساده است، اما یک وابستگی محکم بین container app و micro frontend وجود دارد. هر زمانکه یک micro frontend بروزرسانی میشود، نیاز است که container app ، به منظور یکپارچه کردن بروز رسانی، دوباره استقرار یابد.
3-Run time integration
container app دسترسی به کدهای micro frontendها را زمانیکه در مرورگر اجرا میشوند، دارد. یکی از راههای انجام این روش، استفاده از پلاگین Module Federation مربوط به Webpack است که مراقب ساختن، در دسترس قرار دادن و استفاده از وابستگیها در زمان اجرا میباشد (در ادامه، این حالت را با جزئیات بیشتری مورد بررسی قرار خواهیم داد).
در این حالت وابستگی بین container app و micro frontendها وجود ندارد و یکپارچگی در زمان اجرا اتفاق میافتد. هر micro frontend میتواند به صورت مستقل توسعه داده شود و استقرار یابد، بدون اینکه container app را دوباره استقرار دهیم.
در این حالت راه اندازی در مقایسه با compile-time integration پیچیدهتر است.
Webpack Module Federation Plugin
پلاگین module federation در Webpack 5.0 معرفی شد که امکان توسعه appهای micro frontend را با بارگذاری پویای کدهای appهای micro frontend را در container app ، فراهم میسازد.
همچنین این امکان را فراهم میکند که dependency ها را بین remote appها و host app، به منظور جلوگیری از کدهای تکراری و کاهش دادن سایز build، به اشتراک بگذاریم.
در این مقاله ما یک مثال را بررسی خواهیم کرد که شامل دو micro frontend ساده میباشد و سپس آنها را در یک host app ادغام میکنیم.
ساختار نهایی بهصورت زیر خواهد بود:
ایجاد کردن اولین Remote app
یک پوشه را به نام remote1 ایجاد کنید و سپس یک فایل را به نام package.json، در پوشهی ایجاد شده، با دستور زیر ایجاد کنید.
پوشهی ایجاد شده را در code editor خود باز کنید (من از vs code استفاده میکنم) و سپس وابستگیهای زیر را به فایل package.json اضافه کنید.
npm install html-webpack-plugin webpack webpack-cli webpack-dev-server --save
1) webpack/ webpack-CLI: برای استفاده از webpack و دستورات webpack CLI
2) html-webpack-plugin/webpack-devserver: سرور توسعه محلی با live reloading
و همچنین اسکریپت webpack serve را در بخش scripts، به منظور serve کردن application در مرورگر اضافه کنید.
اکنون فایل package.json شما همانند زیر میباشد:
{
"name": "remote1",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"html-webpack-plugin": "^5.3.2",
"webpack": "^5.57.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}
در ادامه، یک پوشهی جدید را به نام public ایجاد کنید و یک فایل را به نام index.html در پوشهی public اضافه کنید سپس HTML markup زیر را درون فایل index.html اضافه کنید، که شامل یک div نگهدارنده میباشد، جائیکه خروجی app در زمان توسعه، در آن قرار میگیرد.
<!DOCTYPE html>
<html>
<head>
<title>
Remote1(Localhost:7001)
</title>
</head>
<body>
<div id="dev-remote1"></div>
</body>
</html>
در ادامه یک پوشهی جدید را به نام src ایجاد کنید و دو فایل را با نامهای index.js و startup.js در آن قرار دهید. در ادامه به این دو فایل برمیگردیم و کدهای لازم را در آن قرار میدهیم.
یک فایل جدید را به نام webpack.config.js در ریشهی پروژه ایجاد میکنیم که در آن تنظیمات webpack را قرار میدهیم. در این فایل، دو پلاگین را به نامهای Webpack و Module Federation، اضافه میکنیم.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: {
port: 7001,
},
plugins: [
new ModuleFederationPlugin({
name: 'remote1',
filename: 'remoteEntry.js',
exposes: {
'./RemoteApp1': './src/startup',
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
فایل remoteEntry.js شامل یک لیست از فایلهای در معرض قرار داده شدهی توسط remote app به همراه مسیرهای آنها میباشد. از این فایل در زمان ادغام کردن remote app در host app استفاده میشود.
index.js نقطهی ورودی remote app ما میباشد. به منظور جلوگیری از eager loading، کدهای startup را در یک js فایل جداگانه به نام startup.js قرار میدهیم. از این رو فایل index.js شامل فقط یک import میباشد.
در درون فایل startup.js ، یک تابع به نام mount را export میکنیم. این تابع یک element را دریافت میکند؛ جائیکه قرار است خروجی app در آن قرار گیرد. در حالت development، خروجی در یک div نگهدارنده با شناسه dev-remote1 در فایل محلی index.html قرار میگیرد.
const mount = (el) => {
el.innerHTML = '<div>Remote 1 Content</div>';
};
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector('#dev-remote1');
if (el) {
mount(el);
}
}
export { mount };
فایلها را ذخیره کنید و سپس دستور
npm start را جهت مشاهدهی خروجی اجرا کنید. در اینجا برنامه بر روی پورت 7001 اجرا میشود . ( http://localhost:7001 )
ایجاد کردن دومین Remote app
در اینجا نیز مراحل، دقیقا شبیه به ایجاد remote app قبلی میباشد. یک پوشه را به نام remote2 در کنار پوشهی remote1 ایجاد کنید و یک فایل را به نام package.json، با وابستگیهای معرفی شده ایجاد کنید و سپس دستور npm install را بزنید تا وابستگیها نصب شوند.
{
"name": "remote2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"html-webpack-plugin": "^5.3.2",
"webpack": "^5.57.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}
سپس یک فایل را با نام index.html و با محتوای زیر، در پوشهی public ایجاد کنید:
<!DOCTYPE html>
<html>
<head>
<title>
Remote2(Localhost:7002)
</title>
</head>
<body>
<div id="dev-remote2"></div>
</body>
</html>
در ادامه یک فایل را با نام webpack.config.js در ریشهی پروژهی remote2 ایجاد کنید و محتوای زیر را در آن قرار دهید. مطمئن باشید که پورت اجرایی برنامه 7002 میباشد.
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: {
port: 7002,
},
plugins: [
new ModuleFederationPlugin({
name: 'remote2',
filename: 'remoteEntry.js',
exposes: {
'./RemoteApp2': './src/startup',
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
فایل فایل index.js برای remote2
فایل startup.js برای remote2
const mount = (el) => {
el.innerHTML = '<div>Remote 2 Content</div>';
};
if (process.env.NODE_ENV === 'development') {
const el = document.querySelector('#dev-remote2');
if (el) {
mount(el);
}
}
export { mount };
اکنون میتوانید با اجرای دستور npm start برنامه را بر روی پورت 7002 اجرا کنید . ( http://localhost:7002 )
ایجاد کردن Host app و یکپارچه کردن آن با Remote app ها
یک پوشهی جدید را به نام container در کنار دو پوشهی قبلی (remote1, remote2) ایجاد کنید. در پوشهی ایجاد شده، یک فایل را به نام package.json ایجاد کنید و محتوای زیر را در آن قرار دهید و سپس دستور npm install را بزنید تا لیست وابستگیها دریافت شود.
{
"name": "container",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "webpack serve"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"html-webpack-plugin": "^5.3.2",
"webpack": "^5.57.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.3.1"
}
}
اکنون یک پوشه را به نام public، در ریشهی پروژه ایجاد کنید و یک فایل را به نام index.html، در آن قرار دهید. در این فایل دو div نگهدارنده به منظور هاست کردن محتوا، از هر remote app قرار دارد.
<!DOCTYPE html>
<html>
<head>
<title>Host App (Localhost:7000)</title>
</head>
<body>
<div id="remote1-app"></div>
<div id="remote2-app"></div>
</body>
</html>
یک پوشه به نام src، در ریشه پروژه ایجاد کنید و دو فایل را با نامهای index.js و startup.js، در آن قرار دهید. کمی جلوتر به این دو فایل میپردازیم.
یک فایل را با نام webpack.config.js، با تنظیمات زیر در ریشهی پروژه قرار دهید. در اینجا پورت اجرایی برنامه، 7000 تعیین شدهاست:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = {
mode: 'development',
devServer: {
port: 7000,
},
plugins: [
new ModuleFederationPlugin({
name: 'container',
remotes: {
remote1: 'remote1@http://localhost:7001/remoteEntry.js',
remote2: 'remote2@http://localhost:7002/remoteEntry.js',
},
}),
new HtmlWebpackPlugin({
template: './public/index.html',
}),
],
};
برای خصوصیت remotes در پلاگین Module Federation باید remote appهای را تعریف کنیم که container app میخواهد به آن دسترسی داشته باشد. خصوصیت remotes شامل زوج کلید/مقدارها (key/value) میباشد و در اینجا کلید، رشته و مقدار هم، رشته است. مقدار (value) برای کلید (key) با نام remote app شروع میشود که آن را در بخش تنظیمات پلاگین module federation مربوط به remote app مربوطه قرار دادهایم. سپس @ قرار میگیرد و در ادامه آن، آدرس جایی را که remoteEntry.js مربوط به remote app قرار دارد، مینویسیم.
دقیقا مثل remote app ها، در فایل index.js مربوط به import ، host app زیر را انجام میدهیم:
در فایل remote app ، startup.js ها را همانند زیر import میکنیم و سپس آنها را در فایل index.html میزبان سوار میکنیم.
import { mount as remote1Mount } from 'remote1/RemoteApp1';
import { mount as remote2Mount } from 'remote2/RemoteApp2';
remote1Mount(document.querySelector('#remote1-app'));
remote2Mount(document.querySelector('#remote2-app'));
در ادامه جهت مشاهده خروجی، app میزبان را با دستور npm start اجرا میکنیم. اکنون شما میتوانید خروجی remote app ها را در host app ببینید. لازم به ذکر است که در هنگام اجرای دستور npm start برای host app ، هر دو remote app ایجاد شده باید در حالت اجرا باشند.
ملاحظات
Module federation در Webpack نسخه 5 به بالا، در دسترس قرار دارد. شما میتوانید وابستگیهای بین remote app و host app را به اشتراک بگذارید. این کار را با اضافه کردن آنها به تنظیمات پلاگین module federation برای remote app و host app انجام دهید.
new ModuleFederationPlugin({
name: 'remote1',
filename: 'remoteEntry.js',
exposes: {
'./RemoteApp1': './src/startup',
},
shared:['react', 'react-dom']
}),
ارتباط بین host و remote app میتواند از طریق callbackها انجام شود.