Πώς να αναπτύξετε και να αναπτύξετε Micro-Frontends με Single-SPA

Οι μικρο-frontends είναι το μέλλον της ανάπτυξης ιστοσελίδων front-end.

Εμπνευσμένο από μικροσυσκευές, οι οποίες σας επιτρέπουν να χωρίσετε το backend σας σε μικρότερα κομμάτια, τα micro-frontend σας επιτρέπουν να δημιουργήσετε, να δοκιμάσετε και να αναπτύξετε κομμάτια της εφαρμογής frontend σας ανεξάρτητα το ένα από το άλλο.

Ανάλογα με το πλαίσιο micro-frontend που επιλέγετε, μπορείτε ακόμη και να έχετε πολλές εφαρμογές micro-frontend - γραμμένες σε React, Angular, Vue ή οτιδήποτε άλλο - συνυπάρχουν ειρηνικά μαζί στην ίδια μεγαλύτερη εφαρμογή.

Σε αυτό το άρθρο, θα αναπτύξουμε μια εφαρμογή που αποτελείται από micro-frontends χρησιμοποιώντας single-spa και θα την αναπτύξουμε στο Heroku.

Θα δημιουργήσουμε συνεχή ενοποίηση χρησιμοποιώντας το Travis CI. Κάθε αγωγός CI θα ομαδοποιήσει τη JavaScript για μια εφαρμογή micro-frontend και στη συνέχεια θα ανεβάσει τα προκύπτοντα αντικείμενα κατασκευής στο AWS S3.

Τέλος, θα κάνουμε μια ενημέρωση σε μία από τις εφαρμογές micro-frontend και θα δούμε πώς μπορεί να αναπτυχθεί στην παραγωγή ανεξάρτητα από τις άλλες εφαρμογές micro-frontend.

Επισκόπηση της εφαρμογής επίδειξης

Επίδειξη εφαρμογής - τελικό αποτέλεσμα

Πριν συζητήσουμε τις αναλυτικές οδηγίες, ας ρίξουμε μια γρήγορη επισκόπηση του τι αποτελεί η εφαρμογή επίδειξης. Αυτή η εφαρμογή αποτελείται από τέσσερις υπο-εφαρμογές:

  1. Μια εφαρμογή κοντέινερ που χρησιμεύει ως κοντέινερ της κύριας σελίδας και συντονίζει την τοποθέτηση και αποσύνδεση των εφαρμογών micro-frontend
  2. Μια εφαρμογή micro-frontend navbar που είναι πάντα παρούσα στη σελίδα
  3. Μια εφαρμογή "σελίδα 1" micro-frontend που εμφανίζεται μόνο όταν είναι ενεργή
  4. Μια εφαρμογή μικρο-frontend "σελίδα 2" που εμφανίζεται επίσης μόνο όταν είναι ενεργή

Όλες αυτές οι τέσσερις εφαρμογές ζουν σε ξεχωριστά repos, διαθέσιμα στο GitHub, με το οποίο έχω συνδέσει παραπάνω.

Το τελικό αποτέλεσμα είναι αρκετά απλό όσον αφορά τη διεπαφή χρήστη, αλλά, για να είμαστε σαφείς, η διεπαφή χρήστη δεν είναι το σημείο εδώ.

Εάν ακολουθείτε το δικό σας μηχάνημα, μέχρι το τέλος αυτού του άρθρου θα έχετε επίσης όλες τις απαραίτητες υποδομές για να ξεκινήσετε με τη δική σας εφαρμογή micro-frontend.

Εντάξει, πιάσε το δικό σου εξοπλισμό, γιατί είναι ώρα να βουτήξεις

Δημιουργία της εφαρμογής κοντέινερ

Για να δημιουργήσουμε τις εφαρμογές για αυτό το demo, θα χρησιμοποιήσουμε ένα εργαλείο διεπαφής γραμμής εντολών (CLI) που ονομάζεται create-single-spa. Η έκδοση του create-single-spa κατά τη στιγμή της σύνταξης είναι 1.10.0 και η έκδοση του single-spa που εγκαθίσταται μέσω του CLI είναι 4.4.2.

Θα ακολουθήσουμε αυτά τα βήματα για να δημιουργήσουμε την εφαρμογή κοντέινερ (επίσης μερικές φορές ονομάζεται root config):

mkdir single-spa-demo cd single-spa-demo mkdir single-spa-demo-root-config cd single-spa-demo-root-config npx create-single-spa

Στη συνέχεια, θα ακολουθήσουμε τις οδηγίες CLI:

  1. Επιλέξτε "single spa root config"
  2. Επιλέξτε "νήματα" ή "npm" (επέλεξα "νήματα")
  3. Εισαγάγετε ένα όνομα οργανισμού (χρησιμοποίησα το "thawkin3", αλλά μπορεί να είναι ό, τι θέλετε)

Μεγάλος! Τώρα, εάν ρίξετε μια ματιά στον single-spa-demo-root-configκατάλογο, θα πρέπει να δείτε μια εφαρμογή διαμόρφωσης root skeleton Θα το προσαρμόσουμε λίγο, αλλά πρώτα ας χρησιμοποιήσουμε επίσης το εργαλείο CLI για να δημιουργήσουμε τις άλλες τρεις εφαρμογές micro-frontend.

Δημιουργία εφαρμογών Micro-Frontend

Για να δημιουργήσουμε την πρώτη μας εφαρμογή micro-frontend, το navbar, θα ακολουθήσουμε τα εξής βήματα:

cd .. mkdir single-spa-demo-nav cd single-spa-demo-nav npx create-single-spa

Στη συνέχεια, θα ακολουθήσουμε τις οδηγίες CLI:

  1. Επιλέξτε "εφαρμογή / δέμα με μονό σπα"
  2. Επιλέξτε "αντίδραση"
  3. Επιλέξτε "νήματα" ή "npm" (επέλεξα "νήματα")
  4. Εισαγάγετε ένα όνομα οργανισμού, το ίδιο που χρησιμοποιήσατε κατά τη δημιουργία της εφαρμογής root config ("thawkin3" στην περίπτωσή μου)
  5. Εισαγάγετε ένα όνομα έργου (χρησιμοποίησα "single-spa-demo-nav")

Τώρα που δημιουργήσαμε την εφαρμογή navbar, μπορούμε να ακολουθήσουμε αυτά τα ίδια βήματα για να δημιουργήσουμε τις εφαρμογές δύο σελίδων. Αλλά, θα αντικαταστήσουμε κάθε μέρος που βλέπουμε το "single-spa-demo-nav" με το "single-spa-demo-page-1" για πρώτη φορά και στη συνέχεια με το "single-spa-demo-page-2" το δεύτερη φορά.

Σε αυτό το σημείο έχουμε δημιουργήσει και τις τέσσερις εφαρμογές που χρειαζόμαστε: μία εφαρμογή κοντέινερ και τρεις εφαρμογές micro-frontend. Τώρα ήρθε η ώρα να ενώσουμε τις εφαρμογές μας.

Καταχώριση των εφαρμογών Micro-Frontend στην εφαρμογή Container

Όπως αναφέρθηκε προηγουμένως, μία από τις κύριες ευθύνες της εφαρμογής κοντέινερ είναι ο συντονισμός όταν κάθε εφαρμογή είναι "ενεργή" ή όχι. Με άλλα λόγια, χειρίζεται πότε κάθε εφαρμογή πρέπει να εμφανίζεται ή να αποκρύπτεται.

Για να βοηθήσουμε την εφαρμογή κοντέινερ να κατανοήσει πότε πρέπει να εμφανίζεται κάθε εφαρμογή, της παρέχουμε αυτό που ονομάζεται "λειτουργίες δραστηριότητας". Κάθε εφαρμογή έχει μια συνάρτηση δραστηριότητας που απλώς επιστρέφει ένα boolean, true ή false, για το αν η εφαρμογή είναι αυτήν τη στιγμή ενεργή.

Μέσα στον single-spa-demo-root-configκατάλογο, στο activity-functions.jsαρχείο, θα γράψουμε τις ακόλουθες λειτουργίες δραστηριότητας για τις τρεις εφαρμογές micro-frontend.

export function prefix(location, ...prefixes) { return prefixes.some( prefix => location.href.indexOf(`${location.origin}/${prefix}`) !== -1 ); } export function nav() { // The nav is always active return true; } export function page1(location) { return prefix(location, 'page1'); } export function page2(location) { return prefix(location, 'page2'); }

Στη συνέχεια, πρέπει να καταχωρήσουμε τις τρεις εφαρμογές micro-frontend στο single-spa. Για να το κάνουμε αυτό, χρησιμοποιούμε τη registerApplicationσυνάρτηση. Αυτή η συνάρτηση δέχεται τουλάχιστον τρία ορίσματα: το όνομα της εφαρμογής, μια μέθοδο φόρτωσης της εφαρμογής και μια συνάρτηση δραστηριότητας για να προσδιορίσει πότε είναι ενεργή η εφαρμογή.

Μέσα στον single-spa-demo-root-configκατάλογο, στο root-config.jsαρχείο, θα προσθέσουμε τον ακόλουθο κώδικα για να καταχωρήσουμε τις εφαρμογές μας:

import { registerApplication, start } from "single-spa"; import * as isActive from "./activity-functions"; registerApplication( "@thawkin3/single-spa-demo-nav", () => System.import("@thawkin3/single-spa-demo-nav"), isActive.nav ); registerApplication( "@thawkin3/single-spa-demo-page-1", () => System.import("@thawkin3/single-spa-demo-page-1"), isActive.page1 ); registerApplication( "@thawkin3/single-spa-demo-page-2", () => System.import("@thawkin3/single-spa-demo-page-2"), isActive.page2 ); start();

Τώρα που έχουμε ρυθμίσει τις λειτουργίες δραστηριότητας και καταγράψαμε τις εφαρμογές μας, το τελευταίο βήμα για να μπορέσουμε να το εκτελέσουμε τοπικά είναι να ενημερώσουμε τον τοπικό χάρτη εισαγωγής μέσα στο index.ejsαρχείο στον ίδιο κατάλογο.

Θα προσθέσουμε τον ακόλουθο κώδικα μέσα στην headετικέτα για να καθορίσουμε πού μπορεί να βρεθεί κάθε εφαρμογή όταν εκτελείται τοπικά:

  { "imports": { "@thawkin3/root-config": "//localhost:9000/root-config.js", "@thawkin3/single-spa-demo-nav": "//localhost:9001/thawkin3-single-spa-demo-nav.js", "@thawkin3/single-spa-demo-page-1": "//localhost:9002/thawkin3-single-spa-demo-page-1.js", "@thawkin3/single-spa-demo-page-2": "//localhost:9003/thawkin3-single-spa-demo-page-2.js" } }   

Κάθε εφαρμογή περιέχει το δικό της σενάριο εκκίνησης, πράγμα που σημαίνει ότι κάθε εφαρμογή θα εκτελείται τοπικά στον δικό της διακομιστή ανάπτυξης κατά τη διάρκεια της τοπικής ανάπτυξης. Όπως μπορείτε να δείτε, η εφαρμογή navbar βρίσκεται στη θύρα 9001, η εφαρμογή σελίδας 1 βρίσκεται στη θύρα 9002 και η εφαρμογή σελίδας 2 βρίσκεται στη θύρα 9003.

Με τη φροντίδα αυτών των τριών βημάτων, ας δοκιμάσουμε την εφαρμογή μας.

Δοκιμή εκτέλεσης για τοπική εκτέλεση

Για να λειτουργήσει η εφαρμογή μας τοπικά, μπορούμε να ακολουθήσουμε αυτά τα βήματα:

  1. Ανοίξτε τέσσερις καρτέλες τερματικού, μία για κάθε εφαρμογή
  2. Για το root config, στον single-spa-demo-root-configκατάλογο: yarn start(τρέχει στη θύρα 9000 από προεπιλογή)
  3. Για την εφαρμογή nav, στον single-spa-demo-navκατάλογο:yarn start --port 9001
  4. Για την εφαρμογή σελίδας 1, στον single-spa-demo-page-1κατάλογο:yarn start --port 9002
  5. Για την εφαρμογή σελίδας 2, στον single-spa-demo-page-2κατάλογο:yarn start --port 9003

Τώρα, θα περιηγηθούμε στο πρόγραμμα περιήγησης στο // localhost: 9000 για να δείτε την εφαρμογή μας.

Πρέπει να δούμε… κάποιο κείμενο! Σούπερ συναρπαστικό.

Επίδειξη εφαρμογής - κύρια σελίδα

Στην κύρια σελίδα μας, το navbar εμφανίζεται επειδή η εφαρμογή navbar είναι πάντα ενεργή.

Τώρα, ας μεταβούμε στη διεύθυνση // localhost: 9000 / page1. Όπως φαίνεται στις λειτουργίες δραστηριότητάς μας παραπάνω, ορίσαμε ότι η εφαρμογή σελίδας 1 θα πρέπει να είναι ενεργή (εμφανίζεται) όταν η διαδρομή URL ξεκινά με "σελίδα1". Λοιπόν, αυτό ενεργοποιεί την εφαρμογή σελίδας 1 και θα πρέπει να δούμε το κείμενο τόσο για τη γραμμή πλοήγησης όσο και για την εφαρμογή σελίδας 1 τώρα.

Επίδειξη εφαρμογής - διαδρομή σελίδας 1

Για άλλη μια φορά, ας πλοηγηθούμε τώρα στο // localhost: 9000 / page2. Όπως ήταν αναμενόμενο, ενεργοποιεί την εφαρμογή σελίδας 2, οπότε θα πρέπει να δούμε το κείμενο για τη γραμμή πλοήγησης και την εφαρμογή σελίδας 2 τώρα.

Επίδειξη εφαρμογής - διαδρομή σελίδας 2

Κάνοντας μικρές τροποποιήσεις στις εφαρμογές

Μέχρι στιγμής η εφαρμογή μας δεν είναι πολύ συναρπαστική για να το δούμε, αλλά έχουμε μια λειτουργική ρύθμιση μικρο-frontend που λειτουργεί τοπικά Εάν δεν πανηγυρίζετε τώρα στο κάθισμά σας, θα πρέπει να είστε!

Ας κάνουμε κάποιες μικρές βελτιώσεις στις εφαρμογές μας, ώστε να φαίνονται και να συμπεριφέρονται λίγο πιο ωραία.

Καθορισμός των εμπορευματοκιβωτίων στήριξης

Πρώτον, εάν ανανεώνετε τη σελίδα σας ξανά και ξανά κατά την προβολή της εφαρμογής, ενδέχεται να παρατηρήσετε ότι μερικές φορές οι εφαρμογές φορτώνονται εκτός λειτουργίας, με την εφαρμογή σελίδας να εμφανίζεται πάνω από την εφαρμογή navbar.

Αυτό συμβαίνει επειδή δεν έχουμε προσδιορίσει πού πρέπει να τοποθετηθεί κάθε εφαρμογή. Οι εφαρμογές απλώς φορτώνονται από το SystemJS και, στη συνέχεια, όποια εφαρμογή τελειώσει η ταχύτερη φόρτωση προσαρτάται πρώτα στη σελίδα.

Μπορούμε να το διορθώσουμε καθορίζοντας ένα κοντέινερ προσάρτησης για κάθε εφαρμογή κατά την εγγραφή τους.

Στο index.ejsαρχείο μας στο οποίο εργαζόμασταν προηγουμένως, ας προσθέσουμε κάποιο HTML για να χρησιμεύσουμε ως τα κύρια κοντέινερ περιεχομένου για τη σελίδα:

Στη συνέχεια, στο root-config.jsαρχείο μας όπου έχουμε καταχωρίσει τις εφαρμογές μας, ας δώσουμε ένα τέταρτο όρισμα σε κάθε κλήση λειτουργίας που περιλαμβάνει το στοιχείο DOM όπου θα θέλαμε να προσαρτήσουμε κάθε εφαρμογή:

import { registerApplication, start } from "single-spa"; import * as isActive from "./activity-functions"; registerApplication( "@thawkin3/single-spa-demo-nav", () => System.import("@thawkin3/single-spa-demo-nav"), isActive.nav, { domElement: document.getElementById('nav-container') } ); registerApplication( "@thawkin3/single-spa-demo-page-1", () => System.import("@thawkin3/single-spa-demo-page-1"), isActive.page1, { domElement: document.getElementById('page-1-container') } ); registerApplication( "@thawkin3/single-spa-demo-page-2", () => System.import("@thawkin3/single-spa-demo-page-2"), isActive.page2, { domElement: document.getElementById('page-2-container') } ); start();

Τώρα, οι εφαρμογές θα τοποθετούνται πάντα σε μια συγκεκριμένη και προβλέψιμη τοποθεσία. Ομορφη!

Στυλ της εφαρμογής

Next, let’s style up our app a bit. Plain black text on a white background isn’t very interesting to look at.

In the single-spa-demo-root-config directory, in the index.ejs file again, we can add some basic styles for the whole app by pasting the following CSS at the bottom of the head tag:

 body, html { margin: 0; padding: 0; font-size: 16px; font-family: Arial, Helvetica, sans-serif; height: 100%; } body { display: flex; flex-direction: column; } * { box-sizing: border-box; } 

Next, we can style our navbar app by finding the single-spa-demo-nav directory, creating a root.component.css file, and adding the following CSS:

.nav { display: flex; flex-direction: row; padding: 20px; background: #000; color: #fff; } .link { margin-right: 20px; color: #fff; text-decoration: none; } .link:hover, .link:focus { color: #1098f7; }

We can then update the root.component.js file in the same directory to import the CSS file and apply those classes and styles to our HTML. We'll also change the navbar content to actually contain two links so we can navigate around the app by clicking the links instead of entering a new URL in the browser's address bar.

import React from "react"; import "./root.component.css"; export default function Root() { return (   Page 1   Page 2   ); }

We’ll follow a similar process for the page 1 and page 2 apps as well. We’ll create a root.component.css file for each app in their respective project directories and update the root.component.js files for both apps too.

For the page 1 app, the changes look like this:

.container1 { background: #1098f7; color: white; padding: 20px; display: flex; align-items: center; justify-content: center; flex: 1; font-size: 3rem; }
import React from "react"; import "./root.component.css"; export default function Root() { return ( 

Page 1 App

); }

And for the page 2 app, the changes look like this:

.container2 { background: #9e4770; color: white; padding: 20px; display: flex; align-items: center; justify-content: center; flex: 1; font-size: 3rem; }
import React from "react"; import "./root.component.css"; export default function Root() { return ( 

Page 2 App

); }

Adding React Router

The last small change we’ll make is to add React Router to our app. Right now the two links we’ve placed in the navbar are just normal anchor tags, so navigating from page to page causes a page refresh. Our app will feel much smoother if the navigation is handled client-side with React Router.

To use React Router, we’ll first need to install it. From the terminal, in the single-spa-demo-nav directory, we'll install React Router using yarn by entering yarn add react-router-dom. (Or if you're using npm, you can enter npm install react-router-dom.)

Then, in the single-spa-demo-nav directory in the root.component.js file, we'll replace our anchor tags with React Router's Link components like so:

import React from "react"; import { BrowserRouter, Link } from "react-router-dom"; import "./root.component.css"; export default function Root() { return (    Page 1   Page 2    ); }

Cool. That looks and works much better!

Επίδειξη εφαρμογής - με στυλ και χρήση του React Router

Getting Ready for Production

At this point we have everything we need to continue working on the app while running it locally. But how do we get it hosted somewhere publicly available?

There are several possible approaches we can take using our tools of choice, but the main tasks are:

  1. to have somewhere we can upload our build artifacts, like a CDN, and
  2. to automate this process of uploading artifacts each time we merge new code into the master branch.

For this article, we’re going to use AWS S3 to store our assets, and we’re going to use Travis CI to run a build job and an upload job as part of a continuous integration pipeline.

Let’s get the S3 bucket set up first.

Setting up the AWS S3 Bucket

It should go without saying, but you’ll need an AWS account if you’re following along here.

If we are the root user on our AWS account, we can create a new IAM user that has programmatic access only. This means we’ll be given an access key ID and a secret access key from AWS when we create the new user. We’ll want to store these in a safe place since we’ll need them later.

Finally, this user should be given permissions to work with S3 only, so that the level of access is limited if our keys were to fall into the wrong hands.

AWS has some great resources for best practices with access keys and managing access keys for IAM users that would be well worth checking out if you’re unfamiliar with how to do this.

Next we need to create an S3 bucket. S3 stands for Simple Storage Service and is essentially a place to upload and store files hosted on Amazon’s servers. A bucket is simply a directory.

I’ve named my bucket “single-spa-demo,” but you can name yours whatever you’d like. You can follow the AWS guides for how to create a new bucket for more info.

AWS S3 bucket

Once we have our bucket created, it’s also important to make sure the bucket is public and that CORS (cross-origin resource sharing) is enabled for our bucket so that we can access and use our uploaded assets in our app.

In the permissions for our bucket, we can add the following CORS configuration rules:

  * GET  

In the AWS console, it ends up looking like this after we hit Save:

Creating a Travis CI Job to Upload Artifacts to AWS S3

Now that we have somewhere to upload files, let’s set up an automated process that will take care of uploading new JavaScript bundles each time we merge new code into the master branch for any of our repos.

To do this, we’re going to use Travis CI. As mentioned earlier, each app lives in its own repo on GitHub, so we have four GitHub repos to work with. We can integrate Travis CI with each of our repos and set up continuous integration pipelines for each one.

To configure Travis CI for any given project, we create a .travis.yml file in the project's root directory. Let's create that file in the single-spa-demo-root-config directory and insert the following code:

language: node_js node_js: - node script: - yarn build - echo "Commit sha - $TRAVIS_COMMIT" - mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT - mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/ deploy: provider: s3 access_key_id: "$AWS_ACCESS_KEY_ID" secret_access_key: "$AWS_SECRET_ACCESS_KEY" bucket: "single-spa-demo" region: "us-west-2" cache-control: "max-age=31536000" acl: "public_read" local_dir: dist skip_cleanup: true on: branch: master

This implementation is what I came up with after reviewing the Travis CI docs for AWS S3 uploads and a single-spa Travis CI example config.

Because we don’t want our AWS secrets exposed in our GitHub repo, we can store those as environment variables. You can place environment variables and their secret values within the Travis CI web console for anything that you want to keep private, so that’s where the .travis.yml file gets those values from.

Now, when we commit and push new code to the master branch, the Travis CI job will run, which will build the JavaScript bundle for the app and then upload those assets to S3. To verify, we can check out the AWS console to see our newly uploaded files:

Uploaded files as a result of a Travis CI job

Neat! So far so good. Now we need to implement the same Travis CI configuration for our other three micro-frontend apps, but swapping out the directory names in the .travis.yml file as needed. After following the same steps and merging our code, we now have four directories created in our S3 bucket, one for each repo.

Four directories within our S3 bucket

Creating an Import Map for Production

Let’s recap what we’ve done so far. We have four apps, all living in separate GitHub repos. Each repo is set up with Travis CI to run a job when code is merged into the master branch, and that job handles uploading the build artifacts into an S3 bucket.

With all that in one place, there’s still one thing missing: How do these new build artifacts get referenced in our container app? In other words, even though we’re pushing up new JavaScript bundles for our micro-frontends with each new update, the new code isn’t actually used in our container app yet!

If we think back to how we got our app running locally, we used an import map. This import map is simply JSON that tells the container app where each JavaScript bundle can be found.

But, our import map from earlier was specifically used for running the app locally. Now we need to create an import map that will be used in the production environment.

If we look in the single-spa-demo-root-config directory, in the index.ejs file, we see this line:

Opening up that URL in the browser reveals an import map that looks like this:

{ "imports": { "react": "//cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js", "react-dom": "//cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js", "single-spa": "//cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js", "@react-mf/root-config": "//react.microfrontends.app/root-config/e129469347bb89b7ff74bcbebb53cc0bb4f5e27f/react-mf-root-config.js", "@react-mf/navbar": "//react.microfrontends.app/navbar/631442f229de2401a1e7c7835dc7a56f7db606ea/react-mf-navbar.js", "@react-mf/styleguide": "//react.microfrontends.app/styleguide/f965d7d74e99f032c27ba464e55051ae519b05dd/react-mf-styleguide.js", "@react-mf/people": "//react.microfrontends.app/people/dd205282fbd60b09bb3a937180291f56e300d9db/react-mf-people.js", "@react-mf/api": "//react.microfrontends.app/api/2966a1ca7799753466b7f4834ed6b4f2283123c5/react-mf-api.js", "@react-mf/planets": "//react.microfrontends.app/planets/5f7fc62b71baeb7a0724d4d214565faedffd8f61/react-mf-planets.js", "@react-mf/things": "//react.microfrontends.app/things/7f209a1ed9ac9690835c57a3a8eb59c17114bb1d/react-mf-things.js", "rxjs": "//cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/rxjs.min.js", "rxjs/operators": "//cdn.jsdelivr.net/npm/@esm-bundle/[email protected]/system/rxjs-operators.min.js" } }

That import map was the default one provided as an example when we used the CLI to generate our container app. What we need to do now is replace this example import map with an import map that actually references the bundles we’re using.

So, using the original import map as a template, we can create a new file called importmap.json, place it outside of our repos and add JSON that looks like this:

{ "imports": { "react": "//cdn.jsdelivr.net/npm/[email protected]/umd/react.production.min.js", "react-dom": "//cdn.jsdelivr.net/npm/[email protected]/umd/react-dom.production.min.js", "single-spa": "//cdn.jsdelivr.net/npm/[email protected]/lib/system/single-spa.min.js", "@thawkin3/root-config": "//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/179ba4f2ce4d517bf461bee986d1026c34967141/root-config.js", "@thawkin3/single-spa-demo-nav": "//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-nav/f0e9d35392ea0da8385f6cd490d6c06577809f16/thawkin3-single-spa-demo-nav.js", "@thawkin3/single-spa-demo-page-1": "//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-1/4fd417ee3faf575fcc29d17d874e52c15e6f0780/thawkin3-single-spa-demo-page-1.js", "@thawkin3/single-spa-demo-page-2": "//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/single-spa-demo-page-2/8c58a825c1552aab823bcbd5bdd13faf2bd4f9dc/thawkin3-single-spa-demo-page-2.js" } }

You’ll note that the first three imports are for shared dependencies: react, react-dom, and single-spa. That way we don’t have four copies of React in our app causing bloat and longer download times. Next, we have imports for each of our four apps. The URL is simply the URL for each uploaded file in S3 (called an “object” in AWS terminology).

Now that we have this file created, we can manually upload it to our bucket in S3 through the AWS console.

Note: This is a pretty important and interesting caveat when using single-spa: The import map doesn’t actually live anywhere in source control or in any of the git repos. That way, the import map can be updated on the fly without requiring checked-in changes in a repo. We’ll come back to this concept in a little bit.

Import map manually uploaded to the S3 bucket

Finally, we can now reference this new file in our index.ejs file instead of referencing the original import map.

Creating a Production Server

We are getting closer to having something up and running in production! We’re going to host this demo on Heroku, so in order to do that, we’ll need to create a simple Node.js and Express server to serve our file.

First, in the single-spa-demo-root-config directory, we'll install express by running yarn add express (or npm install express). Next, we'll add a file called server.js that contains a small amount of code for starting up an express server and serving our main index.html file.

const express = require("express"); const path = require("path"); const PORT = process.env.PORT || 5000; express() .use(express.static(path.join(__dirname, "dist"))) .get("*", (req, res) => { res.sendFile("index.html", { root: "dist" }); }) .listen(PORT, () => console.log(`Listening on ${PORT}`));

Finally, we’ll update the NPM scripts in our package.json file to differentiate between running the server in development mode and running the server in production mode.

"scripts": { "build": "webpack --mode=production", "lint": "eslint src", "prettier": "prettier --write './**'", "start:dev": "webpack-dev-server --mode=development --port 9000 --env.isLocal=true", "start": "node server.js", "test": "jest" }

Deploying to Heroku

Now that we have a production server ready, let’s get this thing deployed to Heroku! In order to do so, you’ll need to have a Heroku account created, the Heroku CLI installed, and be logged in. Deploying to Heroku is as easy as 1–2–3:

  1. In the single-spa-demo-root-config directory: heroku create thawkin3-single-spa-demo (changing that last argument to a unique name to be used for your Heroku app)
  2. git push heroku master
  3. heroku open

And with that, we are up and running in production. Upon running the heroku open command, you should see your app open in your browser. Try navigating between pages using the nav links to see the different micro-frontend apps mount and unmount.

Demo app — up and running in production

Making Updates

At this point, you may be asking yourself, “All that work for this? Why?” And you’d be right. Sort of. This is a lot of work, and we don’t have much to show for it, at least not visually. But, we’ve laid the groundwork for whatever app improvements we’d like.

The setup cost for any microservice or micro-frontend is often a lot higher than the setup cost for a monolith; it’s not until later that you start to reap the rewards.

So let’s start thinking about future modifications. Let’s say that it’s now five or ten years later, and your app has grown. A lot. And, in that time, a hot new framework has been released, and you’re dying to re-write your entire app using that new framework.

When working with a monolith, this would likely be a years-long effort and may be nearly impossible to accomplish. But, with micro-frontends, you could swap out technologies one piece of the app at a time, allowing you to slowly and smoothly transition to a new tech stack. Magic!

Or, you may have one piece of your app that changes frequently and another piece of your app that is rarely touched. While making updates to the volatile app, wouldn’t it be nice if you could just leave the legacy code alone?

With a monolith, it’s possible that changes you make in one place of your app may affect other sections of your app. What if you modified some stylesheets that multiple sections of the monolith were using? Or what if you updated a dependency that was used in many different places?

With a micro-frontend approach, you can leave those worries behind, refactoring and updating one app where needed while leaving legacy apps alone.

But, how do you make these kinds of updates? Or updates of any sort, really?

Right now we have our production import map in our index.ejs file, but it's just pointing to the file we manually uploaded to our S3 bucket. If we wanted to release some new changes right now, we'd need to push new code for one of the micro-frontends, get a new build artifact, and then manually update the import map with a reference to the new JavaScript bundle.

Is there a way we could automate this? Yes!

Updating One of the Apps

Let’s say we want to update our page 1 app to have different text showing. In order to automate the deployment of this change, we can update our CI pipeline to not only build an artifact and upload it to our S3 bucket, but to also update the import map to reference the new URL for the latest JavaScript bundle.

Let’s start by updating our .travis.yml file like so:

language: node_js node_js: - node env: global: # include $HOME/.local/bin for `aws` - PATH=$HOME/.local/bin:$PATH before_install: - pyenv global 3.7.1 - pip install -U pip - pip install awscli script: - yarn build - echo "Commit sha - $TRAVIS_COMMIT" - mkdir -p dist/@thawkin3/root-config/$TRAVIS_COMMIT - mv dist/*.* dist/@thawkin3/root-config/$TRAVIS_COMMIT/ deploy: provider: s3 access_key_id: "$AWS_ACCESS_KEY_ID" secret_access_key: "$AWS_SECRET_ACCESS_KEY" bucket: "single-spa-demo" region: "us-west-2" cache-control: "max-age=31536000" acl: "public_read" local_dir: dist skip_cleanup: true on: branch: master after_deploy: - chmod +x after_deploy.sh - "./after_deploy.sh"

The main changes here are adding a global environment variable, installing the AWS CLI, and adding an after_deploy script as part of the pipeline. This references an after_deploy.sh file that we need to create. The contents will be:

echo "Downloading import map from S3" aws s3 cp s3://single-spa-demo/@thawkin3/importmap.json importmap.json echo "Updating import map to point to new version of @thawkin3/root-config" node update-importmap.mjs echo "Uploading new import map to S3" aws s3 cp importmap.json s3://single-spa-demo/@thawkin3/importmap.json --cache-control 'public, must-revalidate, max-age=0' --acl 'public-read' echo "Deployment successful"

This file downloads the existing import map from S3, modifies it to reference the new build artifact, and then re-uploads the updated import map to S3. To handle the actual updating of the import map file’s contents, we use a custom script that we’ll add in a file called update-importmap.mjs.

// Note that this file requires [email protected] or higher (or the --experimental-modules flag) import fs from "fs"; import path from "path"; import https from "https"; const importMapFilePath = path.resolve(process.cwd(), "importmap.json"); const importMap = JSON.parse(fs.readFileSync(importMapFilePath)); const url = `//single-spa-demo.s3-us-west-2.amazonaws.com/%40thawkin3/root-config/${process.env.TRAVIS_COMMIT}/root-config.js`; https .get(url, res => { // HTTP redirects (301, 302, etc) not currently supported, but could be added if (res.statusCode >= 200 && res.statusCode  { urlNotDownloadable(url, err); }); function urlNotDownloadable(url, err) { throw Error( `Refusing to update import map - could not download javascript file at url ${url}. Error was '${err.message}'` ); }

Note that we need to make these changes for these three files in all of our GitHub repos so that each one is able to update the import map after creating a new build artifact.

The file contents will be nearly identical for each repo, but we’ll need to change the app names or URL paths to the appropriate values for each one.

A Side Note on the Import Map

Earlier I mentioned that the import map file we manually uploaded to S3 doesn’t actually live anywhere in any of our GitHub repos or in any of our checked-in code. If you’re like me, this probably seems really odd! Shouldn’t everything be in source control?

The reason it’s not in source control is so that our CI pipeline can handle updating the import map with each new micro-frontend app release.

If the import map were in source control, making an update to one micro-frontend app would require changes in two repos: the micro-frontend app repo where the change is made, and the root config repo where the import map would be checked in. This sort of setup would invalidate one of micro-frontend architecture’s main benefits, which is that each app can be deployed completely independent of the other apps.

In order to achieve some level of source control on the import map, we can always use S3’s versioning feature for our bucket.

Moment of Truth

With those modifications to our CI pipelines in place, it’s time for the final moment of truth: Can we update one of our micro-frontend apps, deploy it independently, and then see those changes take effect in production without having to touch any of our other apps?

In the single-spa-demo-page-1 directory, in the root.component.js file, let's change the text from "Page 1 App" to "Page 1 App - UPDATED!" Next, let's commit that change and push and merge it to master.

This will kick off the Travis CI pipeline to build the new page 1 app artifact and then update the import map to reference that new file URL.

Εάν στη συνέχεια πλοηγηθούμε στο πρόγραμμα περιήγησής μας στη διεύθυνση //thawkin3-single-spa-demo.herokuapp.com/page1, θα δούμε τώρα… drum roll παρακαλώ… την ενημερωμένη εφαρμογή μας!

Demo app — successfully updating one of the micro-frontend apps

συμπέρασμα

Το είπα πριν και θα το πω ξανά: Τα Micro-frontends είναι το μέλλον της ανάπτυξης web frontend.

Τα οφέλη είναι τεράστια, συμπεριλαμβανομένων ανεξάρτητων εφαρμογών, ανεξάρτητων τομέων ιδιοκτησίας, ταχύτερων χρόνων κατασκευής και δοκιμών και της δυνατότητας ανάμιξης και αντιστοίχισης διαφόρων πλαισίων, εάν χρειάζεται.

Υπάρχουν ορισμένα μειονεκτήματα, όπως το αρχικό κόστος εγκατάστασης και η πολυπλοκότητα της διατήρησης μιας κατανεμημένης αρχιτεκτονικής, αλλά πιστεύω ακράδαντα ότι τα οφέλη υπερτερούν του κόστους.

Το μονό σπα καθιστά εύκολη την αρχιτεκτονική micro-frontend. Τώρα κι εσείς μπορείτε να διαλύσετε τον μονόλιθο!