preface

The author of this article fully liver more than a week, many times consider the revision of content, and strive to help readers build a micro front frame, understand the principle. If you think it’s good, give it a thumbs up.

Micro front-end is a popular technology architecture at present, many readers secretly asked me the principle of it. In order to explain how this works, I will walk you through implementing a micro front-end framework from scratch, which contains the following features:

  • How to perform route hijacking
  • How to render a child application
  • How to implement JS sandbox and style isolation
  • Enhance the experiential function

In addition, in the process of implementation, the author will also talk about what technical solutions can be used to achieve the micro front end and do the above functions when there are ways to achieve.

Here is the final output warehouse address for this article: Toy-Micro.

Micro front-end implementation scheme

There are a lot of micro front-end implementation schemes, such as:

  1. Qiankun, own IMPLEMENTATION of JS and style isolation
  2. Icestark, iframe scheme, browser native isolation, but there are some problems
  3. Emp, Webpack 5 Module Federation solution
  4. Solutions like WebComponent

But many of the scenarios that implementations solve fall into two categories:

  • Single instance: Only one sub-application exists on the page, usually using Qiankun
  • Multiple instances: The current page has multiple child applications that can be isolated using browser native isolation schemes such as iframe or WebComponent

Of course, this doesn’t mean that single instances can only be used with Qiankun. Browser-native isolation schemes are possible, as long as you accept their limitations:

The most important feature of iframe is that it provides a browser native hard isolation scheme, no matter the style isolation, JS isolation and other problems can be solved perfectly. However, its biggest problem is that its isolation can not be broken, leading to the application context can not be shared, resulting in the development experience, product experience problems.

The above excerpt is from Why Not Iframe.

The implementation scheme in this paper is the same as that in Qiankun, but the functions and principles involved are universal, which is also needed in another implementation scheme.

Pre – work

Before we start, we need to set up the development environment. Here, you can choose the main/sub-application stack. For example, the main application uses React and the sub-application uses Vue. Each application uses the corresponding scaffolding tool to initialize the project, so we don’t need to initialize the project. Remember that if you are in a React project, you need to execute YARN eject again.

I recommend you to directly use the example folder in the author’s warehouse, the configuration is configured, we just need to feel at ease with the author step by step to do the micro front end on the line. In this example, the main application is React and the sub-application is Vue. The resulting directory structure is roughly as follows:

The body of the

Before reading the text, I assume that you have already used the micro-front-end framework and understood the concepts involved, such as that the main application is responsible for the overall layout and the configuration and registration of sub-applications. If you have not already used it, we recommend that you briefly read the documentation for the use of any micro-front-end framework.

Application of registration

After having the master application, we need to register the information of the sub-application in the master application, which contains the following pieces:

  • Name: subapplication noun
  • Entry: Entry of sub-application resources
  • Container: node for the master application to render child applications
  • ActiveRule: Render the child application under which routes

In fact, this information is similar to how we register routes in a project. Entry can be used as a component to render, Container can be used as a node to render routes, and activeRule can be used as a rule to match routes.

Let’s first implement the function to register the child application:

// src/types.ts
export interface IAppInfo {
  name: string;
  entry: string;
  container: string;
  activeRule: string;
}

// src/start.ts
export const registerMicroApps = (appList: IAppInfo[]) = > {
  setAppList(appList);
};

// src/appList/index.ts
let appList: IAppInfo[] = [];

export const setAppList = (list: IAppInfo[]) = > {
  appList = list;
};

export const getAppList = () = > {
  return appList;
};
Copy the code

The above implementation is as simple as saving the appList passed in by the user.

Routing hijacked

Once we have the list of child applications, we need to start the micro front end to render the appropriate child applications, that is, we need to determine the route to render the appropriate application. But before we go any further, we need to consider a question: how do we listen for route changes to determine which subapplications to render?

For non-SPA (single page application) architectures, this is not a problem at all, as we just need to determine the current URL and render the application when launching the micro front end; However, in the SPA architecture, route changes do not trigger page refreshes, so we need a way to know if we need to switch subapplications or do nothing.

If you know the Router library principles, you should immediately come up with a solution. If you don’t know, you can read my previous articles for yourself.

In order to take care of readers who do not understand, the author here first briefly talk about the principle of routing.

Currently, single-page applications use routing in two ways:

  1. Hash mode, which is carried in the URL#
  2. Histroy mode, which is the common URL format

Here are two illustrations to show what events and apis are involved in each mode:

From the above figure, we can see that route changes involve two events:

  • popstate
  • hashchange

So these are two events that we definitely need to monitor. In addition, calls to pushState and replaceState also cause route changes but do not trigger events, so we need to override these functions.

Now that we know what events to listen for and what functions to override, let’s implement the code:

// src/route/index.ts

// Keep the old method
const originalPush = window.history.pushState;
const originalReplace = window.history.replaceState;

export const hijackRoute = () = > {
  // Override the method
  window.history.pushState = (. args) = > {
    // Call the old method
    originalPush.apply(window.history, args);
    // The URL change logic is actually how to handle child applications
    // ...
  };
  window.history.replaceState = (. args) = > {
    originalReplace.apply(window.history, args);
    // URL change logic
    // ...
  };

  // Listen for events that trigger the URL change logic
  window.addEventListener("hashchange".() = > {});
  window.addEventListener("popstate".() = > {});

  / / rewrite
  window.addEventListener = hijackEventListener(window.addEventListener);
  window.removeEventListener = hijackEventListener(window.removeEventListener);
};

const capturedListeners: Record<EventType, Function[] > = {hashchange: [].popstate: [],};const hasListeners = (name: EventType, fn: Function) = > {
  return capturedListeners[name].filter((listener) = > listener === fn).length;
};
const hijackEventListener = (func: Function) :any= > {
  return function (name: string, fn: Function) {
    // Save the callback function if the following events occur
    if (name === "hashchange" || name === "popstate") {
      if(! hasListeners(name, fn)) { capturedListeners[name].push(fn);return;
      } else {
        capturedListeners[name] = capturedListeners[name].filter(
          (listener) = > listener !== fn
        );
      }
    }
    return func.apply(window.arguments);
  };
};
// Used after subsequent rendering of the child application to execute the previously saved callback function
export function callCapturedListeners() {
  if (historyEvent) {
    Object.keys(capturedListeners).forEach((eventName) = > {
      const listeners = capturedListeners[eventName as EventType]
      if (listeners.length) {
        listeners.forEach((listener) = > {
          // @ts-ignore
          listener.call(this, historyEvent)
        })
      }
    })
    historyEvent = null}}Copy the code

The above code looks at many lines, but what it actually does is very simple, divided into the following steps:

  1. rewritepushStateAs well asreplaceStateMethod, in which the original method is called, how to execute the logic of the child application
  2. Listening to thehashchangepopstateEvent, how does the event execute the logic to handle the child application
  3. Override the listener/remove event function if the listener is appliedhashchangepopstateThe event stores the callback function for later use

Application life cycle

Now that we have implemented route hijacking, we need to consider how to implement the logic to handle the child application, that is, how to handle the child application loading resources and mount and unload the child application. If you look at this, this looks like a component. Components also need to handle these things and expose the lifecycle for the user to do what they want.

Therefore, for a sub-application, we also need to implement a life cycle, since the sub-application has a life cycle, the main application must also have a life cycle, and must be corresponding to the sub-application life cycle.

So at this point we can sort out the life cycle of the master/child application.

For the main application, there are three life cycles:

  1. beforeLoad: Before mounting the child application
  2. mounted: After mounting the child application
  3. unmounted: Uninstalls sub-applications

Of course, it’s perfectly fine if you want to increase the life cycle, but I’ve only implemented three for simplicity.

For sub-applications, generic is also divided into the following three life cycles:

  1. bootstrap: Triggers the first application loading. It is used to configure the global information of sub-applications
  2. mount: Triggered when an application is mounted, often used to render child applications
  3. unmount: Triggered when an application is uninstalled. It is used to destroy child applications

Next we implement the register main application lifecycle function:

// src/types.ts
export interfaceILifeCycle { beforeLoad? : LifeCycle | LifeCycle[]; mounted? : LifeCycle | LifeCycle[]; unmounted? : LifeCycle | LifeCycle[]; }// src/start.ts
// Rewrite the previous one
export const registerMicroApps = (appList: IAppInfo[], lifeCycle? : ILifeCycle) = > {
  setAppList(appList);
  lifeCycle && setLifeCycle(lifeCycle);
};

// src/lifeCycle/index.ts
let lifeCycle: ILifeCycle = {};

export const setLifeCycle = (list: ILifeCycle) = > {
  lifeCycle = list;
};
Copy the code

Since it is the life cycle of the main application, we register the child application along with it.

Then the life cycle of the child application:

// src/enums.ts
// Set the status of the child application
export enum AppStatus {
  NOT_LOADED = "NOT_LOADED",
  LOADING = "LOADING",
  LOADED = "LOADED",
  BOOTSTRAPPING = "BOOTSTRAPPING",
  NOT_MOUNTED = "NOT_MOUNTED",
  MOUNTING = "MOUNTING",
  MOUNTED = "MOUNTED",
  UNMOUNTING = "UNMOUNTING",}// src/lifeCycle/index.ts
export const runBeforeLoad = async (app: IInternalAppInfo) => {
  app.status = AppStatus.LOADING;
  await runLifeCycle("beforeLoad", app);

  app = awaitLoad sub-application resources; app.status = AppStatus.LOADED; };export const runBoostrap = async (app: IInternalAppInfo) => {
  if(app.status ! == AppStatus.LOADED) {return app;
  }
  app.status = AppStatus.BOOTSTRAPPING;
  awaitapp.bootstrap? .(app); app.status = AppStatus.NOT_MOUNTED; };export const runMounted = async (app: IInternalAppInfo) => {
  app.status = AppStatus.MOUNTING;
  awaitapp.mount? .(app); app.status = AppStatus.MOUNTED;await runLifeCycle("mounted", app);
};

export const runUnmounted = async (app: IInternalAppInfo) => {
  app.status = AppStatus.UNMOUNTING;
  awaitapp.unmount? .(app); app.status = AppStatus.NOT_MOUNTED;await runLifeCycle("unmounted", app);
};

const runLifeCycle = async (name: keyof ILifeCycle, app: IAppInfo) => {
  const fn = lifeCycle[name];
  if (fn instanceof Array) {
    await Promise.all(fn.map((item) = > item(app)));
  } else {
    await fn?.(app);
  }
};
Copy the code

The above code looks a lot, the actual implementation is very simple, to sum up is:

  • Set the sub-application state for logical determination and optimization. For example, when an application is in a state of notNOT_LOADEDEach application starts withNOT_LOADEDThe next time you render the application, you don’t need to reload the resource
  • If you need to deal with logic, for examplebeforeLoadWe need to load the child application resources
  • To execute the life cycle of the primary and child applications, pay attention to the execution sequence. For details, see the life cycle execution sequence of the parent and child components

Perfect route hijacking

With the application lifecycle implemented, we can now refine the “how to handle child applications” logic that was left out of the previous route-hijacking.

This logic is actually quite simple after we complete the life cycle and can be divided into the following steps:

  1. Determines whether the current URL is the same as the previous URL. If so, continue
  2. Use of course URL to match the corresponding sub-application, this is divided into several situations:
    • When you start the microfront for the first time, you only need to render successfully matched child applications
    • If no subapplication is switched, no subapplication needs to be processed
    • Switch sub-applications. At this time, it is necessary to find out the previously rendered sub-applications for unloading, and then render the successfully matched sub-applications
  3. Save the current URL for the next first step

Now that we’ve figured out the steps, let’s implement it:

let lastUrl: string | null = null
export const reroute = (url: string) = > {
  if(url ! == lastUrl) {const{actives, unmounts} = Matches a route and searches for a qualified subapplication// Execute the lifecycle
    Promise.all(
      unmounts
        .map(async (app) => {
          await runUnmounted(app)
        })
        .concat(
          actives.map(async (app) => {
            await runBeforeLoad(app)
            await runBoostrap(app)
            await runMounted(app)
          })
        )
    ).then(() = > {
      // Executes a function not used in the route hijacking section
      callCapturedListeners()
    })
  }
  lastUrl = url || location.href
}
Copy the code

The above body of code is executing the lifecycle functions sequentially, but the function to match the route is not implemented because we need to consider some issues first.

You must have used routing in your daily project development, so you should know that the principle of routing matching is mainly composed of two parts:

  • Nested relations
  • Path to the grammar

Nesting means that if my current route is set to /vue, then something like /vue or /vue/ XXX will match the route unless we set excart (exact match).

Path syntax: path syntax: path syntax

<Route path="/hello/:name">         // Matches /hello/ Michael and /hello/ Ryan<Route path="/hello(/:name)"> /hello/ Michael and /hello/ Ryan <Route path="/files/*.*"> // match /files/hello. JPG and /files/path/to/helloCopy the code

So it seems that route matching is still quite troublesome to implement, so is there an easy way to implement this function? The answer is yes, we can read the Route library source code to find that they use path-to-regexp library inside, interested readers can read the library documentation, I have covered, we will only look at the use of one API.

With this solution in place, let’s quickly implement the following routing matching functions:

export const getAppListStatus = () = > {
  // List of applications to render
  const actives: IInternalAppInfo[] = []
  // List of applications to be uninstalled
  const unmounts: IInternalAppInfo[] = []
  // Get the list of registered child applications
  const list = getAppList() as IInternalAppInfo[]
  list.forEach((app) = > {
    // Match the route
    const isActive = match(app.activeRule, { end: false })(location.pathname)
    // Check the application status
    switch (app.status) {
      case AppStatus.NOT_LOADED:
      case AppStatus.LOADING:
      case AppStatus.LOADED:
      case AppStatus.BOOTSTRAPPING:
      case AppStatus.NOT_MOUNTED:
        isActive && actives.push(app)
        break
      caseAppStatus.MOUNTED: ! isActive && unmounts.push(app)break}})return { actives, unmounts }
}
Copy the code

Don’t forget to call reroute to complete the route-hijacking function. The full code can be read here.

Perfect life cycle

Earlier in the implementation lifecycle, we still had an important step “loading the child application resources” to be completed, which we will take care of in this section.

To load resources, we must first need a resource entry, just like the NPM package we used, each package must have an entry file. Back to the registerMicroApps function, we initially passed the entry parameter to the function, which is the resource entry for the child application.

Resource entry is actually divided into two schemes:

  1. JS Entry
  2. HTML Entry

Both schemes are literal; the former loads all static resources through JS, while the latter loads all static resources through HTML.

JS Entry is one approach used in single-SPA. But it’s a bit restrictive, requiring the user to bundle all the files together, and unless your project isn’t performance sensitive, you can probably pass it.

HTML Entry is much better, because all websites use HTML as their Entry file. In this solution, we basically don’t need to change the packaging method, it’s almost non-invasive to the user development, we just need to find the static resources in THE HTML to load and run to render the child application, so we chose this solution.

Let’s start implementing this section.

Load resources

First we need to fetch the HTML content, here we just call the native fetch to fetch it.

// src/utils
export const fetchResource = async (url: string) = > {return await fetch(url).then(async (res) => await res.text())
}
// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const htmlFile = await fetchResource(entry)

  return app
}
Copy the code

In my repository example, after we switch the route to /vue, we can print out the contents of the loaded HTML file.

<! DOCTYPEhtml>
<html lang="">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <link rel="icon" href="/favicon.ico">
    <title>sub</title>
  <link href="/js/app.js" rel="preload" as="script"><link href="/js/chunk-vendors.js" rel="preload" as="script"></head>
  <body>
    <noscript>
      <strong>We're sorry but sub doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
    </noscript>
    <div id="app"></div>
    <! -- built files will be auto injected -->
  <script type="text/javascript" src="/js/chunk-vendors.js"></script>
  <script type="text/javascript" src="/js/app.js"></script></body>
</html>
Copy the code

We can see several static resource urls with relative paths in this file, which we then need to load. However, it is important to note that these resources can only be correctly loaded under their own BaseURL. If they are loaded under the main application’s BaseURL, a 404 error must be reported.

One more thing to note is that since we are loading the child app’s resources at the URL of the main app, this can trigger cross-domain restrictions. Therefore, it is important to pay attention to cross-domain processing in both development and production environments.

For example, if the development environment subapplication is Vue, the way to handle cross-domain:

// vue.config.js
module.exports = {
  devServer: {
    headers: {
      'Access-Control-Allow-Origin': The '*',,}}}Copy the code

Next, we need to process the paths of these resources first, concatenate the relative paths into the correct absolute paths, and then fetch.

// src/utils
export function getCompletionURL(src: string | null, baseURI: string) {
  if(! src)return src
  // If the URL is already at the beginning of the protocol, return it directly
  if (/^(https|http)/.test(src)) return src
	// Concatenate urls using native methods
  return new URL(src, getCompletionBaseURL(baseURI)).toString()
}
// Get the complete BaseURL
// Because the user may enter // XXX or https://xxx in the application entry format
export function getCompletionBaseURL(url: string) {
  return url.startsWith('/ /')?`${location.protocol}${url}` : url
}
Copy the code

I don’t need to describe the function of the above code, the annotation is very detailed, next we need to find the resources in the HTML file and fetch.

To find the resource, we need to parse the HTML content:

// src/loader/parse.ts
export const parseHTML = (parent: HTMLElement, app: IInternalAppInfo) = > {
  const children = Array.from(parent.children) as HTMLElement[]
  children.length && children.forEach((item) = > parseHTML(item, app))

  for (const dom of children) {
    if (/^(link)$/i.test(dom.tagName)) {
      / / processing link
    } else if (/^(script)$/i.test(dom.tagName)) {
      / / processing script
    } else if (/^(img)$/i.test(dom.tagName) && dom.hasAttribute('src')) {
      // Process the image, after all, the image resource must also use the relative path 404
      dom.setAttribute(
        'src',
        getCompletionURL(dom.getAttribute('src')!, app.entry)!
      )
    }
  }

  return{}}Copy the code

Parsing content is easy, we recursively look for elements, link, script, img elements to find out and do the corresponding processing.

First let’s see how we handle link:

// src/loader/parse.ts
// Complete parseHTML logic
if (/^(link)$/i.test(dom.tagName)) {
  const data = parseLink(dom, parent, app)
  data && links.push(data)
}
const parseLink = (link: HTMLElement, parent: HTMLElement, app: IInternalAppInfo) = > {
  const rel = link.getAttribute('rel')
  const href = link.getAttribute('href')
  let comment: Comment | null
  // Determine whether to obtain CSS resources
  if (rel === 'stylesheet' && href) {
    comment = document.createComment(`link replaced by micro`)
    // @ts-ignore
    comment && parent.replaceChild(comment, script)
    return getCompletionURL(href, app.entry)
  } else if (href) {
    link.setAttribute('href', getCompletionURL(href, app.entry)!) }}Copy the code

When dealing with link tags, we only need to deal with CSS resources, other resources of preload/prefetch simply replace href.

// src/loader/parse.ts
// Complete parseHTML logic
if (/^(link)$/i.test(dom.tagName)) {
  const data = parseScript(dom, parent, app)
  data.text && inlineScript.push(data.text)
  data.url && scripts.push(data.url)
}
const parseScript = (script: HTMLElement, parent: HTMLElement, app: IInternalAppInfo) = > {
  let comment: Comment | null
  const src = script.getAttribute('src')
  // A SRC file is a JS file. A SRC file is an inline script
  if (src) {
    comment = document.createComment('script replaced by micro')}else if (script.innerHTML) {
    comment = document.createComment('inline script replaced by micro')}// @ts-ignore
  comment && parent.replaceChild(comment, script)
  return { url: getCompletionURL(src, app.entry), text: script.innerHTML }
}
Copy the code

When dealing with script tags, we need to distinguish between JS files and in-line code, which also requires fecth to fetch the content once.

Then we return all parsed scripts, links, inlineScript in parseHTML.

Next we load the CSS and then the JS file in order:

// src/loader/index.ts
export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const fakeContainer = document.createElement('div')
  fakeContainer.innerHTML = htmlFile
  const { scripts, links, inlineScript } = parseHTML(fakeContainer, app)

  await Promise.all(links.map((link) = > fetchResource(link)))

  const jsCode = (
    await Promise.all(scripts.map((script) = > fetchResource(script)))
  ).concat(inlineScript)

  return app
}
Copy the code

Above we have realized from loading HTML files to parsing files to find all static resources to loading CSS and JS files. But in fact our implementation is still a little rough, although the core content is implemented, but there are still some details that are not considered.

Therefore, we can also consider directly using the three-party library to realize the process of loading and parsing files. Here, we choose the import-HTml-entry library, which does the same internal things as our core, but deals with a lot of details.

If you want to use the library directly, you can modify loadHTML to look like this:

export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  // template: processed HTML content
  // getExternalStyleSheets: fetch CSS file
  // getExternalScripts: fetch JS file
  const { template, getExternalScripts, getExternalStyleSheets } =
    await importEntry(entry)
  const dom = document.querySelector(container)

  if(! dom) {throw new Error(Container does not exist)}// Mount HTML to the microfront-end container
  dom.innerHTML = template
  // Load the file
  await getExternalStyleSheets()
  const jsCode = await getExternalScripts()

  return app
}
Copy the code

Run JS

Once we have all the JS content, it’s time to run the JS. Once we’ve done this, we can see the child application rendered on the page.

In this section, we’ll start with the easy part, which is how to run JS.

There are roughly two ways we want to execute a JS string:

  1. eval(js string)
  2. new Function(js string)()

Here we choose the second way to achieve:

const runJS = (value: string, app: IInternalAppInfo) = > {
  const code = `
    ${value}
    return window['${app.name}']
  `
  return new Function(code).call(window.window)}Copy the code

I don’t know if you remember when we registered our children we gave each child a name property, which is actually very important, and we’ll use it in the future. In addition, when setting the name of the child app, you also need to change the packaging configuration slightly, and set one of the options to the same content.

For example, if we set name: Vue to one of the sub-applications whose technology stack is Vue, then we also need to set the following in the packaging configuration:

// vue.config.js
module.exports = {
  configureWebpack: {
    output: {
      // Same as name
      library: `vue`}},}Copy the code

After configuration, we can access the contents of the application’s JS entry file export via window.vue:

As you can see in the figure above, these exported functions are the life cycle of the child application, and we need to take these functions and call them.

Finally, we call runJS in loadHTML and we’re done:

export const loadHTML = async (app: IInternalAppInfo) => {
  const { container, entry } = app

  const { template, getExternalScripts, getExternalStyleSheets } =
    await importEntry(entry)
  const dom = document.querySelector(container)

  if(! dom) {throw new Error(Container does not exist)
  }

  dom.innerHTML = template

  await getExternalStyleSheets()
  const jsCode = await getExternalScripts()

  jsCode.forEach((script) = > {
    const lifeCycle = runJS(script, app)
    if (lifeCycle) {
      app.bootstrap = lifeCycle.bootstrap
      app.mount = lifeCycle.mount
      app.unmount = lifeCycle.unmount
    }
  })

  return app
}
Copy the code

After completing the above steps, we should see the child application render normally!

However, this is not the end of the step, let us consider the question: ** child application changes the global variable how? ** All of our current applications can get and change the content on the Window, so global variable conflicts between applications can cause problems, so we need to fix this next.

JS sandbox

If we want to prevent the child application from directly modifying the properties of the window and also want to access the contents of the Window, then we have to make a fake window for the child application, that is, to implement a JS sandbox.

There are many ways to implement a sandbox, such as:

  1. The snapshot
  2. Proxy

In fact, this scheme is very simple to implement. In other words, it is easy to record all the contents of the current Window before mounting the child application, and then let the child application play randomly until the unmount child application restore the window before mounting. This solution is easy to implement, but the only drawback is that the performance is slow. Readers who are interested can directly see the implementation of Qiankun without Posting the code here.

Let’s talk about Proxy again, which is also the solution we choose. Many readers should have known how to use it, after all, Vue3 responsive principle is said to be rotten. If you don’t already know it, you can read the MDN documentation for yourself.

export class ProxySandbox {
  proxy: any
  running = false
  constructor() {
    // Create a false window
    const fakeWindow = Object.create(null)
    const proxy = new Proxy(fakeWindow, {
      set: (target: any, p: string, value: any) = > {
        // If the current sandbox is running, set the value directly to fakeWindow
        if (this.running) {
          target[p] = value
        }
        return true
      },
      get(target: any.p: string) :any {
        // Prevent users from skipping classes
        switch (p) {
          case 'window':
          case 'self':
          case 'globalThis':
            return proxy
        }
        // If the property does not exist on fakeWindow, but exists on window
        // Take the value from window
        if(!window.hasOwnProperty.call(target, p) &&
          window.hasOwnProperty(p)
        ) {
          // @ts-ignore
          const value = window[p]
          if (typeof value === 'function') return value.bind(window)
          return value
        }
        return target[p]
      },
      has() {
        return true}})this.proxy = proxy
  }
  // Activate the sandbox
  active() {
    this.running = true
  }
  // Deactivate the sandbox
  inactive() {
    this.running = false}}Copy the code

The above code is just a first version of the sandbox, the core idea is to create a fakeWindow, if the user set the value of the fakeWindow, so that it does not affect the global variable. If the user takes the value, it determines whether the property exists on fakeWindow or Window.

Of course, we still need to improve the actual use of the sandbox, but also need to deal with some details, here we recommend you directly read the source code of Qiankun, the code is not much, nothing more than a lot of processing boundary cases.

Note also that both snapshots and Proxy sandboxes are needed, but the former is a degraded version of the latter. After all, not all browsers support Proxy.

Finally, we need to modify the runJS code to use the sandbox:

const runJS = (value: string, app: IInternalAppInfo) = > {
  if(! app.proxy) { app.proxy =new ProxySandbox()
    // Hang the sandbox on the global properties
    // @ts-ignore
    window.__CURRENT_PROXY__ = app.proxy.proxy
  }
  // Activate the sandbox
  app.proxy.active()
  // Call JS with sandbox instead of global environment
  const code = `
    return (window => {
      ${value}
      return window['${app.name}']
    })(window.__CURRENT_PROXY__)
  `
  return new Function(code)()
}
Copy the code

At this point, we have actually completed the core functions of the entire micro front end. Because the text expression is difficult to coherent context of all the steps to improve the function, so if you are not on the reading article, or recommended to see the source of the author’s warehouse.

Now we’re going to do some improvements.

Improved function

prefetch

Our current approach is to match a child application and then load the child application, which is not efficient. We prefer that the user load up all the other child apps while browsing the current one, so that the user doesn’t have to wait to switch apps.

There is not much code to implement, and we can use our previous import-html-entry to do it immediately:

// src/start.ts
export const start = () = > {
  const list = getAppList()
  if(! list.length) {throw new Error('Please register the application first')
  }

  hijackRoute()
  reroute(window.location.href)

  // Prefetch is required only for children whose state is NOT_LOADED
  list.forEach((app) = > {
    if ((app as IInternalAppInfo).status === AppStatus.NOT_LOADED) {
      prefetch(app as IInternalAppInfo)
    }
  })
}
// src/utils.ts
export const prefetch = async (app: IInternalAppInfo) => {
  requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(
      app.entry
    )
    requestIdleCallback(getExternalStyleSheets)
    requestIdleCallback(getExternalScripts)
  })
}
Copy the code

There is little else to say about the code above, except to talk about the requestIdleCallback function.

* * * * window. RequestIdleCallback () method will be called function in browser free time line. This enables developers to perform background and low-priority work on the main event loop without affecting the delay of critical events such as animations and input responses.

We use this function to perform prefetch when the browser is idle. This function is also useful in React, except that it implements a polyfill version internally. Since there are some issues with the API (50ms at best) that haven’t been resolved, but in our scenario that won’t be a problem, you can use it directly.

Resource caching mechanism

When we load resources once, users certainly do not want to load resources again when they enter the application next time, so we need to implement the caching mechanism of resources.

Since we used import-html-entry in the previous section, we have built-in caching. If you want to implement it yourself, you can refer to the internal implementation.

In simple terms, it is to make an object cache under each request of the file content, the next request to determine whether there is value in the object, if there is a direct use of the line.

Global communication and status

This part is not implemented in my code, but I can provide some ideas if you are interested in doing it yourself.

Global communication and state are actually completely an implementation of the publish-subscribe model, which should not be a problem as long as you write events by hand.

You can also read the global state implementation of Qiankun, which is only 100 lines of code.

The last

Here is the end of the article, the whole article nearly ten thousand words, read down may be many readers will have some doubts, you can choose to read a few times or combined with the author of the source code to read.

In addition, you can also ask questions in the communication area, and I will answer them in my spare time.

Author: yck

Warehouse: making

Public number: the front end is really fun

Special statement: the original is not easy, without authorization shall not be reproduced or copied, if you need to reproduce can contact the author authorized