微前端--qiankun

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} = appregisterApplication({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 = appContainerconst appELement = containerElement.firstChild as HTMLElementreturn 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.tsconst 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 remountingif (marks && !marks.length) {performanceMark(markName);}}},async () => {if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {return prevAppUnmountedDeferred.promise;}return undefined;},// initial wrapper element before app mount/remountasync () => {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 containerappWrapperElement = 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 beforeLoadasync () => execHooksChain(toArray(beforeMount), app, global),async (props) => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }),// finish loading after app mountedasync () => 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 unmountedasync () => {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 gcappWrapperElement = 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() {// 此处省略许多代码... 占位2sandbox.active();// 此处省略许多代码... 占位3},async unmount() {// 此处省略许多代码... 占位4sandbox.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);// 关键点1if (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 stylereturn getInlineCode(styleLink);} else {// external stylesreturn 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.mzph.cn/pingmian/49475.shtml

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

相关文章

【中项】系统集成项目管理工程师-第5章 软件工程-5.4软件实现

前言&#xff1a;系统集成项目管理工程师专业&#xff0c;现分享一些教材知识点。觉得文章还不错的喜欢点赞收藏的同时帮忙点点关注。 软考同样是国家人社部和工信部组织的国家级考试&#xff0c;全称为“全国计算机与软件专业技术资格&#xff08;水平&#xff09;考试”&…

51单片机嵌入式开发:14、STC89C52RC 之HX1838红外解码NEC+数码管+串口打印+LED显示

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 STC89C52RC 之HX1838红外解码NEC数码管串口打印LED显示 STC89C52RC 之HX1838红外解码NEC数码管串口打印LED显示1 概述2 硬件电路2.1 遥控器2.2 红外接收器电路2.3 STC89C52单…

【JAVA开发笔记】Reids下载、安装、配置-Windows篇(超详细,含Redis可视化管理工具!!!)

目录 1. Redis 简介 2. 下载 Redis 安装包 3. 开启 Redis 服务 4. 配置环境变量 5. Redis 服务注册为系统服务 6. Redis 服务测试和简单使用 7. 下载安装 Redis 管理工具 8. 管理工具连接 Redis 服务器 1. Redis 简介 Redis&#xff08;Remote Dictionary Server&…

Pytorch transforms 的研究

绝对路径与相对路径差别 transforms的使用 from torchvision import transforms from PIL import Imageimg_path "dataset/train/bees/16838648_415acd9e3f.jpg" img Image.open(img_path) tensor_trans transforms.ToTensor() tensor_img tensor_trans(img) prin…

PostgreSQL 中如何实现数据的批量插入和更新?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 PostgreSQL 中如何实现数据的批量插入和更新&#xff1f;一、批量插入数据1. 使用 INSERT INTO 语句结…

redis的持久化机制以及集群模式

1.redis的持久化机制 内存数据库具有高速读写的优势&#xff0c;但由于数据存储在内存中&#xff0c;一旦服务器停止或崩溃&#xff0c;所有数据将会丢失。持久化机制的引入旨在将内存中的数据持久化到磁盘上&#xff0c;从而在服务器重启后能够恢复数据&#xff0c;提供更好的…

初阶数据结构——二叉树大汇总

这篇博客将会讲到二叉树的部分内容及堆的相关知识~ 这里将会涉及到大量的递归&#xff08;头大&#xff09; 目录 1.树 1.1树的概念 1.2树的相关概念 1.3树的表示 1.4树的实际应用 2.二叉树 2.1二叉树的概念 2.2特殊的二叉树 2.2.1 满二叉树 2.2.2 完全二叉树 2.2…

如何用python在大麦网抢票?新手也能学会!

使用Python如何在大麦网抢票 背景介绍 大麦网是一个知名的演出票务平台&#xff0c;很多演唱会、体育赛事等热门活动的门票都可以在这里购买。由于热门场次的门票往往会在开售时秒光&#xff0c;因此抢票成为了很多人的一项技术活。本文将介绍如何使用Python编写程序来自动在大…

基于STM32的逻辑分析仪

文章目录 一、逻辑分析仪体验1、使用示例1.1 逻辑分析仪1.2 开源软件PulseView 2、核心技术2.1 技术方案2.2 信号采集与存储2.3 数据上传 3、使用逻辑分析仪4、 SourceInsight 使用技巧4.1新建工程4.2 设置工程名及工程数据目录4.3 指定源码目录4.4 添加源码4.5 同步文件4.6 操…

在windows上使用Docker部署一个简易的web程序

使用Docker部署一个python的web服务&#x1f680; 由于是从事算法相关工作&#xff0c;之前在项目中&#xff0c;需要将写完的代码服务&#xff0c;部署在docker上&#xff0c;以此是开始接触了Docker这个工具&#xff0c;由于之前也没系统学习过&#xff0c;之后应该可能还会用…

视频压缩大小怎么压缩?几种简单视频压缩方法教给你

现如今&#xff0c;视频已成为我们生活和工作中不可或缺的一部分。然而&#xff0c;高清视频往往伴随着庞大的文件体积&#xff0c;这给存储和传输带来了不小的挑战。这时候我们就需要对视频进行压缩处理&#xff0c;方便储存和发送&#xff0c;那么怎么有效压缩视频呢&#xf…

java之回合制游戏以及如何优化

public class Role {private String name;private int blood;//空参public Role() {}//包含全部参数的构造public Role(String name, int blood) {this.name name;this.blood blood;}public String getName() {return name;}public void setName(String name) {this.name na…

提交高通量测序原始数据到 SRA --- 操作流程

❝ 写在前面 由于最近在提交课题数据到 NCBI 数据库&#xff0c;整理了相关笔记。本着自己学习、分享他人的态度&#xff0c;分享学习笔记&#xff0c;希望能对大家有所帮助。推荐先按顺序阅读往期内容&#xff1a; 1. 提交高通量测序数据到 GEO --- 说明书 目录 1 注册 NCBI 账…

【C++】关联容器探秘:Map与Multimap详解

目录 1.映射类 map 0. 引入 pair&#xff1a; 1.定义 2.插入 3. 遍历 4.❗operator[]的实现 5. 插入 运用 2.Multimap 类 0. 引入&#xff1a;不去重的 Multi 1. Multimap 不支持 Operator[] 2. Multimap 的删除 1.映射类 map 0. 引入 pair&#xff1a; 在C中&…

1 go语言环境的搭建

本专栏将从基础开始&#xff0c;循序渐进&#xff0c;由浅入深讲解Go语言&#xff0c;希望大家都能够从中有所收获&#xff0c;也请大家多多支持。 查看相关资料与知识库 专栏地址:Go专栏 如果文章知识点有错误的地方&#xff0c;请指正&#xff01;大家一起学习&#xff0c;…

软件测试---测试需求分析

课程目标 什么是软件测试需求 软件测试需求的必要性 如何对软件测试需求进行分析&#xff08;重点&#xff09; 课程补充 灰度测试&#xff08;基于功能&#xff09;&#xff1a;先发布部分功能&#xff0c;然后看用户的反馈&#xff0c;再去发布另外一部分的功能更新。 A/B测…

运筹学笔记

计算的时间问题&#xff01;计算机解决了计算量的问题&#xff01; 计算机的发展对运筹学研究起到了极大的促进作用。 运筹学的一个特征之一是它常常会考虑寻求问题模型的最佳解决方案&#xff08;称为最优解&#xff09;。 没有人能成为运筹学所有方面的专家。 分析学越来越流…

C++学习笔记04-补充知识点(问题-解答自查版)

前言 以下问题以Q&A形式记录&#xff0c;基本上都是笔者在初学一轮后&#xff0c;掌握不牢或者频繁忘记的点 Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系&#xff0c;也适合做查漏补缺和复盘。 本文对读者可以用作自查&#xff0c;答案在后面&#xff0…

国内微短剧系统平台抖音微信付费小程序app开发源代码交付

微短剧作为当下热门的内容&#xff0c;结合抖音平台的广泛用户基础&#xff0c;开发微短剧付费小程序APP具有显著的市场潜力&#xff0c;用户对于短剧内容的需求旺盛&#xff0c;特别是在言情、总裁、赘婿等热门题材方面&#xff0c;接下来给大家普及一下微短剧小程序系统。 顺…

rce漏洞-ctfshow(50-70)

Web51 if(!preg_match("/\;|cat|flag| |[0-9]|\\$|\*|more|less|head|sort|tail|sed|cut|tac|awk|strings|od|curl|\|\%|\x09|\x26/i", $c)){ system($c." >/dev/null 2>&1"); } Nl&#xff0c;绕过tac&#xff0c;cat&#xff0c;绕…