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();
// 此处省略许多代码...
});
});
}
- 执行卸载逻辑
- 执行完卸载逻辑后,在执行相关的挂载逻辑
- 在不同阶段派发自定义事件
学习用,学习了博主杨艺韬的文章