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

Preface

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 the Web application, introduce

With the gradual maturity of web technology, more and more application architectures tend to be complex, such as giant console projects such as Alibaba Cloud. Each product has its own team responsible for maintenance and iteration. The cost of maintenance, release, and control becomes gradually uncontrollable as the business volume grows. In this context, micro-front-end applications are born. Micro-front-ends already have many mature practices within Ali, so I won’t repeat them here. This article uses the micro front end as an introduction to discuss the similar problems faced by some alternative Web applications.

Ups and downs of modern text editors

After Microsoft GitHub in 2018, Atom has often been used to tease, the so-called so-called one mountain can not tolerate two tigers. At the moment when VS Code has become the first choice of editors for front-end engineers, the position of Atom is very embarrassing. In terms of performance, it is spiked by VS Code, which is also Electron. In terms of plug-ins, the total number of plug-ins for VS Code has exceeded the 1w mark last year. Atom, which has been released for more than a year, is still at 8k+. Coupled with the popularity of heavyweight protocols such as LSP/DAP officially led by Microsoft, the status of Atom as a benchmark application of Web/Electron technology has been smashed by VS Code.

The declining discussion of Atom on the Internet has always been inseparable from performance. Atom is indeed too slow, the reason is largely dragged down by its plug-in architecture. In particular, Atom has opened too many permissions at the UI level to customize plugin developers, the quality of plugins is uneven, and the security risks caused by the completely open UI have become the Achilles heel of Atom. Even the FileTree, Tab bar, Setting Views and other important components of the main interface are implemented through plug-ins. In contrast, VS Code is much closed. The VS Code plug-in runs completely on the Node.js side, and only a few of the UI customizations are encapsulated as pure method call APIs.

But on the other hand, VS Code, a relatively closed plug-in UI solution, cannot satisfy some functions that require stronger customization. More plug-in developers have begun to modify the underlying and even source code of VS Code to achieve customization. For example, the popular VS Code Background in the community  , this plug-in 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 Electron in the VS Code bundle excludes FFmpeg, which makes it impossible to play audio and video in the Webview view. To use this plug-in, you need to replace the FFmpeg dynamic link library. These plug-ins will inevitably cause a certain degree of damage to the VS Code installation package, causing users to uninstall and reinstall.

More than 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. In recent years, it has gradually been favored by UI designers. 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 reduces the development threshold to a certain extent. Since Figma officially announced the open plug-in system test in June last year, more and more Designer/Developer have developed 300+ plug-ins, including graphic resources, file archives, and even importing 3D models.

How does Figma's plug-in 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

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

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

Figma can be seen that the insert into the inlet of  main the  ui two parts, main logic card contains the actual operation, and ui is a plug-in HTML snippet. That is, UI and logic are separated. After installing a Color Search plug-in and observing the page structure, you can find  main that the js file is wrapped in an iframe and loaded on the page. The sandbox mechanism of the main entrance will be explained in detail later. And  ui in the final HTML can also be rendered in an iframe wrapped inside, which would effectively prevent the plug-in UI layer CSS code causes global style pollution.

 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 entrance 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 pop-up window after the plug-in is activated.

Figma's official blog explains the sandbox mechanism of its plug-in in detail. The solution they tried at first was  iframea sandbox environment that comes with the browser. Wrap the plug-in code in an iframe. Due to the natural limitation of the iframe, this will ensure that the plug-in code cannot operate the Figma main interface context. 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 the iframe plugin scripts only through postMessage communicate with the main thread, which leads to any plug-in API calls must be wrapped in an asynchronous  async/await way, which is no doubt the target user Figma non The designers of professional front-end developers are not friendly enough. Secondly, for larger documents, the performance cost of postMessage communication serialization is too high, and may even cause memory leaks.

The Figma team chose to return to the main thread of the browser, but directly run the third-party code on the main thread, and the resulting security issues are inevitable. Eventually they found a draft Realm API that was still in stage2  . Realm Aim to create a domain object to isolate the API of the third-party JavaScript scope.

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 is worth noting that Realm can also be implemented using JavaScript's existing features, namely,  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
}

In the previous article, the main entrance of the Figma plug-in that is wrapped by the iframe contains a scope that is taken over by Realm. You can think of it as a sample code similar to this one  白名单 API. After all, maintaining a whitelist is more implemented than blocking a blacklist. concise. But in fact, due to JavaScript's prototype inheritance, plug-ins can still console.log access external objects through  the prototype chain of methods. The ideal solution is to wrap these whitelist APIs in the Realm context once to completely isolate 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(); 

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

Obviously, doing this for each API in the whitelist is very complicated and error-prone. So how to build a safe and easy to add API sandbox environment?

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

This is a so-called  Membrane Pattern defensive programming model that is used to achieve a layer of mediation with subcomponents (in a broad sense) in the 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, while shielding some features that you don't want to be accessed. For  Membrane detailed discussion, please refer to the   two articles Isolating application sub-components with membranes  and  Membranes in JavaScript .

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

In addition, Figma also retains the original iframe, allowing plug-ins to create an iframe and insert any JavaScript into it. At the same time, it can communicate with JavaScript scripts in the sandbox via postMessage.

How to have both fish and bear's paw?

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

  1. A certain degree of JavaScript code sandbox isolation mechanism, the main body of the application 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 be used to isolate the JavaScript runtime. The most popular in the community is to use some language features (with, realm, Proxy, etc. API) to block (Or proxy) Global objects such as Window and Document, establish a whitelist mechanism, and rewrite APIs that may be potentially dangerous operations (such as Alibaba Cloud Console OS-Browser VM ). In addition, Figma tries to embed a platform-independent JavaScript interpreter. All third-party code is executed through the embedded interpreter. And the use of Web Worker do DOM Diff calculated and the results sent back to the UI thread to render this scheme as early as 2013 has already been carried out on the practice , this paper the authors JSDOM the Node.js platform widespread The test library runs on Web Worker. In recent years,  preact-worker-demo  , react-worker-dom  and other projects based on Web Worker DOM Renderer have tried 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. All 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. Each solution has its own costs and trade-offs.

CSS scope

In the CSS style isolation scheme, 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 used to add hash or namespace to the class during translation. This type of solution relies on the plug-in code compilation process. The more trendy is the use of  Web Component  of Shadow DOM, the plug-in element wrapped with Web Component, Shadow Root unable to act on the internal external style, the same style inside the Shadow Root can not affect the outside.

At last

This article enumerates some of the problems facing the plug-in architecture of large-scale Web applications such as editors and design tools, as well as the solutions practiced by the community. Whether it is the iframe that makes people love and hate, or Realm, Web Worker, Shadow DOM, etc., at present, each solution has its own advantages and disadvantages. However, as the complexity of Web applications grows, the need for plug-inization is gradually being valued by major standardization organizations. The next article will focus on the exploration and practice of plug-in architecture in KAITIAN IDE, including JavaScript sandbox, CSS isolation, Web Worker, etc.

Original link

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

Guess you like

Origin blog.csdn.net/weixin_43970890/article/details/114927719