• Micro front End 01: Js isolation mechanism analysis (Snapshot sandbox, two kinds of proxy sandbox)
  • Micro Front End 02: Analysis of microapplication loading process (from registration of microapplication to internal implementation of loadApp method)
  • Micro front End 03: Sandbox Container Analysis of Universe (concrete application of Js sandbox mechanism after establishment)
  • Microfront 04: Resource loading mechanism for The Universe (internal implementation of import-HTml-Entry)
  • Micro front End 05: Implementation of loadMicroApp method and analysis of data communication mechanism

As mentioned in micro Front 01: Js isolation mechanism analysis (snapshot sandbox, two kinds of proxy sandbox), Universe is built on the basis of single-SPA. Compared to single-SPA, Universe mainly completes two things: loading microapplications and resource isolation. This paper mainly discusses the loading process of microapplications in the universe.

Qiankun’s microapplication loading process mainly triggers the following four scenarios:

  • throughregisterMicroAppsSign up for microapps
  • throughloadMicroAppManually load microapplications
  • callstartTriggers the preload logic
  • Manual callprefetchAppsPerform preloading

No matter what scenario triggers the microapplication loading logic, there is only one method to perform the microapplication loading itself, and that is the loadApp method in the SRC /loaser.ts file. In order to facilitate your understanding and understanding of the position of microapplication loading logic in the universe, I have listed the main trigger scenarios above. As for the methods listed above, they are apis exposed by the universe, and you can refer to the relevant uses in the universe document. This article will start with a detailed microapplication registration process and then introduce the implementation details in the loadApp method. In the process of introducing the details of loadApp implementation, I will first analyze the main process and key links of loading microapplications. On the basis of our understanding of the main process, the key points that need to be paid attention to are divided into several sections. However, for some contents that may be more detailed, we will use new articles for detailed analysis. For example, in the micro front 01: Qiankun Js isolation mechanism principle analysis (snapshot sandbox, two kinds of proxy sandbox) mentioned three sandbox, we analyzed its core principle at the time, but how they play a role in the tide at that time, did not mention, although the application of load flow and the sandbox mechanism have strong correlation, but this part of content and more, So we will cover this in detail in a future article, combining sandbox code with load flow code. Please see below for details.

Qiankun’s micro application registration process

Please take a look at the picture below:

From the figure, we can see two important points corresponding to step 4 and step 5 in the flow chart respectively:

  • The registration of microapplications actually takes place in single-SPA
  • The life cycle function exposed by the child application is returned by the function argument supplied by the universe
// SRC /apis. Ts
export function registerMicroApps<T extends ObjectType> (
  apps: Array<RegistrableApp<T>>, lifeCycles? : FrameworkLifeCycles<T>,) {
  // Other code omitted here...
  unregisteredApps.forEach((app) = > {
    const{ name, activeRule, loader = noop, props, ... appConfig } = app; registerApplication({// Key point 1
      name,
      app: async() = > {// Critical point 2
        // Other code omitted here...
        const{ mount, ... otherMicroAppConfigs } = (// Keypoint 4
          awaitloadApp({ name, props, ... appConfig }, frameworkConfiguration, lifeCycles) )();return {// Keypoint 3
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

Copy the code

Let’s start with some key information from the code snippet above:

About the role that single-SPA plays here

As you can see intuitively from key point 1 in the code snippet above, the actual method to initiate a registered microapplication is the registerApplication method, which is imported from single-SPA. This mirrors what we said earlier, that the universe is built on single-SPA. If there is no universe, can we use single-SPA directly to enable our project to be able to access micro applications? The answer is yes. The only thing that qiankun does here is load resources and isolate resources. Here you will have some inspiration, enhance those mature but not powerful open source libraries, can let us do more with less, not only avoid duplication and improve work efficiency. At the same time, it also provides an idea for those who want to participate in open source contributions but do not know how to start.

The return value of the microapplication load function

The microapplication loading function indicated at comment key point 2 in the above code snippet corresponds to step 5 in the flowchart, and the core logic is the loadApp method indicated at comment key point 4 in the code (the other logic is ignored for the moment and will be mentioned later in this article when appropriate). Step 5 in the flowchart above corresponds to key point 3 in the code snippet above. When the app method at key point 2 is executed, the object at key point 3 is returned, including mount, name, bootstrap, unmount and other attributes. These attributes are actually required for single-SPA to register microapplications, because the so-called registered microapplications in single-SPA, In essence, it is to obtain the relevant life cycle methods of microapplication exposure, and then realize the control of microapplication by controlling these life cycle methods in the subsequent process of program operation.

Next, we’ll look inside loadApp, where the core logic of microapplication loading is, and this is the subject of this article, below.

Internal implementation of loadApp

The body flow of loadApp

Please have a brief look at the flow chart:

As you can see from the figure, the internal logic of loadApp is quite complex, with roughly 11 main steps, if some details are omitted. Since the flow in the diagram is code execution flow, it may not be easy to understand on first reading, but it doesn’t matter that the following sections will break down the details step by step with the code. Before diving into the details, please understand the core features of loadApp: That is to get the js/ CSS/HTML resources of the micro-application, and process these resources, then construct and execute a method that needs to be executed in the life cycle, and finally return a function, and the return value of this function is an object, which contains the life cycle method of the micro-application. With this basic understanding, we can proceed to the following detailed interpretation.

Interesting details in loadApp

Methods for obtaining microapplication resources

The importEntry function in the import-HTML-Entry library is used to fetch microapplication resources in step 3 of the flow chart. This function solves two problems: how to fetch resources locally and how to handle them appropriately to meet actual needs. About the specific implementation of this library, how to obtain resources, how to deal with resources, I will follow a separate article to explain, please friends look forward to. The code for calling this function is as follows:

  // SRC /loader.ts
  const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
  /** * A brief introduction to these variables, which will be covered in more detail where appropriate: A string containing HTML and CSS resources * execScripts: a function that returns an object * assetPublicPath: a relative path to a remote resource on the page * /Copy the code

Convert the obtained template(involving HTML/CSS) into a DOM node

In code snippet 2, we mentioned that template is a string. Why is it a string? In fact, it is very simple. Resources arriving locally from the network as byte streams can only be converted to strings for processing. Here we need to convert the string into a concrete Dom node. How do you do that? The specific code involves two parts:

// SRC /loader.ts
const appContent = getDefaultTplWrapper(appInstanceId)(template);
let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsolation,
    scopedCSS,
    appInstanceId,
  );
Copy the code
// code fragment 4, corresponding file: SRC /utils
export function getDefaultTplWrapper(name: string) {
  return (tpl: string) = > `<div id="${getWrapperId(name)}" data-name="${name}" data-version="${version}">${tpl}</div>`;
}
Copy the code

The appContent in snippet 3 corresponds to the appContent mentioned in Step 4 of the flowchart. The initialAppWrapperElement in the third code snippet is the DOM element initialAppWrapperElement mentioned in step 5 of the flowchart. As you can see from the code, the function getDefaultTplWrapper wraps a div around the template wrapper and sets the id, data-name, data-version, and other attributes on the div. Why wrap a label like that? In my opinion, there are two advantages. The first is to ensure that there is only one root node after template is converted into DOM node, so that the accuracy can be ensured when mounting and unmounting microapplications in the future. The second is to set an identifying attribute on the tag to avoid conflicts with the attribute on the original root element of the microapplication.

The getDefaultTplWrapper method does nothing but return a function. This mechanism avoids passing the name argument repeatedly. In fact, this is a point that we can learn in the daily process of writing code, in the case of certain parameters that need to be passed frequently and those parameters do not change very often, especially in the case of processing these parameters, can be optimized in this way. On the one hand, it makes the parameters of the function call more concise, and at the same time avoids the repeated processing of these parameters. This is what is often called granulation of functions, which makes good use of the closure mechanism. Having said that, I’d like to express an old point of view of mine: there are no technical masters, only developers with solid fundamentals. I share this sentence with you, hoping to lay a solid foundation together with you and go further and further on the road of technology.

Next, how to convert the string appContent to the DOM node initialAppWrapperElement depends on the createElement method shown in section 3, which looks like this:

// SRC /loader.ts
function createElement(appContent: string, strictStyleIsolation: boolean, scopedCSS: boolean, appInstanceId: string,) :HTMLElement {
  const containerElement = document.createElement('div');
  containerElement.innerHTML = appContent;
  // appContent always wrapped with a singular div
  const appElement = containerElement.firstChild as HTMLElement;
  // Omit other code...
  return appElement;
}
Copy the code

The key in code snippet 4 is to create an empty div called containerElement, set its content to the appContent mentioned above, and get the first child of containerElement as the DOM element to be returned. Of course, there is some work to be done on the DOM element, which is omitted. What does this do? See the comment above appContent always Wrapped with a singular div. If appContent has multiple root nodes, only the first node will be fetched and applied. If you have the same scenario in your everyday coding, I think you can just reuse these three lines of code.

Processing and isolation of CSS resources

The following lines are omitted from Code snippet 4. The function of these lines of code is to process the style in appElement. Limited by space, we don’t care about how to deal with these styles. We will use special articles to analyze them in detail.

if (scopedCSS) {
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
    if(! attr) { appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId); }const styleNodes = appElement.querySelectorAll('style') | | []; forEach(styleNodes,(stylesheetElement: HTMLStyleElement) = >{ css.process(appElement! , stylesheetElement, appInstanceId); }); }Copy the code

The pitfalls of shadow DOM

In fact, the following lines of code were omitted from Snippet 4:

if (strictStyleIsolation) {
    if(! supportShadowDOM) {console.warn(
        '[qiankun]: As current browser not support shadow dom, your strictStyleIsolation configuration will be ignored! ',); }else {
      const { innerHTML } = appElement;
      appElement.innerHTML = ' ';
      let shadow: ShadowRoot;
      if (appElement.attachShadow) {
        shadow = appElement.attachShadow({ mode: 'open' });
      } else {
        // createShadowRoot was proposed in initial spec, which has then been deprecated
        shadow = (appElement as any).createShadowRoot();
      }
      shadow.innerHTML = innerHTML;
    }
Copy the code

The main function of these lines of code is to determine if the current environment supports shadow DOM if it has strict style isolation, and to bind elements to shadow DOM if it does. As for what shadow DOM is, I don’t have space to explain it here. If a friend is still not clear after looking up information can leave a message to communicate. However, it should be pointed out that although shadow DOM can achieve good isolation, there is a problem that needs attention. Elements are autonomous in the Shadow DOM and cannot be influenced by the outside world. However, if the element is mounted outside the Shadow DOM, it will not work properly. For example, many React bullets are directly mounted to the body. In this case, measures should be taken to avoid them. In the documentation for the start method to the API, Qiankun mentions the following:

Strict style isolation based on ShadowDOM is not a solution that can be used mindlessly. In most cases, it is necessary to access the application and make some adaptation to run properly in ShadowDOM (for example, in the React scenario, these problems need to be solved. Users need to be aware of what strictStyleIsolation means. In the future, Qiankun will provide more official practice documents to help users quickly transform their applications into microapplications that can run in ShadowDOM environment.

About the function initialAppWrapperGetter

This function exists between step 6 and step 7 in the flowchart. We have already got the DOM element initialAppWrapperElement of the micro-application. Why is there a function to get the DOM element of the micro-application?

// SRC /loader.ts
  constinitialAppWrapperGetter = getAppWrapperGetter( appInstanceId, !! legacyRender, strictStyleIsolation, scopedCSS,() = > initialAppWrapperElement,
  );
  /** generate app wrapper dom getter */
function getAppWrapperGetter(
  appInstanceId: string,
  useLegacyRender: boolean,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  elementGetter: () => HTMLElement | null.) {
  return () = > {
    if (useLegacyRender) {
      // omit some code...
      const appWrapper = document.getElementById(getWrapperId(appInstanceId));
      // omit some code...
      returnappWrapper! ; }const element = elementGetter();
      // Omit some code
    returnelement! ; }; }Copy the code

From code snippet 5 above, we can actually see that the getAppWrapperGetter method exists to accommodate the old custom rendering mechanism, which we will not mention here, but can simply be interpreted as mounting a DOM node to a DOM node. This also reminds us that we must be careful when designing a system, or we will often have to make similar compatibility measures in order for the lower versions to work properly. If legacyRender hadn’t been designed in the first place, getAppWrapperGetter wouldn’t have been necessary, and the overall readability and ease of use would have improved. Of course, as an excellent micro front-end framework, qiankun is also gradually evolving, compatible with low version of the behavior is difficult to avoid.

Application of sandbox mechanism

In our last article, we looked at the core principles of three sandbox mechanics in the universe. But it’s not enough to understand the principles, we have to use them in programs to make them work. In view of the specific application of sandbox involves a lot of code, it is not convenient to elaborate in this article. I will write a separate article to analyze it in the future. Here is a brief introduction, involving the following code:

// SRC /loader.ts
let global = globalContext;
  let mountSandbox = () = > Promise.resolve();
  let unmountSandbox = () = > Promise.resolve();
  const useLooseSandbox = typeof sandbox === 'object'&&!!!!! sandbox.loose;let sandboxContainer;
  if (sandbox) {
    sandboxContainer = createSandboxContainer(
      appInstanceId,
      // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
      initialAppWrapperGetter,
      scopedCSS,
      useLooseSandbox,
      excludeAssetFilter,
      global,);global = sandboxContainer.instance.proxy as typeof window;
    mountSandbox = sandboxContainer.mount;
    unmountSandbox = sandboxContainer.unmount;
  }
Copy the code

This section of code is between steps 6 and 7 in the flowchart. I think the most core inside the line of code is global = sandboxContainer instance. The proxy as typeof window; Because it is the sandbox proxy object in the sandbox container that is responsible for the subsequent operations in the micro-application. If you don’t understand the terms sandbox container and sandbox code object, you can skip them first.

Some functions that need to be executed during the lifecycle

// SRC /loader.ts
const {
  beforeUnmount = [],
  afterUnmount = [],
  afterMount = [],
  beforeMount = [],
  beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) = > concat(v1 ?? [], v2 ?? []));
Copy the code

In code snippe7, corresponding to step 7 in the flowchart, these array objects store many functions in beforeUnmount, afterUnmount, afterMount, beforeMount, and beforeLoad, and these functions are executed at appropriate times. So what is the right time? As mentioned above, microapplications expose lifecycle methods, and single-SPA calls these lifecycle methods to control the state of the microapplication. The methods in code snippet 7 are put into the lifecycle methods, which are covered again later in this article. As for the specific execution and content of mergeWith and getAddOns methods here, because the content is not too much, friends can read by themselves, if they do not understand the content after reading, you can leave a message to communicate.

Great use of the reduce method for arrays: execHooksChain

Code snippet 7 executes, followed by a line of code:

await execHooksChain(toArray(beforeLoad), app, global);
Copy the code

We don’t care what methods are in beforeLoad, which are determined by code snippet seven. Let’s just look at the function execHooksChain for now:

// SRC /loader.ts
function execHooksChain<T extends ObjectType> (
  hooks: Array<LifeCycleFn<T>>,
  app: LoadableApp<T>,
  global = window.) :Promise<any> {
  if (hooks.length) {
    return hooks.reduce((chain, hook) = > chain.then(() = > hook(app, global)), Promise.resolve());
  }

  return Promise.resolve();
}
Copy the code

Here is a clever use of the array reduce function. Imagine how to achieve the same function without writing it this way. I think it should be like this:

// Code snippet nine
for(let i = 0; i < hooks.length; i++){
  await hooks[i](app, global);
}
Copy the code

In order to realize the same function, in fact, both methods can be used, and friends can choose by themselves. However, if they do not have a solid foundation and do not have a clear understanding of reduce method, it may be difficult for them to read relevant codes for the first time. The code fragment 9 I wrote can help those who do not have a solid foundation understand the functions of code fragment 8.

The value returned after the microapplication is loaded

The microapplication load process returns a function when it completes execution, as shown in the code:

  const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) = > {
    // omit the relevant code
    const parcelConfig: ParcelConfigObject = {
      // omit the relevant code
    }
    return parcelConfig;
  }
Copy the code

Our first reaction might be, since the load is complete, why not just return the relevant content instead of returning a function? The answer lies in the parameter remountContainer, because the returned object is actually the lifecycle function exposed by the microapplication required by single-SPA. We know that there is a mount in the microapplication lifecycle method. Our microapplication will eventually be mounted to somewhere, normally the container parameter passed in when the user registers the microapplication. But what if, after registration, the microapplication needs to be mounted somewhere else, so the return value is a function, not an object.

The return object of parcelConfigGetter

const parcelConfig: ParcelConfigObject = {
      name: appInstanceId,
      bootstrap,
      mount: [
        async() = > {if (process.env.NODE_ENV === 'development') {
            const marks = performanceGetEntriesByName(markName, 'mark');
            // mark length is zero means the app is remounting
            if(marks && ! marks.length) { performanceMark(markName); }}},async() = > {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
            return prevAppUnmountedDeferred.promise;
          }

          return undefined;
        },
        // initial wrapper element before app mount/remount
        async() => { appWrapperElement = initialAppWrapperElement; appWrapperGetter = getAppWrapperGetter( appInstanceId, !! legacyRender, strictStyleIsolation, scopedCSS,() = > appWrapperElement,
          );
        },
        // Add a mount hook to ensure that the container dom structure is set before the application is loaded
        async() = > {constuseNewContainer = remountContainer ! == initialContainer;if(useNewContainer || ! appWrapperElement) {// element will be destroyed after unmounted, we need to recreate it if it not exist
            // or we try to remount into a new container
            appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
            syncAppWrapperElement2Sandbox(appWrapperElement);
          }

          render({ element: appWrapperElement, loading: true.container: remountContainer }, 'mounting');
        },
        mountSandbox,
        // exec the chain after rendering to keep the behavior with beforeLoad
        async () => execHooksChain(toArray(beforeMount), app, global),
        async(props) => mount({ ... props,container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
        // finish loading after app mounted
        async () => render({ element: appWrapperElement, loading: false.container: remountContainer }, 'mounted'),
        async () => execHooksChain(toArray(afterMount), app, global),
        // initialize the unmount defer after app mounted and resolve the defer after it unmounted
        async() = > {if (await validateSingularMode(singular, app)) {
            prevAppUnmountedDeferred = new Deferred<void>();
          }
        },
        async() = > {if (process.env.NODE_ENV === 'development') {
            const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`; performanceMeasure(measureName, markName); }},].unmount: [
        async () => execHooksChain(toArray(beforeUnmount), app, global),
        async(props) => unmount({ ... props,container: appWrapperGetter() }),
        unmountSandbox,
        async () => execHooksChain(toArray(afterUnmount), app, global),
        async () => {
          render({ element: null.loading: false.container: remountContainer }, 'unmounted');
          offGlobalStateChange(appInstanceId);
          // for gc
          appWrapperElement = null;
          syncAppWrapperElement2Sandbox(appWrapperElement);
        },
        async() = > {if ((awaitvalidateSingularMode(singular, app)) && prevAppUnmountedDeferred) { prevAppUnmountedDeferred.resolve(); }},]};Copy the code

The returned object has a lot of content, but if you look at it from a macro point of view, it has only four properties: name, bootstrap, mount, and unmount. Yes, these are exactly the life cycle functions that single-SPA needs to expose for microapplications. The next step is to control the microapplication by executing the corresponding lifecycle functions. Because this is the end result of microapplication loading, a lot of other logic has been brought together to produce this result object. I’m not going to go through each of these functions right now, because it’s kind of fragmentary, and it’s not going to make sense. Therefore, the following articles will first introduce the parts that have not been covered in detail in this article. After a more comprehensive understanding of the whole world, we will dive into single-SPA, where these methods will be used, and we will find the right opportunity to explain the many methods in detail here.

Good use of Promise: Deferred

Now that loadApp is complete and a function, parcelConfigGetter, is returned, we move our vision to where loadApp is called, which is code fragment 1 of this article. But snippet 1 omits the code I’m going to talk about now, see here:

registerApplication({
      name,
      app: async() = > {// omit code...
        await frameworkStartedDefer.promise;
        const{ mount, ... otherMicroAppConfigs } = (awaitloadApp({ name, props, ... appConfig }, frameworkConfiguration, lifeCycles) )();// omit the code....
      },
Copy the code

Have found it odd to have a line of code that await frameworkStartedDefer. Promise. This line of code is actually used in conjunction with the following code snippet:

// Owning file: SRC /apis
export function start(opts: FrameworkConfiguration = {}) {
  // Omit other code...
  frameworkStartedDefer.resolve();
}
Copy the code

So what exactly is frameworkStartedDefer?

// Owning file: SRC /apis
const frameworkStartedDefer = new Deferred<void> ();// File: SRC /utils.ts
export class Deferred<T> {
  promise: Promise<T>; resolve! :(value: T | PromiseLike<T>) = > void; reject! :(reason? : any) = > void;
  constructor() {
    this.promise = new Promise((resolve, reject) = > {
      this.resolve = resolve;
      this.reject = reject; }); }}Copy the code

It’s neat to control the order in which code that belongs to two different methods executes by controlling a Promise’s resolve and Reject methods. If there is a similar scenario in daily development, it can be used for reference.

summary

This paper introduces the registration process of Qiankun micro-application, and from the registration process of micro-application, leads to the loading process of micro-application, we have analyzed some key links in the loading process of micro-application. As there are many details, it is difficult to introduce all of them in this article. I will analyze them in the form of a new article. Please look forward to it. Friends can also be interested in their own points in the comments section, I will consider showing the content of related topics in the following articles.

Welcome to follow my wechat subscription number: Yang Yitao, you can get my latest news.

After reading this article, feel the harvest of friends like it, can improve the digging force value, I hope to become an excellent writer of digging gold this year.