time: 2021.6.9
author: heyunjiang
目前项目采用的是众多模块共享一个前端项目,存在如下问题
简单一句话:庞大应用拆分,由多个小应用组成
特点
iframe 优点
iframe 缺点
加载对应 js 文件,暴露对应组件或实例变量
优点
待解决技术点
微前端解决方案之一,内部集成 single-spa
主应用:registerMicroApps、registerApplication、loadApp、start,根据 url 动态匹配子应用 子应用:提供 name、entry(uri 地址) 等
问题
关键实现原理 1 - loadApp
import { importEntry } from 'import-html-entry';
export async function loadApp<T extends object>(
app: LoadableApp<T>,
configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObject> {
// entry 表示子应用的 uri
const { entry, name: appName, render: legacyRender, container } = app;
const { singular = true, jsSandbox = true, cssIsolation = false, ...importEntryOpts } = configuration;
// 拿到入口 html 、可执行的脚本、静态资源连接
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
// 等待其他子应用 unmounting
if (await validateSingularMode(singular, app)) {
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
}
const appInstanceId = `${appName}_${
appInstanceCounts.hasOwnProperty(appName) ? (appInstanceCounts[appName] ?? 0) + 1 : 0
}`;
const appContent = getDefaultTplWrapper(appInstanceId)(template);
let element: HTMLElement | null = createElement(appContent, cssIsolation);
const render = getRender(appContent, container, legacyRender);
// 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕
render({ element, loading: true });
// 沙箱配置
const containerGetter = getAppWrapperGetter(appInstanceId, !!legacyRender, cssIsolation, () => element);
let global: Window = window;
let mountSandbox = () => Promise.resolve();
let unmountSandbox = () => Promise.resolve();
if (jsSandbox) {
const sandbox = genSandbox(appName, containerGetter, Boolean(singular));
// 用沙箱的代理对象作为接下来使用的全局对象
global = sandbox.sandbox;
mountSandbox = sandbox.mount;
unmountSandbox = sandbox.unmount;
}
const { beforeUnmount = [], afterUnmount = [], afterMount = [], beforeMount = [], beforeLoad = [] } = mergeWith(
{},
getAddOns(global, assetPublicPath),
lifeCycles,
(v1, v2) => concat(v1 ?? [], v2 ?? []),
);
// 执行 beforeLoad 钩子
await execHooksChain(toArray(beforeLoad), app);
// 解析脚本,并缓存其返回的 promise 结果
if (!appExportPromiseCaches[appName]) {
appExportPromiseCaches[appName] = execScripts(global, !singular);
}
// 读取子应用配置的 bootstrap、mount、unmount 约束
const scriptExports: any = await appExportPromiseCaches[appName];
let bootstrap;
let mount: any;
let unmount: any;
if (validateExportLifecycle(scriptExports)) {
// eslint-disable-next-line prefer-destructuring
bootstrap = scriptExports.bootstrap;
// eslint-disable-next-line prefer-destructuring
mount = scriptExports.mount;
// eslint-disable-next-line prefer-destructuring
unmount = scriptExports.unmount;
} else {
if (process.env.NODE_ENV === 'development') {
console.warn(
`[qiankun] lifecycle not found from ${appName} entry exports, fallback to get from window['${appName}']`,
);
}
// fallback to global variable who named with ${appName} while module exports not found
const globalVariableExports = (global as any)[appName];
if (validateExportLifecycle(globalVariableExports)) {
// eslint-disable-next-line prefer-destructuring
bootstrap = globalVariableExports.bootstrap;
// eslint-disable-next-line prefer-destructuring
mount = globalVariableExports.mount;
// eslint-disable-next-line prefer-destructuring
unmount = globalVariableExports.unmount;
} else {
delete appExportPromiseCaches[appName];
throw new Error(`[qiankun] You need to export lifecycle functions in ${appName} entry`);
}
}
return {
name: appInstanceId,
bootstrap: [bootstrap],
mount: [
// 1 等待其他子应用 unmounting
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
// 2 第二次渲染。添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
async () => {
// element would be destroyed after unmounted, we need to recreate it if it not exist
element = element || createElement(appContent, cssIsolation);
render({ element, loading: true });
},
// 3 执行 beforeMount 钩子
async () => execHooksChain(toArray(beforeMount), app),
// 4 沙箱 mount
mountSandbox,
// 5 应用 mount
async props => mount({ ...props, container: containerGetter() }),
// 6 应用 mount 完成后结束 loading
async () => render({ element, loading: false }),
// 7 执行 afterMount 钩子
async () => execHooksChain(toArray(afterMount), app),
// initialize the unmount defer after app mounted and resolve the defer after it unmounted
async () => {
if (await validateSingularMode(singular, app)) {
prevAppUnmountedDeferred = new Deferred<void>();
}
},
],
unmount: [
async () => execHooksChain(toArray(beforeUnmount), app),
async props => unmount({ ...props, container: containerGetter() }),
unmountSandbox,
async () => execHooksChain(toArray(afterUnmount), app),
async () => {
render({ element: null, loading: false });
// for gc
element = null;
},
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
prevAppUnmountedDeferred.resolve();
}
},
],
};
}
关键实现原理 2 - mount
这里是子应用内部提供的渲染方法,可以自行控制渲染到什么地方
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, document.getElementById('react15Root'));
}
关键实现原理 3 - importEntry
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
是如何通过入口解析文档的呢?返回的又是什么数据格式?脚本获取是在 importEntry 还是 execScripts 获取?
不同于 url 刷新跳转,这里目的是实现无缝刷新
export function importEntry(entry, opts = {}) {
if (typeof entry === 'string') {
return importHTML(entry, { fetch, getPublicPath, getTemplate });
}
}
export default function importHTML(url, opts = {}) {
let fetch = defaultFetch; // 也就是 window.fetch
let getPublicPath = defaultGetPublicPath;
let getTemplate = defaultGetTemplate;
return embedHTMLCache[url] || (embedHTMLCache[url] = fetch(url)
.then(response => response.text())
.then(html => {
const assetPublicPath = getPublicPath(url);
// processTpl 采用一系列正则匹配,查找 template, style, script
const { template, scripts, entry, styles } = processTpl(getTemplate(html), assetPublicPath);
return getEmbedHTML(template, styles, { fetch }).then(embedHTML => ({
template: embedHTML,
assetPublicPath,
getExternalScripts: () => getExternalScripts(scripts, fetch),
getExternalStyleSheets: () => getExternalStyleSheets(styles, fetch),
execScripts: proxy => {
if (!scripts.length) {
return Promise.resolve();
}
return execScripts(entry, scripts, proxy, { fetch });
},
}));
}));
};
子应用提供者:暴露出对应的模块,统一打包进指定 filename 文件,通过 exposes 读取对应组件,还可以通过 shared 共享公共模块
父应用获取:通过