Summary of micro front end x reconfiguration practice

preface

Hello, I'm a sea monster. Recently, I changed to a new Department to do content related to intelligent platform. The first task I received was to refactor the previous front-end project.

Refactoring is better than rewriting. Because the original project is ant design Vue + Vue family bucket, you need to switch to Ant Design + ant design pro + react family bucket.

What's more troublesome is that the product manager won't give us a lot of time to specialize in refactoring. We should do requirements while refactoring. Under such a challenge, I thought of the micro front-end solution. Let's share with you the implementation practice of the micro front-end in reconfiguration.

I simplified this practice and put it in Github On, you can play with clone by yourself.

Technology stack

First, let's talk about the technology stack. The old project mainly uses the following technologies:

  • frame
    • Vue
    • vuex
    • vue-router
  • style
    • scss
  • UI
    • ant-design-vue
    • ant-design-pro for vue
  • Scaffolding
    • vue-cli

The technologies required for the new project include:

  • frame
    • React
    • redux + redux-toolkit
    • react-router
  • of new style
    • less
  • UI
    • react-design-react
    • react-design-pro for react
  • Scaffolding
    • Self created scaffolding within the team

You can see that there is almost no intersection between the two projects except business.

Micro front end strategy

As the main application, the old project loads the pages in the new project (sub application) through qiankun.

  • When there is no requirement, rewrite the page in the new project (sub application). After rewriting, load the page of the new project in the old project (main application) and drop the page of the old project
  • When there is a demand, it is also to rewrite the surface of the new project (sub application) and then make the corresponding demand (take more time from the product). After rewriting, load the page of the new project in the old project (main application)

In this way, we can avoid the situation of "I want to refactor for a whole month", and we can migrate page by page slowly. Finally, after all pages are written in the new project, directly drop the old project, and the new project can stand out from behind the scenes. It is equivalent to that the old project has become a double since the first day of rewriting.

If you only look at the architecture diagram drawn above, you will think: ah, isn't it finished by introducing a qiankun? In fact, there are still many details and problems to pay attention to.

Upgraded architecture

One problem with the above architecture is that each time you click MenuItem in the sidebar, a sub page of the micro application will be loaded, that is:

The switching between micro application sub pages is actually the routing switching in the micro application. There is no need to reload the micro application to switch the micro application sub pages.

So I thought of a way: I put a component Container next to < router View >. After entering the main application, this component directly loads the whole micro application.

<a-layout>
  <!--  page    -->
  <a-layout-content>
    <!--   Subapplication container     -->
    <micro-app-container></micro-app-container>
    <!--   Main application routing     -->
    <router-view/>
  </a-layout-content>
</a-layout>

When displaying the old page, set the Container height to 0. When displaying the new page, open the Container height automatically.

// micro-app-container

<template>
<div class="container" :style="{ height: visible ? '100%' : 0 }">
  <div id="micro-app-container"></div>
</div>
</template>

<script>
import { registerMicroApps, start } from 'qiankun'

export default {
  name: "Container",
  props: {
    visible: {
      type: Boolean,
      defaultValue: false,
    }
  },
  mounted() {
    registerMicroApps([
      {
        name: 'microReactApp',
        entry: '//localhost:3000',
        container: '#micro-app-container',
        activeRule: '/#/micro-react-app',
      },
    ])
    start()
  },
}
</script>

In this way, when entering the old project, the Container will be automatically mounted and the sub application will be loaded. When switching A new page, it is essentially A route switching in the sub application, rather than switching from application A to application B.

Layout of subapplications

Since the pages in the new project (sub application) should be used by the old project (main application), the sub application should also have two sets of layouts:

The first set of standard management background layout includes side, Header and Content. When the other set of side is used as a sub application, only the layout of the Content part is displayed.

// Layout when running alone
export const StandaloneLayout: FC = () => {
  return (
    <AntLayout className={styles.layout}>
      <Sider/>
      <AntLayout>
        <Header />
        <Content />
      </AntLayout>
    </AntLayout>
  )
}

// Layout when applied as a child
export const MicroAppLayout = () => {
  return (
    <Content />
  )
}

Finally, through window__ POWERED_ BY_ QIANKUN__ You can switch between different layouts.

import { StandaloneLayout, MicroAppLayout } from "./components/Layout";

const Layout = window.__POWERED_BY_QIANKUN__ ? MicroAppLayout : StandaloneLayout;

function App() {
  return (
    <Layout/>
  );
}

Style conflict

qiankun enables JS isolation (sandbox) and closes CSS style isolation by default. Why do you do that? Because CSS isolation cannot be done without brains, let's talk about this problem.

qiankun provides two CSS isolation methods (sandbox): strict sandbox and experimental sandbox.

Strict sandbox

Opening code:

start({
  sandbox: {
    strictStyleIsolation: true,
  }
})

Strict sandbox mainly realizes CSS style isolation through ShadowDOM. The effect is that when sub applications are hung on ShadowDOM, the styles of main and sub applications are completely isolated and cannot affect each other. You said: isn't that good? No No No.

The advantages of this sandbox have also become its own disadvantages: in addition to the hard isolation of styles, DOM elements are also directly hard isolated, resulting in the loss of some Modal, Popover and Drawer components of sub applications because they can't find the body of the main application, and even run outside the whole screen.

Remember I just said that ant design is used for both main and sub applications? Modal and Popover of Ant Design
The implementation of Drawer is to hang it on the document With such isolation, they hang on the whole element and take off.

Experimental sandbox

Opening code:

start({
  sandbox: {
    experimentalStyleIsolation: true,
  }
})

This sandbox implementation method is to add suffix labels to the styles of sub applications, which is a bit like scoped in Vue. The styles are "soft isolated" by name, such as:

In fact, this method has done a good job of style isolation, but people often like to write in the main application! important to override the original style of ant design components:

.ant-xxx {
   color: white: !important;
}

And! Importnat has the highest priority if it is also used by micro applications Ant XXX class is easily affected by the style of the main application. Therefore, when loading micro applications, we also need to deal with the style conflict between ant design.

Ant design style conflict

Ant design provides a very good class name prefix function: prefixCls is used for style isolation. Naturally, I also use it:

// Custom prefix
const prefixCls = 'cmsAnt';

// Set Modal, Message, Notification rootPrefixCls
ConfigProvider.config({
  prefixCls,
})

// Render
function render(props: any) {
  const { container, state, commit, dispatch } = props;

  const value = { state, commit, dispatch };

  const root = (
    <ConfigProvider prefixCls={prefixCls}>
      <HashRouter basename={basename}>
        <MicroAppContext.Provider value={value}>
          <App />
        </MicroAppContext.Provider>
      </HashRouter>
    </ConfigProvider>
  );

  ReactDOM.render(root, container
    ? container.querySelector('#root')
    : document.querySelector('#root'));
}
@ant-prefix: cmsAnt; // Introduced to change the global variable value

But I don't know why. After changing the ant prefix variable in the less file, the style of Ant Design Pro is still the same. Some component styles have changed, and some have not changed.

Finally, I updated the global ant prefix less variable through modifyVars of less loader during packaging:

var webpackConfig = {
  test: /.(less)$/,
  use: [
    ...
    {
      loader: 'less-loader',
      options: {
        lessOptions: {
          modifyVars: {
            'ant-prefix': 'cmsAnt'
          },
          sourceMap: true,
          javascriptEnabled: true,
        }
      }
    }
  ]
}

Specific Issue Issue: Ant Design Pro does not take effect after changing prefixCls in ant design.

Master sub application status management

The old project (main application) uses vuex global state management, so sometimes the state in the main application needs to be changed in the new project page (sub application). Here I use qiankun's global state.

First, create globalActions in the Container, then listen to the vuex status change, notify the sub application of each change, and pass the vuex commit and dispatch functions to the sub application:

import {initGlobalState, registerMicroApps, start} from 'qiankun'

const globalActions = initGlobalState({
  state: {},
  commit: null,
  dispatch: null,
});

export default {
  name: "Container",
  props: {
    visible: {
      type: Boolean,
      defaultValue: false,
    }
  },
  mounted() {
    const { dispatch, commit, state } = this.$store;
    registerMicroApps([
      {
        name: 'microReactApp',
        entry: '//localhost:3000',
        container: '#micro-app-container',
        activeRule: '/#/micro-react-app',
        // The status, commit and dispatch of the main application are transmitted during initialization
        props: {
          state,
          dispatch,
          commit,
        }
      },
    ])
    
    start()
    
    // After the store of vuex is changed, the status, commit and dispatch of the main application are passed in again
    this.$store.watch((state) => {
      console.log('state', state);
      globalActions.setGlobalState({
        state,
        commit,
        dispatch
      });
    })
  },
}

The sub application receives the state, commit and dispatch functions from the main application, creates a new Context, and puts these things into the MicroAppContext. (because Redux does not support storing nonserializable values such as functions, it can only be stored in the Context first)

// Render
function render(props: any) {
  const { container, state, commit, dispatch } = props;

  const value = { state, commit, dispatch };

  const root = (
    <HashRouter basename={basename}>
      <MicroAppContext.Provider value={value}>
        <App />
      </MicroAppContext.Provider>
    </HashRouter>
  );

  ReactDOM.render(root, container
    ? container.querySelector('#root')
    : document.querySelector('#root'));
}

In this way, the sub application can also change the value of the main application through commit and dispatch.

const OrderList: FC = () => {
  const { state, commit } = useContext(MicroAppContext);

  return (
    <div>
      <h1 className="title">[[micro application] order list</h1>

      <div>
        <p>Primary application Counter: {state.counter}</p>
        <Button type="primary" onClick={() => commit('increment')}>[Micro application]+1</Button>
        <Button danger onClick={() => commit('decrement')}>[Micro application]-1</Button>
      </div>
    </div>
  )
}

Of course, this practice is also my own "invention". I don't know whether it is a good practice. I can only say that it can Work.

Global variable error

Another problem is that when the child application implicitly uses global variables, the import HTML entry will explode directly when executing JS. For example, micro applications have the following < script >:

var x = {}; // If an error is reported, it should be changed to window x = {};

x.a = 1 // If an error is reported, it should be changed to window x.a = 1;

function a() {} // To change to window a = () => {}

a() // If an error is reported, it should be changed to window a()

After the main application loads the micro application, the above x and a will all report xxx is undefined. This is because qiankun will execute this part of JS code when loading the micro application. At this time, the variable declared by var is no longer a global variable and other files cannot be obtained.

The solution is to use window XXX to explicitly define / use global variables. Specific visible Issue: sub application global variable undefined

When the primary application switches routes, the sub application routes are not updated

As long as both primary and secondary applications use Hash routing, there is a high probability that they will encounter this problem.

For example, your main application has two routes / micro app / home and / micro app / user, actvieRule is / # / micro app, and the sub application also has two corresponding routes / micro app / home and / micro app / user.

If you switch from / micro app / home to / micro app / user in the main application, you will find that the routing of the sub application has not changed. But if you switch in the sub application of the main application, you can switch successfully.

This is because the main application does not switch routes through location URL can trigger the hash change event to change the route, while the react router only listens to the hash change event. Therefore, when the main application switches the route, the hash change event is not triggered, so the sub application cannot listen to the route change and will not switch the page.

See the following for details: Issue: the sub application is loaded normally, but the main application switches the route, the sub application does not jump, and the browser returns to move forward to trigger the sub application jump.

The solution is simple. Choose one of the following three:

  • Replace the Link hyperlink mode in the vue main application with the native a tag, so as to trigger the hash change event of the browser
  • The main application manually listens for route changes and manually triggers the hash change event
  • Both the main application and the sub application use the browser history mode

Loading status

The main application still needs a lot of time to load sub applications, so it's best to show a loading state. qiankun just provides a loader callback to let us control the loading state of sub applications:

<div class="container" :style="{ height: visible ? '100%' : 0 }">
  <a-spin v-if="loading"></a-spin>
  <div id="micro-app-container"></div>
</div>
registerMicroApps([
  {
    name: 'microReactApp',
    entry: '//localhost:3000',
    container: '#micro-app-container',
    activeRule: '/#/micro-react-app',
    props: {
      state,
      dispatch,
      commit,
    },
    loader: (loading) => {
      this.loading = loading // Control loading status
    }
  },
])
start()

summary

In general, the micro front end is really helpful in deconstructing boulder applications. If we want to reconstruct the whole application, the Department will not suspend the business first and give the development a whole month for special reconstruction. You can only give you one or two more days when evaluating new requirements.

The micro front end can solve the problem of refactoring while doing new requirements in the process of refactoring, so that new and old pages can coexist without stopping the whole business at once.

Keywords: Javascript Front-end TypeScript

Added by chrisprse on Wed, 05 Jan 2022 12:06:27 +0200