این قسمت دوم از این مطلب است. اگر قسمت اول را نخوانده اید توصیه میکنم حتما ابتدا آن را مطالعه کنید
چگونه یک پیاده سازی واقعی به سبک Micro Frontends داشته باشیم - قسمت اول
در قسمت اول درباره اینکه قرار است چه چیزی و چطور پیاده سازی شود صحبت کردم و در این قسمت به جزییات پیاده سازی خواهم پرداخت و با جزییات دقیق خواهم گفت که این مثال چگونه پیاده سازی شده است.
در ابتدا بهتر است فریم ورک ها و کتابخانه هایی که در این پیاده سازی استفاده شده اند را معرفی کنم:
این فریم ورک به عنوان فریم ورک اصلی هم برای پیاده سازی core این مثال و هم برای پیاده سازی اکثر app ها استفاده شده است. البته دو app هم توسط Vue.js توسعه داده شده و به کمک Custom element امکان قرار گرفتن در کنار app های دیگر را پیدا کرده اند.
اگر می خواهید در مورد Angular بیشتر بدانید اینجا را مطالعه کنید.
اگر می خواهید در مورد Vue.js بیشتر بدانید اینجا را مطالعه کنید.
اگر می خواهید در مورد Custom elements بیشتر بدانید اینجا را مطالعه کنید.
این یک plugin است که از نسخه 5 webpack رسما به آن اضافه شد و این امکان را به Webpack اضافه کرد که Webpack بتواند بین local module ها و remote module ها تمایز قائل شود. local module ها، ماژول هایی هستند که در زمان build در دسترس هستند و می توانند به صورت مستقیم در فرایند build مشارکت کنند اما remote module ها، ماژول هایی هستند که در زمان build در دسترس نیستند و در فرایند دیگری build شده و یا خواهند شد و اکنون تنها آن را به webpack معرفی می کنیم بدون اینکه اطلاعاتی در مورد آن داشته باشیم.(حتی در بسیاری از مواقع از جمله در مثال ما این معرفی هم در زمان اجرا صورت میگیرد). امیدوارم توضیحات مختصر من توانسته باشد دلیل اینکه این plugin چقدر میتواند برای توسعه سیستم های بر پایه Micro Frontends مفید باشد را روشن کرده باشد.
Multiple separate builds should form a single application. These separate builds should not have dependencies between each other, so they can be developed and deployed individually. This is often known as Micro-Frontends, but is not limited to that.
(https://webpack.js.org/concepts/module-federation/)
اگر تمایل دارید بیشتر با آن آشنا شوید این مطلب را مطالعه کنید هر چند برای فهمیدن بهتر و کاملتر حتما لازم است مثالهای مربوطه را ببینید.
یک کتابخانه سبک بر پایه Angular که از آن برای مدیریت app ها، load و Initialize کردن آنها و دیگر موارد مشابه استفاده کرده ام. در نوشته های بعدی به صورت مفصل در مورد آن توضیح خواهم داد.
در کنار این سه مورد اصلی از یک کتابخانه دیگر هم استفاده شده است. (@angular/builders)
اما دلیل استفاده از این کتابخانه چیست؟ هر چند Angular در roadmap خود پشتیبانی از Micro frontends را در برنامه دارد اما در حال حاضر( نسخه 12) اعمال تنظیمات مربوط Module Federation در Webpack را به صورت مستقیم پشتیبانی نمیکند. برای این منظور می توان از امکانی که Angular برای سفارشی سازی فرایند build در اختیار توسعه دهندگان گداشته است استفاده کرد(Angular Cli Builder). کتابخانه های زیادی برای این منظور وجود دارد، حتی شما میتوانید خودتان انواع سفارشی Builder را بنویسید، اما من برای این قسمت تصمیم گرفتم از این کتابخانه استفاده کنم.
در ادامه مراحل پیاده سازی این مثال را با هم مرور میکنیم:
( اگر با این موضوع آشنا نیستید این مطلب را مطالعه کنید)
من برای این منظور از yarn استفاده میکنم. شما میتوانید از yarn و یا npm استفاده کنید.
yarn add @angular-builders/custom-webpack @narik/micro-frontends-infrastructure @narik/micro-frontends-core @narik/micro-frontends-ui @angular-architects/module-federation
علاوه بر وابستگی های بالا که مربوط به Micro Frontends هستند به دلیل اینکه در این مثال من از کامپوننت های Angular Material استفاده کردهام لازم است تا وابستگی های زیر نیز اضافه شوند.( بنا به تصمیم شما ممکن است این وابستگی ها نیاز نباشند و با اینکه نیاز به یکسری وابستگی های دیگر باشد)
yarn add @angular/cdk @angular/flex-layout @angular/material
این کار بسیار ساده است. ابتدا در app.module.ts ، ماژول های مربوطه را import می کنیم
import { MicroFrontendsCoreModule } from '@narik/micro-frontends-core' ... imports:[ ..., MicroFrontendsCoreModule.forRoot() ]
سپس لازم است تا به کمک APP_INITIALIZER فرایند initialize شدن سرویس های Narik Micro Frontends را انجام دهیم. این موضوع تضمین میکند تا قبل از Initialize شدن Narik Micro-Frontends اجرای برنامه شروع نشود.
import {MicroFrontendsService} from '@narik/micro-frontends-infrastructure' ..... providers[ { provide: APP_INITIALIZER, useFactory: initializeMicroFrontendsService, deps: [MicroFrontendsService], multi: true, } ] .... export function initializeMicroFrontendsService( microFrontendsService: MicroFrontendsService ): () => Promise<void> { return () => microFrontendsService.initialize(); }
تنظیم app.module.ts به پایان رسید. فایل کامل شده را می توانید در اینجا ببینید.
پس از آن دو کار کوچک در app.component داریم. در ابتدا در فایل app.component.html محتوای زیر را می گذاریم
<span *ngIf="microFrontendsService.initializing$ | async">loading</span> <div id="content-root"></div>
در خط اول فقط نمایش یک loading است تا app شما load شود. شما میتوانید loading دلخواه خود را بگذارید و در خط دوم شما مکان load شدن app ها را مشخص می کنید.
در ادامه در فایل app.component.ts لازم است تا app پیش فرض را load کنیم و در مکان مورد نظر نمایش دهیم.
export class AppComponent{ constructor(public microFrontendsService: MicroFrontendsService) { this.microFrontendsService.loadAndInitializeDefaultApp('#content-root'); } }
فایلهای کامل app.component.html و app.component.ts را می توانید در اینجا و اینجا ببینید.
منظور من از app پیش فرض چیست؟ همانطور که در قسمت اول گفتم برای این مثال 10 app داریم. app پیش فرض، app ی است که در ابتدا باید load و initialize شود که در مثال ما همان shell app است. اینکه چگونه این app مشخص می شود را در ادامه خواهیم دید.
کاری که تا کنون انجام دادیم روی قسمتی از سیستم بود که اسم آن را host یا میزبان می گذاریم، که در واقع تنها کاری که انجام می دهد میزبانی از app های سیستم است و هیچ کار دیگری با این قسمت نداریم. بقیه کار پیاده سازی سیستم می شود پیاده سازی app ها که هر کدام می تواند توسط یک تیم پیاده سازی شود و مستقل تست و deploy شودو app میزبان تنها با یکسری config از وجود آنها باخبر می شود که این config ها در زمان اجرا هم می توانند تغییر کنند.
هر چند در این مثال من تمامی app ها در یک Angular Workspace در کنار هم قرار داده ام ولی همانطور که گفتم این app های هیچ وابستگی به هم ندارند و می توانند کاملا به به صورت مستقل توسعه داده شوند. برای ایجاد هر app مراحل زیر را انجام می دهیم:
ng g application app-name
پس از اینکه app ایجاد شد تنظیمات Module Federation را روی آن انجام می دهیم. برای این منظور ابتدا در فایل angular.json تنظمیات بیلدرها را برای این app به @angular-builders/custom-webpack تغییر میدهیم و پس از آن به کمک تنظیمات customWebpackConfig آدرس فایل سفارشی تنظیمات webpack را مشخص می کنیم.
موارد مطرح شده در این قسمت تنظیمات مربوط به @angular-builders/custom-webpack و Module Federation است که جزییات آن در حوصله این نوشته نیست. با مراجعه یه مستندات هر کدام می توانید جزییات بیشتر و کاملتری را ببینید. در نوشته ای که قبلا برای یک مثال دیگر نوشته ام این موضوع را کمی بیشتر توضیح دادهام.
Tutorial: How create a pluggable Angular app with Webpack Module Federation
میتوانید فایل های کامل شده را در این آدرس ها ببینید. فایل angular.json ، extra-webpack.config js
در این مثال shell app بسیار ساده است. فقط در app.component.html محتوای زیر قرار میگیرد.
همانطور که مشاهده می کنید در این کد یک toolbar در بالا قرار گرفته است و در پایین نیز یک router-outlet که قرار است دیگر app ها در این قسمت نمایش داده شوند. همانطور که قبلا گفته شد shell app فقط وظیفه مشخص کردن layout را دارد. تنها موردی که در این کد لازم است تا در مورد آن صحبت شود این قسمت است
<extension-host key="shopping-cart-button"></extension-host>
در این قسمت shell یه سیستم اینگونه می گوید که در داخل toolbar فضایی در نظر گرفته است برای بقیه app ها و از بقیه app ها می خواهد که محتوای مد نظرشان را در این قسمت قرار دهند.
اگر هیچ app دیگری برای این قسمت محتوا ارائه ندهد طبیعتا هیچ چیزی به این قسمت اضافه نخواهد شد و اختلالی در کارکرد shell به وجود نخواهد آمد. مقدار Key هم نام فضای در نظر گرفته شده را مشخص می کند . در ادامه خواهیم دید که چگونه app ها می توانند برای اینگونه فضا ها محتوا ارائه کنند.
در این خط extension-host کامپوننتی است که در MicroFrontendsUiModule ارائه شده است.
و مرحله آخر برای اینکه shell app ما آماده استفاده باشد این است که آن را به سیستم معرفی کنیم.
کتابخانه Narik Micro Frontends مفهومی دارد به نام app discovery که وظیفه آن شناسایی app های سیستم است. در حال حاضر پیاده سازی پیش فرض این مفهوم در این کتابخانه بر اساس یک فایل json است که مشخصات app ها در آن قرار میگیرد. این فایل به نام apps.json در مسیر assets قرار میگیرد.
برای معرفی shell اطلاعات زیر را به apps.json اضافه میکنیم.
مشخصه key شناسه یکتای هر app است. در مورد app پیش فرض قبلا صحبت کردیم. isDefault مشخص میکند که آیا این app، پیش فرض است و یا نه. مشخصه load مشخص میکند که این app چگونه باید load شود و initialize مشخص میکند که این app چکونه باید initialize شود. در کتابخانه Narik Micro Frontends دو مفهوم وجود دارد به نام app loader و app Initializer.
وظیفه app loader لود کردن app ها بر اساس اطلاعاتی است که در قسمت مشخصه load برای app مشخص شده است. پیاده سازی پیش فرض برای این مفهوم، لود app به کمک Module Federation است.
وظیفه app initializer این است که app ها را initialize کند. اینکه چگونه یک app باید initialize شود بر اساس نوع app متفاوت خواهد بود. در حال حاضر پیاده سازی های مختلفی از app initializer در کتابخانه Narik Micro Frontends وجود دارد که بر اساس مشخصه type از یکی از این موارد استفاده می شود. مثلا این مقدار برای shell app برابر "angular-app" است . پس از AngularAppInitializer استفاده خواهد شد. این نوع initializer بر اساس module و bootstrapComponent مشخص شده، میتواند app لود شده را initialize کرده و در سیستم قرار دهد.
پس از انجام این کارها shell app آماده است.
تمامی مراحل کاملا شبیه shell app است با چند تفاوت ساده. تفاوت اول در معرفی این app به سیستم است.
همانطور که می بینید چند تقاوت با shell app دارد. اول از همه مشخصه eagerInitialize
. این مشخصه به سیستم می گوید که این app در همان ابتدا باید initialize شود. یعنی حتی قبل از load. چرا؟ اگر به نوع این app دقت کنید میبینید که این app از نوع angular-routing-app است . پس لازم است در همان ابتدا routing مربوطه به سیستم معرفی شود , و سیستم هر گاه درخواست route متناسب را دریافت کند این app را لود خواهد کرد. دومین نکته این است که تنظیمات این app دارای یک قسمت metadata است.در این قسمت این app اطلاعات بیشتری به سیستم خواهد داد. فایل metadata را با هم ببینیم.
این فایل دارای دو قسمت مهم است. apps و extension-points. در قسمت apps ، مواردی معرفی می شود که با app های دیگر هیچ تفاوتی ندارند، فقط زیر مجموعه این app تلقی میشوند. یعنی این app ها هم میتوانستند در همان فایل apps.json قرار بگیرند ولی برای مدیریت راحت تر app ها، این فیچر اضافه شده است که یک app بتواند app های زیر مجموعه خودش را داشته باشد. یکی از مزایای این روش این است که اگر مثلا Shopping Cart app غیر فعال شود تمامی app های زیر مجموعه هم غیر فعال خواهند شد.
قسمت دوم قسمت extension-points است. حتما به خاطر دارید در shell app در مورد موضوعی صحبت کردیم به نام extension point و اینکه shell app فضایی را در toolbar خود فراهم کرده است که بقیه app ها برای آن قسمت محتوا مشخص کنند. در این قسمت Shopping Cart app میگوید که برای extension point های با Key برابر "shopping-cart-button" می خواهد که shopping-cart-button app لود شود. اگر محتوای همین فایل را نگاه کنید "shopping-cart-button" یک app است مثل همه app ها با این تفاوت که نوع آن برابر " angular-component" است. سیستم کاری که میکند این است که پس از لود app یک نمونه از کامپوننت مشخص شده می سازد و در ان extension point قرار میدهد.
همینطور Shopping Cart app برای دو point دیگر محتوا مشخص کرده است. با این تفاوت که app های مشخص شده در این قسمت نوع متفاوتی دارند. اگر به فایل دقت کنید نوع این app ها "custom-element" است. روال کار کاملا شبیه مدل قبل است با این تفاوت که پس از لود app یه نمونه از custom element مشخص شده ساخته می شود.
تنها یک نمونه app مانده است که تا کنون بررسی نکرده ایم که نمونه آن هم در این فایل وجود دارد "angular-service". این نوع app ها یک یا چند پیاده سازی سرویس یه سیستم اضافه میکنند. سرویس هایی که در اختیار بقیه app ها قرار خواهند گرفت. البته بقیه app ها هیچگاه درگیر اینکه پیاده سازی توسط کدام app و یا اینکه چگونه پیاده سازی انجام گرفته است نخواهند شد. بلکه contract سرویس در اختیار آنها قرار خواهد گرفت و آنها از آن استفاده خواهند کرد. دو نمونه app که از این نوع در این مثال داریم shopping-cart-service و tax-service هستند.
بقیه app ها هم به همین روش به سیستم اضافه می شوند. حتما جزییات بیشتری هم در این مثال وجود دارد که فکر میکنم پرداختن به آنها در این نوشته ضرورتی ندارد.
https://github.com/NarikMe/narik-micro-frontends-sample
https://github.com/NarikMe/narik-micro-frontends