preface

Recently, I have seen several articles related to the micro front end, which basically mentioned the IFrame mode for the micro front end architecture, but I did not see that article finally chose the IFrame architecture. In that case, I’ll try to use iframe to implement a micro front end.

Project introduction

Demo: notes.jindll.com/frame

Project address: github.com/luokaibin/m…

The project can be divided into two parts, one is the unified login and registration entrance, the other is the main management content area.

I plan is land registration section and the content of administrative header and value as a basic framework project (called the parent project) follow-up, content area (also forms part of the) according to the business to differentiate different sub-projects, then users access to different business, the parent project through iframe will load in different subprojects.

The expected goals

  • Divide a large project into smaller projects to reduce the maintenance complexity of the project
  • To maximize the decoupling of each business, when a business is iterative development, or multiple businesses are iterative development at the same time, to avoid mutual influence among businesses, code conflicts and other problems.
  • Incremental update, standalone deployment, fast rollback. When each business development or iteration is completed, the business can be deployed independently without involving other businesses. When a problem occurs, the system can be rolled back in time, avoiding other unpredictable problems.
  • Technology stack irrelevant. When a project becomes very large, it may not be appropriate to force the unification of the technology stack, and the goals and requirements of each specific business are different. It may be better to use other technology stacks to achieve the business.
  • Fast packaging, optimized loading speed. As a project becomes large, there will be more and more dependencies, but these dependencies may only be useful for a particular business, but these dependencies will eventually be packaged into the project, causing two problems: (1) the project packaging speed is slower and (2) the resulting packages are larger and browser loading speed is slower and slower.
  • .

Implementation approach

The project

The Demo plan is divided into one parent project and two sub-projects. Subprojects are divided according to business. Demo is also relatively simple because it is mainly used for demonstration, and it is not strictly divided according to business. The parent project includes login, registration, password retrieval, header and aside of management area. The subproject A is aside data management and home page. The subproject B is personal center and account management.

Parent-child project communication

Microfront-end architecture is bound to involve data sharing and business communication. Iframe as a micro front-end architecture, can make each project a high degree of independence, at the same time iframe can also be very good to prevent the JS and CSS of each project from influencing each other; At the same time, iframe is adopted as the micro front-end architecture, which means that all our projects do not need to be deployed under the same domain name. To put it more bluntly, we can introduce third-party products, expose a set of API to the outside world, enable the third party to develop based on this API, introduce third-party products, and third-party products can be deployed freely.

These are the advantages of IFrame, but at the same time, a high degree of independence, the introduction of third-party products, also causes inconvenient information sharing between projects, domain name and other causes, the parent project cannot operate the DOM of the child project, the child project cannot access the local storage of the parent project. Based on this, we need to introduce a new API postMessage. PostMessage is a new API introduced by H5. It can make scripts from different sources securely communicate across sources, and it can support information sharing and message passing between our parent and child projects.

Related problems and solutions

I. Main content area component division

Main content area is composed of four parts, the header component (corresponding to the file is/SRC/components/header. The vue), value components (corresponding to the file is/SRC/components/menu. The vue), And the breadcrumb navigation is composed of the welcome section corresponding to specific menu items; The four parts are assembled in/SRC /views/main.vue. The breadcrumbs are not individually packaged in main.vue. The welcome part is router-view.

Route management and father-son communication

First click different menus, “Welcome area” needs to display different pages, these different pages are divided into different projects according to business, these projects are packaged and deployed, finally load the online address of different projects through iframe.

The first step

So the first step is to know which menu the user clicked on, and the best way to do that is to listen for routes.

So we write a/SRC /views/template.vue component that listens for route changes.

<template>
  <div class="container">
    <iframe
      :src="src"
      name="content"
      seamless
    ></iframe>
  </div>
</template>

<script>
export default {
  data() {
    return {
      src: undefined,
    };
  },
	watch: {
    $route() {
      
    },
  },
}
</script>

Copy the code

We then make all menus that correspond to the route render component template.vue, that is, the parent project clicks on any menu to redirect to the Template page.

[{path: '/data/table'.name: 'Table'.component: template,
    meta: {
      name: 'Form Management'.title: 'Form Management'.icon: 'fa fa-table'.auth: true,}}, {path: '/data/chart'.name: 'Chart'.component: template,
    meta: {
      name: 'Chart Management'.title: 'Chart Management'.icon: 'fa fa-bar-chart'.auth: true,}},]Copy the code

In this way, we can also successfully implement the template to listen for route changes.

The second step

After listening for the route change, the second step is to know which subproject the route belongs to and what the project address is. Then assign the address to SRC and let iframe load the subproject.

So we can configure the address of the orgin subproject in the meta attribute of the route. It is also possible that the subproject may not be deployed at the root of the domain name, so it is best to provide a pathName project path so that the route can be written this way

/* router.js */
const itemA = {
  orgin:
    process.env.NODE_ENV === 'production'
      ? 'https://notes.jindll.com'
      : 'http://192.168.2.110:8128'.pathName: '/itemA/'};// Configure the addresses of different environments
export const menuRouter = [
  {
    path: '/'.component: main,
    redirect: '/index'.children: [{path: '/index'.name: 'Index'.component: template,
        meta: {
          ...itemA,
          name: 'home'.title: 'Welcome'.icon: 'fa fa-home'.auth: true,},},],},];Copy the code

After the template.vue listens for route changes, the subproject can be loaded via iframe

export default { data() { return { src: undefined, }; }, watch: { $route() { this.init(); }, }, methods: { init() { const { meta: { orgin, pathName } } = this.$route; this.src = `${orgin}${pathName}; }}},Copy the code

The third step

One problem is that when you go to the Template page for the first time, watch doesn’t listen for route changes. The mounted life cycle is executed by calling init() once. The mounted life cycle is executed by calling an onload method on iframe tags. Although SRC is undefined for the first time, onload will still fire. Here’s the second solution I chose.

The template. Vue changes

<template>
  <div class="container">
    <iframe
      :src="src"
      name="content"
      @load="postMsg"
      seamless
    ></iframe>
  </div>
</template>

<script>
export default {
  data() {
    return {
      src: undefined,
    };
  },
	watch: {
    $route() {
      this.init();
    },
  },
	methods: {
    postMsg() {
      this.init();
    },
		init() {
      const { meta: { orgin, pathName } } = this.$route;
      this.src = `${orgin}${pathName};
    },
	}
}
</script>

Copy the code

The fourth step

So far, although we successfully loaded the sub-project in, but loaded in is the home page of the sub-project, which is not what we need, so the first sub-project home page needs to be blank, the second to tell the page we want to show the sub-project, let the sub-project to jump; Another solution here is to form the corresponding sub-project address into SRC and let iframe load it. However, since the change of SRC will cause iframe to reload and the page will be blank, so the experience is not good, so WE give up decisively.

So the next step, when the route changes, we need to determine whether the project address has changed, if the project address has changed, directly modify SRC, reload another sub-project, if the project address has not changed, directly send a message to the sub-project, let the sub-project jump to the corresponding page.

The template. Vue changes

<template> <div class="container"> <iframe :src="src" name="content" @load="postMsg" seamless ></iframe> </div> </template> <script> export default { data() { return { src: undefined, }; }, watch: { $route() { this.init(); }, }, methods: { postMsg() { this.init(); }, init() { const { path, name, meta: { orgin, pathName }, } = this.$route; If (this. SRC = = = ` ${orgin} ${pathName} `) {/ / send subprojects message} else {this. SRC = ` ${orgin} ${pathName} `; } }, } } </script>Copy the code

Then the second step is to send messages to subprojects, postMessage for parent-child project communication tools, which is a new API in H5 that also supports cross-window and cross-domain communication.

To send a message to a subproject, you first need to get the subproject window object, so add another method to methods to get the subproject object

methods: {
  getIframe() {
    return window.frames['content']; }},Copy the code

Then write a method that sends a message. It needs to receive the message content, add a parameter to the message content, and then the subproject receives the message and decides that I want it, and I did send it.

methods: {
  getIframe() {
    return window.frames['content'];
  },
  sendmessage(msg) {
    const content = this.getIframe();
    msg.source = 'main';
    const {
      meta: { orgin },
    } = this.$route; content.postMessage(msg, orgin); }},Copy the code

The last message is sent, template.vue corresponding code

<template>
  <div class="container">
    <iframe
      :src="src"
      name="content"
      @load="postMsg"
      seamless
    ></iframe>
  </div>
</template>

<script>
export default {
  data() {
    return {
      src: undefined,
    };
  },
	watch: {
    $route() {
      this.init();
    },
  },
	methods: {
    getIframe() {
      return window.frames['content'];
    },
    sendmessage(msg) {
      const content = this.getIframe();
      msg.source = 'main';
      const {
        meta: { orgin },
      } = this.$route;
      content.postMessage(msg, orgin);
    },
    postMsg() {
      this.init();
    },
		init() {
      const {
        path,
        name,
        meta: { orgin, pathName },
      } = this.$route;
      if (this.src === `${orgin}${pathName}`) {
        this.sendmessage({ path, name });
      } else {
        this.src = `${orgin}${pathName}`;
      }
    },
	}
}
</script>

Copy the code

Step 5

When the parent project sends the message successfully, the child project should receive the message and jump. However, there is the previous problem. When the iframe loads the default address, which is the front page of the subproject, the page needs to be blank, which is the scenario we discussed at the beginning of step 4.

Make sure that the home page of the subproject is blank, i.e. there is no content on the home page, so the home page of the subproject is (itemA and itemB same)/SRC /views/layout.vue.

<template>
  <div class="fixedLayout">
    <router-view />
  </div>
</template>

<script>
export default {
};
</script>
Copy the code

Layout. Vue has a router-View embedded so that we can receive messages from the parent project in Layout.

<template> <div class="fixedLayout"> <router-view /> </div> </template> <script> export default { methods: ReceiveMsg ({data}) {// When receiveMsg is triggered, an event object will be sent. Event. Data is the message from the parent project. If (data.source! == 'main') { return; } // If the message is sent by the parent project, we will go to the name attribute in the message, // If your parent project path is different from that of the child project, you need to map it by yourself. Const {name} = data; this.$router.push({ name }); },}, mounted() {// When the message message is sent, ReceiveMsg method window.addEventListener('message', this. ReceiveMsg); }}; </script>Copy the code

Step 6

So far, we have solved the core problem of the whole project. During the project development process, that is, in the development environment, we directly load the subproject through iframe. In the production environment, we load the online address of the subproject. The code is actually the const itemA =…… in the parent project router-js Those, where we set up a different address based on the environment.

We then use postMessage to bridge parent-child communication, and because postMessage is part of the H5 standard, it doesn’t limit your stack. It can send messages between Vue React Angular front-end frameworks. Second, because it is part of the H5 standard, all browsers so far support the postMessage API.

Now that the core part is over, it’s time for the problems and solutions related to interaction.

A mask covers the entire screen

In the data Management == table management, we have a new button. When clicking the new button, a Dialog Dialog will pop up, in which we can enter data.

First of all, we need to make it clear that this dialog box that pops up needs to be a subproject; This dialog box input sub-project, on the one hand, business division is more clear, on the other hand, sub-project can get this data, related processing is more convenient. So the conclusion is that the code for this dialog box must be written in a subproject.

Normally, the child project is loaded by the parent project with an iframe, which is the size of the red box. That’s about the size of the dialog box mask.

So for the dialog mask to cover the full screen, the width of the iframe needs to be the same as the width of the screen (specifically, the browser viewable area); There are two ways to do this:

  1. When the user clicks the New button, it sends a message to the parent project, asking the parent project to change the width and height of the iframe. The child project then adds the padding to shrink the content area to the red box at the same time.
  2. The width of the iframe itself is the same as the width of the screen. The subproject adds padding to shrink the content area to the red box. In normal cases, the level of headers, aside, and breadcrumbs should be higher than the level of iframe, so that iframe does not block clicking on these areas; When the user clicks the New button and the dialog box appears, send a message to the parent project asking the parent project to increase the level of the IFrame so that the mask covers the full screen.

Here, I finally choose the second scheme, because when using the first scheme, the sudden change of the width and height of iframe will cause the content area to change the width and height in a flash, and the user experience is not good, so I finally choose the second scheme.

The first step

So the first step, I need to change the hierarchy of header, aside, bread crumbs. I gave these three levels 2, and the iframe level is 1 by default, so normally, these three will cover the iframe ahead.

Change the code/SRC /views/main.vue, here remove the code that is not important to this step.

<template> <el-container class="main_container"> <! -- S header --> <el-header class="main_header"> <MainHeader /> </el-header><! -- E header --> <el-container class="main_content"> <! -- S aside --> <el-aside class="main_aside"> <Menu /> </el-aside><! -- E aside --> <el-main class="main_primary"> <! <el-breadcrumb class="coutom_bread" separator="/"> <el-breadcrumb-item v-for="item of breadcrumbList" :key="item.path" :to="{ path: item.path }" >{{ item.name }}</el-breadcrumb-item > </el-breadcrumb><! -- E breadcrumb --> <router-view></ el-main> </el-container> </el-container> </template> <style lang="less" scoped> .main_header { z-index: 2; height: 80px ! important; } .main_aside { z-index: 2; width: 230px ! important; } .main_primary { .coutom_bread { z-index: 2; height: 50px; } } </style>Copy the code

The second step

The second step is to set the default level and the activation level for iframe and listen for messages sent by subprojects.

/ SRC /views/template.vue

<template> <div class="container"> <iframe :class="{ item: true, cover_item: isCoverIframe }" :src="src" frameborder="0" name="content" id="content" ref="content" @load="postMsg" seamless ></iframe> </div> </template> <script> export default {data() {return {isCoverIframe: false, // iframe full screen}; }, mounted() { window.addEventListener('message', this.receiveMsg); ReceiveMsg ({data}) {if (data.source! == 'content') { return; } const { action } = data; If (action === 'methodRun') {if (action === 'methodRun') {if (action === 'methodRun') {if (action === 'methodRun') {if (action === 'methodRun') {if (action === 'methodRun') { Const {funName, params = undefined} = data; this[funName](params); }}, // Set Iframe full screen setIframeCover() {this.iscoveriframe = true; }, // Cancel Iframe cancelIframeCover() {this.iscoveriframe = false; ,}}}; </script> <style lang="less" scoped> .item { position: fixed; z-index: 1; top: 0; bottom: 0; left: 0; right: 0; width: 100vw; height: 100vh; } .cover_item { z-index: 2; } </style>Copy the code

< mounted > < receiveMsg > < receiveMsg > < receiveMsg > < receiveMsg > If so, remove funName and Params, and see which method the child project calls, and what parameter is passed to this method. Finally, this [funName] (params); Call the corresponding method and pass in the parameters.

The setIframeCover and cancelIframeCover methods are used to add and cancel a class to an iframe, changing the hierarchy of the iframe.

The third step

/ SRC /views/layout.vue (itemB) {/ SRC /views/layout.vue

<template>
  <div class="fixedLayout">
    <router-view />
  </div>
</template>

<style lang="less" scoped>
.fixedLayout {
  box-sizing: border-box;
  padding-left: 230px;
  padding-top: 130px;
  width: 100vw;
  height: 100vh;
}
</style>

Copy the code

First of all, the padding value is fixed, because my parent project is an elastic box layout, and the height of header, aside, and breadcrumbs is fixed, so there is no problem with the padding of my sub-project being fixed.

The fourth step

Finally, the child project clicks the New button and sends a message to the parent project. Modify the code itemA project/SRC/views/tableManage vue

<script> export default { methods: { openDialog() { top.postMessage( { source: 'content', action: 'methodRun', funName: 'setIframeCover', }, this.topOrgin, ); ,}}}; </script>Copy the code

To send a message to the parent project, first we need to get the window object of the parent project. Since I have only one layer of nested iframe here, the parent project window object and the top-level window object are the same, so I take top, the message content action represents the corresponding action, funName represents the name of the method to call the parent project method;

This.toporgin is the parent project address, and this value is mixed in with the code in/SRC /utils/mixin.js, which is also context-sensitive.

Step 5

By now, the mask covering the full screen is also solved, but there are still some, after the data is added, the mask is removed, and the iframe level is modified; After the addition is complete, a success message is displayed on the page.

The rest of these, as with the full screen solution is the same idea, but still send a message; Displaying a success message requires passing an argument when calling the parent project method. The solution is almost the same.

Finally, solve the Dialog Dialog mask to cover the full screen is also finished.

4. Kill the scroll bar in iframe

For a variety of reasons you will find scrollbars in your iframe, but the parent project also has scrollbars. Unnecessary scrollbars can make the page feel messy, so we need to eliminate unnecessary scrollbars and make the page scroll properly.

Change code/SRC/app. vue, both subprojects have the same file path, and remove code that is irrelevant to this step.

<template> <div id="app"> <router-view /> </div> </template> <style lang="less"> #app { -ms-overflow-style: none; /* Scrollbar-width: none; #app::-webkit-scrollbar {display: none; } </style>Copy the code

These hidden scrollbar properties can be used anywhere, not just within an IFrame.

conclusion

By now, the core part of the whole project has been completed, the rest of the login function, login is actually local, the account and password input randomly, after clicking login, I saved a state in the Cookie, and then set the domain to the top-level domain, so that the sub-project can also access the Cookie;

The second table-managed data is Mock generated, and both the dialog-managed and form-managed forms are element-UI components, null-checked.

The graphs are managed using Ali’s G2Plot, and the data is also written dead.

The personal center and account management of itemB project were originally intended to be written, but they were found to be nothing more than forms. There were no new problems to be solved, so I didn’t write them.

In addition, this demo also stepped in a lot of holes, took a lot of detours, so let’s summarize.

postMessage

The first one uses postMessage to send A message. If you want to send A message to B from window A, you need to say bwindow.postmessage ().

iframe

Because the address of the subproject is different from that of the parent project, that is, the cross-domain problem is involved. When cross-domain is involved, both the parent project and the parent project are subject to cross-domain restrictions.

When I tried to print top and window.frames[‘content’], the console. Log failed to print top and window.frames. When I tried to print top and window.frames[‘content’], the console. Console. log was annotated by accident, and the message was sent directly to postMessage, but no error was reported.

In the mounted life cycle of the parent project, try to retrieve the child window object and send a message, but it is ok to click the button to send a message. If the parent project sends a message to the child project, make sure that the iframe has been loaded.

The dialog mask covers the full screen

At the beginning, I used method 1, but it turned out that the sudden change of the width and height of iframe would cause the change of page flash, which seriously affected the user experience. Therefore, I finally came up with method 2, which may leave some useless codes in the process of detour and tramping on holes in the project.

contrast

This part is not a pit, I plan to write a comparison with other micro front-end architecture solutions, but currently only IFrame is used to achieve the demo, the demo of other solutions has not started, so this part is empty first, to summarize the advantages and disadvantages of IFrame (it is not called advantages and disadvantages, mainly for the realization of the expected goal).

  • The first advantage is that the services are fully decoupled and the services are separated
  • The second advantage is that incremental update is fully realized. After that, other businesses carry out iterative operations. They only need to package and deploy their own business projects, and the impact of other services is zero.
  • The physical isolation of the third IFrame can fully realize the technology stack independence of each project.
  • Fourth, as for fast packaging, the project is smaller and smaller, the packaging speed will naturally improve.
  • The fifth advantage is that the hard isolation of iframe makes the JS and CSS of each project completely independent, without style contamination and variable conflicts
  • The sixth disadvantage is that the business is completely decoupled and the project is independent, so the common components between the businesses can not be shared, and each business can only be encapsulated again. But this is not a disadvantage, it can only be said that there is a gain and a loss, but generally speaking, the advantages outweigh the disadvantages

The deployment of online

At the beginning, the father-child project was intended to be deployed under different domain names, so as to demonstrate cross-domain. However, since the father-child project in the development environment was already cross-domain, and I was too lazy to assign domain name resolution, I finally deployed under different paths of the same domain name, which was a benefit, that is, the same origin. Parent and child projects can operate on the same Cookie, storage, and indexDB, facilitating local data sharing.

Remaining issues and optimization direction

Cancel message event listening

In the Mounted life cycle, both parent and child projects listen for message events via window.addEventListener, but this listener is tied to the window. It is best to disable message event listening when the page is destroyed using the removeEventListener API

Dynamically add Settings menu bar

At present, the route of the parent project is written to the input project, so if we add a new subproject or add new content in the sidebar, we need to modify the route of the parent project again, and the parent project also needs to issue a version again.

So this logic can be modified, in the add menu to manage the route management function, only open for developers, to implement the function to addRoutes, and then adjust the interface through the vue-router addRoutes API to achieve dynamic addRoutes.

Make templates for subprojects and parent projects

In the case of the same technology stack, for example, both the parent project and the subproject of demo are Vue, and the subproject have something in common, that is, the padding needs to be modified and reduced to the red box. In order to facilitate the opening of new projects, the parent project and the subproject can be made into a Vue template. When creating a project with VUE-CLI, directly pull the specified template. Save the process of modification and go straight out of the box.

Secondary encapsulation of postMessage

At present, our parent project and sub-project receive and send messages scattered in each page, without a unified method, which is not particularly good, so we can consider secondary encapsulation of postMessage, and adopt a unified method for sending and receiving messages on each page, which is convenient for future maintenance and modification.

Another option is to encapsulate postMessage in a synchronous form. For example, if a child project needs to fetch data from its parent project, it calls a method that returns the data directly, rather than sending messages to the parent project and receiving the message from the parent project.

Iframe no sense switching

This demo has two subprojects, you can obviously see that when you switch from one subproject to another, there will be two or three seconds of blank space in the content area, which is not a good experience, so we need to implement insensitive switching between subprojects to remove the page white space. The parent project template.vue contains multiple iframes wrapped with keep-alive, i.e. all the children are loaded in ahead of time, and then the orgin of the routing meta attribute determines which iframe to display. This will load all iframes in ahead of time, and route switching between subprojects will eliminate those two or three seconds of white space.

series

Nginx common configuration

Structural design of large front-end projects

Git management scheme for large front-end projects

How to significantly improve front-end loading performance

Installing nginx on Linux

How to implement a message prompt component (Vue) using JS calls