我正在参加「掘金·启航计划」
前言
在第一章节中我们大致浏览了一下icestark是如何控制子应用的加载与卸载的,本章节我们将会看到完整的子应用加载的过程。
快速链接
-icestark源码解读(一):控制微应用加载与卸载的核心原理
概念
在阅读子应用加载的过程之前,我们先了解下几个概念,有助于我们后续的阅读。
microApps
一个存储所有子应用完整配置信息的全局变量,其类型是一个数组
子应用的6个状态
NOT_LOADED
标志着子应用的静态资源还没有下载,也就是子应用的静态资源还没有获取
LOADING_ASSETS
标志着子应用处于正在下载中
LOAD_ERROR
标志着子应用下载出现了错误
NOT_MOUNTED
标志着子应用的静态资源已经全部下载完成,但是没有挂载到主应用的DOM节点上
MOUNTED
标志着子应用已经插入到主应用的DOM节点上面了,此时网页中已经能够看到子应用的页面了
UNMOUNTED
标志着子应用已经从主应用的DOM节点上移除掉了
createMicroApp 函数
在上一章节中我们看到了reroute函数(控制子应用加载与卸载的函数),reroute函数中加载子应用的方法就是是createMicroApp函数。
export async function createMicroApp(
app: string | AppConfig,
appLifecyle?: AppLifecylceOptions,
configuration?: StartConfiguration,
) {
const appName = typeof app === 'string' ? app : app.name; // 获取子应用的唯一标识
if (typeof app !== 'string') {
registerAppBeforeLoad(app, appLifecyle); // 这一步其实就是将子应用放到全局变量microApps之中
}
mergeThenUpdateAppConfig(appName, configuration); // 合并更新子应用的配置信息
const appConfig = getAppConfig(appName); // 获取子应用的配置信息
if (!appConfig || !appName) {
console.error(`[icestark] fail to get app config of ${appName}`);
return null;
}
const { container, basename, activePath, configuration: userConfiguration, findActivePath } = appConfig;
if (container) {
setCache('root', container); // 缓存子应用的的根DOM节点
}
const { fetch } = userConfiguration;
if (shouldSetBasename(activePath, basename)) {
let pathString = findActivePath(window.location.href);
// When use `createMicroApp` lonely, `activePath` maybe not provided.
pathString = typeof pathString === 'string' ? pathString : '';
setCache('basename', getAppBasename(pathString, basename));
}
switch (appConfig.status) { // 子应用的status
case NOT_LOADED: // 未加载
case LOAD_ERROR: // 加载错误
await loadApp(appConfig);
break;
case UNMOUNTED: // 已卸载
if (!appConfig.cached) {
const appendAssets = [
...(appConfig?.appAssets?.cssList || []),
// In vite development mode, styles are inserted into DOM manually.
// While es module natively imported twice may never excute twice.
// https://github.com/ice-lab/icestark/issues/555
...(appConfig?.loadScriptMode === 'import' ? filterRemovedAssets(importCachedAssets[appConfig.name] ?? [], ['LINK', 'STYLE']) : []),
];
await loadAndAppendCssAssets(appendAssets, {
cacheCss: shouldCacheCss(appConfig.loadScriptMode),
fetch,
});
}
await mountMicroApp(appConfig.name);
break;
case NOT_MOUNTED: // 未挂载
await mountMicroApp(appConfig.name);
break;
default:
break;
}
return getAppConfig(appName); // 返回要注册的子应用配置信息
}
在createMicroApp函数中,先是拿到子应用的完整配置信息,然后将子应用注册到全局变量microApps之中,注册完成之后合并更新子应用的配置信息,紧接着缓存子应用的挂载目标节点,然后为子应用统一添加路由匹配的basename, 再根据子应用的状态去决定执行加载、挂载子应用。我们在这里顺便看下icestark内部的cache是如何做到的
cache
const namespace = 'ICESTARK'; // 命名空间
/**
* 设置缓存
* @param key
* @param value
*/
export const setCache = (key: string, value: any): void => {
if (!(window as any)[namespace]) {
(window as any)[namespace] = {};
}
(window as any)[namespace][key] = value;
};
/**
* 从缓存中读取数据
* @param key
*/
export const getCache = (key: string): any => {
const icestark: any = (window as any)[namespace];
return icestark && icestark[key] ? icestark[key] : null;
};
icestark的cache很简单,就是将数据直接挂到window对象身上的ICESTARK属性下。
registerAppBeforeLoad 函数
function registerAppBeforeLoad(app: AppConfig, options?: AppLifecylceOptions) {
const { name } = app; // 取出子应用的唯一标识
const appIndex = getAppNames().indexOf(name); // 查看存储所有子应用的全局变量中是否有当前子应用
if (appIndex === -1) {
registerMicroApp(app, options); // 注册子应用
} else {
updateAppConfig(name, app); // 更新子应用的配置信息
}
return getAppConfig(name);
}
该函数其实本质是在判断全局变量microApps中是否有目标子应用,有就更新配置,没有的话就执行registerMicroApp函数追加进去。
registerMicroApp 函数
/**
* 注册子应用
* @param appConfig
* @param appLifecyle
*/
export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylceOptions) {
// check appConfig.name
if (getAppNames().includes(appConfig.name)) {
throw Error(`name ${appConfig.name} already been regsitered`);
}
const { activePath, hashType = false, exact = false, sensitive = false, strict = false } = appConfig;
/**
* Format activePath in advance
*/
const activePathArray = formatPath(activePath, {
hashType,
exact,
sensitive,
strict,
});
const { basename: frameworkBasename } = globalConfiguration;
const findActivePath = findActivePathCurry(mergeFrameworkBaseToPath(activePathArray, frameworkBasename));
const microApp = {
status: NOT_LOADED,
...appConfig,
appLifecycle: appLifecyle,
findActivePath,
};
microApps.push(microApp); // 向全局 microApps 数组变量插入子应用
}
从该函数中我们可以看到其最终目的是向microApps变量中插入子应用的配置信息,并且在push之前将子应用的状态设置为了NOT_LOADED。所以说icestark中的注册子应用的目的就是向microApps变量中插入子应用的配置信息。
mergeThenUpdateAppConfig函数
function mergeThenUpdateAppConfig(name: string, configuration?: StartConfiguration) {
const appConfig = getAppConfig(name); // 获取子应用配置信息
if (!appConfig) {
return;
}
const { umd, sandbox } = appConfig;
// 位子应用创建一个沙箱
const appSandbox = createSandbox(sandbox) as Sandbox;
// 获取加载子应用js的方式是fetch还是script链接
const sandboxEnabled = sandbox && !appSandbox.sandboxDisabled;
const loadScriptMode = appConfig.loadScriptMode ?? (umd || sandboxEnabled ? 'fetch' : 'script');
// Merge global configuration
const cfgs = {
...globalConfiguration,
...configuration,
};
// 更新子应用的配置
updateAppConfig(name, {
appSandbox,
loadScriptMode,
configuration: cfgs,
});
}
该函数的主要作用就是为子应用创建一个沙箱并且决定加载子应用js的方式
loadApp 函数
/**
* 下载子应用
* @param app
*/
async function loadApp(app: MicroApp) {
const { title, name, configuration } = app;
if (title) {
document.title = title; // 更改页面的标题
}
updateAppConfig(name, { status: LOADING_ASSETS }); // 更新子应用的状态为正在加载资源 LOADING_ASSETS
let lifeCycle: ModuleLifeCycle = {};
try {
lifeCycle = await loadAppModule(app); // 加载子应用的js和css资源
// in case of app status modified by unload event
if (getAppStatus(name) === LOADING_ASSETS) {
updateAppConfig(name, { ...lifeCycle, status: NOT_MOUNTED }); // 更新子应用的状态为未挂载
}
} catch (err) {
configuration.onError(err);
log.error(err);
updateAppConfig(name, { status: LOAD_ERROR }); // 更新子应用的状态为加载错误
}
if (lifeCycle.mount) {
await mountMicroApp(name); // 子应用静态资源获取完成后执行挂载App
}
}
该函数的主要逻辑是;更改浏览器页签的标题文字,将子应用的状态设置为LOADING_ASSETS正在加载资源中,调用loadAppModule函数去获取子应用的静态资源,获取完成之后更改子应用的状态为NOT_MOUNTED未挂载状态, 最后调用mountMicroApp函数去挂载子应用。现在我们去看下loadAppModule函数是如何去拿到子应用的静态资源的。
loadAppModule 函数
/**
* 下载子应用的核心逻辑
* @param appConfig 子应用的配置信息
* @returns
*/
export async function loadAppModule(appConfig: AppConfig) {
const { onLoadingApp, onFinishLoading, fetch } = getAppConfig(appConfig.name)?.configuration || globalConfiguration;
let lifecycle: ModuleLifeCycle = {};
onLoadingApp(appConfig); // 执行子应用开始加载的回调 onLoadingApp
const { url, container, entry, entryContent, name, scriptAttributes = [], loadScriptMode, appSandbox } = appConfig;
// 根据配置的子应用的url 或者entry去获取子应用的静态资源文件对应的url地址
const appAssets = url ? getUrlAssets(url) : await getEntryAssets({
root: container,
entry,
href: location.href,
entryContent,
assetsCacheKey: name,
fetch,
});
updateAppConfig(appConfig.name, { appAssets }); // 更新子应用的配置信息
const cacheCss = shouldCacheCss(loadScriptMode); // 是否要缓存css
switch (loadScriptMode) {
case 'import': // 说明是ESM应用
await loadAndAppendCssAssets([
...appAssets.cssList,
...filterRemovedAssets(importCachedAssets[name] || [], ['LINK', 'STYLE']),
], {
cacheCss,
fetch,
});
lifecycle = await loadScriptByImport(appAssets.jsList);
// Not to handle script element temporarily.
break;
case 'fetch':
await loadAndAppendCssAssets(appAssets.cssList, {
cacheCss,
fetch,
});
lifecycle = await loadScriptByFetch(appAssets.jsList, appSandbox, fetch);
break;
default:
await Promise.all([
loadAndAppendCssAssets(appAssets.cssList, {
cacheCss,
fetch,
}),
loadAndAppendJsAssets(appAssets, { scriptAttributes }),
]);
lifecycle =
getLifecyleByLibrary() ||
getLifecyleByRegister() ||
{};
}
if (isEmpty(lifecycle)) {
log.error(
formatErrMessage(
ErrorCode.EMPTY_LIFECYCLES,
isDev && 'Unable to retrieve lifecycles of {0} after loading it',
appConfig.name,
),
);
}
onFinishLoading(appConfig); // 执行子应用加载完成的回调 onLoadingApp
return combineLifecyle(lifecycle, appConfig);
}
该函数中先是根据子应用配置的是url还是entry来去调用getUrlAssets或者getEntryAssets去将子应用的资源地址组合成jsList & cssList的形式。然后根据子应的加载模式(fetch or import or script)来去执行loadAndAppendCssAssets loadScriptByImport loadScriptByFetch loadAndAppendJsAssets这些函数,将css与js获取并插入。
组合子应用资源的格式
getUrlAssets
/**
* 根据js和css的url去组合成cssList 和 jsList的数据格式
* @param urls
*/
export function getUrlAssets(urls: string | string[]) {
const jsList = [];
const cssList = [];
toArray(urls).forEach((url) => {
// //icestark.com/index.css -> true
// //icestark.com/index.css?timeSamp=1575443657834 -> true
// //icestark.com/index.css?query=test.js -> false
const isCss: boolean = IS_CSS_REGEX.test(url); // 判断是不是css url
const assest: Asset = {
type: AssetTypeEnum.EXTERNAL, // 给静态资源指定类型,inline / external
content: url, // 静态资源的url地址
};
if (isCss) {
cssList.push(assest);
} else {
jsList.push(assest);
}
});
return { jsList, cssList };
}
getEntryAssets
/**
* 通过子应用的ip + 端口号 获取子应用的静态资源并插入到主应用的DOM节点上面
* @param root
* @param entry
* @param entryContent
* @param assetsCacheKey
* @param href
* @param fetch
*/
export async function getEntryAssets({
root,
entry,
entryContent,
assetsCacheKey,
href = location.href,
fetch = defaultFetch,
}: {
root?: HTMLElement | ShadowRoot; // 子应用要挂载到主应用上的节点
entry?: string; // 子应用的访问地址 ip + port
entryContent?: string; // 开发者自己配置的 子应用html结构
assetsCacheKey: string; // 缓存数据的key值
href?: string;
fetch?: Fetch;
assertsCached?: boolean;
}) {
const cachedContent = cachedProcessedContent[assetsCacheKey];
let htmlContent = entryContent;
if (!cachedContent) {
if (!htmlContent && entry) {
if (!fetch) {
log.warn('Current environment does not support window.fetch, please use custom fetch');
throw new Error(
`fetch ${entry} error: Current environment does not support window.fetch, please use custom fetch`,
);
}
// 通过fetch api 根据entry地址获取子应用的html字符串
htmlContent = await fetch(entry).then((res) => res.text());
}
// 缓存子应用的html字符串
cachedProcessedContent[assetsCacheKey] = htmlContent;
}
// 解析html字符串,拿到静态资源的资源的url
const { html, assets } = processHtml(cachedContent || htmlContent, entry || href);
if (root) {
root.appendChild(html); // 将子应用插入到主应用的DOM节点上面
}
return assets;
}
该函数的作用是利用配置的entry(子应用部署所在的ip+端口号),通过fetchapi 去获取到子应用的html结构字符串,通过processHtml函数从html结构字符串中取出html结构以及html中css 与 js的url,最后将子应用的html内容append到主应用的节点上面。
processHtml 函数
/**
* 从html字符串中提取子应用DOM结构以及根据entry组合子应用静态资源js与css的url地址
* @param html
* @param entry
*/
export function processHtml(html: string, entry?: string): ProcessedContent {
if (!html) return { html: document.createElement('div'), assets: { cssList: [], jsList: [] } };
// 通过DOMParser将html字符串获取完整的DOM对象
const domContent = (new DOMParser()).parseFromString(html.replace(COMMENT_REGEX, ''), 'text/html');
// 创建base标签,将子应用html中所有相对路径全部变成指向子应用entry的绝对路径
if (entry) {
const baseElementMatch = html.match(BASE_LOOSE_REGEX);
const baseElements = domContent.getElementsByTagName('base');
const hasBaseElement = baseElements.length > 0;
if (baseElementMatch && hasBaseElement) {
// Only take the first one into consideration.
const baseElement = baseElements[0];
const [, baseHerf] = baseElementMatch;
// 将子应用的资源url由相对路径改成带有子应用域名端口号的绝对路径
baseElement.href = isAbsoluteUrl(baseHerf) ? baseHerf : getUrl(entry, baseHerf);
} else {
// add base URI for absolute resource.
// see more https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
const base = document.createElement('base');
// <base /> element also takes effects if href includues `.html`
base.href = entry;
domContent.getElementsByTagName('head')[0].appendChild(base);
}
}
// 获取子应用中所有的script标签
const scripts = Array.from(domContent.getElementsByTagName('script'));
const processedJSAssets = scripts.map((script) => {
const inlineScript = script.src === EMPTY_STRING; // 判断是不是行内script
const module = script.type === 'module'; // 判断是不是ESModule
// 获取子应用非行内script的url(entry + 资源路径) ---- 暂且成为外部js
const externalSrc = !inlineScript && (isAbsoluteUrl(script.src) ? script.src : getUrl(entry, script.src));
const commentType = inlineScript ? AssetCommentEnum.PROCESSED : AssetCommentEnum.REPLACED;
// 使用注释来代替script标签节点进行占位
replaceNodeWithComment(script, getComment('script', inlineScript ? 'inline' : script.src, commentType));
return {
module, // ESModule的标识
type: inlineScript ? AssetTypeEnum.INLINE : AssetTypeEnum.EXTERNAL, // 行内还是外部js
content:
inlineScript
? (
// If entryContent provided, skip this.
(module && entry)
? replaceImportIdentifier(script.text, entry)
: script.text)
: externalSrc,
};
});
// 获取子应用中所有的style标签
const inlineStyleSheets = Array.from(domContent.getElementsByTagName('style')); // 获取行内样式
const externalStyleSheets = Array.from(domContent.getElementsByTagName('link')) // 获取link标签外部样式
.filter((link) => !link.rel || link.rel.includes(STYLESHEET_LINK_TYPE));
const processedCSSAssets = [
...inlineStyleSheets
.map((sheet) => {
replaceNodeWithComment(sheet, getComment('style', 'inline', AssetCommentEnum.REPLACED)); // 使用注释来代替style标签节点
return {
type: AssetTypeEnum.INLINE,
content: sheet.innerText,
};
}),
...externalStyleSheets
.map((sheet) => {
replaceNodeWithComment(sheet, getComment('link', sheet.href, AssetCommentEnum.PROCESSED)); // 使用注释来代替link标签节点
return {
type: AssetTypeEnum.EXTERNAL,
content: isAbsoluteUrl(sheet.href) ? sheet.href : getUrl(entry, sheet.href),
};
}),
];
if (entry) {
// 移除之前为子应用创建的base标签,以此来避免影响主应用
const baseNodes = domContent.getElementsByTagName('base');
for (let i = 0; i < baseNodes.length; ++i) {
baseNodes[i]?.parentNode.removeChild(baseNodes[i]);
}
}
return {
html: domContent.getElementsByTagName('html')[0], // 子应用的html DOM对象 (此时已经移除了子应用自己全部的script以及css,由icestark注释来代替占位)
assets: {
jsList: processedJSAssets, // 子应用js静态资源
cssList: processedCSSAssets, // 子应用css静态资源
},
};
}
-
该函数接收html字符串&entry作为参数,首先是利用
new DOMParser()将html从字符串解析成DOM对象。在解析的过程中会遇到一个问题,就是子应用的html 对象中的<script />和<link />的src或者href地址是相对路径,此时在主应用中运行子应用那么肯定是相对于主应用的,而主应用里面并没有子应用引用的资源。为了避免这个问题,我们就需要把子应用的相对路径更改为绝对路径(子应用的entry与子应用相对路径合并成为绝对路径)。 -
具体需要怎么做,我们可以看到源码中先是创建一个
base标签,并指定base标签的href属性值为子应用的entry,然后将base标签插入至子应用的document文档中的header标签内,这样浏览器在解析子应用时会将子应用的所有相对路径前加上base的href。这样子应用document中所有的路径都会指向子应用的entry路径。 base标签介绍 -
添加base标签之后,紧接着就是遍历子应用所有的script与style link标签,拿到所有的js与css的url以及行内js、css,同时将行内和外部引用的js与css从子应用的document中移除掉,使用icestark专门的注释来对其进行占位。
loadAndAppendCssAssets 函数
/**
* 下载并插入 css assets
*
* @export
* @param {Assets} assets
*/
export async function loadAndAppendCssAssets(cssList: Array<Asset | HTMLElement>, {
cacheCss = false,
fetch = defaultFetch,
}: {
cacheCss?: boolean;
fetch?: Fetch;
}) {
const cssRoot: HTMLElement = document.getElementsByTagName('head')[0]; // 主应用的head标签
if (cacheCss) {
let useLinks = false;
let cssContents = null;
try {
// No need to cache css when running into `<style />`
const needCachedCss = cssList.filter((css) => !isElement(css));
cssContents = await fetchStyles(
needCachedCss as Asset[],
fetch,
);
} catch (e) {
useLinks = true;
}
// Try hard to avoid break-change if fetching links error.
// And supposed to be remove from 3.x
if (!useLinks) {
return await Promise.all([
...cssContents.map((content, index) => appendCSS(
cssRoot,
{ content, type: AssetTypeEnum.INLINE }, `${PREFIX}-css-${index}`,
)),
...cssList.filter((css) => isElement(css)).map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)),
]);
}
}
// load css content
return await Promise.all(
cssList.map((asset, index) => appendCSS(cssRoot, asset, `${PREFIX}-css-${index}`)),
);
}
该函数主要作用是根据子应用的css资源的url来获取css并插入到主应用document之中。
首先是拿到主应用的header标签作为css插入的根节点,然后根据css的url,通过fetch api进行调用获取。对于插入到主应用document中子应用的style和link标签会添加icestark=dynamic标识,以此来与主应用的css资源进行区分。同时还需要监听每一个link标签的load事件,等所有的link标签链接的外部样式资源全部下载完成,再去执行后面的代码
/**
* 创建 link/style 元素 接受子应用的css资源 并且 插入 到 主应用的head标签内部
*/
export function appendCSS(
root: HTMLElement | ShadowRoot,
asset: Asset | HTMLElement,
id: string,
): Promise<void> {
return new Promise<void>(async (resolve, reject) => {
if (!root) reject(new Error('no root element for css asset'));
if (isElement(asset)) { // asset 是DOM 的话,直接插入即可
root.append(asset);
resolve();
return;
}
const { type, content } = asset;
if (type && type === AssetTypeEnum.INLINE) { // 行内样式的话直接插入到style标签即可
const styleElement: HTMLStyleElement = document.createElement('style');
styleElement.id = id;
styleElement.setAttribute(PREFIX, DYNAMIC);
styleElement.innerHTML = content;
root.appendChild(styleElement);
resolve();
return;
}
/**
* if external resource is cached by prefetch, use cached content instead.
* For cachedStyleContent may fail to fetch (cors, and so on),recover to original way
*/
let useExternalLink = true;
if (type && type === AssetTypeEnum.EXTERNAL && cachedStyleContent[content]) { // 从缓存中,直接取出样式资源并插入到主应用的head标签即可
try {
const styleElement: HTMLStyleElement = document.createElement('style');
styleElement.innerHTML = await cachedStyleContent[content];
styleElement.id = id;
styleElement.setAttribute(PREFIX, DYNAMIC);
root.appendChild(styleElement);
useExternalLink = false;
resolve();
} catch (e) {
useExternalLink = true;
}
}
if (useExternalLink) { // 说明是需要外部链接样式资源
const element: HTMLLinkElement = document.createElement('link');
element.setAttribute(PREFIX, DYNAMIC);
element.id = id;
element.rel = 'stylesheet';
element.href = content;
// 监听元素加载错误情况
element.addEventListener(
'error',
() => {
log.error(
formatErrMessage(
ErrorCode.CSS_LOAD_ERROR,
isDev && 'The stylesheets loaded error: {0}',
(content || asset) as string,
),
);
return resolve();
},
false,
);
// 监听元素加载完成
element.addEventListener('load', () => resolve(), false);
// 将link标签插入到主应用的head标签内部
root.appendChild(element);
}
});
}
loadScriptByImport函数
/**
* 下载 es modules 子应用并且获取顺序的生命周期.
* `import` returns a promise for the module namespace object of the requested module which means
* + non-export returns empty object
* + default export return object with `default` key
*/
export async function loadScriptByImport(jsList: Asset[]): Promise<null | ModuleLifeCycle> {
let mount = null;
let unmount = null;
await asyncForEach(jsList, async (js, index) => {
if (js.type === AssetTypeEnum.INLINE) { // 加载行内js
await appendExternalScript(js, {
id: `${PREFIX}-js-module-${index}`,
});
} else { // 加载外部js
let dynamicImport = null;
try {
/**
* 使用 new Function 去检测浏览器是否支持import 函数导入js的语法
* Then use `new Function` to escape compile error.
* Inspired by [dynamic-import-polyfill](https://github.com/GoogleChromeLabs/dynamic-import-polyfill)
*/
// eslint-disable-next-line no-new-func
dynamicImport = new Function('url', 'return import(url)');
} catch (e) {
return Promise.reject(
new Error(
formatErrMessage(
ErrorCode.UNSUPPORTED_IMPORT_BROWSER,
isDev && 'You can not use loadScriptMode = import where dynamic import is not supported by browsers.',
),
),
);
}
try {
if (dynamicImport) {
// 使用import函数去导入es module的js
const { mount: maybeMount, unmount: maybeUnmount } = await dynamicImport(js.content);
if (maybeMount && maybeUnmount) {
mount = maybeMount;
unmount = maybeUnmount;
}
}
} catch (e) {
return Promise.reject(e);
}
}
});
if (mount && unmount) {
return {
mount,
unmount,
};
}
return null;
}
该函数的主要作用是加载ES Module的子应用,根据子应用的js资源以及url来获取子应用js并插入到主应用document之中。
-
对于行内js,直接创建script标签去引用子应用的js,同时添加icestark=dynamic标识用于区分主应用的js,又由于加载的是es module, 故需要给script标签设置type=module属性,最后插入到主应用的header标签内。
-
对于外部js,采用import函数的方式引入,由于在chorme 61之下以及ie上 直接使用import函数导入会报错,故源码里面采用es6 的 new Function 方式创建一个函数用于去执行import(子应用外部js的url)代码,这样做的目的是去检测浏览器是否支持import函数方式导入js,如果不支持则会抛出可被try catch捕捉的错误
loadScriptByFetch 函数
/**
* 通过fetch的方式去下载子应用的js
*/
export function loadScriptByFetch(jsList: Asset[], sandbox?: Sandbox, fetch = window.fetch) {
return fetchScripts(jsList, fetch) // fetchScripts 获取js
.then((scriptTexts) => {
const globalwindow = getGobalWindow(sandbox);
const libraryExport = executeScripts(scriptTexts, sandbox, globalwindow); // executeScripts 执行js
let moduleInfo = getLifecyleByLibrary() || getLifecyleByRegister();
if (!moduleInfo) {
moduleInfo = (libraryExport ? globalwindow[libraryExport] : {}) as ModuleLifeCycle;
if (globalwindow[libraryExport]) {
delete globalwindow[libraryExport];
}
}
return moduleInfo;
});
}
/**
* 通过window.fetch获取js文本
* @param jsList
* @param fetch
*/
export function fetchScripts(jsList: Asset[], fetch: Fetch = defaultFetch) {
return Promise.all(jsList.map((asset) => {
const { type, content } = asset;
if (type === AssetTypeEnum.INLINE) { // 对于行内js直接返回
return content;
} else { // 对于外部js利用fetch api 获取
// content will script url when type is AssetTypeEnum.EXTERNAL
// eslint-disable-next-line no-return-assign
return cachedScriptsContent[content]
/**
* If code is being evaluated as a string with `eval` or via `new Function`,then the source origin
* will be the page's origin. As a result, `//# sourceURL` appends to the generated code.
* See https://sourcemaps.info/spec.html
*/
|| (cachedScriptsContent[content] = fetch(content)
.then((res) => res.text())
.then((res) => `${res} \n //# sourceURL=${content}`)
);
}
}));
}
/**
* 采用eval函数去执行js
* @param scripts
* @param sandbox
* @param globalwindow
*/
function executeScripts(scripts: string[], sandbox?: Sandbox, globalwindow: Window = window) {
let libraryExport = null;
for (let idx = 0; idx < scripts.length; ++idx) {
const lastScript = idx === scripts.length - 1;
if (lastScript) {
noteGlobalProps(globalwindow);
}
if (sandbox?.execScriptInSandbox) {
sandbox.execScriptInSandbox(scripts[idx]);
} else {
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/eval
// eslint-disable-next-line no-eval
(0, eval)(scripts[idx]); // eval 执行js
}
if (lastScript) {
libraryExport = getGlobalProp(globalwindow);
}
}
return libraryExport;
}
该函数是加载子应用js的第二种方式,主要通过window.fetch api 去获取子应用的js,拿到js之后,通过
eval函数去执行js。
- 对于行内js,直接运行
eval函数执行 - 对于外部js,先通过fetch去获取,拿到字符串类型的js代码之后,再去运行
eval函数执行
loadAndAppendJsAssets 函数
/**
* 下载并插入 js 资源, compatible with v1
* @export
* @param {Assets} assets
* @param {Sandbox} [sandbox]
* @returns
*/
export function loadAndAppendJsAssets(
assets: Assets,
{
scriptAttributes = [],
}: {
scriptAttributes?: ScriptAttributes;
},
) {
const jsRoot: HTMLElement = document.getElementsByTagName('head')[0]; // 获取主应用的head标签
const { jsList } = assets; // 获取js静态资源
// 加载js
const hasInlineScript = jsList.find((asset) => asset.type === AssetTypeEnum.INLINE);
if (hasInlineScript) {
// make sure js assets loaded in order if has inline scripts
return jsList.reduce((chain, asset, index) => {
return chain.then(() => appendExternalScript(asset, {
root: jsRoot,
scriptAttributes,
id: `${PREFIX}-js-${index}`,
}));
}, Promise.resolve());
}
return Promise.all(
jsList.map((asset, index) => appendExternalScript(asset, {
root: jsRoot,
scriptAttributes,
id: `${PREFIX}-js-${index}`,
})),
);
}
/**
* Create script element (without inline) and append to root
* 创建script标签并插入到主应用的head标签内部
*/
export function appendExternalScript(asset: string | Asset,
{
id,
root = document.getElementsByTagName('head')[0],
scriptAttributes = [],
}: {
id: string;
root?: HTMLElement | ShadowRoot;
scriptAttributes?: ScriptAttributes;
}): Promise<void> {
return new Promise<void>((resolve, reject) => {
const { type, content, module } = (asset as Asset);
// 创建script标签
const element: HTMLScriptElement = document.createElement('script');
// 行内 script 代码 直接插入到创建的script标签里面即可
if (type && type === AssetTypeEnum.INLINE) {
element.innerHTML = content;
element.id = id;
element.setAttribute(PREFIX, DYNAMIC); // 添加icestark=dynamic标识
module && (element.type = 'module');
root.appendChild(element);
/*
* For inline script never fire onload event, resolve it immediately.
*/
resolve();
return;
}
// 给script标签设置自定义的属性
setAttributeForScriptNode(element, {
module,
id,
src: content || (asset as string),
scriptAttributes,
});
if (isAssetExist(element, 'script')) {
resolve();
return;
}
element.addEventListener(
'error',
() => {
reject(
new Error(
formatErrMessage(
ErrorCode.JS_LOAD_ERROR,
isDev && 'The script resources loaded error: {0}',
(content || asset) as string,
),
),
);
},
false,
);
// 监听外部js 加载完成事件
element.addEventListener('load', () => resolve(), false);
// 将外部链接的script代码添加至主应用的head标签内
root.appendChild(element);
});
}
该函数是默认下载子应用js的一种方式,
- 对于行内js,直接创建script标签去引用子应用的js,同时添加icestark=dynamic标识用于区分主应用的js,插入到主应用的header标签内。
- 对于外部js,同样也是创建script标签,设置src为外部js的URL,添加icestark=dynamic标识,并监听script标签的load事件,等待子应用所有的js下载完成。
mountMicroApp 函数
/**
* 执行子应用挂载的生命周期函数
* @param appName
*/
export async function mountMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
// check current url before mount
const shouldMount = appConfig?.mount && appConfig?.findActivePath(window.location.href);
if (shouldMount) {
// 执行子应用的挂载生命周期函数
if (appConfig?.mount) {
await appConfig.mount({ container: appConfig.container, customProps: appConfig.props });
}
// 更新子应用的状态为已挂载
updateAppConfig(appName, { status: MOUNTED });
}
}
该函数用于在子应用静态资源获取完成并插入到主应用document之后,去执行子应用的已挂载生命周期,以及更新microApps中子应用的状态为 MOUNTED 已挂载。该函数是上面loadApp函数的最后一步执行代码。到此子应用的挂载就完成了。
总结
经过上面的源码阅读,现在可以对icestark加载子应用做一个简要的总结。
- 定义
microApps全局变量去储存所有子应用的配置信息 - 解析子应用的html结构,从中获取子应用的行内以及外部js与css
- 对于css的加载,直接创建link与style标签去引用子应用的css资源,并插入到主应用的header标签内
- 对于js的加载,分为三种方式:es module 子应用js加载 、 fetch 加载子应用js、 默认加载子应用js
- es module 子应用js加载
创建script标签包裹行内js,利用import函数,导入外部js
- fetch 加载子应用js
无论是行内js还是外部js,都是通过eval函数去执行,只不过外部js要先通过fetch api 获取js 代码。
- 默认加载子应用js
无论是行内js还是外部js,都是直接创建script标签去引用子应用的js资源,并插入到主应用的header标签内
- 子应用加载完成,执行子应用配置的挂载生命周期函数,更新
microApps中对应子应用的配置信息