Recently, we have implemented some micro front-end business scenarios and encountered some problems. After looking at their implementation, we found that both Garfish and Qiankun are constantly improving their implementation. However, I also looked at their implementation and found that compared with Garfish, they have some shortcomings in case processing. So this is the implementation analysis of Garfish. The following will be analyzed from resource loading entry, resource parsing, sandbox environment and code execution to understand the main implementation logic of the micro front end.

The version of Qiankun in the following comparison is 2.4.0, if there is an incorrect comment please correct.

This article involves a lot of code analysis, hoping to see the implementation logic more directly from the implementation level, rather than through a few diagrams to explain the concept.

How to parse resources (HTML entry)

Obtaining resource content

Load the resource as an entry file based on the URL provided. The implementation of loading is very simple. Fetch is used to fetch the resource content. If it is an HTML resource entry, tags are serialized and related processing is performed, as you will see later. If it is a JS file, it directly instantiates a JS resource class. The purpose is to store basic information about the type, size, code string, and so on of the loaded resource. And attempts to cache loaded resources.

Below is the implementation of loading various resources, such as getting HTML files, JS files, CSS files. This method is used several times to load resources throughout the process.

// Load arbitrary resources, but all will be converted to string load(url: string, config? Config = {mode: 'cors',... config, ... requestConfig }; this.loadings[url] = fetch(url, If (res.status >= 400) {error(' load failed with status "${res.status}" '); } const type = res.headers.get('content-type'); return res.text().then((code) => ({ code, type, res })); }) .then(({ code, type, res }) => { let manager; const blob = new Blob([code]); const size = Number(blob.size); const ft = parseContentType(type); / / classifying load resources processing / / below several instances of the new code block string and the purpose of the resource type some basic information, such as the if (isJs (ft) | | / js /. The test (res) url)) {manager = new JsResource({ url, code, size, attributes: [] }); } else if (isHtml(ft) || /.html/.test(res.url)) { manager = new HtmlResource({ url, code, size }); } else if (isCss(ft) || /.css/.test(res.url)) { manager = new CssResource({ url, code, size }); } else { error(`Invalid resource type "${type}"`); Loadings [url] = null;} // All requests are maintained by a promise map. currentSize += isNaN(size) ? 0 : size; if (! IsOverCapacity (currentSize) | | this. ForceCaches. From (url)) {/ / try to cache loading resource enclosing caches [url] = manager; } return manager; }) .catch((e) => { const message = e instanceof Error ? e.message : String(e); error(`${message}, url: "${url}"`); }); return this.loadings[url]; }}Copy the code

When the HTML entry is loaded, this method helps us get the content of the entry HTML file, which needs to be parsed for download.

Serialized DOM tree

Because HTML entries are special, this section is analyzed separately. How to parse and process HTML files. First we get the file content of the resource in the previous step. The next step is to perform AST parsing of the loaded HTML resources to structure the DOM so that different types of tag content can be extracted. Himalaya is used here. Try the address Jew. Ski /himalaya/ online and parse the content as follows. Parse the DOM text into a JSON structure.

After structuring, depth-first traversal is carried out to extract link,style and Script tags

/ / this call way. QueryVNodesByTagNames ([' link ', 'style', This. Ast is the result of the parse illustrated above. Private queryVNodesByTagNames(tagNames: Array<string>) { const res: Record<string, Array<VNode>> = {}; for (const tagName of tagNames) { res[tagName] = []; } const traverse = (vnode: VNode | VText) => { if (vnode.type === 'element') { const { tagName, children } = vnode; if (tagNames.indexOf(tagName) > -1) { res[tagName].push(vnode); } children.forEach((vnode) => traverse(vnode)); }}; this.ast.forEach((vnode) => traverse(vnode)); return res; }Copy the code

As the current implementation of each framework is basically js generated DOM and mounted to the specified element, so here as long as the three loading resources of the label extracted basically complete the page loading. Of course, the sub-system entry needs to be modified with the loading mode of the micro-front-end, so that the mount function points to the DOM provided by the main application. At this point we have completed the extraction of the basic resources.

Build the runtime environment

The next step is to instantiate the current child application. We need the run-time independence of the child application without affecting the main application’s code. Therefore, the child application needs to run inside the specified sandbox, which is a core part of the micro front end implementation. Let’s first look at the code for the instantiator application

// Each child reference is instantiated using this method private createApp(appInfo: appInfo, opts: LoadAppOptions, Manager: HtmlResource, isHtmlMode: Boolean,) {const run = (resources: ResourceModules) => {let AppCtor = opts.sandbox.snapshot? SnapshotApp : App; if (! window.Proxy) { warn( 'Since proxy is not supported, the sandbox is downgraded to snapshot sandbox', ); AppCtor = SnapshotApp; } const app = new AppCtor(this.context, appInfo, opts, manager, resources, resources); // provide HTML entry isHtmlMode,); this.context.emit(CREATE_APP, app); return app; }; Const MJS = promise.all (this.takejsResources (manager as HtmlResource)); const mlink = Promise.all(this.takeLinkResources(manager as HtmlResource)); return Promise.all([mjs, mlink]).then(([js, link]) => run({ js, link })); }Copy the code

This is just an overview of the creation and loading process of a subapplication, basically a context, some resource information. Specific details can be followed to see the whole process of the source string. Next, take a look at the implementation of the sandbox, the context in which the application is run

Code execution

We’ve already parsed getting a Script resource in the Getting Resource content section. In instantiating the app, there is a method execScript implemented as follows, where the code parameter is the code string that our script gets.

execScript( code: string, url? : string, options? : { async? : boolean; noEntry? : boolean }, ) { try { (this.sandbox as Sandbox).execScript(code, url, options); } catch (e) { this.context.emit(ERROR_COMPILE_APP, this, e); throw e; }}Copy the code

As you can see, this part of the implementation calls execScript in the sandbox. Here’s the pre-knowledge: Basically all sandbox code execution uses the with syntax to handle the execution context of the code, and it has natural advantages. This approach is also used in VUE to handle the template access variable this keyword.

Let’s look at the implementation.

execScript(code: string, url = '', options? ExecScriptOptions: ExecScriptOptions) {// Omit some minor code, preserve the core logic // the context is the agent we created above window const context = this.context; const refs = { url, code, context }; // Create a script tag if the URL exists, Const revertCurrentScript = setDocCurrentScript(this, code, url, async); try { const sourceUrl = url ? `//# sourceURL=${url}\n` : ''; let code = `${refs.code}\n${sourceUrl}`; If (this.options.opensandbox) {// If the mode is not strict, the with package is required to ensure that the internal code is executed in the context of the window code after the proxy =! this.options.useStrict ? `with(window) {; ${this.attachedCode + code}}` : code; // this function constructs the code execution environment evalWithEnv(code, {window: refs.context,... this.overrideContext.overrides, unstable_sandbox: this, }); } } revertCurrentScript(); if (noEntry) { refs.context.module = this.overrideContext.overrides.module; refs.context.exports = context.module.exports; }}Copy the code

The implementation logic of evalWithEnv is very simple. The implementation logic of evalWithEnv is very simple. The implementation logic of evalWithEnv is simple, which is to execute our code content in a constructed context.

export function internFunc(internalizeString) { const temporaryOb = {}; temporaryOb[internalizeString] = true; return Object.keys(temporaryOb)[0]; } export function evalWithEnv(code: string, params: Record<string, any>) { const keys = Object.keys(params); Const randomValKey = '__garfish__exec_temporary__'; const vals = keys.map((k) => `window.${randomValKey}.${k}`); try { rawWindow[randomValKey] = params; // We can see that we first bind the proxied window as the context, and then specify the object we proxied and overridden, Const evalInfo = ['; const evalInfo = ['; (function(${keys.join(',')}){`, `\n}).call(${vals[0]},${vals.join(',')});`, ]; const internalizeString = internFunc(evalInfo[0] + code + evalInfo[1]); // The expression (0, eval) causes eval to be executed in the global scope (0, eval)(internalizeString); } finally { delete rawWindow[randomValKey]; }}Copy the code

At this point we know that the execution environment of our code is constructed by the window and override methods of our proxy, which, combined with the features of the with statement above, can solve the problem of variable promotion. At this point we have completed the path analysis of the code from load to execution.

conclusion

Most of the above analysis in order to explain the basic ideas, the basic realization of the micro front end, in the actual execution process will have a lot of other logical judgment and load optimization, if interested can refer to the source code. At present, Garfish is also in the process of continuous improvement, because many scenarios need user verification, the development can take into account the business case is limited after all, when WRITING this article, there are nearly 100 commit submission updates every day. You can see there are a lot of optimized scenarios. In general, the micro front end does solve the problems of difficult project migration, slow technology upgrade and difficult project maintenance to a large extent. If you have any of these pain points, try it.

Garfish open Source link: github.com/modern-js-d…