微前端实现的核心要素
- 子应用的加载
- 应用间运行时隔离
- 应用间通信
- 路由劫持
对于qiankun来说,路由劫持是在singleSpa上去做的
qiankun是single-spa的一层封装
在qiankun中,真正去加载解析子应用的逻辑是在import-html-entry这个包中实现的
子应用加载流程
- 拿到子应用的entry配置的链接
- 通过fetch获取链接返回的html字符串
- 解析html字符串
- 拿到html模板
- 通过fetch获取外联css内容,并插入到html模板中
- 通过fetch获取外联js,并执行
- 通过appendChild将html字符串插入到container配置的节点下
html解析
- 当我们配置子应用的entry后,qiankun会去通过fetch获取到子应用的html字符串(子应用资源需要允许跨域);
- 拿到html字符串后,会调用
processTpl方法通过一大堆正则去匹配获取html中对应得js(内联、外联)、css(内联、外联)、注释、入口脚本entry等等。
processTpl方法会返回我们加载子应用所需要的四个组成部分:
-
template : html模板
-
script : js脚本(内联、外联)
-
styles : css样式表(内联、外联)
-
entry : 子应用入口js脚本文件,如果没有默认解析到最后一个js脚本代替
function processTpl(tpl, baseURI) {
return {
template: template,
scripts: scripts,
styles: styles,
// set the last script as entry if have not set
entry: entry || scripts[scripts.length - 1]
};
}
CSS处理
接下来在拿到子应用依赖的各种资源关系后,会去通过fetch获取css,并将css全部以内联的形式插入到html模板中。到此对css的处理大致就完成了。
function getEmbedHTML(template, styles) {
var opts = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
var _opts$fetch = opts.fetch,
fetch = _opts$fetch === void 0 ? defaultFetch : _opts$fetch;
var embedHTML = template;
return _getExternalStyleSheets(styles, fetch).then(function (styleSheets) {
embedHTML = styles.reduce(function (html, styleSrc, i) {
html = html.replace((0, _processTpl2.genLinkReplaceSymbol)(styleSrc), "<style>/* ".concat(styleSrc, " */").concat(styleSheets[i], "</style>"));
return html;
}, embedHTML);
return embedHTML;
});
}
js处理
接下来是对js的处理,这里qiankun 和 icestark的处理模式就不同了
首先简单说下icestark, icestark是在解析完html后拿到子应用的js依赖,通过动态创建script标签的形式去加载js,因此在icestark是无视js跨域的(icestark的entry模式和url模式均是如此, 区别在于entry模式多了异步fetch拉html字符串并解析js、css依赖,而url模式只需要指定子应用的脚本和样式依赖即可)。
而qiankun则采用了另一种办法,首先同理会通过fetch获取外联的js字符串。
function _getExternalScripts(scripts) {
var fetch = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultFetch;
var errorCallback = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : function () {};
var fetchScript = function fetchScript(scriptUrl) {
return scriptCache[scriptUrl] || (scriptCache[scriptUrl] = fetch(scriptUrl).then(function (response) {
// usually browser treats 4xx and 5xx response of script loading as an error and will fire a script error event
// https://stackoverflow.com/questions/5625420/what-http-headers-responses-trigger-the-onerror-handler-on-a-script-tag/5625603
if (response.status >= 400) {
errorCallback();
throw new Error("".concat(scriptUrl, " load failed with status ").concat(response.status));
}
return response.text();
}));
};
return Promise.all(scripts.map(function (script) {
if (typeof script === 'string') {
if (isInlineCode(script)) {
// if it is inline script
return (0, _utils.getInlineCode)(script);
} else {
// external script
return fetchScript(script);
}
} else {
// use idle time to load async script
var src = script.src,
async = script.async;
if (async) {
return {
src: src,
async: true,
content: new Promise(function (resolve, reject) {
return (0, _utils.requestIdleCallback)(function () {
return fetchScript(src).then(resolve, reject);
});
})
};
}
return fetchScript(src);
}
}));
}
接下来会创建一个匿名自执行函数包裹住获取到的js字符串,最后通过eval去创建一个上下文执行js代码。
function _execScripts(entry, scripts) {
return _getExternalScripts(scripts, fetch, error).then(function (scriptsText) {
var geval = function geval(scriptSrc, inlineScript) {
var rawCode = beforeExec(inlineScript, scriptSrc) || inlineScript;
var code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal);
(0, eval)(code);
afterExec(inlineScript, scriptSrc);
};
子应用的挂载相关代码
一个子应用的配置如下
{
name: 'react16',
entry: '//localhost:7100',
container: '#subapp-viewport',
loader,
activeRule: '/react16',
},
通过entry配置的链接,获取到的html字符串,最终会通过appendChild挂载到容器#subapp-viewport中
- 调用loadApp()方法,
export async function loadApp<T extends ObjectType>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> {
const initialContainer = 'container' in app ? app.container : undefined;
const legacyRender = 'render' in app ? app.render : undefined;
const render = getRender(appName, appContent, legacyRender);
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
}
- 调用render()方法,render函数的第一个参数为html字符串
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
const appContent = getDefaultTplWrapper(appInstanceId, appName)(template);
let initialAppWrapperElement: HTMLElement | null = createElement(
appContent,
strictStyleIsolation,
scopedCSS,
appName,
);
function getRender(appName: string, appContent: string, legacyRender?: HTMLContentRender) {
const render: ElementRender = ({ element, loading, container }, phase) => {
if (legacyRender) {
if (process.env.NODE_ENV === 'development') {
console.warn(
'[qiankun] Custom rendering function is deprecated, you can use the container element setting instead!',
);
}
return legacyRender({ loading, appContent: element ? appContent : '' });
}
const containerElement = getContainer(container!);
// The container might have be removed after micro app unmounted.
// Such as the micro app unmount lifecycle called by a react componentWillUnmount lifecycle, after micro app unmounted, the react component might also be removed
if (phase !== 'unmounted') {
const errorMsg = (() => {
switch (phase) {
case 'loading':
case 'mounting':
return `Target container with ${container} not existed while ${appName} ${phase}!`;
case 'mounted':
return `Target container with ${container} not existed after ${appName} ${phase}!`;
default:
return `Target container with ${container} not existed while ${appName} rendering!`;
}
})();
assertElementExist(containerElement, errorMsg);
}
if (containerElement && !containerElement.contains(element)) {
// clear the container
while (containerElement!.firstChild) {
rawRemoveChild.call(containerElement, containerElement!.firstChild);
}
// append the element to container if it exist
if (element) {
rawAppendChild.call(containerElement, element);
}
}
return undefined;
};
return render;
}