mf-maestro

a micro frontends mediator

Usage no npm install needed!

<script type="module">
  import mfMaestro from 'https://cdn.skypack.dev/mf-maestro';
</script>

README

At the beginning, MfMaestro was more a way to share some experiments with the community about micro-[services, frontend] technologies. If you use it, don't think it is the only way to build this kind of app, even if we are trying to make it a really great tool to do this job. Always think someone is already building something better somewhere else. Keep searching, share knowledge, mix and challenge ideas... This is the best way to improve this work.

This implementation uses React > 16.8 with functional components and hooks, but we are working on differents versions (Elm next, because we like coding with types and the goddamned compiler!), because we think what is important is the architecture and the patterns, not the language used.

MfMaestro

MfMaestro (npm link) is a frontend mediator to build browser-based applications. It works by aggregating at runtime compatible micro-frontends, served from http servers by independent microservices. One of the most important aspect in MfMaestro is to try to keep things SIMPLE, NOT COUPLED, with high cohesion, so each team can release at its own rythm. This was specially important when we designed the event system to synchronize micro-frontends.

You can think of the main motivation as this :

"As an application creator, I want to build an app with multiple pages, each one with its url. I'm owner (I design) of these pages, my main css, and the routing/navigations rules. On these pages, I will simply put micro-frontends which I'm not owner of. These micro-frontends are exposed on http urls by teams that don't know anything about others applications on the page where they will be added, so they can't react directly to "events" of these others applications to load or change their data. Thus, I use MfMaestro as a mediator to "synchronize" them".

It's a kind of pattern/DSL/framework (you can stay "high", almost only declarative/config, or go deep with more programming skills) to build your application with few steps :

  1. Create one or many micro-frontend apps compatible with MfMaestro dynamic load mechanism (demo) and a manifest file (demo), and put them on a webserver
  2. Create your main application (the one loading your micro-frontends) with mf-maestro npm package (a simple npm init and npm install)
  3. Add pages folder with your first page (see demo)
  4. Add microfrontends to pages with our react component MicroAppComponent. It's as simple as : <MicroAppComponent app="microapp-name-in-manifest" manifestUrl="https://...manifest-url..." />
  5. Start your app. Your page should load and micro-frontends will load from their urls and start.
  6. Now you can go deeper : improve your app with events, routing, navigation, create more complex MicroAppComponent component to handle specific logic... everything that allows you to build a real and more complex app than a simple "hello micro-frontend".

As "The Maestro", you create "DEPENDENCIES/COUPLING" between micro-frontends you put on your pages by defining how they must react. It's your role because you are building your application. And another application's builder will create with the same micro-frontends another app with a totally different navigation and look and feel.

Documentation sections

Repository organization

  • in src/ you find the Mediator source code.

  • in tests/, you have the tests suite.

  • in demo/, you find two directories :

    • in mediator-app/, you have a demo mediator app (we call a mediator app the main application where you mount your micro-frontends. It's your app, the one your users will use). You can use it as a start for your tests or your new application, but you'll need to remove some code you might not need, like the src/components/.

      • in public/, you have all files exposed by react dev server to use the demo :
        • the index.html is the page loaded when you call the http://localhost:3000/
        • in assets/, you find some micro-frontends (js and css) we load in the demo while navigating and emitting events.
    • in micro-frontends-apps/, you have micro-frontends projects coded with different frameworks. Each one is independent, with its own build process, package.json, starts with npm start on its own port, and serves its own manifest.json and app.js files and can be loaded in its own html page on their localhost port). They are started and used in the demo. These apps are here to demonstrate that coding a new micro-frontend changes nothing in your usual developer experience. (for example, since they also have their own independant html page, you can load them in webpage and test them independently).

      • in iframe-for-demo/, you find a project with iframe security demo. It just starts a webpack-dev-server to load the index.html in the main demo.

Installation

To add MfMaestro to your project, go to your project directory and add the npm package :

npm install mf-maestro --save

There are some peerDependencies in package json. You need to add them to your app :

"peerDependencies": {
  "react": "^16.8.6",
  "react-dom": "^16.8.6",
  "react-router-dom": "^5.0.1",
}

This might change in the future.

Demo

A demo application (also used for integration tests) is availabe in thedemodirectory

It loads multiple micro applications, coded with different frameworks and versions (VannillaJS, Elm, Angular, Vue, React, EmberJs). The important mechanisms to look at are tested in the tests/test.js file.

To start the demo :

  1. Go to mf-maestro root and run : npm run demo (for each demo micro-frontend, it will install node modules and start a webpack dev server)
  2. Go to https://localhost:3000/
  3. After you stop the demo process, run npm run stop to kill all started webpack-dev-server processes. If you want to clean all created directories for the demo, you can use npm run clean. It will delete ALL node_modules and dist directories.

Tests

Go to mf-maestro root and run : npm run test

Tests are run using the demo app, with TestCafe. By default, tests are run with chrome headless, but you can use all TestCafe functionnalities.

How to use MfMaestro

As explained in the intro, when we designed MfMaestro, we wanted to build applications by aggregating micro-frontends on pages. This allows to build new online applications efficiently. A team exposes its micro-frontends with a manifest file, available at a url like https://service1.mydomain.com/mf_maestro.json, with this structure :

{
  "micro-app-2": {
    "css": "/service2/assets/micro-app-2/app.2342.css", <= micro-frontend css
    "url":"/service2/assets/micro-app-2/app.9876.js" <= micro-frontend code
  },
  "micro-app-1": {
    "css": "/service1/assets/micro-app-1/app.4536.css",
    "url":"/service1/assets/micro-app-1/app.9876.js"
  }
  ...
}

You can think of manifests as kind of API to get micro-frontends code (js) and design (css). We called it manifest, but you can name it as you want. There is nothing official here. It's just a new concept.

It's a json stucture, where the keys are the names of the apps we will use as props in the MicroAppComponent on pages : <MicroAppComponent app="micro-app-2" manifestUrl="https://service1.mydomain.com/mf_maestro.json" />

You can have a look at our 2 manifest files (1 and 2) in the demo application. And look how we build a page : it's almost only declarative at the beginning. Simple and fast to start!

When MfMaestro find the MicroAppComponent component in a page, it will load the manifest declared in prop manifestUrl, then use the app prop to load the associated javascript and css.

The manifest and javascript is cached in the Redux store of MfMaestro. The css is added to the dom. When the micro-frontend is unmounted, the cached data will be used when MfMaestro wants to mount another instance of the same micro-frontend : that means no manifest or javascript files loaded twice. Same if you navigate to another page. The state is global to your application.

If you put many times the same micro-frontend on a page (we tried with more than 1000), MfMaestro gracefully handle load and start of all instances, loading only one time the manifest and the js code, and automatically starting all instances when everything is ready. Don't worry about this!

When you build your service applications, you should add to your process the automatic generation of manifest files.

How to build a micro-frontend application to be compatible with MfMaestro

The simplest application can be :

  window.MfMaestro.registerMicroApp("micro-app-1", {
    start: (targetAppNode, params, options) => {
      // we'll detail params and options later,
      //they are just here so you know there are usefull things send to the start function :)
      console.log(`starting ${options.groupRef}`);
    },
    stop: (targetAppNode, options) => {
      console.log(`stopping ${options.groupeRef}`);
    },
  });

This code could go in the app.9876.js file for example.

To be usable by MfMaestro, your micro-frontend code always start by calling the window.MfMaestro.registerMicroApp function with the name of your app (the same name used in the manifest key) and a { start, stop } object as arguments. The startmethod is called when MfMaestro mounts the MicroAppComponent component, and stop when it unmounts. Usually you put in start function the code to start your Elm/React/Vue/Angular/EmberJS/VanillaJS... application, add events listening and reactions, navigation rules to use modals for example, etc etc... But you can do it deeper in your app, pages, or micro-frontend's code. For example, put here common app's behaviours, and add on each page its events to prevent headache in events and navigation handling in one place for the whole app!!!!. In stop function, you will usually pay attention to be sure you don't have memory leak when you unmount you micro-frontend, like when you attached an event listener and forget to remove it (But for this specific case, MfMaestro automatically gives you, in the options argument of the start function, a method to automatically manage events, so you don't have to, see bellow). You have multiple articles on internet about how to create and detect memory leaks in javascript in the browser :

If you need to forward variables between start and stop function, you can return a variable from start function and it will be passed to stop function in options.startResult. You can view a demo in https://github.com/alotela/mf-maestro/blob/master/demo/mediator-app/public/assets/micro-app-1/app.js

So, on our web server, for a microservice, we have N .js files (one per micro-frontend application, see the demo files here : 1, 2, 3, 4, 5, 6), each one registering in our main application an object with a start and a stop method when it is loaded, and 1 manifest file per service (see demo here: 1, 2) with metadata to tell to our MfMaestro app where to find the js and css files for our micro-frontends.

Our global architecture, in terms of files per service, looks like this:

service1:
  mf-maestro-manifest.json (https://service1.mydomain.com/assests/mf-maestro-manifest.json)
  micro-app-1-1.js (https://service1.mydomain.com/assests/js/micro-app-1-1.js)
  micro-app-1-1.css (https://service1.mydomain.com/assests/js/micro-app-1-2.css)
  micro-app-1-2.js (https://service1.mydomain.com/assests/js/micro-app-1-2.js)
  micro-app-1-2.css (https://service1.mydomain.com/assests/js/micro-app-1-2.css)

service2:
  mf-maestro-manifest.json (https://service2.mydomain.com/assests/mf-maestro-manifest.json)
  micro-app-2-1.js (https://service2.mydomain.com/assests/js/micro-app-2-1.js)
  micro-app-2-1.css (https://service2.mydomain.com/assests/js/micro-app-2-1.css)
  micro-app-2-2.js (https://service2.mydomain.com/assests/js/micro-app-2-2.js)
  micro-app-2-2.css (https://service1.mydomain.com/assests/js/micro-app-2-2.css)
...
serviceN:
  mf-maestro-manifest.json (https://serviceN.mydomain.com/assests/mf-maestro-manifest.json)
  micro-app-N-1.js (https://serviceN.mydomain.com/assests/js/micro-app-2-1.js)
  micro-app-N-1.css (https://serviceN.mydomain.com/assests/js/micro-app-2-1.css)
  micro-app-N-2.js (https://serviceN.mydomain.com/assests/js/micro-app-2-2.js)
  micro-app-N-2.css (https://serviceN.mydomain.com/assests/js/micro-app-2-2.css)
  ...
  micro-app-N-M.js (https://serviceN.mydomain.com/assests/js/micro-app-N-M.js)
  micro-app-N-M.css (https://serviceN.mydomain.com/assests/js/micro-app-N-M.js)

How do we use our micro applications with MfMaestro main app

To load and use micro-frontends, we need to design a main app, where you define your pages, the routing and events. We call this app the "Mediator" since it is inspired by the mediator pattern (Design Patterns: Elements of Reusable Object-Oriented Software)

To clarify the terms "pages", "routing" and "events", this is a simple organization for a mediator application :

MyMediatorApp/
  components/ (1)
    users/ (2)
      detail.js
      edit.js
      master.js
      new.js
  index.js (3)
  init.js (4)
  pages/ (5)
    MainPage.js (6)
    Page1.js
   ...

(1) components directory : put here your own React components for your pages. You decide to create a new component usually when you need to handle specific logic (events handling, side effects...) not available in MfMaestro basic components. You can view an example.

(2) components/users directory : this directory contains a component with more logic : see an example

(3) index.js : this file starts the app (see an example) calling the startMediatorfunction with 4 args :

  • targetDomElementId : the id of the dom element where we want to start our app
  • MainPage : the root component of your app, injected into the React router
  • init : an optional function called when the mediator starts to configure the application, see bellow init.js (4)
  • hashRouting : an optional boolean, true if you want to use hash-based routing, default to false if you omit it.

(4) init.js : this file exports a function (run when the main app starts) used mainly to define how our events are managed and config options (see demo file). The object returned by this function is also used as a config for our MediatorApp. For now you can only pass these keys :

  • MicroAppLoadingComponent : a component to replace the default loading component (what is visible on the page in the micro-frontend destination div while the mediator loads a manifest or a js code file : default and custom for demo app. This component will be use as default component for all MF. You can also set a loader component for each MF byb using props.loader={YourComponent} on MicroAppComponent see below.

(5) pages directory : all app's pages where we put components/micro-frontends.

(6) MainPage.js : the main page of our app (see example) injected in the startMediatorfunction in index.js file (3). Usually this page would be your main router.

(7) Page1.js : a real page with content see demo file

Options and params sent to start() function of your micro-frontend

  1. appNode

the dom node where your app put its content. Use it like a normal dom node, nothing special. You can view how to use it here

  1. params

This is a simple object with params for your app. params is a merge between params prop of your react component and url path params defined in the react router. Url params are passed to all micro-frontends, and prop params are unique for each one. You can view a demo here for params prop and for path params router / micro-frontend

  1. options

options is an object with these attributes :

  • groupRef

This is the id of the html node where you can find the "app-wrapper" node of your app (the node where your app is injected). You should not need it most of the time. It is not the node where your app starts, but its parent

  • events

an object with already binded functions to use events. IMPORTANT : always use these functions to add/remove events, because it will automatically manage events listeners, removing listeners when your app is removed from the dom. Not using these functions can lead to memory leak (your components will be removed from the dom, but will stay in memory because of a reference to a listener). You can view a demo here with emit and on

  • navigation

an object to block/unblock navigation between page transition. Usefull for example if you want to show a modal to your user before he leaves the current page. demo

  • queryParams

an object with query params you send to your component from url. The format is json5. This is usefull if you want to intialize components on your page, for example to load a specific resource, or go to a specific page in your component. Since the architecture of MfMaestro is based on pages where you aggregate your components, you can have one OR many components on your page. So if you want to initialize each of them, you can't do it with the url. That's why we choosed to use query params for this requirement.

Options and params sent to stop() function of your micro-frontend

stopreceive the appNodeand an options object with groupRef and events. They are (and work the same) the same arguments sent to start() function.

How to use query params

We build pages, with many components on each one. To send parameters to each one, we need a url like

https://mydomain.com/users?microApp1Ref={var1:12,var2:"var2value",...}&microApp2Ref={var1:"hello", var3:"world",...}

and be able to send these values in your micro-frontends start functions.

To let the mediator connect your url's json5 to a micro-frontend on the page, you need to add a groupRef prop to your react MicroAppComponent component (demo), and use this groupRef as the key of your json5 value in your query string (home1 here): ...myUrl?home1={my json5 object}

The events system in MfMaestro

How do you synchronize micro-frontends on a page, so they can react to what is happening into the application, while you want to be able to put the same micro-frontend on different pages with different micro-frontends each time ? Use the mediator, Luke!

Your micro-frontends react to events. It's their "api". When you document a micro-frontend, you give the manifest file, but also the list of events it emits or can react to. But! Since you don't know on which page and with which other micro-frontends it will be aggregated, how can you react to events emitted by others micro-frontends ? Well, in MfMaestro we have our events list of functions passed to the start function of your micro-frontend in the options.events parameter :

  • emit(event, ...args) emit an event in the system. It accepts these arguments : - event : a string, the name of your event. - ...args : has many args as you want. They will be passed to the listeners "as is". It's like a payload for your events. If the last argument is an object containing the afterEvent key, then, this argument is removed from the arguments and the function afterEvent is called just after the event has been emitted. MfMaestro automatically adds the groupRef as last arg sent to callbacks.

You can view some examples in the micro app 2 demo code.

  • on(event, callback, context) add a permanent listener for an event. It takes these arguments : - event : a string, the name of the event you want to react to. - callback : a function called when the event is received, with all args (those passed to the emit function) as parameters. !! If you want to stop this listener, you'll need to call removeListener with this callback reference (see bellow). Thus, you should store it in a variable and pass it as parameters. - context (optional) : the context (the "this") if your callback is not binded.

> Targetting 1 micro-frontend when you have multiple instances of the same micro-frontend on the page :

When you add a listener to an event, you will add the listener to your event and to a second event named groupRef + ":" + event. This is done automatically. So if your event is called "mf1:users:clicked" in the micro-frontend with a groupRef "MyMf1", you will also add a listener to the event "MyMf1:mf1:users:clicked". This let you react to an event with just a specific micro-frontend. Imagine you have on the same page two instances of the same micro-frontend. If you want an event emitted by another micro-frontend to trigger an event of these micro-frontends, you can use the mutateEvent(sourceEvent, targetEvent) function. But if you do so, both micro-frontends will react. If you want to target only one of them, since they both listen to 2 events (one common : "target-event-name" and one not common : "groupRef:target-event-name"), you just need to mutate your event like this : mutateEvent("source-event-name", "groupRef:target-event-name").

  • once(event, callback, context) same as on(event, callback, context), but reacts only one time to events.

  • redirectOnEvent(event, path, options) react to an event by changing the url (page). It takes these arguments :

    • event : a string, the name of the event you want to react to.
    • path : the new url you want to redirect to. You can use variables in path using ":" like this: /users/:id. When you emit your message, the args will replace the variables. if you add redirectOnEvent("myEvent", "/users/:id/articles/:slug"), and later call emit(event, 12, "my-article-1, ...), the url will change to /users/12/articles/my-article-1. BUT if the second params passed to emit is an object, then variables will be replaced by this object's keys: emit(event, {id: 12, slug: "my-article-1}, ...) (you can also access deeper attributes in an object with a '.': for the following object: {resource: {id: "foo", slug: "bar"}}, you can write the following url: /users/:resource.id/articles/:resource.slug)
    • options (optional) : an object with these attributes - emitBefore : a string, an event emitted before the url's change is done - emitAfter : a string, an event emitted after the url's change is done

  • mutateEvent(sourceEvent, targetEvent, transformArgsFn) add an event listener to emit targetEvent when sourceEvent is emitted. It takes these arguments :

    • sourceEvent : a string, the name of the event you want to react to
    • targetEvent : a string (the name of the event to emit) or a function that will receive all args and return the name of the target event (usefull when you want to dynamically determine targetEvent)
    • transformArgsFn (optional) : a function that will receive all "...args" passed to emit() to transform them if necessary
  • removeEventMutation(sourceEvent, targetEvent) remove an existing event mutation created with mutateEvent. The args are :

    • sourceEvent : a string, the name of the event you want to stop mutate
    • targetEvent : a string, the target event used in mutateEvent.
  • removeListener(event, callback, context) remove a listener. It takes these arguments : - event : a string, the name of the event from which you want to remove the listener - callback : the listener you added earlier (callback reference) - context : a context if needed

We use eventemitter3 to manage events. If you want to know more about function params, see the doc on github and the api of node event emitter.

The navigation system in MfMaestro

In the options passed to the startfunction, you have options.navigation. You find in this object 2 functions you can call when you need to block or authorize navigation. This can be usefull for example, if you need to show a modal to the user for validation before leaving a page.

To block a navigation, just call options.navigation.blockNavigation(). For example, you can on a button, set a callback on the click that will call options.navigation.blockNavigation() and emit an event to change to change the navigation. MfMaestro (using history will store the target url and prevent navigation. You can then show a modal, or ask for a validation or anything you need to do. You will call another callback when the user react to your UI. In this callback, you will process what you need to do before leaving the page, and call options.navigation.unblockNavigation(). The page will then change. You can view a demo here

When you call options.navigation.unblockNavigation(), a message is emited : emit("navigation:location:changed", { location: state.targetLocation });. You can listen to this message if you need to react to it.

The services system in MfMaestro

MfMaestro includes standard services you can call from your MF. They are available in the options passed to the startfunction, you have options.services.

  • Notifications A mf can decide to show a notification to the user. In the options passed to the startfunction, you have options.services.notify(view file). This function takes theses arguments: - payload: an object with type attribute (possible values : "success" or "error") and message (the string you want to show to the user in the notification)

You can view a demo here. This is the first version. Customization options will come soon.

More services to come soon...

The useEvents effect

The useEvents effect is a react effect we use everywhere we need the events system. We use it:

  • in the MediatorApp to call the init() function with main app events.
  • in the MicroAppComponent to be able to call start() and stop() with options.events argument.
  • in pages, when we want to activate/deactivate events or navigation only when we are on a specific page. This allows to not have the whole events config in the init function. You can look at some demo pages.

To use it, just import { useEvents } from "mf-maestro"; and then initialize it with const [groupRef, events] = useEvents("NameOfThisGroupRef");. When you call useEvents(), the argument is an optional string (if you don't pass anyting, a uuid is generated by default). This string is used to manage the effect and its state with redux. It returns an array, where the first element is the string itself, and the second element is the events object with all functions you need to manage events binded so that the effect can when you unload the component remove all listeners so that you don't have to worry. When you define a groupRef prop to MicroAppComponent, it is used as useEvents() argument.

How to embed untrusted micro-frontend securily ?

If you need to embed a micro-frontend with strong security garanty, you can use our IframeMicroAppcomponent. It will load an iframe with a configuration to authorize only some events in both ways. You can see the demo on page Home :

<IframeMicroApp
  authorizedEvents={["iframe:user:clicked"]}
  forwards={{ "ma1:event": "ma1:event:toIframe" }}
  groupRef="iframe1"
  src="http://localhost:3010/"
  style={{ border: "0", height: "200px", margin: "10px", width: "500px" }}
/>

You can pass these props :

  • authorizedEvents : an array with the events you are ok to receive from iframe.
  • forwards : an object with the mutation of events you want to pass to the iframe (here ma1:eventwill be passed to iframe as ma1:event:toIframe).
  • src : iframe src.
  • style : some styles attributes for the iframe.

This implementation is quite new, so it will be improved soon with new features. But it already works and you can use it.

You have a demo iframe in demo/micro-frontends-apps/iframe/. As all demo, it is independent and startable on its own port. See webpack config.

The ACLs system

MfMaestro includes an acls module. You can use it :

  • to control if the user can access (load/mount) a micro-frontend before it is loaded and visible
  • to check in your micro-frontends if the user can view some features
  • to handle anything related to ACLs

You can use the default functions or redefine each function to fit your needs. To use the default system, you start by loading your ACLs with the acls.resetAcls(yourAclsMap) (see a demo). The acls map object is a simple map of backend => acls attributes:

{
    myBackend1Reference: ["delete", "detail", "edit", "master", "new"],
    myBackend2Reference: ["delete", "detail", "edit", "master", "new"],
    ...
    myBackendXReference: ["delete", "detail", "edit", "master", "new", "customAcl1", "customAclX"]
}

You can add/replace or remove acls with addAcls(aclsMapToMergeToAcls) and removeAcls(processorNameAsKeyToDelete):

import { acls } from "mf-maestro";

...

function f() {
    ...
    addAcls({myBackend1Reference: ["delete", "detail"]});
    ...
    removeAcls("myBackend1Reference");
    ...
}

You can check for acls with 3 functions:

  • hasAcl(backendReference, acl): to check for one acl (string) in one backend reference (string)
  • hasAnyAcls(backendReference, acls): to check if any acl in acls array (of strings) is in the loaded acls
  • hasAcls(backendReference, acls): to check if all acls (array of strings) are in the loaded acls

Manage acls in your micro-frontend

If you need to manage ACLs in your micro-frontend, you have 2 options:

  • load acls from the bakend when your micro-frontend is starting
  • use the acls services passed to your start function as options.services.acls. You will find the same functions as describe earlier:
    • acls() : returns acls registered for the associated backend
    • hasAcl(acl): check if the acl is present into the registered acls for the associated backend
    • hasAnyAcls(acls): same but for any check
    • hasAcls(acls): same but for all check
    • removeAcls() : delete all acls for this backend
    • resetAcls(acls) : reset (replace) acls for this backend to acls args

but binded (partial application) to the backend reference of the MF. You can look the buildScopedAclsFn function for the code. You can use all binded methods in your MF to decide if you need to hide or show buttons, links...

The backend reference used in buildScopedAclsFn function comes from the maestro.json file (you have an attribute processor to specify (see demo) or you can add a processorName props into you page when you add the MicroAppComponent (see demo in "noacls-hidden" MicroAppComponent). You can also associate a noAclHide={[true | false]} props so that when MfMaestro will load the microAppComponent, if the user does not have the acls to use it, it won't be started (and even the code won't be loaded). Acls to start a MF are specified in maestro.json in the requiredAcls attribute (see demo) or in the requiredAclsprops of MicroAppComponent (see demo in "noacls-hidden" MicroAppComponent).

The MicroAppComponent in detail

To load a microfrontend on a page, you use the MicroAppComponent component:

<MicroAppComponent
  app="micro-app-1"
  groupRef="micro-app-1-group-ref"
  manifestUrl="http://localhost:3000/assets/manifest.json"
  noAclHide={true}
  processorName="myBackend1Reference"
  requiredAcls: ["show", "update"]
/>

It accepts these parameters as props:

  • app : string, the name of the app you want to load in the manifest file (loaded from manifestUrl)

  • cssClass : css class of the wrapper div added by MfMarstro around your MF

  • groupRef : string, this prop is used for many things related to your MF (events target, html node id, query params json5 identifier...). It must be uniq in the page!

  • loader : react component, use it as a loader while MfMaestro is loading your MF code. MicroAppComponent has many load states:

    • "appNotInManifest",
    • "loadingCode",
    • "loadingManifest",
    • "manifestLoaded",
    • "manifestLoadError",
    • "manifestNotLoaded",
    • "propsChanged",
    • "canStart"

    It starts by loading the manifest, then the code and css. When it is ready to start, it will switch to canStart and replace the default loading component. But before all of this, it is in a global loadingstate, with different steps as you can see from states. You have the option to pass a react component as props, that will receive the different states from the list above and the groupRef and replace the default loader. This is usefull when you want to have a specialized loader for your MF. For more information about this feature, you can view the demo loader used here (search the line with loader={CustomLoader}).

  • manifestUrl : string, the url of the manifest that contains your app script and css urls and others data.

  • noAclHide : boolean, if you use the acls system and this is set to true, then your MF won't be showed if you don't have the required ACLS.

  • processorName : The processor name to check acls in acls stored with acls.resetAcls(yourAclsMap)

  • queryParser : query parser function to parse url variables if you don't use JSON5

  • requiredAcls : the required acls to start the microfrontend

Design - Styles - Css

One strategy to style micro-frontends is to build a first version with as few css rules as needed on an isolated page. Your MF should not manage its position, nor width or height. Use the whole width. If you need to have a fixed width UI, put a "box inside a full width box". Your position, width and height will be given by the mediator's team, when they add your MF to a page. Just design them so they are usable on all devices, reponsive, etc etc... Do not forget to scope your css rules so they won't interfer with others elements on pages. Integrate your micro-frontends on your mediator app (where you will have your main css). Your main rules should now add some design to your micro-frontends. In your main application, overide the micro-frontends css rules to finish the adaptation of your micro-frontends.

This work (design css agnostic micro-frontends, and then adapt to your main app) requires a good knowledge of css, to keep your micro-frontends as modular as possible, while staying adaptable.

Our micro-frontends guidelines

  • Your micro-frontend "api" is its list of in/out messages. Never listen to a direct message of another service. You would create dependencies.

  • Never share data between micro-frontends

A micro-frontend only gets its data from its backend (and if you are still using http calls to share data between you backend services, you should really think to switch to event sourcing). The only data you can passed are the one you can find in url (id, query params....).

  • A micro-frontend must always have a "loading" or "waiting" state since, sometimes, the data you need to load might not be in your microservice yet.

  • At the start of the service, load the current state (http call to backend). We don't use frontend store most of the time. They are complicated to manage. Each time we need to access a data, we make a call to backend.

  • if you manage lists, do not refresh automatically. If someone is using the list on page X, the refresh will change everyting. It is better to let the user know a new state is available.

  • add a mechanism to let the user know when he's using an outdated version of the micro-frontend, with a way to reload the service.

micro-frontend development process

  • ⚠ Do not try to developp your MF directly in a mediator application. You will be dependent from others teams releases and if their work does not work well, it will be nightmare.
  • You really need to think about this process as 2 teams/steps responsibilities : first design, code and test your MF in a dedicated page in its own project (as you would do with a library) as the feature team's responsibility, then add it to your mediator app as the mediator app team's responsibility. If you don't do that, you will not create reusable MF. Your MF must be fully usable when it is in an isolated page.
  • To start a new micro-frontend, start by defining the entity it will manage and the associated UI.
  • Your UI will accept input events, emit output events, and communicate with its backend via http API often. Define these events and http calls. UI data and I/O events define what you need to test. YOUR EVENTS ARE THE API OF YOUR MF.
  • Build a project for each MF with a development server (like webpack-dev-server or rollupjs-dev-server). Add an index.html page where you will only load your MF.
  • If you need to integrate your MF with a particular design, add the css to this page.
  • When you develop a new micro-frontend, you will use mf-maestro events methods in your code. If you need to test your micro-frontend as an independent application, you can use mf-maestro-tester. This project emulates MfMaestro events methods. You can look into our demo to use it. It's really easy and it also demonstrates that your application is not tightly coupled with mf-maestro if you need later to use it with something else. You will just have a mecanism to handle events. Not bad, isn't it ?

Tips & tricks

  • When you add a new page, do not forget it receives as props an object with history, location and match properties (in the httpResourceScope attribute). This let you handle complex navigation cases if you need to dig deeper.
  • Never use a micro-frontend inside another one. We want teams independence. If you break this rule, you won't be able to scale your project, release loosely...
  • There are exceptional situations when you can break the "Never share data between micro-frontends" rules, but you must really be sure that you won't add dependencies between your services... And most of the time, we don't need it. So if you start doing this, stop and think you designed something bad.
  • When you code a micro-frontend, it is not your role to think about breaking contracts with the main mediator app. If you need to change your events data, change them. It is the responsability of the main team, the one coding the mediator app to adapt. They have tests to validate that your new version will work or not. And you don't know which events they use so maybe changing an event will not change anything for a team.
  • So, as the main team, you need to have browser tests to validate that a new version of a micro-frontend won't break your UI. Pay attention to testing events.
  • When your MF is started, if any error is thrown, MicroAppComponent will catch it and show a default UI with the error. You can have a look in MicroAppComponent.js. It uses a simple Error boundaries React components. You can customize the look of this error overriding css styles.

TODO

  • improve this doc again and again...
  • add a mechanism to extract the framework from micro-frontend's build and be able to cache an already loaded framework (by version) and give it to a micro-frontend if it needs to. This would reduce micro-frontends sizes, since for now, each one needs to load its own version. This is the main drawback of MfMaestro for now.
  • add list of UI/UX patterns we have been developping
  • explain backend realtime architecture
  • add realtime frontend architecture
  • improve demo to better match real use cases
  • add recommandation for backend architecture to use MfMaestro the most efficient way
  • add recommandation "how to write micro-frontends"
  • explain dynamic replacement of microfrontend using props
  • add a demo with web components
  • explain css architecture and how to use it
  • improve parameters validation
  • add injection mechanism for error boundaries

Problems & solutions