Defining Fragments Composed in Micro Frontends as Web Components and Sharing them with Module Federation
This is a brief introduction on how to define fragments composed in Micro Frontends (hereafter, MFE) as Web Components and share them with Module Federation.
The sample code can be found in the following repository.
※ For more information about MFE, please read the following blog article.
https://silverbirder.github.io/blog/contents/mfe/
Terminology
- Fragment
- UI parts (HTML, CSS, JS, etc.) provided by each frontend team
- It can also be referred to as a component
- Composition
- Building the entire page using fragments
From the famous article by Michael Geers on MFE, there is the following sample diagram.
This example is a sample of an EC site. The checkout team uses React, and the fragments are the following two.
- Purchase button (
buy for 66.00
) - Basket (
busket: 0 items(s)
)
The composition is handled by the product team. Because of the difficulty of coordination, it might be good to have a dedicated team for composition.
Defining Fragments as Web Components
Fragments can be freely defined by each frontend team. They can be written in React, Vue, etc. From the perspective of the team composing the fragments, it would be easier to use if the interfaces of the fragments are unified. Therefore, let's define the fragments as Web Components. (The contents of the definition can be React, Vue, etc.)
I think this method can be applied to any of the following three design patterns for implementing MFE.
- Build-time composition pattern
- Server-side composition pattern
- Client-side composition pattern
Next, I will introduce the sample code.
Search Button Fragment
I will write a search button fragment (Web Components). It is a simple one that defines a button and a click handler. I chose React as the framework.
// ./packages/team-search/src/components/SearchButton/SearchButton.tsx
import React, { createContext } from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
export const CustomElementContext = createContext<HTMLElement>(
document.createElement("div")
);
export class SearchButton extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement("span");
this.attachShadow({ mode: "open" }).appendChild(mountPoint);
ReactDOM.createRoot(mountPoint as HTMLElement).render(
<React.StrictMode>
<CustomElementContext.Provider value={this}>
<App />
</CustomElementContext.Provider>
</React.StrictMode>
);
}
}
// ./packages/team-search/src/components/SearchButton/App.tsx
import { useContext } from "react";
import { CustomElementContext } from "./SearchButton";
const App = () => {
const customElement = useContext(CustomElementContext);
const onClick = () => {
customElement.dispatchEvent(
new CustomEvent("search", { detail: { num: Math.random() } })
);
};
return <button onClick={onClick}>Search</button>;
};
export default App;
This Web Components can be used by writing <search-button />
.
If you want to cooperate with other fragments, use custom events.
This Web Components fires a custom event called CustomEvent("search", { detail: <object> })
when the click button is pressed.
JSON Display Fragment
Next, I will write a fragment (Web Components) that displays the data (<object>
) of this event.
It is a simple one that just displays the given json string.
There are three ways to give data to Web Components. (There may be more)
- HTML attributes (ex.
<div attribute="value">
)- Used with primitive values (numbers, characters, etc.)
- Event listeners (
eventlistener
)- Used with non-primitive values (arrays, etc.)
- Slot (
<slot name="xxx">
)- Used when you want to insert HTML elements
This time, we have chosen HTML attributes.
// ./packages/team-content/src/components/JsonDiv/JsonDiv.tsx
import ReactDOM from "react-dom/client";
import App from "./App";
export class JsonDiv extends HTMLElement {
root: ReactDOM.Root | undefined;
static get observedAttributes() {
return ["value"];
}
attributeChangedCallback() {
const value = this.getAttribute("value") || ("{}" as string);
const props = { json: value };
if (this.root) {
this.root.render(<App {...props} />);
}
}
connectedCallback() {
const value = this.getAttribute("value") || ("{}" as string);
const props = { json: value };
const mountPoint = document.createElement("span");
this.attachShadow({ mode: "open" }).appendChild(mountPoint);
this.root = ReactDOM.createRoot(mountPoint as HTMLElement);
this.root.render(<App {...props} />);
}
}
This Web Components is used like <json-div value="{}" />
.
// ./packages/team-content/src/components/JsonDiv/App.tsx
type AppProps = {
json: string;
};
const App = (props: AppProps) => {
const { json } = props;
return <div>{json}</div>;
};
export default App;
App.tsx
simply displays the given json in a <div>
.
Composition
We will compose the fragments introduced so far. In order to compose, we need a mechanism to provide the fragments. Therefore, we use Webpack's Module Federation.
※ Adopting Module Federation has the disadvantage of tying each front-end team's build system to Webpack.
※ I thought importmap
might be another mechanism to provide, but it is unverified.
Module Federation
Module Federation is a feature introduced from Webpack@5.
Each build acts as a container and also consumes other builds as containers. This way each build is able to access any other exposed module by loading it from its container.
Module Federation allows each build to function as a container and use other containers. In this case, we containerize the Web Components' SearchButton and JsonDiv and refer to the containers in the composition build.
Let me introduce the specific code.
Containerization
Let's containerize the search button. (The fragment that displays JSON has the same code)
// .packages/team-search/src/remoteEntry.ts
export { SearchButton } from "./SearchButton";
Export what you provide as a container. Next, define the webpack plugins code.
// .packages/team-search/webpack.config.js
...
const config = {
entry: "./src/index",
plugins: [
new ModuleFederationPlugin({
name: "search",
filename: "remoteEntry.js",
exposes: {
"./App": "./src/remoteEntry",
},
shared: {
react: {
singleton: true,
strictVersion: true,
requiredVersion: "^18.0.0",
eager: true,
},
"react-dom": {
singleton: true,
strictVersion: true,
requiredVersion: "^18.0.0",
eager: true,
},
},
}),
]
};
...
Set the file you exported earlier in exposes. Set shared to prevent duplicate loading of libraries. Now, you can containerize and provide the SearchButton.
Composition
Now, let's look at the composition side build (webpack) that loads the container.
// ./webpack.config.js
const URL_MAP = {
content: process.env.CONTENT_URL || "http://localhost:3001",
search: process.env.SEARCH_URL || "http://localhost:4001",
};
const config = {
entry: "./src/index",
plugins: [
new ModuleFederationPlugin({
name: "all",
remotes: {
content: `content@${URL_MAP.content}/remoteEntry.js`,
search: `search@${URL_MAP.search}/remoteEntry.js`,
},
shared: {
react: {
singleton: true,
strictVersion: true,
requiredVersion: "^18.0.0",
},
"react-dom": {
singleton: true,
strictVersion: true,
requiredVersion: "^18.0.0",
},
},
}),
],
};
Set the URL to load the container in remotes. Next is the entry code.
// ./src/index.ts
// @see: https://webpack.js.org/concepts/module-federation/#uncaught-error-shared-module-is-not-available-for-eager-consumption
import("./bootstrap");
export {};
As you can see from @see
, the entry code needs to be dynamically loaded with import
.
Next is the bootstrap code.
// ./src/bootstrap.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// ./src/App.tsx
import { useEffect } from "react";
const App = () => {
useEffect(() => {
import("content/App").then((module) => {
const { JsonDiv } = module;
if (customElements.get("json-div") === undefined) {
customElements.define("json-div", JsonDiv);
}
});
import("search/App").then((module) => {
const { SearchButton } = module;
if (customElements.get("search-button") === undefined) {
customElements.define("search-button", SearchButton);
}
const SearchButtonElement = document.querySelector("search-button");
SearchButtonElement?.addEventListener("search", ((e: CustomEvent) => {
document
.querySelector("json-div")
?.setAttribute("value", JSON.stringify(e.detail));
}) as EventListener);
});
}, []);
return (
<>
<search-button />
<json-div />
</>
);
};
export default App;
Here, import("content/App")
and import("search/App")
are dynamically loading the container.
What is loaded is Web Components, so define it with customElements.define
.
Also, write a process to listen to the search
event handler of search-button
and set the event data to the value
attribute of json-div
.
With this, the composition is complete.
If you want to see how it actually works, please check the README.md of the repository and give it a try.
Advantages of this method
The advantages and disadvantages of using Web Components as fragments are as follows:
- Advantages
- Compatibility
- Since Web Components are a web standard technology, compatibility with libraries is easy
- You can use custom HTML tags just like using HTML tags
- Independence
- Can be developed in a sandbox environment called Shadow DOM
- Compatibility
- Disadvantages
- Javascript needs to be running
In conclusion
We have introduced a method of defining fragments composed in Micro Frontends with Web Components and sharing them with Module Federation. Although I have no practical experience, I thought it might be useful as an idea.
Share
Related tags
- Created OEmbed and OGP WebComponents for use on my blog site
- Developing an oEmbed component with WebComponents and what I learned
- The Goodness of Web Components
- If you're writing in Markdown, Rocket, an SSG that uses WebComponents, is recommended!
- Debuting "Introduction to Web Components for Beginners" at Techbook Fest 7!
- I read 'Micro Frontends'
- Micro Frontends on the Client Side (ES Module)
- Memo Micro Frontends
- Building Micro Frontends with Cloudflare Workers (Edge Worker)
- Everything you need to know about Micro Frontends
- Micro Frontends with Zalando tailor (LitElement & etcetera)
- Micro Frontends with SSR in Ara-Framework
- Everything I Learned About Micro Frontends
- Created an App to Consistently Record and Visualize Data in a Free Format
- Developing "Bochi-Bochi", an App to Easily Find Cheap Ingredients
- What I Learned from Refreshing My Blog Page with Qwik
- Introducing AI Ghostwriter - A Tool to Improve Writing Efficiency
- Development of Stable Diffusion API
- Created OEmbed and OGP WebComponents for use on my blog site
- Things I Learned from Developing Chrome Extensions (Manifest V3)
- If you're writing in Markdown, Rocket, an SSG that uses WebComponents, is recommended!
- Refreshing Silverbirder's Portfolio Page (v2)
- I Made an API That Only Returns Google Account Images
- Building a TikTok Scraping Infrastructure on GCP and the Challenges Faced
- Micro Frontends on the Client Side (ES Module)
- Micro Frontends with Zalando tailor (LitElement & etcetera)
- Micro Frontends with SSR in Ara-Framework
- Created a GAS Library, zoom-meeting-creator, to Automatically Generate Zoom Meetings
- Introducing a Tool for Bulk Updating Account Images and What I Learned
- Cotlin is a Tool for Collecting Links on Twitter, Discover Presentations from Around the World
- I tried creating rMinc, a service that registers GMail to GCalendar
- I Tried Making a One-Frame Comic Search Service Tiqav2 (Algolia + Cloudinary + Google Cloud Vision API)