样式隔离
样式隔离的方式:
css Modules:
使用webpack上面的css-loader进行相关的配置可以解决,来替换qiankuan上面的样式隔离可以实现性能优化
css-in-js:
好处:维护上解决了css样式的堆积没有办法删除的问题,性能上:自动实现treeShaking与关键css的问题。坏处:解决不了组件扩张的问题,多数CSS-in-JS实现都是在客户端动态生成CSS的,这就意味着会有一定的性能代价但是现在可以使用next.js等服务端渲染框架来解决
Shadow DOM
可以实现完整的样式隔离,原生实现组件化的东西,会有一些问题,没有办法解决在微应用中使用自定义字体,解决的办法只是能够全局的进行字体导入,没有办法实现字体的按需加载,没有办法控制Shadow DOM外面的样式,可能会出现弹窗等组件的bug,可能解决办法是进行dom插入的时候进行劫持的方式让其必须加入微应用的dom的方式。当在react版本较低的时候会影响事件合成(一般是react17以上)
qiankun具体实现
前端的具体就是在加载的时候把style标签当中的遍历一遍加载加上并且拦截document.createElement的方式拦截创建link与style标签 或者直接使用shadow dom,但是这些都有一些问题,这要做性能不是很好,其实使用webpack的css-loader就可以完成相关的配置问题,不用运行时处理。
js沙箱
由于打包的时候实现的是umd格式要来取得微应用的生命周期所以需要window对象的沙箱。当浏览器不支持 proxy,在单实例模式下会产生安全问题
沙箱逃逸
就是qiankun提供的excludeAssetFilter来完成逃逸
cdn加速办法
方法一
config.plugin('html').tap(args => {
if (process.env.NODE_ENV === 'production') {
args[0].cdn = cdn.build
}
if (process.env.NODE_ENV === 'development') {
args[0].cdn = cdn.dev
}
return args
})
方法二:
components: {
'remote-js': {
render(createElement) {
return createElement('script', {
attrs: {
type: 'text/javascript',
src: this.cdn
}
})
},
props: {
cdn: {
type: String,
required: true
}
}
},
'remote-css': {
render(createElement) {
return createElement('link', {
attrs: {
rel: 'stylesheet',
type: 'text/css',
href: this.cdn
}
})
},
props: {
cdn: {
type: String,
required: true
}
}
}
}
qiankun的预加载
加载优化(渲染空闲时网络情况较好的情况下预先加载、多方式预加载微应用,子微应用缓存加载的方式)
requestIdleCallback()
1、window.requestIdleCallback() 方法插入一个函数,这个函数将在浏览器空闲时期被调用。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
网络判断
const isSlowNetwork = navigator.connection
? navigator.connection.saveData ||
(navigator.connection.type !== 'wifi' &&
navigator.connection.type !== 'ethernet' &&
/([23])g/.test(navigator.connection.effectiveType))
: false;
问题:预加载是有问题的(没有选择好合适的加载策略是不是不做更好)
原因:系统的关联性不是特别的高,占用网络带宽,其实还是影响的了浏览器的渲染与响应,兼容性的原因
源码
import type { Entry, ImportEntryOpts } from 'import-html-entry';
import { importEntry } from 'import-html-entry';
import { isFunction } from 'lodash';
import { getAppStatus, getMountedApps, NOT_LOADED } from 'single-spa';
import type { AppMetadata, PrefetchStrategy } from './interfaces';
// 垫片
const requestIdleCallback =
window.requestIdleCallback ||
function requestIdleCallback(cb: CallableFunction) {
const start = Date.now();
return setTimeout(() => {
cb({
didTimeout: false,
timeRemaining() {
return Math.max(0, 50 - (Date.now() - start));
},
});
}, 1);
};
const isSlowNetwork = navigator.connection
? navigator.connection.saveData ||
(navigator.connection.type !== 'wifi' &&
navigator.connection.type !== 'ethernet' &&
/([23])g/.test(navigator.connection.effectiveType))
: false;
function prefetch(entry: Entry, opts?: ImportEntryOpts): void {
if (!navigator.onLine || isSlowNetwork) {
// Don't prefetch if in a slow network or offline
return;
}
requestIdleCallback(async () => {
const { getExternalScripts, getExternalStyleSheets } = await importEntry(entry, opts);
requestIdleCallback(getExternalStyleSheets);
requestIdleCallback(getExternalScripts);
});
}
//预加载的具体实现方式:就是两种,第一个挂在后加载,还有一个是立即加载
function prefetchAfterFirstMounted(apps: AppMetadata[], opts?: ImportEntryOpts): void {
window.addEventListener('single-spa:first-mount', function listener() {
const notLoadedApps = apps.filter((app) => getAppStatus(app.name) === NOT_LOADED);
if (process.env.NODE_ENV === 'development') {
const mountedApps = getMountedApps();
console.log(`[qiankun] prefetch starting after ${mountedApps} mounted...`, notLoadedApps);
}
notLoadedApps.forEach(({ entry }) => prefetch(entry, opts));
window.removeEventListener('single-spa:first-mount', listener);
});
}
export function prefetchImmediately(apps: AppMetadata[], opts?: ImportEntryOpts): void {
if (process.env.NODE_ENV === 'development') {
console.log('[qiankun] prefetch starting for apps...', apps);
}
apps.forEach(({ entry }) => prefetch(entry, opts));
}
//就是确立哪些微应用预加载及各个微应用的具体加载方式
export function doPrefetchStrategy(
apps: AppMetadata[],
prefetchStrategy: PrefetchStrategy,
importEntryOpts?: ImportEntryOpts,
) {
const appsName2Apps = (names: string[]): AppMetadata[] => apps.filter((app) => names.includes(app.name));
if (Array.isArray(prefetchStrategy)) {
prefetchAfterFirstMounted(appsName2Apps(prefetchStrategy as string[]), importEntryOpts);
} else if (isFunction(prefetchStrategy)) {
(async () => {
// critical rendering apps would be prefetch as earlier as possible
const { criticalAppNames = [], minorAppsName = [] } = await prefetchStrategy(apps);
prefetchImmediately(appsName2Apps(criticalAppNames), importEntryOpts);
prefetchAfterFirstMounted(appsName2Apps(minorAppsName), importEntryOpts);
})();
} else {
switch (prefetchStrategy) {
case true:
prefetchAfterFirstMounted(apps, importEntryOpts);
break;
case 'all':
prefetchImmediately(apps, importEntryOpts);
break;
default:
break;
}
}
}
export function loadMicroApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration?: FrameworkConfiguration & { autoStart?: boolean },
lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
const { props, name } = app;
const container = 'container' in app ? app.container : undefined;
been changed and result in a different xpath value
//使用xpath
const containerXPath = getContainerXPath(container);
const appContainerXPathKey = `${name}-${containerXPath}`;
let microApp: MicroApp;
const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => {
let microAppConfig = config;
if (container) {
if (containerXPath) {
const containerMicroApps = containerMicroAppsMap.get(appContainerXPathKey);
if (containerMicroApps?.length) {
const mount = [ async () => { // While there are multiple micro apps mounted on the same container, we must wait until the prev instances all had unmounted // Otherwise it will lead some concurrent issues const prevLoadMicroApps = containerMicroApps.slice(0, containerMicroApps.indexOf(microApp)); const prevLoadMicroAppsWhichNotBroken = prevLoadMicroApps.filter( (v) => v.getStatus() !== 'LOAD_ERROR' && v.getStatus() !== 'SKIP_BECAUSE_BROKEN', ); await Promise.all(prevLoadMicroAppsWhichNotBroken.map((v) => v.unmountPromise)); }, ...toArray(microAppConfig.mount), ];
microAppConfig = {
...config,
mount,
};
}
}
}
return {
...microAppConfig,
// empty bootstrap hook which should not run twice while it calling from cached micro app
bootstrap: () => Promise.resolve(),
};
};
/**
* 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<ParcelConfigObject> => {
const userConfiguration = autoDowngradeForLowVersionBrowser(
configuration ?? { ...frameworkConfiguration, singular: false },
);
const { $$cacheLifecycleByAppName } = userConfiguration;
if (container) {
// using appName as cache for internal experimental scenario
if ($$cacheLifecycleByAppName) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
if (containerXPath) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(appContainerXPathKey);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
}
const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
if (container) {
if ($$cacheLifecycleByAppName) {
appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
} else if (containerXPath) appConfigPromiseGetterMap.set(appContainerXPathKey, parcelConfigObjectGetterPromise);
}
return (await parcelConfigObjectGetterPromise)(container);
};
if (!started && configuration?.autoStart !== false) {
// We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically,
// but in single-spa it will check the start status before it dispatch popstate
// see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101
// ref https://github.com/umijs/qiankun/pull/1071
startSingleSpa({ urlRerouteOnly: frameworkConfiguration.urlRerouteOnly ?? defaultUrlRerouteOnly });
}
microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });
if (container) {
if (containerXPath) {
// Store the microApps which they mounted on the same container
const microAppsRef = containerMicroAppsMap.get(appContainerXPathKey) || [];
microAppsRef.push(microApp);
containerMicroAppsMap.set(appContainerXPathKey, microAppsRef);
const cleanup = () => {
const index = microAppsRef.indexOf(microApp);
microAppsRef.splice(index, 1);
// @ts-ignore
microApp = null;
};
// gc after unmount
microApp.unmountPromise.then(cleanup).catch(cleanup);
}
}
return microApp;
}