qiankun 微前端总览
前言
qiankun源码版本为:2.5.1。import-html-entry源码版本为:1.11.1。single-spa源码版本为:5.9.3。
总览
qiankun从window对象代理,子应用样式代理,fetch请求 &eval执行 js 文件,html-entry四个方面来达到简化single-spa使用的目的。qiankun引入了import-html-entry,其中window对象代理和子应用样式代理的部分是由qiankun完成的;import-html-entry承担了fetch请求 &eval执行 js 文件,html-entry的功能。- 微前端的核心工作:路由劫持,子应用切换,还是需要
single-spa来完成。
qiankun 部分
-
window代理src/sandbox/proxySandbox.ts的 第 69行~ 126行,196行~361行function createFakeWindow(globalContext: Window) { ... } ... export default class ProxySandbox implements SandBox { constructor(name: string, globalContext = window) { ... const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext); ... const proxy = new Proxy(fakeWindow, { set: , get:, has, getOwnPropertyDescriptor, ownKeys, defineProperty, deleteProperty: , getPrototypeOf() { }, }); this.proxy = proxy; }proxySandbox是qiankun中多种沙箱的其中一种,是最常用的一种沙箱。这里有一个问题:
new Proxy生成的代理对象,是在哪里使用的?也就是说,我们在子应用中使用的window对象肯定都是new Proxy生成的代理对象,那么这个new Proxy生成的代理对象是在什么时候替换window 对象的?答案就是
import-html-entry的eval过程中,具体原理我会在import-html-entry部分详细说一下。 -
子应用样式代理
子应用的样式在子应用的生命周期中过程,会经历添加,卸载,再添加,再卸载的过程。这个功能是
qiankun通过拦截link和style标签的添加和移除实现的src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts, 第78行~141行export function patchStrictSandbox() { let containerConfig = proxyAttachContainerConfigMap.get(proxy); const { dynamicStyleSheetElements } = containerConfig; const unpatchDocumentCreate = patchDocumentCreateElement(); const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( (element) => elementAttachContainerConfigMap.has(element), (element) => elementAttachContainerConfigMap.get(element)!, ); return function free() { if (allMicroAppUnmounted) { unpatchDynamicAppendPrototypeFunctions(); unpatchDocumentCreate(); } recordStyledComponentsCSSRules(dynamicStyleSheetElements); };src/sandbox/patchers/dynamicAppend/common.ts中, 第139行 ~ 264行,第306行 ~ 357行function getOverwrittenAppendChildOrInsertBefore() { return function appendChildOrInsertBefore() { ... if (element.tagName) { switch (element.tagName) { case LINK_TAG_NAME: case STYLE_TAG_NAME: { ... const mountDOM = appWrapperGetter(); if (scopedCSS) { // exclude link elements like <link rel="icon" href="favicon.ico"> const linkElementUsingStylesheet = element.tagName?.toUpperCase() === LINK_TAG_NAME && (element as HTMLLinkElement).rel === 'stylesheet' && (element as HTMLLinkElement).href; if (linkElementUsingStylesheet) { const fetch = typeof frameworkConfiguration.fetch === 'function' ? frameworkConfiguration.fetch : frameworkConfiguration.fetch?.fn; stylesheetElement = convertLinkAsStyle( element, (styleElement) => css.process(mountDOM, styleElement, appName), fetch, ); dynamicLinkAttachedInlineStyleMap.set(element, stylesheetElement); } else { css.process(mountDOM, stylesheetElement, appName); } } // eslint-disable-next-line no-shadow dynamicStyleSheetElements.push(stylesheetElement); const referenceNode = mountDOM.contains(refChild) ? refChild : null; return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); } ... } return rawDOMAppendOrInsertBefore.call(this, element, refChild); }; }export function patchHTMLDynamicAppendPrototypeFunctions( isInvokedByMicroApp: (element: HTMLElement) => boolean, containerConfigGetter: (element: HTMLElement) => ContainerConfig, ) { // Just overwrite it while it have not been overwrite if ( HTMLHeadElement.prototype.appendChild === rawHeadAppendChild && HTMLBodyElement.prototype.appendChild === rawBodyAppendChild && HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore ) { HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ rawDOMAppendOrInsertBefore: rawHeadAppendChild, containerConfigGetter, isInvokedByMicroApp, }) as typeof rawHeadAppendChild; ... HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({ rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any, containerConfigGetter, isInvokedByMicroApp, }) as typeof rawHeadInsertBefore; } if ( HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild && HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild ) { HTMLHeadElement.prototype.removeChild = getNewRemoveChild( rawHeadRemoveChild, (element) => containerConfigGetter(element).appWrapperGetter, ); ... } return function unpatch() { HTMLHeadElement.prototype.appendChild = rawHeadAppendChild; HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild; HTMLBodyElement.prototype.appendChild = rawBodyAppendChild; HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild; HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore; }; }import-html-entry 部分
-
fetch请求 &eval执行 js 文件浏览器端的
javaScript的模块化规范有:AMD,CMD,ES模块等规范。 其中的流程都是要:请求js 文件,然后执行 js 文件。AMD和CMD的实现,都是通过生成scritp标签来完成。 但是这样的话,就无法实现window对象的代理。因为qiankun沙箱生成的window对象的代理, 需要通过eval 函数传递过来。src/index.js中,execScripts方法中 第151行~237行,可以看到传递了一个proxy的对象,proxy就是qiankun沙箱生成的window代理对象。
export function execScripts(entry, scripts, proxy = window, opts = {}) { return getExternalScripts(scripts, fetch, error) .then(scriptsText => { const geval = (scriptSrc, inlineScript) => { ... const code = getExecutableScript(scriptSrc, rawCode, proxy, strictGlobal); ... }; }); }src/index.js中,第54行~65行
function getExecutableScript(scriptSrc, scriptText, proxy, strictGlobal) { const sourceUrl = isInlineCode(scriptSrc) ? '' : `//# sourceURL=${scriptSrc}\n`; // 通过这种方式获取全局 window,因为 script 也是在全局作用域下运行的,所以我们通过 window.proxy 绑定时也必须确保绑定到全局 window 上 // 否则在嵌套场景下, window.proxy 设置的是内层应用的 window,而代码其实是在全局作用域运行的,会导致闭包里的 window.proxy 取的是最外层的微应用的 proxy const globalWindow = (0, eval)('window'); globalWindow.proxy = proxy; // TODO 通过 strictGlobal 方式切换 with 闭包,待 with 方式坑趟平后再合并 return strictGlobal ? `;(function(window, self, globalThis){with(window){;${scriptText}\n${sourceUrl}}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);` : `;(function(window, self, globalThis){;${scriptText}\n${sourceUrl}}).bind(window.proxy)(window.proxy, window.proxy, window.proxy);`; } -
html-entryhtml-entry是相对于js-entry而言的,single-spa要求每个子应用打包出来一个js 文件,例如vue项目就需要把public/index.html在打包过程中给干掉(带来了复杂度)。html-entry就避免了这种工作量,在打包过程中不做任何调整,对项目的侵入性就小了很多。这里还会引申出一个问题:例如,
vue项目在使用webpack打包过程中,借助于代码分割, es 的懒加载功能,会生成多个js 文件。webpack会编译代码为createElementscript标签。这些js 文件,是怎么被qiankun框架拦截的?答案是在
qiankuan的源码src/sandbox/patchers/dynamicAppend/common.ts中,第139行~264行, 在向dom 中新增script标签时,调用了import-html-entry开放的execScripts方法,避免了script标签的插入。function getOverwrittenAppendChildOrInsertBefore() { return function appendChildOrInsertBefore() { switch (element.tagName) { ... case SCRIPT_TAG_NAME: { const { src, text } = element as HTMLScriptElement; ... const mountDOM = appWrapperGetter(); const { fetch } = frameworkConfiguration; const referenceNode = mountDOM.contains(refChild) ? refChild : null; if (src) { execScripts(null, [src], proxy, { fetch, strictGlobal, beforeExec: () => { const isCurrentScriptConfigurable = () => { const descriptor = Object.getOwnPropertyDescriptor(document, 'currentScript'); return !descriptor || descriptor.configurable; }; if (isCurrentScriptConfigurable()) { Object.defineProperty(document, 'currentScript', { get(): any { return element; }, configurable: true, }); } }, success: () => { manualInvokeElementOnLoad(element); element = null; }, error: () => { manualInvokeElementOnError(element); element = null; }, }); const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`); dynamicScriptAttachedCommentMap.set(element, dynamicScriptCommentElement); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); } execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal }); const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); } ... } } }; }
single-spa 部分
-
路由劫持
src/navigation/navigation-events.js中, 第139行~197行if (isInBrowser) { window.addEventListener("hashchange", urlReroute); window.addEventListener("popstate", urlReroute); ... window.history.pushState = patchedUpdateState( window.history.pushState, "pushState" ); window.history.replaceState = patchedUpdateState( window.history.replaceState, "replaceState" ); }
single-spa-vue 部分
-
生命周期钩子函数
qiankun通过import-html-entry,已经可以实现dom的挂载,卸载,更新逻辑,所以single-spa-vue在qiankun框架中不需要了(但是需要导出相应的生命周期钩子函数)。
其他
欢迎大家关注微信公众号:赵公子聊前端。