Micro front end, about its good and application scenarios, many classmates have also introduced, then we use the micro front end scheme Qiankun how to achieve the application of “micro front end”?
Several features
When it comes to front-end microservices, there are several features that can’t be ignored.
- Subapplication parallelism
- Parent-child application communication
-
preload
- Preload the resources of the child application when idle
- Loading of public dependencies
- According to the need to load
- JS sandbox
- CSS isolation to achieve the above points, then our sub-application can be multiple combinations, each other, in the face of large project aggregation, also do not have to worry about the project after the maintenance, packaging, online problems.
This share, will be a simple read Qiankun source code, from the general process, understand his implementation principle and technical solutions.
How will our application be configured? – Join the arms of micro-front Arya
Arya- The company’s front-end platform micro-service base
Arya has access to the routing menu and permissions of the permission platform, and can dynamically select the specified pages of sub-applications with micro-service capabilities to combine them into a new platform, so as to facilitate the issuing of permissions and the convergence of functions of various systems.
Create a process
Initialize global configuration -start (opts)
/src/apis.ts
Export function start(opts: frameworkConfiguration = {}) {// default frameworkConfiguration = {prefetch: true, singular: true, sandbox: true, ... opts }; const { prefetch, sandbox, singular, urlRerouteOnly, ... importEntryOpts } = frameworkConfiguration; // Check the prefetch property. If preloading is required, add a global single-spa:first-mount listener. Preload other child resources after the first child is mounted, and optimize the loading speed of subsequent children. if (prefetch) { doPrefetchStrategy(microApps, prefetch, importEntryOpts); } // Set whether or not to enable the sandbox runenvironment, isolate if (sandbox) {if (! window.Proxy) { console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox'); // Snapshot sandbox does not support non-singular mode if (! singular) { console.error('[qiankun] singular is forced to be true when sandbox enable but proxySandbox unavailable'); frameworkConfiguration.singular = true; }}} // Start the main application-single-spa StartSinglesPA ({urlRerouteOnly}); frameworkStartedDefer.resolve(); }
- The start function is responsible for initializing some global Settings and then launching the application.
- Some of these initialized configuration parameters will be used in the RegisterMicroApps child application callback function.
registerMicroApps(apps, lifeCycles?) – Registered child applications
/src/apis.ts
export function registerMicroApps<T extends object = {}>( apps: Array<RegistrableApp<T>>, lifeCycles? : Frameworklifecycles <T>,) {// Construct unregisteredApps = apps.filter(app =>! microApps.some(registeredApp => registeredApp.name === app.name)); microApps = [...microApps, ...unregisteredApps]; unregisteredApps.forEach(app => { const { name, activeRule, loader = noop, props, ... appConfig } = app; // RegisterApplication ({name, app: async () => {loader(true); await frameworkStartedDefer.promise; const { mount, ... otherMicroAppConfigs } = await loadApp( { name, props, ... appConfig }, frameworkConfiguration, lifeCycles, ); return { mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], ... otherMicroAppConfigs, }; }, activeWhen: activeRule, customProps: props, }); }); }
-
In line 13, the registerApplication method of Single-SPA is called to register the child application.
-
Passing parameters: Name, callback function, rules activated by the ActiveRule child application, props, data that the main application needs to pass to the child application.
- When the ActiveRule activation rule is met, the child application is activated, the callback function is executed, and the lifecycle hook function is returned.
-
Get child application resources -import-HTML-entry
src/loader.ts
// get the entry html content and script executor
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
- Use import-HTML-entry to pull static resources from the child application.
- The object returned after the call is as follows:
- The pull code is as follows
-
Making address: https://github.com/kuitos/imp…
- If static resources can be pulled, is it possible to do a simple crawler service to crawl pages daily to execute whether the resources are loaded correctly?
export function importEntry(entry, opts = {}) { // ... // html entry if (typeof entry === 'string') { return importHTML(entry, { fetch, getPublicPath, getTemplate }); } // config entry if (Array.isArray(entry.scripts) || Array.isArray(entry.styles)) { const { scripts = [], styles = [], html = '' } = entry; const setStylePlaceholder2HTML = tpl => styles.reduceRight((html, styleSrc) => `${ genLinkReplaceSymbol(styleSrc) }${ html }`, tpl); const setScriptPlaceholder2HTML = tpl => scripts.reduce((html, scriptSrc) => `${ html }${ genScriptReplaceSymbol(scriptSrc) }`, tpl); return getEmbedHTML(getTemplate(setScriptPlaceholder2HTML(setStylePlaceholder2HTML(html))), styles, {fetch}).then(embedHtml => ({fetch},})); } else { throw new SyntaxError('entry scripts or styles should be array! '); }}
The main application mount applies the HTML template
src/loader.ts
async () => { if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { return prevAppUnmountedDeferred.promise; } return undefined; },
-
Single instance detection. In singleton mode, the new child application mount behavior starts only after the old child application is uninstalled.
- After the old child application is uninstalled – isolation scheme in singleton mode.
const render = getRender(appName, appContent, container, legacyRender); Render ({element, loading: true}, 'loading') {element, loading: true}, 'loading');
-
In the render function, mounts the pulled resource to the node in the specified container.
const containerElement = document.createElement('div'); containerElement.innerHTML = appContent; // appContent always wrapped with a singular div const appElement = containerElement.firstChild as HTMLElement; const containerElement = typeof container === 'string' ? document.querySelector(container) : container; if (element) { rawAppendChild.call(containerElement, element); }
At this stage, the main application has mounted the underlying HTML structure of the child application in one of its containers, and then it needs to mount the child application state using the child application’s corresponding mount methods (such as vue.$mount).
At this point, the page can also start a loading effect based on the Loading parameter until all the content of the child application is loaded.
Sandbox running environment
src/loader.ts
let global = window; let mountSandbox = () => Promise.resolve(); let unmountSandbox = () => Promise.resolve(); if (sandbox) { const sandboxInstance = createSandbox( appName, containerGetter, Boolean(singular), enableScopedCSS, excludeAssetFilter, ); // Global = sandboxInstance.proxy as typeof window; // Global = sandboxInstance.proxy as typeof window; mountSandbox = sandboxInstance.mount; unmountSandbox = sandboxInstance.unmount; }
If the sandbox option is turned off, then the sandbox environment for all children is Windows, which can easily contaminate the global state.
Generate the application runtime sandbox
src/sandbox/index.ts
-
App environment sandbox
- App sandbox refers to the context in which the application will run after the application has been initialized. The environment sandbox for each application will only be initialized once, because the child applications will only trigger Bootstrap once.
- When a child app switches, it actually switches the app environment sandbox.
-
Render the sandbox
- The sandbox generated by the child application before the app mount starts. After each child application switch, the Render sandbox is reinitialized.
The purpose of this design is to ensure that each child application can still run in the environment after Bootstrap application after switching back.
let sandbox: SandBox; if (window.Proxy) { sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName); } else { sandbox = new SnapshotSandbox(appName); }
- Sandbox is mainly divided into LegacySandbox and SnapshotSandbox based on whether it supports Window.Proxy.
LegacySandBox – Single-instance sandbox
src/sandbox/legacy/sandbox.ts
const proxy = new Proxy(fakeWindow, { set(_: Window, p: PropertyKey, value: any): boolean { if (self.sandboxRunning) { if (! rawWindow.hasOwnProperty(p)) { addedPropsMapInSandbox.set(p, value); } else if (! ModifiedPropsOriginalValueMapInSandbox. From the (p)) {/ / if the current window object exists the attribute, and the record was not recorded in the map, Const originalValue = (RawWindow as any)[p]; modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } currentUpdatedPropsValueMap.set(p, value); RawWindow as any [p] = value; rawWindow as any [p] = value; return true; } if (process.env.NODE_ENV === 'development') { console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `); } // In strict-mode, the Proxy handler. Set returns false to throw a TypeError. The error return true should be ignored if the sandbox is unloaded; }, get(_: Window, p: PropertyKey): any { if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') { return proxy; } const value = (rawWindow as any)[p]; return getTargetValue(rawWindow, value); }, has(_: Window, p: string | number | symbol): boolean { return p in rawWindow; }});
- The global Window object is simply understood as a child application. The child application’s actions on the global properties are actions on the properties of the Proxy object.
// Execute the sub-application script file: Eval (function(window) {/* the contents of the child script */})(proxy));
- When the set is called to the child window object set properties, application proxy/all attribute set and update records in addedPropsMapInSandbox or modifiedPropsOriginalValueMapInSandbox first, Then unified records into currentUpdatedPropsValueMap.
- Modify the properties of the global window to complete the value setting.
-
When you call get to value from the child application proxy/window object, it will value directly from the window object. The value for a non-constructor will bind the this pointer to the window object and then return the function. The sandbox isolation of LegacySandBox is achieved by restoring the application state when the sandbox is activated and restoring the primary application state (the global state of the child applications before they are mounted) when uninstalled. Detailed source code in the SRC/sandbox/legacy/sandbox. SingularProxySandbox method of ts.
Proxysandbox multi-instance sandbox
src/sandbox/proxySandbox.ts
constructor(name: string) { this.name = name; this.type = SandBoxType.Proxy; const { updatedValueSet } = this; const self = 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 (self.sandboxRunning) { // @ts-ignore target[p] = value; updatedValueSet.add(p); interceptSystemJsProps(p, value); return true; } if (process.env.NODE_ENV === 'development') { console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `); } // In strict-mode, the Proxy handler. Set returns false to throw a TypeError. The error return true should be ignored if the sandbox is unloaded; }, 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') { document[attachDocProxySymbol] = proxy; // 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 the complex scenarios, such as the micro app runs in the same task context with master in som case // fixme if you have any other good ideas nextTick(() => delete document[attachDocProxySymbol]); return document; } // eslint-disable-next-line no-bitwise const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (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'); return descriptor; } return undefined; }, // trap to support iterator with sandbox ownKeys(target: FakeWindow): PropertyKey[] { return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target))); }, 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; }
- When you call set to the child application Proxy/Window object, all property Settings and updates are hit UpdatedValueSet and stored in the UpdatedValueSet (18 lines UpdatedValueSet.add (p)) collection. To avoid any impact on the window object.
- When a call to get takes a value from the child’s proxy/window object, it takes a value from the child’s sandbox state pool updatedValueSet before taking a value from the main application’s window object if it doesn’t hit. The value for a non-constructor will bind the this pointer to the window object and then return the function.
- This completes the isolation between ProxySandBox sandbox applications and controls access to Proxy/Window object values for all children. The set value is only applied to the UpdatedValueSet set inside the sandbox. The value is taken from the updateValueMap in preference to the Proxy/Window object if not found.
-
By contrast, ProxySandBox is the most complete sandbox mode, completely isolating the window object operation, and also solves the problem that the snapshot mode neutron application is running while still polluting the window.
SnapshotSandbox
src/sandbox/snapshotSandbox.ts
If the window.proxy property is not supported, the SnapshotSandbox sandbox will be used. The sandbox has the following steps:
- Take a snapshot of the Window when activated.
- Bind all properties in the Window snapshot to ModifyPropSMAP for subsequent restore changes.
- Record the change, and if it is different during uninstall, restore the previous window property value.
SnapshotSandbox sandbox is the use of snapshots to achieve the window object state isolation management. In contrast to ProxySandBox, SnapshotSandBox will pollute the Window object during the activation of the child application and is a downward compatible solution for browsers that do not support Proxy.
Add style sheet file hijacking dynamically
src/sandbox/patchers/dynamicAppend.ts
-
Avoid main application, child application style pollution.
- The primary application is compiled with classID and hash code to prevent the primary application from affecting the style of its children.
-
Sub to sub avoid.
- When the child application is currently active, the dynamic Style stylesheet is added to the child application container and can be uninstalled with the child application when the child application is uninstalled to avoid style pollution.
Dynamic script execution of child applications
The main purpose of hijacking the dynamically added script is to replace the window object of the dynamic script runtime with the proxy object, and make the running context of the dynamically added script file of the child application be replaced with the child application itself.
Unmount sandbox – unmountSandbox
src/loader.ts
unmountSandbox = sandboxInstance.unmount;
src/sandbox/index.ts
/** * Restore the global state to where it was before the application was loaded */ async unmount() {// Perform an unmount loop to remove the DOM/style/script; Map (free => free()); sideEffectReBuilders = [...bootstrappingFreers,...mountingFreers]. sandbox.inactive(); },
communication
src/globalState.ts
Qiankun internally provides the initGlobalState method to register MicroAppStateActions instances for communication. This instance has three methods, namely:
- SetGlobalState: Set GlobalState – When a new value is set, a shallow check is performed internally. If the check detects a change in GlobalState, a notification is triggered to all observer functions.
- OnGlobalStateChange: Registered observer function – responds to globalState changes by firing the observer function when globalState changes.
-
OffGlobalStateChange: Cancels the observer function – this instance no longer responds to globalState changes.
Extraction of common resources
review
The | ghost is concerned about the technology, hand in hand to the cloud technology