Coder Social home page Coder Social logo

portal-federation's Introduction

Portal Federation

Portal for micro frontends using Webpack Module Federation and Single SPA

Project Goals

Motivation

Why look into Module Federation? Let's start with a traditional monolithic application product. Team A wrote CommonComponent1 that Team B wants to make an update to. The product needs a new version to be built. Slow and annoying. Let's upgrade to multiple projects that get bundled together and are hosted from a single webserver. Now Team A updates CommonComponent1 to v2 in their own app. Team B does not have to take that update because their app is isolated. However, the product still needs a new version, and the webserver needs to be redeployed. Annoying, still, and perhaps just as slow, just an organized slowness.

Let's now imagine that we have dozens of applications, perhaps a handful that depend on common packages, but all depend on a singular framework. The customer asks for an enhancement which requires a particular framework change, that'll need to make it to a new version of the product (a SPA). Great... Some sorry group of developers needs to make that framework change, propogate a new package version to all common component packages, get them through CI, and continue the process down the chain into the applications, and then the deployment project. What the heck? Did we exchange organization for efficiency?

Knowing what we know today, can we do better?

I'd like to think we can do better, but define better. What is better?

Opinionated "better":

  • Fewer network calls (faster load times)
  • Organized repositories (responsibilities, maintenance, teams)
  • Continuous deployment (low downtime, saas)
  • Easy adoption (scalability, complexity)
  • Proper error handling (consumer issue reporting)

Frameworks & Libraries

  • React 18 - for rendering, context, local state, and lifecycle methods
    • SolidJS - alternative to React, to test Single-SPA capabilities (below)
  • Redux - for application state container
  • Redux Toolkit - for better DX on asynchronous operation calls and communications
  • React Router 6 - for local routing between apps
  • Semantic UI React - user interface component library
  • Webpack 5 - asset/module bundler and server
  • Single-SPA* - microservice liaison (*only required if not all rendering done in React, e.g., SolidJS)
  • Express OIDC - login server
  • MongoDB - user database
  • Vercel - edge deployment (hobby plan)

Use Cases

UC-1: As a user, I want to access the application from a single URL

UC-2: As a user, I need to have my own account

UC-3: As a user, I need to be able to access features that I am allowed to

Requirements

R-1: Main entrypoint shall redirect to login screen (Express OIDC, e.g., http://localhost:3000)

R-2: Main application shall supply some form of global navigation (React, React Router 6)

R-3: Feature applications shall only be exposed to permitted users

R-4: Feature applications shall receive session information from Main application (Redux)

Design Considerations

D-1: Feature applications may have their own internal router and sub pages

D-2: Feature applications should use the same shared UI components from a container (Semantic UI React)

D-3: Feature applications should consume a global state store (Redux)

D-4: Feature applications can provide their own local state store

D-5: Feature application runtimes and (non-shared) dependencies should be bundled (Webpack 5)

D-6: Shared Application dependencies should be federated (Webpack 5)

D-7: Feature applications shall be hosted separately in true distributed fashion (Vite)

D-8: Feature applications shall be configurable via YAML or JSON files

D-9: Configuration should be done at the time of deployment (Vercel)

How it works

Understanding Module Federation

The future of JavaScript is modules, right? An overwhelming majority of the community is in agreement that the standardization and improvements towards modules and lazy imports is headed in the right direction.1 The ways of importing <script> tags and building runtime binaries locally are going to be left to a bygone era. Everyone wants in on the hip new asynchronous operation loading of remote modules via chunks. Users want to access their apps in the cloud via single-sign-on (SSO). Developers want to create cooperatively and efficiently via code sharing. Deployment teams want to support scalability.

It all boils down to if it is feasible, productive, and profitable - at least from a business perspective. I want to do my part by being a professional frontend software engineer and dive into the scalability solutions offered by the Webpack team2.

Module Federation is an interface that allows a JavaScript application to dynamically load code from another application — in the process, sharing dependencies, if an application consuming a federated module does not have a dependency needed by the federated code — Webpack will download the missing dependency from that federated build origin.

That sounds awesome! Code can be shared, but fallbacks exist for each case, always prioritizing the federated code before attempting to request more. Less code duplication, less network traffic, better performance, happier users, happier devs.

Here's a simple visual:

graph BT
	subgraph App
		B1(Libraries)-->B2(Store)-->B3(Components)
		B4(Pages)
	end
	subgraph Internals
		A1(Libraries)-->A2(Store)-->A3(Component A)
		A2-->A5(Component B)
		A6(Component C)
		A3--federated modules-->B4
		A1--federated modules-->B1
	end
Loading

Usage with Single-SPA

In traditional web applications, we serve the assets (HTML, CSS, JavaScript) to render. Now, with module federation we can separately serve our internals, like from a Design System library, and see updates near real-time in all consumer applications. Now, we may want multiple rendering libraries or multiple versions of applications being used in the same frontend. Not having that would make it simpler.

To get different rendering frameworks together, like Vue and React, or React 16 and 18, we can leverage a library called Single-SPA. Single-SPA relies on SystemJS to get the modules onto the DOM. Once the modules are loaded, they should all be using the same lifecycle methods. That won't necessarily work with Webpack 5's Module Federation, however we can replace SystemJS with federated modules.

Here's how that might look:

graph BT
	subgraph Replace
		O(Orchestrator)
		O-->MF(Module Federation)
		S(SystemJS)-->T(-Bin-)
		P1(Parcel)
		P2(Parcel)
		P1-->O
		P2-->O
	end
	subgraph Application
		A1(Libraries)-->A2(Store)-->A3(Component A)
		A2-->A4(Component B)
		A5(Component C)
		A6(Single-SPA)
		MF-->A6
		PP(=Parcel=)
		A3-->PP
		A4-->PP
		A5-->PP
	end
Loading

The code for the Parcels moves back into the applications themselves.

Understanding Module Federation (cont.)

These applications become:

"...bi-directional hosts. Any application that's loaded first, becomes a host - as you change routes and move through an application, loading federated modules in the same way you would implement dynamic imports. However if you were to refresh the page, whatever application first starts on that load, becomes a host."3

Load the Landing page first? That's the host. Navigate from there to the About page? That's a remote. Refresh on the About? Now About is the host. The fetching between hosts and remotes only requires small portions of runtime code, not an entire entrypoint or entire application.

This host/remote debacle can be streamlined in architecture as treating a simple head entrypoint on top of an application. This main entrypoint connects all the other Webpack runtimes and provisions from the orchestration layer at runtime. It's not a normal app entrypoint; only a few KB. We'll call this simple package a Remote Entry. Remote Entries are our special entrypoints that will contain a special Webpack runtime that can interface with a host, we'll call a Portal.

Terminology

Portal App (a remote)

A Portal App is a frontend application, built with Webpack, and will be consumed by the host. In order to be consumed, it must declare what it will expose. It can expose anything from lowest-level components like a particular Button to it's highest-level component like an Initializer. While there is support for bi-directional hosting, I don't see a point in using it from a more traditional app structure, so Portal Apps will not become a host - the user will only access the main entry.

Remote Entry (an entry point)

A Remote Entry, as described above, will act as an entry to its corresponding Portal App. The goal is to have this be as small as possible to avoid network overhead, and provide just enough configuration in order to allow the orchestration from Webpack to perform its operations.

Portal (a shell and host)

The Portal shell application is going to act as a host/hub for all downstream micro frontends to make up a full modular application. Built with Webpack, it will be initialized on the first page load.

graph LR;
P(Portal :3000/)
S1(:3001/remoteEntry.js)
S2(:3002/remoteEntry.js)
A1(:3001/portalApp.js)
A2(:3002/portalApp.js)
P--router-->S1--federation-->A1--bootstrapper-->S1--promise-->P
P-->S2-->A2-->S2-->P
Loading

Pseudo Code Blocks

Let's look at some code that demonstrates a remote app "app_two_remote" exposing a component called Dialog, that is going to be consumed in another remote app "app_one_remote".

//app_one_remote/src/AppRouter.js
import * as React from 'react';
import { Route, Routes } from 'react-router-dom';
import Page1 from './pages/page1';
import Page2 from './pages/page2';

export default function AppRouter() {
	return (
		<Routes>
			<Route path='/page1' element={<Page1 />} />
			<Route path='/page2' element={<Page2 />} />
		</Routes>
	);
};
//--------------------------
//app_one_remote/src/App.js
import * as React from "react";
import AppRouter from './AppRouter'
const AppContainer = React.lazy(() => import("app_one_remote/AppContainer"));

export default function App() {
	return (
		<div>
			<React.Suspense fallback="Loading App Container from Host">
				<AppContainer routes={AppRouter}/>
			</React.Suspense>
		</div>
	);
}

Basic Configuration:

//app_two_remote/webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
	plugins: [
		new ModuleFederationPlugin({
			name: "app_two_remote",
			library: { type: "var", name: "app_two_remote" },
			filename: "remoteEntry.js",
			exposes: {
				./Dialog”: "./src/Dialog"
			},
			remotes: {
				app_one: "app_one_remote", //Not necessary, but demonstrates bi-directional hosting
			},
			shared: ["react", "react-dom", "react-router-dom"]
		}),
		new HtmlWebpackPlugin({
			template: "./public/index.html",
			chunks: ["main"]
		})
	]
};

Basic Consumption:

//app_one_remote/src/pages/Page1.js
const Dialog = React.lazy(() => import('app_two_remote/Dialog'));

export default function Page1() {
	return (
		<div>
			<h1>Page 1</h1>
			<React.Suspense fallback='Loading Material UI Dialog...'>
				<Dialog />
			</React.Suspense>
		</div>
	);
}

There is little to no dependency duplication. Through the shared option — remotes will depend on host dependencies, if the host does not have a dependency, the remote will download its own. No code duplication, but built-in redundancy.3

Deployed Independently

In a true distributed fashion, these micro-frontends should be deployed separately via their own webservers. Each webserver can be on the same host IP, and different ports, or different IPs depending on the networking configuration (blah blah insert CORS/Certs hand-waiving).

5 Misconceptions4

Module Federation != Micro-frontends

Module Federation is a code transport layer, getting runtimes across boundaries. Micro-frontends on the other hand, is an architectural style that suggests sharing code between applications (particularly UI). Typically, micro-frontends are capable of being deployed standalone, while sharing pieces of those frontends can be made easier with federation.

It doesn't manage state

Exposed modules/components that have internal state management will end up being isolated in terms of state. E.g., a Counter exposed from a "remote" project to a "host" project - the "host" imports the Counter from the "remote" and what happens when we increase count? Nothing! All we're doing is sharing the objects and their properties, not runtime mutations.

Federated Modules are deployed like assets

Projects that share code will have to be deployed to operate as static assets. It becomes a static application. We aim to make an asset store, like S3, to host our modules so our app doesn't fail to load scripts or crash if a particular remote goes down. Docker is a poor choice to host these bits. The consuming applications can be hosted from Docker though, so long as they do not intend to expose modules.

Federated Modules are NOT versioned

Let's say Team A makes an update to an exposed component where function signatures were changed, i.e. parameters or return types. Team B uses that exposed component, and refreshes their page, when boom it breaks. One good way to make it obvious is with React's ErrorBoundary. Unlike traditional NPM published packages being consumed on all apps, using federated modules may break at runtime after deployment because of the unsafe version-control. The huge tradeoff here is safety for efficiency.

Federated Modules do NOT have type declarations

Since exposed modules are purely compiled JavaScript, there is no typing information involved, at all! That's a big bummer! How can we remedy this? Well, we can add a local @types/**/index.d.ts within our project, then massage the consuming tsconfig.json with jsx: "react" and paths: ['./src/@types]. It'd be better if we could define some sort of contract between the two, ergo a shared library. Our shared library will have all it's functions, components, modules, and type declarations provided as part of the index.

You can define those types in any build-time available resource, could be an NPM library, a local file, etc. The issue here is build-time vs run-time and it's not specific to Module Federation. - Jack Herrington

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.