preface

With the gradual maturity of Web technology, more and more application architectures tend to be complex. For example, ali Cloud, Tencent Cloud and other giant console projects, each product has its own team responsible for maintenance and iteration. The costs of maintenance, distribution and control are increasingly out of control as the size of the business grows. In this context, micro front end applications were born. There have been many mature practices of micro front end in Ali, which will not be described here. This article starts with a micro front end to explore similar issues facing alternative Web applications.

Modern text editors rise and fall

After The launch of Microsoft’s GitHub in 2018, Atom has often been ridiculed. At a time when VS Code has become the editor of choice for many front-end engineers, Atom is in an awkward position. In terms of performance, VS Code has been slighted by the same Electron VS Code, and in terms of plug-ins, VS Code’s total number of plug-ins passed the 1W mark last year. Atom, released more than a year earlier, is still stuck at 8K +. Coupled with the popularity of Microsoft’s official LSP/DAP protocols, Atom’s status as the benchmark Web/Electron technology application has been eclipsed by VS Code.

The fading chatter about Atom has always been about performance. Atom is slow, in large part because of its plug-in architecture. Atom, in particular, opens too many permissions on the UI level for plug-in developers to customize, and the poor quality of the plug-in and the security risks brought by the fully open UI have become Atom’s Achilles heel. Even its main interface FileTree, Tab bar, Setting Views and other important components are achieved through plug-ins. VS Code, by contrast, is much more closed. VS Code plug-ins run entirely on the Node.js side, and the customization of the UI is rarely encapsulated as a pure method call API.

On the other hand, VS Code’s relatively closed plug-in UI scheme can’t meet some functions that require stronger customization, and more plug-in developers start to modify VS Code’s underlying or even source Code to achieve customization. For example, the popular VS Code Background plugin implements the Background image of the editor area by forcibly modifying the CSS in the VS Code installation file. The other VSC Netease Music is more radical, because the Electron in the VS Code bundle removes FFmpeg so that audio and video cannot be played in the Webview. To use this plug-in, you need to replace FFmpeg’s dynamic link library. These plug-ins will damage the VS Code installation package to some extent, causing users to uninstall and reinstall.

More than an editor. – Fly a horse

Figma is an online collaborative UI design tool. Compared with Sketch, it has advantages such as cross-platform and real-time collaboration, and has been gradually favored by UI designers in recent years. Figma recently launched its plugin system.

As a Web application, Figma’s plugin system is naturally built in JavaScript, which lowers the barrier to development somewhat. Since Figma officially announced the open plug-in system beta last June, more and more Designner/Developer have developed 300+ plug-ins, including graphic resources, archives, and even imported 3D models.

How does Figma’s plugin system work?

This is a Figma plugin directory structure based on the TypeScript + React stack using Webpack

. ├ ─ ─ the README. Md ├ ─ ─ figma. Which s ├ ─ ─ the manifest. Json ├ ─ ─ package - lock. Json ├ ─ ─ package. The json ├ ─ ─ the SRC │ ├ ─ ─ code. The ts │ ├ ─ ─ └─ ├─ ui.css │ ├─ ui.tsx │ ├─ ui.txt │ ├─ ui.txt │ ├─ ui.txt │ ├─ ui.txt │ ├─ ui.txtCopy the code

It contains some simple information in its manifest.json file.

{
  "name": "React Sample"."id": "738168449509241862"."api": "1.0.0"."main": "dist/code.js"."ui": "dist/ui.html"
}
Copy the code

As can be seen from Figma, the plug-in entry is divided into main and UI. Main contains the logic of the actual runtime of the plug-in, while UI is the HTML fragment of a plug-in. UI is separated from logic. Install a Color Search plugin and observe the page structure. You can find that js files in main are wrapped in an iframe and loaded onto the page. The sandbox mechanism of main entry is described in detail later in the article. The HTML in the UI is eventually rendered in an iframe, which effectively avoids global style contamination caused by plugin UI layer CSS code.

The chapter “How Plugins Run” in Figma Developers document gives a brief introduction to the running mechanism of its plug-in system. In a nutshell, Figma creates a minimal JavaScript execution environment for the main entrance of the logical layer in the plug-in. It runs on the main thread of the browser, where the plug-in code does not have access to some of the browser’s global apis and thus cannot affect Figma itself at the code level. The UI layer, on the other hand, has one and only HTML snippet that is rendered into a popover after the plug-in is activated.

The official Figma blog explains the sandbox mechanism of the plugin in detail. The first solution they tried was iframe, a sandbox environment built into the browser. Wrapping the plugin code in iframe, due to the natural limitations of iframe, will ensure that the plugin code cannot manipulate the Figma main interface context, while also leaving only one whitelist API open for the plugin to call. At first glance it seems to solve the problem, but since the plug-in script in iframe can only communicate with the main thread via postMessage, this results in any API calls in the plug-in having to be wrapped as an asynchronous async/await method, This is definitely not friendly to Figma’s target audience of designers who are not professional front-end developers. Second, for larger documents, the performance cost of postMessage communication serialization is prohibitively high and can even lead to memory leaks.

The Figma team chose to go back to the browser main thread, but ran third-party code directly on the main thread, which inevitably raised security issues. Eventually they found a draft Realm API that was still in the stage2 stage. Realm is designed to create a domain object that is used to isolate third-party JavaScript scoped apis.

let g = window; // outer global
let r = new Realm(); // root realm

let f = r.evaluate("(function() { return 17 })");

f() === 17 // true

Reflect.getPrototypeOf(f) === g.Function.prototype // false
Reflect.getPrototypeOf(f) === r.globalThis.Function.prototype // true
Copy the code

It’s worth noting that Realm can also be implemented using JavaScript’s existing features, with and Proxy. This is also a popular sandbox solution in the community.

const whitelist = {
  windiw: undefined.document: undefined.console: window.console,
};

const scopeProxy = new Proxy(whitelist, {
  get(target, prop) {
    if (prop in target) {
      return target[prop]
    }
    return undefined}});with (scopeProxy) {
  eval("console.log(document.write)") // Cannot read property 'write' of undefined!
  eval("console.log('hello')")        // hello
}
Copy the code

The Figma plugin’s main entry, which is wrapped in iframe, contains a scope that is taken over by Realm. You can think of it as a whitelist API like the one in this example, because it is simpler to maintain a whitelist than to block blacklists. But in fact, because of JavaScript prototypal inheritance, plug-ins can still access external objects through the prototype chain of console.log methods. The ideal solution is to wrap these whitelist apis once in a Realm context, completely isolating the prototype chain.

const safeLogFactory = realm.evaluate(` (function safeLogFactory(unsafeLog) { return function safeLog(... args) { unsafeLog(... args); `}}));

const safeLog = safeLogFactory(console.log);

const outerIntrinsics = safeLog instanceOf Function;
const innerIntrinsics = realm.evaluate(`log instanceOf Function`, { log: safeLog });
if(outerIntrinsics || ! innerIntrinsics)throw new TypeError(a); realm.evaluate(`log("Hello outside world!" ) `, { log: safeLog });
Copy the code

Obviously doing this for each of the whitelisted apis is cumbersome and error-prone. So how do you build a sandbox environment that is safe and easy to add apis to?

Duktape is a JavaScript interpreter implemented by C++ for embedded devices. It does not support any browser apis, and naturally it can be compiled into WebAssembly. The Figma team embedded Duktape into a Realm context, The plugin was eventually implemented via Duktape interpretation. This allows you to safely implement the API required by the plug-in without worrying about the plug-in accessing the outside of the sandbox through the prototype chain.

This is a defensive programming Pattern called the Membrane Pattern, which is used to implement a layer of mediation between the program and its subcomponents (in a broad sense). In simple terms, Proxy creates a controllable access boundary for an object, so that it can reserve some features for third-party embedded scripts and block some features that are not expected to be accessed. Can be seen in the two articles On classical day application sub-components with MEMBRANES and membranes in JavaScript for detailed discussion of Membrane.

This is the final Figma plug-in solution, which runs on the main thread without worrying about transmission losses associated with postMessage communication. There was an extra Duktape to explain the cost of execution, but thanks to WebAssembly’s excellent performance, this cost was not significant.

Figma also preserves the original iframe, allowing the plugin to create its own IFrame and insert any JavaScript into it, while communicating with the JavaScript script in the sandbox via postMessage.

How to have your cake and eat it?

We summarize the requirements of this type of plug-in as running third-party code and its custom controls in a Web application, which has some very similar problems to the microfront-end architecture mentioned at the beginning.

  1. Some degree of JavaScript code sandbox isolation, application principals have some control over third-party code (or child applications)
  2. Strong style isolation, third-party code styles do not pollute the application body with CSS

The JavaScript sandbox

JavaScript Sandbox isolation is a perennial topic in the community, and the simplest iframe tag Sandbox attribute already provides JavaScript runtime isolation. Some of the most popular language features in the community (with, Realm, Proxy, etc.) are masking (or Proxy) global objects such as Window and Document, and creating a whitelist mechanism. API rewrite for potentially dangerous operations (e.g. Ali Cloud Console OS-Browser VM). There is also Figma, which attempts to embed platform-independent JavaScript interpreters through which all third party code is executed. As well as DOM Diff calculation using Web Worker and sending the calculation results back to UI thread for rendering, this scheme has been carried out as early as 2013. In this paper, the author runs JSDOM, a widely popular test library of Node.js platform, on Web workers. In recent years, some projects, such as preact-worker-demo and react-worker-dom, have attempted to delegate DOM API to worker threads based on Web worker DOM renderers. The worker-DOM published by Google AMP Project in JSCONF 2018 US implements DOM API on Web worker side. Although there are still some problems in practice (such as synchronization method cannot be simulated), But WorkerDOM has achieved some results in terms of performance and isolation.

These solutions are widely used in Web applications with various plug-in architectures, but most of them are Case By Case, and each solution has its own costs and trade-offs.

CSS scope

In the CSS style isolation scheme, Figma uses the iframe rendering plug-in interface above, sacrificing some performance for relatively perfect style isolation. In modern front-end engineering systems, CSS modules can be translated to add hash or namespace to the class, which is more dependent on the plug-in code compilation process. A more modern approach is to use the Shadow DOM of The Web Component to wrap the plug-in elements around the Web Component. The external styles of Shadow Root cannot be applied internally, and the styles of Shadow Root cannot be applied externally.

The last

This paper lists some problems facing the plug-in architecture of large Web applications such as editors and design tools, as well as the solutions of community practice. Whether it’s iframe, or Realm, Web Worker, Shadow DOM, etc., each solution has its own advantages and disadvantages. However, with the increasing complexity of Web applications, the need of plug-in is gradually paid attention to by major standardization organizations. The next article will focus on exploring and practicing plug-in architectures in the KAITIAN IDE, including JavaScript sandbox, CSS isolation, Web Worker, and more.