preface
In the previous article, I talked about the single-SPA framework, which only implements the scheduling of sub-application lifecycle and the monitoring of URL changes. None of the features of the micro front end are implemented, so it’s not strictly a micro front end framework.
Today let’s talk about a real micro front-end framework: Qiankun. Again, this article will not show you how to implement a Demo, because Github already has a good Demo, if you think the official website Demo is too complicated, you can also see my own small Demo.
What did Qiankun do
First of all, Qiankun is not a single framework, it adds more features to the single-SPA foundation. Here’s what Qiankun provides:
- The implementation of the child application loading, in the original SINGLE-SPA JS Entry based on the HTML Entry
- Style and JS isolation
- More life cycles: beforeMount, afterMount, beforeUnmount, afterUnmount
- Child application preloading
- Global State Management
- Global error handling
I’m not going to go through it feature by feature, because that would be boring, and you’d just know what it is, and you wouldn’t really know where it came from. So I’d rather talk a little bit about where these features came from, how they were thought of.
Multiple entry
Let’s review how single-SPA registers its sub-apps:
singleSpa.registerApplication(
'appName'.() = > System.import('appName'),
location= > location.pathname.startsWith('appName'));Copy the code
As you can see, single-SPA uses JS Entry to connect to the micro application, that is, output a JS, and then bootstrap, mount, and unmount functions.
But it’s not that simple: our projects typically put static resources on the CDN to speed things up. In order not to affected by caching, we will also be JS file is named contenthash garbled filename: jlkasjfdlkj. Jalkjdsflk. JS. As a result, every time the child application is published, the entry JS file name will have to be changed again, which will cause the JS URL introduced by the main application to change again. Trouble!
Another problem with packaging into a single JS file is that the optimization of packaging is gone: on-demand loading, first screen resource loading optimization, CSS independent packaging and other optimization measures are all .
In many cases, the sub-app is already online, such as abcd.com. Isn’t merging multiple subapplications in a micro front essentially merging multiple HTML? So why not just give you the HTML for the child application, and the main application just plugs in and calls it a day? This should be the same as inserting the SRC in .
This way of accessing a child application by providing AN HTML Entry is called an HTML Entry. One of the great things about Qiankun is that it provides an HTML Entry. You can call the registration subapplication function in Qiankun as follows:
registerMicroApps([
{
name: 'react app'.// The child application name
entry: '//localhost:7100'.// Subapplication HTML or url
container: '#yourContainer'.// Mount the container selector
activeRule: '/yourActiveRule'.// Activate the route},]); start();// Go
Copy the code
It is easy to use, just add the single-SPA lifecycle hook to the JS portal, and then release it directly to access.
import-html-entry
However, HTML Entry is not as simple as giving a SINGLE HTML URL to access the entire child application. The HTML file for the child application is just a jumble of tag text. ,
So the authors of Qiankun wrote their own NPM package to handle HTML Entry requirements: import-html-Entry. The usage is as follows:
import importHTML from 'import-html-entry';
importHTML('./subApp/index.html')
.then(res= > {
console.log(res.template); // Get the HTML template
res.execScripts().then(exports= > { // Execute the JS script
const mobx = exports; // Get the JS output
// The following is to take the content of the JS entry and do something with it
const { observable } = mobx;
observable({
name: 'kuitos'})})});Copy the code
Of course, Qiankun already combines import-html-Entry with the child app loading function. All you need to know is that the library is used to retrieve HTML template content, Style styles and JS script content.
With the above understanding, I believe you have ideas for how to load the child application, pseudocode is as follows:
// Parse HTML, get HTML, js, Const {htmlText, jsText, CssText} = importHTMLEntry('https://xxxx.com') // Create container const $= document.querySelector(container) $container.innerhtml = Const $script = createElement('style', cssText) const $script = createElement('script', cssText) jsText) $container.appendChild([$style, $script])Copy the code
In the third step, we have a question: the current application inserts the style and script tags perfectly, but the next application to mount the tags will be polluted by the previous style and script.
In order to solve these two problems, we have to do a good job of style and JS isolation between applications.
Style isolation
Qiankun implements two style isolation schemes recommended by single-SPA: ShadowDOM and Scoped CSS.
Add a ShadowDOM node to the ShadowDOM node. Add a ShadowDOM node to the ShadowDOM node.
if (strictStyleIsolation) {
if(! supportShadowDOM) {/ / an error
// ...
} else {
// Delete the original content
const { innerHTML } = appElement;
appElement.innerHTML = ' ';
let shadow: ShadowRoot;
if (appElement.attachShadow) {
// Add the shadow DOM node
shadow = appElement.attachShadow({ mode: 'open' });
} else {
// Deprecated operation
// ...
}
// Add content to the Shadow DOM nodeshadow.innerHTML = innerHTML; }}Copy the code
Use the natural isolation feature of Shadow DOM to achieve style isolation between sub-applications.
Another option is to Scoped CSS, which essentially means to isolate styles between sub-applications by modifying CSS selectors. For example, you have CSS code like this:
.container {
background: red;
}
div {
color: red;
}
Copy the code
Qiankun scans the given CSS text and prefaces the selector with the name of the child application using a regular match. If it encounters an element selector, it adds a parent class name to it. For example:
.subApp.container {
background: red;
}
.subApp div {
color: red;
}
Copy the code
JS isolation
The first step to isolation is to isolate variables on the global object Window. Window.settimeout = undefined; window.settimeout = undefined; window.settimeout = undefined; window.settimeout = undefined; window.settimeout = undefined
So A deeper level of JS isolation is essentially to record the previous values of the current window object, and then recover all the values after A child application comes in and messes with it. This is what SnapshotSandbox does, in pseudo-code like this:
class SnapshotSandbox {...active() {
// Record the current snapshot
this.windowSnapshot = {} as Window;
getKeys(window).forEach(key= > {
this.windowSnapshot[key] = window[key];
})
// Restore the previous changes
getKeys(this.modifyPropsMap).forEach((key) = > {
window[key] = this.modifyPropsMap[key];
});
this.sandboxRunning = true;
}
inactive() {
this.modifyPropsMap = {};
// Record the changes and restore the environment
getKeys(window).forEach((key) = > {
if (window[key] ! = =this.windowSnapshot[key]) {
this.modifyPropsMap[key] = window[key];
window[key] = this.windowSnapshot[key]; }});this.sandboxRunning = false; }}Copy the code
In addition to SnapShotSandbox, Qiankun also provides a sandbox implementation using the ES 6 Proxy:
class SingularProxySandbox {
/** Global variable */ added during sandboxing
private addedPropsMapInSandbox = new Map<PropertyKey, any>();
/** Global variable */ updated during sandbox
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any>();
/** Continuously record the map of updated (new and modified) global variables used to do snapshot */ at any time
private currentUpdatedPropsValueMap = new Map<PropertyKey, any>();
active() {
if (!this.sandboxRunning) {
// Restore the modified value of the child application
this.currentUpdatedPropsValueMap.forEach((v, p) = > setWindowProp(p, v));
}
this.sandboxRunning = true;
}
inactive() {
// Restore the window value before loading the child application
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) = > setWindowProp(p, v));
// Delete the window value added during the child application
this.addedPropsMapInSandbox.forEach((_, p) = > setWindowProp(p, undefined.true));
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.LegacyProxy;
const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;
const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
const proxy = new Proxy(fakeWindow, {
set: (_: Window, key: PropertyKey, value: any): boolean= > {
if (this.sandboxRunning) {
if(! rawWindow[key]) { addedPropsMapInSandbox.set(key, value);// Record the value added during the sandbox
} else if(! modifiedPropsOriginalValueMapInSandbox.has(key)) { modifiedPropsOriginalValueMapInSandbox.set(key, rawWindow[key]);// Record the value before the sandbox
}
currentUpdatedPropsValueMap.set(key, value); // Record the value after the sandbox
// You must reset the window object to ensure that the updated data is available on the next get
(rawWindow as any)[key] = value;
}
},
get(_: Window, key: PropertyKey): any {
return rawWindow[key]
},
}
}
}
Copy the code
SnapshotSandbox is a demoted version of SingularProxySandbox, since there is no Proxy object in some older browsers.
Still, the problem is not over. This applies only to cases where there is only one child on a page, also known as a singular mode. If a page has more than one child application, a SingluarProxySandbox is obviously not enough. In order to solve this problem, Qiankun provides ProxySandbox with the following pseudo-code:
class ProxySandbox {...active() { / / + 1 bs
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() { / / - 1 bs
if (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) = > {
if (this.proxy.hasOwnProperty(p)) {
delete window[p]; // Delete the value added by the child application in the whitelist}}); }this.sandboxRunning = false;
}
constructor(name: string){...const rawWindow = window; // The original window object
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow); // Copy the key-value from the true window to the false Window object
const proxy = new Proxy(fakeWindow, { // The agent copies the window
set: (target: FakeWindow, key: PropertyKey, value: any): boolean= > {
if (this.sandboxRunning) {
target[key] = value // Change the value in fakeWindow
if(variableWhiteList.indexOf(key) ! = = -1) {
rawWindow[key] = value; // Whitelist, change the value of true window
}
updatedValueSet.add(p); // Record the changed value
}
},
get(target: FakeWindow, key: PropertyKey): any {
return target[key] || rawWindow[key] // Look in fakeWindow, but can't find it in straight window}}}},Copy the code
As you can see from the above, there is not a lot of recovery inactive and inactive because the child application is unmounted and the fakeWindow is thrown away.
And so on, say so many top was just discuss the window object isolation, small pattern? Is small.
The sandbox
Now let’s take a look at the sandbox. In fact, both sandbox and JS isolation are ultimately implemented to give the child application a separate environment, which means we have hundreds of things to patch to create the ultimate
However, Qiankun is not everything, it only patches some important functions and listeners.
The most important patches are insertBefore, appendChild, and removeChild.
When we load child applications, it is inevitable that we will encounter dynamic adding/removing CSS and JS scripts. It is possible for either or to insert or remove
Therefore, these three functions should be patched when called by or . The main purpose is not to insert and in the main application, but in the child application. The pseudo code of patch is as follows:
// patch(element)
switch (element.tagName) {
case LINK_TAG_NAME: / / < link > tag
case STYLE_TAG_NAME: { / / < style > tag
if (scopedCSS) { // Use Scoped CSS
if (element.href;) { // Do something like
stylesheetElement = convertLinkAsStyle( // Get the CSS text in
and add the prefix using css.process
element,
(styleElement) = > css.process(mountDOM, styleElement, appName), // Add the prefix callback
fetch,
);
dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement); // Cache and spit it out the next time you load the sandbox
} else { Container {background: red}css.process(mountDOM, stylesheetElement, appName); }}return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); // Insert into the mount container
}
case SCRIPT_TAG_NAME: {
const { src, text } = element as HTMLScriptElement;
if (element.src) { // Handle the external link JS
execScripts(null, [src], proxy, { // Get and execute JS
fetch,
strictGlobal,
});
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); // Insert into the mount container
}
// Handle inline JS
execScripts(null[`<script>${text}</script>`], proxy, { strictGlobal });
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
}
default:
break;
}
Copy the code
Once you have patched the sandbox, you can apply the style and JS to the current subapplication while working with the style and JS scripts. Note that the CSS style text is saved, so when the child applies remount, these styles can be added directly as a cache without any further processing.
The rest of the patches are made for historyListeners, setInterval, addEventListeners and removeEventListeners, which are nothing more than to record the listeners and add some values when mounting them. When unmount, execute it or delete it once.
More life cycles
If the current project is migrated to a child application, the JS in the entry will have to work with Qiankun to make some changes, which may affect the independent operation of the child application. For example, if you have a micro front end, you might have to create a main application and a sub-application locally before you can do development and debugging, which is a pain in the ass.
To solve the problem that subapps can also run independently, Qiankun injected some variables to tell the subapp: “Hey, you’re a son now, use the subapp rendering.” When the child application can’t retrieve the injected variables, it will know: Oh, I’m going to run independently now, just go back to the original rendering, such as:
if (window. __POWERED_BY_QIANKUN__) {
console.log('Micro front End Scene')
renderAsSubApp()
} else {
console.log('Single Unit Scenario')
previousRenderApp()
}
Copy the code
__POWERED_BY_QIANKUN__ = true, because the child application will need this variable at compile time. * * * * * * * * * * * * * * * * * * * * * * * * * *
// getAddOn
export default function getAddOn(global: Window) :FrameworkLifeCycles<any> {
return {
async beforeLoad() {
// eslint-disable-next-line no-param-reassign
global.__POWERED_BY_QIANKUN__ = true;
},
async beforeMount() {
// eslint-disable-next-line no-param-reassign
global.__POWERED_BY_QIANKUN__ = true;
},
async beforeUnmount() {
// eslint-disable-next-line no-param-reassign
delete global.__POWERED_BY_QIANKUN__; }}; }// loadApp
const addOnLifeCycles = getAddOn(window)
return {
load: [addOnLifeCycles.beforeLoad, subApp.load],
mount: [addOnLifeCycles.mount, subApp.mount],
unmount: [addOnLifeCycles.unmount, subApp.unmount]
}
Copy the code
To summarize, the new life cycles are:
- beforeLoad
- beforeMount
- afterMount
- beforeUnmount
- afterUnmount
loadApp
Ok, so that’s all the steps for loading a child app. Here’s a quick summary:
- Import-html-entry Parses HTML to get JavaScript, CSS, HTML
- Create a container with CSS style isolation: Add a Shadow DOM to the container or prefix the CSS text to Scoped the CSS
- Create a sandbox, listen for changes to Windows, and patch some functions
- Provide more life cycles, inject some of the variables provided by Qiankun into beforeXXX
- Returns an object with properties bootstrap, mount, and unmount
preload
Seeing how many steps it takes to load a child application, we can’t help but think: If you can pre-load other children when the first child is free, then switching between children can be faster, i.e. child preloading.
To do something in your spare time, use the requestIdleCallback provided by your browser. Preloading is a form of CSS and JS that can be downloaded from the site. It is a form of javascript that can be downloaded from the site.
requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
Copy the code
Now, let’s think about it a little more imaginatively: should all of the subapps be preloaded all at once? Not really? It is possible that some subapplications will be preloaded and some will not.
So Qiankun offers three preloading strategies:
- All child applications are preloaded immediately
- All child applications are preloaded after the first child application is loaded
- in
criticalAppNames
The child applications in the array should be preloaded immediately, inminorAppsName
The child applications in the array are preloaded after the first child is loaded
Source code implementation is as follows:
export function doPrefetchStrategy(apps: AppMetadata[], prefetchStrategy: PrefetchStrategy, importEntryOpts? : ImportEntryOpts,) {
const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) = > names.includes(app.name));
if (Array.isArray(prefetchStrategy)) {
// All applications are preloaded after the first child is loaded
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
} else if (isFunction(prefetchStrategy)) {
(async() = > {// Half and half
const { criticalAppNames = [], minorAppsName = [] } = awaitprefetchStrategy(apps); prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts); prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts); }) (); }else {
switch (prefetchStrategy) {
case true: // All applications are preloaded after the first child is loaded
prefetchAfterFirstMounted(apps, importEntryOpts);
break;
case 'all': // All child applications are preloaded immediately
prefetchImmediately(apps, importEntryOpts);
break;
default:
break; }}}Copy the code
Global State Management
Global state is most likely to occur in micro front end scenarios, where the main application provides some SDK that can be initialized. At the beginning, you upload an uninitialized SDK, wait for the main application to initialize the SDK, and then notify the child application through a callback: Wake up, the SDK is ready.
This idea is exactly the same as Redux and Event Bus. The state is stored in the gloablState object of the window, and the onGlobalStateChange callback is added.
let gloablState = {}
let deps = {}
// Trigger global listening
function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
Object.keys(deps).forEach((id: string) = > {
if (deps[id] instanceof Function) { deps[id](cloneDeep(state), cloneDeep(prevState)); }}); }// Add a listener for global state changes
function onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately? : boolean) {
deps[id] = callback;
if (fireImmediately) {
constcloneState = cloneDeep(globalState); callback(cloneState, cloneState); }}/ / update the globalState
function setGlobalState(state: Record<string, any> = {}) {
constprevState = globalState globalState = {... cloneDeep(globalState), ... state} emitGlobal(globalState, prevState); }// Unregister dependencies in this application
function offGlobalStateChange() {
delete deps[id];
}
Copy the code
OnGlobalStateChange adds listeners, and when setGlobalState is called to update the value, emitGlobal is called and all corresponding listeners are executed. Call offGlobalStateChange to delete the listener. Easy ~
Global error handling
We listen for error and unhandledrejection events:
export function addGlobalUncaughtErrorHandler(errorHandler: OnErrorEventHandlerNonNull) :void {
window.addEventListener('error', errorHandler);
window.addEventListener('unhandledrejection', errorHandler);
}
export function removeGlobalUncaughtErrorHandler(errorHandler: (... args: any[]) => any) {
window.removeEventListener('error', errorHandler);
window.removeEventListener('unhandledrejection', errorHandler);
}
Copy the code
Add listeners when you use them, remove them when you don’t, no nonsense.
conclusion
Again, summarize what Qiankun did:
- Implementing the loadApp function is the most critical and important step
- CSS style isolation can be realized by Shadow DOM and Scoped CSS
- Realize sandbox, JS isolation, mainly on the Window object, various listeners and methods for isolation
- Provides many life cycles and in some
beforeXXX
Insert the variable provided by Qiankun into the hooks of
- Provide preloading, download HTML, CSS, JS ahead of time, and have three strategies
- All are preloaded immediately
- All are preloaded after the first load
- Some preload immediately, some preload after the first load
- Provides global state management, such as Redux, Event Bus
- Provide global error handling, mainly listening for error and unhandledrejection two events
The last
Although Ali says: “probably the most complete micro front end solution you’ve ever seen ”. However, from the above interpretation of the source code, it can be seen that Qiankun also has some things not done. For example, there is no isolation of localStorage, if many child applications are used localStorage is likely to conflict, in addition, there are cookies, indexedDB sharing, etc. Or what if multiple sub-applications on a single page rely on front-end routing? Of course, the question here is only my personal guess.
One other thing to say: the difficulty with the micro front end is not single-SPA lifecycle, route hijacking. It’s how to load a child app. As you can see from the above, there are a lot of hacky code, such as the prefix in front of the selector, the child application ,
It is also these hacky code, when building micro front end will encounter a lot of problems, and the purpose of micro front end is to integrate multiple mountains, so micro front end solution is destined to have no silver bullet, and the line and cherish it.