Skip to content

Commit

Permalink
feat: 优化了容器元素获取方式
Browse files Browse the repository at this point in the history
  • Loading branch information
bailicangdu committed Nov 19, 2021
1 parent 787b120 commit b9e79a3
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 30 deletions.
2 changes: 1 addition & 1 deletion docs/zh-cn/dom-scope.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

**举个栗子🌰 :**

基座应用和子应用都有一个元素`<div id='root'></div>`,此时子应用通过`document.querySelector('#root')`获取到的一定是自己内部的`#root`元素,而不是基座应用的。
基座应用和子应用都有一个元素`<div id='root'></div>`,此时子应用通过`document.querySelector('#root')`获取到的是自己内部的`#root`元素,而不是基座应用的。

**基座应用可以获取子应用的元素吗?**

Expand Down
6 changes: 6 additions & 0 deletions src/__tests__/common/initial.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ const liveServer = require('../../../scripts/test_server')
global.fetch = require('node-fetch')
jest.useRealTimers()

declare global {
interface Element {
ssrUrl: string
}
}

export const ports = {
main: 9000,
create_app: 9001,
Expand Down
4 changes: 4 additions & 0 deletions src/__tests__/demos/common/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
</style>
<link rel="stylesheet" href="/common/link1.css">
<link rel="stylesheet" href="/common/link2.css" global>
<!-- 分支覆盖 -->
<link rel="stylesheet" href="/common/link2.css" ignore>
<!-- 分支覆盖 -->
<link ignore>
<link rel="preload" href="./manifest.js" as="script">
<!-- 分支覆盖 -->
<link rel="dns-prefetch" href="//test.com">
Expand Down
55 changes: 55 additions & 0 deletions src/__tests__/micro_app_element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,4 +308,59 @@ describe('micro_app_element', () => {
microAppElement19.setAttribute('name', 'test-app19$')
expect(microAppElement19.getAttribute('name')).toBe('test-app19')
})

// 测试ssr配置
test('test ssr mode', async () => {
const microAppElement20 = document.createElement('micro-app')
microAppElement20.setAttribute('name', 'test-app20')
microAppElement20.setAttribute('url', `http://127.0.0.1:${ports.micro_app_element}/common`)
microAppElement20.setAttribute('ssr', 'true')

// 场景1: 测试正常渲染的ssr应用
appCon.appendChild(microAppElement20)

// connectedCallback中会对url地址进行格式化,因为jest环境下,location.pathname 默认为 '/',所以/common被截掉
expect(microAppElement20.ssrUrl).toBe(`http://127.0.0.1:${ports.micro_app_element}/`)

// 场景2: 再次渲染时,去除ssr配置,如果有 ssrUrl,则进行删除
appCon.removeChild(microAppElement20)
microAppElement20.removeAttribute('ssr')
appCon.appendChild(microAppElement20)

expect(microAppElement20.ssrUrl).toBe('')

// 场景3: ssr模式下动态修改url的值,此时ssrUrl会进行同步更新
appCon.removeChild(microAppElement20)
microAppElement20.setAttribute('ssr', 'true')
appCon.appendChild(microAppElement20)

await new Promise((reslove) => {
microAppElement20.addEventListener('mounted', () => {
microAppElement20.setAttribute('url', `http://127.0.0.1:${ports.micro_app_element}/dynamic/`)
defer(() => {
expect(microAppElement20.ssrUrl).toBe(`http://127.0.0.1:${ports.micro_app_element}/`)
reslove(true)
})
})
})

// 场景4: ssr模式已经渲染,修改url的值的同时去除ssr配置,需要将ssrUrl的值删除
const microAppElement21 = document.createElement('micro-app')
microAppElement21.setAttribute('name', 'test-app21')
microAppElement21.setAttribute('url', `http://127.0.0.1:${ports.micro_app_element}/common`)
microAppElement21.setAttribute('ssr', 'true')

appCon.appendChild(microAppElement21)

await new Promise((reslove) => {
microAppElement21.addEventListener('mounted', () => {
microAppElement21.removeAttribute('ssr')
microAppElement21.setAttribute('url', `http://127.0.0.1:${ports.micro_app_element}/dynamic/`)
defer(() => {
expect(microAppElement21.ssrUrl).toBe('')
reslove(true)
})
})
})
})
})
6 changes: 6 additions & 0 deletions src/__tests__/source/patch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,12 @@ describe('source patch', () => {
document.head.appendChild(handleNewNodeDom4)
expect(microAppHead.contains(handleNewNodeDom4)).toBeFalsy()

// 分支覆盖 - 动态插入具有ignore属性的link元素
const handleNewNodeDom6 = document.createElement('link')
handleNewNodeDom6.setAttribute('ignore', 'true')
document.head.appendChild(handleNewNodeDom6)
expect(microAppHead.contains(handleNewNodeDom6)).toBeTruthy()

reslove(true)
}, false)
})
Expand Down
26 changes: 26 additions & 0 deletions src/__tests__/source/scoped_css.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@ describe('source scoped_css', () => {
defer(() => {
// 所有style都被清空内容
expect(dynamicStyle.textContent).toBe('micro-app[name=test-app6] div {color: red;}')

// 将模版style还原,否则下面的test无法运行
document.body.appendChild(templateStyle)
reslove(true)
})
}, false)
Expand Down Expand Up @@ -236,4 +239,27 @@ describe('source scoped_css', () => {
}, false)
})
})

// 分支覆盖 -- 同一个style元素被执行了两次 -- styleElement.__MICRO_APP_HAS_SCOPED__
test('coverage: styleElement.__MICRO_APP_HAS_SCOPED__', async () => {
const microappElement8 = document.createElement('micro-app')
microappElement8.setAttribute('name', 'test-app8')
microappElement8.setAttribute('url', `http://127.0.0.1:${ports.scoped_css}/dynamic/`)

appCon.appendChild(microappElement8)

await new Promise((reslove) => {
microappElement8.addEventListener('mounted', () => {
setAppName('test-app8')
const dynamicStyle1 = document.createElement('style')
document.head.appendChild(dynamicStyle1)
document.head.removeChild(dynamicStyle1)
dynamicStyle1.textContent = 'div {color: red}'
document.head.appendChild(dynamicStyle1)

expect(dynamicStyle1.textContent).toBe('micro-app[name=test-app8] div {color: red;}')
reslove(true)
}, false)
})
})
})
4 changes: 2 additions & 2 deletions src/create_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
isBoolean,
isPromise,
logError,
isShadowRoot,
getRootContainer,
} from './libs/utils'
import dispatchLifecyclesEvent, { dispatchUnmountToMicroApp } from './interact/lifecycles_event'
import globalEnv from './libs/global_env'
Expand Down Expand Up @@ -322,7 +322,7 @@ export default class CreateApp implements AppInterface {
// after execScripts, the app maybe unmounted
if (appStatus.UNMOUNT !== this.status) {
const global = (this.sandBox?.proxyWindow ?? globalEnv.rawWindow) as any
this.libraryName = (isShadowRoot(this.container) ? (this.container as ShadowRoot).host : this.container as Element).getAttribute('library') || `micro-app-${this.name}`
this.libraryName = getRootContainer(this.container!).getAttribute('library') || `micro-app-${this.name}`
// do not use isObject
return typeof global[this.libraryName] === 'object' ? global[this.libraryName] : {}
}
Expand Down
8 changes: 2 additions & 6 deletions src/interact/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
isString,
isFunction,
isPlainObject,
isShadowRoot,
formatAppName,
logError,
getRootContainer,
} from '../libs/utils'

const eventCenter = new EventCenter()
Expand Down Expand Up @@ -191,11 +191,7 @@ export class EventCenterForMicroApp extends EventCenterForGlobal {
}
})

let element = app.container
if (isShadowRoot(element)) {
element = (element as ShadowRoot).host as HTMLElement
}
element.dispatchEvent(event)
getRootContainer(app.container).dispatchEvent(event)
}
}

Expand Down
8 changes: 4 additions & 4 deletions src/interact/lifecycles_event.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import microApp from '../micro_app'
import { logError, isFunction, removeDomScope, isShadowRoot } from '../libs/utils'
import { logError, isFunction, removeDomScope, getRootContainer } from '../libs/utils'

function eventHandler (event: CustomEvent, element: HTMLElement): void {
Object.defineProperties(event, {
Expand Down Expand Up @@ -32,10 +32,10 @@ export default function dispatchLifecyclesEvent (
): void {
if (!element) {
return logError(`element does not exist in lifecycle ${lifecycleName}`, appName)
} else if (isShadowRoot(element)) {
element = (element as ShadowRoot).host as HTMLElement
}

element = getRootContainer(element)

// clear dom scope before dispatch lifeCycles event to base app, especially mounted & unmount
removeDomScope()

Expand All @@ -50,7 +50,7 @@ export default function dispatchLifecyclesEvent (
detail,
})

eventHandler(event, element as HTMLElement)
eventHandler(event, element)
// global hooks
// @ts-ignore
if (isFunction(microApp.lifeCycles?.[lifecycleName])) {
Expand Down
10 changes: 3 additions & 7 deletions src/libs/additional.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,14 @@
import { appInstanceMap } from '../create_app'
import { elementInstanceMap } from '../micro_app_element'
import { releasePatches } from '../source/patch'
import { isShadowRoot } from '../libs/utils'
import { getRootContainer } from '../libs/utils'

function unmountNestedApp (): void {
replaseUnmountOfNestedApp()

appInstanceMap.forEach(app => {
let element = app.container
if (element) {
isShadowRoot(element) && (element = (element as ShadowRoot).host as HTMLElement)
// @ts-ignore
element.disconnectedCallback()
}
// @ts-ignore
app.container && getRootContainer(app.container).disconnectedCallback()
})

!window.__MICRO_APP_UMD_MODE__ && appInstanceMap.clear()
Expand Down
8 changes: 8 additions & 0 deletions src/libs/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,11 @@ export function isUniqueElement (key: string): boolean {
/^html$/i.test(key)
)
}

/**
* get micro-app element
* @param target app container
*/
export function getRootContainer (target: HTMLElement | ShadowRoot): HTMLElement {
return (isShadowRoot(target) ? (target as ShadowRoot).host : target) as HTMLElement
}
19 changes: 9 additions & 10 deletions src/micro_app_element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,17 +222,16 @@ export function defineElement (tagName: string): void {
* when existApp not null:
* scene1: if formatAttrName and this.appName are equal: exitApp is the current app, the url must be different, existApp has been unmounted
* scene2: if formatAttrName and this.appName are different: existApp must be prefetch or unmounted, if url is equal, then just mount, if url is different, then create new app to replace existApp
* scene3: url is different but ssrUrl is equal
* scene4: url is equal but ssrUrl is different, if url is equal, name must different
*/

if (existApp) {
const existAppUrl = existApp.ssrUrl || existApp.url
const activeAppUrl = this.ssrUrl || this.appUrl
if (existAppUrl === activeAppUrl) {
// mount app
this.handleAppMount(existApp)
} else {
this.handleCreateApp()
}
if (
existApp &&
existApp.url === this.appUrl &&
existApp.ssrUrl === this.ssrUrl
) {
// mount app
this.handleAppMount(existApp)
} else {
this.handleCreateApp()
}
Expand Down

0 comments on commit b9e79a3

Please sign in to comment.