DEV Community

Cover image for How to implement Micro Frontends with Reactjs and qiankun
Nima Habibkhoda
Nima Habibkhoda

Posted on

How to implement Micro Frontends with Reactjs and qiankun

<Hello world/>

Maybe you don’t know what is Micro Frontend and why we have to use it, because it is a new technology on frontend. there are few plugins and articles about it, and I think it is better to focus on some plugins that have no documents.
There is a good source about use cases and motivations of Micro frontend.

Micro frontends

What I want to work with, is Qiankun
Qiankun

Our Scenario

Imagine that we have a project named mf-productthat will work like a basket, we will add some products in our basket (Dummy products) and DVA will reduce states to show them on our frontend.

DVA js

Now we want to implement it into parent project that is micro-frontend and we have to show the mf-product in #mf-container DOM. it is easy , isn’t it? but how?


I just know that mf-product is runing on localhost:8001 and we can fetch it from there.

You can pull the project from : https://github.com/nimahkh/micro-frontend

Start

First we have to install qiankun
mkdir micro-front && cd micro-front && yarn add qiankun
Then we will create our package.json to see what we need for start.

{
  "name": "micro-front",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "devDependencies": {
    "@babel/core": "^7.7.2",
    "npm-run-all": "^4.1.5",
    "@babel/plugin-transform-react-jsx": "^7.7.0",
    "@babel/preset-env": "^7.7.1",
    "babel-loader": "^8.0.6",
    "css-loader": "^3.2.0",
    "html-webpack-plugin": "^3.2.0",
    "less-loader": "^6.2.0",
    "style-loader": "^1.0.0",
    "webpack": "^4.41.2",
    "webpack-cli": "^3.3.10",
    "webpack-dev-server": "^3.9.0",
    "cross-env": "^7.0.2"
  },
  "dependencies": {
    "qiankun": "^2.3.5",
    "react": "^16.13.1",
    "react-dom": "^16.13.1",
    "vue": "^2.6.11"
  }
}
Enter fullscreen mode Exit fullscreen mode

Why we need these packages?

We need babel for transpiling our es6 codes and also we need webpack and less for lunching our main project. the main project is the project that will load mf-product from port 8001 into 8000 at div that has #mf-container DOM.

yarn #install dependencies
Enter fullscreen mode Exit fullscreen mode

Create main Project

$ vim index.html
Enter fullscreen mode Exit fullscreen mode

Put the codes below in index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Example</title>
</head>
<body>
  <div class="mainapp">
    <header class="mainapp-header">
      <h1>Micro Frontend</h1>
    </header>
    <div class="mainapp-main">
      <ul class="mainapp-sidemenu">
        <li onclick="push('/mf-product')">Products</li>
      </ul>
      <main id="mf-container"></main>
    </div>
  </div>
<script>
    function push(subapp) { history.pushState(null, subapp, subapp) }
  </script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

The script part will redirect to links.
Now we need index.js file to register our Micro Apps, here we will introduce to the application that we have a project that is lunched on port 8001 and I want to show it on #mf-container that has a url equal to mf-product

import {
  registerMicroApps,
  start,
  setDefaultMountApp,
  prefetchApps,
  addGlobalUncaughtErrorHandler,
} from "qiankun";
import render from "./render/ReactRender";
render({ loading: true });
const loader = (loading) => render({ loading });
registerMicroApps([
  {
    name: "mf-product", // app name registered
    entry: "//localhost:8001",
    container: "#mf-container",
    loader,
    activeRule: "/mf-product",
  },
]);
prefetchApps([{ name: "mf-product", entry: "//locahost:7101" }]);
start();
setDefaultMountApp("/mf-product");
addGlobalUncaughtErrorHandler((event) => console.log(event));
Enter fullscreen mode Exit fullscreen mode

Oh Man , what is Render?

nothing, it is just a loader with Reactjs , and it is why we need babel.

create render folder and create one file named: ReactRender.jsx

import React from "react";
import ReactDOM from "react-dom";
function Render(props) {
  const { loading } = props;
return (
    <>
      {loading && <h4 className="subapp-loading">Loading...</h4>}
      <div id="mf-container" />
    </>
  );
}
export default function render({ loading }) {
  const container = document.getElementById("mf-container");
  ReactDOM.render(<Render loading={loading} />, container);
}
Enter fullscreen mode Exit fullscreen mode

Now we are showing one simple loading until sub app fetched. after fetching the sub app (the application that we want to show it) we have to render it on mf-container .
Just we need to setup our webpack.config.js for lunching the project :

const HtmlWebpackPlugin = require("html-webpack-plugin");
const { name } = require("./package");
module.exports = {
  entry: "./index.js",
  devtool: "source-map",
  devServer: {
    open: true,
    port: "8000",
    clientLogLevel: "warning",
    disableHostCheck: true,
    compress: true,
    headers: {
      "Access-Control-Allow-Origin": "*",
    },
    historyApiFallback: true,
    overlay: { warnings: false, errors: true },
  },
  output: {
    publicPath: "/",
  },
  mode: "development",
  resolve: {
    extensions: [".js", ".jsx", ".ts", ".tsx"],
  },
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
          options: {
            presets: ["@babel/preset-env"],
            plugins: ["@babel/plugin-transform-react-jsx"],
          },
        },
      },
      {
        test: /\.(le|c)ss$/,
        use: ["style-loader", "css-loader", "less-loader"],
      },
    ],
  },
  plugins: [
    new HtmlWebpackPlugin({
      filename: "index.html",
      template: "./index.html",
      minify: {
        removeComments: true,
        collapseWhitespace: true,
      },
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

It is very easy, I am not going to explain it. just we are trying to transpile and compile our plugins and finaly run the main on port 8000.

Now what?

We need to pull the project that we want to implement it on main.
Change your directory to root project (where package.json is), then pull the project.

git pull https://github.com/nimahkh/mf-product
Enter fullscreen mode Exit fullscreen mode

Our project is created with UMI, so I will explain in two ways, one with UMI and one with pure React.

UMI

it is not important what we did, and what are our components, just we have to describe our .umirc.js

import { name } from "./package.json";
import { slave } from "umi-plugin-qiankun";
export default {
  base: name,
  publicPath: "/mf-product/",
  outputPath: "./dist",
  mountElementId: "mf-product",
  plugins: [
    [
      slave,
      {
        keepOriginalRoutes: true
      }
    ],
    [
      "umi-plugin-react",
      {
        title: "mf-product",
        antd: true,
        dva: {
          always: true,
          hmr: true
        },
        dynamicImport: true,
        routes: {
          exclude: [
            /models\//,
            /services\//,
            /model\.(t|j)sx?$/,
            /service\.(t|j)sx?$/
          ]
        }
      }
    ]
  ]
};
Enter fullscreen mode Exit fullscreen mode

just we have to install umi-plugin-qiankun

$ yarn add @umijs/plugin-qiankun
Enter fullscreen mode Exit fullscreen mode

Pure React

At first we have to ovveride webpack with wired or rescript, I am going to do it with rescript. first create .rescriptsrc.js

const { name } = require('./package');
module.exports = {
  webpack: config => {
    config.output.library = `${name}-[name]`;
    config.output.libraryTarget = 'umd';
    config.output.jsonpFunction = `webpackJsonp_${name}`;
    config.output.globalObject = 'window';
return config;
  },
devServer: _ => {
    const config = _;
config.headers = {
      'Access-Control-Allow-Origin': '*',
    };
    config.historyApiFallback = true;
config.hot = false;
    config.watchContentBase = false;
    config.liveReload = false;
return config;
  },
};
Enter fullscreen mode Exit fullscreen mode

This code will create output library from your package.json name property.
Now inside your package.json , change start command inside scripts block : "start": "rescripts start" .
Add .env inside root directory:

SKIP_PREFLIGHT_CHECK=true
BROWSER=none
PORT=7100
WDS_SOCKET_PORT=7100
Enter fullscreen mode Exit fullscreen mode

We are near to end, just we have to create public-path.js.

why?

We have to introduce to qiankun that we have global variable with name of our app to load it.
src/public-path.js

if (window.__POWERED_BY_QIANKUN__) {
  // eslint-disable-next-line no-undef
 __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
}
Enter fullscreen mode Exit fullscreen mode

Now you have to just change index.js of the application like below:

src/index.js

import './public-path';
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
function render(props) {
  const { container } = props;
  ReactDOM.render(<App />, container ? container.querySelector('#root') : document.querySelector('#root'));
}
function storeTest(props) {
  props.onGlobalStateChange((value, prev) => console.log(`[onGlobalStateChange - ${props.name}]:`, value, prev), true);
  props.setGlobalState({
    ignore: props.name,
    user: {
      name: props.name,
    },
  });
}
if (!window.__POWERED_BY_QIANKUN__) {
  render({});
}
export async function bootstrap() {
  console.log('[react16] react app bootstraped');
}
export async function mount(props) {
  console.log('[react16] props from main framework', props);
  storeTest(props);
  render(props);
}
export async function unmount(props) {
  const { container } = props;
  ReactDOM.unmountComponentAtNode(container ? container.querySelector('#root') : document.querySelector('#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();
Enter fullscreen mode Exit fullscreen mode

It is done dude!
Now come to see what we did, but there is no scripts block in main package.json, so come to add it

"scripts": {
    "start:product": "cd mf-product  && yarn start",
    "start:main": "webpack-dev-server",
    "start": "npm-run-all --parallel start:*",
    "start:main-m": "cross-env MODE=multiple webpack-dev-server",
    "start:multiple": "run-p start:main-m start:product",
    "test": "echo \"Error: no test specified\" && exit 1"
  }
Enter fullscreen mode Exit fullscreen mode

YESSSSSS!, now just run

$ yarn start
Enter fullscreen mode Exit fullscreen mode

Now you can check localhost:8000 and you will see the mf-project in our main layout, also you can check localhost:8001 to see that mf-product is working on it’s port.
It is very nice, isn’t it?
I hope that I explained it clearly.
<good by world />

Top comments (1)

Collapse
 
kyle_hensoon profile image
kyle henson

that was very useful, thanks Nima , keep going