מיקרו פרונטאנד

מידע על הארכיטקטורה החדשה שסוחפת מפתחי פרונט אנד - גם הסבר תיאורטי וכמובן שגם דוגמה מעשית פשוטה.

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

כשאנו מדברים על ארכיטקטורה – הדבר הראשון שאנחנו צריכים לשאול את עצמנו זה איזו בעיות היא פותרת לנו. הדבר השני שאנו צריכים לשאול את עצמנו זה איזו בעיות היא יוצרת לנו. אין כזה דבר ארכיטקטורה מושלמת שאין בה שום בעיות והיא Silver bullet. יש ארכיטקטורה שמתאימה לבעיה מסוימת שאנו רוצים לפתור או שלפחות היא הכי מתאימה.

איזו בעיה מיקרו פרונט אנד פותרת?

בעוד שריאקט (או אנגולר, או vue, אבל פה אני משתמש בריאקט) היא מעולה, כמו כל קוד – היא בסכנה להתנפח. אם אני לבדי או בצוות קטן עובדים על אפליקצית ריאקט, אנו לא נראה את זה. אבל אם כבר מתחילים להיות כמה צוותים שעובדים על אפליקצית ריאקט אחת – הם מתחילים לדרוך אחד על השני. או שתהליכי ה-CI\CD (הבדיקות והדיפלוימנט) הופכים לארוכים מדי. בגדול, בדיוק כמו בצד שרת, גם בצד לקוח יכול להיות לנו מונוליט – כלומר כמות עצומה של קוד שהופך להיות מאוד תלוי במרכיבים שונים והופך להיות מאוד מאוד כואב לשינוי. מיקרו פרונט אנד, בהכללה, זה המיקרו סרוויסים של הפרונט.

תיאור כללי של דרך הפתרון

באמצעים שונים (שאותם אסקור בהמשך) אנו מחלקים את האפליקציה הריאקטית הגדולה שלנו לכמה חלקים. למשל אפליקציה שאחראית רק על ה-Header, אפליקציה שאחראית רק על האנליטיקות, אפליקציה שאחראית רק על עמוד מסוים או אפליקצית "עמוד" כללית ואפליקצית "פרטי לקוח" כללית.

אני אתן דוגמה מהאתר הזה – תדמיינו שהוא אתר עצום שיש בו גם פרופיל לקוחות, גם סל קניות, גם דף שירותים וכו'. אני יכול לבנות אותו כאפליקצית ריאקט ענקית או לחלק אותו כשכל חלק זה אפליקציה נפרדת/Iframe שיכולים לתקשר בינהם או לשאוב מידע ממקור מרכזי אחד.

כל אפליקציה נפרדת יכולה להבדק כאפליקציה נפרדת, לעבור תהליך דיפלוימנט נפרד עם גרסאות נפרדות.

איזו בעיות מיקרו פרונט אנד כן יוצר לנו?

כאמור אין שיטה שהיא חפה מבעיות. הבעיה המרכזית היא שיתוף מידע בין מיקרו פרונט אנד שונים. בגדול למרות שטכנית זה אפשרי – הארכיטקטורה גורסת שלא אמור להיות שיתוף מידע כזה אבל לפעמים כן צריך. דרך הפתרון היא להמנע במידת האפשר מתקשורת בין המיקרופרנט אנד ולבנות סרוויס שמשתף מידע ולהמנע מתלויות.

הבעיה השניה היא חוסר אחידות בעיצוב והמצאת הגלגל מחדש. ברגע שמדובר באפליקציות נפרדות שמפותחות על ידי צוותים נפרדים, יש סיכוי גבוה לשונות ויזואלית בין מרכיבים דומים. את הבעיה הזו אפשר לפתור או עם ספרית קומפוננטות כמו Material UI למשל או אחת שתיצרו בעצמכם שאפשר לצרוך אותה ישירות או דרך Federated Module.

דוגמה טכנית – איך זה עובד

כדי להדגים אני אשתמש בריאקט (אפשר כמובן לממש בכל פריימוורק/ספריה) ו Create React App. אני אצור מיקרו פרונטאנד של footer ושל header.

ראשית אני אצור תיקיה ובתוכנה אצור את הקונטיינר ואת שתי האפליקציות המכילות את ה-footer ואת ה-header:

npx create-react-app container –template typescript
npx create-react-app header –template typescript
npx create-react-app footer –template typescript

נתחיל עם הקונטיינר. זו האפליקציה הראשית שלנו. זו שכוללת את כל המיקרופרונט אנד ואחראית לטעון אותן. ישנן כמה דרכים לבצע ניהול וטעינה של מיקרו פרונטאנד:

טעינה בזמן הבילד – ממש לשים את האפליקציות כתלויות באפליקציה הראשית – יתרון – די פשוט להבנה, חסרון – די מעקר את המיקרופרונט אנד כי בסופו של דבר הכל מתנקז לאותו בילד. אז פחות לעניין.

טעינה בזמן ריצה – עם iframe (לא להקיא בבקשה) – יש לזה יתרונות – מאוד פשוט לנהל ולהבין את הסיפור הזה. החסרונות הם ברורים – זה טיפה'לה בעייתי לתקשר בינהם אם צריך ושנית יש מגבלות של אייפריים (מבחינה גרפית, למשל מודלים ותפריטים אחרים).

טעינה בזמן ריצה – עם src – במקרה הזה זו בחירה די קלאסית

<script src="https://footer.example.com/bundle.js"></script>
<script src="https://header.example.com/bundle.js"></script>

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

אנחנו נבחר בטעינה בזמן ריצה עם src. בואו וניצור את footer ואת header. עבור הדוגמה אני רק אכניס טקסט של This is Footer במיקרופרונט אנד של הפוטר וטקסט של This is Header במיקרופרונט אנד של ה-Header.

עורך קוד שיש בו דוגמה של ה-App.tsx של כל מיקרופרונט אנד עם This is the Header באפליקצית Header.
עורך קוד שיש בו דוגמה של ה-App.tsx של כל מיקרופרונט אנד עם This is the Header באפליקצית Header.

אני יכול להריץ כל אפליקציה כזו בנפרד עם npm start או yarn start. תראה כמובן אפליקציה מאוד מאוד משמעותית אבל כזו שרצה בפורט 3000:

אני יכול כמובן לבחור את הפורט שבו האפליקציה רצה ואעשה זאת. אני אבחר את פורט 3001 עבור ה-Header ופורט 3002 עבור ה-Footer. פורט 3000 יישמר עבור הקונטיינר. איך אני בוחר את הפורט? באמצעות הגדרה של PORT. למשל:

  "scripts": {
    "start": "PORT=3002 react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

נגדיר PORT=3002 בפוטר ו-PORT 3001 ב-header.

השלב השני הוא למנוע code splitting. אנחנו צריכים קובץ ג'אווהסקריפט יחיד. את זה אי אפשר לעשות עם create react app שהוא לא ejected (כלומר שהסקריפטים של create react app מנותקים ממנו) ואנו צריכים לשנות את קבצי הקונפיגורציה על מנת לעשות את זה. איך נעשה את זה? אפשר עצמאית אבל אני מעדיף עם react-app-rewired – מודול קטן וחביב שמאפשר לשנות קונפיגורציה ואיך Create React App מתנהגת גם מבלי לעשות לה eject. אני אתקין אותה גם ב-footer וגם ב-header.

npm install react-app-rewired

אני אצור config-overrides.js ב-root של כל מיקרו פרונטאנד (כלומר גם ב-header וגם ב-footer) ושם אני אורה ל-create react app ליצור קובץ אחד:

module.exports = {
  webpack: (config, env) => {
    config.optimization.runtimeChunk = false;
    config.optimization.splitChunks = {
      cacheGroups: {
        default: false,
      },
    };

    config.output.filename = "static/js/[name].js";

    config.plugins[5].options.filename = "static/css/[name].css";
    config.plugins[5].options.moduleFilename = () => "static/css/main.css";
    return config;
  },
};

זה בעצם קינפוג של הוובפק כדי שיהיה לנו entrypoint אחיד.

אחרי שעשיתי את זה, אני אחליף את הפקודה react-scripts ב-react-app-rewire ב-package.json והוא יראה כך:

{
  "name": "footer",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@testing-library/jest-dom": "^5.16.4",
    "@testing-library/react": "^13.3.0",
    "@testing-library/user-event": "^13.5.0",
    "@types/jest": "^27.5.2",
    "@types/node": "^16.11.47",
    "@types/react": "^18.0.15",
    "@types/react-dom": "^18.0.6",
    "react": "^18.2.0",
    "react-app-rewired": "^2.2.1",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "typescript": "^4.7.4",
    "web-vitals": "^2.1.4"
  },
  "scripts": {
    "start": "PORT=3002 react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-app-rewired eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

השינויים האלו משפיעים על הבילד בלבד. אם אני עכשיו אריץ npm run build, אני אוכל לראות בקובץ asset-manifest.json שיש לי entrypoint אחיד:

{
  "files": {
    "main.css": "/static/css/main.css",
    "main.js": "/static/js/main.js",
    "static/js/787.69859e86.chunk.js": "/static/js/787.69859e86.chunk.js",
    "index.html": "/index.html",
    "main.css.map": "/static/css/main.css.map",
    "main.js.map": "/static/js/main.js.map",
    "787.69859e86.chunk.js.map": "/static/js/787.69859e86.chunk.js.map"
  },
  "entrypoints": [
    "static/css/main.css",
    "static/js/main.js"
  ]
}

השלב האחרון הוא לשנות ה-ID של אלמנט האב בכל אפליקציה – גם ה-Header וגם ה-Footer. איך עושים את זה? מאוד פשוט – ב-index.html בתיקית public במקום:

<div id="root"></div>

במיקרו פרונטאנד של Footer וגם במיקרו פרונטאנד של Header. חשוב לבחור שם אחיד עם סיומת אחידה למשל:

<div id="footerApp"></div>

וגם:

<div id="headerApp"></div>

אחרי שהחלפנו את השם ב-index.html, חשוב לא לשכוח ולהחליף את השם גם ב-index.tsx מ:

document.getElementById('root') as HTMLElement

אל:

document.getElementById('headerApp') as HTMLElement

ובהתאמה:

document.getElementById('footerApp') as HTMLElement

נבדוק באמצעות npm start שהכל עובד ובאמת כל השינויים הם כמו שצריך. כל מיקרופרונט אנד יכול לרוץ עצמאית בפורט שלו. 3001 ל-header ו-3002 ל-footer.

עכשיו צריך לחזור לקונטיינר כדי להכניס בו טעינה כמו שצריך למיקרופרונט אנד – בזמן ריצה וגם אחרי הבילד. זה כבר יותר פשוט, במיוחד אם עושים את זה באופן נאיבי, כפי שאנו נעשה. בלי ראוטינג וכאב ראש. השלב הראשון הוא להגדיר באפליקצית הקונטיינר את הסביבה שלנו והכתובות. ניצור env לוקלי לפיתוח. קובץ env. פשוט שנראה כך:

REACT_APP_HEADER_HOST=http://localhost:3001
REACT_APP_FOOTER_HOST=http://localhost:3002

ועכשיו לעבודת הקידוד. במקרה הזה, אנחנו צריכים לטעון את שתי האפליקציות ואת זה אנו נעשה ראשית עם יצירת קומפוננטת טעינה בשם MicroFrontend.tsx שתהיה ב-root של קומפוננטת ה-container:

import { useEffect } from 'react';

interface Props {
  name: string;
  host: string;
}

const MicroFrontend = ({ name, host }: Props) => {
  useEffect(() => {
    const scriptId = `micro-frontend-script-${name}`;

    const renderRemoteJS = async () => {
      const manifest = await fetch(`${host}/asset-manifest.json`).then((res) =>
        res.json()
      );
      const script = document.createElement('script');
      script.id = scriptId;
      script.crossOrigin = '';
      script.src = `${host}${manifest.files['main.js']}`;
      document.head.appendChild(script);
    };

    renderRemoteJS();

  }, [name, host]);

  return <main id={`${name}App`} />;
};

export default MicroFrontend;

הקומפוננטה הזו בסופו של דבר טוענת את main.js לפי כתובת שאנו ניתן לה ויוצרת גם אלמנט אב עם id בדומה ל-HTML של המיקרו פרונטאנד שכבר שינינו בכל אפליקציה בנפרד.

השלב הבא הוא פשוט לקרוא לה במקום המתאים עם המיקרו פרונטאנד שאנו רוצים. איפה? ב-App.tsx של קומפוננטת הקונטיינר:

import "./App.css";
import MicroFrontend from "./MicroFrontend";

const { REACT_APP_HEADER_HOST: headerHost, REACT_APP_FOOTER_HOST: footerHost } =
  process.env;

function App() {
  return (
    <div>
      <MicroFrontend host={headerHost as string} name="header" />
      <MicroFrontend host={footerHost as string} name="footer" />
    </div>
  );
}

export default App;

ו… זה הכל! כדי שהכל יעבוד אנו צריכים לפתוח שלושה אינסטנסים של הקומנד ליין וללחוץ על npm run בכולם. אם הכל יהיה תקין? נראה בפורט 3000 את שני המיקרו פרונטאנד שלנו. כל מיקרו פרונטאנד רץ עצמאית ואפשר לפתח אותו בנפרד. כאשר אני רוצה להעלות לפרודקשן, אני אצור env לפרודקשן שלי שבמקום local host יכיל את הכתובות האמיתיות.

אבל כמובן שנזכור שזו דוגמת Hello World. מה עם ראוטינג? מה קורה אם אני רוצה לחלוק רק מודולים ספציפיים? זה? זה במאמר המשך.

פוסטים נוספים שכדאי לקרוא

בינה מלאכותית

להריץ ממשק של open-webui על הרספברי פיי

להפעיל ממשק של צ׳אט ג׳יפיטי שאפשר לגשת אליו מכל מחשב ברשת הביתית על רספברי פיי עם מודל בשם tinydolphin שרץ על רספברי פיי.

גלילה לראש העמוד