微前端--qiankun

news2024/11/14 13:59:22

qiankun

qiankun分为accpication和parcel模式。
aplication模式基于路由工作,将应用分为两类,基座应用和子应用,基座应用维护路由注册表,根据路由的变化来切换子应用。子应用是一个独立的应用,需要提供生命周期方法供基座应用使用。
parcel模式和路由无关,子应用切换是手动控制的,具体是通过qiankun提供的loadMicroApp来实现的。

js隔离机制

乾坤有三种js隔离机制,SnapshotSandbox LegacySandbox ProxySandbox

SnapshotSandBox 快照沙箱

class SnapshotSandBox{
    windowSnapshot = {};
    modifyPropsMap = {};
    active(){
        for(const prop in window){
            this.windowSnapshot[prop] = window[prop];
        }
        Object.keys(this.modifyPropsMap).forEach(prop=>{
            window[prop] = this.modifyPropsMap[prop];
        });
    }
    inactive(){
        for(const prop in window){
            if(window[prop] !== this.windowSnapshot[prop]){
                this.modifyPropsMap[prop] = window[prop];
                window[prop] = this.windowSnapshot[prop];
            }
        }
    }
}
// 验证:
let snapshotSandBox = new SnapshotSandBox();
snapshotSandBox.active();
window.city = 'Beijing';
console.log("window.city-01:", window.city);
snapshotSandBox.inactive();
console.log("window.city-02:", window.city);
snapshotSandBox.active();
console.log("window.city-03:", window.city);
snapshotSandBox.inactive();

//输出:
//window.city-01: Beijing
//window.city-02: undefined
//window.city-03: Beijing

  • 沙箱激活: 微应用处于运行中,这个阶段可能会对window上的属性操作进行改变。
  • 沙箱失活: 就是微应用停止了对window的影响
  • 在沙箱激活的时候:记录window当时的状态(我们把这个状态称之为快照,也就是快照沙箱这个名称的来源),恢复上一次沙箱失活时记录的沙箱运行过程中对window做的状态改变,也就是上一次沙箱激活后对window做了哪些改变,现在也保持一样的改变。
  • 沙箱失活: 记录window上有哪些状态发生了变化(沙箱自激活开始,到失活的这段时间);清除沙箱在激活之后在window上改变的状态,从代码可以看出,就是让window此时的属性状态和刚激活时候的window的属性状态进行对比,不同的属性状态就以快照为准,恢复到未改变之前的状态。
    这种沙箱无法同时运行多个微应用

LegacySandBox 代理沙箱

class LegacySandBox{
    addedPropsMapInSandbox = new Map();
    modifiedPropsOriginalValueMapInSandbox = new Map();
    currentUpdatedPropsValueMap = new Map();
    proxyWindow;
    setWindowProp(prop, value, toDelete = false){
        if(value === undefined && toDelete){
            delete window[prop];
        }else{
            window[prop] = value;
        }
    }
    active(){
        this.currentUpdatedPropsValueMap.forEach((value, prop)=>this.setWindowProp(prop, value));
    }
    inactive(){
        this.modifiedPropsOriginalValueMapInSandbox.forEach((value, prop)=>this.setWindowProp(prop, value));
        this.addedPropsMapInSandbox.forEach((_, prop)=>this.setWindowProp(prop, undefined, true));
    }
    constructor(){
        const fakeWindow = Object.create(null);
        this.proxyWindow = new Proxy(fakeWindow,{
            set:(target, prop, value, receiver)=>{
                const originalVal = window[prop];
                if(!window.hasOwnProperty(prop)){
                    this.addedPropsMapInSandbox.set(prop, value);
                }else if(!this.modifiedPropsOriginalValueMapInSandbox.has(prop)){
                    this.modifiedPropsOriginalValueMapInSandbox.set(prop, originalVal);
                }
                this.currentUpdatedPropsValueMap.set(prop, value);
                window[prop] = value;
            },
            get:(target, prop, receiver)=>{
                return target[prop];
            }
        });
    }
}
// 验证:
let legacySandBox = new LegacySandBox();
legacySandBox.active();
legacySandBox.proxyWindow.city = 'Beijing';
console.log('window.city-01:', window.city);
legacySandBox.inactive();
console.log('window.city-02:', window.city);
legacySandBox.active();
console.log('window.city-03:', window.city);
legacySandBox.inactive();
// 输出:
// window.city-01: Beijing
// window.city-02: undefined
// window.city-03: Beijing

ProxySandBox 代理沙箱

class ProxySandBox{
    proxyWindow;
    isRunning = false;
    active(){
        this.isRunning = true;
    }
    inactive(){
        this.isRunning = false;
    }
    constructor(){
        const fakeWindow = Object.create(null);
        this.proxyWindow = new Proxy(fakeWindow,{
            set:(target, prop, value, receiver)=>{
                if(this.isRunning){
                    target[prop] = value;
                }
            },
            get:(target, prop, receiver)=>{
                return  prop in target ? target[prop] : window[prop];
            }
        });
    }
}
// 验证:
let proxySandBox1 = new ProxySandBox();
let proxySandBox2 = new ProxySandBox();
proxySandBox1.active();
proxySandBox2.active();
proxySandBox1.proxyWindow.city = 'Beijing';
proxySandBox2.proxyWindow.city = 'Shanghai';
console.log('active:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
console.log('active:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
console.log('window:window.city:', window.city);
proxySandBox1.inactive();
proxySandBox2.inactive();
console.log('inactive:proxySandBox1:window.city:', proxySandBox1.proxyWindow.city);
console.log('inactive:proxySandBox2:window.city:', proxySandBox2.proxyWindow.city);
console.log('window:window.city:', window.city);
// 输出:
// active:proxySandBox1:window.city: Beijing
// active:proxySandBox2:window.city: Shanghai
// window:window.city: undefined
// inactive:proxySandBox1:window.city: Beijing
// inactive:proxySandBox2:window.city: Shanghai
// window:window.city: undefined

支持一个页面运行多个微应用

资源加载

  • 通过registerMicroApps注册微应用
  • 通过loadMicroApp手动加载微应用
  • 调用start时触发了预加载逻辑
  • 手动调用prefetchApps执行加载
    在这里插入图片描述
export function registerMicroApps<T extends objectType>(apps: Array<RegistrableApp<T>, lifeCycles?: FrameworkLifeCycles<T>,) {
    unregisteredApps.forEach((app) => {
        const {name, activeRule, loader = noop, porps, ...appConfig} = app
        registerApplication({
            name,
            app: async() => {
                const {mount, ...otherMicroAppConfig} = {
                    await loadApp({name, props, ...appConfig}, frameworkConfiguration, lifeCycles)()
                    return {
                        mount: [async () => loader(true), ...toArray(mount), async() => loader(false)],
                        ...otherMicroAppConfigs,
                    }
                }
                activeWhen: activeRule,
                customProps: props
            }        
        })
    })
}
  • name: 子应用的唯一标识
  • entry: 子应用的入口
  • container: 子应用挂载的节点
  • activeRule: 子应用激活的条件。

loadApp的主体流程

核心功能: 获取微应用的js/css/html等资源,并对这些资源进行加工,然后构造和执行生命周期中需要执行的方法。最后返回一个函数,这个函数的返回值是一个对象,该对象包含了微应用的生命周期方法。
在这里插入图片描述

在这里插入图片描述

获取微应用资源的方法

依赖了库import-html-entry的importEntry函数。

const {template, execScripts, assetPublicPath} = await importEntry(entry, importEntryOpts)
// template: 一个字符串,内部包含了html、css资源
// assetPublicPath:访问页面远程资源的相对路径
// execScripts:一个函数,执行该函数后会返回一个对象

将获取的template涉及的html/css转换为dom节点

const appContent = getDefaultTplWrapper(appInstanceId)(template)
let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsop,
    scopedCSS,    
    appInstanceId
)
export function getDefaultTplWrapper(name: string) {
  return (tpl: string) => `<div id="${getWrapperId(name)}" data-name="${name}" data-version="${version}">${tpl}</div>`;
}
function createElement(
    appContent: string,
    strictStyleIsolation:boolean,
    scopedCSS: boolean,
    appInstanceId: string    
): HTMLElement {
    const containerElement = document.createElement('div')
    containerElement.innerHTML = appContainer
    const appELement = containerElement.firstChild as HTMLElement
    return appElement
}

css资源的处理和隔离方法

if(scopedCSS) {
    const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr)
    if(!attr) {
        appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId)
    }
    const styleNodes = appElement.querySelectorAll('style') || []
    forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
        css.process(appElement!, stylesheetElement, appInstanceId)
    })
}

关于函数initialAppWrapperGetter

// 代码片段五,所属文件:src/loader.ts
  const initialAppWrapperGetter = getAppWrapperGetter(
    appInstanceId,
    !!legacyRender,
    strictStyleIsolation,
    scopedCSS,
    () => initialAppWrapperElement,
  );
  /** generate app wrapper dom getter */
function getAppWrapperGetter(
  appInstanceId: string,
  useLegacyRender: boolean,
  strictStyleIsolation: boolean,
  scopedCSS: boolean,
  elementGetter: () => HTMLElement | null,
) {
  return () => {
    if (useLegacyRender) {
      // 省略一些代码...
      const appWrapper = document.getElementById(getWrapperId(appInstanceId));
      // 省略一些代码...
      return appWrapper!;
    }
    const element = elementGetter();
      // 省略一些代码
    return element!;
  };
}

兼容。

一些生命周期中需要执行的函数

const {
  beforeUnmount = [],
  afterUnmount = [],
  afterMount = [],
  beforeMount = [],
  beforeLoad = [],
} = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) => concat(v1 ?? [], v2 ?? []));

execHooksChain

await execHooksChain(toArray(beforeLoad), app, global)
function execHooksChain<T extends ObjectType>(
  hooks: Array<LifeCycleFn<T>>,
  app: LoadableApp<T>,
  global = window,
): Promise<any> {
  if (hooks.length) {
    return hooks.reduce((chain, hook) => chain.then(() => hook(app, global)), Promise.resolve());
  }

  return Promise.resolve();
}

微应用加载完成后的返回值

  const parcelConfigGetter: ParcelConfigObjectGetter = (remountContainer = initialContainer) => {
    // 省略相关代码
    const parcelConfig: ParcelConfigObject = {
      // 省略相关代码
    }
    return parcelConfig;
  }

parcelConfigGetter返回对象

const parcelConfig: ParcelConfigObject = {
      name: appInstanceId,
      bootstrap,
      mount: [
        async () => {
          if (process.env.NODE_ENV === 'development') {
            const marks = performanceGetEntriesByName(markName, 'mark');
            // mark length is zero means the app is remounting
            if (marks && !marks.length) {
              performanceMark(markName);
            }
          }
        },
        async () => {
          if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
            return prevAppUnmountedDeferred.promise;
          }

          return undefined;
        },
        // initial wrapper element before app mount/remount
        async () => {
          appWrapperElement = initialAppWrapperElement;
          appWrapperGetter = getAppWrapperGetter(
            appInstanceId,
            !!legacyRender,
            strictStyleIsolation,
            scopedCSS,
            () => appWrapperElement,
          );
        },
        // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
        async () => {
          const useNewContainer = remountContainer !== initialContainer;
          if (useNewContainer || !appWrapperElement) {
            // element will be destroyed after unmounted, we need to recreate it if it not exist
            // or we try to remount into a new container
            appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
            syncAppWrapperElement2Sandbox(appWrapperElement);
          }

          render({ element: appWrapperElement, loading: true, container: remountContainer }, 'mounting');
        },
        mountSandbox,
        // exec the chain after rendering to keep the behavior with beforeLoad
        async () => execHooksChain(toArray(beforeMount), app, global),
        async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),
        // finish loading after app mounted
        async () => render({ element: appWrapperElement, loading: false, container: remountContainer }, 'mounted'),
        async () => execHooksChain(toArray(afterMount), app, global),
        // initialize the unmount defer after app mounted and resolve the defer after it unmounted
        async () => {
          if (await validateSingularMode(singular, app)) {
            prevAppUnmountedDeferred = new Deferred<void>();
          }
        },
        async () => {
          if (process.env.NODE_ENV === 'development') {
            const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
            performanceMeasure(measureName, markName);
          }
        },
      ],
      unmount: [
        async () => execHooksChain(toArray(beforeUnmount), app, global),
        async (props) => unmount({ ...props, container: appWrapperGetter() }),
        unmountSandbox,
        async () => execHooksChain(toArray(afterUnmount), app, global),
        async () => {
          render({ element: null, loading: false, container: remountContainer }, 'unmounted');
          offGlobalStateChange(appInstanceId);
          // for gc
          appWrapperElement = null;
          syncAppWrapperElement2Sandbox(appWrapperElement);
        },
        async () => {
          if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
            prevAppUnmountedDeferred.resolve();
          }
        },
      ],
    };

沙箱容器分析

export function createSandboxContainer(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  scopedCSS: boolean,
  useLooseSandbox?: boolean,
  excludeAssetFilter?: (url: string) => boolean,
  globalContext?: typeof window,
) {
  let sandbox: SandBox;
  if (window.Proxy) {
    sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
  } else {
    sandbox = new SnapshotSandbox(appName);
  }
  // 此处省略许多代码...   占位1
const bootstrappingFreers = patchAtBootstrapping(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter);
let mountingFreers: Freer[] = []; 
let sideEffectsRebuilders: Rebuilder[] = [];
  return {
    instance: sandbox,
    async mount() {
      // 此处省略许多代码... 占位2
      sandbox.active();
      // 此处省略许多代码... 占位3
    },
    async unmount() {
      // 此处省略许多代码... 占位4
      sandbox.inactive();
      // 此处省略许多代码... 占位5
    }
  };
}

这个对象包含三个属性instace, mount, unmount,其中instace代表沙箱实例,mount,unmount是两个方法,这俩方法使用的是sandbox.active, sandbox.inactive,两个方法让沙箱激活或者失活

patchAtBootstrapping

export function patchAtBootstrapping(
  appName: string,
  elementGetter: () => HTMLElement | ShadowRoot,
  sandbox: SandBox,
  scopedCSS: boolean,
  excludeAssetFilter?: CallableFunction,
): Freer[] {
  const patchersInSandbox = {
    [SandBoxType.LegacyProxy]: [
      () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
    [SandBoxType.Proxy]: [
      () => patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
    [SandBoxType.Snapshot]: [
      () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
    ],
  };

  return patchersInSandbox[sandbox.type]?.map((patch) => patch());
}

函数patchAtBootstrapping只做了一件事情,就是根据不同的沙箱类型,执行后并以数组的形式返回执行结果。

函数patchStrictSandbox

export function patchStrictSandbox(
  appName: string,
  appWrapperGetter: () => HTMLElement | ShadowRoot,
  proxy: Window,
  mounting = true,
  scopedCSS = false,
  excludeAssetFilter?: CallableFunction,
): Freer {
    //*********************第一部分*********************/
    let containerConfig = proxyAttachContainerConfigMap.get(proxy);
    if (!containerConfig) {
        containerConfig = {
          appName,
          proxy,
          appWrapperGetter,
          dynamicStyleSheetElements: [],
          strictGlobal: true,
          excludeAssetFilter,
          scopedCSS,
        };
        proxyAttachContainerConfigMap.set(proxy, containerConfig);
    }
    const { dynamicStyleSheetElements } = containerConfig;

    /***********************第二部分*********************/
    const unpatchDocumentCreate = patchDocumentCreateElement();
    const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
        (element) => elementAttachContainerConfigMap.has(element),
        (element) => elementAttachContainerConfigMap.get(element)!,
    );
    // 此处省略许多代码... 
  return function free() {
    // 此处省略许多代码... 占位2
    // 此处省略许多代码...
if (allMicroAppUnmounted) {
  unpatchDynamicAppendPrototypeFunctions();
  unpatchDocumentCreate();
}
recordStyledComponentsCSSRules(dynamicStyleSheetElements);
    return function rebuild() {
       // 此处省略许多代码... 占位3
    };
  };
}
let freeFunc = patchStrictSandbox(许多参数...); // 第一步:在这个函数里面执行了代码,影响了程序状态
let rebuidFun = freeFunc(); // 第二步:将第一步中对程序状态的影响撤销掉
rebuidFun();// 第三步:恢复到第一步执行完成时程序的状态

patchDocumentCreateElement

function patchDocumentCreateElement() {
    // 省略许多代码...
    const rawDocumentCreateElement = document.createElement;
    Document.prototype.createElement = function createElement(
        // 省略许多代码...
     ): HTMLElement {
      const element = rawDocumentCreateElement.call(this, tagName, options);
      // 关键点1
      if (isHijackingTag(tagName)) {
        // 省略许多代码
      }
      return element;
    };
    // 关键点2 
    if (document.hasOwnProperty('createElement')) {
      document.createElement = Document.prototype.createElement;
    }
    // 关键点3 
    docCreatePatchedMap.set(Document.prototype.createElement, rawDocumentCreateElement);
  }
    
  return function unpatch() {
    // 关键点4
    //此次省略一些代码...
    Document.prototype.createElement = docCreateElementFnBeforeOverwrite;
    document.createElement = docCreateElementFnBeforeOverwrite;
  };

主要是重写document.prototype.createElement

free函数

// 此处省略许多代码...
if (allMicroAppUnmounted) {
  unpatchDynamicAppendPrototypeFunctions();
  unpatchDocumentCreate();
}
recordStyledComponentsCSSRules(dynamicStyleSheetElements);
export function recordStyledComponentsCSSRules(styleElements: HTMLStyleElement[]): void {
  styleElements.forEach((styleElement) => {
    if (styleElement instanceof HTMLStyleElement && isStyledComponentsLike(styleElement)) {
      if (styleElement.sheet) {
        styledComponentCSSRulesMap.set(styleElement, (styleElement.sheet as CSSStyleSheet).cssRules);
      }
    }
  });
}

cssRules代表着一条条具体的css样式,从远程加载而来,对其中的内容进行解析,生成一个style标签。

rebuild

return function rebuild() {
  rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
    const appWrapper = appWrapperGetter();
    if (!appWrapper.contains(stylesheetElement)) {
      rawHeadAppendChild.call(appWrapper, stylesheetElement);
      return true;
    }

    return false;
  });
};
export function rebuildCSSRules(
  styleSheetElements: HTMLStyleElement[],
  reAppendElement: (stylesheetElement: HTMLStyleElement) => boolean,
) {
  styleSheetElements.forEach((stylesheetElement) => {
    const appendSuccess = reAppendElement(stylesheetElement);
    if (appendSuccess) {
      if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
        const cssRules = getStyledElementCSSRules(stylesheetElement);
        if (cssRules) {
          for (let i = 0; i < cssRules.length; i++) {
            const cssRule = cssRules[i];
            const cssStyleSheetElement = stylesheetElement.sheet as CSSStyleSheet;
            cssStyleSheetElement.insertRule(cssRule.cssText, cssStyleSheetElement.cssRules.length);
          }
        }
      }
    }
  });
}

将前面生成的style标签添加到微应用上,将之前保存的cssrule插入到对应的style标签上。

资源加载机制

在这里插入图片描述

importEntry

export function importEntry(entry, opts = {}) {
	const { fetch = defaultFetch, getTemplate = defaultGetTemplate, postProcessTemplate } = opts;
	const getPublicPath = opts.getPublicPath || opts.getDomain || defaultGetPublicPath;
	// 省略一些不太关键的代码...
	if (typeof entry === 'string') {
		return importHTML(entry, {
			fetch,
			getPublicPath,
			getTemplate,
			postProcessTemplate,
		});
	}
    // 此处省略了许多代码... 占位1
}
功能
  • 加载css/js资源,并且将加载的资源嵌入到html中
  • 获取script资源的exports对象
类型
  • entry。如果是string,importEntry会调用importHTML执行相关逻辑。会加载styles,scripts对应的资源嵌入到字符串html中,styles对应的style资源的url数组,scripts参数对应的是js资源的url数组,参数html是一个字符串,是一个html页面的具体内容
  • ImportEntryOpts: fetch: 自定义加载资源的方法,getPublicPath:自定义资源访问的相关路径。getTemplate: 自定义的html资源预处理的函数。

importHTML

export default function importHTML(url, opts = {}) {
	// 这里省略许多代码... 占位1
return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
    .then(response => readResAsString(response, autoDecodeResponse))
    .then(html => {
        const assetPublicPath = getPublicPath(url);
        const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath, postProcessTemplate);
        return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
            template: embedHTML,
            assetPublicPath,
            getExternalScripts: () => getExternalScripts(scripts, fetch),
            getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
            execScripts: (proxy, strictGlobal, execScriptsHooks = {}) => {
                if (!scripts.length) {
                    return Promise.resolve();
                }
                return execScripts(entry, scripts, proxy, {
                    fetch,
                    strictGlobal,
                    beforeExec: execScriptsHooks.beforeExec,
                    afterExec: execScriptsHooks.afterExec,
                });
            },
        }));
    }));
}
  • 调用fetch请求html资源
  • 调用processTpl处理资源
  • 调用getEmbedHTML对processTpl处理后的资源中的链接的远程js,css资源取到本地并嵌入到html中。

processTpl

// 代码片段3,所属文件:src/process-tpl.js
/*
  匹配整个script标签及其包含的内容,比如 <script>xxxxx</script>或<script xxx>xxxxx</script>

  [\s\S]    匹配所有字符。\s 是匹配所有空白符,包括换行,\S 非空白符,不包括换行
  *         匹配前面的子表达式零次或多次
  +         匹配前面的子表达式一次或多次

  正则表达式后面的全局标记 g 指定将该表达式应用到输入字符串中能够查找到的尽可能多的匹配。
  表达式的结尾处的不区分大小写 i 标记指定不区分大小写。   
*/
const ALL_SCRIPT_REGEX = /(<script[\s\S]*?>)[\s\S]*?<\/script>/gi;
/*
    . 匹配除换行符 \n 之外的任何单字符
    ? 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符。

    圆括号会有一个副作用,使相关的匹配会被缓存,此时可用 ?: 放在第一个选项前来消除这种副作用。
    其中 ?: 是非捕获元之一,还有两个非捕获元是 ?= 和 ?!, ?=为正向预查,在任何开始匹配圆括
    号内的正则表达式模式的位置来匹配搜索字符串,?!为负向预查,在任何开始不匹配该正则表达式模
    式的位置来匹配搜索字符串。
    举例:exp1(?!exp2):查找后面不是 exp2 的 exp1。

    所以这里的真实含义是匹配script标签,但type不能是text/ng-template
*/
const SCRIPT_TAG_REGEX = /<(script)\s+((?!type=('|")text\/ng-template\3).)*?>.*?<\/\1>/is;
/*
* 匹配包含src属性的script标签

  ^ 匹配输入字符串的开始位置,但在方括号表达式中使用时,表示不接受该方括号表达式中的字符集合。
*/
const SCRIPT_SRC_REGEX = /.*\ssrc=('|")?([^>'"\s]+)/;
// 匹配含 type 属性的标签
const SCRIPT_TYPE_REGEX = /.*\stype=('|")?([^>'"\s]+)/;
// 匹配含entry属性的标签//
const SCRIPT_ENTRY_REGEX = /.*\sentry\s*.*/;
// 匹配含 async属性的标签
const SCRIPT_ASYNC_REGEX = /.*\sasync\s*.*/;
// 匹配向后兼容的nomodule标记
const SCRIPT_NO_MODULE_REGEX = /.*\snomodule\s*.*/;
// 匹配含type=module的标签
const SCRIPT_MODULE_REGEX = /.*\stype=('|")?module('|")?\s*.*/;
// 匹配link标签
const LINK_TAG_REGEX = /<(link)\s+.*?>/isg;
// 匹配含 rel=preload或rel=prefetch 的标签, 小提示:rel用于规定当前文档与被了链接文档之间的关系,比如rel=“icon”等
const LINK_PRELOAD_OR_PREFETCH_REGEX = /\srel=('|")?(preload|prefetch)\1/;
// 匹配含href属性的标签
const LINK_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
// 匹配含as=font的标签
const LINK_AS_FONT = /.*\sas=('|")?font\1.*/;
// 匹配style标签
const STYLE_TAG_REGEX = /<style[^>]*>[\s\S]*?<\/style>/gi;
// 匹配rel=stylesheet的标签
const STYLE_TYPE_REGEX = /\s+rel=('|")?stylesheet\1.*/;
// 匹配含href属性的标签
const STYLE_HREF_REGEX = /.*\shref=('|")?([^>'"\s]+)/;
// 匹配注释
const HTML_COMMENT_REGEX = /<!--([\s\S]*?)-->/g;
// 匹配含ignore属性的 link标签
const LINK_IGNORE_REGEX = /<link(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
// 匹配含ignore属性的style标签
const STYLE_IGNORE_REGEX = /<style(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
// 匹配含ignore属性的script标签
const SCRIPT_IGNORE_REGEX = /<script(\s+|\s+.+\s+)ignore(\s*|\s+.*|=.*)>/is;
// 代码片段4,所属文件:src/process-tpl.js
export default function processTpl(tpl, baseURI, postProcessTemplate) {
    // 这里省略许多代码...
    let styles = [];
	const template = tpl
		.replace(HTML_COMMENT_REGEX, '') // 删掉注释
		.replace(LINK_TAG_REGEX, match => {
                // 这里省略许多代码...
                // 如果link标签中有ignore属性,则替换成占位符`<!-- ignore asset ${ href || 'file'} replaced by import-html-entry -->`
                // 如果link标签中没有ignore属性,将标签替换成占位符`<!-- ${preloadOrPrefetch ? 'prefetch/preload' : ''} link ${linkHref} replaced by import-html-entry -->`
		})
		.replace(STYLE_TAG_REGEX, match => {
                // 这里省略许多代码...
                // 如果style标签有ignore属性,则将标签替换成占位符`<!-- ignore asset style file replaced by import-html-entry -->`
		})
		.replace(ALL_SCRIPT_REGEX, (match, scriptTag) => {
                // 这里省略许多代码...
                // 这里虽然有很多代码,但可以概括为匹配正则表达式,替换成相应的占位符
		});

	// 这里省略一些代码...
	let tplResult = {
		template,
		scripts,
		styles,
		entry: entry || scripts[scripts.length - 1],
	};
	// 这里省略一些代码...
	return tplResult;
}

getEmbedHTML

function getEmbedHTML(template, styles, opts = {}) {
	const { fetch = defaultFetch } = opts;
	let embedHTML = template;

	return getExternalStyleSheets(styles, fetch)
		.then(styleSheets => {
			embedHTML = styles.reduce((html, styleSrc, i) => {
				html = html.replace(genLinkReplaceSymbol(styleSrc), `<style>/* ${styleSrc} */${styleSheets[i]}</style>`);
				return html;
			}, embedHTML);
			return embedHTML;
		});
}

export function getExternalStyleSheets(styles, fetch = defaultFetch) {
	return Promise.all(styles.map(styleLink => {
			if (isInlineCode(styleLink)) {
				// if it is inline style
				return getInlineCode(styleLink);
			} else {
				// external styles
				return styleCache[styleLink] ||
					(styleCache[styleLink] = fetch(styleLink).then(response => response.text()));
			}

		},
	));
}
  • 获取processTpl中提到style资源链接对应的资源内容
  • 将这些资源拼接称为style标签。然后将processTpl中的占位符替换掉

execScripts

export function execScripts(entry, scripts, proxy = window, opts = {}) {
	// 此处省略许多代码...
	return getExternalScripts(scripts, fetch, error)// 和获取js资源链接对应的内容
		.then(scriptsText => {
			const geval = (scriptSrc, inlineScript) => {
				// 此处省略许多代码...
                // 这里主要是把js代码进行一定处理,然后拼装成一个自执行函数,然后用eval执行
                // 这里最关键的是调用了getExecutableScript,绑定了window.proxy改变js代码中的this引用
			};

			function exec(scriptSrc, inlineScript, resolve) {
				// 这里省略许多代码...
				// 根据不同的条件,在不同的时机调用geval函数执行js代码,并将入口函数执行完暴露的含有微应用生命周期函数的对象返回
				// 这里省略许多代码...
			}

			function schedule(i, resolvePromise) {
                // 这里省略许多代码...
                // 依次调用exec函数执行js资源对应的代码
			}

			return new Promise(resolve => schedule(0, success || resolve));
		});
}

数据通信机制分析

export function registerMicroApps<T extends ObjectType>(
  apps: Array<RegistrableApp<T>>,
  lifeCycles?: FrameworkLifeCycles<T>,
) {
  // 省略许多代码...
  unregisteredApps.forEach((app) => {
    const { name, activeRule, loader = noop, props, ...appConfig } = app;

    registerApplication({
      name,
      app: async () => {
        // 省略许多代码...
        const { mount, ...otherMicroAppConfigs } = (
          await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
        )();
        // 省略许多代码...
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}

传入的props参数,在加载微应用的时候直接传入即可,这些参数在微应用执行生命周期方法的时候获取到。

全局事件通信

// 注:为了更容易理解,下面代码和源码中有点出入...
function onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) {
  // 该函数主要用于监听事件,将传入的callback函数进行保存
};

function setGlobalState(state: Record<string, any> = {}) {
  // 该函数主要用于更新数据,同时触发全局事件,调用函数onGlobalStateChange保存的对应callback函数
}

触发全局事件

function emitGlobal(state: Record<string, any>, prevState: Record<string, any>) {
  Object.keys(deps).forEach((id: string) => {
    if (deps[id] instanceof Function) {
      deps[id](cloneDeep(state), cloneDeep(prevState));
    }
  });
}

sinle-spa中的reroute函数

在这里插入图片描述

reroute函数的核心逻辑

export function reroute(pendingPromises = [], eventArguments) {
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();
  // 此处省略许多代码...
  if (isStarted()) {
    // 此处省略一些代码...
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    return loadApps();
  }
  // 此处省略许多代码...
}
  • 通过函数getAppChanges获取在single-spa注册过的微应用,并用四个数组变量来区分这些微应用下一步的处理以及状态。
  • 根据isStarted()的返回值进行判断,如果调用start函数,则调用performAppChanged函数根据getAppChanges函数的返回值对微应用进行相应的处理,然后改变相应的状态。如果微调用过start函数,则调用loadApp函数执行加载操作

getAppChanges

export function getAppChanges() {
  const appsToUnload = [],
    appsToUnmount = [],
    appsToLoad = [],
    appsToMount = [];
  // 此处省略一些代码...
  apps.forEach((app) => {
    const appShouldBeActive =
      app.status !== SKIP_BECAUSE_BROKEN && shouldBeActive(app);

    switch (app.status) {
      case LOAD_ERROR:
        if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
          appsToLoad.push(app);
        }
        break;
      case NOT_LOADED:
      case LOADING_SOURCE_CODE:
        if (appShouldBeActive) {
          appsToLoad.push(app);
        }
        break;
      case NOT_BOOTSTRAPPED:
      case NOT_MOUNTED:
        if (!appShouldBeActive && getAppUnloadInfo(toName(app))) {
          appsToUnload.push(app);
        } else if (appShouldBeActive) {
          appsToMount.push(app);
        }
        break;
      case MOUNTED:
        if (!appShouldBeActive) {
          appsToUnmount.push(app);
        }
        break;
    }
  });

  return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}

定义4个数组,然后根据微应用当前所处的不同状态,推断出函数即将要进入的状态,并把即将要进入同一个状态的微应用放到一个相同的数组中。

数组appsToLoad

appsToLoad: NOT_LOADED, LOADING_SOURCE_CODE
appsToLoad数组中存放的微应用在后续的逻辑中即将被加载,在加载中,状态会变化为LOADING_SOURCE_CODE。加载完成之后,状态变成为NOT_BOOTSTRAPPED。

export function toLoadPromise(app) {
  return Promise.resolve().then(() => {
    if (app.loadPromise) {
      return app.loadPromise;
    }
    if (app.status !== NOT_LOADED && app.status !== LOAD_ERROR) {
      return app;
    }
    // ...
    return (app.loadPromise = Promise.resolve()
      .then(() => {
        // ...
        delete app.loadPromise;
        // ...
      })
      .catch((err) => {
        delete app.loadPromise;
        // ...
      }));
  });
}

数组appsToUnload

处于NOT_BOOTSTRAPPED, NOT_MOUNTED状态的微应用,如果不需要处于激活状态且getAppUnloadInfo(toName(app))返回值为true,将微应用加载到appsToUnload中

export function getAppUnloadInfo(appName) {
  return appsToUnload[appName];
}

appsToUnload是一个全局对象,不是函数getAppChanges中的appsToUnload数组

unloadApplication

文档中的内容,如果希望重新执行bootstrap,可以调用unloadApplication函数是一个不错的选择。一般情况下,是不会轻易卸载微应用的,流程图中MOUNTED -> UNMOUNTING -> UNLOADING -> UNLOADED,如果不是用户手动柑橘,调用unloadApplication,是不会发生的。

toUnloadPromise

appsToUnload中的微应用即将被执行的主要逻辑都在函数toUnloadPromise中

export function toUnloadPromise(app) {
  return Promise.resolve().then(() => {
    const unloadInfo = appsToUnload[toName(app)];
    // 对象appsToUnload没有值,说明没有调用过unloadApplicaton函数,没必要继续
    if (!unloadInfo) {
      return app;
    }
    // 说明已经处于NOT_LOADED状态
    if (app.status === NOT_LOADED) {
      finishUnloadingApp(app, unloadInfo);
      return app;
    }
    // 已经在卸载中的状态,等执行结果就可以了,注意这里的promise是从对象appsToUnload上面取的
    if (app.status === UNLOADING) {
      return unloadInfo.promise.then(() => app);
    }
    // 应用的状态转换应该符合流程图所示,只有处于UNMOUNTED状态下的微应用才可以有->UNLOADING->UNLOADED的转化
    if (app.status !== NOT_MOUNTED && app.status !== LOAD_ERROR) {
      return app;
    }

    const unloadPromise =
      app.status === LOAD_ERROR
        ? Promise.resolve()
        : reasonableTime(app, "unload");

    app.status = UNLOADING;

    return unloadPromise
      .then(() => {
        finishUnloadingApp(app, unloadInfo);
        return app;
      })
      .catch((err) => {
        errorUnloadingApp(app, unloadInfo, err);
        return app;
      });
  });
}
  • 不能符合执行条件的情况进行拦截。
  • 利用reasonableTime函数真正的卸载的相关逻辑
  • 执行函数finishUnloadingApp或则errorUnloadingApp更改微任务的状态。
resonableTime
export function reasonableTime(appOrParcel, lifecycle) {
  // 此处省略许多代码...
  return new Promise((resolve, reject) => {
    // 此处省略许多代码...
    appOrParcel[lifecycle](getProps(appOrParcel))
      .then((val) => {
        finished = true;
        resolve(val);
      })
      .catch((val) => {
        finished = true;
        reject(val);
      });
    // 此处省略许多代码...
  });
}
  • 超时处理
  • 执行微应用的lifecycle变量对应的函数,在加载阶段让微应用具备了卸载的能力。
appsToMount, appsToUnmount, appsToMount

NOT_BOOTSTRAPPED NOT_MOUNTED状态的微应用,如果和路由规则匹配,则改微应用将会被添加到数组appsToMount。

performAppChanges

function performAppChanges() {
    return Promise.resolve().then(() => {
      // 此处省略许多代码...
      const unloadPromises = appsToUnload.map(toUnloadPromise);
      const unmountUnloadPromises = appsToUnmount
        .map(toUnmountPromise)
        .map((unmountPromise) => unmountPromise.then(toUnloadPromise));

      const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises);

      const unmountAllPromise = Promise.all(allUnmountPromises);

      unmountAllPromise.then(() => {
        // 此处省略许多代码...
      });
      // 此处省略许多代码...
      return unmountAllPromise
        .catch((err) => {
          callAllEventListeners();
          throw err;
        })
        .then(() => {
          callAllEventListeners();
          // 此处省略许多代码...
        });
    });
  }
  • 执行卸载逻辑
  • 执行完卸载逻辑后,在执行相关的挂载逻辑
  • 在不同阶段派发自定义事件

学习用,学习了博主杨艺韬的文章

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1945387.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

AI智能名片小程序在内容营销中的创新应用:以“48小时够你玩”系列为例

摘要&#xff1a;在数字化时代&#xff0c;内容营销已成为企业连接消费者、塑造品牌形象、推动销售增长的关键策略。AI智能名片小程序&#xff0c;作为新兴技术的集大成者&#xff0c;以其智能化、个性化、便捷化的特点&#xff0c;为内容营销注入了新的活力。本文深入探讨了AI…

[C++][STL源码剖析] 详解AVL树的实现

1.概念 二叉搜索树虽可以缩短查找的效率&#xff0c;但如果数据有序或接近有序二叉搜索树将退化为单支树&#xff0c;查找元素相当于在顺序表中搜索元素&#xff0c;效率低下。 因此&#xff0c;两位俄罗斯的数学家G.M.Adelson-Velskii 和E.M.Landis在1962年 发明了一种解决上…

医疗信息化之PACS系统源码,C#医学影像系统源码,成熟在用稳定运中

C#语言开发的一套PACS系统源码&#xff0c;C/S架构&#xff0c;成熟稳定&#xff0c;多家大型综合医院应用案例。自主版权&#xff0c;支持二次开发&#xff0c;授权后可商用。 医学影像存储与传输系统是针对数据库存储、传输服务、图像处理进行了优化,存储更安全、传输更稳定、…

《云原生安全攻防》-- 容器攻击案例:镜像投毒与Fork炸弹

在本节课程中&#xff0c;我们将介绍两个比较有意思的容器攻击案例&#xff0c;镜像投毒与Fork炸弹。 在这个课程中&#xff0c;我们将学习以下内容&#xff1a; 镜像投毒&#xff1a;构建恶意镜像&#xff0c;诱导用户拉取镜像创建容器。 Fork炸弹&#xff1a;Fork炸弹的攻击…

Axure设计之轮播图(动态面板+中继器)

轮播图&#xff08;Carousel&#xff09;是一种网页或应用界面中常见的组件&#xff0c;用于展示一系列的图片或内容&#xff0c;通常通过自动播放或用户交互&#xff08;如点击箭头按钮&#xff09;来切换展示不同的内容。轮播图能够吸引用户的注意力&#xff0c;有效展示重要…

无人机10公里WiFi图传摄像模组,飞睿智能超清远距离无线监控,智能安防新潮流

在这个科技日新月异的时代&#xff0c;我们对影像的捕捉和传播有了更高的要求。从传统的有线传输到无线WiFi图传&#xff0c;每一次技术的飞跃都为我们带来了全新的视觉体验。今天&#xff0c;我们要探讨的&#xff0c;正是一款具有划时代意义的科技产品——飞睿智能10公里WiFi…

PySide的style().unpolish()与style().unpolish()

在 Qt 框架中&#xff0c;polish() 方法用于重新应用样式表或更新控件的外观。它的作用是强制控件重新应用样式表&#xff0c;以确保外观的更新能反映在控件上。这个方法通常在控件的样式表更改或控件的属性发生变化后使用&#xff08;发生变化的属性与外观的改变有相关&#x…

国外镜像学术导航与国内专利资源中心查询网址神器!!!快收藏!

目录 ​编辑 中国专利公布公告 思谋学术&#xff1a;思谋学术_谷歌学术搜索和文献资源 谷歌学术 学术资源工具 学术文献检索 文献下载 科研快讯 中国专利公布公告 http://epub.cnipa.gov.cn/ 简介&#xff1a;中国国家知识产权局&#xff08;CNIPA&#xff09;的电…

Vivado生成网表文件并创建自定义IP

平台&#xff1a;vivado2018.3 应用场景&#xff0c;在设计的过程中&#xff0c;我们一些特定的模块需要交付给别人&#xff0c;但是又不想让他们看到其中的源码。就可以将源码封装成网表和IP文件。 vivado生成网表文件 设置综合。设置-flatten_hierarchy* 为full。 这里可…

一文了解图像处理、图像分析和图像理解

图像处理&#xff1a;利⽤计算机对图像进⾏去除噪声、增强、复原、分割、特征提取、识别、等处理的理论、⽅法和技术。狭义的图像处理主要是对图像进⾏各种加⼯&#xff0c;以改变图像的视觉效果并为⾃动识别奠定基础&#xff0c;或对图像进⾏压缩编码以减少所需存储空间。 图…

802.11无线网络权威指南(二):无线帧结构

802.11无线网络权威指南&#xff08;二&#xff09;&#xff1a;无线帧结构 无线协议桢的三种类型无线网络帧结构完整帧格式control frameDuration/IDAddressSequence ControlQoS ControlHT Control 字段Frame Body 帧体FCS 校验域 帧细节管理帧控制帧RTS 帧CTS 帧ACK 帧格式PS…

使用BenchmarkDotNet对C#代码进行基准测试

一、基准测试 基准测试&#xff08;benchmarking&#xff09;是一种测量和评估软件性能指标的活动。你可以在某个时候通过基准测试建立一个已知的性能水平&#xff08;称为基准线&#xff09;&#xff0c;当系统的软硬件环境发生变化之后再进行一次基准测试以确定那些变化对性…

手持气象站:掌中的天气预报员

在科技日新月异的今天&#xff0c;手持气象站作为一种便携、智能的气象监测设备&#xff0c;逐渐成为了户外探险、农业生产、环境监测等领域的得力助手。它以其小巧的体积、丰富的功能和精准的数据&#xff0c;赢得了广大用户的青睐。 一、手持气象站的概述 手持气象站是一种集…

Springboot 多数据源事务

起因 在一个service方法上使用的事务,其中有方法是调用的多数据源orderDB 但是多数据源没有生效,而是使用的primaryDB 原因 spring 事务实现的方式 以 Transactional 注解为例 (也可以看 TransactionTemplate&#xff0c; 这个流程更简单一点)。 入口&#xff1a;ProxyTransa…

算法通关:006_1二分查找

查找一个数组里面是否存在num import java.util.Arrays;/*** Author: ggdpzhk* CreateTime: 2024-07-23*/ public class _006 {//二分查找//保证数组有序才能用二分查找&#xff0c;不然无意义public static boolean exist(int[] arr,int num){if(arr null || arr.length 0)…

VUE3学习第二篇:报错记录

1、在我整理好前端代码框架后&#xff0c;而且也启动好了对应的后台服务&#xff0c;访问页面&#xff0c;正常。 2、报错ReferenceError: defineModel is not defined 学到这里报错了 在vue网站的演练场&#xff0c;使用没问题 但是在我自己的代码里就出问题了 3、watchEffec…

2024-07-23 Unity插件 Odin Inspector11 —— 使用 Odin 自定义编辑窗口

文章目录 1 OdinEditorWindow1.1 运作方式1.2 使用特性绘制 OdinEditorWindow1.3 在 OdinEditorWindow 中渲染对象 2 OdinMenuEditorWindow2.1 添加菜单导航栏2.2 添加导航栏示例 ​ Odin Window 可以完整地访问 Odin 绘图系统&#xff0c;不再需要操心 Window 的绘制 方式&am…

MySQL的主从复制以及读写分离

主从复制 主从复制的原理&#xff1b; 读写分离&#xff0c;MHA&#xff08;高可用&#xff09;的前提是得要有主从复制 主从复制的模式&#xff1a; 1、MySQL的默认模式&#xff1a; 异步模式 住主库在更新完事务之后会立即把结果返回给从服务器&#xff0c;并不关心从库…

Github 2024-07-17 开源项目日报 Top10

根据Github Trendings的统计,今日(2024-07-17统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量非开发语言项目3Python项目3Rust项目2TypeScript项目2MDX项目1项目化学习 创建周期:2538 天协议类型:MIT LicenseStar数量:161973 个Fork数量…

Unity UGUI 之 Slider

本文仅作学习笔记与交流&#xff0c;不作任何商业用途 本文包括但不限于unity官方手册&#xff0c;唐老狮&#xff0c;麦扣教程知识&#xff0c;引用会标记&#xff0c;如有不足还请斧正 1.Slider是什么 滑块&#xff0c;由三部分组成&#xff1a;背景 填充条 手柄 填充条就是…