Micro Front-end react apps created by Create-React-App
Micro-frontend architecture is a design approach in which a front-end app is decomposed into individual, semi-independent “microapps” working loosely together.
Th applications are loaded into a micro-frontend container. The container runs it as of the micro frontend is its own component and provides seamless workflow to users.
- Launch container app.
- Launch sub-app1 and sub-app2 applications on specific ports.
- Based on the URL, the Container will route to one of the micro front-ends.
- The selected micro front-end goes to the specific port to fetch the application’s asset-manifest.json. From this JSON file, the included main.js is put on a script tag and loaded.
- A manifest file contains a mapping of all asset filenames.
- Container app passes the containerId and history for its micro front-ends to be rendered.
- Install "react-app-rewired" — This allows customizing the app without ejecting app.
npm i --save react-app-rewired
- Modify package.json to set port and use "react-app-rewired" in sub-apps.
"scripts": {
"start": "PORT=4001 react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
- Add config-overrides.js to disable code splitting. By default, code splitting is enabled. An application is split into several chunks that can be loaded onto the page independently. You can see http://localhost:4001/asset-manifest.json before adding react-app-rewired. It clearly shows the app has been chunked.
//config-overrides.js
module.exports = {
webpack: (config, env) => {
config.optimization.runtimeChunk = false;
config.optimization.splitChunks = {
cacheGroups: {
default: false,
},
};
return config;
},
};
- Make changes in src/index.js to define render and unmount functions.
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import * as serviceWorker from "./serviceWorker";
// render micro frontend function
window.rendersubapp1 = (containerId, history) => {
ReactDOM.render(
<App history={history} />,
document.getElementById(containerId)
);
serviceWorker.unregister();
};
// unmount micro frontend function
window.unmountsubapp1 = containerId => {
ReactDOM.unmountComponentAtNode(document.getElementById(containerId));
};
// Mount to root if it is not a micro frontend
if (!document.getElementById("subapp1-container")) {
ReactDOM.render(<App />, document.getElementById("root"));
}
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister();
- If an app is running independent, it will be rendered to root element. If it is a micro front-end, it will be rendered to containerId by window.rendersubapp1.
- Use src/setupProxy.js to set up CORS rule.
module.exports = app => {
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
next();
});
};
- Configure Your .env File to Set Up a Host for Each Micro-Frontend Application in Container app.
REACT_APP_SUBAPP1_HOST=http://localhost:4001
REACT_APP_SUBAPP2_HOST=http://localhost:4002
- Add Microfront.js file in sec directory, It picks up a manifest file from a running application and launches the application through a script and link tag.
import React from 'react';
class MicroFrontend extends React.Component {
componentDidMount() {
const { name, host, document } = this.props;
const scriptId = `micro-frontend-script-${name}`;
if (document.getElementById(scriptId)) {
this.renderMicroFrontend();
return;
}
fetch(`${host}/asset-manifest.json`)
.then(res => res.json())
.then(manifest => {
manifest["entrypoints"].map((entry => {
if (typeof manifest["files"][entry] !== "undefined" && manifest["files"][entry] !== "undefined") {
if (entry.endsWith('.css')) {
const link = document.createElement('link');
link.id = scriptId;
link.href = `${process.env.NODE_ENV === "production" ? host.slice(0, host.lastIndexOf('/')) : host}${manifest["files"][entry]}`;
link.onload = this.renderMicroFrontend;
link.rel = "stylesheet"
document.head.appendChild(link);
}
const script = document.createElement('script');
script.id = scriptId;
script.crossOrigin = '';
script.src = `${process.env.NODE_ENV === "production" ? host.slice(0, host.lastIndexOf('/')) : host}${manifest["files"][entry]}`;
script.onload = this.renderMicroFrontend;
document.head.appendChild(script);
}
})
)
const script = document.createElement('script');
script.id = scriptId;
script.crossOrigin = '';
script.src = `${process.env.NODE_ENV === "production" ? host.slice(0, host.lastIndexOf('/')) : host}${manifest["files"]["main.js"]}`;
script.onload = this.renderMicroFrontend;
document.head.appendChild(script);
const link = document.createElement('link');
link.id = scriptId;
link.href = `${process.env.NODE_ENV === "production" ? host.slice(0, host.lastIndexOf('/')) : host}${manifest["files"]["main.css"]}`;
link.onload = this.renderMicroFrontend;
link.rel = "stylesheet"
document.head.appendChild(link);
});
}
componentWillUnmount() {
const { name, window } = this.props;
window[`unmount${name}`] && window[`unmount${name}`](`${name}-container`);
}
renderMicroFrontend = () => {
const { name, window, history } = this.props;
window[`render${name}`] && window[`render${name}`](`${name}-container`, history);
};
render() {
return <main id={`${this.props.name}-container`} />;
}
}
MicroFrontend.defaultProps = {
document,
window,
};
export default MicroFrontend;
- In Container App Create a Micro-Frontend Component for each micro-frontend application and Use a route to invoke it.
import React from "react";
import { NavLink, BrowserRouter, Route, Switch } from "react-router-dom";
import MicroFrontend from "./MicroFrontend";
const {
REACT_APP_SUBAPP1_HOST: subapp1,
REACT_APP_SUBAPP2_HOST: subapp2
} = process.env;
const SubApp2 = ({ history }) => (
<MicroFrontend history={history} host={subapp2} name="subapp2" />
);
const SubApp1 = ({ history }) => (
<MicroFrontend history={history} host={subapp1} name="subapp1" />
);
const Home = () => (
<>
<p>Rendered by Container</p>
</>
);
const App = props => {
return (
<BrowserRouter>
<div style={{ padding: 25, display: "flex" }}>
<div style={{ padding: "0px 15px" }}>
<NavLink
style={{
textDecoration: "none",
fontWeight: "bold",
color: "#282c34",
fontSize: 20
}}
to="/home"
>
Home
</NavLink>
</div>
<div style={{ padding: "0px 15px" }}>
<NavLink
style={{
textDecoration: "none",
fontWeight: "bold",
color: "#282c34",
fontSize: 20
}}
to="/subapp1"
>
SubApp1
</NavLink>
</div>
<div style={{ padding: "0px 15px" }}>
<NavLink
style={{
textDecoration: "none",
fontWeight: "bold",
color: "#282c34",
fontSize: 20
}}
to="/subapp2"
>
SubApp2
</NavLink>
</div>
</div>
<Switch>
<Route path="/home" component={Home} />
<Route path="/subapp1" render={() => <SubApp1 />} />
<Route path="/subapp2" render={() => <SubApp2 />} />
</Switch>
</BrowserRouter>
);
};
export default App;
Container App: http://localhost:3000
SubApp1: http://localhost:4001
SubApp2: http://localhost:4002