For achieving a more flexible system and providing the right basis for future scalability the microfrontends project requires a solid decomposition of its functional domain.
Right now the split is mainly driven technically, e.g., such that everything related to the primary navigation bar should be in a package (microfrontend) dedicated to this subject. However, since the technical domain is distributed in either way (multiple microfrontends may want to place something in the primary navigation bar) it makes rather sense to distribute in terms of the exposed domain. As a result, repositories can be better understood by domain experts and the model of additions / removals scales better.
Drawing the current architecture could be done in many different ways. Looking at the app shell (the given index.html together with the initial loading script directly referenced in there) we could end up in a diagram like below:
The app shell consists of a loading script that contains SystemJS and the algorithm necessary for loading some initial modules and microfrontends. Furthermore, the translation system is already prepared here.
Some packages (such as react, react-dom, single-spa, but also some openmrs packages) are used also in the loading script - referenced via SystemJS from the import map. The import map is also placed in the index.html from the app shell.
The loading of the microfrontends follows a convention to distinguish between packages (supposed to be lazy loaded) and microfrontends (supposed to be evaluated directly for gathering additional infos such as the activation function).
In terms of repositories we have:
- openmrs-module-spa (backend module and frontend app shell assets)
- openmrs-esm-api, openmrs-esm-config, etc. (core modules for cross-cutting concerns)
- openmrs-esm-login, openmrs-esm-home, ... (core modules for elementary UX elements)
- openmrs-esm-patient-chart and openmrs-esm-patient-widgets (patient chart page and elements on the page)
- openmrs-esm-devtools, openmrs-esm-patient-registration, ... (more pages and elements)
Every repository comes its own Webpack configuration, dependencies, and CI/CD pipeline. In terms of isolation / independence this is as good as it gets.
While the current state does have some benefits, it also comes with drawbacks. Most notably, some of the modules are actually not exchangeable (at least in practice). For instance, no one would want to replace openmrs-esm-api or openmrs-esm-error-handling in practice. In general, these are just cross-cutting concerns that should be treated like invariants (and are actually already treated as such). As a result it makes sense to combine all these cross cutting concerns in the app shell already.
Since single-spa and other external core packages (e.g., react) are similar in that matter, we could also place these in the app shell, too.
Earlier, we distinguished between the cross-cutting concerns and the elementary UX elements. The latter should either be included in the app shell directly or indirectly via an import map. In any case they are assumed to be always there.
Right now pretty much all of the widgets from openmrs-esm-patient-chart-widgets (https://github.com/openmrs/openmrs-esm-patient-chart-widgets/tree/master/src/widgets) should be considered for a dedicated microfrontend. Things like banner potentially distribute among all of the microfrontends.
Also new widgets such as the immunization make perfect sense for distribution into a dedicated package (microfrontend).
One possibility is to utilize a monorepo for combining certain parts:
- openmrs-module-spa (backend module)
- openmrs-esm-spa (app shell incl. openmrs-esm-login, openmrs-esm-home, openmrs-esm-primary-navigation, openmrs-esm-styleguide, openmrs-esm-api, openmrs-esm-module-config, maybe also openmrs-esm-devtools)
- openmrs-esm-patient (patient-registration, patient-chart, decomposed patient-chart-widgets)
The rest remains as single repositories.
The foundation that allows a much more flexible and modular system is an extension component ↔ extension slot mechanism. In general, this follows an event listener ↔ event emitter mechanism - just for components.
The idea is that a component may offer a place / places where additional - context-sensitive - functionality could be displayed. This is an extension slot.
Our extension slot should be capable of:
- defining a fallback if nothing would be shown otherwise
- restricting rendering to a defined number of components (e.g., only allow a single component)
- reordering based on configuration (implicitly handled; ordering key to be defined)
- allowing or banning extensions should also be implicit via the configuration (blocking key to be defined)
- lazy loading the component(s) filling the extension slot, without any need to know where (from what microfrontends) they are coming from
Extension slots pass in their configuration (see above), together with some parameters (context-sensitive), and (most importantly) the name of the extension slot. The name acts as an identifier to find the relevant extension components.
- Please list below!