80
头图

foreword

With the development of technology, the content carried by Single-Page Application -end applications has become increasingly Multi-Page Application , and various problems have MPA based on this. The delay problem of switching experience, but also brought about the long loading time for the first time, and the problem of the boulder application ( Monolithic ) brought by the explosion of the project; for MPA , its deployment is simple, the natural hard isolation between applications, and the Technology stack independent, independent development, independent deployment and other characteristics. If we can combine the characteristics of these two parties, will it bring a better user experience to users and developers? At this point, with the concept of microservices for reference, the micro-frontend came into being.

At present, there are many introductions to the micro-frontend architecture in the community, but most of them stay at the stage of concept introduction. This article will focus on a specific type of scenario, focusing on what value the micro-front-end architecture can bring to and , the technical decisions need to be paid attention to the specific practice process, and supplemented by specific codes, which can truly help you build A production micro-frontend architecture system with available.

What is a micro frontend?

Micro front-end is an architecture similar to micro-services. It applies the concept of micro-service to the browser side, that is, the 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 independently developed and deployed independently.

The value of micro frontends

The micro-frontend architecture has the following core values:

  • Technology stack independent The main framework does not restrict access to the application's technology stack, and sub-applications have full autonomy
  • Independent development and independent deployment The sub-application warehouse is independent, and the front and back ends can be developed independently. After the deployment is completed, the main framework automatically completes the synchronization update
  • When running independently, the state between each sub-application is isolated, and the runtime state is not shared

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

Solutions for mid- and back-end applications

Due to the characteristics of long application life cycle (often 3+ years), middle and background applications are often more web to eventually evolve into a boulder application than other types of applications. This mainly brings two problems: technology stack lags behind , and compiles and deploys slowly . From the perspective of technical implementation, micro-front-end architecture solutions can be roughly divided into the following scenarios:

  • Front-end containerization: iframe can effectively embed another web page/single page application into the current page, CSS and JavaScript between the two pages are isolated from each other. iframe is equivalent to creating a brand new independent hosting environment, similar to sandbox isolation, which means that front-end applications can run independently of each other. If we build an application platform, we will integrate third-party systems, or systems under multiple teams from different departments, and use iframe as a container to accommodate other front-end applications. Obviously, this is still a very reliable solution.
  • Microcomponents: With the help Web Components technology, developers can create reusable custom elements to build cross-frame front-end applications. Usually Web Components is used for sub-application encapsulation, and the sub-application is more like a business component than an application. The real use of Web Components technology in the project is still a little far from us now, but combining Web Components to build front-end applications is a future-oriented evolution 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 to build some business functions into an independent chunk code, which only needs to be loaded remotely when used.

Micro Frontend Architecture

The current micro front-end mainly adopts the combined application routing scheme. The core of the scheme is the "master-slave" idea, that is, it includes a base ( MainApp ) application and several micro ( MicroApp ) applications, and most base applications are a front-end SPA project is mainly responsible for application registration, routing mapping, message delivery, etc., and micro-applications are independent front-end projects. These projects are not limited to the development of 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 detached from the base. The basic process is shown in the following figure:

img When the entire micro-frontend framework is running, the user experience is similar to the one shown below:

img brief description is that there are some menu items in the base application. Clicking on each menu item can display the corresponding micro-application. The switching of these applications is purely front-end unaware, which is a good reference to the non-refresh feature of SPA .

Problems in Micro Frontend Architecture Practice

It can be found that the advantages of the micro front-end architecture are the combination of the advantages of the MPA and SPA architectures. That is, while ensuring that the application has the right to develop independently, it also has the ability to integrate them together to ensure the complete process experience of the product.

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

To achieve 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-frontend kernel, when accessing a page of a sub-application normally, 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 what happens when we manually refresh the browser at this time?

Since our sub-applications are all of lazy load , when the browser is refreshed, the resources of the main frame will be reloaded, and the static resources of the sub-application of load will be asynchronous. The resource may not be fully loaded, resulting in no rules matching the sub-application /subApp/123/detail found in the routing registry. At this time, the NotFound page will be jumped or the direct routing error will be reported.

This problem is encountered in all the schemes of loading sub-applications in the lazy load method. A few years ago, the angularjs community collectively called this problem Future State .

The solution is also very simple, we need to design such a routing mechanism:

The main frame configures the sub-application's route as subApp: { url: '/subApp/**', entry: './subApp.js' } , then when the browser's address is /subApp/abc , the framework needs to load the entry resource first, and after the entry resource is loaded, ensure that the sub-application's routing system is registered in the main frame, and then go to the sub-application The routing system takes over url change events. At the same time, when the sub-application route is cut out, the main framework needs to trigger the corresponding destroy event. When the sub-application listens to the event, it calls its own uninstall method to uninstall the application, such as destroy = () => ReactDOM.unmountAtNode(container) in the React scenario.

To implement such a mechanism, we can hijack the url change event to implement our own routing system, or we can implement the Dynamic Routing capability based on the existing ui router library in the community, especially react-router after v4 , we only need to copy part of the routing Find the logic. Here we recommend directly selecting the relevant practice single-spa with a relatively complete community.

App Entry

After the routing problem is solved, the way the main framework and sub-applications are integrated 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 with all the code. Typically, you have only one package.json , one webpack configuration, resulting in a package that is referenced by the `` tag in a html file.

npm package

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

Dynamically load modules

Sub-applications are built and packaged by themselves, and sub-application resources are dynamically loaded when the main application is running.

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

Obviously, in order to achieve the two core goals of true technology stack independence and independent deployment, in most scenarios, we need to use the scheme of loading sub-applications at runtime.

JS Entry vs HTML Entry

After determining the solution for runtime loading, another decision point is, what form of resources do we need the sub-application to provide as a rendering entry?

The method of JS Entry is usually that the sub-application types the resource into a entry script , such as the single-spa in the example of 062295fdce512f. However, this scheme has many limitations, such as requiring all resources of sub-applications 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, features such as parallel loading of resources cannot be used.

HTML Entry is more flexible, directly type out the sub-application HTML as the entry, the main frame can obtain the static resources of the sub-application through fetch html , and insert HTML document as a sub-node into the container of the main frame. This not only greatly reduces the access cost of the main application, but also basically does not need to be adjusted in the development and packaging methods of sub-applications, and can naturally solve the problem of style isolation between sub-applications (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 scheme, 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 because container cannot be found when it is loaded. But the problem is that the main application does not guarantee that the container node used by the sub-application is a specific markup element. The solution of HTML Entry can naturally solve this problem and preserve the complete environment context of the sub-application, thereby ensuring a good development experience for the sub-application.

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

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

In essence, HTML acts as the role of the application static resource table. In some scenarios, we can also optimize the solution of HTML Entry to Config Entry , thereby reducing one request, such as:

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

in conclusion:

img

Get the list of sub-application resources

html parsing

HTML from the sub-application as the entry, and qiankun will obtain the html string of the sub-application through fetch (this is why the sub-application resource is to allow cross-domain html A lot of regularities are used to match and obtain the corresponding html (inline, js ), css (inline, outline), comments, entry script entry , etc. in 062295fdce52dc.

asset-manifest.json

The build configuration uses the stats-webpack-plugin plugin to generate a resource list manifest.json file, and create-react-app in the react project built with webpack uses webpack-manifest-plugin to generate the resource list by default. Then the main application ajax asynchronously requests manifest.json

img

webpack-manifest-plugin The resource list generated by the plugin 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 when deploying

Create importmap.json (configuration file) on gitlab or the file storage server, and write the project-entry file mapping relationship to importmap.json after the sub-application is deployed

// 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-frontend architecture, we need to obtain some hook references exposed by sub-applications, such as bootstrap , mount , unmount , etc. (refer to single-spa ), so that we can have a complete lifecycle control over the access application. Since sub-applications usually need to support both integrated deployment and independent deployment modes, we can only choose the compatible module format of umd to package our sub-applications. How to get the module reference exported in the remote script when the browser is running is also a problem that needs to be solved.

Usually our first reaction solution is also the simplest solution of is to agree on a global variable between the sub-application and the main framework, mount the exported hook reference to this global variable, and then the main application takes life from it Periodic function .

This solution is very useful, but the biggest problem is that there is a strong contract the main application and the sub-application. So can we find a loosely coupled solution?

At present, there are mainly two types of serious micro-front-end solutions:

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

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

SystemJS

After getting the sub-application build resource list asset-manifest.json , convert it into importmap and dynamically inject it into html (only supported in SystemJS@6.4.0 ), and then use System.import to load

Byte Beat: new Function()

Let's take a look at the implementation of ByteDance first. Their solution is: "The submodule ( Modules ) is a package of CMD , and I use new Function to wrap it up." In two simple sentences, this should mean using fetch or other The request library directly gets the content of the sub-project as the cmd package, and then uses new Function to execute the sub-module as the function body of function , and passes in the custom define and other parameters to execute the module and get the output of the module. The usage of new Function is as follows, using the submodule content as function text is similar to eval , but it will be clearer to use:

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

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

But here it is possible to define global variables such as requirejs as define , and then use the script label to directly refer to the sub-project for natural loading and execution. Why use fetch + new Function ? Probably because the global define method is not convenient to use dynamically inside the component method.

Ant Financial: eval()

qiankun 动态加载

Let's take a look at the implementation of the typical technology stack-independent solution Ant Financial qiankun . The relevant code is in the import-html-entry warehouse that it uses, and it also requests the library through fetch , but it gets it as a sub-package of the umd package. The content of the project does not use the submodule as amd or cmd , but directly eval and mounts window . eval is executed, the bound window object is changed. This is mainly done by intercepting the changes to the window global variable by the Proxy sub-project and doing 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 modules in the unified Meituan takeaway scheme of the technology stack. Their scheme introduction did not elaborate on the loading method of modules, but from the posted code, you can see loadAsyncSubapp and subappRoutes , also The trigger jsonp hook window.wmadSubapp is mentioned, which means that their scheme is implemented through jsonp . You can set webpack of output of libraryTarget to jsonp , so that the configured packaged product will execute the global jsonp method when loading, and pass the export value of the main module as a parameter. Refer to webpack document other-targets . Pro test feasible:

Sub-engineering 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 child project, and then the components can be dynamically integrated into the main project. You can refer to the dynamic routing analysis in combination with 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 to rely on the ability of webpack . Later, someone naturally thought of making webpack better and more convenient to support the mutual loading of construction products between different projects. This is webpack module federation , the usage 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, the dynamic loading of the JS module can be divided into two stages, the loading stage and the execution stage. According to the loading method, there are two methods: script tag loading and fetch request. In the execution phase, the script tag loading is generally performed in conjunction with the global jsonp function for parsing, and what the function does depends on the convention, such as the requirejs method in define and the webpack webpackJsonpCallback dynamically loaded. After the fetch request method gets the module content, 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 latter method can control the parsing more finely.

img

Application sandbox

There are two very critical problems in the micro-frontend architecture solution. Whether or not to solve these two problems will directly indicate whether your solution is actually available in production. It is a pity that the previous community's handling of this issue has always chosen a "detour" method, such as avoiding conflicts through some default agreements between the main and sub-applications. Today, we will try to solve the problem of possible conflicts between applications more intelligently from a purely technical point of view.

Looking at various technical solutions, there is a major premise that determines how this sandbox works: the final micro application is a single instance or multiple instances exist 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 be solved is to clean up and restore on-site when the application is switched. Lightweight and easy to implement.
  • Multi-instance: resources are not exclusive to the application, it is necessary to solve the situation of resource sharing, 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

How does ensure that global variables between sub-applications do not interfere with each other, so as to ensure soft isolation between each sub-application?

This problem is more tricky than the style isolation problem, and the common play of the community is to prefix some global side effects to avoid conflicts. But in fact, we all understand that this method of "oral" agreement between teams is often inefficient and fragile, and all schemes that rely on human constraints are difficult to 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 response to the JS isolation problem, we created a runtime JS sandbox. A simple architecture diagram is drawn:

img

That is, take a snapshot of the global state before the two life cycles of bootstrap and mount of the application start, and then roll back the state to the stage before the start of bootstrap when the application is switched out/unloaded to ensure that all the pollution of the application to the global state is cleared. . When the application enters the second time, it will be restored to the state before mount , thus ensuring that the application has the same global context as the first remount at mount .

The snapshot can be understood as temporarily storing the mount object before window . After mount , a new window object will be generated. After umount , it will return to the temporary window object.

Of course, what is done in the sandbox is far more than that. Others include some hijacking of global event monitoring, etc. to ensure that after the application is switched out, the monitoring of global events can be completely remount . Re-listen to these global events to simulate a sandbox environment consistent with the application's standalone runtime.

When the script of the main application is initially loaded, some events may be monitored globally, such as window.resize . After umount and returning to the temporary 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 multiple instances, because all attributes are saved on window during runtime

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

Simulate sandbox environment (proxy+closure)

To implement the sandbox, we need to isolate the native objects of the browser, but how to isolate and create a sandbox environment? Node module in vm can achieve similar capabilities, but the browser will not work, but we can use the ability of closures and variable scope to simulate a sandbox environment, such as the following code:

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

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

Therefore ConsoleOS (a micro-frontend solution for Alibaba Cloud Management System) implements a plug-in of Wepback . When the application code is constructed, 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 following function closure, so that we can pass in the simulated window, document and other objects 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 purpose.

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

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

Of course, there are many details to consider, for example: only the iframe in the same domain can extract the corresponding contentWindow . Therefore, it is necessary to provide an empty same-domain URL of the host application as the iframe initially loaded by this URL . Of course, according to the specification of HTML , this URL uses about:blank to ensure the same domain, and no resource loading will occur, but it will happen that the iframe associated with this history cannot be operated, and the routing transformation can only be changed to hash mode.

As shown in the figure below, in addition to an isolated window environment, the micro-frontend also needs to share some global objects. At this time, we can use a proxy to achieve it. Proxy can intercept the reading and function calls of the target object, and then Perform operation processing. It does not operate the object directly, but like the proxy mode, operates through the proxy object of the object, and when doing these operations, you can add some additional operations that are needed. After we take out the corresponding native object in iframe , generate the corresponding Proxy for the specific object that needs to be isolated, and then make some specific settings for some attribute acquisition and attribute setting, for example, so that the document can be loaded on the same DOM tree, For document , most of the properties and methods of DOM operations are directly used in the properties and methods of document in the host browser.

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 sub-applications have their own sandbox environment, all previously exclusive resources are now exclusive to the application (especially location , history ), so sub-applications can also be loaded at the same time. And for some variables, we can also set some access permissions in proxy to limit the ability of sub-applications, such as Cookie , LocalStoage read and write.

When this iframe is removed, the variables written in window and some timeout time set will also be removed (of course, DOM event needs to be sandboxed and then removed in the host).
To sum up, our sandbox can achieve the following features:

image.png

If you understand the above introduction to the principle, 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. However, when it comes to this part of closure construction, each micro-frontend 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 good 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-frontend scenario, sub-applications of different technology stacks will be integrated into the same runtime, we must ensure that the styles of each sub-application will not interfere with each other at the framework layer.

Shadow DOM?

For the problem of "Isolated Styles" , if browser compatibility is not considered, usually the first solution that comes to our minds is Web Components . Based on the Web Components capability of Shadow DOM , we can wrap each sub-application into a Shadow DOM to ensure absolute isolation of its runtime styles.

However, the Shadow DOM solution will encounter a common problem in engineering practice. For example, we build a sub-application that is rendered in Shadow DOM in this way:

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 the style scope of the sub-application is only under the shadow element, once the sub-application runs out of bounds and runs outside the scene to build DOM , it will inevitably lead to the situation where the built DOM cannot apply the style of the sub-application .

For example, the sub-app component is called in antd modal . Since modal is dynamically mounted to document.body , and because of the characteristics of Shadow DOM , the style of antd will only take effect in the antd shadow . The solution is to float the antd style up one layer and throw it into 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?

The common practice of the community is to avoid style conflicts by agreeing to the css prefix, that is, each sub-application uses a specific prefix to name class , or directly write style based on css module scheme. For a brand new project, this is of course feasible, but usually the goal of micro-frontend architecture is to solve the access problem of stock/legacy applications. Obviously legacy apps are often hard to motivate for a major makeover.

The most important thing is that there is an unsolvable problem in the way agreed by . If the third-party component library is used in the sub-application, the third-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 solve the above problem, all elements of the sub-application are inserted into the id="root" tag, id is unique, so by adding the attribute selector prefix #root can make the css style take effect in the specified <div id="root"></div>

Dynamic Stylesheet !

The solution to is actually very simple. We only need to uninstall its style sheet after the application is cut out/uninstalled. The principle is that the browser will refactor the entire CSSOM for the insertion and removal of all style sheets. So as to achieve the purpose of inserting and unloading . This ensures that only one applied style sheet is in effect at a time.

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

For example, in HTML Entry mode, the DOM structure after the sub-application is loaded 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 sub-application is replaced or uninstalled, the 062295fdce64ea of the subApp node will also be overwritten, //alipay.com/subapp.css innerHTML naturally be removed and uninstalled.

The limitation of is that there is still the possibility of style conflict between the site framework itself or its components ( header/menu/footer ) and the currently running micro-application, and the second is that there is no way to support the situation that multiple micro-applications are running and displayed at the same time.

qiankun VS single-spa

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

qiankun

From the perspective of , the micro- is the "micro-application loader", which mainly solves the problem of how to multiple scattered projects, which can be seen from the point of itself. out:

img

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

single-spa

From the point of view of single-spa, micro-frontends are "micro-module loader", which mainly solves: how to realize the "microservice" of front-end, so that applications, components and logic can become sharable microservices. As can be seen in single-spa's overview of microfrontends:

img

In single-spa it seems that there are three types of micro-frontends: micro-applications, micro-components, and micro-modules. In fact, single-spa requires them to be packaged in the form of SystemJS . In other words, they are essentially micro-modules .

SystemJS is a tool for loading modules at runtime. It is a complete replacement for the native ES Module at this stage (the browser does not officially support importMap ). SystemJS dynamically loaded module must be the SystemJS module or UMD module.

What is the difference between qiankun and single-spa?

Based on single-spa , Qiankun strengthens the micro-application integration capability, but abandons the micro-module capability. Therefore, the difference between them is that the granularity of the microservice is , the granularity that Qiankun can serve is the application level, and the single-spa is the module level. They can both split the front end, but the granularity of splitting is different.

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

Why did do this? We need to understand the background of these two frameworks:

qiankun: Ali has a large number of projects that are in disrepair, and the business side urgently needs tools to integrate them quickly and safely. From this point of view, 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 microservices, realizes front-end microservices, and makes applications, components and logic into sharable microservices, and realizes micro-frontends in the true sense. So single-spa is trying to make a game-changer .

Here I also put together a diagram for easy understanding:

img

Summarize

To realize the micro-frontend, module loading, style and script sandbox isolation are all hurdles that cannot be bypassed. This article collects some resources from the Internet, and sorts out some feasible solutions based on my own understanding. I hope you all correct me~

Reference article

may be the most complete micro frontend solution you have ever seen

Architecture Design: Micro Front End Architecture

Several micro front-end solutions to

"Micro Front End" solution for existing large MPA projects

Talk about the micro front end

Micro front-end architecture based on Single-SPA

Single-Spa + Vue Cli Micro Frontend Landing Guide + Video (project isolation remote loading, automatic introduction)

Sandbox implementation of Alibaba Cloud open platform micro front-end solution

about several ways of front-end JS sandbox implementation

How to do dynamic loading of JS modules in the micro front-end

micro front end - the easiest to understand micro front end knowledge


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。


引用和评论

0 条评论