Frontend Shell Application
We will create a new "shell" MFE to act as the top-level host for all other MFEs. It is exclusively responsible for:
Initializing the application via
@edx/frontend-platform
.Loading the base, expected version of all our shared dependencies.
Rendering the "layout" of the application, including the header and footer.
Loading the brand.
Acting as the entry-point for loading MFEs at runtime and during development.
Like other hosts, it is also responsible for:
Loading all the manifests from the "guest" MFEs it intends to load.
Using module federation to load the guest MFEs on demand.
Architecture
Relationship to @edx/frontend-platform
The shell is an MFE, whereas frontend-platform is a library providing access to shared services (“platform exports” in the diagram) like authentication, an HTTP client, config, internationalization, logging, and analytics. MFEs depend on frontend-platform, but they should be decoupled from the shell application.
This means that frontend-platform stays an independent library which both the shell and other MFEs depend on. Long term (modulo maintaining backwards compatibility), the shell resembles MFEs as they exist today - initializing the app - and today’s MFEs slim down and cease to run frontend-platform’s initialize
function, as they’re just a collection of modules being loaded into the shell.
frontend-platform is a “singleton” shared dependency, meaning only one version of it can exist in the page. This ensures guest MFEs can access the shared services that the shell set up at startup time.
Preserving access to Guest config
Guest MFEs also need to be able to access their own configuration defined at build time, or via the MFE config API. This means they need their own version of frontend-platform’s export getConfig
. To accomplish this, we will likely need to find a way to not share some small subset of frontend-platform, likely the exports in @edx/frontend-platform/config
.
Dynamic Routes
We will likely want to add a helper component in frontend-platform that marries a react-router Route
with Suspense
and a component loaded at runtime from the appropriate guest MFE.
As an example, say we have a route for the profile page: /profile
.
When we navigate to that route, we need to load the ProfilePage
module from the frontend-app-profile
MFE. The shell will need module federation configuration that describes where to find this module. The element
of the Route
will be a React Suspense
component that waits for module federation to load and supply ProfilePage
and fulfill the Suspense’s promise.
Shared Dependency Resolution
Shared dependencies (React, Paragon, frontend-platform, etc.) are resolved at runtime based on the version requirements of modules. Webpack module federation handles this, we don’t have to write this code ourselves. Broadly, if a compatible version (according to semantic versioning) of a shared dependency already exists on the page, a module will use that. If a compatible version doesn’t exist, the module will fall back to the version bundled with its own MFE, which was also deployed. If a compatible versions exist in general, MFEs will never need to fall back to their own version, meaning end users download less code.
There’s a special case around “singleton” shared dependencies, such as React and frontend-platform. Singletons are denoted by a singleton: true
flag in the ModuleFederationPlugin config, and are useful when a dependency only functions properly when there’s one version of it on a page. React throws warnings and doesn’t work correctly if there are two versions of it on the page, and frontend-platform similarly doesn’t work properly if it’s loaded more than once.
Default Modules
Default modules aren’t represented on the diagram above, but are an important part of the architecture. An unmodified release of the Open edX Platform will by default include a number of core MFEs and components such as the brand, header, and footer. It will also include default frontend-platform service implementations for logging and analytics.
Minimally, operators will want to override the brand, header and footer. More adventurous operators may wish to replace entire MFEs, or use different MFEs for different users based on runtime configuration.
The shell application should include a default set of MFEs which functions with zero custom configuration. Operators should be able to easily override some or all of that default configuration.