头图

Micro front end solution

记得要微笑
中文

Preface

With the development of technology, the content carried by front-end applications is becoming more and more complex, and various problems arising from this have also arisen at the historic moment, from MPA ( Multi-Page Application , multi-page application) to SPA ( Single-Page Application , single-page application), although it has been solved The delay problem of the switching experience, but also brought about the long first loading time, and the huge stone application ( Monolithic ) caused by the explosive growth of the project; for MPA , its deployment is simple, and the applications are naturally hard-isolated, and have Technology stack has nothing to do with, independent development, independent deployment and other characteristics. If the characteristics of the two parties can be combined, will it bring a better user experience to users and development? At this point, after drawing on the concept of microservices, the micro front end came into being.

At present, the community has a lot of introductions about micro front-end architecture, but most of them stay at the stage of concept introduction. The paper will be on one specific type of scene, highlighting the micro front-end architecture can what value and technology practice decision-making process needs attention , supplemented by specific code, which can help you build a real sense A produces a micro front-end architecture system that can be used with

What is a micro front end?

Micro front-end is an architecture similar to micro-services. It applies the concept of micro-services to the browser side, that is, a single-page front-end application is transformed from a single monolithic application to an application that aggregates multiple small front-end applications into one. Each front-end application can also be developed and deployed independently.

The value of micro frontends

The micro front-end architecture has the following core values:

  • Technology stack has nothing to do with the main framework does not restrict access to the application technology stack, sub-applications have complete autonomy
  • Independent development and independent deployment The sub-application warehouse is independent, the front and back ends can be developed independently, and the main framework is automatically updated after the deployment is completed.
  • The state of each sub-application is isolated when running independently, and the running state is not shared

The micro-frontend architecture is designed to solve the problem that a single application has evolved from an ordinary application to a monolithic application ( Frontend Monolith ) in a relatively long time span due to the increase and changes of participating personnel and teams. The problem of unmaintainable applications. This type of problem is especially common in enterprise-level Web applications.

Solutions for mid- and back-end applications

Due to the characteristics of long application life cycle (every 3+ years) and other characteristics of middle and back-end applications, the probability of finally evolving into a boulder application is often higher than that of other types of web applications. This mainly brings two problems: technology stack lags behind , compilation and deployment slow From the perspective of technical implementation, micro front-end architecture solutions are roughly divided into the following types of scenarios:

  • Front-end containerization: iframe can effectively embed another webpage/single-page application into the current page. The CSS and JavaScript between the two pages are isolated from each other. iframe equivalent to creating a new independent hosting environment, similar to sandbox isolation, which means that front-end applications can run independently of each other. iframe an application platform, we will integrate third-party systems or systems under multiple departments and teams in the system, and use 060f963772c197 as a container to accommodate other front-end applications. Obviously, this is still a very reliable solution.
  • Micro components: With the help of Web Components technology, developers can create reusable custom elements to build cross-frame front-end applications. Web Components is used to encapsulate the sub-application, and the sub-application is more like a business component rather than an application. The real use of Web Components technology in the project is still a little far away from us now, but combining Web Components to build front-end applications is a future-oriented architecture.
  • Micro application: Through software engineering, in the deployment and construction environment, multiple independent applications are combined into a single application.
  • Micro module: Develop a new construction system, and construct part of the business functions into an independent chunk code, which only needs to be loaded remotely when using it.

Micro front-end architecture

The current micro front end mainly adopts a combined application routing scheme. The core of the scheme is the "master-slave" idea, which includes a base ( MainApp ) application and several micro ( MicroApp ) applications. Most of the base applications are a front end SPA project is mainly responsible for application registration, routing mapping, message distribution, etc., while micro-applications are independent front-end projects. These projects are not limited to the React,Vue,Angular or JQuery . Each micro-application is registered in the base application and managed by the base , But it can also be accessed separately if it is separated from the base. The basic process is shown in the following figure:

img When the entire micro front-end framework is running, the user experience is similar to the following figure:

img simple description of 160f963772c2f1 is that there are some menu items in the base application. Click each menu item to display the corresponding micro-applications. The switching of these applications is purely front-end SPA , and it is a good reference to the non-refresh feature of 060f963772c2ef.

Problems in the practice of micro front-end architecture

It can be found that the advantages of the micro front-end architecture are the combination of the advantages of MPA and SPA . That is, while ensuring that applications have independent development rights, they also have the ability to integrate them to ensure a complete product process experience.

Stitching layer acts as a scheduler, which decides to activate different sub-applications under different conditions. Therefore, the positioning of the main frame is only: navigation routing + resource loading frame .

To implement such a set of architecture, we need to solve the following technical problems:

关键技术实现

Routing system and Future State

In a product that implements a micro front-end kernel, when we normally access a page of a sub-application, there may be such a link:

img

At this time, the address of the browser may be https://app.alipay.com/subApp/123/detail . Imagine if we refresh the browser manually at this time, what will happen?

Since our sub-applications are all lazy load , when the browser is refreshed, the resources of the main frame will be reloaded. At the same time load are asynchronous. Because the routing system of the main application has been activated at this time, but the sub-application’s The resources may not be fully loaded yet, which leads to the fact that there is no /subApp/123/detail found in the routing registry. At this time, it will cause the NotFound page to jump or directly route an error.

This problem lazy load ways to load sub-applications. The angularjs community collectively called this problem Future State years ago.

The solution is also very simple. We need to design such a routing mechanism:

The main framework configures the route of the sub-application to subApp: { url: '/subApp/**', entry: './subApp.js' } . When the browser address is /subApp/abc , the framework needs to load the entry resource entry resource is loaded, make sure that the route system of the sub-application is registered in the main framework and then go to the sub-application. The routing system takes over the url change event. At the same time when the sub-application routing cut out, the main frame required to trigger the appropriate destroy event, sub-application when listening to the event, call your own uninstall uninstall applications, such as React scenario destroy = () => ReactDOM.unmountAtNode(container) .

To implement such a mechanism, we can go hijack url change event in order to achieve their own routing system can also be based on the existing community ui router library , especially react-router in v4 the realization of the Dynamic Routing ability, we just need to rewrite part of the route The logic found is enough. Here we recommend directly choosing the relatively complete related practice in the community single-spa .

App Entry

After solving the routing problem, the way of integrating the main framework and sub-applications will also become a technical decision that needs to be focused on.

Sub-application loading method

Monorepo

The easiest way to use single-spa is to have a repository that contains all the code. Usually, you only have one package.json and one webpack configuration, and generate a package, which is referenced by the ``tag html

NPM package

Create a parent application, npm installs each single-spa application. Each sub-application is in a separate code repository and is responsible for releasing a new version every time it is updated. When the single-spa application changes, the root application should be reinstalled, rebuilt, and redeployed.

Dynamically load modules

The sub-application is built and packaged by itself, and the sub-application resources are dynamically loaded when the main application is running.

ProgramadvantageDisadvantage
Monorepo1. The easiest to deploy
2. Single version control
1. For each individual project, a Webpack configuration and package.json means insufficient flexibility and freedom.
2. When your project gets bigger and bigger, the packaging speed becomes slower and slower.
3. The build and deployment are all bundled together, which requires a fixed release plan instead of temporary release
NPM package1. npm installation is more familiar with development and easy to build
2. The independent npm package means that each application can be packaged separately before npm
1. The parent application must reinstall the child application to rebuild or deploy
Dynamically load modulesThe main application and sub-applications are completely decoupled, and the sub-applications can use any technology stackThere will be more runtime complexity and overhead

Obviously, in order to achieve the two core goals of real technology stack has nothing to do with independent deployment, in most scenarios we need to use the solution of loading sub-applications at runtime.

JS Entry vs HTML Entry

After determining the runtime loading plan, another point that needs to be decided is, what kind of resources do we need to provide from the sub-application as the rendering entrance?

JS Entry is usually that the sub-application classifies the resource as a entry script , such as the single-spa in example . However, this solution has many limitations. For example, all resources of the sub-application are required to be packaged into a js bundle , including css , pictures and other resources. In addition to the problem that the typed package may be bulky, the features such as parallel loading of resources cannot be used.

HTML Entry is more flexible, directly typing out the sub-application HTML as the entrance, the main framework can obtain the static resources of the sub-application fetch html HTML document as the child node into the container of the main framework. In this way, not only can the access cost of the main application be greatly reduced, the development method and packaging method of the sub-applications basically do not need to be adjusted, and the problem of style isolation between sub-applications can be naturally solved (mentioned later). Imagine such a scenario:

<!-- 子应用 index.html -->
<script src="//unpkg/antd.min.js"></script>
<body>
  <main id="root"></main>
</body>
// 子应用入口
ReactDOM.render(<App/>, document.getElementById('root'))

If it is the JS Entry solution, the main framework needs to build the corresponding container node (such as the "#root" node here) before the sub-application is loaded, otherwise the sub-application will report an error container The problem is that the main application does not guarantee that the container node used by the sub-application is a specific tag element. The HTML Entry solution can naturally solve this problem and retain the complete environment context of the sub-application, thereby ensuring a good development experience for the sub-application.

HTML Entry scheme, the way the main frame registers the sub-applications becomes:

framework.registerApp('subApp1', { entry: '//abc.alipay.com/index.html'})

In essence here HTML act as the role of the application of static resource table, in some scenarios, we can also HTML Entry optimal solutions to Config Entry , thereby reducing a request, such as:

framework.registerApp('subApp1', { html: '', scripts: ['//abc.alipay.com/index.js'], css: ['//abc.alipay.com/index.css']})

in conclusion:

img

Get a list of sub-application resources

html parsing

The sub-applications to break out HTML as the entrance, qiankun will go through fetch get to the sub-application html string (This is why the child application resources permit cross-domain), get html after the string, it calls processTpl method by a to match a regular piles acquire html corresponding js (inline, outreach), css (inline, outreach), annotation, entry script entry like.

asset-manifest.json

Build configuration using stats-webpack-plugin plug-in to generate a resource list manifest.json file with create-react-app built react project webpack use the default webpack-manifest-plugin generate a list of resources. Then the main application ajax asynchronously requests manifest.json

img

webpack-manifest-plugin plug-in generated resource list asset-manifest.json

{
  "files": {
    "main.js": "/index.js",
    "main.js.map": "/index.js.map",
    "static/js/1.97da22d3.chunk.js": "/static/js/1.97da22d3.chunk.js",
    "static/js/1.97da22d3.chunk.js.map": "/static/js/1.97da22d3.chunk.js.map",
    "static/css/2.8e475c3e.chunk.css": "/static/css/2.8e475c3e.chunk.css",
    "static/js/2.67d7628e.chunk.js": "/static/js/2.67d7628e.chunk.js",
    "static/css/2.8e475c3e.chunk.css.map": "/static/css/2.8e475c3e.chunk.css.map",
    "static/js/2.67d7628e.chunk.js.map": "/static/js/2.67d7628e.chunk.js.map",
    "static/css/3.5b52ba8f.chunk.css": "/static/css/3.5b52ba8f.chunk.css",
    "static/js/3.0e198e04.chunk.js": "/static/js/3.0e198e04.chunk.js",
    "static/css/3.5b52ba8f.chunk.css.map": "/static/css/3.5b52ba8f.chunk.css.map",
    "static/js/3.0e198e04.chunk.js.map": "/static/js/3.0e198e04.chunk.js.map",
    "index.html": "/index.html"
  },
  "entrypoints": [
    "index.js"
  ]
}

Generate importmap during deployment

importmap.json (configuration file) on gitlab or the file storage server, and write the project-entry file mapping relationship to importmap.json

// importmap.json
{
  imports: {
    'icec-cloud-inquiry-mall-react': 'http://localhost:8234/index.js',
    'icec-cloud-product-seller-react': 'http://localhost:8235/index.js'
  }
}

Dynamic loading of JS modules

Under the micro front-end architecture, we need to obtain some hook references exposed by the sub-applications, such as bootstrap , mount , unmount etc. (refer to single-spa ), so as to have a complete lifecycle control of the access application. However, since sub-applications usually have the requirement of supporting both integrated deployment and independent deployment modes, we can only choose umd a compatible module format to package our sub-applications. How to obtain the module reference exported in the remote script while the browser is running is also a problem that needs to be solved.

Usually our first response is , which is to agree on a global variable between the sub-application and the main frame, mount the exported hook reference to this global variable, and then the main application takes life from it. Periodic function .

This scheme is very useful, but the biggest problem is that there is a strong agreement between the . Can we find a loosely coupled solution?

The current serious micro-front-end solutions are mainly of two types:

  • One is the technology stack-independent type between projects represented qiankun
  • The other is the unified technology stack between projects represented by Meituan Takeaway.

For technology stack-independent type, dynamic loading of sub-projects is mainly to let the sub-projects render the content to a certain DOM node. Therefore, the purpose of dynamic loading is mainly to execute the code of sub-projects, and in addition, it is necessary to get some of the sub-project declarations. Life cycle hooks; the goal of the unified technology stack is to directly get the components and other content output by the sub-projects, and dynamically embed them into the main project to complete the analysis.

SystemJS

To get the child application to build a resource list asset-manifest.json later, into importmap dynamic injection html (in SystemJS@6.4.0 is supported), and then use System.import load

Byte bounce: new Function()

Let’s take a look at the implementation of byte beating first. Their solution is: “The sub-modules ( Modules CMD packages one by one, and I use new Function to wrap them.” In two simple sentences, this should mean using fetch or other direct library as a request to get the contents of the sub-project cmd packet, then new Function the submodule as function function body is performed, incoming custom define parameters to perform get module and output module. new Function is as follows, the content of the submodule is used as the function text, eval is similar to 060f963772cd7a, but it will be clearer to use:

let sum = new Function('a', 'b', 'return a + b');

alert( sum(1, 2) ); // 3

But here you could define requirejs define globally, and then use the script tag to directly reference the sub-project to load and execute naturally. Why use fetch + new Function ? It may be because the global define method is not convenient to use dynamically inside the component method.

Ant Financial: eval()

qiankun 动态加载

Then look at the typical technology stack gold ant-independent program service qiankun implementation, related code in its use import-html-entry warehouse , again by fetch like database request, but get as umd sub-packet The project content does not use the submodule as amd or cmd , but directly eval and mounts window . eval executed, the bound window object was changed. This is mainly through the Proxy interception sub-project to window global variables and do custom isolation processing.

qiankun recommended sub-application webpack configuration:

const packageName = require('./package.json').name;

module.exports = {
  output: {
    library: `${packageName}-[name]`,
    libraryTarget: 'umd',
    jsonpFunction: `webpackJsonp_${packageName}`,
  },
};

Meituan Takeaway: jsonp

Next, let’s take a look at the dynamic loading method of the module in the unified Meituan takeaway program of the technology stack. Their program introduction did not loadAsyncSubapp on the module loading method, but from the posted code, you can see 060f963772d0c6 and subappRoutes . Mentioned to trigger the jsonp hook window.wmadSubapp , which means that their solution is implemented jsonp You can set the webpack of the output of the libraryTarget to jsonp , so that the configured packaged product will execute the global jsonp method when it is loaded, passing in the export value of the main module as a parameter. Refer to webpack document other-targets . Pro-test is feasible:

webpack config

output: {
  library: `registerSubApp`,
  libraryTarget: 'jsonp',
}

Main module

export default App

webpack 产物 webpack product

Parent project

window.registerSubApp = function (lib) {
  ReactDOM.render(
    React.createElement(lib.default),
    document.getElementById('root')
  )
}
// lib = {default: App}

In this way, the parent project can directly get the components of the sub-project, and then the components can be dynamically integrated into the main project. You can refer to the dynamic routing analysis react-router introduced in their article.

Webpack module federation

Since the main packaging scheme of the current front-end project is webpack , many dynamic loading schemes of the micro-front end need webpack . Later, people naturally thought of making webpack better and more convenient to support the mutual loading of build products between different projects. This is webpack module federation , the usage method may be:

new ModuleFederationPlugin({
    name: 'app_two',
    library: { type: 'global', name: 'app_a' },
    remotes: {
      app_one: 'app_one',
      app_three: 'app_three'
    },
    exposes: {
       AppContainer: './src/App'
    },
    shared: ['react', 'react-dom', 'relay-runtime']
})
----
import('app_one/AppContainer')

In general, JS module can be divided into two stages, the loading stage and the execution stage. According to the loading mode, there are two modes: script tag loading and fetch In the implementation phase, script label loaded with general global jsonp function to do analytic, what specific functions do you see the way the agreement, such as requirejs in define methods and webpack when loading dynamic webpackJsonpCallback . fetch request method, you need to use the eval or new Function method for package control analysis.

Both methods can basically achieve the effect. The loading method of the former method is simpler, and the control of the analysis in the latter method can be more refined.

img

Application sandbox

There are two very critical issues in the micro-front-end architecture solution. Whether or not these two issues are resolved will directly indicate whether your solution is really available for production. It's a pity that the community will always choose a "bypass" approach when dealing with this issue before, such as avoiding conflicts through some default agreements between the main and sub-applications. Today, we will try to solve possible conflicts between applications more intelligently from a purely technical perspective.

Throughout various technical solutions, there is a major premise that determines how this sandbox is done: the final micro-application is a single instance or multiple instances in the host application. This directly determines the complexity and technical solution of the sandbox.

  • Single instance: Only one micro application instance exists at the same time. At this moment, all browser resources of the browser are exclusively owned by this application. The solution to a large extent is to clean up and restore on-site when the application is switched. It is relatively lightweight and simple to implement.
  • Multi-instance: The resource is not exclusive to the application, it is necessary to solve the resource sharing situation, such as routing, style, global variable reading and writing, DOM. There may be many situations that need to be considered, and the implementation is more complicated.

image.png

JS isolation

ensure that the global variables between each sub-application will not interfere with each other, so as to ensure the soft isolation between each sub-application?

This problem is more difficult than the problem of style isolation. The common gameplay in the community is to add various prefixes to some global side effects to avoid conflicts. But in fact, we all understand that this method of "verbal" agreement between teams is often inefficient and fragile. All solutions that rely on human constraints can hardly avoid online bug caused by human negligence. So is it possible for us to create a useful and completely unconstrained JS isolation solution?

Environment snapshot

In JS isolation problem of 060f963772d53c, we created a run-time sandbox JS Simply drew an architecture diagram:

img

That is, take a snapshot of the global state before the start of the two life cycles of the bootstrap and mount bootstrap when the application is cut out/uninstalled to ensure that the application's pollution to the global state is cleared. . When the application is re-entering the secondary restored to mount state before, ensuring that applications remount have now for the first time when mount when consistent global context.

Snapshots can be understood as the temporary mount before window objects, mount later will produce a new window object, when umount back and back to the staging of window objects

Of course, the sandbox is far more than that. Others include hijacking of global event monitoring, etc., to ensure that after the application is cut out, the global event monitoring can be completely uninstalled, and it will also be at remount . Re-monitor these global events to simulate a sandbox environment consistent with when the application is running independently.

When the script of the main application is initially loaded, certain events may be monitored globally, such as window.resize . After umount and back to the temporarily stored window object, it needs to be monitored again.

Sample code:

class DiffSandbox {
  constructor(name) {
    this.name = name;
    this.modifyMap = {}; // 存放修改的属性
    this.windowSnapshot = {};
  }

  active() {
    // 缓存active状态的沙箱
    this.windowSnapshot = {};
    for (const item in window) {
      this.windowSnapshot[item] = window[item];
    }
    Object.keys(this.modifyMap).forEach(p => {
      window[p] = this.modifyMap[p];
    })
  }

  inactive() {
    for (const item in window) {
      if (this.windowSnapshot[item] !== window[item]) {
        // 记录变更
        this.modifyMap[item] = window[item];
        // 还原window
        window[item] = this.windowSnapshot[item];
      }
    }
  }
}

const diffSandbox = new DiffSandbox('diff沙箱');
diffSandbox.active();  // 激活沙箱
window.a = '1'
console.log('开启沙箱:',window.a);
diffSandbox.inactive(); //失活沙箱
console.log('失活沙箱:', window.a);
diffSandbox.active();   // 重新激活
console.log('再次激活', window.a);

This method also cannot support multi-instance, because all attributes are stored on window during operation.

In a single instance scenario, if the properties and methods injected by the sub-application into the browser's native objects do not conflict with the original properties and methods in the main application, then it is feasible

Simulate sandbox environment (proxy+closure)

To achieve a sandbox, we need to isolate the browser's native objects, but how to isolate and establish a sandbox environment? Node is a vm module in 060f963772d7fe to achieve similar capabilities, but browsers can’t, but we can use closure capabilities and variable scope to simulate a sandbox environment, such as the following code:

function foo(window) {
  console.log(window.document);
}
foo({
    document: {};
});

For example, output of this code must be {} , rather than the native browser document .

Therefore, ConsoleOS (a micro-front-end solution for Wepback plug-in. When the application code is built, a layer of wrap code is added to the sub-application code, and a closure is created to isolate the browsers that need to be isolated. The native object becomes obtained from the function closure below, so that we can pass in objects such as simulated window and document when the application is loaded.

// 打包代码
__CONSOLE_OS_GLOBAL_HOOK__(id, function (require, module, exports, {window, document, location, history}) {
  /* 打包代码 */
})
function __CONSOLE_OS_GLOBAL_HOOK__(id, entry) {
  entry(require, module, exports, {window, document, location, history})
}

Of course, it can also be achieved without engineering means, or by requesting a script, then splicing this code at runtime, and then eval or new Function to achieve the same goal.

With the sandbox isolation capability, the remaining problem is how to implement this bunch of browser native objects. The initial idea was that we ECMA specification (there are still similar ideas), but found that the cost is too high. However, after our various experiments, we found a very tricky approach. We can take the new iframe object and take out the native browser object inside through contentWindow . Because these objects are naturally isolated, it saves the cost of their own implementation.

const iframe = document.createElement( 'iframe', {src:'about:blank'});
document.body.appendChild(iframe);
const sandboxGlobal = iframe.contentWindow;

Of course, there are many details that need to be considered. For example, only iframe same domain can retrieve the corresponding contentWindow . Therefore, it is necessary to provide an empty same domain URL host application as the iframe initially loaded by URL . Of course, according to HTML specifications, this URL with the about:blank necessarily guarantee the same domain, resource loading will not happen, but it will happen and this iframe the associated history can not be operated, this time routing of transformation can only become hash mode.

As shown in the figure below, in addition to an isolated window environment, the micro front end actually needs to share some global objects. At this time, we can use a proxy to achieve this. Proxy can intercept the reading of the target object, function calls and other operations, and then Perform operation processing. It does not directly manipulate the object, but is like a proxy mode, which operates through the proxy object of the object. When performing these operations, you can add some additional operations that you need. We removed the corresponding iframe after object natively, requiring isolation of a specific object generation corresponding Proxy , then access and property attributes, do some specific settings, such as for the document can be loaded in the same DOM tree, for document , most DOM host browser properties and method of operation, or directly in document properties and methods.

class SandboxWindow {
    constructor(context, frameWindow) {
        return new Proxy(frameWindow, {
          set(target, name, value) {
              if (name in context) { // 优先使用共享对象
                  return context[name] = value;
              }
              target[name] = value;
              return target[name];
          },

          get(target, name) {
              if (name in context) { // 优先使用共享对象
                  return context[name];
              }

              if(typeof target[ name ] === 'function' && /^[a-z]/.test( name )) {
                  return target[ name ].bind && target[ name ].bind( target );
              } 
              
              return target[name];
          }
        });
    }
}

// 需要全局共享的变量
const context = { document:window.document, history: window.history }

// 创建沙箱
const newSandboxWindow = new SandboxWindow(context, sandboxGlobal);

// 执行chunk时注入沙箱环境
((window) => {new Function('chunk code')})(newSandboxWindow);

Since the sub-application has its own sandbox environment, all previously exclusive resources are now exclusive to the application (especially location and history ), so the sub-application can also be loaded at the same time. And for some variables, we can also proxy to restrict the ability of sub-applications, such as Cookie , LocalStoage read and write.

When the iframe is removed, the variables window timeout time settings will also be removed (of course, the DOM event needs to be recorded in the sandbox and then removed from the host).
To sum up, our sandbox can achieve the following characteristics:

image.png

If you understand the introduction to the principle above, you can see that the sandbox implementation includes two levels:

  • Simulation of native browser objects ( Browser-VM )
  • How to build a closure environment

Browser-VM can be used directly, this part is completely universal. But when it comes to this part of closure construction, each micro front-end system is not consistent and may need to be modified, such as:

import { createContext, removeContext } from '@alicloud/console-os-browser-vm';
const context = await createContext();
const run = window.eval(`
  (() => function({window, history, locaiton, document}) {
    window.test = 1;
  })()
`)
run(context);
console.log(context.window.test);
console.log(window.test);
// 操作虚拟化浏览器对象
context.history.pushState(null, null, '/test');
context.locaiton.hash = 'foo'
// 销毁一个 context
await removeContext( context );

Of course, you can directly choose the evalScripts method provided by the sandbox:

import { evalScripts } from '@alicloud/console-os-browser-vm';
const context = evalScripts('window.test = 1;')
console.log(window.test === undefined) // true

Style isolation

Since in the micro front-end scenario, sub-applications of different technology stacks will be integrated into the same runtime, we must ensure at the framework layer that there is no problem of style interference between each sub-application.

Shadow DOM?

"Isolated Styles" the problem of 060f963772e13d, if browser compatibility is not considered, usually the first solution that Web Components to our mind will be 060f963772e141. Based Web Components of Shadow DOM ability, we can each sub-application package to a Shadow DOM in absolute isolation to ensure style of its operation.

But the Shadow DOM solution will encounter a common problem in engineering practice. For example, we built a sub-application rendered Shadow DOM

const shadow = document.querySelector('#hostElement').attachShadow({mode: 'open'});
shadow.innerHTML = '<sub-app>Here is some new text</sub-app><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css">';

Since sub-application is only under the shadow element, once there is a scene in the sub-application that runs out of bounds and runs outside to build DOM , it will inevitably lead to the situation that the constructed DOM cannot apply the style of the sub-application .

For example sub-app was called antd modal components due modal is dynamically mounted to document.body , and because Shadow DOM characteristics antd style will only shadow entry into force of this scope, the result is a pop-up box can not be applied to antd style. The solution is to antd style to the main document, but doing so means that the style of the sub-application is directly leaked to the main document. gg...

CSS Module or CSS Namespace?

usual practice of the css prefix 060f963772e2b5, that is, each sub-application uses a specific prefix to name class , or write the style directly based on the css module scheme. For a brand-new project, this is of course feasible, but usually the more goal of micro-front-end architecture is to solve the access problem of stock/legacy applications. Obviously, it is often difficult for legacy applications to have the motivation to make substantial transformations.

The most important thing is that agreed method. If a three-party component library is used in the sub-application, the three-party library does not support customized prefixes while writing a large number of global styles? For example, the a application introduces antd 2.x , and the b application introduces antd 3.x . The two versions of antd are written into the global .menu class , but what if they are not compatible with each other?

Combining CSS Module and CSS Namespace can handle the above problems

Dynamic Stylesheet !

Solving the solution is actually very simple. We only need to uninstall the style sheet at the same time after the application is cut out/uninstalled. The principle is that the browser will rebuild the CSSOM So as to achieve the purpose of inserting and unloading style . This ensures that only one applied style sheet is effective at a point in time.

HTML Entry solution mentioned above has inherent style isolation characteristics, because after the application is uninstalled, the HTML structure will be directly removed, thereby automatically removing its style sheet.

For example, in HTML Entry DOM after loading the sub-application may look like this:

<html>
  <body>
    <main id="subApp">
      // 子应用完整的 html 结构
      <link rel="stylesheet" href="//alipay.com/subapp.css">
      <div id="root">....</div>
    </main>
  </body>
</html>

When the child is replaced or uninstall the application, subApp node innerHTML will be overwritten, //alipay.com/subapp.css will naturally be removed style also will be uninstalled.

limitation of 160f963772e4ed is the possibility of style conflicts between the site framework itself or its components ( header/menu/footer ) and the currently running micro-applications, and the second is that there is no way to support multiple micro-applications running and displaying at the same time.

qiankun VS single-spa

The problem described above is caused by the qiankun framework when the ants landed. qiankun is developed based on single-spa . What is the difference between it and single-spa

qiankun

From the perspective of Qiankun, the micro front end is the "micro application loader". It mainly solves the problem of how to secure , quickly multiple scattered projects, which can be seen from the point of Qiankun itself. Out:

img

All these features serve the positioning of "micro-application loader".

single-spa

From the perspective of single-spa, the micro front end is the "micro module loader". It mainly solves the problem of how to realize the "micro service" of the front end, so that applications, components, and logic can become shared micro services. It can be seen from single-spa's overview of micro-frontends:

img

In single-spa opinion micro distal three types: Application of micro-micro components, micro modules, actually single-spa requires that they are in SystemJS packaged form, in other words they are essentially micromodule .

SystemJS is a tool for loading modules at runtime. It is a complete replacement for the native ES Module importMap SystemJS dynamically loaded modules must SystemJS module or UMD module.

What is the difference between qiankun and single-spa?

Based on single-spa , Qiankun has strengthened the ability of micro-application integration, but abandoned the ability of micro-modules. So, the difference between them is size micro services , the size of the universe that can be served is the application level, and single-spa is a module-level. They can split the front end, but the granularity of the split is different.

  1. Micro application loader: The granularity of "micro" is application, that is, HTML , which can only do application-level sharing
  2. Micro module loader: The granularity of "micro" is the module, that is, the JS module, which can share at the module level

Why do you want to do this? We have to think about the background of these two frames:

qiankun: Ali has a large number of disrepaired projects inside, and the business side urgently needs tools to integrate them quickly and safely. From this perspective, Qiankun has no requirement for module federation at all. Its requirement is only how to integrate projects quickly and safely. So Qiankun wants to be a micro front-end tool.

single-spa: learns back-end micro-services, realizes front-end micro-services, makes applications, components, and logics shareable micro-services, and realizes a true micro-front-end. So single-spa wants to make a game-changer .

Here I have also compiled a diagram to facilitate understanding:

img

to sum up

To realize the micro front-end, module loading, style and script sandbox isolation are all obstacles that cannot be avoided. This article has collected some resources from the Internet and sorted out some feasible solutions based on my own understanding. Hope everyone corrects~

Reference article

may be the most complete micro front-end solution you have ever seen

architecture design: micro front-end architecture

several micro front-end solutions

suitable for the "micro front end" solution of existing large-scale MPA projects

talk about the micro front end

Micro front-end architecture based on Single-SPA

Single-Spa + Vue Cli micro front-end landing guide + video (project isolation and remote loading, automatic introduction)

Cloud open platform micro front-end solution sandbox implementation

Talking about several ways to realize the front-end JS sandbox

How to dynamically load JS modules in the era of micro front end

micro front end-the easiest micro front end knowledge

阅读 3.6k
avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

989 声望
1.9k 粉丝
0 条评论
你知道吗?

avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

989 声望
1.9k 粉丝
宣传栏