Micro front end, about his good and application scenarios, many senior students have also introduced, then we use the micro front end solution Qiankun how to achieve the application of the “micro front end”?
Several features
When it comes to front end microservices, there are certainly several features.
- Subapplication parallelism
- Parent-child application communication
- preload
- Preloads resources for child applications when idle
- Loading of public dependencies
- According to the need to load
- JS sandbox
- CSS isolation
By doing the above points, our sub-applications can be combined in multiple ways without affecting each other. In the face of the aggregation of large projects, there is no need to worry about the maintenance, packaging and on-line problems after project aggregation.
In this sharing, I will simply read the source code of Qiankun and understand his implementation principle and technical solution from the general process.
How is our application configured? – Join Arya in the arms of micro-front-end
Arya- The company’s front-end platform for microservice pedestals
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 and combine them into a new platform, which is convenient for the delivery of permissions and the convergence of functions of various systems.
Create a process
Initializing the global configuration – start(OPTS)
/src/apis.ts
Export function start(opts: FrameworkConfiguration = {}) {// Default setting FrameworkConfiguration = {prefetch: true, singular: true, sandbox: true, ... opts }; const { prefetch, sandbox, singular, urlRerouteOnly, ... importEntryOpts } = frameworkConfiguration; // Check the prefetch property. If preloading is required, add the global event single-spa:first-mount listener to preload other child application resources after the first child application is mounted to optimize the loading speed of subsequent other child applications. if (prefetch) { doPrefetchStrategy(microApps, prefetch, importEntryOpts); 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(); }Copy the code
- The start function initializes some global Settings and then starts the application.
- Some of these initialization configuration parameters are used in the callback function of the registerMicroApps registered child application.
registerMicroApps(apps, lifeCycles?) – Register sub-applications
/src/apis.ts
export function registerMicroApps<T extends object = {}>( apps: Array<RegistrableApp<T>>, lifeCycles? : FrameworkLifeCycles<T>,) {const 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, }); }); }Copy the code
- At line 13, call single-SPA’s registerApplication method to register the child application.
- Parameters: Name, callback function, activeRule sub-application activated rules, props, data that the primary application needs to pass to the sub-application.
- When activeRule activation rules are met, the child application is activated, the callback function is executed, and the lifecycle hook function is returned.
- Parameters: Name, callback function, activeRule sub-application activated rules, props, data that the primary application needs to pass to the sub-application.
Get subapplication resources – import-html-entry
src/loader.ts
// get the entry html content and script executor
const { template, execScripts, assetPublicPath } = await importEntry(entry, importEntryOpts);
Copy the code
- 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:Github.com/kuitos/impo…
- If static resources can be pulled, can a simple crawler service be made?
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 => ({// here handle the same importHTML, omit},})); } else { throw new SyntaxError('entry scripts or styles should be array! '); }}Copy the code
The primary application mounts the HTML template of the child application
src/loader.ts
async () => {
if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) {
return prevAppUnmountedDeferred.promise;
}
return undefined;
},
Copy the code
- Single instance for detection. In single-instance mode, the new child application mount behavior starts after the old child application is unmounted.
- After the old subapplication is uninstalled – isolation scheme in singleton mode.
const render = getRender(appName, appContent, container, legacyRender); Render ({element, loading: true}, 'loading');Copy the code
- The Render function mounts the pulled resource to a 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);
}
Copy the code
At this stage, the master application has mounted the basic HTML structure of the child application to a container of the master application, and then needs to mount the child application corresponding to the mount method (such as Vue.$mount) to mount the child application state.
In this case, you can enable a loading effect based on the loading parameter until all sub-applications are loaded.
Sandbox operation 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; // Use sandboxinstance. proxy as typeof window; mountSandbox = sandboxInstance.mount; unmountSandbox = sandboxInstance.unmount; }Copy the code
This is the core sandbox logic. If you turn off the sandbox option, all child applications will have a sandbox environment that is Window, which will easily pollute the global state.
Generate an application runtime sandbox
src/sandbox/index.ts
- App environment sandbox
- The app environment sandbox refers to the context in which the app will run after it has been initialized. Each application’s environment sandbox will only be initialized once, because the child application will only trigger bootstrap once.
- When a child app switches, it’s actually switching the app environment sandbox.
- Render the sandbox
- The child app generates a fine sandbox before app mount starts. The Render sandbox is reinitialized after each child application switch.
The purpose of this design is to ensure that each child application can be switched back and run in the same environment as the application bootstrap.
let sandbox: SandBox;
if (window.Proxy) {
sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName);
} else {
sandbox = new SnapshotSandbox(appName);
}
Copy the code
- SandBox internal sandboxes are divided into LegacySandbox and SnapshotSandbox based on whether window.Proxy is supported.
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); [p] = value; // The window object must be reset to get the updated data (rawWindow as any). 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, Proxy handler.set returns false and raises TypeError, which should be ignored in case of sandbox unmount; }, 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; }});Copy the code
- For the window global object simply understood as a sub-application, the sub-application’s operation on the global attribute is the operation on the proxy object attribute.
// Subapplication script file execution process: Eval (function(window) {/* child script file contents */})(proxy)); eval(function(window) {/* child script file contents */})(proxy));Copy the code
- 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 setting.
- When get is called to take a value from the proxy/window object of the child application, the value is taken directly from the window object. Values that are not constructors will bind the this pointer to the window object and return the function.
LegacySandbox sandbox isolation is achieved by returning the atomic application state when the sandbox is activated and the master application state (the global state before the child application was mounted) when the sandbox is unmounted. 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, Proxy handler.set returns false and raises TypeError, which should be ignored in case of sandbox unmount; }, 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; }Copy the code
- When set is called to set a property to the proxy/window object of the child application, all property Settings and updates hit updatedValueSet and are stored in the updatedValueSet collection (line 18 updatedValueset.add (p)). To avoid having an impact on the Window object.
- When get is called from the proxy/ Window object of the child application, the value is first taken from the sandbox state pool updatedValueSet of the child application. If no match is made, the value is taken from the window object of the main application. Values that are not constructors will bind the this pointer to the window object and return the function.
- In this way, isolation between ProxySandbox sandbox applications is complete, and access to proxy/ Window object values is controlled for all child applications. The value is applied only to the internal updatedValueSet collection in the sandbox, and is taken first from the child application independent state pool (updateValueMap). If not found, the value is taken from the proxy/ Window object.
- ProxySandbox, by contrast, is the most complete sandbox mode, completely isolating operations on Window objects and eliminating the problem of contaminate the Window during the run of an application in snapshot mode.
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 Window when activated.
2. Bind all properties in the Window snapshot to modifyPropsMap for subsequent restoration.3. Record the changes. If the changes are different during uninstallation, restore the value of the window property.
The SnapshotSandbox sandbox uses snapshots to isolate the state of window objects. In contrast to ProxySandbox, the SnapshotSandbox will pollute the Window object during child application activation, which is a backward compatibility scheme for browsers that do not support the Proxy property.
Dynamically add stylesheet file hijacking
src/sandbox/patchers/dynamicAppend.ts
- Avoid style contamination of main and sub-apps.
- The primary application is compiled using classID plus hash code to prevent the primary application from influencing the style of the child application.
- Child to child to avoid.
- When the child application is active, the dynamic style style sheet is added to the child application container. When the child application is unloaded, the style sheet can also be unloaded with the child application to avoid style contamination.
Dynamic script execution of child applications
The main purpose of hijacking dynamically added scripts is to replace the window object of the dynamic script with a proxy object, so that the running context of the dynamically added script file of the child application is replaced with the child application itself.
UnmountSandbox – unmountSandbox
src/loader.ts
unmountSandbox = sandboxInstance.unmount;
Copy the code
src/sandbox/index.ts
// async unmount() {// Loop unmount functions - remove dom/ styles/scripts, etc.; SideEffectsRebuilders = [...bootstrappingFreers,...mountingFreers].map(free => Free ()); sandbox.inactive(); },Copy the code
communication
src/globalState.ts
The initGlobalState method is provided internally to register MicroAppStateActions instances for communication, which have three methods:
- SetGlobalState: setGlobalState – when a new value is set, a shallow check is performed internally, and if a globalState change is detected, a notification is triggered to inform all observer functions.
- OnGlobalStateChange: Registers the observer function – in response to globalState changes, the observer function is triggered when globalState changes.
- OffGlobalStateChange: Cancel the observer function – the instance no longer responds to globalState changes.
Extraction of common resources
review
Ghostout focus on the object technology, hand in hand to the cloud of technology