元素隔离
micro app中采用的是对doucment对象的一些方法重写,每次获取dom时从自身容器顶层dom节点来获取,以此达到元素的隔离
样式隔离
- 使用前缀
- postcss
- 加载子应用时在在选择器上手动添加一个关于子应用的前缀
- css modules
- shadow dom
micro app 中对于样式的处理时对每个样式规则,添加自身容器对名字添加前缀比如:micro-app[name=app-root] .child{}
达到样式的隔离
Js的处理
micro app 中对于js采用的是沙箱机制,对于顶层对象采用的代理的形式
对于全局对象的修改会被代理到代理对象中,记录修改的key值, 在停止时会把记录的key在原始window、和代理对象的window中删除掉,然后清空所有的key值
function executeCode(code) {
const rawWindow = window;
const fakeWindow = {count: 1000};
let _code = `(function (window, self) {
with(window) {
${code}
}
}).call(fakeWindow, fakeWindow)`;
eval(_code);
}
executeCode(`
console.log(window)
`);
// log -> {count: 1000}
数据通信
micro app 中采用的是发布订阅的形式
源代码:
import { CallableFunctionForInteract } from '@micro-app/types'
import { logError, isFunction, isPlainObject } from '../libs/utils'
export default class EventCenter {
eventList = new Map<string, {
data: Record<PropertyKey, unknown>,
callbacks: Set<CallableFunctionForInteract>,
}>()
// whether the name is legal
isLegalName (name: string): boolean {
if (!name) {
logError('event-center: Invalid name')
return false
}
return true
}
/**
* add listener
* @param name event name
* @param f listener
* @param autoTrigger If there is cached data when first bind listener, whether it needs to trigger, default is false
*/
on (name: string, f: CallableFunctionForInteract, autoTrigger = false): void {
if (this.isLegalName(name)) {
if (!isFunction(f)) {
return logError('event-center: Invalid callback function')
}
let eventInfo = this.eventList.get(name)
if (!eventInfo) {
eventInfo = {
data: {},
callbacks: new Set(),
}
this.eventList.set(name, eventInfo)
} else if (autoTrigger && Object.getOwnPropertyNames(eventInfo.data).length) {
// auto trigger when data not null
f(eventInfo.data)
}
eventInfo.callbacks.add(f)
}
}
// remove listener, but the data is not cleared
off (name: string, f?: CallableFunctionForInteract): void {
if (this.isLegalName(name)) {
const eventInfo = this.eventList.get(name)
if (eventInfo) {
if (isFunction(f)) {
eventInfo.callbacks.delete(f!)
} else {
eventInfo.callbacks.clear()
}
}
}
}
// eventInfo中保存的事最近一次发布的data
// 所以才获取时获取到的就是最新的
// dispatch data
dispatch (name: string, data: Record<PropertyKey, unknown>): void {
if (this.isLegalName(name)) {
if (!isPlainObject(data)) {
return logError('event-center: data must be object')
}
let eventInfo = this.eventList.get(name)
if (eventInfo) {
// Update when the data is not equal
if (eventInfo.data !== data) {
eventInfo.data = data
// 通知所有监听函数
for (const f of eventInfo.callbacks) {
f(data)
}
}
} else {
eventInfo = {
data: data,
callbacks: new Set(),
}
this.eventList.set(name, eventInfo)
}
}
}
// get data
getData (name: string): Record<PropertyKey, unknown> | null {
const eventInfo = this.eventList.get(name)
return eventInfo?.data ?? null
}
}
实例中使用的getData()
就是继承了baseAppEventCenter的实例,实例时传入appName, 在之上封装了一层,所以可以直接获取data ,而不用传入当前的appname
// 实例化sandBox时创建eventcenter
// 因为在with块中,所以访问microApp就可以拿到
private initMicroAppWindow (microAppWindow: microAppWindowType, appName: string, url: string): void {
// ...code
microAppWindow.microApp = Object.assign(new EventCenterForMicroApp(appName), {
removeDomScope,
pureCreateElement,
})
// ...code
}
// 应用中
function App() {
console.log(window. microApp);
return (
<div className="child" >
<h1>App子应用</h1>
<p>react 18 子应用</p>
<p>设置路由</p>
</div>
);
}
EventCenterForMicroApp
appName: "app-root"
pureCreateElement: ƒ pureCreateElement(tagName, options)
removeDomScope: ƒ removeDomScope()
[[Prototype]]: EventCenterForGlobal
addDataListener: ƒ addDataListener(cb, autoTrigger)
clearDataListener: ƒ clearDataListener()
constructor: class EventCenterForMicroApp
dispatch: ƒ dispatch(data)
// getData 就是上面的eventCenter
getData: ƒ getData()
removeDataListener: ƒ removeDataListener(cb)
预加载
基于requestIdleCallback(命名冲突~~~)
export const requestIdleCallback = globalThis.requestIdleCallback ||
function (fn: CallableFunction) {
const lastTime = Date.now()
return setTimeout(function () {
fn({
didTimeout: false,
timeRemaining () {
return Math.max(0, 50 - (Date.now() - lastTime))
},
})
}, 50)
}
function preFetchInSerial (prefetchApp: prefetchParam): Promise<void> {
return new Promise((resolve) => {
requestIdleCallback(() => {
if (isPlainObject(prefetchApp) && navigator.onLine) {
prefetchApp.name = formatAppName(prefetchApp.name)
prefetchApp.url = formatAppURL(prefetchApp.url, prefetchApp.name)
if (
prefetchApp.name &&
prefetchApp.url &&
!appInstanceMap.has(prefetchApp.name)
) {
// 实例化组件类
const app = new CreateApp({
name: prefetchApp.name,
url: prefetchApp.url,
scopecss: !(prefetchApp.disableScopecss ?? microApp.disableScopecss),
useSandbox: !(prefetchApp.disableSandbox ?? microApp.disableSandbox),
})
app.isPrefetch = true
app.prefetchResolve = resolve
// 保存整个应用中所有的子应用实例
// 设置 实例
appInstanceMap.set(prefetchApp.name, app)
} else {
resolve()
}
} else {
resolve()
}
})
})
}
// 实例化以后就是加载应用
// 请求html,links,script,一切加载完毕以后
// 调用app的onLoad方法, 在里面执行prefetchResolve
// 预加载完成