What is a micro front end

The micro front end is a kind of technical means and method strategy that multiple teams jointly build modern Web applications by releasing functions independently.

Micro-front-end refers to the architecture concept of micro-service, which separates a large front-end application into multiple independent and flexible small applications. Each application can be developed, run and deployed independently, and then these small applications can be combined into a complete application. The micro front end can not only integrate multiple projects into one, but also reduce the coupling between projects and improve project scalability. Compared with the whole front-end warehouse, the front-end warehouse under the micro front end architecture tends to be smaller and more flexible.

features

  • Technology stack independent The main framework does not limit the access application stack, the sub-application can choose the technology stack
  • Independent development/deployment Each team has its own warehouse, deployed independently, and does not depend on each other
  • Incremental upgrade When an application is large, it is difficult to upgrade or refactor the technology, while micro applications have the nature of incremental upgrade
  • Independent run time Microapplications are independent of each other in run time and have independent state management
  • Improving efficiency The larger the application, the harder it is to maintain, and the lower the collaboration efficiency. Microapplications can be easily broken down to improve efficiency

Microfront-end solutions currently available

There are currently several types of microfront-end solutions:

Based on theiframeComplete isolation

As a front-end developer, we are already familiar with iframe, the ability to run one application in isolation from another. It has significant advantages:

  1. It’s very simple and doesn’t require any modification
  2. Perfect isolation, JS, CSS are independent running environment
  3. Unlimited use, you can put more than one on the pageiframeTo combine the business

Of course, the disadvantages are also very prominent:

  1. Unable to maintain the routing state, the routing state is lost after refreshing
  2. Complete isolation makes interaction with child applications extremely difficult
  3. iframeThe popover in cannot break through itself
  4. The entire application is loaded with full resources, but the loading is slow

These significant shortcomings have also encouraged the creation of other schemes.

Based on thesingle-spaRoute hijacking scheme

Single-spa performs switching between sub-applications by hijacking routes, but the access method needs to fuse its own routes, which has certain limitations.

Qiankun was incubated from Ant Fintech’s unified access platform for cloud products based on micro front-end architecture. It encapsulates single-SPA. It mainly solves some pain points and shortcomings of single-SPA. The import-html-entry package parses THE HTML to obtain the resource path, and then parses and loads the resource.

By modifying the execution environment, it realizes the features of JS sandbox, style isolation and so on.

jingdongmicro-appplan

Jingdong Micro-App does not follow the idea of single-SPA, but draws on the idea of WebComponent. Through the combination of CustomElement and custom ShadowDom, the micro front end is encapsulated into a webComponents. Thus realize the component rendering of the micro front end.

inViteUsing a micro front end

When we moved from UmiJS to Vite, the micro front end became an imperative and many solutions were investigated.

Why doesn’t it workqiankun

Qiankun is currently the mainstream micro front end solution for the community. It’s perfect and popular, but the biggest problem is that it doesn’t support Vite. It used import-html-entry to parse HTML to obtain resources. Since Qiankun executed js content through Eval, and the script tag type in Vite was type=”module”, It contains module code such as import/export, so an error is reported: it is not allowed to use import in scripts other than type=”module”.

Taking a step back from the implementation, we used single-SPA and systemJS for micro front-end loading solutions, but also stepped on a lot of holes. Single-spa doesn’t have a friendly tutorial to tap into, and while there’s plenty of documentation, most of it deals with concepts that feel esoteric at the time.

Later, I looked at the source code and found that this is what… Most of the code in there revolves around routing hijacking, and there’s nothing document-like about it. And we can’t use it for routing hijacking, so why would we use it?

On a componentized level, single-SPA is not elegantly implemented.

  1. It hijacks the route withreact-routerIncompatible with the componentized mind
  2. Access mode a lot of complicated configuration
  3. Single-instance scenarios, in which only one child application is presented at a time

Later, considering the shortcomings of single-SPA, we could implement a componentized micro-front-end solution ourselves.

How to achieve a simple, transparent, componentized solution

Implementing a microapplication with componentized thinking is very simple: the child exports a method, the master app loads the child and calls the method, passing in an Element node argument, the child gets the Element node and puts its own component appendChild on the Element node.

Type conventions

To do this, we need to agree on an interaction between a master application and its children. Three hooks are used to ensure proper application execution, update, and uninstallation.

Type definition:

export interface AppConfig {
  / / a mountmount? :(props: unknown) = > void;
  / / updaterender? :(props: unknown) = > ReactNode | void;
  / / unloadingunmount? :() = > void;
}
Copy the code

Subapplication Export

By convention of type, we can export subapplications: mount, Render, unmount as primary hooks.

React sub-application implements:

export default (container: HTMLElement) => {
  let handleRender: (props: AppProps) = > void;

  // Wrap a new component for update processing
  function Main(props: AppProps) {
    const [state, setState] = React.useState(props);
    // Extract the setState method to the render function call, keeping the parent application triggering the update
    handleRender = setState;
    return <App {. state} / >;
  }

  return {
    mount(props: AppProps) {
      ReactDOM.render(<Main {. props} / >, container);
    },
    render(props: AppProps){ handleRender? .(props); },unmount(){ ReactDOM.unmountComponentAtNode(container); }}; };Copy the code

Vue sub-application implementation:

import { createApp } from 'vue';
import App from './App.vue';

export default (container: HTMLElement) => {
  / / create
  const app = createApp(App);
  return {
    mount() {
      / / loading
      app.mount(container);
    },
    unmount() {
      / / unloadingapp.unmount(); }}; };Copy the code

Main application implementation

Reactimplementation

The core code is only a dozen lines long and deals mainly with interaction with child applications (error-handling code is hidden for legibility) :

export function MicroApp({ entry, ... props }: MicroAppProps) {
  // Node to pass to the child application
  const containerRef = useRef<HTMLDivElement>(null);
  // Sub-application configuration
  const configRef = useRef<AppConfig>();

  useLayoutEffect(() = > {
    import(/* @vite-ignore */ entry).then((res) = > {
      // Pass div to child application render
      const config = res.default(containerRef.current);
      // Call the child application's load methodconfig.mount? .(props); configRef.current = config; });return () = > {
      // Call the unload method of the child applicationconfigRef.current? .unmount? . (); configRef.current =undefined;
    };
  }, [entry]);

  return <div ref={containerRef}>{configRef.current? .render? .(props)}</div>;
}
Copy the code

Complete. The loading, updating, and unloading of the master and child applications have been completed. Now it’s a component that can render multiple different child applications at the same time, which is much more elegant than single-SPA.

Entry sub application address, of course, the actual situation will give different address according to dev and PROd mode:

<MicroApp className="micro-app" entry="//localhost:3002/src/main.tsx" />
Copy the code

Vueimplementation

<script setup lang="ts">
import { onMounted, onUnmounted, ref } from 'vue';

const{ entry, ... props } = defineProps<{entry: string} > ();const container = ref<HTMLDivElement | null> (null);
const config = ref();

onMounted(() = > {
  const element = container.value;
  import(/* @vite-ignore */ entry).then((res) = > {
    // Pass div to child application render
    const config = res.default(element);
    // Call the child application's load methodconfig.mount? .(props); config.value = config; }); }); onUnmounted(() = > {
  // Call the unload method of the child applicationconfig.value? .unmount? . (); }); </script><template>
  <div ref="container"></div>
</template>
Copy the code

How to make the child application run independently

Many solutions, such as single-SPA, mount a variable to the window and determine whether the variable is in a micro-front-end environment, which is not elegant. In ESM, we can tell by passing in import.meta.url:

if (!import.meta.url.includes('microAppEnv')) {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>.document.getElementById('root')); }Copy the code

Import changes:

// Add environment parameters and the current time to avoid being cached
import(/* @vite-ignore */ `${entry}? microAppEnv&t=The ${Date.now()}`);
Copy the code

Browser compatibility

Internet Explorer has been phased out, and with Vite, we only need browsers that support import features. Of course, if you’re considering Internet Explorer, it’s easy: replace the import from the above code with system. import (systemjs), which is also the preferred use of single-spa.

The browser Chrome Edge Firefox Internet Explorer Safari
import 61 16 60 No 10.1
Dynamic import 63 79 67 No 11.1
import.meta 64 79 62 No 11.1

Module utilities

Must our child components use mount and unount modes? The answer is not necessarily, if our technology stacks are React. Our child application only needs to export a Render. Use the same React render. The advantage is that the child can consume the parent’s Provider. However, the React instance between two applications must be the same. Otherwise, an error will be reported.

React, react-dom, styled- Componets and other common modules can be pre-packaged into ESM modules and used in file services.

Change Vite configuration add alias:

defineConfig({
  resolve: {
    alias: {
      react: '//localhost:8000/[email protected]'.'react-dom': '//localhost:8000/[email protected]',}}});Copy the code

So you can happily use the same React code. You can also separate the common modules between the main app and its children, making the total app volume smaller. Of course, if not http2, you need to consider the granularity of the problem.

Online CDN solution: esM.sh

There is also the importMap solution, which is not very compatible, but the future is the trend:

<script type="importmap">
  {
    "imports": {
      "react": "//localhost:8000/[email protected]"}}</script>
Copy the code

Parent-child communication

Component micro applications, which can communicate by passing parameters, are exactly the React component communication model.

Resource path

In Vite dev mode, static resources in child applications are typically introduced like this:

import logo from './images/logo.svg';

<img src={logo} />;
Copy the code

The path of the image is /basename/ SRC /logo.svg, which will be 404 in the main application. Because the path only exists in the child application. We need to work with the URL module so that the path is preceded by the origin prefix:

const logoURL = new URL(logo, import.meta.url);

<img src={logoURL.href} />;
Copy the code

Of course, this is a bit tedious to use, so we can package it as a Vite plug-in to handle the scenario automatically.

Routing synchronization

The project uses the React-router, so it may have route asynchronism because it is not the same instance of the React-Router. That is, there is no linkage between routes.

The react-router supports custom history libraries, which we can create:

import { createBrowserHistory } from 'history';

export const history = createBrowserHistory();

// Main application: route entry
<HistoryRouter history={history}>{children}</HistoryRouter>;

// Master application: pass to child application
<Route
  path="/child-app/*"
  element={<MicroApp entry="//localhost:3002/src/main.tsx" history={history} />} / >;

// Sub application: route entry
<HistoryRouter basename="/child-app" history={history}>
  {children}
</HistoryRouter>;
Copy the code

Eventually the child application uses the same history module. Of course, this is not the only implementation, nor is it an elegant way to communicate with a child application by passing a route instance navigate.

Note: The child application’s basename must be the same as the path name of the main application. You also need to modify the base field of the Vite configuration:

export default defineConfig({
  base: '/child-app/'.server: {
    port: 3002,},plugins: [react()],
});
Copy the code

JS sandbox

Because sandboxes are not supported under ESM, there is no way to dynamically change the module Window object in the execution environment, and no way to inject new global objects.

React and Vue projects rarely modify global variables. Code specification checks are the most important.

CSS style isolation

Automatic CSS style isolation comes at a cost. In general, we recommend that sub-applications use different CSS prefixes and CSS Modules to basically achieve the requirements.

Packaged deployment

The deployment can be placed in different directories based on the base of the subapplication, and the names correspond. Configure nginx forwarding rules. We can use a uniform routing prefix for sub-applications so that Nginx can distinguish between primary applications and configure common rules.

For example, put the main app in the System directory and the child app in the directory starting with app- :

location ~ ^/app-.*(.. +)$ { root /usr/share/nginx/html; } location / { try_files$uri $uri/ /index.html;
    root /usr/share/nginx/html/system;
    index  index.html index.htm;
}
Copy the code

advantages

  • The simple core is less than 100 lines of code and requires no extra documentation
  • Flexible access by convention or incremental enhancement
  • Transparency No hijacking schemes, more logical transparency
  • Componentize Componentize rendering and parameter communication
  • Vite based ESM support for the future
  • Backward compatibility The SystemJS solution is optional and is compatible with browsers of earlier versions

Have a sample?

Example code in Github, interested friends can clone down to learn. Because our technology stack is React, the main application in this example is implemented using React.

  • Micro Front-end Components (React) : github.com/MinJieLiu/m…
  • Example microfront-end: github.com/MinJieLiu/m…

conclusion

Microfront-end solutions are best suited for team scenarios, and it is particularly important to create a solution that can be controlled by the team.

References:

  1. Developer.mozilla.org/zh-CN/docs/…
  2. Developer.mozilla.org/zh-CN/docs/…

Vite migration practices

We migrated from UmiJS to Vite

Click add React Group to communicate. Welcome to pay attention to the public number: front star