Implementing micro frontend
Stack
VueJS -> container
ReactJS + web component -> component
Rollup -> bundling component
ReactJS + Web component
Here is the react component
Count/index.js
import React from 'react'
import styles from "./count.module.scss";
const Count = ({count}) => {
return (
<div className={styles.text}>{count}</div>
)
}
export default Count
Count/count.module.scss
.text{
color: aquamarine;
}
Here is the script of web component which is wrapper of react component
Count/count.js
import ReactDOM from 'react-dom/client';
import Count from '.';
class CountElement extends HTMLElement {
get count(){
return this.getAttribute("count");
}
set count(val){
this.setAttribute("count",val);
}
connectedCallback() {
this.attachShadow({mode: 'open'});
let link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'http://localhost:3000/component.css');
this.shadowRoot.appendChild(link);
const mountPoint = document.createElement("div");
this.shadowRoot.appendChild(mountPoint);
const root = ReactDOM.createRoot(mountPoint);
root.render(<Count count={this.count}/>);
}
}
window.customElements.define("count-display", CountElement);
CountButton/countButton.js
import ReactDOM from 'react-dom/client';
import CountButton from '.';
class CountButtonElement extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
let link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('href', 'http://localhost:3000/component.css');
this.shadowRoot.appendChild(link);
const mountPoint = document.createElement("div");
this.shadowRoot.appendChild(mountPoint);
const root = ReactDOM.createRoot(mountPoint);
// emit the event with custom name
const event = new CustomEvent("onAddCount",{
// allow event buddle up to parent
buddles: true,
// allow event to propagate across shadow boundaries
composed:true
})
root.render(<CountButton onAddCount={()=>{
document.dispatchEvent(event);
}} />);
}
}
window.customElements.define("count-button", CountButtonElement);
Here is the main target bundling file
component.js
import "./Count/count";
import "./CountButton/countButton";
Here is the rollup file, to bundle css and web component js file as a output
rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import { terser } from "rollup-plugin-terser";
import postcss from 'rollup-plugin-postcss';
import commonjs from '@rollup/plugin-commonjs';
import babel from '@rollup/plugin-babel';
import externalGlobals from "rollup-plugin-external-globals";
// hard node env to production for bundled js file
const Global = `var process = {
env: {
NODE_ENV: 'production'
}
}`
// eslint-disable-next-line import/no-anonymous-default-export
export default {
input: "src/component/component.js",
output: {
file: "public/component.js",
format: "esm",
banner: Global
},
// prevent bundling with react
external: ["react","react-dom", "react-scripts"],
plugins: [
resolve(),
babel({
presets: ["@babel/preset-react"],
}),
commonjs(),
// reduce the bundle size
terser(),
// support sass, bundle multiple sass files into single css file
postcss({
extract: true,
modules: true,
use: ['sass'],
}),
// make sure react is injected from the script of vue container
externalGlobals({
react: "React",
"react-dom": "ReactDOM",
})
],
}
Here is the package.json
package.json
{
"name": "react-microfrontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.3.0",
"@testing-library/user-event": "^13.5.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
},
"devDependencies": {
"@babel/preset-react": "^7.18.6",
"@rollup/plugin-babel": "^5.3.1",
"@rollup/plugin-commonjs": "^22.0.0",
"@rollup/plugin-node-resolve": "^13.3.0",
"node-sass": "^7.0.1",
"postcss": "^8.4.14",
"rollup": "^2.75.7",
"rollup-plugin-dts": "^4.2.2",
"rollup-plugin-external-globals": "^0.6.1",
"rollup-plugin-peer-deps-external": "^2.2.4",
"rollup-plugin-postcss": "^4.0.2",
"rollup-plugin-sass": "^1.2.12",
"rollup-plugin-terser": "^7.0.2",
"sass": "^1.53.0"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"build:rollup": "rollup -c"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}
VueJS
Here is the public html file which is injected the output script file from react repo and react script file
public/index.html
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin ></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin ></script>
<script type="module" src="http://localhost:3000/component.js" defer></script>
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<noscript>
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
Here is main app file of vue
App.vue
<template>
<count-display :count="count" :key="count" />
<count-button />
</template>
<script>
export default {
name: 'App',
components: {
},
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
},
mounted(){
document.addEventListener("onAddCount", this.increment);
}
}
</script>
<style>
</style>
Result
Last updated
Was this helpful?