בחלק זה נראה איך למשוך רכיב מפרויקט אחד ולהטמיע אותו בפרויקט שני ללא צורך בשינוי ניתוב.
כאשר נרצה לגשת לתהליך הזה בצורה סטנדרטית נראה שיש בעיה אחת קטנה וממש מציקה, כל רכיב או מודול שנרצה למשוך יסומן באדום מהסיבה הפשוטה שהוא לא ממש קיים בפרויקט שמושך אותו ולכן בקומפיילר מעלה שגיאה שאין רכיב כזה.
הפיתרון הראשון והפחות טוב הוא להקים קובץ declaration ולעדכן שם כל רכיב ומודול שמגיע מבחוץ 😂…
כן… אנחנו לא הולכים לעשות משהו כזה, אנחנו מפתחים משמע אנחנו "עצלנים" וצריכים שמשהו יעשה את זה בשבילנו.
הפיתרון השני זה להקים directive והוא יטפל בכל התהלך ויחזיר את הרכיב ל-HTML שלנו.
MFE2
בתוך ספרית APP ניצור ספריה חדשה בשם component ונפתח טרמינל בספריה הזו (CMD).
ניצור 2 רכיבים בספריה :
ng g c main-content ng g c side-content
נחשוף אותם:
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const mf = require("@angular-architects/module-federation/webpack");
const path = require("path");
const share = mf.share;
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, 'tsconfig.json'),
[/* mapped paths to share */]);
module.exports = {
output: {
uniqueName: "mfe2",
publicPath: "auto"
},
optimization: {
runtimeChunk: false
},
resolve: {
alias: {
...sharedMappings.getAliases(),
}
},
experiments: {
outputModule: true
},
plugins: [
new ModuleFederationPlugin({
library: { type: "module" },
// For remotes (please adjust)
name: "mfe2",
filename: "mfe2.js",
exposes: {
'./MainContentComponent': path.resolve(__dirname, './src/app/component/main-content/main-content.component.ts'),
'./SideContentComponent': path.resolve(__dirname, './src/app/component/side-content/side-content.component.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()
})
}),
sharedMappings.getPlugin()
],
};
הקובץ הזה כבר קיים לכם, צריך רק להוסיף את שורה 35 ו-36.
סיימנו את הצד הזה, עכשיו נעבור לצד שמושך את הרכיבים.
SHELL
בתוך APP ניצור ספריה בשם directives ובתוכה נפתח טרמינל (CMD) ונריץ את הפקודה הבאה:
ng g d creator
נעדכן את הדיירקטיב שלנו כך:
import { loadRemoteModule } from '@angular-architects/module-federation';
import { Directive, Input, ViewContainerRef } from '@angular/core';
@Directive({
selector: '[appCreator]'
})
export class CreatorDirective {
private componentData: ICompData = {} as ICompData;
@Input('appCreator') set data(value: ICompData) {
this.componentData = value;
}
constructor(private viewContainerRef: ViewContainerRef) { }
ngOnInit() {
return this.componentInit()
}
private async componentInit() {
Promise.all([
loadRemoteModule({
type: 'module',
remoteEntry: this.componentData.mfLink,
exposedModule: './' + this.componentData.module
}).then(m => {
console.log('m', m);
return this.viewContainerRef.createComponent(m[this.componentData.module]);
}).catch(e => {
console.log(e);
})
]).catch(e => {
console.log(e);
})
}
}
export interface ICompData {
module: string,
mfLink: string
}שורה 9 – 13: לא חובה לכתוב את זה כך, אפשר להשתמש ב-data בשורה 11 כאינפוט שהוא גם המשתנה שעלה עובדים ואז אין צורך בשורה 9 ואין צורך לבצע SET.
אני עושה את זה כהכנה ל"מזגן". כן חשוב לשים את השם של הדיירקטיב כאליאס בתוך ה-input, אם תרצו להשתמש בו כמו שנראה ממש עוד מעט.
שורה 21: בגלל ששורה 23 מחזירה פרומיס אנחנו צריכים לעדכן את הפונקציה שלנו כמשהו לא סינכרוני.
שורה 22: ממתין שכל התהליכים יסתיימו לפני שתחזור תשובה.
שורה 23: פונקציה באדיבות module federation , מקבלת את הנתונים שאנחנו כבר מכירים כדי למשוך את המודול המתאים.
- type במקרה שלנו תמיד מודול
- remoteEntry המיקום של הקובץ והשם שלו
- exposedModule שם המודול שכתבנו ב-MFE2 בקובץ webpack.config.js (שורות 35+36).
שורה 29: אחרי שמשכנו את המודול הנכון, אנחנו רוצים להוציא את הרכיב מתוכו. שם הרכיב הוא לא השם שכתוב ב-exposes אלא שם ה-CLASS ברכיב עצמו שיש עליו export
MFE2
export class MainContentComponent implements OnInit {
constructor() { }
ngOnInit(): void {
}
}שורה 1: זה השם של הרכיב שלו נקרא ב-SHELL.
ולכן, אם שם ה-expos ושם ה-export אותו דבר אנחנו יכולים לבצע את שורה 29 כמו שהיא כתובה.
במידה שנרצה לנתק את הקשר הזה, אפשר להוסיף לאינטרפייס ערך חדש בשם name ולשנות את שורה 29
מ- m[this.componentData.module] ל- m[this.componentData.name].
כרגע נמשיך כמו שזה כתוב למעלה.
שורה 40 – 43: הגדרה של אינטרפייס, לא לשכוח לשים אותו ב-export כדאי שמקומות אחרים ימשכו אותו.
ב-SHELL, תחת APP ניצור ספריה בשם components, נפתח CMD בתוך components ונריץ את הפקודה:
ng g c content
לאחר מכן נבצע את השינויים הבאים:
import { Component, Input, OnInit } from '@angular/core';
import { ICompData } from 'src/app/directives/creator.directive';
@Component({
selector: 'app-content',
templateUrl: './content.component.html',
styleUrls: ['./content.component.scss']
})
export class ContentComponent implements OnInit {
private _componentData: ICompData = {} as ICompData;
@Input() set componentData(value: ICompData) {
this._componentData = value;
}
get componentData() {
return this._componentData;
}
constructor() { }
ngOnInit(): void {
}
}
<div class="border">
<p>content works!</p>
<ng-template [appCreator]="componentData" *ngIf="componentData.mfLink">
</ng-template>
</div>.border {
border: 1px solid rebeccapurple;
margin: 5px;
}
אפשר להגיד שהכל מוכן, עכשיו כל מה שאנחנו צריכים זה לשים את content באיזה html שנרצה וניתן לו את המידע שהוא צריך.
בפרויקט SHELL נפתח בתוך ספרית APP, ספריה נוספת בשם PAGES ובתוכה נפעיל טרמינל
ng g c home
את הרכיב home נשנה כך:
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' };
public side: ICompData = { module: 'SideContentComponent', mfLink: 'http://localhost:3001/mfe2.js' };
constructor() { }
ngOnInit(): void {
}
}
<p>home works!</p> <app-content [componentData]="main"></app-content> <app-content [componentData]="side"></app-content>
דבר אחרון, נעדכן את הראוט שלנו כדי שיעלה את HOME
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HomeComponent } from './pages/home/home.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{
path: 'lazy', loadChildren: () => import('mfe1/MainModule').then(m => {
console.log(m);
return m.MainModule;
})
}
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule { }
כאשר הפרויקט יעלה, אנחנו נראה את דף הבית עם עוד 2 רכיבים שהגיעו מ-MFE2.
בחלק הבא נשנה את הדיירקטיב כך שידע להתמודד עם רכיב או עם מודול לבד (נעשה אותו קצת יותר חכם).😵





