Some background introductions of "fried cold rice"
This article will not introduce the basic knowledge of Web Worker and the use of basic API from the beginning (only part of it is involved). If you haven't learned about Web Worker, you can refer to the relevant introduction in the Workers document
Since the official HTML5 recommendation standard was released in 2014, HTML5 has added more and more powerful features and functions. Among them, the introduction of the Web Worker concept is eye-catching, but it has not been aroused much. And was overwhelmed by the "revolution" wave of frameworks such as Angular, Vue, and React on the subsequent engineering side. Of course, we always read some articles by chance, or did some exercises in application scenarios for the purpose of learning, or even actually used them in actual projects involving a large amount of data calculation scenarios. But I believe that there are many people who are as at a loss as I am, unable to find any applications of this kind of tall technology that can play a wide role in actual project scenarios.
The reason is that Web Worker's independent operation of the main thread of the UI has made it a lot of consideration for performance optimization attempts (such as some image analysis, 3D calculation and drawing, etc.) to ensure that while performing a large number of calculations, the page is Can have a timely response. These performance optimization requirements involve low frequency on the front-end side on the one hand, and can also be solved by microtasks or server-side processing on the other hand. It cannot optimize the polling scenario under the front-end page with a technology like Web Socket. Can bring about a qualitative change.
It was not until the emergence of the explosive micro-front-end architecture in 2019, based on the need for JavaScript sandbox isolation between micro-applications, that Web Worker was able to leap from a marginalized position to my central vision again. According to the relevant knowledge of Web Worker that I have learned, I know that Web Worker works in a separate sub-thread (although this sub-thread is a bit weaker than the sub-thread of compiled languages such as Java, such as unable to lock Etc.), the threads have their own isolation characteristics. Based on this "physical" isolation, can the isolation of JavaScript runtime be achieved?
The rest of this article will introduce some data collection, understanding, and my process of stepping on the pit and thinking in the process of exploring the implementation of JavaScript sandbox isolation scheme based on Web Worker. Although the content of the entire article may be "fried cold rice", I still hope that my process of exploring the plan can be helpful to you who are reading this article.
JavaScript sandbox
Before exploring solutions based on Web Workers, we must first understand the current problem to be solved-JavaScript sandbox.
When it comes to sandboxes, I would first think of sandbox games that I have played out of interest, but the JavaScript sandbox we want to explore is different from sandbox games. Sandbox games focus on the abstraction, combination, and physical force system of the basic elements of the world. Implementation and so on, while the JavaScript sandbox pays more attention to the isolation of the operating state when using shared data.
In real JavaScript-related scenarios, we know that the browser we usually use is a sandbox, and the JavaScript code running in the browser cannot directly access the file system, display, or any other hardware. Each tab page in the Chrome browser is also a sandbox, the data in each tab page cannot directly affect each other, and the interfaces are all running in an independent context. And running HTML pages under the same browser tab, what are the more detailed scenarios that require sandboxing?
After we have been a front-end developer for a long time, we can easily think of many application scenarios that use sandbox requirements under the same page, such as:
- When executing third-party JavaScript code obtained from untrusted sources (such as importing plug-ins, processing data returned by jsonp requests, etc.).
- Online code editor scene (such as the famous codesandbox ).
- Use server-side rendering solutions.
- Calculation of the expression in the template string.
- ... ...
Here we first go back to the beginning, first assume the premise is under the micro front-end architecture design I am facing. In the micro front-end architecture (recommended articles Thinking in Microfrontend , embrace the front-end development architecture of the cloud era-micro front-end etc.), one of its most critical design is the implementation of the scheduling between each sub-application and its running state Maintenance, and common requirements such as the use of global event monitoring by each sub-application at runtime and the effect of global CSS styles will become a polluting side effect when multiple sub-applications are switched. In order to solve these side effects, many micro front-end architectures appeared later (Such as Qiankun ) has various realizations. For example, common namespace prefixes in CSS isolation, Shadow DOM, universe sandbox css runtime dynamic additions and deletions, etc., all have specific and effective practices, and the most troublesome thing here is the JavaScript sand between micro-applications. Box isolation.
In the micro front-end architecture, JavaScript sandbox isolation needs to solve the following problems:
- The global methods/variables hanging on the window (such as setTimeout, scrolling and other global event monitoring, etc.) are cleaned and restored when the sub-application is switched.
- The read and write security policy restrictions of Cookie, LocalStorage, etc.
- Implementation of independent routing for each sub-application.
- When multiple micro-applications coexist, they are implemented independently.
In the Qiankun architecture design, there are two entry files to pay attention to about the sandbox, one is proxySandbox.ts , the other is snapshotSandbox.ts , they are based on the proxy implementation of the constants and methods commonly used on windows. And when Proxy is not supported, downgrade to achieve backup and restore through snapshots. Combined with the sharing of related open source articles, briefly summarize its implementation ideas: the original version uses the snapshot sandbox , which simulates ES6's Proxy API, hijacks the window through the proxy, and when the sub-application modifies or uses the properties or methods on the window , Record the corresponding operation, generate a snapshot every time a sub-application is mounted/uninstalled, and restore from the recorded snapshot when switching to the current sub-application from the outside again. Later, in order to be compatible with the coexistence of multiple sub-applications, Based on Proxy, all global constants and method interfaces of the proxy are implemented, and an independent operating environment is constructed for each sub-application.
Another idea worth learning from is the Browser VM of the Alibaba Cloud development platform. Its core entry logic is in the Context.js file. Its concrete realization idea is as follows:
- Learn
with
achieve the effect, in webpack compiler package for each sub-stage application code wrapped in a layer of code (see its add-on pack breezr-plugin-os under relevant documents), to create a closure, passing its own window simulation, document , location, history and other global objects (see the root directory related documents). - In the simulated Context , the new iframe object provides an empty (about:blank) URL of the same domain as the host application as the initial loading URL of the iframe (an empty URL will not cause resource loading, but will generate and The associated history in this iframe cannot be manipulated. At this time, the routing transformation only supports hash mode), and then the native browser object
contentWindow
taken out through 06076b62154a41 (because the iframe object is naturally isolated, the Mock implementation is omitted here. The cost of all APIs). - After taking out the native object in the corresponding iframe, continue to generate the corresponding Proxy for the specific object that needs to be isolated, and then do some specific implementations for some attribute acquisition and attribute setting (for example, window.document needs to return a specific sandbox document instead of The document of the current browser, etc.).
- In order for the content of the document to be loaded on the same DOM tree, for the document, most of the properties and methods of the DOM operation still directly use the properties and methods of the document in the host browser.
In general, in Browser VM , it can be seen that its implementation part still draws on Qiankun or other micro-front-end architecture ideas, such as proxy and interception of common global objects. And with the help of the Proxy feature, some security policies can also be implemented for the reading and writing of Cookie and LocalStorage. But its biggest highlight is to make some clever implementations with the help of iframes. When the iframe created for each sub-application is removed, the variables written on the window under it, setTimeout, global event monitoring, etc. will also be moved. In addition; based on Proxy, DOM events are recorded in the sandbox, and then removed during the life cycle of the host, which can realize the entire JavaScript sandbox isolation mechanism with a small development cost.
In addition to the popular solutions in the above communities, I also recently Figma product in the UI design field large-scale Web application plug-in architecture exploration article, which also produced an isolation solution based on its plug-in system. At first, Figma also put the plug-in code into an iframe for execution and communicated with the main thread through postMessage. However, due to the ease of use and performance problems caused by postMessage serialization, Figma chose to put the plug-in in the main thread for execution. The scheme adopted by Figma is based on the Realm API, which is currently still in the draft stage, and compiles Duktape, a C++ implementation of the JavaScript interpreter, into WebAssembly, and then embeds it in the Realm context to realize the independent operation of the three-party plug-in under its product. This solution and the explored Web Worker-based implementation may be able to combine better, and continue to pay attention.
Web Worker and DOM rendering
After understanding the "past and present" of the JavaScript sandbox, we will turn our attention to the protagonist of this article-Web Worker.
As mentioned at the beginning of this article, the form of Web Worker sub-threads is also a natural sandbox isolation. The ideal way is to learn from Browser VM , and create Workers for each sub-application package through the Webpack plug-in during the compilation phase. The code of the object allows the sub-application to run in its corresponding single Worker instance, such as:
__WRAP_WORKER__(`/* 打包代码 */ }`);
function __WRAP_WORKER__(appCode) {
var blob = new Blob([appCode]);
var appWorker = new Worker(window.URL.createObjectURL(blob));
}
But after understanding the implementation process of the JavaScript sandbox under the micro front end, it is not difficult to find several problems that will inevitably be encountered in the JavaScript sandbox that implements the micro front end scenario in the Web Worker:
- Due to thread safety design considerations, Web Workers do not support DOM operations and must be implemented by notifying the UI main thread through postMessage.
- Web Workers cannot access browser global objects such as window and document.
Other problems, such as the inability of Web Workers to access global variables and functions on the page, and the inability to call BOM APIs such as alert and confirm, are minor problems compared to the inability to access the window and document global objects. But the good news is that timer functions such as setTimeout and setInterval can be used normally in Web Worker, and Ajax requests can still be sent.
Therefore, the first to solve the problem is to perform DOM operations in a single Web Worker instance. First of all, we have a major premise: Web Worker cannot render DOM , so we need to split DOM operations based on actual application scenarios.
React Worker DOM
Because the sub-applications in our micro front-end architecture are limited to the React technology stack, I first set my sights on solutions based on the React framework.
In React, we know the basic fact that the rendering phase is divided into two phases: Diffing the DOM tree change and actually rendering the page DOM. Can we put the Diff process in the Web Worker, and then pass the rendering phase What if postMessage communicates with the main thread and then puts it on the main thread? A simple search is quite embarrassing, and some big guys have tried it 5 or 6 years ago. Here we can refer to the open source code react-worker-dom
The implementation ideas in react-worker-dom In common/channel.js , it uniformly encapsulates the interface between the child thread and the main thread to communicate with each other and the interface for serialized communication data, and then we can see that the total entry file that implements DOM logic processing under Worker is in worker Under the directory , from the entry file, you can see that it implements the entry file WorkerBridge.js and other DOM structures, Diff operations, and life cycle mocks based on the React library implementation after calculating the DOM and notifying the main thread for rendering through postMessage Interface and other related code, and the entry file that accepts the rendering event communication is in the under the 16076b62154cbc page directory. receiving the node operation event, the entry file is combined with WorkerDomNodeImpl.js to realize the actual rendering update of the DOM in the main thread.
Make a brief summary. Based on the React technology stack, a certain degree of DOM sandbox can be achieved by separating the Diff and rendering stages under Web Worker, but this is not the JavaScript sandbox under the micro-front-end architecture that we want. Let alone the cost-benefit ratio of splitting the Diff stage and the rendering stage. First of all, these many efforts based on the particularity of the technology stack framework will have the problem of maintenance and upgrades that are difficult to control with the upgrade of the framework itself; Secondly, if the technology stack framework used by each sub-application is different, it is necessary to encapsulate the adapted interfaces for these different frameworks, and the scalability and universality are weak. Finally, the most important point is that this method has not yet solved the problem. The problem of resource sharing, or rather, just started the first step to solve this problem.
Next, let's continue to discuss another solution for DOM manipulation under Worker. The issue of resource sharing under window will be discussed later.
AMP WorkerDOM
When I started to with the many "natural" problems that were actually developed with ideas such as 16076b62154d19 react-worker-dom , I browsed other DOM frameworks because they also have plug-in mechanisms and accidentally came into my mind. It is Google’s AMP .
AMP open source project , in addition to the amphtml , there are many other projects that use new technologies such as Shadow DOM, Web Component, etc. After a brief glance under the project, I am pleased to see the project worker-dom .
A cursory look at the worker-dom main-thread and worker-thread in the src root directory. After opening the two directories, we can find that it is related to splitting the DOM. The logic and DOM rendering ideas are basically similar to react-worker-dom worker-dom has nothing to do with the upper framework, and its implementation is closer to the bottom of the DOM.
look at the relevant code of the 16076b62154ddb worker-thread DOM logic layer, you can see that the dom directory implements all the relevant node elements, attribute interfaces, document objects and other codes based on the DOM standard in the upper directory. It also implements global properties and methods such as Canvas, CSS, events, and Storage.
Then look at main-thread . On the one hand, its key function is to provide an interface for loading worker files to render pages from the main thread. On the other hand, it can be understood from the code of two files: worker.ts and nodes.ts
In worker.ts , a layer of code is wrapped as I originally imagined to automatically generate Worker objects and proxy all DOM operations in the code to the simulated WorkerDOM objects:
const code = `
'use strict';
(function(){
${workerDOMScript}
self['window'] = self;
var workerDOM = WorkerThread.workerDOM;
WorkerThread.hydrate(
workerDOM.document,
${JSON.stringify(strings)},
${JSON.stringify(skeleton)},
${JSON.stringify(cssKeys)},
${JSON.stringify(globalEventHandlerKeys)},
[${window.innerWidth}, ${window.innerHeight}],
${JSON.stringify(localStorageInit)},
${JSON.stringify(sessionStorageInit)}
);
workerDOM.document[${TransferrableKeys.observe}](this);
Object.keys(workerDOM).forEach(function(k){self[k]=workerDOM[k]});
}).call(self);
${authorScript}
//# sourceURL=${encodeURI(config.authorURL)}`;
this[TransferrableKeys.worker] = new Worker(URL.createObjectURL(new Blob([code])));
In nodes.ts , the construction and storage of real element nodes are realized (based on whether and how the storage data structure is optimized in the rendering stage, the source code needs to be further studied).
At the same time, transfer directory defines the specification of the message communication between the logic layer and the UI rendering layer.
In general, the AMP WorkerDOM solution discards the constraints of the upper framework, and by constructing all relevant APIs of the DOM from the bottom, it truly has nothing to do with the framework technology stack. On the one hand, it can be fully implemented as the bottom layer of the upper framework to support the secondary encapsulation and migration of various upper frameworks (such as project amp-react-prototype ), and on the other hand, it combines the current mainstream JavaScript sandbox solution by simulating the window The way of document global method and proxy to the main thread realizes partial JavaScript sandbox isolation (I haven't seen the relevant code implementation of routing isolation for the time being).
Of course, from my personal point of view, AMP WorkerDOM also has certain limitations in its current implementation. One is the migration cost of the current mainstream upper-level frameworks such as Vue, React, etc. and the adaptation cost of the community ecology. The other is that it has not yet seen relevant implementation solutions under single-page applications, and is supported by large-scale PC micro-front-end applications. Can't find a better solution yet.
In fact, after understanding the implementation of AMP WorkerDOM, react-worker-dom idea can also have a general direction: the follow-up process of rendering communication can be considered in conjunction with the related implementation of Browser VM When the Worker object is generated, an iframe object is also generated, and then all operations under the DOM are sent to the main thread through postMessage, and then executed with the iframe bound to it. At the same time, the specific rendering implementation is forwarded to the original through the proxy. WorkerDomNodeImpl.js logic to implement the actual update of the DOM.
Summary and some personal prospects
Let's talk about some personal summary first. The realization of the JavaScript sandbox under the micro-front-end architecture under Web Worker was originally a flash of personal inspiration. After in-depth investigation, although in the end it was still not possible to find the optimal solution in the implementation of the plan due to problems of this kind and abandon the adoption of community general Scheme, but it still does not prevent me from continuing to be optimistic about Web Worker technology in implementing plug-in sandbox applications. The plug-in mechanism has always been a popular design in the front-end field. From Webpack compilation tools to IDE development tools, from Web application-level entity plug-ins to plug-in extension design in application architecture design, combined with WebAssembly technology, Web Worker will undoubtedly be used in plug-ins. The design occupies a pivotal position.
The second is some forward thinking of some individuals. In fact, it can be seen from the research process of Web Worker's implementation of DOM rendering that based on the idea of separating logic and UI, the subsequent architecture design of the front-end has a great opportunity to produce certain changes. At present, whether it is the prevailing Vue or React framework, its framework design, whether it is MVVM or Flux combined with Redux, is still essentially a framework design driven by the View layer (personal opinion), which is flexible and also produces performance Optimization, difficulty in collaborative development after the upgrade of large-scale projects, and the separation of Web Worker-based logic and UI will promote further business layering of the entire process of data acquisition, processing, and consumption, thereby solidifying a complete set of MVX Design ideas.
Of course, I personally are still in the preliminary investigation stage of the above, and I still need to think about the immature ones. Just listen and practice it later.
Author: ES2049 / Jinzhi Kai
The article can be reprinted at will, but please keep this link to the original text.
You are very welcome to join ES2049 Studio if you are passionate. Please send your resume to caijun.hcj@alibaba-inc.com .
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。