preface

The practice and summary of qiankun’s Micro front End Scheme also summarized many “pothpoints” and solutions of Qiankun. This article is a supplement to the previous one. I wanted to edit it directly, but it did not expect to trigger the maximum length limit of the article content:

So I had to reorganize, record some pits and research, hoping to be helpful to everyone. This paper is based on the 2.x version of Qiankun.

Potential pits and solutions

Some problems were discovered and solved by themselves, and some were seen in the Issue area of qiankun’s warehouse. For these problems, I try my best to explain the causes and solutions and methods.

Failed to load subproject font file

qiankunFor subprojectsjs/cssThe processing of

I said earlier: After requesting the index.html of the sub-project, Qiankun would first match the JS/CSS tags with the regex, and then replace them. It needed to load JS/CSS and run by itself, and then remove HTML /head/body tags. The rest of the content is inserted into the subproject’s container as is:

forjs ( <script>Label)

The contents of an inline JS are recorded directly to an object, while the contents of an external js fetch (string) are then recorded to the object.

if (isInlineCode(script)) {
    return getInlineCode(script);
} else {
    return fetchScript(script);
}

const fetchScript = scriptUrl= > scriptCache[scriptUrl] ||
		(scriptCache[scriptUrl] = fetch(scriptUrl).then(response= > response.text()));
Copy the code

To run the subproject, execute these js:

/ / inline js
eval(`; (function(window){;${inlineScript}\n}).bind(window.proxy)(window.proxy); `)
/ / js outside the chain
eval(`; (function(window){;${downloadedScriptText}\n}).bind(window.proxy)(window.proxy); `))
Copy the code

One difficulty in loading and running external js is how to ensure the correct order of js execution.

The async and defer properties for the

  1. defer: equivalent to the outer chainjsIt’s at the bottom of the page
  2. async: executes asynchronously relative to the rest of the page, as soon as it’s loaded. Commonly used inGoogle Analytics

Therefore, as long as the outer chain JS can distinguish whether async exists or not,

Suppose the HTML has the following js tags:

<script src="./a.js" onload="console.log('a load')"></script>
<script src="./b.js" onload="console.log('b load')"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.5.1/jquery.js"></script>
<script src="./c.js" onload="console.log('c load')"></script>
Copy the code

The normal browser logic for loading and executing is to load in parallel, but execute sequentially, but execute first as soon as the first load is ready. If the third load is not complete, the fourth load will not be executed first.

As shown in the figure, I limit the network speed to 5KB /s. The third JS has not been loaded yet, but the fourth JS has been loaded but will not be executed.

Qiankun loads in parallel, but executes sequentially after all js loads are complete. It’s a little different from the browser’s native load order, but the effect is the same.

There is a little room for optimization: as long as the js in front of it is loaded and executed, it can be executed as soon as it is loaded, without waiting for the JS behind it to finish loading.

forcss ( <style><link>Label)

The loading logic remains the same: the contents of an inline CSS (

However, execution is “similar” to js: content is placed in

This will change the CSS from the outer chain to the inline CSS. The advantage is that you can switch subsystems and apply CSS styles directly without repeated requests, making the sub-projects load faster.

But there is a hidden trap. If you use a font file in your CSS, and it’s a relative path, which is the path relative to the CSS linked to the link, and now it’s inline, and the relative path is the path relative to the index.html, you’ll get a font file 404.

Even worse, development mode does not have this problem. In development mode, the path will be injected into publicPath and will have this problem after packaging.

How to solve the problem of font file loading failure of subproject

Although the problem was caused by the sub-project being changed to

The root cause is that font files are webpacked but not injected with path prefixes. So modify webpack configuration, let the font file through urL-loader processing, packaging into Base64, can solve this problem.

Add the following to the webpack configuration of the subproject:

module.exports = {
  chainWebpack: (config) = > {
    config.module
      .rule('fonts')
      .test(/.(ttf|otf|eot|woff|woff2)$/)
      .use('url-loader')
      .loader('url-loader')
      .options({})
      .end()
  },
}
Copy the code

A review of the source code shows that VUE-CLI4 does the same, but it limits fonts up to 4KB to being packaged as Base64.

Subprojects are deployed in a secondary directory

To deploy a vUE project to a secondary directory, publicPath must be configured.

Then, it is necessary to pay attention to the entry address entry when registering sub-projects.

Assuming that the subproject is deployed in the app-vue-hash directory, writing entry directly to http://localhost/app-vue-hash will cause an error in obtaining publicPath for qiankun. The relative path of the subproject entry address http://localhost/app-vue-hash is http://localhost, and the relative path we want for the subproject is http://localhost/app-vue-hash. In this case, we only need to write http://localhost/app-vue-hash/, the final/can not be omitted.

Qiankun Obtain publicPath

function defaultGetPublicPath(url) {
  try {
    // URL constructors do not support urls with // prefixes
    const { origin, pathname } = new URL(url.startsWith('/ /')?`${location.protocol}${url}` : url, location.href);
    const paths = pathname.split('/');
    // Remove the last element
    paths.pop();
    return `${origin}${paths.join('/')}/ `;
  } catch (e) {
    console.warn(e);
    return ' '; }}Copy the code

Through testing, we can find that http://localhost/app and http://localhost/app/ are two servers with different paths and the same HTML, and then introduce a resource with a relative path into the HTML. The addresses resolved by the browser are:

It shows that the processing of Qiankun’s publicPath acquisition is correct.

Compatibility with Internet Explorer 11

Fetch was used in the loading of sub-projects in Qiankun, so fetch polyfill needed to be introduced for IE11. However, when fetch polyfill was introduced, errors were reported under IE11.

The error appears to be single-spa.min.js, but this code is compressed, so it is not easy to troubleshoot.

Go to node_modules\ single-spa-package. json, go to “module”: “lib/esm/single-spa.min.js”, change it to “module”: “Lib /esm/single-spa.dev.js” and restart the project.

It was found that single-SPA reported an error because the APP failed to load. The code is as follows:

function handleAppError(err, app, newStatus) {
  var transformedErr = transformErr(err, app, newStatus);
  if (errorHandlers.length) {
    errorHandlers.forEach(function (handler) {
      return handler(transformedErr);
    });
  } else {
    setTimeout(function () {
      throw transformedErr; // This line throws an error message that is displayed on the console}); }}Copy the code

The error information of APP loading is printed as follows:

After investigation, it was the fetch’s Polyfill that reported the error.

However, simply introducing fetch-polyfill and then using fetch in your project will not report this error.

An error was reported when qiankun and Fetch -polyfill were introduced simultaneously:

import 'fetch-polyfill';
import { registerMicroApps, start } from 'qiankun';

console.log(fetch);
console.log(fetch("http://localhost:1111"));
Copy the code

I have been investigating the incompatibility of these two plugins for a long time. Finally, the qiankun author’s solution was to use WHATWG-Fetch and then display polyfills listing Promise, symbol, etc.

Just import the following in the main project entry file (remember to install dependencies) :

import 'whatwg-fetch';
// import 'custom-event-polyfill'; // If an error is reported, we need to introduce this
import 'core-js/stable/promise';
import 'core-js/stable/symbol';
import 'core-js/stable/string/starts-with';
import 'core-js/web/url';
Copy the code

IE11 can run perfectly, but there will be some network related error, does not affect the page running, should be caused by websocket, production environment will not report error.

How to gracefully integrate IE 2020

vueSub-project memory leak problem

This problem is very difficult to find in the Issue area of Qiankun: github.com/umijs/qiank… I’ll skip the screening process. The solution is simple.

Empty the DOM when the subproject is destroyed:

export async function unmount() {
  instance.$destroy();
+ instance.$el.innerHTML = ""; // Add this line of code
  instance = null;
  router = null;
}
Copy the code

However, switching back and forth between subprojects does not increase memory. That is, even if the memory occupied by the subproject is not freed when the subproject is unloaded, the memory will be reused the next time it is loaded. Will the subproject load faster? (Not verified yet)

Special needs exploration and thinking

After meeting the basic use of micro front-end, some optimization and special needs are investigated to facilitate better use.

keep-alivedemand

The keep-alive subitem is intended not to be unloaded when the subitem is switched, but simply to hide the style (display: None) so that it will be opened faster next time.

Keep-alive needs to be used with caution as multiple subprojects are loaded and running at the same time, which increases the risk of JS/CSS contamination.

The JS sandbox in Qiankun’s proxy mode can perfectly support multiple projects, but don’t forget the cancer of IE11. The SANDbox in IE11 uses the diff method, which makes multiple projects share a sandbox, which means there is no sandbox. There may also be conflicts between routes.

The multi-project CSS sandbox doesn’t have a particularly good handle either. The best option is class namespace + CSS-scoped.

There are several ways to fulfill keep-alive requirements. Solution 1 is recommended.

Plan 1: with helploadMicroAppfunction

Try implementing the keep-alive requirement using its existing API: LoadMicroApp function is used to manually load and uninstall the subproject. Generally, the TAB page that has keep-alive requirements is the TAB page. When a new TAB page is added, the subproject will be loaded, and when the TAB page is closed, the subproject will be unloaded.

Since there was no TAB page in the demo, I just loaded all the subprojects and saw what they looked like. There weren’t many changes.

Main project app. vue file:

<template>
  <div id="app">
    <header>
      <router-link to="/app-vue-hash/">app-vue-hash</router-link>
      <router-link to="/app-vue-history/">app-vue-history</router-link>
      <router-link to="/about">about</router-link>
    </header>
    <div id="appContainer1" v-show="$route.path.startsWith('/app-vue-hash/')"></div>
    <div id="appContainer2" v-show="$route.path.startsWith('/app-vue-history/')"></div>
    <router-view></router-view>
  </div>
</template>

<script>
import { loadMicroApp } from 'qiankun';

const apps = [
  { 
    name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer1'.props: { data : { store, router } }
  },
  { 
    name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer2'.props: { data : store }
  }
]

export default {
  mounted() {
    // Load the current subproject first
    const path = this.$route.path;
    const currentAppIndex = apps.findIndex(item= > path.includes(item.name));
    if(currentAppIndex ! = = -1) {const currApp = apps.splice(currentAppIndex, 1) [0];
      apps.unshift(currApp);
    }
    // loadMicroApp returns an array of app lifecycle functions
    const loadApps = apps.map(item= > loadMicroApp(item))
    // When the TAB page is closed, call app unmount in loadApps}},</script>
Copy the code

The DOM of the subproject is not cleared:

Plan 2: ModifyqiankunThe source code

It is also relatively simple to implement: subsystem unload does not empty the DOM in the container or unload the Vue instance, hidden with display: None. When the subsystem is loaded, determine whether the container has content first, and do not reinsert the HTML of the subsystem if it already exists.

There are four main steps:

  1. Modify the subprojectrenderFunction, not instantiated repeatedlyvue
function render() {
  if(! instance){ router =new VueRouter({
      base: window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/'.mode: 'history',
      routes,
    });
    instance = new Vue({
      router,
      store,
      render: h= > h(App),
    }).$mount('#appVueHistory'); }}Copy the code
  1. Modify the subprojectunmountLife cycle, subprojectsunmountWhen unloadingvueThe instance
export async function unmount() {
  // instance.$destroy();
  // instance = null;
  // router = null;
}
Copy the code
  1. Change the registration and container of the subproject of the main project, and put a separate container for each subproject (of course you can also put a container to deal with the problem). Then it’s all about switching subsystems to hide the rest
<div id="appContainer1" v-show="$route.path && $route.path.startsWith('/app-vue-hash')"></div>
<div id="appContainer2" v-show="$route.path && $route.path.startsWith('/app-vue-history')"></div>
Copy the code
registerMicroApps([
  {
    name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer1'.activeRule: '/app-vue-hash'.props: { data : { store, router } }
  },
  { 
    name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer2'.activeRule: '/app-vue-history'.props: { data : store }
  },
]);
Copy the code
  1. Modify theqiankunSource: subproject loading before the first judgment there is no content, content is not processed; The subsystem is not cleared during uninstallationdom

This plugin can be used to modify the qiankun/es/loader.js file as follows:

Modify qiankun/es/sandbox/patchers/dynamicHeadAppend js:

At this point, it can realize the effect of switching subitems without clearing and entering second loading next time:

This scheme is only for reference to deepen the understanding of Qiankun.

Scheme 3: cache subprojectdom

Source: Issue of Qiankun Warehouse

Caches dom of vue instance and modifs entry file of subproject:

let instance = null;
let router = null;

function render() {
  // A new route instance must be created, otherwise it cannot respond to URL changes.
  router = new VueRouter({
    mode: 'hash'.base: process.env.BASE_URL,
    routes
  });

  if (window.__POWERED_BY_QIANKUN__ && window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__) {
    const cachedInstance = window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__;

    // Get _vnode from the original Vue instance
    const cachedNode =
      // (cachedInstance.cachedInstance && cachedInstance.cachedInstance._vnode) ||
      cachedInstance._vnode;

    // Make the current route available on the original Vue instancerouter.apps.push(... cachedInstance.catchRoute.apps); instance =new Vue({
      router,
      store,
      render: () = > cachedNode
    });

    // Cache the initial Vue instance
    instance.cachedInstance = cachedInstance;

    router.onReady(() = > {
      const { path } = router.currentRoute;
      const { path: oldPath } = cachedInstance.$router.currentRoute;
      // If the current route is inconsistent with the last uninstallation, switch to the new route
      if (path !== oldPath) {
        cachedInstance.$router.push(path);
      }
    });
    instance.$mount('#appVueHash');
  } else {
    console.log('Normal instantiation');
    // Instantiate normally
    instance = new Vue({
      router,
      store,
      render: h= > h(App)
    }).$mount('#appVueHash'); }}if (!window.__POWERED_BY_QIANKUN__) {
  render();
}

export async function bootstrap() {
  console.log('[vue] vue app bootstraped');
}

export async function mount(props) {
  console.log('[vue] props from main framework', props);
  render();
}

export async function unmount() {
  console.log('[vue] vue app unmount');
  const cachedInstance = instance.cachedInstance || instance;
  window.__CACHE_INSTANCE_BY_QIAN_KUN_FOR_VUE__ = cachedInstance;
  const cachedNode = cachedInstance._vnode;
  if(! cachedNode.data.keepAlive) cachedNode.data.keepAlive =true;
  cachedInstance.catchRoute = {
    apps: [...instance.$router.apps]
  }
  instance.$destroy();
  router = null;
  instance.$router.apps = [];
}
Copy the code

Reuse common dependencies (scenarios)

Externals of webpack must be configured for subprojects to reuse common dependencies. Once this is configured, when the subprojects run independently, these dependencies will come from only the script tags in index.html.

There are two cases:

  • Dependency “reuse” between subprojects

This is easy to do, you just need to make sure the dependent urls are consistent. For example, subproject A uses VUE, and subproject B also uses the same version of VUE. If both projects use the same CND file, the load will be read from the cache first:

const fetchScript = scriptUrl= > scriptCache[scriptUrl] ||
	(scriptCache[scriptUrl] = fetch(scriptUrl).then(response= > response.text()));
Copy the code
  • Subprojects reuse main project dependencies

Just add the ignore attribute (a custom, non-standard attribute) to the script and link tags that are common dependencies in the index.html subproject.

With this property, qiankun would no longer load the JS/CSS. The sub-projects would run independently and the JS/CSS would still be loaded. In this way, “sub-projects reuse the dependency of the main project”.

<link ignore rel="stylesheet" href="//cnd.com/antd.css">
<script ignore src="//cnd.com/antd.js"></script>
Copy the code

Note that if the primary project uses externals, the subproject can reuse its dependencies, but the subproject that does not reuse dependencies will report an error.

Uncaught TypeError: Cannot re-define property: $Router

Error reason

If the subproject is not configured with externals, the Vue in the project is a global variable, but does not belong to the window, so this if does not apply when the subproject is running independently:

if (inBrowser && window.Vue) {
  window.Vue.use(VueRouter)
}
Copy the code

When qiankun ran the sub-project, she first looked for the window of the sub-project, then for the window of the parent project, and found Vue on the window. The parent Vue of the window project has VueRouter installed, and the child project’s own global Vue has not been installed.

Solution 1: Handle global variables before loading subprojects

Assume that the app-vue-Hash subproject reuses main project dependencies and the app-vue-history subproject does not.

Add the following code when registering subprojects in the main project to solve the problem:

registerMicroApps(apps,
{
  beforeLoad(app){
    if(app.name === 'app-vue-hash') {// If the page is refreshed directly in the app-vue-hash subproject, window.Vue2 is undefined
      // Check whether window.Vue2 exists
      if(window.Vue2){
        window.Vue = window.Vue2; 
        window.Vue2 = undefined; }}else if(app.name === 'app-vue-history') {window.Vue2 = window.Vue; 
      window.Vue = undefined}}});Copy the code

Solution 2: PasspropsTransfer rely on

For the compatibility problem above, the main project can use props to transfer dependencies to subprojects without externals.

When the main project registers, it passes dependencies to subprojects (omitting some unnecessary code) :

import VueRouter from 'vue-router'
registerMicroApps([
  {
    name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer'.activeRule: '/app-vue-hash'.props: { data : { VueRouter } }
  },
]);
Copy the code

The subproject configures externals and externals with ignore:

function render(parent = {}) {
  if(! instance){// When it runs independently, it relies on window.vueroute with its own external chain
    const VueRouter = parent.VueRouter || window.VueRouter; 
    Vue.use(VueRouter);
    router = new VueRouter({
      routes,
    });
    instance = new Vue({
      router,
      store,
      render: h= > h(App),
    }).$mount('#appVueHash'); }}export async function mount(props) {
  render(props.data);
}
Copy the code

Solution 3: Change the dependency names of the main project and subprojects

Change the name of the dependency reused by the main application and its children so that other children that do not reuse the dependency are not affected. Specific changes are:

  1. Modify the child application and the master applicationexternalsConfigure, change the name of the dependency, do not useVue
externals: {
  'vue': 'Vue2' , // This tells Webpack to treat Winodw.vue2 as vue
}
Copy the code
  1. Import external links in the main applicationvue.jsThen, change the name toVue2
<script src="https://unpkg.com/[email protected]/dist/vue.runtime.min.js"></script>
<script>
window.Vue2 = winow.Vue;
window.Vue = undefined;
</script>
Copy the code

Of the three schemes, scheme 3 is recommended, which is more concise and convenient.

Of the main project routehashhistorybattle

Before writing some content is not too comprehensive, recomb once. Divided into three cases for discussion:

Main project routinghistorymodel

If the main project uses history mode, it needs to use Location. pathname to distinguish the different sub-projects, which is also one of the forms recommended by Qiankun. To register a subproject, activeRule only needs to write the path:

registerMicroApps([
   { 
      name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer'.activeRule: '/app-vue-hash',}])Copy the code

Advantages:

  1. Subprojects are availablehistoryMode can also be usedhashMode. In this way, old projects can be directly connected, strong compatibility.
  2. righthashSchema subitems have no impact and do not need to be modified

Disadvantages:

  1. historyMode routing needs to be setbase
  2. Jumps between subprojects need to use the parent project’srouterObject (no<a>The reason why links jump directly is<a>The link refreshes the page.

PushState (null, ‘name’, ‘/app-vue-hash/#/about’) does not refresh the page.

Whether it is the router object of the parent project, or the native history object, the jump is js. There is a slight user experience problem: tabs (

and
) support the browser’s default right click menu, while js does not:

Main project routinghashSchema and subprojects do nothistoryModel routing

This means that the main project and all subprojects are hash mode. In this case, there are two ways to do it:

  1. withpathTo distinguish between subprojects

I don’t want to go over how to do that

Advantages: No need to modify subproject internal code

Cons: Jumping between projects depends on native History objects

  1. withhashTo distinguish between subprojects

This way both the master project and its subprojects take over routing, for example:

  • /#/vue/home: The home page of the vue subproject is loaded, but in fact, the entire route to the home page of the vue subproject is /#/vue/home

  • /#/react/about: the about page of the react subproject will be loaded. Similarly, the full route to the about page of the react subproject is /#/react/about

  • /#/about: The about page of the main project will be loaded

Do this by customizing activeRule:

const getActiveRule = hash= > location= > location.hash.startsWith(hash);
registerMicroApps([
   { 
      name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer'.activeRule: getActiveRule('#/app-vue-hash'),},])Copy the code

You then need to prefix all routes of the subproject with this prefix, or set the root route of the subproject to this prefix.

const routes = [
  {
    path: '/app-vue-hash'.name: 'Home'.component: Home,
    children: [
      // All other routes are written here]}]Copy the code

If the component is a new project is also good, if it is old project, the influence is bigger, the inside of the subprojects routing jump (< the router – the link >, the router. Push (), the router. Repace ()) if you are using the path, you will need to modify, coupled with the prefix, If the name jump is used, there is no change: router.push({name: ‘user’})

Advantages: All jumps between projects can use their own Router object or

directly, without the parent project’s route object or native history object

Disadvantages: Intrusive changes to subprojects, no impact if the project is new.

Main project routinghashPatterns and subprojects havehistoryModel routing

The main project is in hash mode, so the jump between subprojects can only use the native history object. We can use either path or hash to distinguish subprojects:

  1. withpathTo distinguish between subprojects

The main project is history, and the pros and cons are the same.

  • /vue-hash/#/home: it will loadvueThe subprojectshomepage
  • /vue-history/about: it will loadvue-historyThe subprojectsaboutpage
  • /#/about: Loads the main itemaboutpage
  1. withhashTo distinguish between subprojects

This isn’t a good idea, it’s a little unconventional, but you can also use:

  • /home/#/vue: it will loadvueThe subprojectshomepage
  • /#/vue-hash/about: it will loadvue-hashThe subprojectsaboutpage
  • /#/about: Loads the main itemaboutpage

Advantages: no

Disadvantages: Intrusive changes to hash subitems. If it is a new item, it has no impact.

conclusion

Both hash and history modes can be used for the main project route.

Component sharing between projects

Component sharing: The NPM package is preferred. However, if private NPM is not deployed and the project cannot be put on GitHub because of privacy issues, or the service component with data can be shared in the following ways.

Component sharing between parent and child projects

Since the main project is loaded first and then the subprojects are loaded, it is generally the case that the subprojects reuse the components of the main project (the reuse of the subprojects by the main project is discussed below).

When the main project loads, mount the component to the window and register the subproject directly.

Main Project Entry File:

import HelloWorld from '@/components/HelloWorld.vue'
window.commonComponent = { HelloWorld };
Copy the code

Subprojects directly use:

components: { 
  HelloWorld: window.__POWERED_BY_QIANKUN__ ? window.commonComponent.HelloWorld :
     import('@/components/HelloWorld.vue'))}Copy the code

Component sharing between subprojects (Weak dependence)

What is weak dependency? Even the subproject itself has this component. When the other subproject has loaded, it reuses their component. If the other subproject has not loaded, it uses its own component.

The scenario is to avoid reloading components that may not be global but are only used by a page. There are three steps:

  1. Since global variables are not shared between subprojects, the main project provides a global variable to hold components that are passed to subprojects that need to share components, via props.

  2. The subproject takes this variable and mounts it to the window

export async function mount(props) {
  window.commonComponent = props.data.commonComponent;
  render(props.data);
}
Copy the code
  1. The shared components in the subproject are written as asynchronous components
components: {
   HelloWorld: () = > {
      if(!window.commonComponent){
        // Independent runtime
        window.commonComponent = {};
      }
      const HelloWorld = window.commonComponent.HelloWorld;
      return HelloWorld || (window.commonComponent.HelloWorld =
             import('@/components/HelloWorld.vue')); }}Copy the code

There is a bug: styles for shared components do not load when switching back and forth. I feel that this bug is the same as “sub-project jumps to main project page, main project style does not load”, there is no good solution for the moment.

Another subproject can also be written this way, as long as one subproject is loaded once, the other project can be reused directly, without reloading.

Component sharing between subprojects (Strong dependence on)

Unlike weak dependencies, a subproject does not have the component itself, so another subproject must be loaded before it can get it.

So how do you ensure the loading order between subprojects? Solution: Manually load another subproject before the subproject uses this component, and ensure that the component is always available (if you have permissions, loading will fail without permissions)

Qiankun can also use loadMicroApp to manually load subprojects. The basic steps are as follows:

  1. Also, since global variables are not shared between subprojects, the main project provides a global variable to hold components throughpropsTo subprojects that use the component, and also toloadMicroAppThe function passes.
const commonComponent = {};
registerMicroApps(
  [
    { 
      name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer'.activeRule: '/app-vue-hash'.props: { data : { loadMicroApp, commonComponent } }
    },
  ],
)
Copy the code
  1. Subprojects mount load functions and public variables globally
export async function mount(props) {
  window.commonComponent = props.data.commonComponent;
  window.loadMicroApp = props.data.loadMicroApp;
  render();
}
Copy the code
  1. The subproject manually loads the subproject that provides the component in the page that needs to use the component, and when it is loaded, it can get the component.

Note here: Since the subproject that provides the component is loaded by loadMicroApp, the public variable that holds the component must be passed by loadMicroApp. Also: You need to provide a container to load the subproject. Just provide a hidden container in app.vue.

const app = window.loadMicroApp({
  name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer2'.props: { data: { commonComponent: window.commonComponent } }
})
await app.mountPromise;
Copy the code

Because components are heavily dependent, subprojects cannot run independently here. There are two options for registering components:

  • One is to use asynchronous components:
components: {
   HelloWorld: async() = > {// This app is best written as a global variable for the current page
      // Because unmount the current page requires calling its' unmount 'to unload the subproject
      const app = window.loadMicroApp({
        name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer2'.props: { data: { commonComponent: window.commonComponent } }
      })
      await app.mountPromise;
      return window.commonComponent.HelloWorld
   }
}
Copy the code
  • The second is dynamic registration, first usev-ifTo hide the tag:
<HelloWorld v-if="loadingEnd"/>
Copy the code
async created() {
  const app = window.loadMicroApp({
    name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer2'.props: { data: { commonComponent: window.commonComponent } }
  })
  await app.mountPromise;
  Vue.component('HelloWorld'.window.commonComponent.HelloWorld)
  this.loadingEnd = true;
},
Copy the code

How do vUE documents handle load state of asynchronous components: Handle load state

  1. Another subproject shares components, which is important to noteloadMicroAppIt is independent of the route, so shared components must be mounted on public variables in the entry file, not on the route page.

If the shared component and its children do not depend on global plug-ins such as Store and I18N, there is a speculative treatment: Vue is not instantiated, only the component is mounted.

export async function mount(props) {
  // If there is a commonComponent variable, another subitem is loaded via loadMicroApp
  // He only needs to mount the component
  if(props.data.commonComponent){
     props.data.commonComponent.HelloWorld = HelloWorld;
  }else{
  // There is no commonComponent variable, indicating that the main project is loaded through registerMicroApps
  // While making this a simple judgment, you can also pass other parameter judgmentsrender(); }}Copy the code

If this component and its children depend on global plug-ins such as Store and i18n, then we need to return a function that, when called, executes directly:

export async function mount(props) {
  // If there is a commonComponent variable, another subitem is loaded via loadMicroApp
  // He only needs to mount the component
  if(props.data.commonComponent){
     const createHelloWorld = container= > new Vue({
     	el: container,
        store,
        i18n,
        render: h= > h(HelloWorld)
     });
     props.data.commonComponent.createHelloWorld = createHelloWorld;
  }else{
  // There is no commonComponent variable, indicating that the main project is loaded through registerMicroApps
  // While making this a simple judgment, you can also pass other parameter judgmentsrender(); }}Copy the code

Components that are reused by the parent project also apply to components that are shared between the child projects. If you want to implement “widgets” (some with data diagrams) that the main project uses for the subproject, you need to use a strong dependency scheme

Qiankun nested

Firstly, the Qiankun Project was modified according to the requirements of sub-projects before being connected. The basic changes are as follows:

  1. Modify the packaging configuration to allow cross-domain andumd
  2. Change the root ID#app
  3. Modify the route file to instantiate the route in the entry file
  4. Modify thepublic-pathThe file
  5. Modify the entry file, willqiankunThe required lifecycle is exposed

Plan 1: Run one of the subprojectsqiankunThe instance

Existing problems:

  1. Subprojects cannot determine whether to run independently or to be integrated based on existing information
if (!window.__POWERED_BY_QIANKUN__) {
  render();
}
Copy the code

__POWERED_BY_QIANKUN__ is true when it is independently run and true when it is integrated.

__POWERED_BY_QIANKUN_PARENT__ = true in the main project entry file. Use this variable to distinguish between being integrated and running independently

  1. Subproject entry file modification

There are mainly the following points for attention:

  • Avoid duplicate registration of sun Tzu project when switching sub-projects.
  • Since the subproject is injected with a prefix, the route of the grandchild project is also prefixed with that prefix
  • Note container conflicts, with subprojects and grandchild projects using different containers
let router = null;
let instance = null;
let flag = false;
function render() {
  router = new VueRouter({
    base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun' : '/'.mode: 'history',
    routes,
  });
  const childRoute = ['/app-vue-hash'.'/app-vue-history'];
  const isChildRoute = path= > childRoute.some(item= > path.startsWith(item))
  const rawAppendChild = HTMLHeadElement.prototype.appendChild;
  const rawAddEventListener = window.addEventListener;
  router.beforeEach((to, from, next) = > {
    // Jump from subproject to main project
    if(isChildRoute(from.path) && ! isChildRoute(to.path)){ HTMLHeadElement.prototype.appendChild = rawAppendChild;window.addEventListener = rawAddEventListener;
    }
    next();
  });

  instance = new Vue({
    router,
    store,
    render: h= > h(App),
  }).$mount('#appQiankun');

  if(! flag){ registerMicroApps([ {name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: '#appContainer'.activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-hash' : '/app-vue-hash'.props: { data : { store, router } }
      },
      { 
        name: 'app-vue-history'.entry: 'http://localhost:2222'.container: '#appContainer'.activeRule: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' : '/app-vue-history'.props: { data : store }
      },
    ]);
    
    start();
    flag = true}}if (!window.__POWERED_BY_QIANKUN_PARENT__) {
  render();
}

export async function bootstrap() {
  console.log('vue app bootstraped');
}

export async function mount(props) {
  render();
}

export async function unmount() {
  instance.$destroy();
  instance = null;
  router = null;
}
Copy the code
  1. historyMode routing of the grandchild projectbaseModify the
base: window.__POWERED_BY_QIANKUN_PARENT__ ? '/app-qiankun/app-vue-history' : 
      (window.__POWERED_BY_QIANKUN__ ? '/app-vue-history' : '/'),
Copy the code
  1. Package configuration changes

After the above operation is complete, the sub-project of Qiankun can be loaded in the main project, but click on the grandson project, an error is reported, and the life cycle can not be found.

Modify the package configuration of Sun Tzu project:

- library: `${name}-[name]`,
+ library: `${name}`,
Copy the code

And then you reboot it.

The reason is that qiankun takes the life cycle of the subproject and takes the last variable mounted to the window when the subproject runs first. If it is not a life cycle function, it will take it according to appName. Make webpack’s Library value correspond to appName.

Plan 2: The main project willqiankunIs passed to the subproject

The basic steps are the same as above, but there is a bug: Sun Tzu project does not load. However, the sub-project of The main project could be loaded normally, but the project of Sun Tzu did not load nor reported an error. It felt like a bug. The two projects share part of the route prefix, but the one with the long path is not loaded.

If the Sun Tzu project does not share the routing prefix with the sub-project, it can be loaded normally. Therefore, this practical scenario tends to: the nested sub-projects are registered as peer sub-projects, the main project container is directly used, and the registration function of the main project is shared. These Sun Tzu projects are themselves sub-projects of the main project.

The code for registering sub-projects is as follows:

  if(! flag){let registerMicroApps = parentData.registerMicroApps;
    let start = parentData.start;
    if(!window.__POWERED_BY_QIANKUN_PARENT__){
      const model = await import('qiankun');
      registerMicroApps = model.registerMicroApps;
      start = model.start;
    }
    registerMicroApps([
      { 
        name: 'app-vue-hash'.entry: 'http://localhost:1111'.container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer'.activeRule: '/app-vue-hash'.props: { data : { store, router } }
      },
      { 
        name: 'app-vue-history'.entry: 'http://localhost:2222'.container: window.__POWERED_BY_QIANKUN_PARENT__ ? '#appContainerParent' : '#appContainer'.activeRule: '/app-vue-history'.props: { data : store }
      },
    ]);
    start();
    flag = true;
}
Copy the code

qiankunUsed to summarize

  1. Most of the problems: accidentally refresh the page error, container can not be found.

Solution 1: Register and start qiankun during component Mounted cycles

Solution 2: After new Vue(), wait for the DOM to load and then register and start Qiankun

const vueApp = new Vue({ router, store, render: h => h(App) }).$mount("#app"); VueApp.$nextTick(() => {// register here and start qiankun})Copy the code
  1. What I was worried about: All of themjsScripts andcssFiles are cached in memory, so will too many subprojects cause the browser to freeze?

See the reply from the author in the issue area:

After reusing the main project dependencies, the js and CSS volumes of a subproject are around 2m-5m, so there is little to worry about.

  1. qiankunMultiple applications run simultaneouslyjsSandbox handling

Two child applications exist at the same time, and two global variables window.a are added. How to ensure that the two can run at the same time but do not interfere with each other?

With proxy proxy, all child application global variable changes are made in the closure and are not actually written back to the Window, thus avoiding contamination between multiple instances.

At the end

If you have any questions or mistakes, please point them out. Thank you!