diff --git a/.eslintrc.js b/.eslintrc.js
index 309bade..12a24dc 100755
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -14,4 +14,9 @@ module.exports = {
'no-underscore-dangle': 0,
'no-plusplus': 0,
},
+ parserOptions: {
+ tsconfigRootDir: __dirname,
+ project: './tsconfig.json',
+ createDefaultProgram: true,
+ },
};
diff --git a/examples/main/multiple.js b/examples/main/multiple.js
index 92d734c..c4130fb 100644
--- a/examples/main/multiple.js
+++ b/examples/main/multiple.js
@@ -9,6 +9,15 @@ const app1 = loadMicroApp(
},
);
+// for cached scenario
+setTimeout(() => {
+ app1.unmount();
+
+ setTimeout(() => {
+ loadMicroApp({ name: 'react15', entry: '//localhost:7102', container: '#react15' });
+ }, 1000 * 5);
+}, 1000 * 5);
+
const app2 = loadMicroApp(
{ name: 'vue', entry: '//localhost:7101', container: '#vue' },
{
diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts
index 21451f4..098f8d2 100644
--- a/src/__tests__/utils.test.ts
+++ b/src/__tests__/utils.test.ts
@@ -1,4 +1,11 @@
-import { getWrapperId, getDefaultTplWrapper, validateExportLifecycle, sleep, Deferred } from '../utils';
+import {
+ Deferred,
+ getDefaultTplWrapper,
+ getWrapperId,
+ getXPathForElement,
+ sleep,
+ validateExportLifecycle,
+} from '../utils';
test('should wrap the id [1]', () => {
const id = 'REACT16';
@@ -86,3 +93,22 @@ test('Deferred should worked [2]', async () => {
expect(err).toBeInstanceOf(Error);
});
+
+test('should getXPathForElement work well', () => {
+ const article = document.createElement('article');
+ article.innerHTML = `
+
+ `;
+
+ document.body.appendChild(article);
+ const testNode = document.querySelector('#testNode');
+ const xpath = getXPathForElement(testNode!, document);
+ expect(xpath).toEqual(
+ // eslint-disable-next-line max-len
+ `/*[name()='HTML' and namespace-uri()='http://www.w3.org/1999/xhtml']/*[name()='BODY' and namespace-uri()='http://www.w3.org/1999/xhtml'][1]/*[name()='ARTICLE' and namespace-uri()='http://www.w3.org/1999/xhtml'][1]/*[name()='DIV' and namespace-uri()='http://www.w3.org/1999/xhtml'][1]/*[name()='DIV' and namespace-uri()='http://www.w3.org/1999/xhtml'][2]`,
+ );
+});
diff --git a/src/apis.ts b/src/apis.ts
index 8b170f8..415bf03 100644
--- a/src/apis.ts
+++ b/src/apis.ts
@@ -1,9 +1,9 @@
import { noop } from 'lodash';
-import { mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
+import { mountRootParcel, ParcelConfigObject, registerApplication, start as startSingleSpa } from 'single-spa';
import { FrameworkConfiguration, FrameworkLifeCycles, LoadableApp, MicroApp, RegistrableApp } from './interfaces';
import { loadApp } from './loader';
import { doPrefetchStrategy } from './prefetch';
-import { Deferred, toArray } from './utils';
+import { Deferred, getXPathForElement, toArray } from './utils';
let microApps: RegistrableApp[] = [];
@@ -46,16 +46,55 @@ export function registerMicroApps(
});
}
+const appConfigMap = new Map>();
+
export function loadMicroApp(
app: LoadableApp,
configuration?: FrameworkConfiguration,
lifeCycles?: FrameworkLifeCycles,
): MicroApp {
- const { props } = app;
- return mountRootParcel(() => loadApp(app, configuration ?? frameworkConfiguration, lifeCycles), {
- domElement: document.createElement('div'),
- ...props,
- });
+ const { props, name } = app;
+
+ const getContainerXpath = (container: string | HTMLElement): string | void => {
+ const containerElement = typeof container === 'string' ? document.querySelector(container) : container;
+ if (containerElement) {
+ return getXPathForElement(containerElement, document);
+ }
+
+ return undefined;
+ };
+
+ /**
+ * using name + container xpath as the micro app instance id,
+ * it means if you rendering a micro app to a dom which have been rendered before,
+ * the micro app would not load and evaluate its lifecycles again
+ */
+ const memorizedLoadingFn = async (): Promise => {
+ const container = 'container' in app ? app.container : undefined;
+ if (container) {
+ const xpath = getContainerXpath(container);
+ if (xpath) {
+ const parcelConfig = appConfigMap.get(`${name}-${xpath}`);
+ if (parcelConfig) return parcelConfig;
+ }
+ }
+
+ const parcelConfig = loadApp(app, configuration ?? frameworkConfiguration, lifeCycles);
+
+ if (container) {
+ const xpath = getContainerXpath(container);
+ if (xpath)
+ appConfigMap.set(
+ `${name}-${xpath}`,
+ // empty bootstrap hook which should not run twice while it calling from cached micro app
+ parcelConfig.then(config => ({ ...config, bootstrap: () => Promise.resolve() })),
+ );
+ }
+
+ return parcelConfig;
+ };
+
+ return mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });
}
export function start(opts: FrameworkConfiguration = {}) {
diff --git a/src/utils.ts b/src/utils.ts
index 5ae1408..0e7867e 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -126,3 +126,40 @@ export function isEnableScopedCSS(opt: FrameworkConfiguration) {
return !!opt.sandbox.experimentalStyleIsolation;
}
+
+/**
+ * copy from https://developer.mozilla.org/zh-CN/docs/Using_XPath
+ * @param el
+ * @param xml
+ */
+export function getXPathForElement(el: Node, xml: Document) {
+ let xpath = '';
+ let pos;
+ let tmpEle;
+ let element = el;
+
+ while (element !== xml.documentElement) {
+ pos = 0;
+ tmpEle = element;
+ while (tmpEle) {
+ if (tmpEle.nodeType === 1 && tmpEle.nodeName === element.nodeName) {
+ // If it is ELEMENT_NODE of the same name
+ pos += 1;
+ }
+ tmpEle = tmpEle.previousSibling;
+ }
+
+ xpath = `*[name()='${element.nodeName}' and namespace-uri()='${
+ element.namespaceURI === null ? '' : element.namespaceURI
+ }'][${pos}]/${xpath}`;
+
+ element = element.parentNode!;
+ }
+
+ xpath = `/*[name()='${xml.documentElement.nodeName}' and namespace-uri()='${
+ element.namespaceURI === null ? '' : element.namespaceURI
+ }']/${xpath}`;
+ xpath = xpath.replace(/\/$/, '');
+
+ return xpath;
+}