Exploration of plug-in architecture for large-scale web applications

foreword

Due to the length of the article, this article will be divided into a series of articles, with the help of the plug-in architecture of Web applications, to introduce

With the gradual maturity of Web technology, more and more application architectures tend to be complex, such as Alibaba Cloud and other giant console projects, each product has its own team responsible for maintenance and iteration. Whether it is maintenance, release, and control costs, it is gradually uncontrollable with the growth of business volume. In this context, micro-frontend applications are born. Micro-frontends have many mature practices within Alibaba, which will not be repeated here. This article takes micro-frontends as an introduction to discuss similar problems faced by some alternative web applications.

The ups and downs of modern text editors

After Microsoft GitHub in 2018, Atom was often used to ridicule, the so-called one mountain cannot tolerate two tigers. At the moment when VS Code has become the editor of choice for many front-end engineers, Atom's position is very embarrassing. In terms of performance, it is secondly killed by VS Code, which is also Electron. In terms of plug-ins, the total number of plug-ins in VS Code has exceeded 1w last year. Released for over a year, Atom is still stuck at 8k+. Coupled with the popularization of heavyweight protocols such as LSP/DAP officially led by Microsoft, Atom's status as a benchmark application of Web/Electron technology has long been sacked by VS Code.

The online discussion of Atom's waning has always been inseparable from performance. Atom is really slow, and the reason is largely dragged down by its plugin architecture. In particular, Atom opens up too many permissions at the UI level for plugin developers to customize, the quality of plugins is uneven, and the security risks brought by the fully open UI to plugins have become Atom's Achilles' heel. Even the important components such as FileTree, Tab bar, Setting Views and so on of its main interface are realized through plug-ins. In contrast, VS Code is much more closed. The VS Code plug-in runs entirely on the Node.js side, and there are only a few APIs that are encapsulated as pure method calls for UI customization.

On the other hand, VS Code, a relatively closed plug-in UI solution, cannot satisfy some functions that require more customization. More plug-in developers have begun to modify the bottom layer of VS Code and even the source code to achieve customization. For example, VS Code Background , which is very popular in the community , this 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 excludes FFmpeg, which makes it impossible to play audio and video in the Webview view. Using this plug-in needs to replace the dynamic link library of FFmpeg. These plugins will inevitably cause damage to the VS Code installation package to a certain extent, causing users to uninstall and reinstall.   

More than an editor - flying horse

Figma  is an online collaborative UI design tool. Compared with Sketch, it has the advantages of cross-platform and real-time collaboration. It has gradually been favored by UI designers in recent years. Recently, Figma also officially launched its plug-in system .

As a web application, Figma's plug-in system is naturally built based on JavaScript, which lowers the development threshold to a certain extent. Since Figma officially announced the open plug-in system test in June last year, more and more Designers/Developers have developed 300+ plug-ins, including graphic resources, file archiving, and even importing 3D models.

How does Figma's plugin system work?

This is a Figma plugin directory structure built with Webpack based on TypeScript + React technology stack 

.
├── README.md
├── figma.d.ts
├── manifest.json
├── package-lock.json
├── package.json
├── src
│   ├── code.ts
│   ├── logo.svg
│   ├── ui.css
│   ├── ui.html
│   └── ui.tsx
├── tsconfig.json
└── webpack.config.js

In its file contains some simple information. manifest.json 

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

It can be seen that Figma divides the plug-in entry into two parts, the main contains the actual runtime logic of the plug-in, and the ui is an HTML fragment of the plug-in. That is, UI is separated from logic. After installing a Color Search plug-in and observing the page structure, you can find that the js file is wrapped in an iframe and loaded on the page. The sandbox mechanism of the main entry will be explained in detail later. The HTML in the middle is also wrapped in an iframe and rendered, which will effectively avoid the global style pollution caused by the CSS code of the plugin UI layer. main  ui  main  ui 

 There is a chapter How Plugins Run in the Figma Developers document that briefly introduces the operating mechanism of its plug-in system. In short, Figma creates a minimal JavaScript execution environment for the main entry of the logic layer in the plug-in, which runs on the main thread of the browser. , In this execution environment, the plug-in code cannot access some browser global APIs, so it cannot affect the operation of Figma itself at the code level. The UI layer has one and only one HTML code snippet, which is rendered into a popup window after the plugin is activated.  

The sandbox mechanism of its plugin is explained in detail in Figma's official blog . At first, the solution they tried was a sandbox environment that comes with the browser. Wrap the plug-in code in an iframe. Due to the natural limitation of iframe, this will ensure that the plug-in code cannot operate the Figma main interface context, and at the same time, only a whitelist API can be opened for the plug-in to call. At first glance it seems to solve the problem, but since the plugin script in the iframe can only communicate with the main thread via postMessage, any API call in the plugin has to be wrapped as an asynchronous method, which is certainly not suitable for Figma's target users. Designers for professional front-end developers are not friendly enough. Second, for larger documents, the performance cost of serializing the postMessage communication is too high, and it can even lead to memory leaks. iframe async/await 

The Figma team chose to go back to the main thread of the browser, but directly run the third-party code in the main thread, and the security issues caused by this are unavoidable. Eventually they found a draft Realm API in stage 2 . Aims to create a domain object for isolating third-party JavaScript scoped APIs. Realm 

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

It's worth noting that Realm can also be implemented using features that JavaScript currently has, namely AND . This is also the most popular sandbox solution in the community. with  Proxy

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
}

The main entry of the Figma plugin wrapped by the iframe in the previous article contains a scope that is taken over by Realm. You can think of it as a piece of code similar to this example. After all, maintaining a whitelist is more efficient than blocking a blacklist. concise. But in fact, due to JavaScript's prototypal inheritance, plugins can still access external objects through the method's prototype chain. The ideal solution is to wrap these whitelist APIs once in the Realm context to completely isolate the prototype chain. 白名单 API console.log 

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(); 

realm.evaluate(`log("Hello outside world!")`, { log: safeLog });

Obviously doing this for every whitelisted API is tedious and error-prone. So how do you build a sandbox environment that is safe and easy to add APIs?

Duktape  is a JavaScript interpreter implemented in C++ for embedded devices. It does not support any browser API. Naturally, it can be compiled into WebAssembly. The Figma team embeds Duktape into the Realm context, and the plugin is finally interpreted and executed by Duktape . In this way, the API required by the plugin can be safely implemented, and there is no need to worry about the plugin accessing the outside of the sandbox through the prototype chain.

This is a so-called defensive programming pattern for implementing a layer of intermediary with subcomponents (broadly) in a program. Simply put, it is a proxy, which creates a controllable access boundary for an object, so that it can reserve some features for third-party embedded scripts, and shield some features that are not expected to be accessed. For a detailed discussion, see the articles Isolating application sub-components with membranes and Membranes in JavaScript . Membrane Pattern  Membrane     

This is the final Figma plug-in solution, it runs on the main thread and doesn't need to worry about the transmission loss caused by postMessage communication. There is one more consumption of Duktape interpretation execution, but thanks to the excellent performance of WebAssembly, this part of the consumption is not very large.

In addition, Figma also retains the original iframe, allowing plugins to create iframes and insert arbitrary JavaScript in them, and it can communicate with the JavaScript scripts in the sandbox through postMessage.

How to have both fish and bear's paw?

We summarize the need for this type of plugin as running third-party code and its custom controls in a web application , which has some problems very similar to the micro-frontend architecture mentioned at the beginning .

  1. A certain degree of JavaScript code sandbox isolation mechanism, the application body has a certain ability to control third-party code (or sub-applications).
  2. Strong style isolation, third-party code styles do not cause CSS pollution to the application body

JavaScript sandbox

JavaScript sandbox isolation is an enduring topic in the community. The simplest iframe tag Sandbox attribute can already isolate JavaScript runtime. The more popular in the community is to use some language features (with, realm, Proxy and other APIs) to shield (or proxy) Window, Document and other global objects, establish a whitelist mechanism, and rewrite APIs that may potentially dangerous operations (such as Alibaba Cloud Console OS - Browser VM ). There is also Figma, which tries to embed a platform-independent JavaScript interpreter, where all third-party code is executed through the embedded interpreter. And use Web Worker to do DOM Diff calculation, and send the calculation results back to the UI thread for rendering. This scheme has been practiced as early as 2013. In this paper, the author uses JSDOM, the widely popular Node.js platform The test library runs on the Web Worker. In recent years , projects such as preact-worker-demo , react-worker-dom and other projects based on Web Worker's DOM Renderer try to proxy the DOM API to the Worker thread. The worker-dom announced by the Google AMP Project in JSCONF 2018 US implements the DOM API on the Web Worker side. Although there are still some problems in practice (for example, the synchronization method cannot be simulated), the WorkerDOM is in terms of performance and isolation. have achieved certain results.      

The above solutions are widely used in various plug-in architecture Web applications, 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, as shown above, Figma uses iframe to render the plug-in interface, sacrificing some performance in exchange for relatively perfect style isolation. Under the modern front-end engineering system, CSS Module can be implemented by adding hash or namespace to the class during translation. This kind of solution is more dependent on the plug-in code compilation process. The newer trend is to use the Shadow DOM of Web Component to wrap the plug-in elements with Web Component. The external styles of Shadow Root cannot act on the inside, and the styles inside the Shadow Root cannot affect the outside.  

finally

This article lists some of the problems faced by the plug-in architecture of large-scale Web applications such as editors and design tools, as well as solutions practiced by the community. Whether it is the iframe that people love and hate, or Realm, Web Worker, Shadow DOM, etc., each solution currently has its own advantages and disadvantages. However, with the increasing complexity of Web applications, the need for plug-in has gradually been paid attention to by major standardization organizations. The next article will focus on the exploration and practice of plugin architecture in KAITIAN IDE, including JavaScript sandbox, CSS isolation, Web Worker, etc.

Original link

This article is original content of Alibaba Cloud and may not be reproduced without permission.

{{o.name}}
{{m.name}}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=324134994&siteId=291194637