פעם קודמת ראינו איך לבנות את הדיירקטיב, הפעם נהפוך אותו לקצת יותר חכם כדי שנוכל להעביר אליו מודולים שלמים ומתוכם להוציא את הרכיב שאנחנו רוצים.
מה אנחנו רוצים שהוא ידע לעשות?
- למשוך רכיב
- למשוך רכיב מתוך מודול
- להתקין תלויות (dependency) במידת הצורך
- להעביר מידע ל-Input של רכיב במידת הצורך
🛑 מה שנראה כרגע בדיירקטיב עובד רק במצב DEV, אחרי שנראה איך זה עובד נבצע שינוי מסויים בדיירקטיב וב-MFE1 כדי שזה יעבוד גם במצב PROD ואני אסביר למה זה קורה.
SHELL
כך יראה הדיירקטיב בתצורה החדשה
import { loadRemoteModule } from '@angular-architects/module-federation';
import { createNgModuleRef, Directive, Injector, Input, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appCreator]'
})
export class CreatorDirective {
@Input('appCreator') set data(value: ICompData) {
this.componentInit(value);
}
constructor(private vcr: ViewContainerRef, private injector: Injector) { }
private async componentInit(componentData: ICompData) {
Promise.all([
loadRemoteModule({
type: 'module',
remoteEntry: componentData.mfLink,
exposedModule: './' + componentData.module
}).then(m => {
if (m[componentData.compName]) {
return this.vcr.createComponent(m[componentData.compName], { injector: this.injector });
} else {
const moduleRef = createNgModuleRef(m[componentData.module], this.injector);
const microComp = this.vcr.createComponent<any>(m[componentData.module].ɵmod.exports.find(
(c: any) => { return c.name === componentData.compName }
), { ngModuleRef: moduleRef });
microComp.instance.data = 'HI :)';
return microComp;
}
}).catch(e => { console.warn(e); })
]).catch(e => { console.warn(e); })
}
}
export interface ICompData {
module: string,
mfLink: string,
compName: string
}נעבור על השינויים שבוצעו ונסביר אותם:
שורה 8-10: כמו בתצורה הקודמת רק שהפעם במקום להוציא את המידע למשתנה, אנחנו מפעילים פונקציה ושולחים אליה את המידע.
שורה 19: אנחנו בודקים אם המודול שלנו בעל אותו שם של הרכיב, אם כן אז אנחנו מבינים שלא מדובר במודול שמכיל רכיבים לשליפה אלא מה שיש לנו הוא הרכיב עצמו ואפשר להטמיע אותו בדף.
שורה 22: מכינים את המודול והקשרים שלו לצירוף של הרכיב, כך שאם יש קשרים/ תלויות, הם יותקנו יחד עם הרכיב.
שורה 23: שליפה של הרכיב המתאים על ידי בדיקה של exports, אני רק רוצה לציין שאין חובה לשלוף משם, ניתן לבצע את הבדיקה גם על declarations. אבל למען הסדר הטוב בצד ה-MFE1 (שנראה בהמשך), אנחנו נסתכל רק על מה שמוחצן החוצה מהמודול.
אחרי שמצאנו את הרכיב שלנו, נחזיר אותו ליצירת הרכיב בצירוף הקשרים מהמודול.
שורה 26: זו דוגמה לאיך אפשר להעביר מידע לתוך הרכיב במידה שיש לו INPUT. ניתן כמובן להוסיף אותה להתקנת הרכיב בשורה 20 ורק אז לבצע return.
שורה 36: זו התוספת לאינטרפייס כדי שנוכל לשלוף רכיב מתוך מודול שלם.
בהתאם לשינויים נעדכן גם את המידע שנכנס לדיירקטיב.
<app-content [componentData]="main"></app-content> <app-content [componentData]="side"></app-content> <app-content [componentData]="partOne"></app-content>
import { Component, OnInit } from '@angular/core';
import { ICompData } from 'src/app/directives/creator.directive';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.scss']
})
export class HomeComponent implements OnInit {
public main: ICompData = { module: 'MainContentComponent', mfLink: 'http://localhost:3001/mfe2.js', compName: 'MainContentComponent' };
public side: ICompData = { module: 'SideContentComponent', mfLink: 'http://localhost:3001/mfe2.js', compName: 'SideContentComponent' };
public partOne: ICompData = { module: 'CollectionModule', mfLink: 'http://localhost:3000/mfe1.js', compName: 'PartOneComponent' };
constructor() { }
ngOnInit(): void {
}
}
שימו לב, במקרה שלנו, בגלל שאנחנו מכריחים לשלוח את compName אז במידה ששלחנו רכיב, שם המודול ושם הרכיב יהיו אותו דבר.
במקרה שאנחנו שולחים מודול ורוצים לחלץ ממנו רכיב כמו בשורה 12, אז שם המודול ושם הרכיב שונים.
MFE1
ניצור מודול חדש ב- src > app > components
ng g m collection
ניכנס לספריה שנוצרה (collection), בתוך הספריה יש את המודול collection.module.ts , בספריה ניצור 2 רכיבים חדשים:
ng g c part-one ng g c part-two
נכנס למודול ונעדכן אותו כך:
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { PartOneComponent } from './part-one/part-one.component';
import { PartTwoComponent } from './part-two/part-two.component';
const DEC_AND_EXP = [
PartOneComponent,
PartTwoComponent
]
@NgModule({
declarations: [
[...DEC_AND_EXP]
],
imports: [
CommonModule
],
exports: [
[...DEC_AND_EXP]
]
})
export class CollectionModule { }
הרעיון כאן הוא, במקום לשכפל את כל הרכיבים שיש ב-declarations ל-exports , אנחנו ניצור משתנה ששם נעדכן את הרכיבים החדשים והעתקים שלו יהיו גם ב-declarations וגם ב-exports.
כדי שהמודול יעבור קימפול, נצרף אותו למודול הראשי
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { SubPageComponent } from './pages/sub-page/sub-page.component';
import { CollectionModule } from './components/collection/collection.module';
@NgModule({
declarations: [
AppComponent,
SubPageComponent,
],
imports: [
BrowserModule,
AppRoutingModule,
CollectionModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
לא משנה איך הקובץ שלכם נראה, השורה שמעניינת אותנו היא שורה 17.
ולבסוף, נעדכן גם את הקובץ שמחצין את המודול החוצה:
new ModuleFederationPlugin({
library: { type: "module" },
// For remotes (please adjust)
name: "mfe1",
filename: "mfe1.js",
exposes: {
'./MainModule': path.resolve(__dirname, 'src/app/pages/main/main.module.ts'),
'./CollectionModule': path.resolve(__dirname, 'src/app/components/collection/collection.module.ts')
},
shared: share({
"@angular/core": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/common/http": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
}),גם כאן, השורה שמעניינת אותנו היא שורה 9.
לא לשכוח לעצור את הקומפיילר ולהפעיל מחדש.
!!! הבעיה היא שכאשר אנחנו במצב פיתוח, המודול שלנו (collectionModule) מגיע שאכן הוא מאכלס את כל הרכיבים כמו שכותבים אותם במודול עצמו, ולכן שורה 23 בדיירקטייב יכולה לעבוד ולשלוף את הרכיב החוצה.
אבל במצב PROD, שזה המצב שבסופו של דבר תגיעו (בסיום הפיתוח) אין יותר TS ואין יותר חלוקה לרכיבים, אלא כל הקבצים מקומפלים לבלוקים של JS ואם אין הפרדה של הרכיב הספציפי החוצה לקובץ משלו אנחנו צריכים לטעון את כל המודול עם כל הרכיבים בו, כי הם גוש אחד של קוד. !!!
אז מה עושים? כרגע האופציה היחידה שמצאתי שעובדת היא להחצין כל רכיב בנפרד ולהחצין את המודול במידה שאנחנו רוצים להעביר תלויות של רכיבים במודולים שונים שלא בהכרח יהיו זמינים בפרויקט שמושך את הרכיב שלנו.
המצב הסופי של הדיירקטיב
import { loadRemoteModule } from '@angular-architects/module-federation';
import { createNgModuleRef, Directive, Injector, Input, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appCreator]'
})
export class CreatorDirective {
@Input('appCreator') set data(value: ICompData) {
this.componentInit(value);
}
constructor(private vcr: ViewContainerRef, private injector: Injector) { }
private componentInit(componentData: ICompData) {
return Promise.all([
componentData.module === componentData.compName ?
loadRemoteModule({
type: 'module',
remoteEntry: componentData.mfLink,
exposedModule: './' + componentData.module
}).then((m: any) => {
return this.vcr.createComponent(m[componentData.compName], { injector: this.injector });
}) :
loadRemoteModule({
type: 'module',
remoteEntry: componentData.mfLink,
exposedModule: './' + componentData.module
}).then(m => {
const moduleRef = createNgModuleRef(m[componentData.module], this.injector);
Promise.all([
loadRemoteModule({
type: 'module',
remoteEntry: componentData.mfLink,
exposedModule: './' + componentData.compName
})
]).then((c: any) => {
const microComp = this.vcr.createComponent<any>(c[0][componentData.compName], { ngModuleRef: moduleRef });
return microComp;
})
}).catch(e => { console.warn(e); })
]).catch(e => { console.warn(e); })
}
}
export interface ICompData {
module: string,
mfLink: string,
compName: string
}שורה 14: אנחנו בודקים האם שם הרכיב זהה לשם המודול, במידה שכן מדובר ברכיב בודד ללא צורך בתלויות ולכן אפשר ישר למשוך אותו וליצר אותו.
שורה 21: במידה שהם לא שוום זה אומר שיש לנו מודול ואנחנו רוצים להתקין רכיב בתוספת תלויות שיש במודול
ולכן יש 2 קריאות של פרומיס, הראשונה היא עבור המודול, כדי להכין את התלויות. הקריאה השניה היא עבור הרכיב המרוחק וחיבור של תלויות מהמודול אליו לפני ההקמה שלו.
שורה 27: יצירה של משתנה שיחזיק את התלויות עם כל המודול של הרכיב.
שורה 28: הקריאה השניה לרכיב עצמו.
שורה 35: יצירת הרכיב יחד עם התלויות של המודול שלו.
בנוסף צריך לעשות שינוי נוסף ב-MFE1
exposes: {
'./MainModule': path.resolve(__dirname, 'src/app/pages/main/main.module.ts'),
'./CollectionModule': path.resolve(__dirname, 'src/app/components/collection/collection.module.ts'),
'./PartOneComponent': path.resolve(__dirname, 'src/app/components/collection/part-one/part-one.component.ts')
},זהו זה! סיימנו.
אם לא פיספסנו משהו, אנחנו אמורים לראות את part-one על הדף.
בחלק הבא, נראה איך להעביר מידע בין רכיבים ששיכים למיקרו-פרונטאנדים שונים.✨






הי,
נהניתי מאד מהמאמרים על Module Federation
תוכל בבקשה לתת לי יותר פירוט איך מיצאים סרוויס ?
תודה רבה
היי,
סרביסים הם פונקציות משותפות לפרויקט, עד כמה שאני יודע, לא ניתן לשתף סרוויס.
הוא לא יהיה זמין לך בפרויקטים שקוראים לו בגלל שהוא לא קיים באותם פרויקטים.
אם היה אפשר ליצא סרביס, משמע לשתף אותו בין מספר פרויקטים שונים, אז לא היינו צריכים לשתף מידע באיוונטים או איחסון מקומי.
כאשר מבצעים קריאה לרכיב והוא תלוי בסרביס (מיבא אותו לתוך הרכיב) אז גם הקוד של הסרביס מגיע ביחד איתו, אבל הוא אינסטאנס חדש ולא משותף עם פרויקטים מיקרו אחרים.
צריך לזכור שכל פרויקט הוא סוג של אי די סגור , נכון שניתן להעביר אליו נתונים בזמן היצירה שלו בפרויקט היעד , אבל הוא עדיין אי והוא לא משתף מידע עם איים אחרים.