qiankun随笔

398 阅读3分钟

样式隔离

样式隔离的方式:

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