preface

Hello, I’m a sea monster. Recently, I moved to a new department, which is engaged in intelligent platform related content. My first assignment was to refactor the old front-end project.

Refactoring is more like rewriting. Because the original project was ant-design-vue + Vue family bucket, we need to switch to Ant-Design + Ant-design-Pro + React family bucket.

To make matters worse, product managers don’t give us a lot of time to refactor, we do requirements as we refactor. Under such challenges, I came up with the micro front end solution. Let’s share with you the implementation practice of the micro front end in the reconstruction.

I simplified this practice and put it on Github so that you can clone yourself.

Technology stack

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

  • The framework
    • Vue
    • vuex
    • vue-router
  • style
    • scss
  • UI
    • ant-design-vue
    • ant-design-pro for vue
  • The scaffold
    • vue-cli

The technologies needed for the new project are:

  • The framework
    • React
    • redux + redux-toolkit
    • react-router
  • new
    • less
  • UI
    • react-design-react
    • react-design-pro for react
  • The scaffold
    • The team created its own scaffolding

You can see that the two projects have little in common other than business.

Microfront-end strategy

The old project will be used as the main app, and the new project (sub-app) will be loaded via Qiankun.

  • When there are no requirements, rewrite the page in the new project (sub-application), and 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 are requirements, the new project (sub-application) rewrite the area to do the corresponding requirements (more time from the product), after rewriting, load the page of the new project in the old project (main application)

Instead of “I’m going to do refactoring for a whole month,” you can migrate page by page. Finally, after all the pages are written with the new project, the old project can be directly removed and the new project can come out from behind the scenes. From the first day of the rewrite, the old project becomes a stand-in.

If you just look at the architecture drawing above, you will think: Ah, why not introduce an Qiankun? There are actually a lot of details and problems that need attention.

Updated Architecture

One problem with the architecture above is that every time you click on the MenuItem in the sidebar, the subpage of the micro-app is loaded, i.e. :

The switch between micro-application sub-pages is actually the routing switch in micro-application. It is not necessary to do the switch between micro-application sub-pages by reloading a micro-application.

So, I came up with an idea: I put a component Container next to the

. Once in the main application, this component first loads the entire microapplication directly.

<a-layout>
  <! Page -- -- -- >
  <a-layout-content>
    <! -- Child application container -->
    <micro-app-container></micro-app-container>
    <! -- Primary application route -->
    <router-view/>
  </a-layout-content>
</a-layout>
Copy the code

Set the Container height to 0 when old pages are displayed, and automatically expand the Container height when new pages are displayed.

// 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>
Copy the code

This way, when entering the old project, the Container will automatically be mounted and the child application will be loaded. When you switch to A new page, you’re essentially switching routes within the child app, not from app A to app B.

The layout of the child application

Since the pages in the new project (child app) will be available to the old project (main app), the child app should also have two layouts:

The first set of standard admin background layouts, with Sider, Header and Content, and the second set of side as a child application, showing only the Content part of the layout.

// Layout when run alone
export const StandaloneLayout: FC = () = > {
  return (
    <AntLayout className={styles.layout}>
      <Sider/>
      <AntLayout>
        <Header />
        <Content />
      </AntLayout>
    </AntLayout>)}// Layout as a child application
export const MicroAppLayout = () = > {
  return (
    <Content />)}Copy the code

Finally, window.__powered_by_QIANkun__ can switch between different layouts.

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

const Layout = window.__POWERED_BY_QIANKUN__ ? MicroAppLayout : StandaloneLayout;

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

Conflict styles

Qiankun turns JS isolation (sandbox) on and CSS style isolation off by default. Why do you do that? Because CSS isolation can’t be done mindlessly, here’s how it works.

The Qiankun offers two types of CSS isolation (sandbox) : strict and experimental.

Strict sandbox

Open code:

start({
  sandbox: {
    strictStyleIsolation: true,}})Copy the code

Strict sandbox implements CSS style isolation mainly through ShadowDOM. The effect is that when the child application is hung on ShadowDOM, the styles of the master application are completely isolated and cannot affect each other. You say: Isn’t that great? No No No.

The advantages of this sandbox are its own disadvantages: in addition to hard isolation of styles, DOM elements are also directly hard isolated, causing some Modal, Popover, or Drawer components of child applications to be lost because the body of the main application cannot be found, or even out of the entire screen.

Remember when I said that both the main application and its children use Ant-Design? Ant-design implementations of Modal and Popover drawers are attached to document.body, so in isolation, they’re attached to the entire element.

Experimental sandbox

Open code:

start({
  sandbox: {
    experimentalStyleIsolation: true,}})Copy the code

This sandbox implementation suffixes the styles of child applications, a bit like Scoped in Vue, to “soft isolate” styles by name, like this:

In fact, this is a good way to do style isolation, but the main application often people like to write! Important to override the original ant-Design component style:

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

But!!!! Importnant has the highest priority, and if the micro-application also uses the.ant-xxx class, it is easily influenced by the style of the main application. So when loading microapplications, you also need to deal with style conflicts between Ant-Design.

Ant-design style conflicts

Ant-design provides a nice class name prefix feature: I use prefixCls for style isolation, which I naturally use:

// Customize the prefix
const prefixCls = 'cmsAnt';

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

/ / rendering
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'));
}
Copy the code
@ant-prefix: cmsAnt; // to change the value of a global variable
Copy the code

But for some reason, after changing the ant-prefix variable in the less file, the ant-Design-Pro style remains the same, with some component styles changed and some not.

Finally, I used modifyVars of less-Loader to update the global ant-prefix less variable at package time:

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

Ant-design-pro does not take effect after ant-Design changes prefixCls.

Master and child application status management

The old project (main application) used vuex globalState management, so the new project page (sub-application) sometimes needed to change the state of the main application. Here I used globalState in qiankun.

Create globalActions in the Container, listen for vuex state changes, notify the child application of each change, and pass vuex’s commit and Dispatch functions to the child 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 state of the host application and commit and dispatch are passed in during initialization
        props: {
          state,
          dispatch,
          commit,
        }
      },
    ])
    
    start()
    
    // Vuex store changes and then passes in the status of the main application again with commit and dispatch
    this.$store.watch((state) = > {
      console.log('state', state); globalActions.setGlobalState({ state, commit, dispatch }); }})},Copy the code

The child app receives the state, commit, and dispatch functions from the master app, while creating a new Context and putting them all in the MicroAppContext. (Redux does not support storing nonserializable values of functions, so only Context can be stored first)

/ / rendering
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'));
}

// Listen on globalState while mounting, just change it and render App again
export async function mount(props: any) {
  console.log('[micro-react-app] mount', props);
  props.onGlobalStateChange((state: any) = > {
    console.log('[micro-react-app] vuex Status Update ')
    render(state);
  })
  render(props);
}
Copy the code

As a result, the child application can also change the value of the master application with commit and Dispatch.

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

  return (
    <div>
      <h1 className="title">[Micro application] Order list</h1>

      <div>
        <p>Counter: {state.counter} for main application</p>
        <Button type="primary" onClick={()= >Commit ('increment')}> 1</Button>
        <Button danger onClick={()= >Commit ('decrement')}> retirement -1</Button>
      </div>
    </div>)}Copy the code

Of course, I invented this practice myself. I don’t know if it’s a good practice, but I can only say it works.

Global variable error

Another problem is that import-html-entry will explode when executing JS when a child application implicitly uses global variables. For example, microapplications have

var x = {}; X = {};

x.a = 1 A = 1; // window. X.a = 1;

function a() {} A = () => {}

a() // 报错,要改成 window.a()
Copy the code

After the microapplication was loaded by the main application, the x and A above would all say XXX is undefined, because the JS code would be executed when the microapplication was loaded by Qiankun. At this time, the variables declared by var were no longer global variables, and other files could not be obtained.

The solution is to explicitly define/use global variables using window. XXX. Visible Issue: Subapplication global variable undefined

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

If both the host and child applications use Hash routing, this problem is highly likely to occur.

/#/micro-app /user /micro app/home /user /micro app/user /micro app/home /micro app/user /micro app/home /micro app/user Sub-applications also have corresponding /micro-app/home and /micro-app/user routes.

So if you switch from /micro-app/home to /micro-app/user in the main app, you’ll see that the route of the child app hasn’t changed. But if you switch within a child of the main app, you can switch.

Url triggers hash change events to change routes. The React-router only listens for hash change events. Therefore, when the primary application switches routes, If the Hash change event is not triggered, the child application cannot listen to the route change and therefore does not switch pages.

Issue: The sub-application can be loaded normally, but the main application switches routes and the sub-application does not jump. When the browser returns to forward, the sub-application jumps.

The solution is simple: choose one of three:

  • Replace the Link hyperlink mode in the main vue application with the native A tag to trigger the browser hash change event
  • The main application manually listens for route changes and triggers the Hash change event
  • Use browser History mode for both main and child applications

Loading status

The main application still needs a lot of time to load the sub-application, so it is better to show the loading state.

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

conclusion

Overall, the micro front end is really helpful in the deconstruction of megalithic applications. In a situation like ours where you’re refactoring an entire application, you don’t want to shut down business and give development a whole month to refactor. You only need a day or two more to evaluate new requirements.

The micro front end can solve the problem of new requirements and reconstruction in the process of reconstruction, so that the new and old pages can coexist, and the whole business will not stop to do reconstruction work at a time.