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; +}