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
andJavaScript
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 useiframe
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. UsuallyWeb Components
is used for sub-application encapsulation, and the sub-application is more like a business component than an application. The real use ofWeb Components
technology in the project is still a little far from us now, but combiningWeb 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:
When the entire micro-frontend framework is running, the user experience is similar to the one shown below:
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:
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.
plan | advantage | shortcoming |
---|---|---|
Monorepo | 1. 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 package | 1. 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 modules | The main application and the sub-application are completely decoupled, and the sub-application can use any technology stack | There 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:
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
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()
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
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.
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.
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:
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 themount
object beforewindow
. Aftermount
, a newwindow
object will be generated. Afterumount
, it will return to the temporarywindow
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 aswindow.resize
. Afterumount
and returning to the temporarywindow
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:
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:
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:
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 nativeES Module
at this stage (the browser does not officially supportimportMap
).SystemJS
dynamically loaded module must be theSystemJS
module orUMD
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.
- Micro application loader: The granularity of "micro" is application, that is,
HTML
, it can only achieve application-level sharing - 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:
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
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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。