Develop Main App

In the previous Experience micro frontend, an example was used to demonstrate how to create and configure micro frontend sub-applications. Through this chapter, you can further understand how to develop the main application, and its common configuration.

After creating an Modern.js project through the @modern-js/create command, you can execute pnpm run new in the project (the modern new command is actually executed). After selecting the 「micro frontend」 mode, the micro frontend will be installed. Dependencies, just register the plugin manually.

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { garfishPlugin } from '@modern-js/plugin-garfish';

export default defineConfig({
  runtime: {
    router: true,
    state: true,
    masterApp: true,
  },
  plugins: [appTools(), garfishPlugin()],
});

Register Sub-app

When the information is provided on the masterApp configuration, the application will be considered the main application. At present, there are two configuration methods for sub-app information, and these two methods are applied to different scenarios.

Register sub-app info directly

You can register the sub-application information directly through the configuration:

TIP

It can be configured at runtime via the API defineConfig.

When the parameter is a function, it cannot be serialized to the output code, so please configure it through defineConfig when it comes to configuration such as functions

src/App.tsx
import { defineConfig } from '@modern-js/runtime';

defineConfig(App, {
  masterApp: {
    apps: [
      {
        name: 'DashBoard',
        entry: 'http://127.0.0.1:8081/',
      },
      {
        name: 'TableList',
        entry: 'http://localhost:8082',
      },
    ],
  },
});

Custom remote app list

This function allows you to pull a list of remote child applications and register them with the runtime framework:

App.tsx
defineConfig(App, {
  masterApp: {
    manifest: {
      getAppList: async () => {
        // get from remote api
        return [
          {
            name: 'Table',
            entry: 'http://localhost:8081',
            // activeWhen: '/table'
          },
          {
            name: 'Dashboard',
            entry: 'http://localhost:8082',
            // activeWhen: '/dashboard'
          },
        ];
      },
    },
  },
});

Renderer sub-app

There are two ways to load sub-app in micro frontend:

  1. **Sub-app component ** Get the components of each sub-app, and then you can render the sub-app of micro frontend just like using ordinary'React 'components.
  2. Centralized routing Through centralized routing configuration, the corresponding sub-app of rendering is automatically activated according to the current page pathname.

Sub-app component

Developers can use the useModuleApps method to obtain the components of each child application.

Through the combined use of the router component, developers can autonomously render different sub-applications according to different routes.

Suppose our subapp list is configured as follows:

modern.config.ts
import { appTools, defineConfig } from '@modern-js/app-tools';
import { garfishPlugin } from '@modern-js/plugin-garfish';

export default defineConfig({
  runtime: {
    router: true,
    state: true,
    masterApp: true,
  },
  plugins: [appTools(), garfishPlugin()],
});

App.tsx as follow:

App.tsx
import { useModuleApps } from '@modern-js/plugin-garfish/runtime';

import {
  RouterProvider,
  Route,
  createBrowserRouter,
  createRoutesFromElements,
  BrowserRouter,
  Link,
  Outlet,
} from '@modern-js/runtime/router';

const AppLayout = () => (
  <>
    <div>
      <Link to={'/table'}>load file-based sub-app</Link>
    </div>
    <div>
      <Link to={'/dashboard'}>load self-controlled sub-app</Link>
    </div>
    <div>
      <Link to={'/'}>unmount sub-app</Link>
    </div>
    <Outlet />
  </>
);

export default () => {
  const { apps, MApp } = useModuleApps();

  // Instead of using the MApp component, you need to use createBrowserRouter to create the route
  const router = createBrowserRouter(
    createRoutesFromElements(
      <Route path="/" element={<AppLayout />}>
        {apps.map(app => {
          const { Component } = app;
          // Fuzzy match, path needs to be written in a pattern similar to abc/*
          return (
            <Route
              key={app.name}
              path={`${app.name.toLowerCase()}/*`}
              element={
                <Component
                  loadable={{
                    loading: ({ pastDelay, error }: any) => {
                      if (error) {
                        return <div>error: {error?.message}</div>;
                      } else if (pastDelay) {
                        return <div>loading</div>;
                      } else {
                        return null;
                      }
                    },
                  }}
                />
              }
            />
          );
        })}
      </Route>,
    ),
  );

  return (
    // Use MApp to automatically load sub-applications according to the configured activeWhen parameters (this project is configured in modern.config.ts)
    // <BrowserRouter>
    //   <MApp />
    // </BrowserRouter>

    // Manually write the Route component to load the sub-application, which is convenient for scenarios that require pre-operation such as authentication
    <>
      <RouterProvider router={router} />
    </>
  );
};

Here, the activation route of Table is customized as /table through the Route component, and the activation route of Dashboard is /dashboard.

Centralized routing

Centralized Routing is a way to centrally configure active routes for subapps. We enable Centralized Routing by adding an activeWhen field to the subapp list information.

src/App.tsx
import { defineConfig } from '@modern-js/runtime';

defineConfig(App, {
  masterApp: {
    apps: [
      {
        name: 'DashBoard',
        entry: 'http://127.0.0.1:8081/',
      },
      {
        name: 'TableList',
        entry: 'http://localhost:8082',
      },
    ],
  },
});

Use useModuleApp api in Main App, get MApp component and then render it

Main App: App.tsx
import { useModuleApp } from '@modern-js/plugin-garfish/runtime';

function App() {
  const { MApp } = useModuleApps();

  return (
    <div>
      <MApp />
    </div>
  );
}

After starting the application in this way, accessing the '/table' route will render the'Table 'sub-application, and accessing the'/dashboard 'route will render the'Dashboard' sub-application.

Mix Mode

Of course, sub-application components and centralized routing can be mixed.

  • One molecular application is activated as a sub-application component, and the other part is activated as a centralized routing.
  • A molecular application can be activated either as a centralized routing or as a sub-application component.

Loading

By configuring the loadable configuration, loading components can be added for 「centralized routing」 and 「sub-applicati」, and errors, timeouts, flashes, etc. can be considered, so as to provide users with a better user experience. The design and implementation of this function refer to react-loadable, and the basic functions are similar.

function Loading() {
  return <div>Loading...</div>;
}

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
      }}
    />
  <>
}

Add Error Status

When the micro-frontend sub-application fails to load or render, the loading component will receive the error parameter (if there is no error, the error is null).

function Loading({ error }) {
  if (error) {
    return <div>Error msg {error?.message}</div>;
  } else {
    return <div>Loading...</div>;
  }
}

Avoid Loading Flash Back

Sometimes the display time of the loading component may be less than 200ms, and the loading component will flash back at this time. Many user studies have proved that the loading flash back situation will cause the user to perceive that the loading content takes longer than the actual time.

When loading is less than 200ms, if the content is not displayed, the user will think it is faster.

Therefore, the loading component also provides the pastDelay parameter, which will only be true when it exceeds the set delay display. You can set the delay duration through the delay parameter.

function Loading({ error, pastDelay }) {
  if (error) {
    return <div>Error! {error?.message}</div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

The default value of delay is 200ms, you can set the delay display time through delay in loadable.

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
        delay: 300 // 0.3 seconds
      }}
    />
  <>
}

Add Timeout State

Sometimes because of the network, the micro-front-end sub-application fails to load, resulting in the loading state being displayed all the time, which is very bad for users, because they don't know the right response to get a specific response, whether they need to refresh the page, by Increasing the timeout state can solve this problem well.

The loading component will get the timeOut parameter when timeout, when the micro frontend application loads timeout, it will get the timeOut property value of true.

function Loading({ error, timeOut, pastDelay }) {
  if (error) {
    return <div>Error! {error?.message}</div>;
  } else if (timeOut) {
    return <div>Loading timed out, please refresh the page... </div>;
  } else if (pastDelay) {
    return <div>Loading...</div>;
  } else {
    return null;
  }
}

The timeout state is off and can be enabled by setting the timeout parameter in loadable:

function App(){
  return <>
    <Table
      loadable={{
        loading: Loading,
        timeOut: 10000 // 10s
      }}
    />
  <>
}