preface
With the development of technology, the content carried by front-end applications is increasingly complex, and various problems arising from this have emerged, from MPA (multi-page Application) to SPA (single-page Application), While it solved the switching experience latency problem, it also introduced the Monolithic application problem caused by long first load times, and the explosive growth of the engineering. For MPA, its deployment is simple, applications are naturally hard isolated, and it has the characteristics of technology stack independence, independent development, and independent deployment. If you can combine these two characteristics, will it lead to a better user experience for users and developers? At this point, under the reference of micro service concept, the micro front end came into being.
An architectural style where independently deliverable frontend applications are composed into a greater whole. [Micro Frontends from martinfowler.com]
(martinfowler.com/articles/mi…).
According to martinfowler’s definition of a microfront end: Micro front end is a kind of multiple front-end application by an independent delivery of the overall architectural style, the front end and micro service is a kind of architectural style, so it is not a framework or library, but a style or a thought, so in order to realize the micro front-end solutions have a lot of kinds, the most common solution has the following kinds:
- Routing distribution
-
iframe
-
Application microservices
- Micro parts,
- The application of the
- Web Components
Related comparison:
plan | Development costs | Maintenance costs | The feasibility of | Same framework requirements | To realize the difficulty | The potential risk | The ground practice |
---|---|---|---|---|---|---|---|
Routing distribution | low | low | high | no | easy | There is no | HTTP server reverse proxy, such as nginx configuration location |
iframe | low | low | high | no | easy | Seo unfriendly, cookie management, communication mechanism, popover problem, refresh back, security issues | The front and back ends do not separate items commonly used |
Application microservices | high | low | In the | no | hard | Sharing and isolation granularity is inconsistent | Qiankun, ICestark, MOOA and similar single-SPA applications |
Micro parts, | high | In the | low | is | hard | Implement the micro component management mechanism | There is no |
The application of the | In the | In the | high | is | normal | Multiple portfolio projects, each deployment escalation needs to be considered | emp |
Web Components | high | low | high | no | normal | New API, browser compatibility | There is no |
For the selection of micro front-end solutions, one or a combination of the above solutions should be selected from existing resources and historical accumulation, and from different dimensions (e.g. : Sharing ability, isolation mechanism, data scheme, routing authentication, etc.) to consider, to achieve smooth migration of the project, so as to achieve the iterative upgrading of the architecture of the gradual reconstruction, avoid by all means for the sake of the architecture and architecture, do not meaningless show off, any technology is appropriate is the best, big qiao does not work, esabre no edge!
Scheme comparison
This paper focuses on the analysis of the application of micro-service and micro-application of several landing schemes, to do a simple exploration of its implementation ideas
- single-spa
- qiankun
- icestark
- emp
- piral
The source code parsing
Single – spa source
The whole idea of single-SPA is to use lifecycle hook functions to load applications of hijacked routes, with the core files apps and Reroute
apps.js
export function getAppChanges() { const appsToUnload = [], appsToUnmount = [], appsToLoad = [], appsToMount = []; const currentTime = new Date().getTime(); 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 }; } export function getMountedApps() { return apps.filter(isActive).map(toName); } export function registerApplication( appNameOrConfig, appOrLoadApp, activeWhen, customProps ) { const registration = sanitizeArguments( appNameOrConfig, appOrLoadApp, activeWhen, customProps ); if (getAppNames().indexOf(registration.name) ! == -1) throw Error( formatErrorMessage( 21, __DEV__ && `There is already an app registered with name ${registration.name}`, registration.name ) ); apps.push( assign( { loadErrorTime: null, status: NOT_LOADED, parcels: {}, devtools: { overlays: { options: {}, selectors: [], }, }, }, registration ) ); if (isInBrowser) { ensureJQuerySupport(); reroute(); } } export function unregisterApplication(appName) { if (apps.filter((app) => toName(app) === appName).length === 0) { throw Error( formatErrorMessage( 25, __DEV__ && `Cannot unregister application '${appName}' because no such application has been registered`, appName ) ); } return unloadApplication(appName).then(() => { const appIndex = apps.map(toName).indexOf(appName); apps.splice(appIndex, 1); }); } export function unloadApplication(appName, opts = { waitForUnmount: false }) { if (typeof appName ! == "string") { throw Error( formatErrorMessage( 26, __DEV__ && `unloadApplication requires a string 'appName'` ) ); } const app = find(apps, (App) => toName(App) === appName); if (! app) { throw Error( formatErrorMessage( 27, __DEV__ && `Could not unload application '${appName}' because no such application has been registered`, appName ) ); } const appUnloadInfo = getAppUnloadInfo(toName(app)); if (opts && opts.waitForUnmount) { if (appUnloadInfo) { return appUnloadInfo.promise; } else { const promise = new Promise((resolve, reject) => { addAppToUnload(app, () => promise, resolve, reject); }); return promise; } } else { let resultPromise; if (appUnloadInfo) { resultPromise = appUnloadInfo.promise; immediatelyUnloadApp(app, appUnloadInfo.resolve, appUnloadInfo.reject); } else { resultPromise = new Promise((resolve, reject) => { addAppToUnload(app, () => resultPromise, resolve, reject); immediatelyUnloadApp(app, resolve, reject); }); } return resultPromise; }}Copy the code
reroute.js
export function reroute(pendingPromises = [], eventArguments) { if (appChangeUnderway) { return new Promise((resolve, reject) => { peopleWaitingOnAppChange.push({ resolve, reject, eventArguments, }); }); } const { appsToUnload, appsToUnmount, appsToLoad, appsToMount, } = getAppChanges(); let appsThatChanged, navigationIsCanceled = false, oldUrl = currentUrl, newUrl = (currentUrl = window.location.href); if (isStarted()) { appChangeUnderway = true; appsThatChanged = appsToUnload.concat( appsToLoad, appsToUnmount, appsToMount ); return performAppChanges(); } else { appsThatChanged = appsToLoad; return loadApps(); } function cancelNavigation() { navigationIsCanceled = true; } function loadApps() { return Promise.resolve().then(() => { const loadPromises = appsToLoad.map(toLoadPromise); return ( Promise.all(loadPromises) .then(callAllEventListeners) .then(() => []) .catch((err) => { callAllEventListeners(); throw err; })); }); } function performAppChanges() { return Promise.resolve().then(() => { window.dispatchEvent( new CustomEvent( appsThatChanged.length === 0 ? "single-spa:before-no-app-change" : "single-spa:before-app-change", getCustomEventDetail(true) ) ); window.dispatchEvent( new CustomEvent( "single-spa:before-routing-event", getCustomEventDetail(true, { cancelNavigation }) ) ); if (navigationIsCanceled) { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); finishUpAndReturn(); navigateToUrl(oldUrl); return; } 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(() => { window.dispatchEvent( new CustomEvent( "single-spa:before-mount-routing-event", getCustomEventDetail(true) ) ); }); const loadThenMountPromises = appsToLoad.map((app) => { return toLoadPromise(app).then((app) => tryToBootstrapAndMount(app, unmountAllPromise) ); }); const mountPromises = appsToMount .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0) .map((appToMount) => { return tryToBootstrapAndMount(appToMount, unmountAllPromise); }); return unmountAllPromise .catch((err) => { callAllEventListeners(); throw err; }) .then(() => { callAllEventListeners(); return Promise.all(loadThenMountPromises.concat(mountPromises)) .catch((err) => { pendingPromises.forEach((promise) => promise.reject(err)); throw err; }) .then(finishUpAndReturn); }); }); } function finishUpAndReturn() { const returnValue = getMountedApps(); pendingPromises.forEach((promise) => promise.resolve(returnValue)); try { const appChangeEventName = appsThatChanged.length === 0 ? "single-spa:no-app-change" : "single-spa:app-change"; window.dispatchEvent( new CustomEvent(appChangeEventName, getCustomEventDetail()) ); window.dispatchEvent( new CustomEvent("single-spa:routing-event", getCustomEventDetail()) ); } catch (err) { setTimeout(() => { throw err; }); } appChangeUnderway = false; if (peopleWaitingOnAppChange.length > 0) { const nextPendingPromises = peopleWaitingOnAppChange; peopleWaitingOnAppChange = []; reroute(nextPendingPromises); } return returnValue; } function callAllEventListeners() { pendingPromises.forEach((pendingPromise) => { callCapturedEventListeners(pendingPromise.eventArguments); }); callCapturedEventListeners(eventArguments); } function getCustomEventDetail(isBeforeChanges = false, extraProperties) { const newAppStatuses = {}; const appsByNewStatus = { [MOUNTED]: [], [NOT_MOUNTED]: [], [NOT_LOADED]: [], [SKIP_BECAUSE_BROKEN]: [], }; if (isBeforeChanges) { appsToLoad.concat(appsToMount).forEach((app, index) => { addApp(app, MOUNTED); }); appsToUnload.forEach((app) => { addApp(app, NOT_LOADED); }); appsToUnmount.forEach((app) => { addApp(app, NOT_MOUNTED); }); } else { appsThatChanged.forEach((app) => { addApp(app); }); } const result = { detail: { newAppStatuses, appsByNewStatus, totalAppChanges: appsThatChanged.length, originalEvent: eventArguments? .[0], oldUrl, newUrl, navigationIsCanceled, }, }; if (extraProperties) { assign(result.detail, extraProperties); } return result; function addApp(app, status) { const appName = toName(app); status = status || getAppStatus(appName); newAppStatuses[appName] = status; const statusArr = (appsByNewStatus[status] = appsByNewStatus[status] || []); statusArr.push(appName); }}}Copy the code
Qiankun source
Qiankun is a framework that encapsulates isolation and sharing mechanisms based on single-SPA, simplifying the related life cycle of single-SPA, and providing sandbox isolation and sharing mechanisms
sandbox
export type SandBox = {
/** 沙箱的名字 */
name: string;
/** 沙箱的类型 */
type: SandBoxType;
/** 沙箱导出的代理实体 */
proxy: WindowProxy;
/** 沙箱是否在运行中 */
sandboxRunning: boolean;
/** latest set property */
latestSetProp?: PropertyKey | null;
/** 启动沙箱 */
active: () => void;
/** 关闭沙箱 */
inactive: () => void;
};
// Proxy沙箱
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
private updatedValueSet = new Set<PropertyKey>();
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null;
active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}
if (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
// @ts-ignore
delete window[p];
}
});
}
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const rawWindow = window;
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// We must kept its description while the property existed in rawWindow before
if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// @ts-ignore
target[p] = value;
}
if (variableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
rawWindow[p] = value;
}
updatedValueSet.add(p);
this.latestSetProp = p;
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}!`);
}
// 在 strict-mode 下,Proxy 的 handler.set 返回 false 会抛出 TypeError,在沙箱卸载的情况下应该忽略错误
return true;
},
get(target: FakeWindow, p: PropertyKey): any {
if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
}
if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))
) {
// if your master app in an iframe context, allow these props escape the sandbox
if (rawWindow === rawWindow.parent) {
return proxy;
}
return (rawWindow as any)[p];
}
// proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
if (p === 'hasOwnProperty') {
return hasOwnProperty;
}
// mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
if (p === 'document' || p === 'eval') {
setCurrentRunningSandboxProxy(proxy);
// FIXME if you have any other good ideas
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case
nextTick(() => setCurrentRunningSandboxProxy(null));
switch (p) {
case 'document':
return document;
case 'eval':
// eslint-disable-next-line no-eval
return eval;
// no default
}
}
// eslint-disable-next-line no-nested-ternary
const value = propertiesWithGetter.has(p)
? (rawWindow as any)[p]
: p in target
? (target as any)[p]
: (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
// trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(target: FakeWindow, p: string | number | symbol): boolean {
return p in unscopables || p in target || p in rawWindow;
},
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
/*
as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
> A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object.
*/
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
}
if (rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
descriptorTargetMap.set(p, 'rawWindow');
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if (descriptor && !descriptor.configurable) {
descriptor.configurable = true;
}
return descriptor;
}
return undefined;
},
// trap to support iterator with sandbox
ownKeys(target: FakeWindow): PropertyKey[] {
const keys = uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
return keys;
},
defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
const from = descriptorTargetMap.get(p);
/*
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p),
otherwise it would cause a TypeError with illegal invocation.
*/
switch (from) {
case 'rawWindow':
return Reflect.defineProperty(rawWindow, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},
deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);
return true;
}
return true;
},
});
this.proxy = proxy;
activeSandboxCount++;
}
}
// 快照snapshot沙箱
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;
name: string;
type: SandBoxType;
sandboxRunning = true;
private windowSnapshot!: Window;
private modifyPropsMap: Record<any, any> = {};
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
active() {
// 记录当前快照
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});
// 恢复之前的变更
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
});
this.sandboxRunning = true;
}
inactive() {
this.modifyPropsMap = {};
iter(window, (prop) => {
if (window[prop] !== this.windowSnapshot[prop]) {
// 记录变更,恢复环境
this.modifyPropsMap[prop] = window[prop];
window[prop] = this.windowSnapshot[prop];
}
});
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap));
}
this.sandboxRunning = false;
}
}
Copy the code
globalState.js
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)); }}); } export function initGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[Qiankun] state has not changed! '); } else { const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep(state); emitGlobal(globalState, prevGlobalState); } return getMicroAppStateActions(`global-${+new Date()}`, true); } export function getMicroAppStateActions(id: string, isMaster? : boolean): MicroAppStateActions {return {/** * onGlobalStateChange Global dependency listener ** Dependencies that need to be triggered to collect setState ** Restrictions: Each child application has only one active global listener. The new listener overwrites the old listener. If only part of the listener is listened on, use onGlobalStateChange * * this is designed to reduce memory explosion caused by global listener abuse. callback * } * * @param callback * @param fireImmediately */ onGlobalStateChange(callback: OnGlobalStateChangeCallback, fireImmediately? : boolean) { if (! (callback instanceof Function)) { console.error('[qiankun] callback must be function! '); return; } if (deps[id]) { console.warn(`[qiankun] '${id}' global listener already exists before this, new listener will overwrite it.`); } deps[id] = callback; const cloneState = cloneDeep(globalState); if (fireImmediately) { callback(cloneState, cloneState); }}, /** * setGlobalState updates store data ** 1. Validates the layer 1 properties of the input state. Only the bucket properties declared at initialization will be changed * 2. * * @param state */ setGlobalState(state: Record<string, any> = {}) {if (state === globalState) {console.warn('[Qiankun] state has not changed! '); return false; } const changeKeys: string[] = []; const prevGlobalState = cloneDeep(globalState); globalState = cloneDeep( Object.keys(state).reduce((_globalState, changeKey) => { if (isMaster || _globalState.hasOwnProperty(changeKey)) { changeKeys.push(changeKey); return Object.assign(_globalState, { [changeKey]: state[changeKey] }); } console.warn(' [Qiankun] '${changeKey}' not declared when init state! `); return _globalState; }, globalState), ); If (changekeys. length === 0) {console.warn('[Qiankun] state has not changed! '); return false; } emitGlobal(globalState, prevGlobalState); return true; }, // Cancel dependencies offGlobalStateChange() {delete deps[id]; return true; }}; }Copy the code
Icestark source
Ice is a front-end framework for the whole process of Amoy team, which includes scaffolding, component library, VScode plug-in, Lowcode generation and other related ecology, among which ICestark is the application of related micro-front-end. Ice-related architecture is not extended here. To put it simply, its essence is a powerful ecosystem formed by the form of microkernel and various plug-in markets. This paper focuses on the micro front end, so only the related content of the micro front end is discussed
apps.ts
export function registerMicroApp(appConfig: AppConfig, appLifecyle?: AppLifecylceOptions) {
// check appConfig.name
if (getAppNames().includes(appConfig.name)) {
throw Error(`name ${appConfig.name} already been regsitered`);
}
// set activeRules
const { activePath, hashType = false, exact = false, sensitive = false, strict = false } = appConfig;
const activeRules: (ActiveFn | string | MatchOptions)[] = Array.isArray(activePath) ? activePath : [activePath];
const checkActive = activePath
? (url: string) => activeRules.map((activeRule: ActiveFn | string | MatchOptions) => {
if (typeof activeRule === 'function' ) {
return activeRule;
} else {
const pathOptions: MatchOptions = { hashType, exact, sensitive, strict };
const pathInfo = Object.prototype.toString.call(activeRule) === '[object Object]'
? { ...pathOptions, ...(activeRule as MatchOptions) }
: { path: activeRule as string, ...pathOptions };
return (checkUrl: string) => matchActivePath(checkUrl, pathInfo);
}
}).some((activeRule: ActiveFn) => activeRule(url))
// active app when activePath is not specified
: () => true;
const microApp = {
status: NOT_LOADED,
...appConfig,
appLifecycle: appLifecyle,
checkActive,
};
microApps.push(microApp);
}
export function registerMicroApps(appConfigs: AppConfig[], appLifecyle?: AppLifecylceOptions) {
appConfigs.forEach(appConfig => {
registerMicroApp(appConfig, appLifecyle);
});
}
// 可以加载module粒度的应用
export async function loadAppModule(appConfig: AppConfig) {
const { onLoadingApp, onFinishLoading, fetch } = getAppConfig(appConfig.name)?.configuration || globalConfiguration;
let lifecycle: ModuleLifeCycle = {};
onLoadingApp(appConfig);
const appSandbox = createSandbox(appConfig.sandbox);
const { url, container, entry, entryContent, name } = appConfig;
const appAssets = url ? getUrlAssets(url) : await getEntryAssets({
root: container,
entry,
href: location.href,
entryContent,
assetsCacheKey: name,
fetch,
});
updateAppConfig(appConfig.name, { appAssets, appSandbox });
cacheLoadMode(appConfig);
if (appConfig.umd) {
await loadAndAppendCssAssets(appAssets);
lifecycle = await loadUmdModule(appAssets.jsList, appSandbox);
} else {
await appendAssets(appAssets, appSandbox, fetch);
lifecycle = {
mount: getCache(AppLifeCycleEnum.AppEnter),
unmount: getCache(AppLifeCycleEnum.AppLeave),
};
setCache(AppLifeCycleEnum.AppEnter, null);
setCache(AppLifeCycleEnum.AppLeave, null);
}
onFinishLoading(appConfig);
return combineLifecyle(lifecycle, appConfig);
}
function capitalize(str: string) {
if (typeof str !== 'string') return '';
return `${str.charAt(0).toUpperCase()}${str.slice(1)}`;
}
async function callAppLifecycle(primaryKey: string, lifecycleKey: string, appConfig: AppConfig) {
if (appConfig.appLifecycle && appConfig.appLifecycle[`${primaryKey}${capitalize(lifecycleKey)}`]) {
await appConfig.appLifecycle[`${primaryKey}${capitalize(lifecycleKey)}`](appConfig);
}
}
function combineLifecyle(lifecycle: ModuleLifeCycle, appConfig: AppConfig) {
const combinedLifecyle = { ...lifecycle };
['mount', 'unmount', 'update'].forEach((lifecycleKey) => {
if (lifecycle[lifecycleKey]) {
combinedLifecyle[lifecycleKey] = async (props) => {
await callAppLifecycle('before', lifecycleKey, appConfig);
await lifecycle[lifecycleKey](props);
await callAppLifecycle('after', lifecycleKey, appConfig);
};
}
});
return combinedLifecyle;
}
export async function mountMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
// check current url before mount
if (appConfig && appConfig.checkActive(window.location.href) && appConfig.status !== MOUNTED) {
updateAppConfig(appName, { status: MOUNTED });
if (appConfig.mount) {
await appConfig.mount({ container: appConfig.container, customProps: appConfig.props });
}
}
}
export async function unmountMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
if (appConfig && (appConfig.status === MOUNTED || appConfig.status === LOADING_ASSETS || appConfig.status === NOT_MOUNTED)) {
// remove assets if app is not cached
const { shouldAssetsRemove } = getAppConfig(appName)?.configuration || globalConfiguration;
emptyAssets(shouldAssetsRemove, !appConfig.cached && appConfig.name);
updateAppConfig(appName, { status: UNMOUNTED });
if (!appConfig.cached && appConfig.appSandbox) {
appConfig.appSandbox.clear();
appConfig.appSandbox = null;
}
if (appConfig.unmount) {
await appConfig.unmount({ container: appConfig.container, customProps: appConfig.props });
}
}
}
// unload micro app, load app bundles when create micro app
export async function unloadMicroApp(appName: string) {
const appConfig = getAppConfig(appName);
if (appConfig) {
unmountMicroApp(appName);
delete appConfig.mount;
delete appConfig.unmount;
delete appConfig.appAssets;
updateAppConfig(appName, { status: NOT_LOADED });
} else {
console.log(`[icestark] can not find app ${appName} when call unloadMicroApp`);
}
}
// remove app config from cache
export function removeMicroApp(appName: string) {
const appIndex = getAppNames().indexOf(appName);
if (appIndex > -1) {
// unload micro app in case of app is mounted
unloadMicroApp(appName);
microApps.splice(appIndex, 1);
} else {
console.log(`[icestark] can not find app ${appName} when call removeMicroApp`);
}
}
export function removeMicroApps(appNames: string[]) {
appNames.forEach((appName) => {
removeMicroApp(appName);
});
}
// clear all micro app configs
export function clearMicroApps () {
getAppNames().forEach(name => {
unloadMicroApp(name);
});
microApps = [];
}
Copy the code
start.ts
export function reroute (url: string, type: RouteType | 'init' | 'popstate'| 'hashchange' ) { const { pathname, query, hash } = urlParse(url, true); // trigger onRouteChange when url is changed if (lastUrl ! == url) { globalConfiguration.onRouteChange(url, pathname, query, hash, type); const unmountApps = []; const activeApps = []; getMicroApps().forEach((microApp: AppConfig) => { const shouldBeActive = microApp.checkActive(url); if (shouldBeActive) { activeApps.push(microApp); } else { unmountApps.push(microApp); }}); // trigger onActiveApps when url is changed globalConfiguration.onActiveApps(activeApps); // call captured event after app mounted Promise.all( // call unmount apps unmountApps.map(async (unmountApp) => { if (unmountApp.status === MOUNTED || unmountApp.status === LOADING_ASSETS) { globalConfiguration.onAppLeave(unmountApp); } await unmountMicroApp(unmountApp.name); }).concat(activeApps.map(async (activeApp) => { if (activeApp.status ! == MOUNTED) { globalConfiguration.onAppEnter(activeApp); } await createMicroApp(activeApp); })) ).then(() => { callCapturedEventListeners(); }); } lastUrl = url; }; function start(options? : StartConfiguration) { if (started) { console.log('icestark has been already started'); return; } started = true; recordAssets(); // update globalConfiguration Object.keys(options || {}).forEach((configKey) => { globalConfiguration[configKey] = options[configKey]; }); hijackHistory(); hijackEventListener(); // trigger init router globalConfiguration.reroute(location.href, 'init'); } function unload() { unHijackEventListener(); unHijackHistory(); started = false; // remove all assets added by micro apps emptyAssets(globalConfiguration.shouldAssetsRemove, true); clearMicroApps(); }Copy the code
Emp source
The implementation mode of EMP is completely different from single-SPa-like schemes. It utilizes the module federation mechanism of Webpack5 to realize the shared call between modules. The bigs of YY realize the function of service mesh similar to microservice based on the shared transmission of XXX.D. TS of TS. Emp provides complete scaffolding functionality
emp-cli
const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
module.exports = (env, config, {analyze, empEnv, ts, progress, createName, createPath, hot}) => {
const isDev = env === 'development'
const conf = {
plugin: {
mf: {
plugin: ModuleFederationPlugin,
args: [{}],
},
},
}
if (ts) {
createName = createName || 'index.d.ts'
createPath = createPath ? resolveApp(createPath) : resolveApp('dist')
conf.plugin.tunedts = {
plugin: TuneDtsPlugin,
args: [
{
output: path.join(createPath, createName),
path: createPath,
name: createName,
isDefault: true,
},
],
}
}
config.merge(conf)
}
Copy the code
emp-tune-dts-plugin
function tuneType(createPath, createName, isDefault, Operation = emptyFunc) {// Retrieve d.ts file const filePath = '${createPath}/${createName}' const fileData = fs.readFileSync(filePath, {encoding: 'utf-8'}) let newFileData = '' newFileData = fileData isDefault && (newFileData = defaultRepalce(fileData)) // } newFileData && (newFileData = operation(newFileData) ? Operation (newFileData) : newFileData) // Overwrite the original index.d.stfs. WriteFileSync (filePath, newFileData, {encoding: 'utf-8'}) } class TuneDtsPlugin { constructor(options) { this.options = options || {} } apply(compiler) { const _options = this.options console.log('------------TuneDtsPlugin Working----------') if (compiler.options.output.path) { _options.path = compiler.options.output.path _options.output = `${compiler.options.output.path}/${_options.name}` } compiler.hooks.afterEmit.tap(plugin, function () { setTimeout(function () { generateType(_options) }) }) } }Copy the code
Piral source
Piral is a micro front-end framework based on React. It defines two concepts: One is Piral, which is an application shell for an application. It hosts various applications, including components shared by Pilets that define when these applications are loaded and integrated. The other is Pilet, which is a special feature module that hosts different applications and contains independent resources, which determine the loading time of components. Piral isolates resources by adding a layer of Pilets, and shares data without this layer
piral
// render.tsx export function renderInstance(options? : PiralRenderOptions) { return runInstance((app, selector) => render(app, getContainer(selector)), options); } // run.tsx export function runInstance(runner: PiralRunner, options: PiralRenderOptions = {}) { const { selector = '#app', settings, layout, errors, middleware = noChange, ... config } = options; const { app, instance } = getAppInstance(middleware(config), { settings, layout, errors }); runner(app, selector); return instance; }Copy the code
piral-base
// dependency.ts export function compileDependency( name: string, content: string, link = '', dependencies? : AvailableDependencies, ): Promise<PiletApp> { const app = evalDependency(name, content, link, dependencies); return checkPiletAppAsync(name, app); } // fetch.ts export function defaultFetchDependency(url: string) { return fetch(url, { method: 'GET', cache: 'force-cache', }).then((m) => m.text()); }Copy the code
piral-core
// actions export function initialize(ctx: GlobalStateContext, loading: boolean, error: Error | undefined, modules: Array<Pilet>) { ctx.dispatch((state) => ({ ... state, app: { ... state.app, error, loading, }, modules, })); } export function injectPilet(ctx: GlobalStateContext, pilet: Pilet) { ctx.dispatch((state) => ({ ... state, modules: replaceOrAddItem(state.modules, pilet, (m) => m.name === pilet.name), registry: removeNested<RegistryState, BaseRegistration>(state.registry, (m) => m.pilet === pilet.name), })); ctx.emit('unload-pilet', { name: pilet.name, }); } export function setComponent<TKey extends keyof ComponentsState>( ctx: GlobalStateContext, name: TKey, component: ComponentsState[TKey], ) { ctx.dispatch((state) => ({ ... state, components: withKey(state.components, name, component), })); } export function setErrorComponent<TKey extends keyof ErrorComponentsState>( ctx: GlobalStateContext, type: TKey, component: ErrorComponentsState[TKey], ) { ctx.dispatch((state) => ({ ... state, errorComponents: withKey(state.errorComponents, type, component), })); } export function setRoute<T = {}>( ctx: GlobalStateContext, path: string, component: ComponentType<RouteComponentProps<T>>, ) { ctx.dispatch((state) => ({ ... state, routes: withKey(state.routes, path, component), })); } export function includeProvider(ctx: GlobalStateContext, provider: JSX.Element) { const wrapper: React.FC = (props) => cloneElement(provider, props); ctx.dispatch((state) => ({ ... state, provider: ! state.provider ? wrapper : (props) => createElement(state.provider, undefined, wrapper(props)), })); } // createGlobalState.ts export function createGlobalState(customState: NestedPartial<GlobalState> = {}) { const defaultState: GlobalState = { app: { error: undefined, loading: typeof window ! == 'undefined', layout: 'desktop', }, components: { ErrorInfo: DefaultErrorInfo, LoadingIndicator: DefaultLoadingIndicator, Router: BrowserRouter, Layout: DefaultLayout, }, errorComponents: {}, registry: { extensions: {}, pages: {}, wrappers: {}, }, routes: {}, data: {}, portals: {}, modules: [], }; return Atom.of(extend(defaultState, customState)); }Copy the code
conclusion
There are many practical solutions for micro-front-end implementation. If you want to know more about the framework, you can read this article about 11 micro-front-end frameworks that are very popular in 2020. The essence of the micro front end lies in the isolation and sharing of resources. The granularity here can be applications, modules, or self-defined abstraction layers, which are all for better “high cohesion, low coupling”. As the saying goes, “There is no silver bullet in software engineering”, there is no general formula and general solution that can solve all problems at once. Only by combining the specific business and choosing appropriate technical solutions can we maximize the role of the architecture.
reference
- [Issue 1917] Practice of micro front end in Xiaomi CRM system
- Best Practices in Microfront-end based on Qiankun (Swastika) – from 0 to 1
- [PPT] @Zhang Kejun: Micro front-end architecture system
- [Issue 1728] Daily Youxian Supply Chain front end team micro front end transformation
- Six or seven ways to implement front-end microservitization
- Micro front end self-check list
- [Issue 2154] EMP microfront-end solutions
- Best Practices of Microfront-end based on Qiankun (illustrated) – Inter-application Communication
- # 1789. ToB enterprise applications using Angular to build a micro front-end architecture
- Probably the most complete microfront-end solution you’ve ever seen
- Micro front end in meituan takeout practice
- Bifrost micro-front-end framework and its practice in Meituan flash purchase
- Iqiyi micro front-end architecture practice
- Polish and application of front-end microserver in Bytedance
- Use the way of micro front – end to build class single – page application
- React based microfront-end: A brief analysis of Piral