“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

preface

The concepts related to the micro front end are not explained here.

At present, it is the mainstream implementation to build micro-front-end applications based on single-SPA, but the example on its official website combines a large number of its own components, which is rather tedious. Not very friendly to beginners

This article will combine Ant Design, SingleSpa and Create React App to achieve a more appropriate example of the actual business scenario

We will implement the following functionality

  • Built based on Ant Design, SingleSpa and Create React App
  • Style isolation
  • Load submodule resources remotely
  • The child application automatically generates a manifest file containing entrypoints

Complete code can be consulted: source code

base

Install dependencies

NPM install @ant-design/pro-layout axios single-spa url-join --saveCopy the code

The functions that the pedestal needs to implement include

  • Basic layout
  • Sub-application Lifecycle Management (Single-SPA)
  • Sub-application remote resource loading

For the sake of demonstration, we will directly use @ant-Design/Pro-Layout to build the basic layout

container.jsx

export default function Container ()  {
  const [settings, setSetting] = useState({ fixSiderbar: true });
  const [pathname, setPathname] = useState('/welcome');
  const history = useHistory();
  return (
    <div
      id="test-pro-layout"
      style={{
        height: '100vh'}} >
      <ProLayout
        {. defaultProps}
        location={{
          pathname,}}waterMarkProps={{
          content: 'Pro Layout'}}onMenuHeaderClick={(e)= > console.log(e)}
        menuItemRender={(item, dom) => (
          <a
            href="javascript:void(0)"
            onClick={()= > {
              setPathname(item.path)
              history.push(item.path)
            }}
          >
            {dom}
          </a>
        )}
        rightContentRender={() => (
          <div>
            <Avatar shape="square" size="small" icon={<UserOutlined />} / ></div>)} {... settings} ><div id="container" ></div>
      </ProLayout>
    </div>
  );
};
Copy the code

Notice here that we added a div with the ID container to host the child application

Complete code can be consulted source code, set up the effect of the following figure

We have configured two child applications, home and About

We store the application information in an array, where host represents the address to load the child application resources and match is used for routing matches

The application is then registered through single-spa’s registerApplication

RegisterApplication takes four parameters

  • AppName: string. The application name must be unique

  • applicationOrLoadingFn: () =>

    . Load the child application and return the lifecycle functions required by single-SPA (mount, bootstrap, unmount)

  • activityFn: (location) => boolean. It is used for route matching of sub-applications. The parameter is window.location. It must be a pure function and can implement route configuration rules by itself

  • customProps? : Object | () = > Object. When we call son application life cycle method, the parameters of the transmission. It is generally used to pass shared data, such as reduex state, but is not used here to avoid complexity.

When applicationOrLoadingFn loads the child application, we call loadResources to load the child application remote resources

The complete code is as follows

// Configure the subapplication
const apps = [
  {
    name: 'home'.host: 'http://localhost:3001'.match: /^\/home/
  },
  {
    name: 'about'.host: 'http://localhost:3002'.match: /^\/about/}]// Register the application
for (let i = 0, app = null; i < apps.length; i++) {
  app = apps[i];
  singleSpa.registerApplication(
    app.name, 
    async (arg) => {
      // The remote resource is loaded here, and once loaded, the child application exposes the lifecycle functions required by single-SPA
      await loadResources(app.host);
      return window[app.name];
    },
    location= > {
      return app.match.test(location.pathname)
    }
  );
}
 / / start
singleSpa.start();
Copy the code

LoadResources Loads subapplication resources

The process for loading a child application’s remote resources is as follows

The child application provides a manifest.json that describes entry file information, similar

{
  "entrypoints": {
    "main": {
      "chunks": [
        "runtime-main"."vendors~main"."main"]."assets": [
        "static/js/bundle.js"."..."
      "children": {},"childAssets": {}}},"publicPath": "http://localhost:3001/"
}
Copy the code

After the base requests the manifest file, it parses the resources in entryPoints and creates the corresponding Style /script tags to load the resources

The entire execution process is as follows

,

Specific code

export const loadResources = async (url) => {
  const [css, js] = await getManifest(url);
  return Promise.all([loadStyles(css), loadScripts(js)]) 
}
Copy the code

Loading manifest. Json

export const getManifest = (url) = >
  new Promise(async (resolve) => {
    const u = urlJoin(url, 'manifest.json');
    
    const { data } = await axios.get(u);
    
    const { entrypoints, publicPath } = data;
    const key = getFirstKey(entrypoints);
    if(! key) {return resolve([])
    }
    const assets = (entrypoints[key].assets || []).filter((file) = >
      /(\.css|\.js)$/.test(file)
    );
    const css = [],
      js = [];
    for (let i = 0; i < assets.length; i++) {
      const asset = assets[i];
      const assetPath = urlJoin(publicPath, asset);
      if (/\.css$/.test(asset)) {
        css.push(assetPath);
      } else if (/\.js$/.test(asset)) {
        js.push(assetPath);
      }
    }
    resolve([css, js])
  });
Copy the code

Load the style

export const loadStyles = async (res) => {
  res = (res || []).filter(href= > !Boolean(hasLoadedStyle(href)))
  return Promise.all(res.map(loadStyle));
}

export const createStyle = async (url) => {
  return new Promise((resolve, reject) = > {
    const styleLink = document.createElement("link");
    styleLink.link = url;
    styleLink.onload = resolve;
    styleLink.onerror = reject;
    document.head.appendChild(styleLink);
  });
};

Copy the code

Load the script

export const loadStyles = async (res) => {
  res = (res || []).filter(href= > !Boolean(hasLoadedStyle(href)))
  return Promise.all(res.map(loadStyle));
}

export const createScript =  (url) = > {
  return new Promise((resolve, reject) = > {
    const s = document.createElement('script');
    s.type = 'text/javascript';
    s.src = url;
    s.onload = resolve;

    s.onerror = (. rest) = > reject(rest);
    document.head.appendChild(s);
});
};
Copy the code

The child application

Here we have home and About child apps

npx create-react-app home


npx create-react-app about

Copy the code

The sub-application needs to implement the following functions:

  • Provide the manifest.json manifest file
  • Building renovation
  • Style isolation
  • Provides life cycle functions required for single-SPA applications

Provides application lifecycle methods

Here we use single-spa-React


import React from "react";
import ReactDOM  from "react-dom";
import singleSpaReact from 'single-spa-react';
import Container from './components/Container'
import './index.css';

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  domElementGetter: () = > document.getElementById('container'),
  rootComponent: Container
});

export const bootstrap = reactLifecycles.bootstrap;
export const mount = reactLifecycles.mount;
export const unmount = reactLifecycles.unmount;



Copy the code

Building renovation

As mentioned earlier, after loading the child application resources, the child application will expose the single-SPA life cycle functions to the window base to call. This requires us to modify the entire build output. Our project was created using create-react-app. Here we use react-app-rewired to extend

npm install react-app-rewired -D
Copy the code

Then replace package.json scripts with create-react-app to execute

"scripts": {
    "start": "react-app-rewired start"."build": "react-app-rewired build"."test": "react-app-rewired test"."eject": "react-app-rewired eject"
  }
Copy the code

Create config-overrides. Js in the project directory

Two things need to be changed

  • 1. Specify the exported object and mount point
  • 2. Solve cross-domain problems

The following

module.exports = {
  webpack: function(config, env) {
    // Application name
    config.output.library = 'about';
    config.output.libraryTarget = "window";
    // The default is "/", since the child application resource is executed in the base and needs to be respecified, it is written to death for demonstration purposes
    config.output.publicPath = 'http://localhost:3002/';
    return config;
  },
  // Resolve cross-domain issues
  devServer: function(configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.disableHostCheck = true
      config.headers = config.headers || {}
      config.headers['Access-Control-Allow-Origin'] = The '*'
      return config
    }
  }
}
Copy the code

Provide the manifest. Json

Here we use the webpack-stats-plugin plugin to achieve this

npm install webpack-stats-plugin -D
Copy the code

Add the configuration in config-overrides. Js

 config.plugins.push(
      new StatsWriterPlugin({
        fields: ['entrypoints'.'publicPath'].filename: "manifest.json" // File name}))Copy the code

After adding the restart project, try accessing localhost:{port}/manifest.json

Style isolation

Here we use postCSs-selector -namespace to add the namespace plug-in to implement, child application CSS selector is added with a prefix to achieve isolation

To extend the postCSS configuration we use react-app-rewi-postcss

npm install react-app-rewire-postcss postcss-selector-namespace -D
Copy the code

Continue to modify config-overrides. Js. The complete configuration file is as follows

const { StatsWriterPlugin } = require("webpack-stats-plugin");
module.exports = {
  webpack: function (config, env) {
    require("react-app-rewire-postcss")(config, {
      plugins: (loader) = > {
        console.log(loader)
        return [
          require("postcss-flexbugs-fixes"),
          require("postcss-preset-env") ({autoprefixer: {
              flexbox: "no-2009",}}),require("postcss-selector-namespace") ({namespace(css) {
              // prefix, if there are global styles that do not need to be added, can also be filtered here
              return ".micro-frontend-home"; },}),]}}); config.output.library ="home";
    config.output.libraryTarget = "window";
    // The default is "/", since the child application resource is executed in the base and needs to be respecified, it is written to death for demonstration purposes
    config.output.publicPath = "http://localhost:3001/";
    config.plugins.push(
      new StatsWriterPlugin({
        fields: ["entrypoints"."publicPath"].filename: "manifest.json"./ / file name}));return config;
  },
  devServer: function (configFunction) {
    return function (proxy, allowedHost) {
      const config = configFunction(proxy, allowedHost);
      config.inline = true;
      config.disableHostCheck = true;
      config.headers = config.headers || {};
      config.headers["Access-Control-Allow-Origin"] = "*";
      returnconfig; }; }};Copy the code

Complete code can be consulted: source code